From 6105c80f7b966d979f6b3bf4292df8f12632a29a Mon Sep 17 00:00:00 2001 From: Nitride <77973576+CharlVS@users.noreply.github.com> Date: Thu, 12 Mar 2026 12:34:35 +0100 Subject: [PATCH 01/40] feat(sdk): typed error handling, trading streams, and activation refactoring (#312) * feat: add polish foundation support in sdk * fix: export typed rpc error models * test(auth): cover delete wallet general error mapping * test(auth): cover generalized wallet rpc errors * fix: harden KDF error parsing against malformed responses - Wrap KdfErrorRegistry.tryParse parser invocation in try/catch to gracefully fall back to GeneralErrorResponse on malformed error_data - Make _intFromJson and _doubleFromJson null-safe (return 0 instead of throwing ArgumentError) - Make Mm2Duration.fromJson handle null input (return Duration.zero) - Apply same fixes to the generator script so regenerated code stays consistent - Refine doc comments on delete_wallet and change_mnemonic_password * fix: harden stream controller lifecycle and refactor fee guard - Add isCancelled flag with canEmit/tearDownResources helpers to orderbook and swap-status stream controllers to prevent race conditions where events could be emitted after cancellation - Extract _throwIfFeeEstimationDisabled helper in FeeManager to remove duplicated guard logic across ETH, UTXO and Tendermint paths - Update bundled coins repo commit hash --- .../app_build/build_config.json | 169 +- .../lib/src/auth/auth_service.dart | 398 +- .../trezor/trezor_initialization_state.dart | 2 +- .../kdf_auth_service_delete_wallet_test.dart | 206 + packages/komodo_defi_rpc_methods/README.md | 19 + .../client/client_rpc_library_extension.dart | 2 +- .../general/address_format.dart | 6 +- .../lib/src/models/base_request.dart | 49 +- .../lib/src/models/error_response.dart | 29 + .../lib/src/models/kdf_error_messages.dart | 527 + .../lib/src/models/mm2_rpc_exceptions.dart | 26856 ++++++++++++++++ .../lib/src/models/models.dart | 2 + .../lib/src/models/task_response_details.dart | 60 +- .../rpc_methods/eth/task_enable_eth_init.dart | 18 +- .../hd_wallet/account_balance.dart | 32 +- .../hd_wallet/get_new_address_task.dart | 10 +- .../scan_for_new_addresses_status.dart | 33 +- .../trezor/trezor_rpc_namespace.dart | 8 +- .../wallet/change_mnemonic_password.dart | 63 +- .../src/rpc_methods/wallet/delete_wallet.dart | 199 +- .../balance/hd_wallet_balance_strategy.dart | 12 +- .../pubkey/hd_multi_address_strategy.dart | 73 +- .../tool/generate_mm2_rpc_exceptions.py | 2268 ++ .../komodo_defi_sdk/lib/komodo_defi_sdk.dart | 19 +- .../src/activation/activation_manager.dart | 39 +- .../activation_strategy_base.dart | 33 +- .../bch_activation_strategy.dart | 50 +- .../custom_erc20_activation_strategy.dart | 30 +- .../erc20_activation_strategy.dart | 32 +- .../eth_task_activation_strategy.dart | 33 +- .../eth_with_tokens_activation_strategy.dart | 36 +- .../qtum_activation_strategy.dart | 61 +- .../sia_activation_strategy.dart | 47 +- .../slp_activation_strategy.dart | 24 +- .../tendermint_activation_strategy.dart | 70 +- .../tendermint_task_activation_strategy.dart | 32 +- .../tendermint_token_activation_strategy.dart | 17 +- .../utxo_activation_strategy.dart | 32 +- .../zhtlc_activation_progress.dart | 9 +- .../zhtlc_activation_progress_estimator.dart | 18 +- .../komodo_defi_sdk/lib/src/bootstrap.dart | 10 + .../lib/src/errors/sdk_error_mapper.dart | 1406 + .../lib/src/fees/fee_manager.dart | 113 +- .../lib/src/komodo_defi_sdk.dart | 37 +- .../lib/src/pubkeys/pubkey_manager.dart | 43 +- .../lib/src/trading/trading_manager.dart | 422 + .../legacy_withdrawal_manager.dart | 55 +- .../src/withdrawals/withdrawal_manager.dart | 92 +- .../test/backward_compatibility_test.dart | 12 + .../test/balances/balance_manager_test.dart | 18 + .../test/errors/sdk_error_mapper_test.dart | 64 + .../transaction_history_strategies_test.dart | 65 + .../src/activation/activation_progress.dart | 11 + .../lib/src/coin_classes/coin_subclasses.dart | 6 +- .../lib/src/errors/sdk_error.dart | 136 + packages/komodo_defi_types/lib/src/types.dart | 1 + .../lib/src/withdrawal/withdrawal_types.dart | 171 +- .../lib/src/core/inputs/fee_info_input.dart | 97 +- .../src/core/inputs/searchable_select.dart | 11 + .../steps/fetch_coin_assets_build_step.dart | 3 + .../steps/github/github_file_downloader.dart | 131 +- .../github/github_file_downloader_test.dart | 46 + .../kdf_operations_server_native.dart | 7 +- .../kdf_operations_server_stub.dart | 7 +- .../kdf_operations_server_web.dart | 10 +- 65 files changed, 33466 insertions(+), 1131 deletions(-) create mode 100644 packages/komodo_defi_local_auth/test/src/kdf_auth_service_delete_wallet_test.dart create mode 100644 packages/komodo_defi_rpc_methods/lib/src/models/kdf_error_messages.dart create mode 100644 packages/komodo_defi_rpc_methods/lib/src/models/mm2_rpc_exceptions.dart create mode 100644 packages/komodo_defi_rpc_methods/tool/generate_mm2_rpc_exceptions.py create mode 100644 packages/komodo_defi_sdk/lib/src/errors/sdk_error_mapper.dart create mode 100644 packages/komodo_defi_sdk/lib/src/trading/trading_manager.dart create mode 100644 packages/komodo_defi_sdk/test/errors/sdk_error_mapper_test.dart create mode 100644 packages/komodo_defi_types/lib/src/errors/sdk_error.dart diff --git a/packages/komodo_defi_framework/app_build/build_config.json b/packages/komodo_defi_framework/app_build/build_config.json index a18941c90..476793f1d 100644 --- a/packages/komodo_defi_framework/app_build/build_config.json +++ b/packages/komodo_defi_framework/app_build/build_config.json @@ -1,87 +1,90 @@ { - "api": { - "api_commit_hash": "475cdb49bc343a8fefdc2caaa1635d5ec426990b", - "branch": "v2.6.0-beta", - "fetch_at_build_enabled": true, - "concurrent_downloads_enabled": true, - "source_urls": [ - "https://api.github.com/repos/GLEECBTC/komodo-defi-framework", - "https://devbuilds.gleec.com", - "https://nebula.decker.im" - ], - "platforms": { - "web": { - "matching_pattern": "^(?:kdf_[a-f0-9]{7,40}-wasm|mm2_[a-f0-9]{7,40}-wasm|mm2-[a-f0-9]{7,40}-wasm)\\.zip$", - "valid_zip_sha256_checksums": [ - "8f16eaf4cc401bad35f8a5c47c512b232d20f2501b38ec6c2b4bcd015c76c32f" + "api": { + "api_commit_hash": "475cdb49bc343a8fefdc2caaa1635d5ec426990b", + "branch": "v2.6.0-beta", + "fetch_at_build_enabled": true, + "concurrent_downloads_enabled": true, + "source_urls": [ + "https://api.github.com/repos/GLEECBTC/komodo-defi-framework", + "https://devbuilds.gleec.com", + "https://nebula.decker.im" ], - "path": "web/kdf/bin" - }, - "ios": { - "matching_pattern": "^(?:kdf_[a-f0-9]{7,40}-ios-aarch64|mm2_[a-f0-9]{7,40}-ios-aarch64|mm2-[a-f0-9]{7,40}-ios-aarch64-CI)\\.zip$", - "valid_zip_sha256_checksums": [ - "aa791e8f269dc56c0fc80c5ca224839074ea2ae554993ee237b98599fe0e659f" - ], - "path": "ios" - }, - "macos": { - "matching_pattern": "^(?:kdf-macos-universal2-[a-f0-9]{7,40}|kdf_[a-f0-9]{7,40}-mac-universal)\\.zip$", - "matching_preference": ["universal2", "mac-arm64"], - "valid_zip_sha256_checksums": [ - "90e7ad6c9cc084cb7a9ae2da0b3f375081a03f3ae265b316a342b63525cb6a8d" - ], - "path": "macos/bin" - }, - "windows": { - "matching_pattern": "^(?:kdf_[a-f0-9]{7,40}-win-x86-64|mm2_[a-f0-9]{7,40}-win-x86-64|mm2-[a-f0-9]{7,40}-Win64)\\.zip$", - "valid_zip_sha256_checksums": [ - "1b845b4b36aa27a122edfd291dfe01cc8581083da4bd99941c8a66b1de069c27" - ], - "path": "windows/bin" - }, - "android-armv7": { - "matching_pattern": "^(?:kdf_[a-f0-9]{7,40}-android-armv7|mm2_[a-f0-9]{7,40}-android-armv7|mm2-[a-f0-9]{7,40}-android-armv7-CI)\\.zip$", - "valid_zip_sha256_checksums": [ - "b21e17f04df13d9b600405f880663f0fdf99c85b2b1e53db4bd85bc933345831" - ], - "path": "android/app/src/main/cpp/libs/armeabi-v7a" - }, - "android-aarch64": { - "matching_pattern": "^(?:kdf_[a-f0-9]{7,40}-android-aarch64|mm2_[a-f0-9]{7,40}-android-aarch64|mm2-[a-f0-9]{7,40}-android-aarch64-CI)\\.zip$", - "valid_zip_sha256_checksums": [ - "56b79ab33f4b86236c120ce982b04ccc23d8cdd43b34f66398ea1f8b011becd3" - ], - "path": "android/app/src/main/cpp/libs/arm64-v8a" - }, - "linux": { - "matching_pattern": "^(?:kdf_[a-f0-9]{7,40}-linux-x86-64|mm2_[a-f0-9]{7,40}-linux-x86-64|mm2-[a-f0-9]{7,40}-Linux-Release)\\.zip$", - "valid_zip_sha256_checksums": [ - "4bfabfcfe44b33b32799a90dcd8d373ba5847d8b56e820a1ce5d702dfb76e2cc" - ], - "path": "linux/bin" - } - } - }, - "coins": { - "fetch_at_build_enabled": true, - "update_commit_on_build": true, - "bundled_coins_repo_commit": "b8f85666325ca1cc6306ca113e7129d73e77e0f0", - "coins_repo_api_url": "https://api.github.com/repos/GLEECBTC/coins", - "coins_repo_content_url": "https://raw.githubusercontent.com/GLEECBTC/coins", - "coins_repo_branch": "master", - "runtime_updates_enabled": true, - "mapped_files": { - "assets/config/coins_config.json": "utils/coins_config_unfiltered.json", - "assets/config/coins.json": "coins", - "assets/config/seed_nodes.json": "seed-nodes.json" - }, - "mapped_folders": { - "assets/coin_icons/png/": "icons" + "platforms": { + "web": { + "matching_pattern": "^(?:kdf_[a-f0-9]{7,40}-wasm|mm2_[a-f0-9]{7,40}-wasm|mm2-[a-f0-9]{7,40}-wasm)\\.zip$", + "valid_zip_sha256_checksums": [ + "8f16eaf4cc401bad35f8a5c47c512b232d20f2501b38ec6c2b4bcd015c76c32f" + ], + "path": "web/kdf/bin" + }, + "ios": { + "matching_pattern": "^(?:kdf_[a-f0-9]{7,40}-ios-aarch64|mm2_[a-f0-9]{7,40}-ios-aarch64|mm2-[a-f0-9]{7,40}-ios-aarch64-CI)\\.zip$", + "valid_zip_sha256_checksums": [ + "aa791e8f269dc56c0fc80c5ca224839074ea2ae554993ee237b98599fe0e659f" + ], + "path": "ios" + }, + "macos": { + "matching_pattern": "^(?:kdf-macos-universal2-[a-f0-9]{7,40}|kdf_[a-f0-9]{7,40}-mac-universal)\\.zip$", + "matching_preference": [ + "universal2", + "mac-arm64" + ], + "valid_zip_sha256_checksums": [ + "90e7ad6c9cc084cb7a9ae2da0b3f375081a03f3ae265b316a342b63525cb6a8d" + ], + "path": "macos/bin" + }, + "windows": { + "matching_pattern": "^(?:kdf_[a-f0-9]{7,40}-win-x86-64|mm2_[a-f0-9]{7,40}-win-x86-64|mm2-[a-f0-9]{7,40}-Win64)\\.zip$", + "valid_zip_sha256_checksums": [ + "1b845b4b36aa27a122edfd291dfe01cc8581083da4bd99941c8a66b1de069c27" + ], + "path": "windows/bin" + }, + "android-armv7": { + "matching_pattern": "^(?:kdf_[a-f0-9]{7,40}-android-armv7|mm2_[a-f0-9]{7,40}-android-armv7|mm2-[a-f0-9]{7,40}-android-armv7-CI)\\.zip$", + "valid_zip_sha256_checksums": [ + "b21e17f04df13d9b600405f880663f0fdf99c85b2b1e53db4bd85bc933345831" + ], + "path": "android/app/src/main/cpp/libs/armeabi-v7a" + }, + "android-aarch64": { + "matching_pattern": "^(?:kdf_[a-f0-9]{7,40}-android-aarch64|mm2_[a-f0-9]{7,40}-android-aarch64|mm2-[a-f0-9]{7,40}-android-aarch64-CI)\\.zip$", + "valid_zip_sha256_checksums": [ + "56b79ab33f4b86236c120ce982b04ccc23d8cdd43b34f66398ea1f8b011becd3" + ], + "path": "android/app/src/main/cpp/libs/arm64-v8a" + }, + "linux": { + "matching_pattern": "^(?:kdf_[a-f0-9]{7,40}-linux-x86-64|mm2_[a-f0-9]{7,40}-linux-x86-64|mm2-[a-f0-9]{7,40}-Linux-Release)\\.zip$", + "valid_zip_sha256_checksums": [ + "4bfabfcfe44b33b32799a90dcd8d373ba5847d8b56e820a1ce5d702dfb76e2cc" + ], + "path": "linux/bin" + } + } }, - "concurrent_downloads_enabled": false, - "cdn_branch_mirrors": { - "master": "https://gleecbtc.github.io/coins", - "main": "https://gleecbtc.github.io/coins" + "coins": { + "fetch_at_build_enabled": true, + "update_commit_on_build": true, + "bundled_coins_repo_commit": "c394bc48ba010966af83500eaae9da45a40d03d3", + "coins_repo_api_url": "https://api.github.com/repos/GLEECBTC/coins", + "coins_repo_content_url": "https://raw.githubusercontent.com/GLEECBTC/coins", + "coins_repo_branch": "master", + "runtime_updates_enabled": true, + "mapped_files": { + "assets/config/coins_config.json": "utils/coins_config_unfiltered.json", + "assets/config/coins.json": "coins", + "assets/config/seed_nodes.json": "seed-nodes.json" + }, + "mapped_folders": { + "assets/coin_icons/png/": "icons" + }, + "concurrent_downloads_enabled": false, + "cdn_branch_mirrors": { + "master": "https://gleecbtc.github.io/coins", + "main": "https://gleecbtc.github.io/coins" + } } - } -} +} \ No newline at end of file diff --git a/packages/komodo_defi_local_auth/lib/src/auth/auth_service.dart b/packages/komodo_defi_local_auth/lib/src/auth/auth_service.dart index 8a745108c..07b64bb1e 100644 --- a/packages/komodo_defi_local_auth/lib/src/auth/auth_service.dart +++ b/packages/komodo_defi_local_auth/lib/src/auth/auth_service.dart @@ -110,7 +110,8 @@ abstract interface class IAuthService { } class KdfAuthService implements IAuthService { - KdfAuthService(this._kdfFramework, this._hostConfig) : _sessionId = const Uuid().v4() { + KdfAuthService(this._kdfFramework, this._hostConfig) + : _sessionId = const Uuid().v4() { _logger.info('[$_sessionId] KdfAuthService initialized'); _startHealthCheck(); _subscribeToShutdownSignals(); @@ -132,6 +133,7 @@ class KdfAuthService implements IAuthService { Future? _ongoingHealthCheck; DateTime? _lastHealthCheckAttempt; DateTime? _lastHealthCheckCompleted; + bool? _lastHealthCheckResult; StreamSubscription? _shutdownSubscription; // Cache for wallet users list to avoid spamming get_wallet_names @@ -148,37 +150,45 @@ class KdfAuthService implements IAuthService { required String password, required AuthOptions options, }) async { - _logger.info('[$_sessionId] signIn: Starting login for wallet: $walletName'); - + _logger.info( + '[$_sessionId] signIn: Starting login for wallet: $walletName', + ); + // Proactively ensure KDF is healthy before attempting login // This prevents login attempts while KDF is down or restarting final isHealthy = await ensureKdfHealthy().timeout( const Duration(seconds: 3), onTimeout: () { - _logger.warning('[$_sessionId] signIn: Health check timed out after 3s'); + _logger.warning( + '[$_sessionId] signIn: Health check timed out after 3s', + ); return false; }, ); - + if (!isHealthy) { - _logger.warning('[$_sessionId] signIn: KDF not healthy, retrying after 1s'); + _logger.warning( + '[$_sessionId] signIn: KDF not healthy, retrying after 1s', + ); // Wait and retry once - await Future.delayed(const Duration(milliseconds: 1000)); + await Future.delayed(const Duration(milliseconds: 1000)); final retryHealthy = await ensureKdfHealthy().timeout( const Duration(seconds: 3), onTimeout: () => false, ); if (!retryHealthy) { - _logger.severe('[$_sessionId] signIn: KDF still not healthy after retry'); + _logger.severe( + '[$_sessionId] signIn: KDF still not healthy after retry', + ); throw AuthException( 'KDF is not available. Please try again.', type: AuthExceptionType.apiConnectionError, ); } } - + _logger.info('[$_sessionId] signIn: KDF healthy, proceeding with login'); - + // [getActiveUser] performs a read lock, which should happen outside of // the write lock to prevent deadlocks. If kdf is not running, null is // returned, so we can safely call it here without any checks. @@ -389,16 +399,49 @@ class KdfAuthService implements IAuthService { currentPassword: currentPassword, newPassword: newPassword, ); - } on ChangeMnemonicIncorrectPasswordErrorResponse catch (e) { + } on MmRpcException catch (e) { + if (_isIncorrectPasswordRpcError(e)) { + throw AuthException( + 'Incorrect current password', + type: AuthExceptionType.incorrectPassword, + details: { + 'error': _extractRpcErrorMessage(e), + 'errorType': e.errorType, + }, + ); + } + + final knownExceptions = _findKnownAuthExceptions(e); + if (knownExceptions.isNotEmpty) { + throw knownExceptions.first; + } + throw AuthException( - 'Incorrect current password', - type: AuthExceptionType.incorrectPassword, - details: {'error': e.error, 'errorType': e.errorType}, + 'Failed to change password: ${_extractRpcErrorMessage(e) ?? e}', + type: AuthExceptionType.generalAuthError, + details: {'errorType': e.errorType}, ); - } catch (e) { - final knownExceptions = AuthException.findExceptionsInLog( - e.toString().toLowerCase(), + } on GeneralErrorResponse catch (e) { + if (_isIncorrectPasswordRpcError(e)) { + throw AuthException( + 'Incorrect current password', + type: AuthExceptionType.incorrectPassword, + details: {'error': e.error, 'errorType': e.errorType}, + ); + } + + final knownExceptions = _findKnownAuthExceptions(e); + if (knownExceptions.isNotEmpty) { + throw knownExceptions.first; + } + + throw AuthException( + 'Failed to change password: ${e.error ?? e}', + type: AuthExceptionType.generalAuthError, + details: {'errorType': e.errorType}, ); + } catch (e) { + final knownExceptions = _findKnownAuthExceptions(e); if (knownExceptions.isNotEmpty) { throw knownExceptions.first; } @@ -425,37 +468,12 @@ class KdfAuthService implements IAuthService { ); await _secureStorage.deleteUser(walletName); _invalidateUsersCache(); - } on DeleteWalletInvalidPasswordErrorResponse catch (e) { - throw AuthException( - e.error ?? 'Invalid password', - type: AuthExceptionType.incorrectPassword, - ); - } on DeleteWalletWalletNotFoundErrorResponse { - throw AuthException.notFound(); - } on DeleteWalletCannotDeleteActiveWalletErrorResponse catch (e) { - throw AuthException( - e.error ?? 'Cannot delete active wallet', - type: AuthExceptionType.generalAuthError, - ); - } on DeleteWalletWalletsStorageErrorResponse catch (e) { - throw AuthException( - e.error ?? 'Wallet storage error', - type: AuthExceptionType.internalError, - ); - } on DeleteWalletInvalidRequestErrorResponse catch (e) { - throw AuthException( - e.error ?? 'Invalid request', - type: AuthExceptionType.internalError, - ); - } on DeleteWalletInternalErrorResponse catch (e) { - throw AuthException( - e.error ?? 'Internal error', - type: AuthExceptionType.internalError, - ); + } on MmRpcException catch (e) { + throw _mapDeleteWalletRpcError(e); + } on GeneralErrorResponse catch (e) { + throw _mapDeleteWalletRpcError(e); } catch (e) { - final knownExceptions = AuthException.findExceptionsInLog( - e.toString().toLowerCase(), - ); + final knownExceptions = _findKnownAuthExceptions(e); if (knownExceptions.isNotEmpty) { throw knownExceptions.first; } @@ -467,6 +485,157 @@ class KdfAuthService implements IAuthService { }); } + AuthException _mapDeleteWalletRpcError(Object error) { + final message = _extractRpcErrorMessage(error); + final errorType = _extractRpcErrorType(error); + + if (_isIncorrectPasswordRpcError(error)) { + return AuthException( + message ?? 'Invalid password', + type: AuthExceptionType.incorrectPassword, + details: {if (errorType != null) 'errorType': errorType}, + ); + } + + if (_isWalletNotFoundRpcError(error)) { + return AuthException.notFound(); + } + + if (_isCannotDeleteActiveWalletError(errorType, message)) { + return AuthException( + message ?? 'Cannot delete active wallet', + type: AuthExceptionType.generalAuthError, + details: {if (errorType != null) 'errorType': errorType}, + ); + } + + if (_isInternalWalletError(errorType) || + error is MnemonicRpcErrorWalletsStorageErrorException || + error is MnemonicRpcErrorInternalException) { + return AuthException( + message ?? 'Internal error', + type: AuthExceptionType.internalError, + details: {if (errorType != null) 'errorType': errorType}, + ); + } + + if ((errorType ?? '').toLowerCase() == 'invalidrequest') { + return AuthException( + message ?? 'Invalid request', + type: AuthExceptionType.internalError, + details: {if (errorType != null) 'errorType': errorType}, + ); + } + + return AuthException( + 'Failed to delete wallet: ${message ?? error}', + type: AuthExceptionType.generalAuthError, + details: {if (errorType != null) 'errorType': errorType}, + ); + } + + bool _isIncorrectPasswordRpcError(Object error) { + if (error is MnemonicRpcErrorInvalidPasswordException) { + return true; + } + + final errorType = _extractRpcErrorType(error)?.toLowerCase(); + if (errorType == 'invalidpassword') { + return true; + } + + final message = _extractRpcErrorMessage(error); + if (message == null || message.isEmpty) { + return false; + } + + return AuthException.findExceptionsInLog( + message, + firstOnly: true, + ).any((item) => item.type == AuthExceptionType.incorrectPassword); + } + + bool _isWalletNotFoundRpcError(Object error) { + final errorType = _extractRpcErrorType(error)?.toLowerCase(); + if (errorType == 'walletnotfound') { + return true; + } + + final message = _extractRpcErrorMessage(error)?.toLowerCase() ?? ''; + if (message.contains('wallet not found') || + message.contains('wallet does not exist') || + message.contains('no wallet found')) { + return true; + } + + return AuthException.findExceptionsInLog( + message, + firstOnly: true, + ).any((item) => item.type == AuthExceptionType.walletNotFound); + } + + bool _isCannotDeleteActiveWalletError(String? errorType, String? message) { + if ((errorType ?? '').toLowerCase() == 'cannotdeleteactivewallet') { + return true; + } + + final lowerMessage = (message ?? '').toLowerCase(); + return lowerMessage.contains('cannot delete active wallet'); + } + + bool _isInternalWalletError(String? errorType) { + switch ((errorType ?? '').toLowerCase()) { + case 'walletsstorageerror': + case 'walletstorageerror': + case 'internal': + case 'internalerror': + return true; + default: + return false; + } + } + + String? _extractRpcErrorType(Object error) { + if (error is MmRpcException) { + return error.errorType; + } + if (error is GeneralErrorResponse) { + return error.errorType; + } + return null; + } + + String? _extractRpcErrorMessage(Object error) { + if (error is MnemonicRpcErrorInvalidPasswordException) { + return error.value; + } + if (error is MnemonicRpcErrorInvalidRequestException) { + return error.value; + } + if (error is MnemonicRpcErrorWalletsStorageErrorException) { + return error.value; + } + if (error is MnemonicRpcErrorInternalException) { + return error.value; + } + if (error is MmRpcException) { + return error.message; + } + if (error is GeneralErrorResponse) { + return error.error; + } + return null; + } + + List _findKnownAuthExceptions(Object error) { + final details = _extractRpcErrorMessage(error); + final errorText = [ + if (details != null) details, + error.toString(), + ].join('\n'); + return AuthException.findExceptionsInLog(errorText.toLowerCase()); + } + void _invalidateUsersCache() { _usersCache = null; _usersCacheTimestamp = null; @@ -587,7 +756,9 @@ class KdfAuthService implements IAuthService { Future ensureKdfHealthy() async { // Single-flight guard: if a health check is already in progress, return that future if (_ongoingHealthCheck != null) { - _logger.info('[$_sessionId] ensureKdfHealthy: Health check already in progress, awaiting result'); + _logger.info( + '[$_sessionId] ensureKdfHealthy: Health check already in progress, awaiting result', + ); return _ongoingHealthCheck!; } @@ -597,8 +768,10 @@ class KdfAuthService implements IAuthService { if (_lastHealthCheckCompleted != null) { final timeSinceLastCheck = now.difference(_lastHealthCheckCompleted!); if (timeSinceLastCheck.inSeconds < 2) { - _logger.info('[$_sessionId] ensureKdfHealthy: In cooldown period (${timeSinceLastCheck.inSeconds}s since last check)'); - return false; + _logger.info( + '[$_sessionId] ensureKdfHealthy: In cooldown period (${timeSinceLastCheck.inSeconds}s since last check)', + ); + return _lastHealthCheckResult ?? false; } } @@ -609,8 +782,13 @@ class KdfAuthService implements IAuthService { try { final result = await _ongoingHealthCheck!; _lastHealthCheckCompleted = DateTime.now(); - final elapsed = _lastHealthCheckCompleted!.difference(_lastHealthCheckAttempt!); - _logger.info('[$_sessionId] ensureKdfHealthy: Completed in ${elapsed.inMilliseconds}ms, result=$result'); + _lastHealthCheckResult = result; + final elapsed = _lastHealthCheckCompleted!.difference( + _lastHealthCheckAttempt!, + ); + _logger.info( + '[$_sessionId] ensureKdfHealthy: Completed in ${elapsed.inMilliseconds}ms, result=$result', + ); return result; } finally { // Clear the ongoing check flag when done @@ -621,60 +799,80 @@ class KdfAuthService implements IAuthService { Future _performHealthCheck() async { _logger.info('[$_sessionId] _performHealthCheck: Starting health check'); final stopwatch = Stopwatch()..start(); - + try { // First check if KDF is healthy with a short timeout final isHealthy = await _kdfFramework.isHealthy().timeout( const Duration(seconds: 2), onTimeout: () { - _logger.warning('[$_sessionId] _performHealthCheck: isHealthy() timed out after 2s'); + _logger.warning( + '[$_sessionId] _performHealthCheck: isHealthy() timed out after 2s', + ); return false; }, ); - + if (isHealthy) { // Double verification: even if isHealthy() returns true, verify with version() RPC // This prevents false positives where native status reports "running" but HTTP is down - _logger.info('[$_sessionId] _performHealthCheck: Initial check passed, performing double verification'); + _logger.info( + '[$_sessionId] _performHealthCheck: Initial check passed, performing double verification', + ); final doubleCheck = await _verifyKdfHealthy().timeout( const Duration(seconds: 2), onTimeout: () { - _logger.warning('[$_sessionId] _performHealthCheck: Double verification timed out'); + _logger.warning( + '[$_sessionId] _performHealthCheck: Double verification timed out', + ); return false; }, ); - + if (doubleCheck) { stopwatch.stop(); - _logger.info('[$_sessionId] _performHealthCheck: KDF is healthy (double verified) in ${stopwatch.elapsedMilliseconds}ms'); + _logger.info( + '[$_sessionId] _performHealthCheck: KDF is healthy (double verified) in ${stopwatch.elapsedMilliseconds}ms', + ); return true; } - - _logger.warning('[$_sessionId] _performHealthCheck: Double verification failed, KDF not actually healthy'); + + _logger.warning( + '[$_sessionId] _performHealthCheck: Double verification failed, KDF not actually healthy', + ); } - _logger.warning('[$_sessionId] _performHealthCheck: KDF is not healthy, forcing full restart'); + _logger.warning( + '[$_sessionId] _performHealthCheck: KDF is not healthy, forcing full restart', + ); // Use _lastEmittedUser instead of calling _getActiveUser() RPC when KDF is down // This avoids blocking on a dead KDF final hadAuthenticatedUser = _lastEmittedUser != null; - _logger.info('[$_sessionId] _performHealthCheck: hadAuthenticatedUser=$hadAuthenticatedUser'); + _logger.info( + '[$_sessionId] _performHealthCheck: hadAuthenticatedUser=$hadAuthenticatedUser', + ); // FORCE a full stop->start cycle when we've determined KDF is unhealthy // Don't trust isRunning() as it can be stale after iOS backgrounding - _logger.info('[$_sessionId] _performHealthCheck: Forcing clean shutdown (ignoring isRunning status)'); + _logger.info( + '[$_sessionId] _performHealthCheck: Forcing clean shutdown (ignoring isRunning status)', + ); try { await _stopKdf().timeout( const Duration(seconds: 2), onTimeout: () { - _logger.warning('[$_sessionId] _performHealthCheck: kdfStop() timed out'); + _logger.warning( + '[$_sessionId] _performHealthCheck: kdfStop() timed out', + ); }, ); } catch (e) { - _logger.warning('[$_sessionId] _performHealthCheck: Error during shutdown: $e (continuing with restart)'); + _logger.warning( + '[$_sessionId] _performHealthCheck: Error during shutdown: $e (continuing with restart)', + ); // KDF might already be dead, continue with restart } - + // Reset HTTP client unconditionally to drop stale keep-alive connections _logger.info('[$_sessionId] _performHealthCheck: Resetting HTTP client'); _kdfFramework.resetHttpClient(); @@ -685,46 +883,66 @@ class KdfAuthService implements IAuthService { final restartStopwatch = Stopwatch()..start(); await _forceStartKdf(); restartStopwatch.stop(); - _logger.info('[$_sessionId] _performHealthCheck: KDF force start completed in ${restartStopwatch.elapsedMilliseconds}ms'); + _logger.info( + '[$_sessionId] _performHealthCheck: KDF force start completed in ${restartStopwatch.elapsedMilliseconds}ms', + ); // Reset HTTP client again after restart to ensure no stale sockets - _logger.info('[$_sessionId] _performHealthCheck: Resetting HTTP client again after restart'); + _logger.info( + '[$_sessionId] _performHealthCheck: Resetting HTTP client again after restart', + ); _kdfFramework.resetHttpClient(); // Add 200ms delay after restart before verification to avoid race where // native status reports "up" but HTTP listener hasn't bound yet - _logger.info('[$_sessionId] _performHealthCheck: Waiting 200ms for HTTP listener to bind'); - await Future.delayed(const Duration(milliseconds: 200)); + _logger.info( + '[$_sessionId] _performHealthCheck: Waiting 200ms for HTTP listener to bind', + ); + await Future.delayed(const Duration(milliseconds: 200)); // Check if restart was successful with a strong health check (version RPC) - _logger.info('[$_sessionId] _performHealthCheck: Verifying KDF health with version check'); + _logger.info( + '[$_sessionId] _performHealthCheck: Verifying KDF health with version check', + ); final verifyStopwatch = Stopwatch()..start(); final isHealthyAfterRestart = await _verifyKdfHealthy().timeout( const Duration(seconds: 2), onTimeout: () { - _logger.warning('[$_sessionId] _performHealthCheck: Health verification timed out'); + _logger.warning( + '[$_sessionId] _performHealthCheck: Health verification timed out', + ); return false; }, ); verifyStopwatch.stop(); - _logger.info('[$_sessionId] _performHealthCheck: Health verification took ${verifyStopwatch.elapsedMilliseconds}ms, result=$isHealthyAfterRestart'); + _logger.info( + '[$_sessionId] _performHealthCheck: Health verification took ${verifyStopwatch.elapsedMilliseconds}ms, result=$isHealthyAfterRestart', + ); // If we had an authenticated user, emit logged-out state // This will trigger the UI to show re-authentication prompt if (hadAuthenticatedUser && _lastEmittedUser != null) { - _logger.info('[$_sessionId] _performHealthCheck: Emitting logged-out state'); + _logger.info( + '[$_sessionId] _performHealthCheck: Emitting logged-out state', + ); _emitAuthStateChange(null); } stopwatch.stop(); - _logger.info('[$_sessionId] _performHealthCheck: Health check completed in ${stopwatch.elapsedMilliseconds}ms, result=$isHealthyAfterRestart'); + _logger.info( + '[$_sessionId] _performHealthCheck: Health check completed in ${stopwatch.elapsedMilliseconds}ms, result=$isHealthyAfterRestart', + ); return isHealthyAfterRestart; } catch (e) { stopwatch.stop(); - _logger.severe('[$_sessionId] _performHealthCheck: Error during health check after ${stopwatch.elapsedMilliseconds}ms: $e'); + _logger.severe( + '[$_sessionId] _performHealthCheck: Error during health check after ${stopwatch.elapsedMilliseconds}ms: $e', + ); // If we can't restart KDF and had an authenticated user, emit logged-out state if (_lastEmittedUser != null) { - _logger.info('[$_sessionId] _performHealthCheck: Emitting logged-out state due to error'); + _logger.info( + '[$_sessionId] _performHealthCheck: Emitting logged-out state due to error', + ); _emitAuthStateChange(null); } // Log the error but don't throw - return false to indicate failure @@ -735,23 +953,31 @@ class KdfAuthService implements IAuthService { /// Force starts KDF without checking isRunning() status /// This is needed when we've determined KDF is unhealthy but isRunning() returns stale true Future _forceStartKdf() async { - _logger.info('[$_sessionId] _forceStartKdf: Starting KDF (bypassing isRunning check)'); + _logger.info( + '[$_sessionId] _forceStartKdf: Starting KDF (bypassing isRunning check)', + ); await _lockWriteOperation(() async { final startStopwatch = Stopwatch()..start(); final result = await _kdfFramework.startKdf(await _noAuthConfig); startStopwatch.stop(); - _logger.info('[$_sessionId] _forceStartKdf: startKdf() returned ${result.name} in ${startStopwatch.elapsedMilliseconds}ms'); - + _logger.info( + '[$_sessionId] _forceStartKdf: startKdf() returned ${result.name} in ${startStopwatch.elapsedMilliseconds}ms', + ); + if (!result.isStartingOrAlreadyRunning()) { - _logger.severe('[$_sessionId] _forceStartKdf: Failed to start KDF: ${result.name}'); + _logger.severe( + '[$_sessionId] _forceStartKdf: Failed to start KDF: ${result.name}', + ); throw KdfExtensions._mapStartupErrorToAuthException(result); } - + _logger.info('[$_sessionId] _forceStartKdf: Waiting for RPC to be up'); final waitStopwatch = Stopwatch()..start(); await _waitUntilKdfRpcIsUp(); waitStopwatch.stop(); - _logger.info('[$_sessionId] _forceStartKdf: RPC is up after ${waitStopwatch.elapsedMilliseconds}ms'); + _logger.info( + '[$_sessionId] _forceStartKdf: RPC is up after ${waitStopwatch.elapsedMilliseconds}ms', + ); }); } @@ -763,7 +989,9 @@ class KdfAuthService implements IAuthService { await _kdfFramework.version(); return true; } catch (e) { - _logger.warning('[$_sessionId] _verifyKdfHealthy: Version check failed: $e'); + _logger.warning( + '[$_sessionId] _verifyKdfHealthy: Version check failed: $e', + ); return false; } } diff --git a/packages/komodo_defi_local_auth/lib/src/trezor/trezor_initialization_state.dart b/packages/komodo_defi_local_auth/lib/src/trezor/trezor_initialization_state.dart index 93cfe37b6..7c8ad61b9 100644 --- a/packages/komodo_defi_local_auth/lib/src/trezor/trezor_initialization_state.dart +++ b/packages/komodo_defi_local_auth/lib/src/trezor/trezor_initialization_state.dart @@ -44,7 +44,7 @@ abstract class TrezorInitializationState with _$TrezorInitializationState { final errorInfo = response.errorInfo; return TrezorInitializationState( status: AuthenticationStatus.error, - error: errorInfo?.error ?? 'Unknown error occurred', + error: exceptionMessage(errorInfo) ?? 'Unknown error occurred', taskId: taskId, ); case 'InProgress': diff --git a/packages/komodo_defi_local_auth/test/src/kdf_auth_service_delete_wallet_test.dart b/packages/komodo_defi_local_auth/test/src/kdf_auth_service_delete_wallet_test.dart new file mode 100644 index 000000000..e56260d4a --- /dev/null +++ b/packages/komodo_defi_local_auth/test/src/kdf_auth_service_delete_wallet_test.dart @@ -0,0 +1,206 @@ +import 'package:flutter_secure_storage/flutter_secure_storage.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:komodo_defi_framework/komodo_defi_framework.dart'; +import 'package:komodo_defi_local_auth/src/auth/auth_service.dart'; +import 'package:komodo_defi_types/komodo_defi_types.dart'; + +class _FakeKdfOperations implements IKdfOperations { + _FakeKdfOperations({required this.responsesByMethod}); + + final Map> responsesByMethod; + + @override + String get operationsName => 'fake'; + + @override + Future kdfMain( + Map startParams, { + int? logLevel, + }) async => KdfStartupResult.ok; + + @override + Future kdfMainStatus() async => MainStatus.rpcIsUp; + + @override + Future kdfStop() async => StopStatus.ok; + + @override + Future isRunning() async => true; + + @override + Future version() async => 'test-version'; + + @override + Future> mm2Rpc(Map request) async { + final method = request['method'] as String?; + if (method == null) { + return {'mmrpc': '2.0', 'result': {}}; + } + + return responsesByMethod[method] ?? + {'mmrpc': '2.0', 'result': {}}; + } + + @override + Future validateSetup() async {} + + @override + Future isAvailable(IKdfHostConfig hostConfig) async => true; + + @override + void resetHttpClient() {} + + @override + void dispose() {} +} + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + setUp(() { + FlutterSecureStorage.setMockInitialValues({}); + }); + + group('KdfAuthService.deleteWallet', () { + test( + 'maps WalletNotFound GeneralErrorResponse to AuthException.notFound', + () async { + final service = _createService( + deleteWalletResponse: { + 'mmrpc': '2.0', + 'result': { + 'details': { + 'error': 'Wallet not found', + 'error_type': 'WalletNotFound', + }, + }, + }, + ); + addTearDown(service.dispose); + + await expectLater( + () => service.deleteWallet(walletName: 'missing', password: 'secret'), + throwsA( + isA().having( + (error) => error.type, + 'type', + AuthExceptionType.walletNotFound, + ), + ), + ); + }, + ); + + test( + 'maps CannotDeleteActiveWallet GeneralErrorResponse to auth error', + () async { + final service = _createService( + deleteWalletResponse: { + 'mmrpc': '2.0', + 'result': { + 'details': { + 'error': 'Cannot delete active wallet', + 'error_type': 'CannotDeleteActiveWallet', + }, + }, + }, + ); + addTearDown(service.dispose); + + await expectLater( + () => service.deleteWallet(walletName: 'active', password: 'secret'), + throwsA( + isA() + .having( + (error) => error.type, + 'type', + AuthExceptionType.generalAuthError, + ) + .having( + (error) => error.message, + 'message', + 'Cannot delete active wallet', + ), + ), + ); + }, + ); + }); + + group('KdfAuthService.updatePassword', () { + test('maps incorrect-password GeneralErrorResponse ' + 'to AuthException.incorrectPassword', () async { + final service = _createService( + changeMnemonicPasswordResponse: { + 'mmrpc': '2.0', + 'result': { + 'details': { + 'error': + 'Error decrypting mnemonic: HMAC error: MAC tag mismatch', + }, + }, + }, + ); + addTearDown(service.dispose); + + await service.restoreSession(_testUser()); + + await expectLater( + () => service.updatePassword( + currentPassword: 'wrong-password', + newPassword: 'new-password', + ), + throwsA( + isA().having( + (error) => error.type, + 'type', + AuthExceptionType.incorrectPassword, + ), + ), + ); + }); + }); +} + +KdfAuthService _createService({ + Map? deleteWalletResponse, + Map? changeMnemonicPasswordResponse, +}) { + final hostConfig = LocalConfig(https: false, rpcPassword: 'rpc-pass'); + final framework = KomodoDefiFramework.createWithOperations( + hostConfig: hostConfig, + kdfOperations: _FakeKdfOperations( + responsesByMethod: { + 'delete_wallet': + deleteWalletResponse ?? + {'mmrpc': '2.0', 'result': null}, + 'change_mnemonic_password': + changeMnemonicPasswordResponse ?? + {'mmrpc': '2.0', 'result': null}, + 'get_wallet_names': { + 'mmrpc': '2.0', + 'result': { + 'wallet_names': ['test-wallet'], + 'activated_wallet': 'test-wallet', + }, + }, + 'stream::shutdown_signal::enable': { + 'mmrpc': '2.0', + 'result': {'streamer_id': 'test-stream'}, + }, + }, + ), + ); + + return KdfAuthService(framework, hostConfig); +} + +KdfUser _testUser() { + return KdfUser( + walletId: WalletId.fromName( + 'test-wallet', + const AuthOptions(derivationMethod: DerivationMethod.hdWallet), + ), + isBip39Seed: true, + ); +} diff --git a/packages/komodo_defi_rpc_methods/README.md b/packages/komodo_defi_rpc_methods/README.md index f940bae1d..1669af82f 100644 --- a/packages/komodo_defi_rpc_methods/README.md +++ b/packages/komodo_defi_rpc_methods/README.md @@ -45,6 +45,25 @@ final signed = await client.rpc.utility.signMessage( Explore exported modules in `lib/src/rpc_methods` for the full surface (activation, wallet, utxo/eth/trezor, trading, orderbook, transaction history, withdrawal, etc.). +## Development + +### Regenerating RPC exceptions + +`lib/src/models/mm2_rpc_exceptions.dart` is generated from the Komodo DeFi Framework error enums. + +```sh +python3 tool/generate_mm2_rpc_exceptions.py --mm2-repo /path/to/komodo-defi-framework +# or let the script clone the API repo into a temp directory: +python3 tool/generate_mm2_rpc_exceptions.py +``` + +Generated error models and exception classes expose `errorType` as the raw +API string (e.g. `NoSuchCoin`), so app code can match on classes/strings +directly without depending on generated `*ErrorType` enums. + +By default, the script runs `dart fix --apply` after generation. Use `--no-fix` +to skip it. + ## License MIT diff --git a/packages/komodo_defi_rpc_methods/lib/src/client/client_rpc_library_extension.dart b/packages/komodo_defi_rpc_methods/lib/src/client/client_rpc_library_extension.dart index a8ec45785..666aac5a7 100644 --- a/packages/komodo_defi_rpc_methods/lib/src/client/client_rpc_library_extension.dart +++ b/packages/komodo_defi_rpc_methods/lib/src/client/client_rpc_library_extension.dart @@ -11,7 +11,7 @@ abstract class BaseRpcMethodNamespace { String? get rpcPass => null; Future execute( - BaseRequest request, + BaseRequest request, ) async { final client = _client; if (client == null) { diff --git a/packages/komodo_defi_rpc_methods/lib/src/common_structures/general/address_format.dart b/packages/komodo_defi_rpc_methods/lib/src/common_structures/general/address_format.dart index e814065ab..80c815b0a 100644 --- a/packages/komodo_defi_rpc_methods/lib/src/common_structures/general/address_format.dart +++ b/packages/komodo_defi_rpc_methods/lib/src/common_structures/general/address_format.dart @@ -9,6 +9,7 @@ class AddressFormat { }) { switch (subClass) { case CoinSubClass.erc20: + case CoinSubClass.grc20: case CoinSubClass.ethereumClassic: return AddressFormat( format: AddressFormatType.mixedCase.toString(), @@ -28,8 +29,9 @@ class AddressFormat { return AddressFormat( format: AddressFormatType.cashAddress.toString(), // Only set network for BCH coins - network: - isBchNetwork ? AddressFormatNetwork.bitcoinCash.toString() : '', + network: isBchNetwork + ? AddressFormatNetwork.bitcoinCash.toString() + : '', ); } } diff --git a/packages/komodo_defi_rpc_methods/lib/src/models/base_request.dart b/packages/komodo_defi_rpc_methods/lib/src/models/base_request.dart index 9a8fd0df9..d20f39c3b 100644 --- a/packages/komodo_defi_rpc_methods/lib/src/models/base_request.dart +++ b/packages/komodo_defi_rpc_methods/lib/src/models/base_request.dart @@ -6,17 +6,33 @@ import 'package:komodo_defi_types/komodo_defi_types.dart'; import 'package:meta/meta.dart'; extension BaseRequestApiClientExtension on ApiClient { - Future post( + Future post( BaseRequest request, ) async { final response = await executeRpc(request.toJson()); if (GeneralErrorResponse.isErrorResponse(response)) { + // Try to parse into a typed KDF exception first + final typedException = _tryParseTypedException(response); + if (typedException != null) { + throw typedException; + } throw GeneralErrorResponse.parse(response); } return request.parseResponse(jsonEncode(response)); } + + /// Attempts to parse the error response into a typed [MmRpcException]. + MmRpcException? _tryParseTypedException(JsonMap response) { + // Extract error details from the response structure + final errorDetails = + response.valueOrNull('result', 'details') ?? + response.valueOrNull('message') ?? + response; + + return KdfErrorRegistry.tryParse(errorDetails); + } } /// Base class for all API requests @@ -24,10 +40,7 @@ extension BaseRequestApiClientExtension on ApiClient { /// Parameters: /// - [T] - The response type /// - [E] - The error response type -abstract class BaseRequest< - T extends BaseResponse, - E extends GeneralErrorResponse -> { +abstract class BaseRequest { BaseRequest({ // required this.client, required this.method, @@ -71,12 +84,23 @@ abstract class BaseRequest< /// Parse response from JSON. This method should handle both success and error responses. /// Subclasses should override [parse] method for success responses instead. + /// + /// Error handling order: + /// 1. Try to parse into a typed [MmRpcException] using [KdfErrorRegistry] + /// 2. Allow subclasses to handle specific error types via [parseCustomErrorResponse] + /// 3. Fall back to [GeneralErrorResponse] via [parseGeneralErrorResponse] T parseResponse(String responseBody) { final json = jsonFromString(responseBody); // First check if this is an error response if (GeneralErrorResponse.isErrorResponse(json)) { - // Allow subclasses to handle specific error types first + // Try to parse into a typed KDF exception first + final typedException = _tryParseTypedException(json); + if (typedException != null) { + throw typedException; + } + + // Allow subclasses to handle specific error types final customError = parseCustomErrorResponse(json); if (customError != null) { throw customError; @@ -92,6 +116,17 @@ abstract class BaseRequest< return parse(json); } + /// Attempts to parse the error response into a typed [MmRpcException]. + MmRpcException? _tryParseTypedException(JsonMap json) { + // Extract error details from the response structure + final errorDetails = + json.valueOrNull('result', 'details') ?? + json.valueOrNull('message') ?? + json; + + return KdfErrorRegistry.tryParse(errorDetails); + } + /// Override this method to provide custom error handling for specific error /// types. Return null if the error is not of a type that this request can handle. E? parseCustomErrorResponse(JsonMap json) => null; @@ -99,7 +134,7 @@ abstract class BaseRequest< /// Handles general error responses. This is a fallback for when /// [parseCustomErrorResponse] returns null. @protected - GeneralErrorResponse? parseGeneralErrorResponse(JsonMap json) { + Exception? parseGeneralErrorResponse(JsonMap json) { if (GeneralErrorResponse.isErrorResponse(json)) { return GeneralErrorResponse.parse(json); } diff --git a/packages/komodo_defi_rpc_methods/lib/src/models/error_response.dart b/packages/komodo_defi_rpc_methods/lib/src/models/error_response.dart index a2e749205..72d984f0b 100644 --- a/packages/komodo_defi_rpc_methods/lib/src/models/error_response.dart +++ b/packages/komodo_defi_rpc_methods/lib/src/models/error_response.dart @@ -48,6 +48,35 @@ class GeneralErrorResponse extends BaseResponse implements Exception { return isError; } + /// Attempts to convert this error response to a typed [MmRpcException]. + /// + /// Returns `null` if the error type is not recognized or if this error + /// response does not contain sufficient type information. + /// + /// Example: + /// ```dart + /// final typedException = errorResponse.toTypedException(); + /// if (typedException != null) { + /// // Handle specific exception type + /// switch (typedException) { + /// case AccountRpcErrorNameTooLongException(): + /// print('Name too long!'); + /// // ... other cases + /// } + /// } + /// ``` + MmRpcException? toTypedException() { + // Build a JSON map suitable for KdfErrorRegistry parsing + final errorJson = { + 'error_type': errorType, + 'error_data': errorData, + 'error': error, + 'error_path': errorPath, + 'error_trace': errorTrace, + }; + return KdfErrorRegistry.tryParse(errorJson); + } + @override Map toJson() { return { diff --git a/packages/komodo_defi_rpc_methods/lib/src/models/kdf_error_messages.dart b/packages/komodo_defi_rpc_methods/lib/src/models/kdf_error_messages.dart new file mode 100644 index 000000000..1d16c6e66 --- /dev/null +++ b/packages/komodo_defi_rpc_methods/lib/src/models/kdf_error_messages.dart @@ -0,0 +1,527 @@ +import 'package:komodo_defi_rpc_methods/src/models/error_response.dart'; +import 'package:komodo_defi_rpc_methods/src/models/mm2_rpc_exceptions.dart'; + +/// Represents a user-friendly error message with localization support. +/// +/// Contains both a locale key for translation lookup and a static English +/// fallback message for when translations are unavailable. +class KdfErrorMessage { + const KdfErrorMessage({ + required this.localeKey, + required this.fallbackMessage, + }); + + /// Locale key for translation lookup (e.g., 'kdfErrorNotSufficientBalance'). + /// + /// Apps using localization can use this key to look up translated messages. + final String localeKey; + + /// Static English message to use when translation is unavailable. + /// + /// This ensures users always see a human-readable message even without + /// proper localization setup. + final String fallbackMessage; + + @override + String toString() => fallbackMessage; +} + +/// Provides user-friendly error messages for KDF exceptions. +/// +/// This class maps technical API error types to human-readable messages, +/// abstracting away API-specific knowledge from developers. +/// +/// ## Usage +/// +/// ```dart +/// try { +/// await withdraw(...); +/// } on MmRpcException catch (e) { +/// final msg = KdfErrorMessages.forException(e); +/// if (msg != null) { +/// // Use locale key for translation, fallback for default +/// showError(translate(msg.localeKey) ?? msg.fallbackMessage); +/// } else { +/// // Unknown error type - use technical message +/// showError(e.message ?? 'An unexpected error occurred'); +/// } +/// } +/// ``` +/// +/// ## Simpler Usage with Extension +/// +/// ```dart +/// try { +/// await withdraw(...); +/// } on MmRpcException catch (e) { +/// showError(e.displayMessage); // Always returns a user-friendly message +/// } +/// ``` +abstract final class KdfErrorMessages { + KdfErrorMessages._(); + + /// Returns a user-friendly message for the given exception. + /// + /// Returns `null` if no mapping exists for the exception's error type. + static KdfErrorMessage? forException(MmRpcException exception) { + return _errorMessages[exception.errorType]; + } + + /// Returns a user-friendly message for the given error type string. + /// + /// This is useful when working with [GeneralErrorResponse] or raw error + /// data. Returns `null` if no mapping exists for the error type. + static KdfErrorMessage? forErrorType(String? errorType) { + if (errorType == null) return null; + return _errorMessages[errorType]; + } + + /// Returns all known error types that have user-friendly messages. + static Iterable get mappedErrorTypes => _errorMessages.keys; + + /// Default message for unknown errors. + static const defaultError = KdfErrorMessage( + localeKey: 'kdfErrorGeneric', + fallbackMessage: 'An unexpected error occurred. Please try again.', + ); + + // --------------------------------------------------------------------------- + // Error Message Mappings + // --------------------------------------------------------------------------- + // Organized by category for maintainability. New error types should be added + // to the appropriate category section. + + static const _errorMessages = { + // ------------------------------------------------------------------------- + // Balance & Funds Errors + // ------------------------------------------------------------------------- + 'NotSufficientBalance': KdfErrorMessage( + localeKey: 'kdfErrorNotSufficientBalance', + fallbackMessage: 'Insufficient balance for this transaction.', + ), + 'NotSufficientPlatformBalanceForFee': KdfErrorMessage( + localeKey: 'kdfErrorNotSufficientPlatformBalanceForFee', + fallbackMessage: 'Insufficient balance to pay network fees.', + ), + 'ZeroBalanceToWithdrawMax': KdfErrorMessage( + localeKey: 'kdfErrorZeroBalanceToWithdrawMax', + fallbackMessage: 'Cannot withdraw: your balance is zero.', + ), + 'AmountTooLow': KdfErrorMessage( + localeKey: 'kdfErrorAmountTooLow', + fallbackMessage: 'The amount is too low for this transaction.', + ), + 'NotEnoughNftsAmount': KdfErrorMessage( + localeKey: 'kdfErrorNotEnoughNftsAmount', + fallbackMessage: 'You do not have enough NFTs for this transaction.', + ), + + // ------------------------------------------------------------------------- + // Address Errors + // ------------------------------------------------------------------------- + 'InvalidAddress': KdfErrorMessage( + localeKey: 'kdfErrorInvalidAddress', + fallbackMessage: 'The provided address is invalid.', + ), + 'FromAddressNotFound': KdfErrorMessage( + localeKey: 'kdfErrorFromAddressNotFound', + fallbackMessage: 'Source address not found.', + ), + 'UnexpectedFromAddress': KdfErrorMessage( + localeKey: 'kdfErrorUnexpectedFromAddress', + fallbackMessage: 'Unexpected source address.', + ), + 'MyAddressNotNftOwner': KdfErrorMessage( + localeKey: 'kdfErrorMyAddressNotNftOwner', + fallbackMessage: 'You are not the owner of this NFT.', + ), + + // ------------------------------------------------------------------------- + // Coin & Asset Errors + // ------------------------------------------------------------------------- + 'NoSuchCoin': KdfErrorMessage( + localeKey: 'kdfErrorNoSuchCoin', + fallbackMessage: 'Asset not found or not activated.', + ), + 'CoinNotFound': KdfErrorMessage( + localeKey: 'kdfErrorCoinNotFound', + fallbackMessage: 'Asset not found.', + ), + 'CoinNotSupported': KdfErrorMessage( + localeKey: 'kdfErrorCoinNotSupported', + fallbackMessage: 'This asset is not supported.', + ), + 'CoinIsNotActive': KdfErrorMessage( + localeKey: 'kdfErrorCoinIsNotActive', + fallbackMessage: 'Please activate the asset first.', + ), + 'CoinDoesntSupportInitWithdraw': KdfErrorMessage( + localeKey: 'kdfErrorCoinDoesntSupportWithdraw', + fallbackMessage: 'This asset does not support withdrawals.', + ), + 'CoinDoesntSupportNftWithdraw': KdfErrorMessage( + localeKey: 'kdfErrorCoinDoesntSupportNftWithdraw', + fallbackMessage: 'This asset does not support NFT withdrawals.', + ), + 'NftProtocolNotSupported': KdfErrorMessage( + localeKey: 'kdfErrorNftProtocolNotSupported', + fallbackMessage: 'NFT protocol is not supported for this asset.', + ), + 'ContractTypeDoesntSupportNftWithdrawing': KdfErrorMessage( + localeKey: 'kdfErrorContractTypeDoesntSupportNft', + fallbackMessage: 'This contract type does not support NFT withdrawals.', + ), + + // ------------------------------------------------------------------------- + // Network & Transport Errors + // ------------------------------------------------------------------------- + 'Transport': KdfErrorMessage( + localeKey: 'kdfErrorTransport', + fallbackMessage: 'Network error. Please check your connection.', + ), + 'Timeout': KdfErrorMessage( + localeKey: 'kdfErrorTimeout', + fallbackMessage: 'Request timed out. Please try again.', + ), + 'TaskTimedOut': KdfErrorMessage( + localeKey: 'kdfErrorTaskTimedOut', + fallbackMessage: 'Operation timed out. Please try again.', + ), + 'InvalidResponse': KdfErrorMessage( + localeKey: 'kdfErrorInvalidResponse', + fallbackMessage: 'Received an invalid response from the server.', + ), + 'UnreachableNodes': KdfErrorMessage( + localeKey: 'kdfErrorUnreachableNodes', + fallbackMessage: 'Unable to connect to network nodes.', + ), + 'AtLeastOneNodeRequired': KdfErrorMessage( + localeKey: 'kdfErrorAtLeastOneNodeRequired', + fallbackMessage: 'At least one network node is required.', + ), + 'ClientConnectionFailed': KdfErrorMessage( + localeKey: 'kdfErrorClientConnectionFailed', + fallbackMessage: 'Failed to connect to the server.', + ), + 'ConnectToNodeError': KdfErrorMessage( + localeKey: 'kdfErrorConnectToNodeError', + fallbackMessage: 'Failed to connect to network node.', + ), + + // ------------------------------------------------------------------------- + // Activation Errors + // ------------------------------------------------------------------------- + 'ActivationFailed': KdfErrorMessage( + localeKey: 'kdfErrorActivationFailed', + fallbackMessage: 'Failed to activate the asset.', + ), + 'CouldNotFetchBalance': KdfErrorMessage( + localeKey: 'kdfErrorCouldNotFetchBalance', + fallbackMessage: 'Could not fetch balance. Please try again.', + ), + 'UnsupportedChain': KdfErrorMessage( + localeKey: 'kdfErrorUnsupportedChain', + fallbackMessage: 'This blockchain is not supported.', + ), + 'ChainIdNotSet': KdfErrorMessage( + localeKey: 'kdfErrorChainIdNotSet', + fallbackMessage: 'Chain ID is not configured.', + ), + 'NoChainIdSet': KdfErrorMessage( + localeKey: 'kdfErrorNoChainIdSet', + fallbackMessage: 'Chain ID is not set.', + ), + + // ------------------------------------------------------------------------- + // Fee Errors + // ------------------------------------------------------------------------- + 'InvalidFeePolicy': KdfErrorMessage( + localeKey: 'kdfErrorInvalidFeePolicy', + fallbackMessage: 'Invalid fee configuration.', + ), + 'InvalidFee': KdfErrorMessage( + localeKey: 'kdfErrorInvalidFee', + fallbackMessage: 'The specified fee is invalid.', + ), + 'InvalidGasApiConfig': KdfErrorMessage( + localeKey: 'kdfErrorInvalidGasApiConfig', + fallbackMessage: 'Invalid gas API configuration.', + ), + + // ------------------------------------------------------------------------- + // Account Errors + // ------------------------------------------------------------------------- + 'NameTooLong': KdfErrorMessage( + localeKey: 'kdfErrorNameTooLong', + fallbackMessage: 'The name is too long.', + ), + 'DescriptionTooLong': KdfErrorMessage( + localeKey: 'kdfErrorDescriptionTooLong', + fallbackMessage: 'The description is too long.', + ), + 'NoSuchAccount': KdfErrorMessage( + localeKey: 'kdfErrorNoSuchAccount', + fallbackMessage: 'Account not found.', + ), + 'NoEnabledAccount': KdfErrorMessage( + localeKey: 'kdfErrorNoEnabledAccount', + fallbackMessage: 'No account is enabled.', + ), + 'AccountExistsAlready': KdfErrorMessage( + localeKey: 'kdfErrorAccountExistsAlready', + fallbackMessage: 'An account with this name already exists.', + ), + 'UnknownAccount': KdfErrorMessage( + localeKey: 'kdfErrorUnknownAccount', + fallbackMessage: 'Unknown account.', + ), + 'ErrorLoadingAccount': KdfErrorMessage( + localeKey: 'kdfErrorLoadingAccount', + fallbackMessage: 'Failed to load account.', + ), + 'ErrorSavingAccount': KdfErrorMessage( + localeKey: 'kdfErrorSavingAccount', + fallbackMessage: 'Failed to save account.', + ), + + // ------------------------------------------------------------------------- + // Hardware Wallet Errors + // ------------------------------------------------------------------------- + 'HwError': KdfErrorMessage( + localeKey: 'kdfErrorHwError', + fallbackMessage: 'Hardware wallet error occurred.', + ), + 'HwContextNotInitialized': KdfErrorMessage( + localeKey: 'kdfErrorHwContextNotInitialized', + fallbackMessage: 'Hardware wallet is not initialized.', + ), + 'CoinDoesntSupportTrezor': KdfErrorMessage( + localeKey: 'kdfErrorCoinDoesntSupportTrezor', + fallbackMessage: 'This asset is not supported on Trezor.', + ), + 'InvalidHardwareWalletCall': KdfErrorMessage( + localeKey: 'kdfErrorInvalidHardwareWalletCall', + fallbackMessage: 'Invalid hardware wallet operation.', + ), + + // ------------------------------------------------------------------------- + // Swap & Trading Errors + // ------------------------------------------------------------------------- + 'NotSupported': KdfErrorMessage( + localeKey: 'kdfErrorNotSupported', + fallbackMessage: 'This operation is not supported.', + ), + 'VolumeTooLow': KdfErrorMessage( + localeKey: 'kdfErrorVolumeTooLow', + fallbackMessage: 'Trade volume is too low.', + ), + 'MyRecentSwapsError': KdfErrorMessage( + localeKey: 'kdfErrorMyRecentSwapsError', + fallbackMessage: 'Failed to fetch recent swaps.', + ), + 'SwapInfoNotAvailable': KdfErrorMessage( + localeKey: 'kdfErrorSwapInfoNotAvailable', + fallbackMessage: 'Swap information is not available.', + ), + + // ------------------------------------------------------------------------- + // Authentication & Wallet Errors + // ------------------------------------------------------------------------- + 'InvalidRequest': KdfErrorMessage( + localeKey: 'kdfErrorInvalidRequest', + fallbackMessage: 'Invalid request.', + ), + 'InvalidPayload': KdfErrorMessage( + localeKey: 'kdfErrorInvalidPayload', + fallbackMessage: 'Invalid data provided.', + ), + 'InvalidMemo': KdfErrorMessage( + localeKey: 'kdfErrorInvalidMemo', + fallbackMessage: 'Invalid memo/tag provided.', + ), + 'InvalidConfiguration': KdfErrorMessage( + localeKey: 'kdfErrorInvalidConfiguration', + fallbackMessage: 'Invalid configuration.', + ), + 'PrivKeyPolicyNotAllowed': KdfErrorMessage( + localeKey: 'kdfErrorPrivKeyPolicyNotAllowed', + fallbackMessage: + 'This operation is not allowed with the current wallet type.', + ), + 'UnexpectedDerivationMethod': KdfErrorMessage( + localeKey: 'kdfErrorUnexpectedDerivationMethod', + fallbackMessage: 'Unexpected wallet derivation method.', + ), + 'ActionNotAllowed': KdfErrorMessage( + localeKey: 'kdfErrorActionNotAllowed', + fallbackMessage: 'This action is not allowed.', + ), + 'UnexpectedUserAction': KdfErrorMessage( + localeKey: 'kdfErrorUnexpectedUserAction', + fallbackMessage: 'Unexpected user action.', + ), + 'BroadcastExpected': KdfErrorMessage( + localeKey: 'kdfErrorBroadcastExpected', + fallbackMessage: 'Transaction broadcast was expected.', + ), + + // ------------------------------------------------------------------------- + // Database & Storage Errors + // ------------------------------------------------------------------------- + 'DbError': KdfErrorMessage( + localeKey: 'kdfErrorDbError', + fallbackMessage: 'Database error occurred.', + ), + 'WalletStorageError': KdfErrorMessage( + localeKey: 'kdfErrorWalletStorageError', + fallbackMessage: 'Wallet storage error occurred.', + ), + 'HDWalletStorageError': KdfErrorMessage( + localeKey: 'kdfErrorHDWalletStorageError', + fallbackMessage: 'HD wallet storage error occurred.', + ), + + // ------------------------------------------------------------------------- + // Internal & System Errors + // ------------------------------------------------------------------------- + 'Internal': KdfErrorMessage( + localeKey: 'kdfErrorInternal', + fallbackMessage: 'An internal error occurred.', + ), + 'InternalError': KdfErrorMessage( + localeKey: 'kdfErrorInternalError', + fallbackMessage: 'An internal error occurred.', + ), + 'UnsupportedError': KdfErrorMessage( + localeKey: 'kdfErrorUnsupportedError', + fallbackMessage: 'Unsupported operation.', + ), + 'SigningError': KdfErrorMessage( + localeKey: 'kdfErrorSigningError', + fallbackMessage: 'Failed to sign the transaction.', + ), + 'SystemTimeError': KdfErrorMessage( + localeKey: 'kdfErrorSystemTimeError', + fallbackMessage: 'System time error. Please check your device time.', + ), + 'NumConversError': KdfErrorMessage( + localeKey: 'kdfErrorNumConversError', + fallbackMessage: 'Number conversion error.', + ), + 'IOError': KdfErrorMessage( + localeKey: 'kdfErrorIOError', + fallbackMessage: 'Input/output error occurred.', + ), + 'RpcError': KdfErrorMessage( + localeKey: 'kdfErrorRpcError', + fallbackMessage: 'RPC communication error.', + ), + 'RpcTaskError': KdfErrorMessage( + localeKey: 'kdfErrorRpcTaskError', + fallbackMessage: 'RPC task error.', + ), + + // ------------------------------------------------------------------------- + // Address Derivation Errors + // ------------------------------------------------------------------------- + 'InvalidBip44Chain': KdfErrorMessage( + localeKey: 'kdfErrorInvalidBip44Chain', + fallbackMessage: 'Invalid BIP44 derivation chain.', + ), + 'Bip32Error': KdfErrorMessage( + localeKey: 'kdfErrorBip32Error', + fallbackMessage: 'Key derivation error.', + ), + 'InvalidPath': KdfErrorMessage( + localeKey: 'kdfErrorInvalidPath', + fallbackMessage: 'Invalid derivation path.', + ), + 'InvalidPathToAddress': KdfErrorMessage( + localeKey: 'kdfErrorInvalidPathToAddress', + fallbackMessage: 'Invalid path to address.', + ), + 'ErrorDeserializingDerivationPath': KdfErrorMessage( + localeKey: 'kdfErrorDeserializingDerivationPath', + fallbackMessage: 'Failed to parse derivation path.', + ), + + // ------------------------------------------------------------------------- + // Contract & Token Errors + // ------------------------------------------------------------------------- + 'InvalidSwapContractAddr': KdfErrorMessage( + localeKey: 'kdfErrorInvalidSwapContractAddr', + fallbackMessage: 'Invalid swap contract address.', + ), + 'InvalidFallbackSwapContract': KdfErrorMessage( + localeKey: 'kdfErrorInvalidFallbackSwapContract', + fallbackMessage: 'Invalid fallback swap contract.', + ), + 'CustomTokenError': KdfErrorMessage( + localeKey: 'kdfErrorCustomTokenError', + fallbackMessage: 'Custom token error.', + ), + 'GetNftInfoError': KdfErrorMessage( + localeKey: 'kdfErrorGetNftInfoError', + fallbackMessage: 'Failed to get NFT information.', + ), + + // ------------------------------------------------------------------------- + // External Service Errors + // ------------------------------------------------------------------------- + 'MetamaskError': KdfErrorMessage( + localeKey: 'kdfErrorMetamaskError', + fallbackMessage: 'MetaMask error occurred.', + ), + 'WalletConnectError': KdfErrorMessage( + localeKey: 'kdfErrorWalletConnectError', + fallbackMessage: 'WalletConnect error occurred.', + ), + }; +} + +/// Extension on [MmRpcException] providing convenient access to user-friendly +/// error messages. +/// +/// This allows developers to get user-friendly messages without needing to +/// understand the underlying error type system. +/// +/// ## Example +/// +/// ```dart +/// try { +/// await withdraw(...); +/// } on MmRpcException catch (e) { +/// // Always returns a human-readable message +/// showErrorDialog(e.displayMessage); +/// +/// // Access structured message info if needed +/// final msg = e.userMessage; +/// if (msg != null) { +/// analytics.logError(msg.localeKey); +/// } +/// } +/// ``` +extension MmRpcExceptionUserMessage on MmRpcException { + /// Returns user-friendly message info for this exception. + /// + /// Returns `null` if no mapping exists for this error type. In that case, + /// use [displayMessage] which always returns a usable message. + KdfErrorMessage? get userMessage => KdfErrorMessages.forException(this); + + /// Returns a user-friendly display message for this exception. + /// + /// This method always returns a usable message: + /// 1. If a user-friendly mapping exists, returns the fallback message + /// 2. Otherwise, returns the technical error message + /// 3. As a last resort, returns a generic error message + String get displayMessage => + userMessage?.fallbackMessage ?? + message ?? + KdfErrorMessages.defaultError.fallbackMessage; + + /// Returns the locale key for this exception if a mapping exists. + /// + /// Returns `null` if no mapping exists. Apps can use this to look up + /// translated messages in their localization system. + String? get localeKey => userMessage?.localeKey; +} diff --git a/packages/komodo_defi_rpc_methods/lib/src/models/mm2_rpc_exceptions.dart b/packages/komodo_defi_rpc_methods/lib/src/models/mm2_rpc_exceptions.dart new file mode 100644 index 000000000..e56e7cfbe --- /dev/null +++ b/packages/komodo_defi_rpc_methods/lib/src/models/mm2_rpc_exceptions.dart @@ -0,0 +1,26856 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND. +// Generated by tool/generate_mm2_rpc_exceptions.py. +// ignore_for_file: public_member_api_docs, constant_identifier_names + +import 'package:komodo_defi_types/komodo_defi_type_utils.dart'; + +int _intFromJson(dynamic value) { + if (value is int) return value; + if (value is num) return value.toInt(); + if (value is String) return int.tryParse(value) ?? 0; + return 0; +} + +double _doubleFromJson(dynamic value) { + if (value is double) return value; + if (value is num) return value.toDouble(); + if (value is String) return double.tryParse(value) ?? 0; + return 0; +} + +String _stringFromJson(dynamic value) { + if (value == null) { + return ''; + } + return value.toString(); +} + +JsonMap _asJsonMap(dynamic json) { + if (json is JsonMap) { + return json; + } + if (json is Map) { + try { + return convertToJsonMap(json); + } catch (_) { + return json.map((key, value) => MapEntry(key.toString(), value)); + } + } + return {}; +} + +List _asJsonList(dynamic json) { + if (json is List) { + return List.from(json); + } + return []; +} + +final class BigDecimal { + const BigDecimal(this.value); + + factory BigDecimal.fromJson(dynamic json) { + return BigDecimal(_stringFromJson(json)); + } + + final String value; + + dynamic toJson() => value; +} + +final class BigUint { + const BigUint(this.value); + + factory BigUint.fromJson(dynamic json) { + return BigUint(_stringFromJson(json)); + } + + final String value; + + dynamic toJson() => value; +} + +final class H160Json { + const H160Json(this.value); + + factory H160Json.fromJson(dynamic json) { + return H160Json(_stringFromJson(json)); + } + + final String value; + + dynamic toJson() => value; +} + +final class H256Json { + const H256Json(this.value); + + factory H256Json.fromJson(dynamic json) { + return H256Json(_stringFromJson(json)); + } + + final String value; + + dynamic toJson() => value; +} + +final class HwPubkey { + const HwPubkey(this.value); + + factory HwPubkey.fromJson(dynamic json) { + return HwPubkey(H160Json.fromJson(json)); + } + + final H160Json value; + + dynamic toJson() => value.toJson(); +} + +final class JsonValue { + const JsonValue(this.value); + + factory JsonValue.fromJson(dynamic json) { + return JsonValue(json); + } + + final dynamic value; + + dynamic toJson() => value; +} + +final class Mm2Duration { + const Mm2Duration(this.value); + + factory Mm2Duration.fromJson(dynamic json) { + if (json is Map) { + final secs = json['secs'] ?? json['seconds'] ?? 0; + final nanos = json['nanos'] ?? 0; + final seconds = _intFromJson(secs); + final nanoseconds = _intFromJson(nanos); + return Mm2Duration( + Duration(microseconds: seconds * 1000000 + (nanoseconds ~/ 1000)), + ); + } + if (json is int || json is num || json is String) { + return Mm2Duration(Duration(seconds: _intFromJson(json))); + } + return const Mm2Duration(Duration.zero); + } + + final Duration value; + + Map toJson() { + final seconds = value.inSeconds; + final nanoseconds = (value.inMicroseconds - seconds * 1000000) * 1000; + return {'secs': seconds, 'nanos': nanoseconds}; + } +} + +final class PathBuf { + const PathBuf(this.value); + + factory PathBuf.fromJson(dynamic json) { + return PathBuf(_stringFromJson(json)); + } + + final String value; + + dynamic toJson() => value; +} + +final class TaskId { + const TaskId(this.value); + + factory TaskId.fromJson(dynamic json) { + return TaskId(_intFromJson(json)); + } + + final int value; + + dynamic toJson() => value; +} + +final class U256 { + const U256(this.value); + + factory U256.fromJson(dynamic json) { + return U256(_stringFromJson(json)); + } + + final String value; + + dynamic toJson() => value; +} + +final class ChannelId { + const ChannelId(this.value); + + factory ChannelId.fromJson(dynamic json) { + return ChannelId(_intFromJson(json)); + } + + final int value; + + dynamic toJson() { + return value; + } +} + +sealed class AccountId { + const AccountId({required this.type}); + + factory AccountId.fromJson(dynamic json) { + if (json is String) { + switch (json) { + case 'Iguana': + return AccountIdIguana.fromJson(); + case 'HD': + return AccountIdHD.fromJson(null); + case 'HW': + return AccountIdHW.fromJson(null); + } + return AccountIdUnknown(type: json, data: json); + } + if (json is Map && json.isNotEmpty) { + final entry = json.entries.first; + switch (entry.key) { + case 'Iguana': + return AccountIdIguana.fromJson(); + case 'HD': + return AccountIdHD.fromJson(entry.value); + case 'HW': + return AccountIdHW.fromJson(entry.value); + } + return AccountIdUnknown(type: entry.key, data: entry.value); + } + return AccountIdUnknown(type: null, data: json); + } + + final String type; + + JsonMap toJson(); +} + +final class AccountIdIguana extends AccountId { + const AccountIdIguana() : super(type: 'Iguana'); + + factory AccountIdIguana.fromJson() => const AccountIdIguana(); + + @override + JsonMap toJson() => {'Iguana': null}; +} + +final class AccountIdHD extends AccountId { + const AccountIdHD({required this.account_idx}) : super(type: 'HD'); + + factory AccountIdHD.fromJson(dynamic json) { + final map = _asJsonMap(json); + return AccountIdHD( + account_idx: _intFromJson(map.value('account_idx')), + ); + } + + final int account_idx; + + @override + JsonMap toJson() => { + 'HD': {'account_idx': account_idx}, + }; +} + +final class AccountIdHW extends AccountId { + const AccountIdHW({required this.device_pubkey}) : super(type: 'HW'); + + factory AccountIdHW.fromJson(dynamic json) { + final map = _asJsonMap(json); + return AccountIdHW( + device_pubkey: HwPubkey.fromJson(map.value('device_pubkey')), + ); + } + + final HwPubkey device_pubkey; + + @override + JsonMap toJson() => { + 'HW': {'device_pubkey': device_pubkey.toJson()}, + }; +} + +final class AccountIdUnknown extends AccountId { + const AccountIdUnknown({required String? type, required this.data}) + : super(type: type ?? 'unknown'); + + final dynamic data; + + @override + JsonMap toJson() => {'type': type, 'data': data}; +} + +sealed class ApiClientError { + const ApiClientError({required this.type}); + + factory ApiClientError.fromJson(dynamic json) { + if (json is String) { + switch (json) { + case 'InvalidParam': + return ApiClientErrorInvalidParam.fromJson(null); + case 'OutOfBounds': + return ApiClientErrorOutOfBounds.fromJson(null); + case 'TransportError': + return ApiClientErrorTransportError.fromJson(null); + case 'ParseBodyError': + return ApiClientErrorParseBodyError.fromJson(null); + case 'GeneralApiError': + return ApiClientErrorGeneralApiError.fromJson(null); + case 'AllowanceNotEnough': + return ApiClientErrorAllowanceNotEnough.fromJson(null); + } + return ApiClientErrorUnknown(type: json, data: json); + } + if (json is Map && json.isNotEmpty) { + final entry = json.entries.first; + switch (entry.key) { + case 'InvalidParam': + return ApiClientErrorInvalidParam.fromJson(entry.value); + case 'OutOfBounds': + return ApiClientErrorOutOfBounds.fromJson(entry.value); + case 'TransportError': + return ApiClientErrorTransportError.fromJson(entry.value); + case 'ParseBodyError': + return ApiClientErrorParseBodyError.fromJson(entry.value); + case 'GeneralApiError': + return ApiClientErrorGeneralApiError.fromJson(entry.value); + case 'AllowanceNotEnough': + return ApiClientErrorAllowanceNotEnough.fromJson(entry.value); + } + return ApiClientErrorUnknown(type: entry.key, data: entry.value); + } + return ApiClientErrorUnknown(type: null, data: json); + } + + final String type; + + JsonMap toJson(); +} + +final class ApiClientErrorInvalidParam extends ApiClientError { + const ApiClientErrorInvalidParam(this.value) : super(type: 'InvalidParam'); + + factory ApiClientErrorInvalidParam.fromJson(dynamic json) { + return ApiClientErrorInvalidParam(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'InvalidParam': value}; +} + +final class ApiClientErrorOutOfBounds extends ApiClientError { + const ApiClientErrorOutOfBounds({ + required this.param, + required this.value, + required this.min, + required this.max, + }) : super(type: 'OutOfBounds'); + + factory ApiClientErrorOutOfBounds.fromJson(dynamic json) { + final map = _asJsonMap(json); + return ApiClientErrorOutOfBounds( + param: _stringFromJson(map.value('param')), + value: _stringFromJson(map.value('value')), + min: _stringFromJson(map.value('min')), + max: _stringFromJson(map.value('max')), + ); + } + + final String param; + final String value; + final String min; + final String max; + + @override + JsonMap toJson() => { + 'OutOfBounds': {'param': param, 'value': value, 'min': min, 'max': max}, + }; +} + +final class ApiClientErrorTransportError extends ApiClientError { + const ApiClientErrorTransportError(this.value) + : super(type: 'TransportError'); + + factory ApiClientErrorTransportError.fromJson(dynamic json) { + return ApiClientErrorTransportError(SlurpError.fromJson(json)); + } + + final SlurpError value; + + @override + JsonMap toJson() => {'TransportError': value.toJson()}; +} + +final class ApiClientErrorParseBodyError extends ApiClientError { + const ApiClientErrorParseBodyError({required this.error_msg}) + : super(type: 'ParseBodyError'); + + factory ApiClientErrorParseBodyError.fromJson(dynamic json) { + final map = _asJsonMap(json); + return ApiClientErrorParseBodyError( + error_msg: _stringFromJson(map.value('error_msg')), + ); + } + + final String error_msg; + + @override + JsonMap toJson() => { + 'ParseBodyError': {'error_msg': error_msg}, + }; +} + +final class ApiClientErrorGeneralApiError extends ApiClientError { + const ApiClientErrorGeneralApiError({ + required this.error_msg, + required this.description, + required this.status_code, + }) : super(type: 'GeneralApiError'); + + factory ApiClientErrorGeneralApiError.fromJson(dynamic json) { + final map = _asJsonMap(json); + return ApiClientErrorGeneralApiError( + error_msg: _stringFromJson(map.value('error_msg')), + description: _stringFromJson(map.value('description')), + status_code: _intFromJson(map.value('status_code')), + ); + } + + final String error_msg; + final String description; + final int status_code; + + @override + JsonMap toJson() => { + 'GeneralApiError': { + 'error_msg': error_msg, + 'description': description, + 'status_code': status_code, + }, + }; +} + +final class ApiClientErrorAllowanceNotEnough extends ApiClientError { + const ApiClientErrorAllowanceNotEnough({ + required this.error_msg, + required this.description, + required this.status_code, + }) : super(type: 'AllowanceNotEnough'); + + factory ApiClientErrorAllowanceNotEnough.fromJson(dynamic json) { + final map = _asJsonMap(json); + return ApiClientErrorAllowanceNotEnough( + error_msg: _stringFromJson(map.value('error_msg')), + description: _stringFromJson(map.value('description')), + status_code: _intFromJson(map.value('status_code')), + ); + } + + final String error_msg; + final String description; + final int status_code; + + @override + JsonMap toJson() => { + 'AllowanceNotEnough': { + 'error_msg': error_msg, + 'description': description, + 'status_code': status_code, + }, + }; +} + +final class ApiClientErrorUnknown extends ApiClientError { + const ApiClientErrorUnknown({required String? type, required this.data}) + : super(type: type ?? 'unknown'); + + final dynamic data; + + @override + JsonMap toJson() => {'type': type, 'data': data}; +} + +enum Bip44Chain { + externalValue, + internal, + unknown; + + static Bip44Chain fromJson(dynamic json) { + String? value; + if (json is Map && json.isNotEmpty) { + value = json.keys.first.toString(); + } else { + value = _stringFromJson(json); + } + switch (value) { + case 'External': + return Bip44Chain.externalValue; + case 'Internal': + return Bip44Chain.internal; + default: + return Bip44Chain.unknown; + } + } + + String toJson() { + switch (this) { + case Bip44Chain.externalValue: + return 'External'; + case Bip44Chain.internal: + return 'Internal'; + case Bip44Chain.unknown: + return 'unknown'; + } + } +} + +sealed class CustomTokenError { + const CustomTokenError({required this.type}); + + factory CustomTokenError.fromJson(dynamic json) { + if (json is String) { + switch (json) { + case 'DuplicateTickerInConfig': + return CustomTokenErrorDuplicateTickerInConfig.fromJson(null); + case 'DuplicateContractInConfig': + return CustomTokenErrorDuplicateContractInConfig.fromJson(null); + case 'TokenWithSameContractAlreadyActivated': + return CustomTokenErrorTokenWithSameContractAlreadyActivated.fromJson( + null, + ); + case 'InvalidTokenAddress': + return CustomTokenErrorInvalidTokenAddress.fromJson(); + } + return CustomTokenErrorUnknown(type: json, data: json); + } + if (json is Map && json.isNotEmpty) { + final entry = json.entries.first; + switch (entry.key) { + case 'DuplicateTickerInConfig': + return CustomTokenErrorDuplicateTickerInConfig.fromJson(entry.value); + case 'DuplicateContractInConfig': + return CustomTokenErrorDuplicateContractInConfig.fromJson( + entry.value, + ); + case 'TokenWithSameContractAlreadyActivated': + return CustomTokenErrorTokenWithSameContractAlreadyActivated.fromJson( + entry.value, + ); + case 'InvalidTokenAddress': + return CustomTokenErrorInvalidTokenAddress.fromJson(); + } + return CustomTokenErrorUnknown(type: entry.key, data: entry.value); + } + return CustomTokenErrorUnknown(type: null, data: json); + } + + final String type; + + JsonMap toJson(); +} + +final class CustomTokenErrorDuplicateTickerInConfig extends CustomTokenError { + const CustomTokenErrorDuplicateTickerInConfig({ + required this.ticker_in_config, + }) : super(type: 'DuplicateTickerInConfig'); + + factory CustomTokenErrorDuplicateTickerInConfig.fromJson(dynamic json) { + final map = _asJsonMap(json); + return CustomTokenErrorDuplicateTickerInConfig( + ticker_in_config: _stringFromJson(map.value('ticker_in_config')), + ); + } + + final String ticker_in_config; + + @override + JsonMap toJson() => { + 'DuplicateTickerInConfig': {'ticker_in_config': ticker_in_config}, + }; +} + +final class CustomTokenErrorDuplicateContractInConfig extends CustomTokenError { + const CustomTokenErrorDuplicateContractInConfig({ + required this.ticker_in_config, + }) : super(type: 'DuplicateContractInConfig'); + + factory CustomTokenErrorDuplicateContractInConfig.fromJson(dynamic json) { + final map = _asJsonMap(json); + return CustomTokenErrorDuplicateContractInConfig( + ticker_in_config: _stringFromJson(map.value('ticker_in_config')), + ); + } + + final String ticker_in_config; + + @override + JsonMap toJson() => { + 'DuplicateContractInConfig': {'ticker_in_config': ticker_in_config}, + }; +} + +final class CustomTokenErrorTokenWithSameContractAlreadyActivated + extends CustomTokenError { + const CustomTokenErrorTokenWithSameContractAlreadyActivated({ + required this.ticker, + required this.contract_address, + }) : super(type: 'TokenWithSameContractAlreadyActivated'); + + factory CustomTokenErrorTokenWithSameContractAlreadyActivated.fromJson( + dynamic json, + ) { + final map = _asJsonMap(json); + return CustomTokenErrorTokenWithSameContractAlreadyActivated( + ticker: _stringFromJson(map.value('ticker')), + contract_address: _stringFromJson(map.value('contract_address')), + ); + } + + final String ticker; + final String contract_address; + + @override + JsonMap toJson() => { + 'TokenWithSameContractAlreadyActivated': { + 'ticker': ticker, + 'contract_address': contract_address, + }, + }; +} + +final class CustomTokenErrorInvalidTokenAddress extends CustomTokenError { + const CustomTokenErrorInvalidTokenAddress() + : super(type: 'InvalidTokenAddress'); + + factory CustomTokenErrorInvalidTokenAddress.fromJson() => + const CustomTokenErrorInvalidTokenAddress(); + + @override + JsonMap toJson() => {'InvalidTokenAddress': null}; +} + +final class CustomTokenErrorUnknown extends CustomTokenError { + const CustomTokenErrorUnknown({required String? type, required this.data}) + : super(type: type ?? 'unknown'); + + final dynamic data; + + @override + JsonMap toJson() => {'type': type, 'data': data}; +} + +sealed class GetEthAddressError { + const GetEthAddressError({required this.type}); + + factory GetEthAddressError.fromJson(dynamic json) { + if (json is String) { + switch (json) { + case 'UnexpectedDerivationMethod': + return GetEthAddressErrorUnexpectedDerivationMethod.fromJson(null); + case 'EthActivationV2Error': + return GetEthAddressErrorEthActivationV2Error.fromJson(null); + case 'Internal': + return GetEthAddressErrorInternal.fromJson(null); + } + return GetEthAddressErrorUnknown(type: json, data: json); + } + if (json is Map && json.isNotEmpty) { + final entry = json.entries.first; + switch (entry.key) { + case 'UnexpectedDerivationMethod': + return GetEthAddressErrorUnexpectedDerivationMethod.fromJson( + entry.value, + ); + case 'EthActivationV2Error': + return GetEthAddressErrorEthActivationV2Error.fromJson(entry.value); + case 'Internal': + return GetEthAddressErrorInternal.fromJson(entry.value); + } + return GetEthAddressErrorUnknown(type: entry.key, data: entry.value); + } + return GetEthAddressErrorUnknown(type: null, data: json); + } + + final String type; + + JsonMap toJson(); +} + +final class GetEthAddressErrorUnexpectedDerivationMethod + extends GetEthAddressError { + const GetEthAddressErrorUnexpectedDerivationMethod(this.value) + : super(type: 'UnexpectedDerivationMethod'); + + factory GetEthAddressErrorUnexpectedDerivationMethod.fromJson(dynamic json) { + return GetEthAddressErrorUnexpectedDerivationMethod( + UnexpectedDerivationMethod.fromJson(json), + ); + } + + final UnexpectedDerivationMethod value; + + @override + JsonMap toJson() => {'UnexpectedDerivationMethod': value.toJson()}; +} + +final class GetEthAddressErrorEthActivationV2Error extends GetEthAddressError { + const GetEthAddressErrorEthActivationV2Error(this.value) + : super(type: 'EthActivationV2Error'); + + factory GetEthAddressErrorEthActivationV2Error.fromJson(dynamic json) { + return GetEthAddressErrorEthActivationV2Error( + EthActivationV2Error.fromJson(json), + ); + } + + final EthActivationV2Error value; + + @override + JsonMap toJson() => {'EthActivationV2Error': value.toJson()}; +} + +final class GetEthAddressErrorInternal extends GetEthAddressError { + const GetEthAddressErrorInternal(this.value) : super(type: 'Internal'); + + factory GetEthAddressErrorInternal.fromJson(dynamic json) { + return GetEthAddressErrorInternal(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'Internal': value}; +} + +final class GetEthAddressErrorUnknown extends GetEthAddressError { + const GetEthAddressErrorUnknown({required String? type, required this.data}) + : super(type: type ?? 'unknown'); + + final dynamic data; + + @override + JsonMap toJson() => {'type': type, 'data': data}; +} + +sealed class GetInfoFromUriError { + const GetInfoFromUriError({required this.type}); + + factory GetInfoFromUriError.fromJson(dynamic json) { + if (json is String) { + switch (json) { + case 'InvalidRequest': + return GetInfoFromUriErrorInvalidRequest.fromJson(null); + case 'Transport': + return GetInfoFromUriErrorTransport.fromJson(null); + case 'InvalidResponse': + return GetInfoFromUriErrorInvalidResponse.fromJson(null); + case 'Internal': + return GetInfoFromUriErrorInternal.fromJson(null); + } + return GetInfoFromUriErrorUnknown(type: json, data: json); + } + if (json is Map && json.isNotEmpty) { + final entry = json.entries.first; + switch (entry.key) { + case 'InvalidRequest': + return GetInfoFromUriErrorInvalidRequest.fromJson(entry.value); + case 'Transport': + return GetInfoFromUriErrorTransport.fromJson(entry.value); + case 'InvalidResponse': + return GetInfoFromUriErrorInvalidResponse.fromJson(entry.value); + case 'Internal': + return GetInfoFromUriErrorInternal.fromJson(entry.value); + } + return GetInfoFromUriErrorUnknown(type: entry.key, data: entry.value); + } + return GetInfoFromUriErrorUnknown(type: null, data: json); + } + + final String type; + + JsonMap toJson(); +} + +final class GetInfoFromUriErrorInvalidRequest extends GetInfoFromUriError { + const GetInfoFromUriErrorInvalidRequest(this.value) + : super(type: 'InvalidRequest'); + + factory GetInfoFromUriErrorInvalidRequest.fromJson(dynamic json) { + return GetInfoFromUriErrorInvalidRequest(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'InvalidRequest': value}; +} + +final class GetInfoFromUriErrorTransport extends GetInfoFromUriError { + const GetInfoFromUriErrorTransport(this.value) : super(type: 'Transport'); + + factory GetInfoFromUriErrorTransport.fromJson(dynamic json) { + return GetInfoFromUriErrorTransport(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'Transport': value}; +} + +final class GetInfoFromUriErrorInvalidResponse extends GetInfoFromUriError { + const GetInfoFromUriErrorInvalidResponse(this.value) + : super(type: 'InvalidResponse'); + + factory GetInfoFromUriErrorInvalidResponse.fromJson(dynamic json) { + return GetInfoFromUriErrorInvalidResponse(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'InvalidResponse': value}; +} + +final class GetInfoFromUriErrorInternal extends GetInfoFromUriError { + const GetInfoFromUriErrorInternal(this.value) : super(type: 'Internal'); + + factory GetInfoFromUriErrorInternal.fromJson(dynamic json) { + return GetInfoFromUriErrorInternal(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'Internal': value}; +} + +final class GetInfoFromUriErrorUnknown extends GetInfoFromUriError { + const GetInfoFromUriErrorUnknown({required String? type, required this.data}) + : super(type: type ?? 'unknown'); + + final dynamic data; + + @override + JsonMap toJson() => {'type': type, 'data': data}; +} + +enum HwRpcError { + noTrezorDeviceAvailable, + foundMultipleDevices, + foundUnexpectedDevice, + invalidPin, + unexpectedMessage, + buttonExpected, + dataError, + pinExpected, + invalidSignature, + processError, + notEnoughFunds, + notInitialized, + wipeCodeMismatch, + invalidSession, + firmwareError, + failureMessageNotFound, + userCancelled, + pongMessageMismatch, + unknown; + + static HwRpcError fromJson(dynamic json) { + String? value; + if (json is Map && json.isNotEmpty) { + value = json.keys.first.toString(); + } else { + value = _stringFromJson(json); + } + switch (value) { + case 'NoTrezorDeviceAvailable': + return HwRpcError.noTrezorDeviceAvailable; + case 'FoundMultipleDevices': + return HwRpcError.foundMultipleDevices; + case 'FoundUnexpectedDevice': + return HwRpcError.foundUnexpectedDevice; + case 'InvalidPin': + return HwRpcError.invalidPin; + case 'UnexpectedMessage': + return HwRpcError.unexpectedMessage; + case 'ButtonExpected': + return HwRpcError.buttonExpected; + case 'DataError': + return HwRpcError.dataError; + case 'PinExpected': + return HwRpcError.pinExpected; + case 'InvalidSignature': + return HwRpcError.invalidSignature; + case 'ProcessError': + return HwRpcError.processError; + case 'NotEnoughFunds': + return HwRpcError.notEnoughFunds; + case 'NotInitialized': + return HwRpcError.notInitialized; + case 'WipeCodeMismatch': + return HwRpcError.wipeCodeMismatch; + case 'InvalidSession': + return HwRpcError.invalidSession; + case 'FirmwareError': + return HwRpcError.firmwareError; + case 'FailureMessageNotFound': + return HwRpcError.failureMessageNotFound; + case 'UserCancelled': + return HwRpcError.userCancelled; + case 'PongMessageMismatch': + return HwRpcError.pongMessageMismatch; + default: + return HwRpcError.unknown; + } + } + + String toJson() { + switch (this) { + case HwRpcError.noTrezorDeviceAvailable: + return 'NoTrezorDeviceAvailable'; + case HwRpcError.foundMultipleDevices: + return 'FoundMultipleDevices'; + case HwRpcError.foundUnexpectedDevice: + return 'FoundUnexpectedDevice'; + case HwRpcError.invalidPin: + return 'InvalidPin'; + case HwRpcError.unexpectedMessage: + return 'UnexpectedMessage'; + case HwRpcError.buttonExpected: + return 'ButtonExpected'; + case HwRpcError.dataError: + return 'DataError'; + case HwRpcError.pinExpected: + return 'PinExpected'; + case HwRpcError.invalidSignature: + return 'InvalidSignature'; + case HwRpcError.processError: + return 'ProcessError'; + case HwRpcError.notEnoughFunds: + return 'NotEnoughFunds'; + case HwRpcError.notInitialized: + return 'NotInitialized'; + case HwRpcError.wipeCodeMismatch: + return 'WipeCodeMismatch'; + case HwRpcError.invalidSession: + return 'InvalidSession'; + case HwRpcError.firmwareError: + return 'FirmwareError'; + case HwRpcError.failureMessageNotFound: + return 'FailureMessageNotFound'; + case HwRpcError.userCancelled: + return 'UserCancelled'; + case HwRpcError.pongMessageMismatch: + return 'PongMessageMismatch'; + case HwRpcError.unknown: + return 'unknown'; + } + } +} + +enum MetamaskRpcError { + ethProviderNotFound, + userCancelled, + unexpectedAccountSelected, + metamaskCtxNotInitialized, + unknown; + + static MetamaskRpcError fromJson(dynamic json) { + String? value; + if (json is Map && json.isNotEmpty) { + value = json.keys.first.toString(); + } else { + value = _stringFromJson(json); + } + switch (value) { + case 'EthProviderNotFound': + return MetamaskRpcError.ethProviderNotFound; + case 'UserCancelled': + return MetamaskRpcError.userCancelled; + case 'UnexpectedAccountSelected': + return MetamaskRpcError.unexpectedAccountSelected; + case 'MetamaskCtxNotInitialized': + return MetamaskRpcError.metamaskCtxNotInitialized; + default: + return MetamaskRpcError.unknown; + } + } + + String toJson() { + switch (this) { + case MetamaskRpcError.ethProviderNotFound: + return 'EthProviderNotFound'; + case MetamaskRpcError.userCancelled: + return 'UserCancelled'; + case MetamaskRpcError.unexpectedAccountSelected: + return 'UnexpectedAccountSelected'; + case MetamaskRpcError.metamaskCtxNotInitialized: + return 'MetamaskCtxNotInitialized'; + case MetamaskRpcError.unknown: + return 'unknown'; + } + } +} + +sealed class NetIdError { + const NetIdError({required this.type}); + + factory NetIdError.fromJson(dynamic json) { + if (json is String) { + switch (json) { + case 'LargerThanMax': + return NetIdErrorLargerThanMax.fromJson(null); + case 'Deprecated': + return NetIdErrorDeprecated.fromJson(null); + } + return NetIdErrorUnknown(type: json, data: json); + } + if (json is Map && json.isNotEmpty) { + final entry = json.entries.first; + switch (entry.key) { + case 'LargerThanMax': + return NetIdErrorLargerThanMax.fromJson(entry.value); + case 'Deprecated': + return NetIdErrorDeprecated.fromJson(entry.value); + } + return NetIdErrorUnknown(type: entry.key, data: entry.value); + } + return NetIdErrorUnknown(type: null, data: json); + } + + final String type; + + JsonMap toJson(); +} + +final class NetIdErrorLargerThanMax extends NetIdError { + const NetIdErrorLargerThanMax({required this.netid, required this.max_netid}) + : super(type: 'LargerThanMax'); + + factory NetIdErrorLargerThanMax.fromJson(dynamic json) { + final map = _asJsonMap(json); + return NetIdErrorLargerThanMax( + netid: _intFromJson(map.value('netid')), + max_netid: _intFromJson(map.value('max_netid')), + ); + } + + final int netid; + final int max_netid; + + @override + JsonMap toJson() => { + 'LargerThanMax': {'netid': netid, 'max_netid': max_netid}, + }; +} + +final class NetIdErrorDeprecated extends NetIdError { + const NetIdErrorDeprecated({required this.netid}) : super(type: 'Deprecated'); + + factory NetIdErrorDeprecated.fromJson(dynamic json) { + final map = _asJsonMap(json); + return NetIdErrorDeprecated( + netid: _intFromJson(map.value('netid')), + ); + } + + final int netid; + + @override + JsonMap toJson() => { + 'Deprecated': {'netid': netid}, + }; +} + +final class NetIdErrorUnknown extends NetIdError { + const NetIdErrorUnknown({required String? type, required this.data}) + : super(type: type ?? 'unknown'); + + final dynamic data; + + @override + JsonMap toJson() => {'type': type, 'data': data}; +} + +sealed class P2PInitError { + const P2PInitError({required this.type}); + + factory P2PInitError.fromJson(dynamic json) { + if (json is String) { + switch (json) { + case 'InvalidWssCert': + return P2PInitErrorInvalidWssCert.fromJson(null); + case 'ErrorDeserializingConfig': + return P2PInitErrorErrorDeserializingConfig.fromJson(null); + case 'FieldNotFoundInConfig': + return P2PInitErrorFieldNotFoundInConfig.fromJson(null); + case 'ErrorReadingCertFile': + return P2PInitErrorErrorReadingCertFile.fromJson(null); + case 'ErrorGettingMyIpAddr': + return P2PInitErrorErrorGettingMyIpAddr.fromJson(null); + case 'InvalidNetId': + return P2PInitErrorInvalidNetId.fromJson(null); + case 'InvalidRelayAddress': + return P2PInitErrorInvalidRelayAddress.fromJson(null); + case 'WasmNodeCannotBeSeed': + return P2PInitErrorWasmNodeCannotBeSeed.fromJson(); + case 'Precheck': + return P2PInitErrorPrecheck.fromJson(null); + case 'Internal': + return P2PInitErrorInternal.fromJson(null); + } + return P2PInitErrorUnknown(type: json, data: json); + } + if (json is Map && json.isNotEmpty) { + final entry = json.entries.first; + switch (entry.key) { + case 'InvalidWssCert': + return P2PInitErrorInvalidWssCert.fromJson(entry.value); + case 'ErrorDeserializingConfig': + return P2PInitErrorErrorDeserializingConfig.fromJson(entry.value); + case 'FieldNotFoundInConfig': + return P2PInitErrorFieldNotFoundInConfig.fromJson(entry.value); + case 'ErrorReadingCertFile': + return P2PInitErrorErrorReadingCertFile.fromJson(entry.value); + case 'ErrorGettingMyIpAddr': + return P2PInitErrorErrorGettingMyIpAddr.fromJson(entry.value); + case 'InvalidNetId': + return P2PInitErrorInvalidNetId.fromJson(entry.value); + case 'InvalidRelayAddress': + return P2PInitErrorInvalidRelayAddress.fromJson(entry.value); + case 'WasmNodeCannotBeSeed': + return P2PInitErrorWasmNodeCannotBeSeed.fromJson(); + case 'Precheck': + return P2PInitErrorPrecheck.fromJson(entry.value); + case 'Internal': + return P2PInitErrorInternal.fromJson(entry.value); + } + return P2PInitErrorUnknown(type: entry.key, data: entry.value); + } + return P2PInitErrorUnknown(type: null, data: json); + } + + final String type; + + JsonMap toJson(); +} + +final class P2PInitErrorInvalidWssCert extends P2PInitError { + const P2PInitErrorInvalidWssCert({ + required this.path, + required this.expected_format, + }) : super(type: 'InvalidWssCert'); + + factory P2PInitErrorInvalidWssCert.fromJson(dynamic json) { + final map = _asJsonMap(json); + return P2PInitErrorInvalidWssCert( + path: PathBuf.fromJson(map.value('path')), + expected_format: _stringFromJson(map.value('expected_format')), + ); + } + + final PathBuf path; + final String expected_format; + + @override + JsonMap toJson() => { + 'InvalidWssCert': { + 'path': path.toJson(), + 'expected_format': expected_format, + }, + }; +} + +final class P2PInitErrorErrorDeserializingConfig extends P2PInitError { + const P2PInitErrorErrorDeserializingConfig({ + required this.field, + required this.error, + }) : super(type: 'ErrorDeserializingConfig'); + + factory P2PInitErrorErrorDeserializingConfig.fromJson(dynamic json) { + final map = _asJsonMap(json); + return P2PInitErrorErrorDeserializingConfig( + field: _stringFromJson(map.value('field')), + error: _stringFromJson(map.value('error')), + ); + } + + final String field; + final String error; + + @override + JsonMap toJson() => { + 'ErrorDeserializingConfig': {'field': field, 'error': error}, + }; +} + +final class P2PInitErrorFieldNotFoundInConfig extends P2PInitError { + const P2PInitErrorFieldNotFoundInConfig({required this.field}) + : super(type: 'FieldNotFoundInConfig'); + + factory P2PInitErrorFieldNotFoundInConfig.fromJson(dynamic json) { + final map = _asJsonMap(json); + return P2PInitErrorFieldNotFoundInConfig( + field: _stringFromJson(map.value('field')), + ); + } + + final String field; + + @override + JsonMap toJson() => { + 'FieldNotFoundInConfig': {'field': field}, + }; +} + +final class P2PInitErrorErrorReadingCertFile extends P2PInitError { + const P2PInitErrorErrorReadingCertFile({ + required this.path, + required this.error, + }) : super(type: 'ErrorReadingCertFile'); + + factory P2PInitErrorErrorReadingCertFile.fromJson(dynamic json) { + final map = _asJsonMap(json); + return P2PInitErrorErrorReadingCertFile( + path: PathBuf.fromJson(map.value('path')), + error: _stringFromJson(map.value('error')), + ); + } + + final PathBuf path; + final String error; + + @override + JsonMap toJson() => { + 'ErrorReadingCertFile': {'path': path.toJson(), 'error': error}, + }; +} + +final class P2PInitErrorErrorGettingMyIpAddr extends P2PInitError { + const P2PInitErrorErrorGettingMyIpAddr(this.value) + : super(type: 'ErrorGettingMyIpAddr'); + + factory P2PInitErrorErrorGettingMyIpAddr.fromJson(dynamic json) { + return P2PInitErrorErrorGettingMyIpAddr(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'ErrorGettingMyIpAddr': value}; +} + +final class P2PInitErrorInvalidNetId extends P2PInitError { + const P2PInitErrorInvalidNetId(this.value) : super(type: 'InvalidNetId'); + + factory P2PInitErrorInvalidNetId.fromJson(dynamic json) { + return P2PInitErrorInvalidNetId(NetIdError.fromJson(json)); + } + + final NetIdError value; + + @override + JsonMap toJson() => {'InvalidNetId': value.toJson()}; +} + +final class P2PInitErrorInvalidRelayAddress extends P2PInitError { + const P2PInitErrorInvalidRelayAddress(this.value) + : super(type: 'InvalidRelayAddress'); + + factory P2PInitErrorInvalidRelayAddress.fromJson(dynamic json) { + return P2PInitErrorInvalidRelayAddress(RelayAddressError.fromJson(json)); + } + + final RelayAddressError value; + + @override + JsonMap toJson() => {'InvalidRelayAddress': value.toJson()}; +} + +final class P2PInitErrorWasmNodeCannotBeSeed extends P2PInitError { + const P2PInitErrorWasmNodeCannotBeSeed() + : super(type: 'WasmNodeCannotBeSeed'); + + factory P2PInitErrorWasmNodeCannotBeSeed.fromJson() => + const P2PInitErrorWasmNodeCannotBeSeed(); + + @override + JsonMap toJson() => {'WasmNodeCannotBeSeed': null}; +} + +final class P2PInitErrorPrecheck extends P2PInitError { + const P2PInitErrorPrecheck({required this.reason}) : super(type: 'Precheck'); + + factory P2PInitErrorPrecheck.fromJson(dynamic json) { + final map = _asJsonMap(json); + return P2PInitErrorPrecheck( + reason: _stringFromJson(map.value('reason')), + ); + } + + final String reason; + + @override + JsonMap toJson() => { + 'Precheck': {'reason': reason}, + }; +} + +final class P2PInitErrorInternal extends P2PInitError { + const P2PInitErrorInternal(this.value) : super(type: 'Internal'); + + factory P2PInitErrorInternal.fromJson(dynamic json) { + return P2PInitErrorInternal(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'Internal': value}; +} + +final class P2PInitErrorUnknown extends P2PInitError { + const P2PInitErrorUnknown({required String? type, required this.data}) + : super(type: type ?? 'unknown'); + + final dynamic data; + + @override + JsonMap toJson() => {'type': type, 'data': data}; +} + +sealed class ParseRfc3339Err { + const ParseRfc3339Err({required this.type}); + + factory ParseRfc3339Err.fromJson(dynamic json) { + if (json is String) { + switch (json) { + case 'ParseTimestampError': + return ParseRfc3339ErrParseTimestampError.fromJson(null); + case 'TryFromIntError': + return ParseRfc3339ErrTryFromIntError.fromJson(null); + } + return ParseRfc3339ErrUnknown(type: json, data: json); + } + if (json is Map && json.isNotEmpty) { + final entry = json.entries.first; + switch (entry.key) { + case 'ParseTimestampError': + return ParseRfc3339ErrParseTimestampError.fromJson(entry.value); + case 'TryFromIntError': + return ParseRfc3339ErrTryFromIntError.fromJson(entry.value); + } + return ParseRfc3339ErrUnknown(type: entry.key, data: entry.value); + } + return ParseRfc3339ErrUnknown(type: null, data: json); + } + + final String type; + + JsonMap toJson(); +} + +final class ParseRfc3339ErrParseTimestampError extends ParseRfc3339Err { + const ParseRfc3339ErrParseTimestampError(this.value) + : super(type: 'ParseTimestampError'); + + factory ParseRfc3339ErrParseTimestampError.fromJson(dynamic json) { + return ParseRfc3339ErrParseTimestampError(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'ParseTimestampError': value}; +} + +final class ParseRfc3339ErrTryFromIntError extends ParseRfc3339Err { + const ParseRfc3339ErrTryFromIntError(this.value) + : super(type: 'TryFromIntError'); + + factory ParseRfc3339ErrTryFromIntError.fromJson(dynamic json) { + return ParseRfc3339ErrTryFromIntError(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'TryFromIntError': value}; +} + +final class ParseRfc3339ErrUnknown extends ParseRfc3339Err { + const ParseRfc3339ErrUnknown({required String? type, required this.data}) + : super(type: type ?? 'unknown'); + + final dynamic data; + + @override + JsonMap toJson() => {'type': type, 'data': data}; +} + +sealed class PrivKeyPolicyNotAllowed { + const PrivKeyPolicyNotAllowed({required this.type}); + + factory PrivKeyPolicyNotAllowed.fromJson(dynamic json) { + if (json is String) { + switch (json) { + case 'HardwareWalletNotSupported': + return PrivKeyPolicyNotAllowedHardwareWalletNotSupported.fromJson(); + case 'UnsupportedMethod': + return PrivKeyPolicyNotAllowedUnsupportedMethod.fromJson(null); + case 'InternalError': + return PrivKeyPolicyNotAllowedInternalError.fromJson(null); + } + return PrivKeyPolicyNotAllowedUnknown(type: json, data: json); + } + if (json is Map && json.isNotEmpty) { + final entry = json.entries.first; + switch (entry.key) { + case 'HardwareWalletNotSupported': + return PrivKeyPolicyNotAllowedHardwareWalletNotSupported.fromJson(); + case 'UnsupportedMethod': + return PrivKeyPolicyNotAllowedUnsupportedMethod.fromJson(entry.value); + case 'InternalError': + return PrivKeyPolicyNotAllowedInternalError.fromJson(entry.value); + } + return PrivKeyPolicyNotAllowedUnknown(type: entry.key, data: entry.value); + } + return PrivKeyPolicyNotAllowedUnknown(type: null, data: json); + } + + final String type; + + JsonMap toJson(); +} + +final class PrivKeyPolicyNotAllowedHardwareWalletNotSupported + extends PrivKeyPolicyNotAllowed { + const PrivKeyPolicyNotAllowedHardwareWalletNotSupported() + : super(type: 'HardwareWalletNotSupported'); + + factory PrivKeyPolicyNotAllowedHardwareWalletNotSupported.fromJson() => + const PrivKeyPolicyNotAllowedHardwareWalletNotSupported(); + + @override + JsonMap toJson() => {'HardwareWalletNotSupported': null}; +} + +final class PrivKeyPolicyNotAllowedUnsupportedMethod + extends PrivKeyPolicyNotAllowed { + const PrivKeyPolicyNotAllowedUnsupportedMethod(this.value) + : super(type: 'UnsupportedMethod'); + + factory PrivKeyPolicyNotAllowedUnsupportedMethod.fromJson(dynamic json) { + return PrivKeyPolicyNotAllowedUnsupportedMethod(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'UnsupportedMethod': value}; +} + +final class PrivKeyPolicyNotAllowedInternalError + extends PrivKeyPolicyNotAllowed { + const PrivKeyPolicyNotAllowedInternalError(this.value) + : super(type: 'InternalError'); + + factory PrivKeyPolicyNotAllowedInternalError.fromJson(dynamic json) { + return PrivKeyPolicyNotAllowedInternalError(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'InternalError': value}; +} + +final class PrivKeyPolicyNotAllowedUnknown extends PrivKeyPolicyNotAllowed { + const PrivKeyPolicyNotAllowedUnknown({ + required String? type, + required this.data, + }) : super(type: type ?? 'unknown'); + + final dynamic data; + + @override + JsonMap toJson() => {'type': type, 'data': data}; +} + +enum ProtectFromSpamError { + unknown; + + static ProtectFromSpamError fromJson(dynamic json) { + String? value; + if (json is Map && json.isNotEmpty) { + value = json.keys.first.toString(); + } else { + value = _stringFromJson(json); + } + switch (value) { + default: + return ProtectFromSpamError.unknown; + } + } + + String toJson() { + switch (this) { + case ProtectFromSpamError.unknown: + return 'unknown'; + } + } +} + +sealed class RelayAddressError { + const RelayAddressError({required this.type}); + + factory RelayAddressError.fromJson(dynamic json) { + if (json is String) { + switch (json) { + case 'FromStrError': + return RelayAddressErrorFromStrError.fromJson(null); + case 'DistributedAddrOnMemoryNetwork': + return RelayAddressErrorDistributedAddrOnMemoryNetwork.fromJson(null); + case 'MemoryAddrOnDistributedNetwork': + return RelayAddressErrorMemoryAddrOnDistributedNetwork.fromJson(null); + } + return RelayAddressErrorUnknown(type: json, data: json); + } + if (json is Map && json.isNotEmpty) { + final entry = json.entries.first; + switch (entry.key) { + case 'FromStrError': + return RelayAddressErrorFromStrError.fromJson(entry.value); + case 'DistributedAddrOnMemoryNetwork': + return RelayAddressErrorDistributedAddrOnMemoryNetwork.fromJson( + entry.value, + ); + case 'MemoryAddrOnDistributedNetwork': + return RelayAddressErrorMemoryAddrOnDistributedNetwork.fromJson( + entry.value, + ); + } + return RelayAddressErrorUnknown(type: entry.key, data: entry.value); + } + return RelayAddressErrorUnknown(type: null, data: json); + } + + final String type; + + JsonMap toJson(); +} + +final class RelayAddressErrorFromStrError extends RelayAddressError { + const RelayAddressErrorFromStrError({required this.found}) + : super(type: 'FromStrError'); + + factory RelayAddressErrorFromStrError.fromJson(dynamic json) { + final map = _asJsonMap(json); + return RelayAddressErrorFromStrError( + found: _stringFromJson(map.value('found')), + ); + } + + final String found; + + @override + JsonMap toJson() => { + 'FromStrError': {'found': found}, + }; +} + +final class RelayAddressErrorDistributedAddrOnMemoryNetwork + extends RelayAddressError { + const RelayAddressErrorDistributedAddrOnMemoryNetwork({ + required this.self_str, + }) : super(type: 'DistributedAddrOnMemoryNetwork'); + + factory RelayAddressErrorDistributedAddrOnMemoryNetwork.fromJson( + dynamic json, + ) { + final map = _asJsonMap(json); + return RelayAddressErrorDistributedAddrOnMemoryNetwork( + self_str: _stringFromJson(map.value('self_str')), + ); + } + + final String self_str; + + @override + JsonMap toJson() => { + 'DistributedAddrOnMemoryNetwork': {'self_str': self_str}, + }; +} + +final class RelayAddressErrorMemoryAddrOnDistributedNetwork + extends RelayAddressError { + const RelayAddressErrorMemoryAddrOnDistributedNetwork({ + required this.self_str, + }) : super(type: 'MemoryAddrOnDistributedNetwork'); + + factory RelayAddressErrorMemoryAddrOnDistributedNetwork.fromJson( + dynamic json, + ) { + final map = _asJsonMap(json); + return RelayAddressErrorMemoryAddrOnDistributedNetwork( + self_str: _stringFromJson(map.value('self_str')), + ); + } + + final String self_str; + + @override + JsonMap toJson() => { + 'MemoryAddrOnDistributedNetwork': {'self_str': self_str}, + }; +} + +final class RelayAddressErrorUnknown extends RelayAddressError { + const RelayAddressErrorUnknown({required String? type, required this.data}) + : super(type: type ?? 'unknown'); + + final dynamic data; + + @override + JsonMap toJson() => {'type': type, 'data': data}; +} + +sealed class SavedSwapError { + const SavedSwapError({required this.type}); + + factory SavedSwapError.fromJson(dynamic json) { + if (json is String) { + switch (json) { + case 'ErrorSaving': + return SavedSwapErrorErrorSaving.fromJson(null); + case 'ErrorLoading': + return SavedSwapErrorErrorLoading.fromJson(null); + case 'ErrorDeserializing': + return SavedSwapErrorErrorDeserializing.fromJson(null); + case 'ErrorSerializing': + return SavedSwapErrorErrorSerializing.fromJson(null); + case 'CursorError': + return SavedSwapErrorCursorError.fromJson(null); + case 'InternalError': + return SavedSwapErrorInternalError.fromJson(null); + } + return SavedSwapErrorUnknown(type: json, data: json); + } + if (json is Map && json.isNotEmpty) { + final entry = json.entries.first; + switch (entry.key) { + case 'ErrorSaving': + return SavedSwapErrorErrorSaving.fromJson(entry.value); + case 'ErrorLoading': + return SavedSwapErrorErrorLoading.fromJson(entry.value); + case 'ErrorDeserializing': + return SavedSwapErrorErrorDeserializing.fromJson(entry.value); + case 'ErrorSerializing': + return SavedSwapErrorErrorSerializing.fromJson(entry.value); + case 'CursorError': + return SavedSwapErrorCursorError.fromJson(entry.value); + case 'InternalError': + return SavedSwapErrorInternalError.fromJson(entry.value); + } + return SavedSwapErrorUnknown(type: entry.key, data: entry.value); + } + return SavedSwapErrorUnknown(type: null, data: json); + } + + final String type; + + JsonMap toJson(); +} + +final class SavedSwapErrorErrorSaving extends SavedSwapError { + const SavedSwapErrorErrorSaving(this.value) : super(type: 'ErrorSaving'); + + factory SavedSwapErrorErrorSaving.fromJson(dynamic json) { + return SavedSwapErrorErrorSaving(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'ErrorSaving': value}; +} + +final class SavedSwapErrorErrorLoading extends SavedSwapError { + const SavedSwapErrorErrorLoading(this.value) : super(type: 'ErrorLoading'); + + factory SavedSwapErrorErrorLoading.fromJson(dynamic json) { + return SavedSwapErrorErrorLoading(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'ErrorLoading': value}; +} + +final class SavedSwapErrorErrorDeserializing extends SavedSwapError { + const SavedSwapErrorErrorDeserializing(this.value) + : super(type: 'ErrorDeserializing'); + + factory SavedSwapErrorErrorDeserializing.fromJson(dynamic json) { + return SavedSwapErrorErrorDeserializing(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'ErrorDeserializing': value}; +} + +final class SavedSwapErrorErrorSerializing extends SavedSwapError { + const SavedSwapErrorErrorSerializing(this.value) + : super(type: 'ErrorSerializing'); + + factory SavedSwapErrorErrorSerializing.fromJson(dynamic json) { + return SavedSwapErrorErrorSerializing(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'ErrorSerializing': value}; +} + +final class SavedSwapErrorCursorError extends SavedSwapError { + const SavedSwapErrorCursorError(this.value) : super(type: 'CursorError'); + + factory SavedSwapErrorCursorError.fromJson(dynamic json) { + return SavedSwapErrorCursorError(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'CursorError': value}; +} + +final class SavedSwapErrorInternalError extends SavedSwapError { + const SavedSwapErrorInternalError(this.value) : super(type: 'InternalError'); + + factory SavedSwapErrorInternalError.fromJson(dynamic json) { + return SavedSwapErrorInternalError(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'InternalError': value}; +} + +final class SavedSwapErrorUnknown extends SavedSwapError { + const SavedSwapErrorUnknown({required String? type, required this.data}) + : super(type: type ?? 'unknown'); + + final dynamic data; + + @override + JsonMap toJson() => {'type': type, 'data': data}; +} + +sealed class SlurpError { + const SlurpError({required this.type}); + + factory SlurpError.fromJson(dynamic json) { + if (json is String) { + switch (json) { + case 'ErrorDeserializing': + return SlurpErrorErrorDeserializing.fromJson(null); + case 'InvalidRequest': + return SlurpErrorInvalidRequest.fromJson(null); + case 'Timeout': + return SlurpErrorTimeout.fromJson(null); + case 'Transport': + return SlurpErrorTransport.fromJson(null); + case 'Internal': + return SlurpErrorInternal.fromJson(null); + } + return SlurpErrorUnknown(type: json, data: json); + } + if (json is Map && json.isNotEmpty) { + final entry = json.entries.first; + switch (entry.key) { + case 'ErrorDeserializing': + return SlurpErrorErrorDeserializing.fromJson(entry.value); + case 'InvalidRequest': + return SlurpErrorInvalidRequest.fromJson(entry.value); + case 'Timeout': + return SlurpErrorTimeout.fromJson(entry.value); + case 'Transport': + return SlurpErrorTransport.fromJson(entry.value); + case 'Internal': + return SlurpErrorInternal.fromJson(entry.value); + } + return SlurpErrorUnknown(type: entry.key, data: entry.value); + } + return SlurpErrorUnknown(type: null, data: json); + } + + final String type; + + JsonMap toJson(); +} + +final class SlurpErrorErrorDeserializing extends SlurpError { + const SlurpErrorErrorDeserializing({required this.uri, required this.error}) + : super(type: 'ErrorDeserializing'); + + factory SlurpErrorErrorDeserializing.fromJson(dynamic json) { + final map = _asJsonMap(json); + return SlurpErrorErrorDeserializing( + uri: _stringFromJson(map.value('uri')), + error: _stringFromJson(map.value('error')), + ); + } + + final String uri; + final String error; + + @override + JsonMap toJson() => { + 'ErrorDeserializing': {'uri': uri, 'error': error}, + }; +} + +final class SlurpErrorInvalidRequest extends SlurpError { + const SlurpErrorInvalidRequest(this.value) : super(type: 'InvalidRequest'); + + factory SlurpErrorInvalidRequest.fromJson(dynamic json) { + return SlurpErrorInvalidRequest(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'InvalidRequest': value}; +} + +final class SlurpErrorTimeout extends SlurpError { + const SlurpErrorTimeout({required this.uri, required this.error}) + : super(type: 'Timeout'); + + factory SlurpErrorTimeout.fromJson(dynamic json) { + final map = _asJsonMap(json); + return SlurpErrorTimeout( + uri: _stringFromJson(map.value('uri')), + error: _stringFromJson(map.value('error')), + ); + } + + final String uri; + final String error; + + @override + JsonMap toJson() => { + 'Timeout': {'uri': uri, 'error': error}, + }; +} + +final class SlurpErrorTransport extends SlurpError { + const SlurpErrorTransport({required this.uri, required this.error}) + : super(type: 'Transport'); + + factory SlurpErrorTransport.fromJson(dynamic json) { + final map = _asJsonMap(json); + return SlurpErrorTransport( + uri: _stringFromJson(map.value('uri')), + error: _stringFromJson(map.value('error')), + ); + } + + final String uri; + final String error; + + @override + JsonMap toJson() => { + 'Transport': {'uri': uri, 'error': error}, + }; +} + +final class SlurpErrorInternal extends SlurpError { + const SlurpErrorInternal(this.value) : super(type: 'Internal'); + + factory SlurpErrorInternal.fromJson(dynamic json) { + return SlurpErrorInternal(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'Internal': value}; +} + +final class SlurpErrorUnknown extends SlurpError { + const SlurpErrorUnknown({required String? type, required this.data}) + : super(type: type ?? 'unknown'); + + final dynamic data; + + @override + JsonMap toJson() => {'type': type, 'data': data}; +} + +sealed class TendermintIBCError { + const TendermintIBCError({required this.type}); + + factory TendermintIBCError.fromJson(dynamic json) { + if (json is String) { + switch (json) { + case 'IBCChannelCouldNotBeFound': + return TendermintIBCErrorIBCChannelCouldNotBeFound.fromJson(null); + case 'IBCChannelNotHealthy': + return TendermintIBCErrorIBCChannelNotHealthy.fromJson(null); + case 'IBCChannelMissingOnNode': + return TendermintIBCErrorIBCChannelMissingOnNode.fromJson(null); + case 'Transport': + return TendermintIBCErrorTransport.fromJson(null); + case 'InternalError': + return TendermintIBCErrorInternalError.fromJson(null); + } + return TendermintIBCErrorUnknown(type: json, data: json); + } + if (json is Map && json.isNotEmpty) { + final entry = json.entries.first; + switch (entry.key) { + case 'IBCChannelCouldNotBeFound': + return TendermintIBCErrorIBCChannelCouldNotBeFound.fromJson( + entry.value, + ); + case 'IBCChannelNotHealthy': + return TendermintIBCErrorIBCChannelNotHealthy.fromJson(entry.value); + case 'IBCChannelMissingOnNode': + return TendermintIBCErrorIBCChannelMissingOnNode.fromJson( + entry.value, + ); + case 'Transport': + return TendermintIBCErrorTransport.fromJson(entry.value); + case 'InternalError': + return TendermintIBCErrorInternalError.fromJson(entry.value); + } + return TendermintIBCErrorUnknown(type: entry.key, data: entry.value); + } + return TendermintIBCErrorUnknown(type: null, data: json); + } + + final String type; + + JsonMap toJson(); +} + +final class TendermintIBCErrorIBCChannelCouldNotBeFound + extends TendermintIBCError { + const TendermintIBCErrorIBCChannelCouldNotBeFound({ + required this.address_prefix, + }) : super(type: 'IBCChannelCouldNotBeFound'); + + factory TendermintIBCErrorIBCChannelCouldNotBeFound.fromJson(dynamic json) { + final map = _asJsonMap(json); + return TendermintIBCErrorIBCChannelCouldNotBeFound( + address_prefix: _stringFromJson(map.value('address_prefix')), + ); + } + + final String address_prefix; + + @override + JsonMap toJson() => { + 'IBCChannelCouldNotBeFound': {'address_prefix': address_prefix}, + }; +} + +final class TendermintIBCErrorIBCChannelNotHealthy extends TendermintIBCError { + const TendermintIBCErrorIBCChannelNotHealthy({required this.channel_id}) + : super(type: 'IBCChannelNotHealthy'); + + factory TendermintIBCErrorIBCChannelNotHealthy.fromJson(dynamic json) { + final map = _asJsonMap(json); + return TendermintIBCErrorIBCChannelNotHealthy( + channel_id: ChannelId.fromJson(map.value('channel_id')), + ); + } + + final ChannelId channel_id; + + @override + JsonMap toJson() => { + 'IBCChannelNotHealthy': {'channel_id': channel_id.toJson()}, + }; +} + +final class TendermintIBCErrorIBCChannelMissingOnNode + extends TendermintIBCError { + const TendermintIBCErrorIBCChannelMissingOnNode({required this.channel_id}) + : super(type: 'IBCChannelMissingOnNode'); + + factory TendermintIBCErrorIBCChannelMissingOnNode.fromJson(dynamic json) { + final map = _asJsonMap(json); + return TendermintIBCErrorIBCChannelMissingOnNode( + channel_id: ChannelId.fromJson(map.value('channel_id')), + ); + } + + final ChannelId channel_id; + + @override + JsonMap toJson() => { + 'IBCChannelMissingOnNode': {'channel_id': channel_id.toJson()}, + }; +} + +final class TendermintIBCErrorTransport extends TendermintIBCError { + const TendermintIBCErrorTransport({required this.reason}) + : super(type: 'Transport'); + + factory TendermintIBCErrorTransport.fromJson(dynamic json) { + final map = _asJsonMap(json); + return TendermintIBCErrorTransport( + reason: _stringFromJson(map.value('reason')), + ); + } + + final String reason; + + @override + JsonMap toJson() => { + 'Transport': {'reason': reason}, + }; +} + +final class TendermintIBCErrorInternalError extends TendermintIBCError { + const TendermintIBCErrorInternalError({required this.reason}) + : super(type: 'InternalError'); + + factory TendermintIBCErrorInternalError.fromJson(dynamic json) { + final map = _asJsonMap(json); + return TendermintIBCErrorInternalError( + reason: _stringFromJson(map.value('reason')), + ); + } + + final String reason; + + @override + JsonMap toJson() => { + 'InternalError': {'reason': reason}, + }; +} + +final class TendermintIBCErrorUnknown extends TendermintIBCError { + const TendermintIBCErrorUnknown({required String? type, required this.data}) + : super(type: type ?? 'unknown'); + + final dynamic data; + + @override + JsonMap toJson() => {'type': type, 'data': data}; +} + +enum TransferConfirmationsError { + unknown; + + static TransferConfirmationsError fromJson(dynamic json) { + String? value; + if (json is Map && json.isNotEmpty) { + value = json.keys.first.toString(); + } else { + value = _stringFromJson(json); + } + switch (value) { + default: + return TransferConfirmationsError.unknown; + } + } + + String toJson() { + switch (this) { + case TransferConfirmationsError.unknown: + return 'unknown'; + } + } +} + +sealed class UnexpectedDerivationMethod { + const UnexpectedDerivationMethod({required this.type}); + + factory UnexpectedDerivationMethod.fromJson(dynamic json) { + if (json is String) { + switch (json) { + case 'ExpectedSingleAddress': + return UnexpectedDerivationMethodExpectedSingleAddress.fromJson(); + case 'ExpectedHDWallet': + return UnexpectedDerivationMethodExpectedHDWallet.fromJson(); + case 'Trezor': + return UnexpectedDerivationMethodTrezor.fromJson(); + case 'UnsupportedError': + return UnexpectedDerivationMethodUnsupportedError.fromJson(null); + case 'InternalError': + return UnexpectedDerivationMethodInternalError.fromJson(null); + } + return UnexpectedDerivationMethodUnknown(type: json, data: json); + } + if (json is Map && json.isNotEmpty) { + final entry = json.entries.first; + switch (entry.key) { + case 'ExpectedSingleAddress': + return UnexpectedDerivationMethodExpectedSingleAddress.fromJson(); + case 'ExpectedHDWallet': + return UnexpectedDerivationMethodExpectedHDWallet.fromJson(); + case 'Trezor': + return UnexpectedDerivationMethodTrezor.fromJson(); + case 'UnsupportedError': + return UnexpectedDerivationMethodUnsupportedError.fromJson( + entry.value, + ); + case 'InternalError': + return UnexpectedDerivationMethodInternalError.fromJson(entry.value); + } + return UnexpectedDerivationMethodUnknown( + type: entry.key, + data: entry.value, + ); + } + return UnexpectedDerivationMethodUnknown(type: null, data: json); + } + + final String type; + + JsonMap toJson(); +} + +final class UnexpectedDerivationMethodExpectedSingleAddress + extends UnexpectedDerivationMethod { + const UnexpectedDerivationMethodExpectedSingleAddress() + : super(type: 'ExpectedSingleAddress'); + + factory UnexpectedDerivationMethodExpectedSingleAddress.fromJson() => + const UnexpectedDerivationMethodExpectedSingleAddress(); + + @override + JsonMap toJson() => {'ExpectedSingleAddress': null}; +} + +final class UnexpectedDerivationMethodExpectedHDWallet + extends UnexpectedDerivationMethod { + const UnexpectedDerivationMethodExpectedHDWallet() + : super(type: 'ExpectedHDWallet'); + + factory UnexpectedDerivationMethodExpectedHDWallet.fromJson() => + const UnexpectedDerivationMethodExpectedHDWallet(); + + @override + JsonMap toJson() => {'ExpectedHDWallet': null}; +} + +final class UnexpectedDerivationMethodTrezor + extends UnexpectedDerivationMethod { + const UnexpectedDerivationMethodTrezor() : super(type: 'Trezor'); + + factory UnexpectedDerivationMethodTrezor.fromJson() => + const UnexpectedDerivationMethodTrezor(); + + @override + JsonMap toJson() => {'Trezor': null}; +} + +final class UnexpectedDerivationMethodUnsupportedError + extends UnexpectedDerivationMethod { + const UnexpectedDerivationMethodUnsupportedError(this.value) + : super(type: 'UnsupportedError'); + + factory UnexpectedDerivationMethodUnsupportedError.fromJson(dynamic json) { + return UnexpectedDerivationMethodUnsupportedError(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'UnsupportedError': value}; +} + +final class UnexpectedDerivationMethodInternalError + extends UnexpectedDerivationMethod { + const UnexpectedDerivationMethodInternalError(this.value) + : super(type: 'InternalError'); + + factory UnexpectedDerivationMethodInternalError.fromJson(dynamic json) { + return UnexpectedDerivationMethodInternalError(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'InternalError': value}; +} + +final class UnexpectedDerivationMethodUnknown + extends UnexpectedDerivationMethod { + const UnexpectedDerivationMethodUnknown({ + required String? type, + required this.data, + }) : super(type: type ?? 'unknown'); + + final dynamic data; + + @override + JsonMap toJson() => {'type': type, 'data': data}; +} + +sealed class UpdateSpamPhishingError { + const UpdateSpamPhishingError({required this.type}); + + factory UpdateSpamPhishingError.fromJson(dynamic json) { + if (json is String) { + switch (json) { + case 'InvalidRequest': + return UpdateSpamPhishingErrorInvalidRequest.fromJson(null); + case 'Transport': + return UpdateSpamPhishingErrorTransport.fromJson(null); + case 'InvalidResponse': + return UpdateSpamPhishingErrorInvalidResponse.fromJson(null); + case 'Internal': + return UpdateSpamPhishingErrorInternal.fromJson(null); + case 'DbError': + return UpdateSpamPhishingErrorDbError.fromJson(null); + case 'GetMyAddressError': + return UpdateSpamPhishingErrorGetMyAddressError.fromJson(null); + } + return UpdateSpamPhishingErrorUnknown(type: json, data: json); + } + if (json is Map && json.isNotEmpty) { + final entry = json.entries.first; + switch (entry.key) { + case 'InvalidRequest': + return UpdateSpamPhishingErrorInvalidRequest.fromJson(entry.value); + case 'Transport': + return UpdateSpamPhishingErrorTransport.fromJson(entry.value); + case 'InvalidResponse': + return UpdateSpamPhishingErrorInvalidResponse.fromJson(entry.value); + case 'Internal': + return UpdateSpamPhishingErrorInternal.fromJson(entry.value); + case 'DbError': + return UpdateSpamPhishingErrorDbError.fromJson(entry.value); + case 'GetMyAddressError': + return UpdateSpamPhishingErrorGetMyAddressError.fromJson(entry.value); + } + return UpdateSpamPhishingErrorUnknown(type: entry.key, data: entry.value); + } + return UpdateSpamPhishingErrorUnknown(type: null, data: json); + } + + final String type; + + JsonMap toJson(); +} + +final class UpdateSpamPhishingErrorInvalidRequest + extends UpdateSpamPhishingError { + const UpdateSpamPhishingErrorInvalidRequest(this.value) + : super(type: 'InvalidRequest'); + + factory UpdateSpamPhishingErrorInvalidRequest.fromJson(dynamic json) { + return UpdateSpamPhishingErrorInvalidRequest(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'InvalidRequest': value}; +} + +final class UpdateSpamPhishingErrorTransport extends UpdateSpamPhishingError { + const UpdateSpamPhishingErrorTransport(this.value) : super(type: 'Transport'); + + factory UpdateSpamPhishingErrorTransport.fromJson(dynamic json) { + return UpdateSpamPhishingErrorTransport(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'Transport': value}; +} + +final class UpdateSpamPhishingErrorInvalidResponse + extends UpdateSpamPhishingError { + const UpdateSpamPhishingErrorInvalidResponse(this.value) + : super(type: 'InvalidResponse'); + + factory UpdateSpamPhishingErrorInvalidResponse.fromJson(dynamic json) { + return UpdateSpamPhishingErrorInvalidResponse(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'InvalidResponse': value}; +} + +final class UpdateSpamPhishingErrorInternal extends UpdateSpamPhishingError { + const UpdateSpamPhishingErrorInternal(this.value) : super(type: 'Internal'); + + factory UpdateSpamPhishingErrorInternal.fromJson(dynamic json) { + return UpdateSpamPhishingErrorInternal(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'Internal': value}; +} + +final class UpdateSpamPhishingErrorDbError extends UpdateSpamPhishingError { + const UpdateSpamPhishingErrorDbError(this.value) : super(type: 'DbError'); + + factory UpdateSpamPhishingErrorDbError.fromJson(dynamic json) { + return UpdateSpamPhishingErrorDbError(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'DbError': value}; +} + +final class UpdateSpamPhishingErrorGetMyAddressError + extends UpdateSpamPhishingError { + const UpdateSpamPhishingErrorGetMyAddressError(this.value) + : super(type: 'GetMyAddressError'); + + factory UpdateSpamPhishingErrorGetMyAddressError.fromJson(dynamic json) { + return UpdateSpamPhishingErrorGetMyAddressError( + GetMyAddressError.fromJson(json), + ); + } + + final GetMyAddressError value; + + @override + JsonMap toJson() => {'GetMyAddressError': value.toJson()}; +} + +final class UpdateSpamPhishingErrorUnknown extends UpdateSpamPhishingError { + const UpdateSpamPhishingErrorUnknown({ + required String? type, + required this.data, + }) : super(type: type ?? 'unknown'); + + final dynamic data; + + @override + JsonMap toJson() => {'type': type, 'data': data}; +} + +sealed class Web3RpcError { + const Web3RpcError({required this.errorType}); + + factory Web3RpcError.fromJson(dynamic json) { + final map = _asJsonMap(json); + final type = map.valueOrNull('error_type'); + final data = map.valueOrNull('error_data'); + switch (type) { + case 'Transport': + return Web3RpcErrorTransport.fromJson(data); + case 'InvalidResponse': + return Web3RpcErrorInvalidResponse.fromJson(data); + case 'Timeout': + return Web3RpcErrorTimeout.fromJson(data); + case 'Internal': + return Web3RpcErrorInternal.fromJson(data); + case 'InvalidGasApiConfig': + return Web3RpcErrorInvalidGasApiConfig.fromJson(data); + case 'NftProtocolNotSupported': + return Web3RpcErrorNftProtocolNotSupported.fromJson(); + case 'NumConversError': + return Web3RpcErrorNumConversError.fromJson(data); + default: + return Web3RpcErrorUnknown(rawErrorType: type, data: data); + } + } + + final String errorType; + + JsonMap toJson(); +} + +final class Web3RpcErrorTransport extends Web3RpcError { + const Web3RpcErrorTransport(this.value) : super(errorType: 'Transport'); + + factory Web3RpcErrorTransport.fromJson(dynamic json) { + return Web3RpcErrorTransport(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class Web3RpcErrorInvalidResponse extends Web3RpcError { + const Web3RpcErrorInvalidResponse(this.value) + : super(errorType: 'InvalidResponse'); + + factory Web3RpcErrorInvalidResponse.fromJson(dynamic json) { + return Web3RpcErrorInvalidResponse(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class Web3RpcErrorTimeout extends Web3RpcError { + const Web3RpcErrorTimeout(this.value) : super(errorType: 'Timeout'); + + factory Web3RpcErrorTimeout.fromJson(dynamic json) { + return Web3RpcErrorTimeout(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class Web3RpcErrorInternal extends Web3RpcError { + const Web3RpcErrorInternal(this.value) : super(errorType: 'Internal'); + + factory Web3RpcErrorInternal.fromJson(dynamic json) { + return Web3RpcErrorInternal(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class Web3RpcErrorInvalidGasApiConfig extends Web3RpcError { + const Web3RpcErrorInvalidGasApiConfig(this.value) + : super(errorType: 'InvalidGasApiConfig'); + + factory Web3RpcErrorInvalidGasApiConfig.fromJson(dynamic json) { + return Web3RpcErrorInvalidGasApiConfig(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class Web3RpcErrorNftProtocolNotSupported extends Web3RpcError { + const Web3RpcErrorNftProtocolNotSupported() + : super(errorType: 'NftProtocolNotSupported'); + + factory Web3RpcErrorNftProtocolNotSupported.fromJson() => + const Web3RpcErrorNftProtocolNotSupported(); + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': null}; +} + +final class Web3RpcErrorNumConversError extends Web3RpcError { + const Web3RpcErrorNumConversError(this.value) + : super(errorType: 'NumConversError'); + + factory Web3RpcErrorNumConversError.fromJson(dynamic json) { + return Web3RpcErrorNumConversError(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class Web3RpcErrorUnknown extends Web3RpcError { + const Web3RpcErrorUnknown({required this.data, this.rawErrorType}) + : super(errorType: 'unknown'); + + final String? rawErrorType; + final dynamic data; + + @override + JsonMap toJson() => { + 'error_type': rawErrorType ?? errorType, + 'error_data': data, + }; +} + +sealed class GetFeeEstimationRequestError { + const GetFeeEstimationRequestError({required this.errorType}); + + factory GetFeeEstimationRequestError.fromJson(dynamic json) { + final map = _asJsonMap(json); + final type = map.valueOrNull('error_type'); + final data = map.valueOrNull('error_data'); + switch (type) { + case 'CoinNotFound': + return GetFeeEstimationRequestErrorCoinNotFound.fromJson(); + case 'Internal': + return GetFeeEstimationRequestErrorInternal.fromJson(data); + case 'CoinNotSupported': + return GetFeeEstimationRequestErrorCoinNotSupported.fromJson(); + default: + return GetFeeEstimationRequestErrorUnknown( + rawErrorType: type, + data: data, + ); + } + } + + final String errorType; + + JsonMap toJson(); +} + +final class GetFeeEstimationRequestErrorCoinNotFound + extends GetFeeEstimationRequestError { + const GetFeeEstimationRequestErrorCoinNotFound() + : super(errorType: 'CoinNotFound'); + + factory GetFeeEstimationRequestErrorCoinNotFound.fromJson() => + const GetFeeEstimationRequestErrorCoinNotFound(); + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': null}; +} + +final class GetFeeEstimationRequestErrorInternal + extends GetFeeEstimationRequestError { + const GetFeeEstimationRequestErrorInternal(this.value) + : super(errorType: 'Internal'); + + factory GetFeeEstimationRequestErrorInternal.fromJson(dynamic json) { + return GetFeeEstimationRequestErrorInternal(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class GetFeeEstimationRequestErrorCoinNotSupported + extends GetFeeEstimationRequestError { + const GetFeeEstimationRequestErrorCoinNotSupported() + : super(errorType: 'CoinNotSupported'); + + factory GetFeeEstimationRequestErrorCoinNotSupported.fromJson() => + const GetFeeEstimationRequestErrorCoinNotSupported(); + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': null}; +} + +final class GetFeeEstimationRequestErrorUnknown + extends GetFeeEstimationRequestError { + const GetFeeEstimationRequestErrorUnknown({ + required this.data, + this.rawErrorType, + }) : super(errorType: 'unknown'); + + final String? rawErrorType; + final dynamic data; + + @override + JsonMap toJson() => { + 'error_type': rawErrorType ?? errorType, + 'error_data': data, + }; +} + +sealed class EthActivationV2Error { + const EthActivationV2Error({required this.errorType}); + + factory EthActivationV2Error.fromJson(dynamic json) { + final map = _asJsonMap(json); + final type = map.valueOrNull('error_type'); + final data = map.valueOrNull('error_data'); + switch (type) { + case 'InvalidPayload': + return EthActivationV2ErrorInvalidPayload.fromJson(data); + case 'InvalidSwapContractAddr': + return EthActivationV2ErrorInvalidSwapContractAddr.fromJson(data); + case 'InvalidFallbackSwapContract': + return EthActivationV2ErrorInvalidFallbackSwapContract.fromJson(data); + case 'InvalidPathToAddress': + return EthActivationV2ErrorInvalidPathToAddress.fromJson(data); + case 'ChainIdNotSet': + return EthActivationV2ErrorChainIdNotSet.fromJson(); + case 'UnsupportedChain': + return EthActivationV2ErrorUnsupportedChain.fromJson(data); + case 'ActivationFailed': + return EthActivationV2ErrorActivationFailed.fromJson(data); + case 'CouldNotFetchBalance': + return EthActivationV2ErrorCouldNotFetchBalance.fromJson(data); + case 'UnreachableNodes': + return EthActivationV2ErrorUnreachableNodes.fromJson(data); + case 'AtLeastOneNodeRequired': + return EthActivationV2ErrorAtLeastOneNodeRequired.fromJson(); + case 'ErrorDeserializingDerivationPath': + return EthActivationV2ErrorErrorDeserializingDerivationPath.fromJson( + data, + ); + case 'PrivKeyPolicyNotAllowed': + return EthActivationV2ErrorPrivKeyPolicyNotAllowed.fromJson(data); + case 'FailedSpawningBalanceEvents': + return EthActivationV2ErrorFailedSpawningBalanceEvents.fromJson(data); + case 'HDWalletStorageError': + return EthActivationV2ErrorHDWalletStorageError.fromJson(data); + case 'MetamaskError': + return EthActivationV2ErrorMetamaskError.fromJson(data); + case 'InternalError': + return EthActivationV2ErrorInternalError.fromJson(data); + case 'Transport': + return EthActivationV2ErrorTransport.fromJson(data); + case 'UnexpectedDerivationMethod': + return EthActivationV2ErrorUnexpectedDerivationMethod.fromJson(data); + case 'CoinDoesntSupportTrezor': + return EthActivationV2ErrorCoinDoesntSupportTrezor.fromJson(); + case 'HwContextNotInitialized': + return EthActivationV2ErrorHwContextNotInitialized.fromJson(); + case 'TaskTimedOut': + return EthActivationV2ErrorTaskTimedOut.fromJson(data); + case 'HwError': + return EthActivationV2ErrorHwError.fromJson(data); + case 'InvalidHardwareWalletCall': + return EthActivationV2ErrorInvalidHardwareWalletCall.fromJson(); + case 'CustomTokenError': + return EthActivationV2ErrorCustomTokenError.fromJson(data); + case 'WalletConnectError': + return EthActivationV2ErrorWalletConnectError.fromJson(data); + default: + return EthActivationV2ErrorUnknown(rawErrorType: type, data: data); + } + } + + final String errorType; + + JsonMap toJson(); +} + +final class EthActivationV2ErrorInvalidPayload extends EthActivationV2Error { + const EthActivationV2ErrorInvalidPayload(this.value) + : super(errorType: 'InvalidPayload'); + + factory EthActivationV2ErrorInvalidPayload.fromJson(dynamic json) { + return EthActivationV2ErrorInvalidPayload(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class EthActivationV2ErrorInvalidSwapContractAddr + extends EthActivationV2Error { + const EthActivationV2ErrorInvalidSwapContractAddr(this.value) + : super(errorType: 'InvalidSwapContractAddr'); + + factory EthActivationV2ErrorInvalidSwapContractAddr.fromJson(dynamic json) { + return EthActivationV2ErrorInvalidSwapContractAddr(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class EthActivationV2ErrorInvalidFallbackSwapContract + extends EthActivationV2Error { + const EthActivationV2ErrorInvalidFallbackSwapContract(this.value) + : super(errorType: 'InvalidFallbackSwapContract'); + + factory EthActivationV2ErrorInvalidFallbackSwapContract.fromJson( + dynamic json, + ) { + return EthActivationV2ErrorInvalidFallbackSwapContract( + _stringFromJson(json), + ); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class EthActivationV2ErrorInvalidPathToAddress + extends EthActivationV2Error { + const EthActivationV2ErrorInvalidPathToAddress(this.value) + : super(errorType: 'InvalidPathToAddress'); + + factory EthActivationV2ErrorInvalidPathToAddress.fromJson(dynamic json) { + return EthActivationV2ErrorInvalidPathToAddress(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class EthActivationV2ErrorChainIdNotSet extends EthActivationV2Error { + const EthActivationV2ErrorChainIdNotSet() : super(errorType: 'ChainIdNotSet'); + + factory EthActivationV2ErrorChainIdNotSet.fromJson() => + const EthActivationV2ErrorChainIdNotSet(); + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': null}; +} + +final class EthActivationV2ErrorUnsupportedChain extends EthActivationV2Error { + const EthActivationV2ErrorUnsupportedChain({ + required this.chain, + required this.feature, + }) : super(errorType: 'UnsupportedChain'); + + factory EthActivationV2ErrorUnsupportedChain.fromJson(dynamic json) { + final map = _asJsonMap(json); + return EthActivationV2ErrorUnsupportedChain( + chain: _stringFromJson(map.value('chain')), + feature: _stringFromJson(map.value('feature')), + ); + } + + final String chain; + final String feature; + + @override + JsonMap toJson() => { + 'error_type': errorType, + 'error_data': {'chain': chain, 'feature': feature}, + }; +} + +final class EthActivationV2ErrorActivationFailed extends EthActivationV2Error { + const EthActivationV2ErrorActivationFailed({ + required this.ticker, + required this.error, + }) : super(errorType: 'ActivationFailed'); + + factory EthActivationV2ErrorActivationFailed.fromJson(dynamic json) { + final map = _asJsonMap(json); + return EthActivationV2ErrorActivationFailed( + ticker: _stringFromJson(map.value('ticker')), + error: _stringFromJson(map.value('error')), + ); + } + + final String ticker; + final String error; + + @override + JsonMap toJson() => { + 'error_type': errorType, + 'error_data': {'ticker': ticker, 'error': error}, + }; +} + +final class EthActivationV2ErrorCouldNotFetchBalance + extends EthActivationV2Error { + const EthActivationV2ErrorCouldNotFetchBalance(this.value) + : super(errorType: 'CouldNotFetchBalance'); + + factory EthActivationV2ErrorCouldNotFetchBalance.fromJson(dynamic json) { + return EthActivationV2ErrorCouldNotFetchBalance(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class EthActivationV2ErrorUnreachableNodes extends EthActivationV2Error { + const EthActivationV2ErrorUnreachableNodes(this.value) + : super(errorType: 'UnreachableNodes'); + + factory EthActivationV2ErrorUnreachableNodes.fromJson(dynamic json) { + return EthActivationV2ErrorUnreachableNodes(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class EthActivationV2ErrorAtLeastOneNodeRequired + extends EthActivationV2Error { + const EthActivationV2ErrorAtLeastOneNodeRequired() + : super(errorType: 'AtLeastOneNodeRequired'); + + factory EthActivationV2ErrorAtLeastOneNodeRequired.fromJson() => + const EthActivationV2ErrorAtLeastOneNodeRequired(); + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': null}; +} + +final class EthActivationV2ErrorErrorDeserializingDerivationPath + extends EthActivationV2Error { + const EthActivationV2ErrorErrorDeserializingDerivationPath(this.value) + : super(errorType: 'ErrorDeserializingDerivationPath'); + + factory EthActivationV2ErrorErrorDeserializingDerivationPath.fromJson( + dynamic json, + ) { + return EthActivationV2ErrorErrorDeserializingDerivationPath( + _stringFromJson(json), + ); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class EthActivationV2ErrorPrivKeyPolicyNotAllowed + extends EthActivationV2Error { + const EthActivationV2ErrorPrivKeyPolicyNotAllowed(this.value) + : super(errorType: 'PrivKeyPolicyNotAllowed'); + + factory EthActivationV2ErrorPrivKeyPolicyNotAllowed.fromJson(dynamic json) { + return EthActivationV2ErrorPrivKeyPolicyNotAllowed( + PrivKeyPolicyNotAllowed.fromJson(json), + ); + } + + final PrivKeyPolicyNotAllowed value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value.toJson()}; +} + +final class EthActivationV2ErrorFailedSpawningBalanceEvents + extends EthActivationV2Error { + const EthActivationV2ErrorFailedSpawningBalanceEvents(this.value) + : super(errorType: 'FailedSpawningBalanceEvents'); + + factory EthActivationV2ErrorFailedSpawningBalanceEvents.fromJson( + dynamic json, + ) { + return EthActivationV2ErrorFailedSpawningBalanceEvents( + _stringFromJson(json), + ); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class EthActivationV2ErrorHDWalletStorageError + extends EthActivationV2Error { + const EthActivationV2ErrorHDWalletStorageError(this.value) + : super(errorType: 'HDWalletStorageError'); + + factory EthActivationV2ErrorHDWalletStorageError.fromJson(dynamic json) { + return EthActivationV2ErrorHDWalletStorageError(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class EthActivationV2ErrorMetamaskError extends EthActivationV2Error { + const EthActivationV2ErrorMetamaskError(this.value) + : super(errorType: 'MetamaskError'); + + factory EthActivationV2ErrorMetamaskError.fromJson(dynamic json) { + return EthActivationV2ErrorMetamaskError(MetamaskRpcError.fromJson(json)); + } + + final MetamaskRpcError value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value.toJson()}; +} + +final class EthActivationV2ErrorInternalError extends EthActivationV2Error { + const EthActivationV2ErrorInternalError(this.value) + : super(errorType: 'InternalError'); + + factory EthActivationV2ErrorInternalError.fromJson(dynamic json) { + return EthActivationV2ErrorInternalError(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class EthActivationV2ErrorTransport extends EthActivationV2Error { + const EthActivationV2ErrorTransport(this.value) + : super(errorType: 'Transport'); + + factory EthActivationV2ErrorTransport.fromJson(dynamic json) { + return EthActivationV2ErrorTransport(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class EthActivationV2ErrorUnexpectedDerivationMethod + extends EthActivationV2Error { + const EthActivationV2ErrorUnexpectedDerivationMethod(this.value) + : super(errorType: 'UnexpectedDerivationMethod'); + + factory EthActivationV2ErrorUnexpectedDerivationMethod.fromJson( + dynamic json, + ) { + return EthActivationV2ErrorUnexpectedDerivationMethod( + UnexpectedDerivationMethod.fromJson(json), + ); + } + + final UnexpectedDerivationMethod value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value.toJson()}; +} + +final class EthActivationV2ErrorCoinDoesntSupportTrezor + extends EthActivationV2Error { + const EthActivationV2ErrorCoinDoesntSupportTrezor() + : super(errorType: 'CoinDoesntSupportTrezor'); + + factory EthActivationV2ErrorCoinDoesntSupportTrezor.fromJson() => + const EthActivationV2ErrorCoinDoesntSupportTrezor(); + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': null}; +} + +final class EthActivationV2ErrorHwContextNotInitialized + extends EthActivationV2Error { + const EthActivationV2ErrorHwContextNotInitialized() + : super(errorType: 'HwContextNotInitialized'); + + factory EthActivationV2ErrorHwContextNotInitialized.fromJson() => + const EthActivationV2ErrorHwContextNotInitialized(); + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': null}; +} + +final class EthActivationV2ErrorTaskTimedOut extends EthActivationV2Error { + const EthActivationV2ErrorTaskTimedOut({required this.duration}) + : super(errorType: 'TaskTimedOut'); + + factory EthActivationV2ErrorTaskTimedOut.fromJson(dynamic json) { + final map = _asJsonMap(json); + return EthActivationV2ErrorTaskTimedOut( + duration: Mm2Duration.fromJson(map.value('duration')), + ); + } + + final Mm2Duration duration; + + @override + JsonMap toJson() => { + 'error_type': errorType, + 'error_data': {'duration': duration.toJson()}, + }; +} + +final class EthActivationV2ErrorHwError extends EthActivationV2Error { + const EthActivationV2ErrorHwError(this.value) : super(errorType: 'HwError'); + + factory EthActivationV2ErrorHwError.fromJson(dynamic json) { + return EthActivationV2ErrorHwError(HwRpcError.fromJson(json)); + } + + final HwRpcError value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value.toJson()}; +} + +final class EthActivationV2ErrorInvalidHardwareWalletCall + extends EthActivationV2Error { + const EthActivationV2ErrorInvalidHardwareWalletCall() + : super(errorType: 'InvalidHardwareWalletCall'); + + factory EthActivationV2ErrorInvalidHardwareWalletCall.fromJson() => + const EthActivationV2ErrorInvalidHardwareWalletCall(); + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': null}; +} + +final class EthActivationV2ErrorCustomTokenError extends EthActivationV2Error { + const EthActivationV2ErrorCustomTokenError(this.value) + : super(errorType: 'CustomTokenError'); + + factory EthActivationV2ErrorCustomTokenError.fromJson(dynamic json) { + return EthActivationV2ErrorCustomTokenError( + CustomTokenError.fromJson(json), + ); + } + + final CustomTokenError value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value.toJson()}; +} + +final class EthActivationV2ErrorWalletConnectError + extends EthActivationV2Error { + const EthActivationV2ErrorWalletConnectError(this.value) + : super(errorType: 'WalletConnectError'); + + factory EthActivationV2ErrorWalletConnectError.fromJson(dynamic json) { + return EthActivationV2ErrorWalletConnectError(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class EthActivationV2ErrorUnknown extends EthActivationV2Error { + const EthActivationV2ErrorUnknown({required this.data, this.rawErrorType}) + : super(errorType: 'unknown'); + + final String? rawErrorType; + final dynamic data; + + @override + JsonMap toJson() => { + 'error_type': rawErrorType ?? errorType, + 'error_data': data, + }; +} + +sealed class EthTokenActivationError { + const EthTokenActivationError({required this.errorType}); + + factory EthTokenActivationError.fromJson(dynamic json) { + final map = _asJsonMap(json); + final type = map.valueOrNull('error_type'); + final data = map.valueOrNull('error_data'); + switch (type) { + case 'InternalError': + return EthTokenActivationErrorInternalError.fromJson(data); + case 'ClientConnectionFailed': + return EthTokenActivationErrorClientConnectionFailed.fromJson(data); + case 'CouldNotFetchBalance': + return EthTokenActivationErrorCouldNotFetchBalance.fromJson(data); + case 'InvalidPayload': + return EthTokenActivationErrorInvalidPayload.fromJson(data); + case 'Transport': + return EthTokenActivationErrorTransport.fromJson(data); + case 'UnexpectedDerivationMethod': + return EthTokenActivationErrorUnexpectedDerivationMethod.fromJson(data); + case 'PrivKeyPolicyNotAllowed': + return EthTokenActivationErrorPrivKeyPolicyNotAllowed.fromJson(data); + case 'CustomTokenError': + return EthTokenActivationErrorCustomTokenError.fromJson(data); + default: + return EthTokenActivationErrorUnknown(rawErrorType: type, data: data); + } + } + + final String errorType; + + JsonMap toJson(); +} + +final class EthTokenActivationErrorInternalError + extends EthTokenActivationError { + const EthTokenActivationErrorInternalError(this.value) + : super(errorType: 'InternalError'); + + factory EthTokenActivationErrorInternalError.fromJson(dynamic json) { + return EthTokenActivationErrorInternalError(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class EthTokenActivationErrorClientConnectionFailed + extends EthTokenActivationError { + const EthTokenActivationErrorClientConnectionFailed(this.value) + : super(errorType: 'ClientConnectionFailed'); + + factory EthTokenActivationErrorClientConnectionFailed.fromJson(dynamic json) { + return EthTokenActivationErrorClientConnectionFailed(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class EthTokenActivationErrorCouldNotFetchBalance + extends EthTokenActivationError { + const EthTokenActivationErrorCouldNotFetchBalance(this.value) + : super(errorType: 'CouldNotFetchBalance'); + + factory EthTokenActivationErrorCouldNotFetchBalance.fromJson(dynamic json) { + return EthTokenActivationErrorCouldNotFetchBalance(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class EthTokenActivationErrorInvalidPayload + extends EthTokenActivationError { + const EthTokenActivationErrorInvalidPayload(this.value) + : super(errorType: 'InvalidPayload'); + + factory EthTokenActivationErrorInvalidPayload.fromJson(dynamic json) { + return EthTokenActivationErrorInvalidPayload(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class EthTokenActivationErrorTransport extends EthTokenActivationError { + const EthTokenActivationErrorTransport(this.value) + : super(errorType: 'Transport'); + + factory EthTokenActivationErrorTransport.fromJson(dynamic json) { + return EthTokenActivationErrorTransport(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class EthTokenActivationErrorUnexpectedDerivationMethod + extends EthTokenActivationError { + const EthTokenActivationErrorUnexpectedDerivationMethod(this.value) + : super(errorType: 'UnexpectedDerivationMethod'); + + factory EthTokenActivationErrorUnexpectedDerivationMethod.fromJson( + dynamic json, + ) { + return EthTokenActivationErrorUnexpectedDerivationMethod( + UnexpectedDerivationMethod.fromJson(json), + ); + } + + final UnexpectedDerivationMethod value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value.toJson()}; +} + +final class EthTokenActivationErrorPrivKeyPolicyNotAllowed + extends EthTokenActivationError { + const EthTokenActivationErrorPrivKeyPolicyNotAllowed(this.value) + : super(errorType: 'PrivKeyPolicyNotAllowed'); + + factory EthTokenActivationErrorPrivKeyPolicyNotAllowed.fromJson( + dynamic json, + ) { + return EthTokenActivationErrorPrivKeyPolicyNotAllowed( + PrivKeyPolicyNotAllowed.fromJson(json), + ); + } + + final PrivKeyPolicyNotAllowed value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value.toJson()}; +} + +final class EthTokenActivationErrorCustomTokenError + extends EthTokenActivationError { + const EthTokenActivationErrorCustomTokenError(this.value) + : super(errorType: 'CustomTokenError'); + + factory EthTokenActivationErrorCustomTokenError.fromJson(dynamic json) { + return EthTokenActivationErrorCustomTokenError( + CustomTokenError.fromJson(json), + ); + } + + final CustomTokenError value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value.toJson()}; +} + +final class EthTokenActivationErrorUnknown extends EthTokenActivationError { + const EthTokenActivationErrorUnknown({required this.data, this.rawErrorType}) + : super(errorType: 'unknown'); + + final String? rawErrorType; + final dynamic data; + + @override + JsonMap toJson() => { + 'error_type': rawErrorType ?? errorType, + 'error_data': data, + }; +} + +sealed class AddressDerivingError { + const AddressDerivingError({required this.errorType}); + + factory AddressDerivingError.fromJson(dynamic json) { + final map = _asJsonMap(json); + final type = map.valueOrNull('error_type'); + final data = map.valueOrNull('error_data'); + switch (type) { + case 'InvalidBip44Chain': + return AddressDerivingErrorInvalidBip44Chain.fromJson(data); + case 'Bip32Error': + return AddressDerivingErrorBip32Error.fromJson(data); + case 'Internal': + return AddressDerivingErrorInternal.fromJson(data); + default: + return AddressDerivingErrorUnknown(rawErrorType: type, data: data); + } + } + + final String errorType; + + JsonMap toJson(); +} + +final class AddressDerivingErrorInvalidBip44Chain extends AddressDerivingError { + const AddressDerivingErrorInvalidBip44Chain({required this.chain}) + : super(errorType: 'InvalidBip44Chain'); + + factory AddressDerivingErrorInvalidBip44Chain.fromJson(dynamic json) { + final map = _asJsonMap(json); + return AddressDerivingErrorInvalidBip44Chain( + chain: Bip44Chain.fromJson(map.value('chain')), + ); + } + + final Bip44Chain chain; + + @override + JsonMap toJson() => { + 'error_type': errorType, + 'error_data': {'chain': chain.toJson()}, + }; +} + +final class AddressDerivingErrorBip32Error extends AddressDerivingError { + const AddressDerivingErrorBip32Error(this.value) + : super(errorType: 'Bip32Error'); + + factory AddressDerivingErrorBip32Error.fromJson(dynamic json) { + return AddressDerivingErrorBip32Error(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class AddressDerivingErrorInternal extends AddressDerivingError { + const AddressDerivingErrorInternal(this.value) : super(errorType: 'Internal'); + + factory AddressDerivingErrorInternal.fromJson(dynamic json) { + return AddressDerivingErrorInternal(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class AddressDerivingErrorUnknown extends AddressDerivingError { + const AddressDerivingErrorUnknown({required this.data, this.rawErrorType}) + : super(errorType: 'unknown'); + + final String? rawErrorType; + final dynamic data; + + @override + JsonMap toJson() => { + 'error_type': rawErrorType ?? errorType, + 'error_data': data, + }; +} + +sealed class EnableLightningError { + const EnableLightningError({required this.errorType}); + + factory EnableLightningError.fromJson(dynamic json) { + final map = _asJsonMap(json); + final type = map.valueOrNull('error_type'); + final data = map.valueOrNull('error_data'); + switch (type) { + case 'InvalidRequest': + return EnableLightningErrorInvalidRequest.fromJson(data); + case 'InvalidConfiguration': + return EnableLightningErrorInvalidConfiguration.fromJson(data); + case 'UnsupportedMode': + return EnableLightningErrorUnsupportedMode.fromJson(data); + case 'IOError': + return EnableLightningErrorIOError.fromJson(data); + case 'InvalidAddress': + return EnableLightningErrorInvalidAddress.fromJson(data); + case 'InvalidPath': + return EnableLightningErrorInvalidPath.fromJson(data); + case 'PrivKeyPolicyNotAllowed': + return EnableLightningErrorPrivKeyPolicyNotAllowed.fromJson(data); + case 'SystemTimeError': + return EnableLightningErrorSystemTimeError.fromJson(data); + case 'RpcError': + return EnableLightningErrorRpcError.fromJson(data); + case 'DbError': + return EnableLightningErrorDbError.fromJson(data); + case 'RpcTaskError': + return EnableLightningErrorRpcTaskError.fromJson(data); + case 'ConnectToNodeError': + return EnableLightningErrorConnectToNodeError.fromJson(data); + case 'Internal': + return EnableLightningErrorInternal.fromJson(data); + default: + return EnableLightningErrorUnknown(rawErrorType: type, data: data); + } + } + + final String errorType; + + JsonMap toJson(); +} + +final class EnableLightningErrorInvalidRequest extends EnableLightningError { + const EnableLightningErrorInvalidRequest(this.value) + : super(errorType: 'InvalidRequest'); + + factory EnableLightningErrorInvalidRequest.fromJson(dynamic json) { + return EnableLightningErrorInvalidRequest(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class EnableLightningErrorInvalidConfiguration + extends EnableLightningError { + const EnableLightningErrorInvalidConfiguration(this.value) + : super(errorType: 'InvalidConfiguration'); + + factory EnableLightningErrorInvalidConfiguration.fromJson(dynamic json) { + return EnableLightningErrorInvalidConfiguration(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class EnableLightningErrorUnsupportedMode extends EnableLightningError { + const EnableLightningErrorUnsupportedMode(this.value0, this.value1) + : super(errorType: 'UnsupportedMode'); + + factory EnableLightningErrorUnsupportedMode.fromJson(dynamic json) { + final list = _asJsonList(json); + return EnableLightningErrorUnsupportedMode( + _stringFromJson(list[0]), + _stringFromJson(list[1]), + ); + } + + final String value0; + final String value1; + + @override + JsonMap toJson() => { + 'error_type': errorType, + 'error_data': [value0, value1], + }; +} + +final class EnableLightningErrorIOError extends EnableLightningError { + const EnableLightningErrorIOError(this.value) : super(errorType: 'IOError'); + + factory EnableLightningErrorIOError.fromJson(dynamic json) { + return EnableLightningErrorIOError(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class EnableLightningErrorInvalidAddress extends EnableLightningError { + const EnableLightningErrorInvalidAddress(this.value) + : super(errorType: 'InvalidAddress'); + + factory EnableLightningErrorInvalidAddress.fromJson(dynamic json) { + return EnableLightningErrorInvalidAddress(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class EnableLightningErrorInvalidPath extends EnableLightningError { + const EnableLightningErrorInvalidPath(this.value) + : super(errorType: 'InvalidPath'); + + factory EnableLightningErrorInvalidPath.fromJson(dynamic json) { + return EnableLightningErrorInvalidPath(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class EnableLightningErrorPrivKeyPolicyNotAllowed + extends EnableLightningError { + const EnableLightningErrorPrivKeyPolicyNotAllowed(this.value) + : super(errorType: 'PrivKeyPolicyNotAllowed'); + + factory EnableLightningErrorPrivKeyPolicyNotAllowed.fromJson(dynamic json) { + return EnableLightningErrorPrivKeyPolicyNotAllowed( + PrivKeyPolicyNotAllowed.fromJson(json), + ); + } + + final PrivKeyPolicyNotAllowed value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value.toJson()}; +} + +final class EnableLightningErrorSystemTimeError extends EnableLightningError { + const EnableLightningErrorSystemTimeError(this.value) + : super(errorType: 'SystemTimeError'); + + factory EnableLightningErrorSystemTimeError.fromJson(dynamic json) { + return EnableLightningErrorSystemTimeError(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class EnableLightningErrorRpcError extends EnableLightningError { + const EnableLightningErrorRpcError(this.value) : super(errorType: 'RpcError'); + + factory EnableLightningErrorRpcError.fromJson(dynamic json) { + return EnableLightningErrorRpcError(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class EnableLightningErrorDbError extends EnableLightningError { + const EnableLightningErrorDbError(this.value) : super(errorType: 'DbError'); + + factory EnableLightningErrorDbError.fromJson(dynamic json) { + return EnableLightningErrorDbError(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class EnableLightningErrorRpcTaskError extends EnableLightningError { + const EnableLightningErrorRpcTaskError(this.value) + : super(errorType: 'RpcTaskError'); + + factory EnableLightningErrorRpcTaskError.fromJson(dynamic json) { + return EnableLightningErrorRpcTaskError(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class EnableLightningErrorConnectToNodeError + extends EnableLightningError { + const EnableLightningErrorConnectToNodeError(this.value) + : super(errorType: 'ConnectToNodeError'); + + factory EnableLightningErrorConnectToNodeError.fromJson(dynamic json) { + return EnableLightningErrorConnectToNodeError(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class EnableLightningErrorInternal extends EnableLightningError { + const EnableLightningErrorInternal(this.value) : super(errorType: 'Internal'); + + factory EnableLightningErrorInternal.fromJson(dynamic json) { + return EnableLightningErrorInternal(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class EnableLightningErrorUnknown extends EnableLightningError { + const EnableLightningErrorUnknown({required this.data, this.rawErrorType}) + : super(errorType: 'unknown'); + + final String? rawErrorType; + final dynamic data; + + @override + JsonMap toJson() => { + 'error_type': rawErrorType ?? errorType, + 'error_data': data, + }; +} + +sealed class BalanceError { + const BalanceError({required this.errorType}); + + factory BalanceError.fromJson(dynamic json) { + final map = _asJsonMap(json); + final type = map.valueOrNull('error_type'); + final data = map.valueOrNull('error_data'); + switch (type) { + case 'Transport': + return BalanceErrorTransport.fromJson(data); + case 'InvalidResponse': + return BalanceErrorInvalidResponse.fromJson(data); + case 'UnexpectedDerivationMethod': + return BalanceErrorUnexpectedDerivationMethod.fromJson(data); + case 'WalletStorageError': + return BalanceErrorWalletStorageError.fromJson(data); + case 'Internal': + return BalanceErrorInternal.fromJson(data); + default: + return BalanceErrorUnknown(rawErrorType: type, data: data); + } + } + + final String errorType; + + JsonMap toJson(); +} + +final class BalanceErrorTransport extends BalanceError { + const BalanceErrorTransport(this.value) : super(errorType: 'Transport'); + + factory BalanceErrorTransport.fromJson(dynamic json) { + return BalanceErrorTransport(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class BalanceErrorInvalidResponse extends BalanceError { + const BalanceErrorInvalidResponse(this.value) + : super(errorType: 'InvalidResponse'); + + factory BalanceErrorInvalidResponse.fromJson(dynamic json) { + return BalanceErrorInvalidResponse(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class BalanceErrorUnexpectedDerivationMethod extends BalanceError { + const BalanceErrorUnexpectedDerivationMethod(this.value) + : super(errorType: 'UnexpectedDerivationMethod'); + + factory BalanceErrorUnexpectedDerivationMethod.fromJson(dynamic json) { + return BalanceErrorUnexpectedDerivationMethod( + UnexpectedDerivationMethod.fromJson(json), + ); + } + + final UnexpectedDerivationMethod value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value.toJson()}; +} + +final class BalanceErrorWalletStorageError extends BalanceError { + const BalanceErrorWalletStorageError(this.value) + : super(errorType: 'WalletStorageError'); + + factory BalanceErrorWalletStorageError.fromJson(dynamic json) { + return BalanceErrorWalletStorageError(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class BalanceErrorInternal extends BalanceError { + const BalanceErrorInternal(this.value) : super(errorType: 'Internal'); + + factory BalanceErrorInternal.fromJson(dynamic json) { + return BalanceErrorInternal(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class BalanceErrorUnknown extends BalanceError { + const BalanceErrorUnknown({required this.data, this.rawErrorType}) + : super(errorType: 'unknown'); + + final String? rawErrorType; + final dynamic data; + + @override + JsonMap toJson() => { + 'error_type': rawErrorType ?? errorType, + 'error_data': data, + }; +} + +sealed class DelegationError { + const DelegationError({required this.errorType}); + + factory DelegationError.fromJson(dynamic json) { + final map = _asJsonMap(json); + final type = map.valueOrNull('error_type'); + final data = map.valueOrNull('error_data'); + switch (type) { + case 'NotSufficientBalance': + return DelegationErrorNotSufficientBalance.fromJson(data); + case 'AmountTooLow': + return DelegationErrorAmountTooLow.fromJson(data); + case 'CoinDoesntSupportDelegation': + return DelegationErrorCoinDoesntSupportDelegation.fromJson(data); + case 'NoSuchCoin': + return DelegationErrorNoSuchCoin.fromJson(data); + case 'CanNotUndelegate': + return DelegationErrorCanNotUndelegate.fromJson(data); + case 'TooMuchToUndelegate': + return DelegationErrorTooMuchToUndelegate.fromJson(data); + case 'UnprofitableReward': + return DelegationErrorUnprofitableReward.fromJson(data); + case 'NothingToClaim': + return DelegationErrorNothingToClaim.fromJson(data); + case 'CannotInteractWithSmartContract': + return DelegationErrorCannotInteractWithSmartContract.fromJson(data); + case 'AddressError': + return DelegationErrorAddressError.fromJson(data); + case 'AlreadyDelegating': + return DelegationErrorAlreadyDelegating.fromJson(data); + case 'DelegationOpsNotSupported': + return DelegationErrorDelegationOpsNotSupported.fromJson(data); + case 'Transport': + return DelegationErrorTransport.fromJson(data); + case 'InvalidPayload': + return DelegationErrorInvalidPayload.fromJson(data); + case 'InternalError': + return DelegationErrorInternalError.fromJson(data); + default: + return DelegationErrorUnknown(rawErrorType: type, data: data); + } + } + + final String errorType; + + JsonMap toJson(); +} + +final class DelegationErrorNotSufficientBalance extends DelegationError { + const DelegationErrorNotSufficientBalance({ + required this.coin, + required this.available, + required this.required, + }) : super(errorType: 'NotSufficientBalance'); + + factory DelegationErrorNotSufficientBalance.fromJson(dynamic json) { + final map = _asJsonMap(json); + return DelegationErrorNotSufficientBalance( + coin: _stringFromJson(map.value('coin')), + available: BigDecimal.fromJson(map.value('available')), + required: BigDecimal.fromJson(map.value('required')), + ); + } + + final String coin; + final BigDecimal available; + final BigDecimal required; + + @override + JsonMap toJson() => { + 'error_type': errorType, + 'error_data': { + 'coin': coin, + 'available': available.toJson(), + 'required': required.toJson(), + }, + }; +} + +final class DelegationErrorAmountTooLow extends DelegationError { + const DelegationErrorAmountTooLow({ + required this.amount, + required this.threshold, + }) : super(errorType: 'AmountTooLow'); + + factory DelegationErrorAmountTooLow.fromJson(dynamic json) { + final map = _asJsonMap(json); + return DelegationErrorAmountTooLow( + amount: BigDecimal.fromJson(map.value('amount')), + threshold: BigDecimal.fromJson(map.value('threshold')), + ); + } + + final BigDecimal amount; + final BigDecimal threshold; + + @override + JsonMap toJson() => { + 'error_type': errorType, + 'error_data': {'amount': amount.toJson(), 'threshold': threshold.toJson()}, + }; +} + +final class DelegationErrorCoinDoesntSupportDelegation extends DelegationError { + const DelegationErrorCoinDoesntSupportDelegation({required this.coin}) + : super(errorType: 'CoinDoesntSupportDelegation'); + + factory DelegationErrorCoinDoesntSupportDelegation.fromJson(dynamic json) { + final map = _asJsonMap(json); + return DelegationErrorCoinDoesntSupportDelegation( + coin: _stringFromJson(map.value('coin')), + ); + } + + final String coin; + + @override + JsonMap toJson() => { + 'error_type': errorType, + 'error_data': {'coin': coin}, + }; +} + +final class DelegationErrorNoSuchCoin extends DelegationError { + const DelegationErrorNoSuchCoin({required this.coin}) + : super(errorType: 'NoSuchCoin'); + + factory DelegationErrorNoSuchCoin.fromJson(dynamic json) { + final map = _asJsonMap(json); + return DelegationErrorNoSuchCoin( + coin: _stringFromJson(map.value('coin')), + ); + } + + final String coin; + + @override + JsonMap toJson() => { + 'error_type': errorType, + 'error_data': {'coin': coin}, + }; +} + +final class DelegationErrorCanNotUndelegate extends DelegationError { + const DelegationErrorCanNotUndelegate({ + required this.delegatorAddr, + required this.validatorAddr, + }) : super(errorType: 'CanNotUndelegate'); + + factory DelegationErrorCanNotUndelegate.fromJson(dynamic json) { + final map = _asJsonMap(json); + return DelegationErrorCanNotUndelegate( + delegatorAddr: _stringFromJson(map.value('delegator_addr')), + validatorAddr: _stringFromJson(map.value('validator_addr')), + ); + } + + final String delegatorAddr; + final String validatorAddr; + + @override + JsonMap toJson() => { + 'error_type': errorType, + 'error_data': { + 'delegator_addr': delegatorAddr, + 'validator_addr': validatorAddr, + }, + }; +} + +final class DelegationErrorTooMuchToUndelegate extends DelegationError { + const DelegationErrorTooMuchToUndelegate({ + required this.available, + required this.requested, + }) : super(errorType: 'TooMuchToUndelegate'); + + factory DelegationErrorTooMuchToUndelegate.fromJson(dynamic json) { + final map = _asJsonMap(json); + return DelegationErrorTooMuchToUndelegate( + available: BigDecimal.fromJson(map.value('available')), + requested: BigDecimal.fromJson(map.value('requested')), + ); + } + + final BigDecimal available; + final BigDecimal requested; + + @override + JsonMap toJson() => { + 'error_type': errorType, + 'error_data': { + 'available': available.toJson(), + 'requested': requested.toJson(), + }, + }; +} + +final class DelegationErrorUnprofitableReward extends DelegationError { + const DelegationErrorUnprofitableReward({ + required this.reward, + required this.fee, + }) : super(errorType: 'UnprofitableReward'); + + factory DelegationErrorUnprofitableReward.fromJson(dynamic json) { + final map = _asJsonMap(json); + return DelegationErrorUnprofitableReward( + reward: BigDecimal.fromJson(map.value('reward')), + fee: BigDecimal.fromJson(map.value('fee')), + ); + } + + final BigDecimal reward; + final BigDecimal fee; + + @override + JsonMap toJson() => { + 'error_type': errorType, + 'error_data': {'reward': reward.toJson(), 'fee': fee.toJson()}, + }; +} + +final class DelegationErrorNothingToClaim extends DelegationError { + const DelegationErrorNothingToClaim({required this.coin}) + : super(errorType: 'NothingToClaim'); + + factory DelegationErrorNothingToClaim.fromJson(dynamic json) { + final map = _asJsonMap(json); + return DelegationErrorNothingToClaim( + coin: _stringFromJson(map.value('coin')), + ); + } + + final String coin; + + @override + JsonMap toJson() => { + 'error_type': errorType, + 'error_data': {'coin': coin}, + }; +} + +final class DelegationErrorCannotInteractWithSmartContract + extends DelegationError { + const DelegationErrorCannotInteractWithSmartContract(this.value) + : super(errorType: 'CannotInteractWithSmartContract'); + + factory DelegationErrorCannotInteractWithSmartContract.fromJson( + dynamic json, + ) { + return DelegationErrorCannotInteractWithSmartContract( + _stringFromJson(json), + ); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class DelegationErrorAddressError extends DelegationError { + const DelegationErrorAddressError(this.value) + : super(errorType: 'AddressError'); + + factory DelegationErrorAddressError.fromJson(dynamic json) { + return DelegationErrorAddressError(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class DelegationErrorAlreadyDelegating extends DelegationError { + const DelegationErrorAlreadyDelegating(this.value) + : super(errorType: 'AlreadyDelegating'); + + factory DelegationErrorAlreadyDelegating.fromJson(dynamic json) { + return DelegationErrorAlreadyDelegating(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class DelegationErrorDelegationOpsNotSupported extends DelegationError { + const DelegationErrorDelegationOpsNotSupported({required this.reason}) + : super(errorType: 'DelegationOpsNotSupported'); + + factory DelegationErrorDelegationOpsNotSupported.fromJson(dynamic json) { + final map = _asJsonMap(json); + return DelegationErrorDelegationOpsNotSupported( + reason: _stringFromJson(map.value('reason')), + ); + } + + final String reason; + + @override + JsonMap toJson() => { + 'error_type': errorType, + 'error_data': {'reason': reason}, + }; +} + +final class DelegationErrorTransport extends DelegationError { + const DelegationErrorTransport(this.value) : super(errorType: 'Transport'); + + factory DelegationErrorTransport.fromJson(dynamic json) { + return DelegationErrorTransport(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class DelegationErrorInvalidPayload extends DelegationError { + const DelegationErrorInvalidPayload({required this.reason}) + : super(errorType: 'InvalidPayload'); + + factory DelegationErrorInvalidPayload.fromJson(dynamic json) { + final map = _asJsonMap(json); + return DelegationErrorInvalidPayload( + reason: _stringFromJson(map.value('reason')), + ); + } + + final String reason; + + @override + JsonMap toJson() => { + 'error_type': errorType, + 'error_data': {'reason': reason}, + }; +} + +final class DelegationErrorInternalError extends DelegationError { + const DelegationErrorInternalError(this.value) + : super(errorType: 'InternalError'); + + factory DelegationErrorInternalError.fromJson(dynamic json) { + return DelegationErrorInternalError(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class DelegationErrorUnknown extends DelegationError { + const DelegationErrorUnknown({required this.data, this.rawErrorType}) + : super(errorType: 'unknown'); + + final String? rawErrorType; + final dynamic data; + + @override + JsonMap toJson() => { + 'error_type': rawErrorType ?? errorType, + 'error_data': data, + }; +} + +sealed class GetMyAddressError { + const GetMyAddressError({required this.errorType}); + + factory GetMyAddressError.fromJson(dynamic json) { + final map = _asJsonMap(json); + final type = map.valueOrNull('error_type'); + final data = map.valueOrNull('error_data'); + switch (type) { + case 'CoinsConfCheckError': + return GetMyAddressErrorCoinsConfCheckError.fromJson(data); + case 'CoinIsNotSupported': + return GetMyAddressErrorCoinIsNotSupported.fromJson(data); + case 'Internal': + return GetMyAddressErrorInternal.fromJson(data); + case 'InvalidRequest': + return GetMyAddressErrorInvalidRequest.fromJson(data); + case 'GetEthAddressError': + return GetMyAddressErrorGetEthAddressError.fromJson(data); + default: + return GetMyAddressErrorUnknown(rawErrorType: type, data: data); + } + } + + final String errorType; + + JsonMap toJson(); +} + +final class GetMyAddressErrorCoinsConfCheckError extends GetMyAddressError { + const GetMyAddressErrorCoinsConfCheckError(this.value) + : super(errorType: 'CoinsConfCheckError'); + + factory GetMyAddressErrorCoinsConfCheckError.fromJson(dynamic json) { + return GetMyAddressErrorCoinsConfCheckError(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class GetMyAddressErrorCoinIsNotSupported extends GetMyAddressError { + const GetMyAddressErrorCoinIsNotSupported(this.value) + : super(errorType: 'CoinIsNotSupported'); + + factory GetMyAddressErrorCoinIsNotSupported.fromJson(dynamic json) { + return GetMyAddressErrorCoinIsNotSupported(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class GetMyAddressErrorInternal extends GetMyAddressError { + const GetMyAddressErrorInternal(this.value) : super(errorType: 'Internal'); + + factory GetMyAddressErrorInternal.fromJson(dynamic json) { + return GetMyAddressErrorInternal(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class GetMyAddressErrorInvalidRequest extends GetMyAddressError { + const GetMyAddressErrorInvalidRequest(this.value) + : super(errorType: 'InvalidRequest'); + + factory GetMyAddressErrorInvalidRequest.fromJson(dynamic json) { + return GetMyAddressErrorInvalidRequest(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class GetMyAddressErrorGetEthAddressError extends GetMyAddressError { + const GetMyAddressErrorGetEthAddressError(this.value) + : super(errorType: 'GetEthAddressError'); + + factory GetMyAddressErrorGetEthAddressError.fromJson(dynamic json) { + return GetMyAddressErrorGetEthAddressError( + GetEthAddressError.fromJson(json), + ); + } + + final GetEthAddressError value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value.toJson()}; +} + +final class GetMyAddressErrorUnknown extends GetMyAddressError { + const GetMyAddressErrorUnknown({required this.data, this.rawErrorType}) + : super(errorType: 'unknown'); + + final String? rawErrorType; + final dynamic data; + + @override + JsonMap toJson() => { + 'error_type': rawErrorType ?? errorType, + 'error_data': data, + }; +} + +sealed class RawTransactionError { + const RawTransactionError({required this.errorType}); + + factory RawTransactionError.fromJson(dynamic json) { + final map = _asJsonMap(json); + final type = map.valueOrNull('error_type'); + final data = map.valueOrNull('error_data'); + switch (type) { + case 'NoSuchCoin': + return RawTransactionErrorNoSuchCoin.fromJson(data); + case 'InvalidHashError': + return RawTransactionErrorInvalidHashError.fromJson(data); + case 'Transport': + return RawTransactionErrorTransport.fromJson(data); + case 'HashNotExist': + return RawTransactionErrorHashNotExist.fromJson(data); + case 'InternalError': + return RawTransactionErrorInternalError.fromJson(data); + case 'DecodeError': + return RawTransactionErrorDecodeError.fromJson(data); + case 'InvalidParam': + return RawTransactionErrorInvalidParam.fromJson(data); + case 'NonExistentPrevOutputError': + return RawTransactionErrorNonExistentPrevOutputError.fromJson(data); + case 'SigningError': + return RawTransactionErrorSigningError.fromJson(data); + case 'NotImplemented': + return RawTransactionErrorNotImplemented.fromJson(data); + case 'TransactionError': + return RawTransactionErrorTransactionError.fromJson(data); + default: + return RawTransactionErrorUnknown(rawErrorType: type, data: data); + } + } + + final String errorType; + + JsonMap toJson(); +} + +final class RawTransactionErrorNoSuchCoin extends RawTransactionError { + const RawTransactionErrorNoSuchCoin({required this.coin}) + : super(errorType: 'NoSuchCoin'); + + factory RawTransactionErrorNoSuchCoin.fromJson(dynamic json) { + final map = _asJsonMap(json); + return RawTransactionErrorNoSuchCoin( + coin: _stringFromJson(map.value('coin')), + ); + } + + final String coin; + + @override + JsonMap toJson() => { + 'error_type': errorType, + 'error_data': {'coin': coin}, + }; +} + +final class RawTransactionErrorInvalidHashError extends RawTransactionError { + const RawTransactionErrorInvalidHashError(this.value) + : super(errorType: 'InvalidHashError'); + + factory RawTransactionErrorInvalidHashError.fromJson(dynamic json) { + return RawTransactionErrorInvalidHashError(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class RawTransactionErrorTransport extends RawTransactionError { + const RawTransactionErrorTransport(this.value) + : super(errorType: 'Transport'); + + factory RawTransactionErrorTransport.fromJson(dynamic json) { + return RawTransactionErrorTransport(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class RawTransactionErrorHashNotExist extends RawTransactionError { + const RawTransactionErrorHashNotExist(this.value) + : super(errorType: 'HashNotExist'); + + factory RawTransactionErrorHashNotExist.fromJson(dynamic json) { + return RawTransactionErrorHashNotExist(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class RawTransactionErrorInternalError extends RawTransactionError { + const RawTransactionErrorInternalError(this.value) + : super(errorType: 'InternalError'); + + factory RawTransactionErrorInternalError.fromJson(dynamic json) { + return RawTransactionErrorInternalError(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class RawTransactionErrorDecodeError extends RawTransactionError { + const RawTransactionErrorDecodeError(this.value) + : super(errorType: 'DecodeError'); + + factory RawTransactionErrorDecodeError.fromJson(dynamic json) { + return RawTransactionErrorDecodeError(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class RawTransactionErrorInvalidParam extends RawTransactionError { + const RawTransactionErrorInvalidParam(this.value) + : super(errorType: 'InvalidParam'); + + factory RawTransactionErrorInvalidParam.fromJson(dynamic json) { + return RawTransactionErrorInvalidParam(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class RawTransactionErrorNonExistentPrevOutputError + extends RawTransactionError { + const RawTransactionErrorNonExistentPrevOutputError(this.value) + : super(errorType: 'NonExistentPrevOutputError'); + + factory RawTransactionErrorNonExistentPrevOutputError.fromJson(dynamic json) { + return RawTransactionErrorNonExistentPrevOutputError(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class RawTransactionErrorSigningError extends RawTransactionError { + const RawTransactionErrorSigningError(this.value) + : super(errorType: 'SigningError'); + + factory RawTransactionErrorSigningError.fromJson(dynamic json) { + return RawTransactionErrorSigningError(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class RawTransactionErrorNotImplemented extends RawTransactionError { + const RawTransactionErrorNotImplemented({required this.coin}) + : super(errorType: 'NotImplemented'); + + factory RawTransactionErrorNotImplemented.fromJson(dynamic json) { + final map = _asJsonMap(json); + return RawTransactionErrorNotImplemented( + coin: _stringFromJson(map.value('coin')), + ); + } + + final String coin; + + @override + JsonMap toJson() => { + 'error_type': errorType, + 'error_data': {'coin': coin}, + }; +} + +final class RawTransactionErrorTransactionError extends RawTransactionError { + const RawTransactionErrorTransactionError(this.value) + : super(errorType: 'TransactionError'); + + factory RawTransactionErrorTransactionError.fromJson(dynamic json) { + return RawTransactionErrorTransactionError(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class RawTransactionErrorUnknown extends RawTransactionError { + const RawTransactionErrorUnknown({required this.data, this.rawErrorType}) + : super(errorType: 'unknown'); + + final String? rawErrorType; + final dynamic data; + + @override + JsonMap toJson() => { + 'error_type': rawErrorType ?? errorType, + 'error_data': data, + }; +} + +sealed class SignatureError { + const SignatureError({required this.errorType}); + + factory SignatureError.fromJson(dynamic json) { + final map = _asJsonMap(json); + final type = map.valueOrNull('error_type'); + final data = map.valueOrNull('error_data'); + switch (type) { + case 'InvalidRequest': + return SignatureErrorInvalidRequest.fromJson(data); + case 'InternalError': + return SignatureErrorInternalError.fromJson(data); + case 'CoinIsNotFound': + return SignatureErrorCoinIsNotFound.fromJson(data); + case 'PrefixNotFound': + return SignatureErrorPrefixNotFound.fromJson(); + default: + return SignatureErrorUnknown(rawErrorType: type, data: data); + } + } + + final String errorType; + + JsonMap toJson(); +} + +final class SignatureErrorInvalidRequest extends SignatureError { + const SignatureErrorInvalidRequest(this.value) + : super(errorType: 'InvalidRequest'); + + factory SignatureErrorInvalidRequest.fromJson(dynamic json) { + return SignatureErrorInvalidRequest(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class SignatureErrorInternalError extends SignatureError { + const SignatureErrorInternalError(this.value) + : super(errorType: 'InternalError'); + + factory SignatureErrorInternalError.fromJson(dynamic json) { + return SignatureErrorInternalError(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class SignatureErrorCoinIsNotFound extends SignatureError { + const SignatureErrorCoinIsNotFound(this.value) + : super(errorType: 'CoinIsNotFound'); + + factory SignatureErrorCoinIsNotFound.fromJson(dynamic json) { + return SignatureErrorCoinIsNotFound(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class SignatureErrorPrefixNotFound extends SignatureError { + const SignatureErrorPrefixNotFound() : super(errorType: 'PrefixNotFound'); + + factory SignatureErrorPrefixNotFound.fromJson() => + const SignatureErrorPrefixNotFound(); + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': null}; +} + +final class SignatureErrorUnknown extends SignatureError { + const SignatureErrorUnknown({required this.data, this.rawErrorType}) + : super(errorType: 'unknown'); + + final String? rawErrorType; + final dynamic data; + + @override + JsonMap toJson() => { + 'error_type': rawErrorType ?? errorType, + 'error_data': data, + }; +} + +sealed class StakingInfoError { + const StakingInfoError({required this.errorType}); + + factory StakingInfoError.fromJson(dynamic json) { + final map = _asJsonMap(json); + final type = map.valueOrNull('error_type'); + final data = map.valueOrNull('error_data'); + switch (type) { + case 'NoSuchCoin': + return StakingInfoErrorNoSuchCoin.fromJson(data); + case 'UnexpectedDerivationMethod': + return StakingInfoErrorUnexpectedDerivationMethod.fromJson(data); + case 'InvalidPayload': + return StakingInfoErrorInvalidPayload.fromJson(data); + case 'Transport': + return StakingInfoErrorTransport.fromJson(data); + case 'Internal': + return StakingInfoErrorInternal.fromJson(data); + default: + return StakingInfoErrorUnknown(rawErrorType: type, data: data); + } + } + + final String errorType; + + JsonMap toJson(); +} + +final class StakingInfoErrorNoSuchCoin extends StakingInfoError { + const StakingInfoErrorNoSuchCoin({required this.coin}) + : super(errorType: 'NoSuchCoin'); + + factory StakingInfoErrorNoSuchCoin.fromJson(dynamic json) { + final map = _asJsonMap(json); + return StakingInfoErrorNoSuchCoin( + coin: _stringFromJson(map.value('coin')), + ); + } + + final String coin; + + @override + JsonMap toJson() => { + 'error_type': errorType, + 'error_data': {'coin': coin}, + }; +} + +final class StakingInfoErrorUnexpectedDerivationMethod + extends StakingInfoError { + const StakingInfoErrorUnexpectedDerivationMethod(this.value) + : super(errorType: 'UnexpectedDerivationMethod'); + + factory StakingInfoErrorUnexpectedDerivationMethod.fromJson(dynamic json) { + return StakingInfoErrorUnexpectedDerivationMethod(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class StakingInfoErrorInvalidPayload extends StakingInfoError { + const StakingInfoErrorInvalidPayload({required this.reason}) + : super(errorType: 'InvalidPayload'); + + factory StakingInfoErrorInvalidPayload.fromJson(dynamic json) { + final map = _asJsonMap(json); + return StakingInfoErrorInvalidPayload( + reason: _stringFromJson(map.value('reason')), + ); + } + + final String reason; + + @override + JsonMap toJson() => { + 'error_type': errorType, + 'error_data': {'reason': reason}, + }; +} + +final class StakingInfoErrorTransport extends StakingInfoError { + const StakingInfoErrorTransport(this.value) : super(errorType: 'Transport'); + + factory StakingInfoErrorTransport.fromJson(dynamic json) { + return StakingInfoErrorTransport(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class StakingInfoErrorInternal extends StakingInfoError { + const StakingInfoErrorInternal(this.value) : super(errorType: 'Internal'); + + factory StakingInfoErrorInternal.fromJson(dynamic json) { + return StakingInfoErrorInternal(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class StakingInfoErrorUnknown extends StakingInfoError { + const StakingInfoErrorUnknown({required this.data, this.rawErrorType}) + : super(errorType: 'unknown'); + + final String? rawErrorType; + final dynamic data; + + @override + JsonMap toJson() => { + 'error_type': rawErrorType ?? errorType, + 'error_data': data, + }; +} + +sealed class SwapTxFeePolicyError { + const SwapTxFeePolicyError({required this.errorType}); + + factory SwapTxFeePolicyError.fromJson(dynamic json) { + final map = _asJsonMap(json); + final type = map.valueOrNull('error_type'); + final data = map.valueOrNull('error_data'); + switch (type) { + case 'NoSuchCoin': + return SwapTxFeePolicyErrorNoSuchCoin.fromJson(data); + case 'NotSupported': + return SwapTxFeePolicyErrorNotSupported.fromJson(data); + default: + return SwapTxFeePolicyErrorUnknown(rawErrorType: type, data: data); + } + } + + final String errorType; + + JsonMap toJson(); +} + +final class SwapTxFeePolicyErrorNoSuchCoin extends SwapTxFeePolicyError { + const SwapTxFeePolicyErrorNoSuchCoin(this.value) + : super(errorType: 'NoSuchCoin'); + + factory SwapTxFeePolicyErrorNoSuchCoin.fromJson(dynamic json) { + return SwapTxFeePolicyErrorNoSuchCoin(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class SwapTxFeePolicyErrorNotSupported extends SwapTxFeePolicyError { + const SwapTxFeePolicyErrorNotSupported(this.value) + : super(errorType: 'NotSupported'); + + factory SwapTxFeePolicyErrorNotSupported.fromJson(dynamic json) { + return SwapTxFeePolicyErrorNotSupported(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class SwapTxFeePolicyErrorUnknown extends SwapTxFeePolicyError { + const SwapTxFeePolicyErrorUnknown({required this.data, this.rawErrorType}) + : super(errorType: 'unknown'); + + final String? rawErrorType; + final dynamic data; + + @override + JsonMap toJson() => { + 'error_type': rawErrorType ?? errorType, + 'error_data': data, + }; +} + +sealed class VerificationError { + const VerificationError({required this.errorType}); + + factory VerificationError.fromJson(dynamic json) { + final map = _asJsonMap(json); + final type = map.valueOrNull('error_type'); + final data = map.valueOrNull('error_data'); + switch (type) { + case 'InvalidRequest': + return VerificationErrorInvalidRequest.fromJson(data); + case 'InternalError': + return VerificationErrorInternalError.fromJson(data); + case 'SignatureDecodingError': + return VerificationErrorSignatureDecodingError.fromJson(data); + case 'AddressDecodingError': + return VerificationErrorAddressDecodingError.fromJson(data); + case 'CoinIsNotFound': + return VerificationErrorCoinIsNotFound.fromJson(data); + case 'PrefixNotFound': + return VerificationErrorPrefixNotFound.fromJson(); + default: + return VerificationErrorUnknown(rawErrorType: type, data: data); + } + } + + final String errorType; + + JsonMap toJson(); +} + +final class VerificationErrorInvalidRequest extends VerificationError { + const VerificationErrorInvalidRequest(this.value) + : super(errorType: 'InvalidRequest'); + + factory VerificationErrorInvalidRequest.fromJson(dynamic json) { + return VerificationErrorInvalidRequest(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class VerificationErrorInternalError extends VerificationError { + const VerificationErrorInternalError(this.value) + : super(errorType: 'InternalError'); + + factory VerificationErrorInternalError.fromJson(dynamic json) { + return VerificationErrorInternalError(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class VerificationErrorSignatureDecodingError extends VerificationError { + const VerificationErrorSignatureDecodingError(this.value) + : super(errorType: 'SignatureDecodingError'); + + factory VerificationErrorSignatureDecodingError.fromJson(dynamic json) { + return VerificationErrorSignatureDecodingError(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class VerificationErrorAddressDecodingError extends VerificationError { + const VerificationErrorAddressDecodingError(this.value) + : super(errorType: 'AddressDecodingError'); + + factory VerificationErrorAddressDecodingError.fromJson(dynamic json) { + return VerificationErrorAddressDecodingError(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class VerificationErrorCoinIsNotFound extends VerificationError { + const VerificationErrorCoinIsNotFound(this.value) + : super(errorType: 'CoinIsNotFound'); + + factory VerificationErrorCoinIsNotFound.fromJson(dynamic json) { + return VerificationErrorCoinIsNotFound(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class VerificationErrorPrefixNotFound extends VerificationError { + const VerificationErrorPrefixNotFound() : super(errorType: 'PrefixNotFound'); + + factory VerificationErrorPrefixNotFound.fromJson() => + const VerificationErrorPrefixNotFound(); + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': null}; +} + +final class VerificationErrorUnknown extends VerificationError { + const VerificationErrorUnknown({required this.data, this.rawErrorType}) + : super(errorType: 'unknown'); + + final String? rawErrorType; + final dynamic data; + + @override + JsonMap toJson() => { + 'error_type': rawErrorType ?? errorType, + 'error_data': data, + }; +} + +sealed class WithdrawError { + const WithdrawError({required this.errorType}); + + factory WithdrawError.fromJson(dynamic json) { + final map = _asJsonMap(json); + final type = map.valueOrNull('error_type'); + final data = map.valueOrNull('error_data'); + switch (type) { + case 'CoinDoesntSupportInitWithdraw': + return WithdrawErrorCoinDoesntSupportInitWithdraw.fromJson(data); + case 'NotSufficientBalance': + return WithdrawErrorNotSufficientBalance.fromJson(data); + case 'NotSufficientPlatformBalanceForFee': + return WithdrawErrorNotSufficientPlatformBalanceForFee.fromJson(data); + case 'ZeroBalanceToWithdrawMax': + return WithdrawErrorZeroBalanceToWithdrawMax.fromJson(); + case 'AmountTooLow': + return WithdrawErrorAmountTooLow.fromJson(data); + case 'InvalidAddress': + return WithdrawErrorInvalidAddress.fromJson(data); + case 'InvalidFeePolicy': + return WithdrawErrorInvalidFeePolicy.fromJson(data); + case 'InvalidFee': + return WithdrawErrorInvalidFee.fromJson(data); + case 'InvalidMemo': + return WithdrawErrorInvalidMemo.fromJson(data); + case 'NoSuchCoin': + return WithdrawErrorNoSuchCoin.fromJson(data); + case 'Timeout': + return WithdrawErrorTimeout.fromJson(data); + case 'FromAddressNotFound': + return WithdrawErrorFromAddressNotFound.fromJson(); + case 'UnexpectedFromAddress': + return WithdrawErrorUnexpectedFromAddress.fromJson(data); + case 'UnknownAccount': + return WithdrawErrorUnknownAccount.fromJson(data); + case 'UnexpectedUserAction': + return WithdrawErrorUnexpectedUserAction.fromJson(data); + case 'HwError': + return WithdrawErrorHwError.fromJson(data); + case 'BroadcastExpected': + return WithdrawErrorBroadcastExpected.fromJson(data); + case 'Transport': + return WithdrawErrorTransport.fromJson(data); + case 'InternalError': + return WithdrawErrorInternalError.fromJson(data); + case 'UnsupportedError': + return WithdrawErrorUnsupportedError.fromJson(data); + case 'CoinDoesntSupportNftWithdraw': + return WithdrawErrorCoinDoesntSupportNftWithdraw.fromJson(data); + case 'ContractTypeDoesntSupportNftWithdrawing': + return WithdrawErrorContractTypeDoesntSupportNftWithdrawing.fromJson( + data, + ); + case 'ActionNotAllowed': + return WithdrawErrorActionNotAllowed.fromJson(data); + case 'GetNftInfoError': + return WithdrawErrorGetNftInfoError.fromJson(data); + case 'NotEnoughNftsAmount': + return WithdrawErrorNotEnoughNftsAmount.fromJson(data); + case 'DbError': + return WithdrawErrorDbError.fromJson(data); + case 'MyAddressNotNftOwner': + return WithdrawErrorMyAddressNotNftOwner.fromJson(data); + case 'NftProtocolNotSupported': + return WithdrawErrorNftProtocolNotSupported.fromJson(); + case 'NoChainIdSet': + return WithdrawErrorNoChainIdSet.fromJson(data); + case 'SigningError': + return WithdrawErrorSigningError.fromJson(data); + case 'TxTypeNotSupported': + return WithdrawErrorTxTypeNotSupported.fromJson(); + case 'IBCError': + return WithdrawErrorIBCError.fromJson(data); + default: + return WithdrawErrorUnknown(rawErrorType: type, data: data); + } + } + + final String errorType; + + JsonMap toJson(); +} + +final class WithdrawErrorCoinDoesntSupportInitWithdraw extends WithdrawError { + const WithdrawErrorCoinDoesntSupportInitWithdraw({required this.coin}) + : super(errorType: 'CoinDoesntSupportInitWithdraw'); + + factory WithdrawErrorCoinDoesntSupportInitWithdraw.fromJson(dynamic json) { + final map = _asJsonMap(json); + return WithdrawErrorCoinDoesntSupportInitWithdraw( + coin: _stringFromJson(map.value('coin')), + ); + } + + final String coin; + + @override + JsonMap toJson() => { + 'error_type': errorType, + 'error_data': {'coin': coin}, + }; +} + +final class WithdrawErrorNotSufficientBalance extends WithdrawError { + const WithdrawErrorNotSufficientBalance({ + required this.coin, + required this.available, + required this.required, + }) : super(errorType: 'NotSufficientBalance'); + + factory WithdrawErrorNotSufficientBalance.fromJson(dynamic json) { + final map = _asJsonMap(json); + return WithdrawErrorNotSufficientBalance( + coin: _stringFromJson(map.value('coin')), + available: BigDecimal.fromJson(map.value('available')), + required: BigDecimal.fromJson(map.value('required')), + ); + } + + final String coin; + final BigDecimal available; + final BigDecimal required; + + @override + JsonMap toJson() => { + 'error_type': errorType, + 'error_data': { + 'coin': coin, + 'available': available.toJson(), + 'required': required.toJson(), + }, + }; +} + +final class WithdrawErrorNotSufficientPlatformBalanceForFee + extends WithdrawError { + const WithdrawErrorNotSufficientPlatformBalanceForFee({ + required this.coin, + required this.available, + required this.required, + }) : super(errorType: 'NotSufficientPlatformBalanceForFee'); + + factory WithdrawErrorNotSufficientPlatformBalanceForFee.fromJson( + dynamic json, + ) { + final map = _asJsonMap(json); + return WithdrawErrorNotSufficientPlatformBalanceForFee( + coin: _stringFromJson(map.value('coin')), + available: BigDecimal.fromJson(map.value('available')), + required: BigDecimal.fromJson(map.value('required')), + ); + } + + final String coin; + final BigDecimal available; + final BigDecimal required; + + @override + JsonMap toJson() => { + 'error_type': errorType, + 'error_data': { + 'coin': coin, + 'available': available.toJson(), + 'required': required.toJson(), + }, + }; +} + +final class WithdrawErrorZeroBalanceToWithdrawMax extends WithdrawError { + const WithdrawErrorZeroBalanceToWithdrawMax() + : super(errorType: 'ZeroBalanceToWithdrawMax'); + + factory WithdrawErrorZeroBalanceToWithdrawMax.fromJson() => + const WithdrawErrorZeroBalanceToWithdrawMax(); + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': null}; +} + +final class WithdrawErrorAmountTooLow extends WithdrawError { + const WithdrawErrorAmountTooLow({ + required this.amount, + required this.threshold, + }) : super(errorType: 'AmountTooLow'); + + factory WithdrawErrorAmountTooLow.fromJson(dynamic json) { + final map = _asJsonMap(json); + return WithdrawErrorAmountTooLow( + amount: BigDecimal.fromJson(map.value('amount')), + threshold: BigDecimal.fromJson(map.value('threshold')), + ); + } + + final BigDecimal amount; + final BigDecimal threshold; + + @override + JsonMap toJson() => { + 'error_type': errorType, + 'error_data': {'amount': amount.toJson(), 'threshold': threshold.toJson()}, + }; +} + +final class WithdrawErrorInvalidAddress extends WithdrawError { + const WithdrawErrorInvalidAddress(this.value) + : super(errorType: 'InvalidAddress'); + + factory WithdrawErrorInvalidAddress.fromJson(dynamic json) { + return WithdrawErrorInvalidAddress(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class WithdrawErrorInvalidFeePolicy extends WithdrawError { + const WithdrawErrorInvalidFeePolicy(this.value) + : super(errorType: 'InvalidFeePolicy'); + + factory WithdrawErrorInvalidFeePolicy.fromJson(dynamic json) { + return WithdrawErrorInvalidFeePolicy(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class WithdrawErrorInvalidFee extends WithdrawError { + const WithdrawErrorInvalidFee({required this.reason, this.details}) + : super(errorType: 'InvalidFee'); + + factory WithdrawErrorInvalidFee.fromJson(dynamic json) { + final map = _asJsonMap(json); + return WithdrawErrorInvalidFee( + reason: _stringFromJson(map.value('reason')), + details: map.valueOrNull('details') == null + ? null + : JsonValue.fromJson(map.valueOrNull('details')), + ); + } + + final String reason; + final JsonValue? details; + + @override + JsonMap toJson() => { + 'error_type': errorType, + 'error_data': {'reason': reason, 'details': details?.toJson()}, + }; +} + +final class WithdrawErrorInvalidMemo extends WithdrawError { + const WithdrawErrorInvalidMemo(this.value) : super(errorType: 'InvalidMemo'); + + factory WithdrawErrorInvalidMemo.fromJson(dynamic json) { + return WithdrawErrorInvalidMemo(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class WithdrawErrorNoSuchCoin extends WithdrawError { + const WithdrawErrorNoSuchCoin({required this.coin}) + : super(errorType: 'NoSuchCoin'); + + factory WithdrawErrorNoSuchCoin.fromJson(dynamic json) { + final map = _asJsonMap(json); + return WithdrawErrorNoSuchCoin( + coin: _stringFromJson(map.value('coin')), + ); + } + + final String coin; + + @override + JsonMap toJson() => { + 'error_type': errorType, + 'error_data': {'coin': coin}, + }; +} + +final class WithdrawErrorTimeout extends WithdrawError { + const WithdrawErrorTimeout(this.value) : super(errorType: 'Timeout'); + + factory WithdrawErrorTimeout.fromJson(dynamic json) { + return WithdrawErrorTimeout(Mm2Duration.fromJson(json)); + } + + final Mm2Duration value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value.toJson()}; +} + +final class WithdrawErrorFromAddressNotFound extends WithdrawError { + const WithdrawErrorFromAddressNotFound() + : super(errorType: 'FromAddressNotFound'); + + factory WithdrawErrorFromAddressNotFound.fromJson() => + const WithdrawErrorFromAddressNotFound(); + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': null}; +} + +final class WithdrawErrorUnexpectedFromAddress extends WithdrawError { + const WithdrawErrorUnexpectedFromAddress(this.value) + : super(errorType: 'UnexpectedFromAddress'); + + factory WithdrawErrorUnexpectedFromAddress.fromJson(dynamic json) { + return WithdrawErrorUnexpectedFromAddress(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class WithdrawErrorUnknownAccount extends WithdrawError { + const WithdrawErrorUnknownAccount({required this.accountId}) + : super(errorType: 'UnknownAccount'); + + factory WithdrawErrorUnknownAccount.fromJson(dynamic json) { + final map = _asJsonMap(json); + return WithdrawErrorUnknownAccount( + accountId: _intFromJson(map.value('account_id')), + ); + } + + final int accountId; + + @override + JsonMap toJson() => { + 'error_type': errorType, + 'error_data': {'account_id': accountId}, + }; +} + +final class WithdrawErrorUnexpectedUserAction extends WithdrawError { + const WithdrawErrorUnexpectedUserAction({required this.expected}) + : super(errorType: 'UnexpectedUserAction'); + + factory WithdrawErrorUnexpectedUserAction.fromJson(dynamic json) { + final map = _asJsonMap(json); + return WithdrawErrorUnexpectedUserAction( + expected: _stringFromJson(map.value('expected')), + ); + } + + final String expected; + + @override + JsonMap toJson() => { + 'error_type': errorType, + 'error_data': {'expected': expected}, + }; +} + +final class WithdrawErrorHwError extends WithdrawError { + const WithdrawErrorHwError(this.value) : super(errorType: 'HwError'); + + factory WithdrawErrorHwError.fromJson(dynamic json) { + return WithdrawErrorHwError(HwRpcError.fromJson(json)); + } + + final HwRpcError value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value.toJson()}; +} + +final class WithdrawErrorBroadcastExpected extends WithdrawError { + const WithdrawErrorBroadcastExpected(this.value) + : super(errorType: 'BroadcastExpected'); + + factory WithdrawErrorBroadcastExpected.fromJson(dynamic json) { + return WithdrawErrorBroadcastExpected(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class WithdrawErrorTransport extends WithdrawError { + const WithdrawErrorTransport(this.value) : super(errorType: 'Transport'); + + factory WithdrawErrorTransport.fromJson(dynamic json) { + return WithdrawErrorTransport(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class WithdrawErrorInternalError extends WithdrawError { + const WithdrawErrorInternalError(this.value) + : super(errorType: 'InternalError'); + + factory WithdrawErrorInternalError.fromJson(dynamic json) { + return WithdrawErrorInternalError(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class WithdrawErrorUnsupportedError extends WithdrawError { + const WithdrawErrorUnsupportedError(this.value) + : super(errorType: 'UnsupportedError'); + + factory WithdrawErrorUnsupportedError.fromJson(dynamic json) { + return WithdrawErrorUnsupportedError(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class WithdrawErrorCoinDoesntSupportNftWithdraw extends WithdrawError { + const WithdrawErrorCoinDoesntSupportNftWithdraw({required this.coin}) + : super(errorType: 'CoinDoesntSupportNftWithdraw'); + + factory WithdrawErrorCoinDoesntSupportNftWithdraw.fromJson(dynamic json) { + final map = _asJsonMap(json); + return WithdrawErrorCoinDoesntSupportNftWithdraw( + coin: _stringFromJson(map.value('coin')), + ); + } + + final String coin; + + @override + JsonMap toJson() => { + 'error_type': errorType, + 'error_data': {'coin': coin}, + }; +} + +final class WithdrawErrorContractTypeDoesntSupportNftWithdrawing + extends WithdrawError { + const WithdrawErrorContractTypeDoesntSupportNftWithdrawing(this.value) + : super(errorType: 'ContractTypeDoesntSupportNftWithdrawing'); + + factory WithdrawErrorContractTypeDoesntSupportNftWithdrawing.fromJson( + dynamic json, + ) { + return WithdrawErrorContractTypeDoesntSupportNftWithdrawing( + _stringFromJson(json), + ); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class WithdrawErrorActionNotAllowed extends WithdrawError { + const WithdrawErrorActionNotAllowed(this.value) + : super(errorType: 'ActionNotAllowed'); + + factory WithdrawErrorActionNotAllowed.fromJson(dynamic json) { + return WithdrawErrorActionNotAllowed(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class WithdrawErrorGetNftInfoError extends WithdrawError { + const WithdrawErrorGetNftInfoError(this.value) + : super(errorType: 'GetNftInfoError'); + + factory WithdrawErrorGetNftInfoError.fromJson(dynamic json) { + return WithdrawErrorGetNftInfoError(GetNftInfoError.fromJson(json)); + } + + final GetNftInfoError value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value.toJson()}; +} + +final class WithdrawErrorNotEnoughNftsAmount extends WithdrawError { + const WithdrawErrorNotEnoughNftsAmount({ + required this.tokenAddress, + required this.tokenId, + required this.available, + required this.required, + }) : super(errorType: 'NotEnoughNftsAmount'); + + factory WithdrawErrorNotEnoughNftsAmount.fromJson(dynamic json) { + final map = _asJsonMap(json); + return WithdrawErrorNotEnoughNftsAmount( + tokenAddress: _stringFromJson(map.value('token_address')), + tokenId: _stringFromJson(map.value('token_id')), + available: BigUint.fromJson(map.value('available')), + required: BigUint.fromJson(map.value('required')), + ); + } + + final String tokenAddress; + final String tokenId; + final BigUint available; + final BigUint required; + + @override + JsonMap toJson() => { + 'error_type': errorType, + 'error_data': { + 'token_address': tokenAddress, + 'token_id': tokenId, + 'available': available.toJson(), + 'required': required.toJson(), + }, + }; +} + +final class WithdrawErrorDbError extends WithdrawError { + const WithdrawErrorDbError(this.value) : super(errorType: 'DbError'); + + factory WithdrawErrorDbError.fromJson(dynamic json) { + return WithdrawErrorDbError(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class WithdrawErrorMyAddressNotNftOwner extends WithdrawError { + const WithdrawErrorMyAddressNotNftOwner({ + required this.myAddress, + required this.tokenOwner, + }) : super(errorType: 'MyAddressNotNftOwner'); + + factory WithdrawErrorMyAddressNotNftOwner.fromJson(dynamic json) { + final map = _asJsonMap(json); + return WithdrawErrorMyAddressNotNftOwner( + myAddress: _stringFromJson(map.value('my_address')), + tokenOwner: _stringFromJson(map.value('token_owner')), + ); + } + + final String myAddress; + final String tokenOwner; + + @override + JsonMap toJson() => { + 'error_type': errorType, + 'error_data': {'my_address': myAddress, 'token_owner': tokenOwner}, + }; +} + +final class WithdrawErrorNftProtocolNotSupported extends WithdrawError { + const WithdrawErrorNftProtocolNotSupported() + : super(errorType: 'NftProtocolNotSupported'); + + factory WithdrawErrorNftProtocolNotSupported.fromJson() => + const WithdrawErrorNftProtocolNotSupported(); + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': null}; +} + +final class WithdrawErrorNoChainIdSet extends WithdrawError { + const WithdrawErrorNoChainIdSet({required this.coin}) + : super(errorType: 'NoChainIdSet'); + + factory WithdrawErrorNoChainIdSet.fromJson(dynamic json) { + final map = _asJsonMap(json); + return WithdrawErrorNoChainIdSet( + coin: _stringFromJson(map.value('coin')), + ); + } + + final String coin; + + @override + JsonMap toJson() => { + 'error_type': errorType, + 'error_data': {'coin': coin}, + }; +} + +final class WithdrawErrorSigningError extends WithdrawError { + const WithdrawErrorSigningError(this.value) + : super(errorType: 'SigningError'); + + factory WithdrawErrorSigningError.fromJson(dynamic json) { + return WithdrawErrorSigningError(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class WithdrawErrorTxTypeNotSupported extends WithdrawError { + const WithdrawErrorTxTypeNotSupported() + : super(errorType: 'TxTypeNotSupported'); + + factory WithdrawErrorTxTypeNotSupported.fromJson() => + const WithdrawErrorTxTypeNotSupported(); + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': null}; +} + +final class WithdrawErrorIBCError extends WithdrawError { + const WithdrawErrorIBCError(this.value) : super(errorType: 'IBCError'); + + factory WithdrawErrorIBCError.fromJson(dynamic json) { + return WithdrawErrorIBCError(TendermintIBCError.fromJson(json)); + } + + final TendermintIBCError value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value.toJson()}; +} + +final class WithdrawErrorUnknown extends WithdrawError { + const WithdrawErrorUnknown({required this.data, this.rawErrorType}) + : super(errorType: 'unknown'); + + final String? rawErrorType; + final dynamic data; + + @override + JsonMap toJson() => { + 'error_type': rawErrorType ?? errorType, + 'error_data': data, + }; +} + +sealed class MyTxHistoryErrorV2 { + const MyTxHistoryErrorV2({required this.errorType}); + + factory MyTxHistoryErrorV2.fromJson(dynamic json) { + final map = _asJsonMap(json); + final type = map.valueOrNull('error_type'); + final data = map.valueOrNull('error_data'); + switch (type) { + case 'CoinIsNotActive': + return MyTxHistoryErrorV2CoinIsNotActive.fromJson(data); + case 'InvalidTarget': + return MyTxHistoryErrorV2InvalidTarget.fromJson(data); + case 'StorageIsNotInitialized': + return MyTxHistoryErrorV2StorageIsNotInitialized.fromJson(data); + case 'StorageError': + return MyTxHistoryErrorV2StorageError.fromJson(data); + case 'RpcError': + return MyTxHistoryErrorV2RpcError.fromJson(data); + case 'NotSupportedFor': + return MyTxHistoryErrorV2NotSupportedFor.fromJson(data); + case 'Internal': + return MyTxHistoryErrorV2Internal.fromJson(data); + default: + return MyTxHistoryErrorV2Unknown(rawErrorType: type, data: data); + } + } + + final String errorType; + + JsonMap toJson(); +} + +final class MyTxHistoryErrorV2CoinIsNotActive extends MyTxHistoryErrorV2 { + const MyTxHistoryErrorV2CoinIsNotActive(this.value) + : super(errorType: 'CoinIsNotActive'); + + factory MyTxHistoryErrorV2CoinIsNotActive.fromJson(dynamic json) { + return MyTxHistoryErrorV2CoinIsNotActive(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class MyTxHistoryErrorV2InvalidTarget extends MyTxHistoryErrorV2 { + const MyTxHistoryErrorV2InvalidTarget(this.value) + : super(errorType: 'InvalidTarget'); + + factory MyTxHistoryErrorV2InvalidTarget.fromJson(dynamic json) { + return MyTxHistoryErrorV2InvalidTarget(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class MyTxHistoryErrorV2StorageIsNotInitialized + extends MyTxHistoryErrorV2 { + const MyTxHistoryErrorV2StorageIsNotInitialized(this.value) + : super(errorType: 'StorageIsNotInitialized'); + + factory MyTxHistoryErrorV2StorageIsNotInitialized.fromJson(dynamic json) { + return MyTxHistoryErrorV2StorageIsNotInitialized(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class MyTxHistoryErrorV2StorageError extends MyTxHistoryErrorV2 { + const MyTxHistoryErrorV2StorageError(this.value) + : super(errorType: 'StorageError'); + + factory MyTxHistoryErrorV2StorageError.fromJson(dynamic json) { + return MyTxHistoryErrorV2StorageError(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class MyTxHistoryErrorV2RpcError extends MyTxHistoryErrorV2 { + const MyTxHistoryErrorV2RpcError(this.value) : super(errorType: 'RpcError'); + + factory MyTxHistoryErrorV2RpcError.fromJson(dynamic json) { + return MyTxHistoryErrorV2RpcError(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class MyTxHistoryErrorV2NotSupportedFor extends MyTxHistoryErrorV2 { + const MyTxHistoryErrorV2NotSupportedFor(this.value) + : super(errorType: 'NotSupportedFor'); + + factory MyTxHistoryErrorV2NotSupportedFor.fromJson(dynamic json) { + return MyTxHistoryErrorV2NotSupportedFor(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class MyTxHistoryErrorV2Internal extends MyTxHistoryErrorV2 { + const MyTxHistoryErrorV2Internal(this.value) : super(errorType: 'Internal'); + + factory MyTxHistoryErrorV2Internal.fromJson(dynamic json) { + return MyTxHistoryErrorV2Internal(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class MyTxHistoryErrorV2Unknown extends MyTxHistoryErrorV2 { + const MyTxHistoryErrorV2Unknown({required this.data, this.rawErrorType}) + : super(errorType: 'unknown'); + + final String? rawErrorType; + final dynamic data; + + @override + JsonMap toJson() => { + 'error_type': rawErrorType ?? errorType, + 'error_data': data, + }; +} + +sealed class ClearNftDbError { + const ClearNftDbError({required this.errorType}); + + factory ClearNftDbError.fromJson(dynamic json) { + final map = _asJsonMap(json); + final type = map.valueOrNull('error_type'); + final data = map.valueOrNull('error_data'); + switch (type) { + case 'DbError': + return ClearNftDbErrorDbError.fromJson(data); + case 'Internal': + return ClearNftDbErrorInternal.fromJson(data); + case 'InvalidRequest': + return ClearNftDbErrorInvalidRequest.fromJson(data); + default: + return ClearNftDbErrorUnknown(rawErrorType: type, data: data); + } + } + + final String errorType; + + JsonMap toJson(); +} + +final class ClearNftDbErrorDbError extends ClearNftDbError { + const ClearNftDbErrorDbError(this.value) : super(errorType: 'DbError'); + + factory ClearNftDbErrorDbError.fromJson(dynamic json) { + return ClearNftDbErrorDbError(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class ClearNftDbErrorInternal extends ClearNftDbError { + const ClearNftDbErrorInternal(this.value) : super(errorType: 'Internal'); + + factory ClearNftDbErrorInternal.fromJson(dynamic json) { + return ClearNftDbErrorInternal(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class ClearNftDbErrorInvalidRequest extends ClearNftDbError { + const ClearNftDbErrorInvalidRequest(this.value) + : super(errorType: 'InvalidRequest'); + + factory ClearNftDbErrorInvalidRequest.fromJson(dynamic json) { + return ClearNftDbErrorInvalidRequest(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class ClearNftDbErrorUnknown extends ClearNftDbError { + const ClearNftDbErrorUnknown({required this.data, this.rawErrorType}) + : super(errorType: 'unknown'); + + final String? rawErrorType; + final dynamic data; + + @override + JsonMap toJson() => { + 'error_type': rawErrorType ?? errorType, + 'error_data': data, + }; +} + +sealed class GetNftInfoError { + const GetNftInfoError({required this.errorType}); + + factory GetNftInfoError.fromJson(dynamic json) { + final map = _asJsonMap(json); + final type = map.valueOrNull('error_type'); + final data = map.valueOrNull('error_data'); + switch (type) { + case 'InvalidRequest': + return GetNftInfoErrorInvalidRequest.fromJson(data); + case 'Transport': + return GetNftInfoErrorTransport.fromJson(data); + case 'InvalidResponse': + return GetNftInfoErrorInvalidResponse.fromJson(data); + case 'Internal': + return GetNftInfoErrorInternal.fromJson(data); + case 'GetEthAddressError': + return GetNftInfoErrorGetEthAddressError.fromJson(data); + case 'TokenNotFoundInWallet': + return GetNftInfoErrorTokenNotFoundInWallet.fromJson(data); + case 'DbError': + return GetNftInfoErrorDbError.fromJson(data); + case 'ParseRfc3339Err': + return GetNftInfoErrorParseRfc3339Err.fromJson(data); + case 'ContractTypeIsNull': + return GetNftInfoErrorContractTypeIsNull.fromJson(); + case 'ProtectFromSpamError': + return GetNftInfoErrorProtectFromSpamError.fromJson(data); + case 'TransferConfirmationsError': + return GetNftInfoErrorTransferConfirmationsError.fromJson(data); + case 'NumConversError': + return GetNftInfoErrorNumConversError.fromJson(data); + default: + return GetNftInfoErrorUnknown(rawErrorType: type, data: data); + } + } + + final String errorType; + + JsonMap toJson(); +} + +final class GetNftInfoErrorInvalidRequest extends GetNftInfoError { + const GetNftInfoErrorInvalidRequest(this.value) + : super(errorType: 'InvalidRequest'); + + factory GetNftInfoErrorInvalidRequest.fromJson(dynamic json) { + return GetNftInfoErrorInvalidRequest(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class GetNftInfoErrorTransport extends GetNftInfoError { + const GetNftInfoErrorTransport(this.value) : super(errorType: 'Transport'); + + factory GetNftInfoErrorTransport.fromJson(dynamic json) { + return GetNftInfoErrorTransport(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class GetNftInfoErrorInvalidResponse extends GetNftInfoError { + const GetNftInfoErrorInvalidResponse(this.value) + : super(errorType: 'InvalidResponse'); + + factory GetNftInfoErrorInvalidResponse.fromJson(dynamic json) { + return GetNftInfoErrorInvalidResponse(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class GetNftInfoErrorInternal extends GetNftInfoError { + const GetNftInfoErrorInternal(this.value) : super(errorType: 'Internal'); + + factory GetNftInfoErrorInternal.fromJson(dynamic json) { + return GetNftInfoErrorInternal(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class GetNftInfoErrorGetEthAddressError extends GetNftInfoError { + const GetNftInfoErrorGetEthAddressError(this.value) + : super(errorType: 'GetEthAddressError'); + + factory GetNftInfoErrorGetEthAddressError.fromJson(dynamic json) { + return GetNftInfoErrorGetEthAddressError(GetEthAddressError.fromJson(json)); + } + + final GetEthAddressError value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value.toJson()}; +} + +final class GetNftInfoErrorTokenNotFoundInWallet extends GetNftInfoError { + const GetNftInfoErrorTokenNotFoundInWallet({ + required this.tokenAddress, + required this.tokenId, + }) : super(errorType: 'TokenNotFoundInWallet'); + + factory GetNftInfoErrorTokenNotFoundInWallet.fromJson(dynamic json) { + final map = _asJsonMap(json); + return GetNftInfoErrorTokenNotFoundInWallet( + tokenAddress: _stringFromJson(map.value('token_address')), + tokenId: _stringFromJson(map.value('token_id')), + ); + } + + final String tokenAddress; + final String tokenId; + + @override + JsonMap toJson() => { + 'error_type': errorType, + 'error_data': {'token_address': tokenAddress, 'token_id': tokenId}, + }; +} + +final class GetNftInfoErrorDbError extends GetNftInfoError { + const GetNftInfoErrorDbError(this.value) : super(errorType: 'DbError'); + + factory GetNftInfoErrorDbError.fromJson(dynamic json) { + return GetNftInfoErrorDbError(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class GetNftInfoErrorParseRfc3339Err extends GetNftInfoError { + const GetNftInfoErrorParseRfc3339Err(this.value) + : super(errorType: 'ParseRfc3339Err'); + + factory GetNftInfoErrorParseRfc3339Err.fromJson(dynamic json) { + return GetNftInfoErrorParseRfc3339Err(ParseRfc3339Err.fromJson(json)); + } + + final ParseRfc3339Err value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value.toJson()}; +} + +final class GetNftInfoErrorContractTypeIsNull extends GetNftInfoError { + const GetNftInfoErrorContractTypeIsNull() + : super(errorType: 'ContractTypeIsNull'); + + factory GetNftInfoErrorContractTypeIsNull.fromJson() => + const GetNftInfoErrorContractTypeIsNull(); + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': null}; +} + +final class GetNftInfoErrorProtectFromSpamError extends GetNftInfoError { + const GetNftInfoErrorProtectFromSpamError(this.value) + : super(errorType: 'ProtectFromSpamError'); + + factory GetNftInfoErrorProtectFromSpamError.fromJson(dynamic json) { + return GetNftInfoErrorProtectFromSpamError( + ProtectFromSpamError.fromJson(json), + ); + } + + final ProtectFromSpamError value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value.toJson()}; +} + +final class GetNftInfoErrorTransferConfirmationsError extends GetNftInfoError { + const GetNftInfoErrorTransferConfirmationsError(this.value) + : super(errorType: 'TransferConfirmationsError'); + + factory GetNftInfoErrorTransferConfirmationsError.fromJson(dynamic json) { + return GetNftInfoErrorTransferConfirmationsError( + TransferConfirmationsError.fromJson(json), + ); + } + + final TransferConfirmationsError value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value.toJson()}; +} + +final class GetNftInfoErrorNumConversError extends GetNftInfoError { + const GetNftInfoErrorNumConversError(this.value) + : super(errorType: 'NumConversError'); + + factory GetNftInfoErrorNumConversError.fromJson(dynamic json) { + return GetNftInfoErrorNumConversError(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class GetNftInfoErrorUnknown extends GetNftInfoError { + const GetNftInfoErrorUnknown({required this.data, this.rawErrorType}) + : super(errorType: 'unknown'); + + final String? rawErrorType; + final dynamic data; + + @override + JsonMap toJson() => { + 'error_type': rawErrorType ?? errorType, + 'error_data': data, + }; +} + +sealed class UpdateNftError { + const UpdateNftError({required this.errorType}); + + factory UpdateNftError.fromJson(dynamic json) { + final map = _asJsonMap(json); + final type = map.valueOrNull('error_type'); + final data = map.valueOrNull('error_data'); + switch (type) { + case 'DbError': + return UpdateNftErrorDbError.fromJson(data); + case 'Internal': + return UpdateNftErrorInternal.fromJson(data); + case 'GetNftInfoError': + return UpdateNftErrorGetNftInfoError.fromJson(data); + case 'GetMyAddressError': + return UpdateNftErrorGetMyAddressError.fromJson(data); + case 'TokenNotFoundInWallet': + return UpdateNftErrorTokenNotFoundInWallet.fromJson(data); + case 'InsufficientAmountInCache': + return UpdateNftErrorInsufficientAmountInCache.fromJson(data); + case 'InvalidBlockOrder': + return UpdateNftErrorInvalidBlockOrder.fromJson(data); + case 'LastScannedBlockNotFound': + return UpdateNftErrorLastScannedBlockNotFound.fromJson(data); + case 'AttemptToReceiveAlreadyOwnedErc721': + return UpdateNftErrorAttemptToReceiveAlreadyOwnedErc721.fromJson(data); + case 'InvalidHexString': + return UpdateNftErrorInvalidHexString.fromJson(data); + case 'UpdateSpamPhishingError': + return UpdateNftErrorUpdateSpamPhishingError.fromJson(data); + case 'GetInfoFromUriError': + return UpdateNftErrorGetInfoFromUriError.fromJson(data); + case 'SerdeError': + return UpdateNftErrorSerdeError.fromJson(data); + case 'ProtectFromSpamError': + return UpdateNftErrorProtectFromSpamError.fromJson(data); + case 'NoSuchCoin': + return UpdateNftErrorNoSuchCoin.fromJson(data); + case 'CoinDoesntSupportNft': + return UpdateNftErrorCoinDoesntSupportNft.fromJson(data); + case 'PrivKeyPolicyNotAllowed': + return UpdateNftErrorPrivKeyPolicyNotAllowed.fromJson(data); + case 'UnexpectedDerivationMethod': + return UpdateNftErrorUnexpectedDerivationMethod.fromJson(data); + default: + return UpdateNftErrorUnknown(rawErrorType: type, data: data); + } + } + + final String errorType; + + JsonMap toJson(); +} + +final class UpdateNftErrorDbError extends UpdateNftError { + const UpdateNftErrorDbError(this.value) : super(errorType: 'DbError'); + + factory UpdateNftErrorDbError.fromJson(dynamic json) { + return UpdateNftErrorDbError(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class UpdateNftErrorInternal extends UpdateNftError { + const UpdateNftErrorInternal(this.value) : super(errorType: 'Internal'); + + factory UpdateNftErrorInternal.fromJson(dynamic json) { + return UpdateNftErrorInternal(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class UpdateNftErrorGetNftInfoError extends UpdateNftError { + const UpdateNftErrorGetNftInfoError(this.value) + : super(errorType: 'GetNftInfoError'); + + factory UpdateNftErrorGetNftInfoError.fromJson(dynamic json) { + return UpdateNftErrorGetNftInfoError(GetNftInfoError.fromJson(json)); + } + + final GetNftInfoError value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value.toJson()}; +} + +final class UpdateNftErrorGetMyAddressError extends UpdateNftError { + const UpdateNftErrorGetMyAddressError(this.value) + : super(errorType: 'GetMyAddressError'); + + factory UpdateNftErrorGetMyAddressError.fromJson(dynamic json) { + return UpdateNftErrorGetMyAddressError(GetMyAddressError.fromJson(json)); + } + + final GetMyAddressError value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value.toJson()}; +} + +final class UpdateNftErrorTokenNotFoundInWallet extends UpdateNftError { + const UpdateNftErrorTokenNotFoundInWallet({ + required this.tokenAddress, + required this.tokenId, + }) : super(errorType: 'TokenNotFoundInWallet'); + + factory UpdateNftErrorTokenNotFoundInWallet.fromJson(dynamic json) { + final map = _asJsonMap(json); + return UpdateNftErrorTokenNotFoundInWallet( + tokenAddress: _stringFromJson(map.value('token_address')), + tokenId: _stringFromJson(map.value('token_id')), + ); + } + + final String tokenAddress; + final String tokenId; + + @override + JsonMap toJson() => { + 'error_type': errorType, + 'error_data': {'token_address': tokenAddress, 'token_id': tokenId}, + }; +} + +final class UpdateNftErrorInsufficientAmountInCache extends UpdateNftError { + const UpdateNftErrorInsufficientAmountInCache({ + required this.amountList, + required this.amountHistory, + }) : super(errorType: 'InsufficientAmountInCache'); + + factory UpdateNftErrorInsufficientAmountInCache.fromJson(dynamic json) { + final map = _asJsonMap(json); + return UpdateNftErrorInsufficientAmountInCache( + amountList: _stringFromJson(map.value('amount_list')), + amountHistory: _stringFromJson(map.value('amount_history')), + ); + } + + final String amountList; + final String amountHistory; + + @override + JsonMap toJson() => { + 'error_type': errorType, + 'error_data': {'amount_list': amountList, 'amount_history': amountHistory}, + }; +} + +final class UpdateNftErrorInvalidBlockOrder extends UpdateNftError { + const UpdateNftErrorInvalidBlockOrder({ + required this.lastScannedBlock, + required this.lastNftBlock, + }) : super(errorType: 'InvalidBlockOrder'); + + factory UpdateNftErrorInvalidBlockOrder.fromJson(dynamic json) { + final map = _asJsonMap(json); + return UpdateNftErrorInvalidBlockOrder( + lastScannedBlock: _stringFromJson( + map.value('last_scanned_block'), + ), + lastNftBlock: _stringFromJson(map.value('last_nft_block')), + ); + } + + final String lastScannedBlock; + final String lastNftBlock; + + @override + JsonMap toJson() => { + 'error_type': errorType, + 'error_data': { + 'last_scanned_block': lastScannedBlock, + 'last_nft_block': lastNftBlock, + }, + }; +} + +final class UpdateNftErrorLastScannedBlockNotFound extends UpdateNftError { + const UpdateNftErrorLastScannedBlockNotFound({required this.lastNftBlock}) + : super(errorType: 'LastScannedBlockNotFound'); + + factory UpdateNftErrorLastScannedBlockNotFound.fromJson(dynamic json) { + final map = _asJsonMap(json); + return UpdateNftErrorLastScannedBlockNotFound( + lastNftBlock: _stringFromJson(map.value('last_nft_block')), + ); + } + + final String lastNftBlock; + + @override + JsonMap toJson() => { + 'error_type': errorType, + 'error_data': {'last_nft_block': lastNftBlock}, + }; +} + +final class UpdateNftErrorAttemptToReceiveAlreadyOwnedErc721 + extends UpdateNftError { + const UpdateNftErrorAttemptToReceiveAlreadyOwnedErc721({required this.txHash}) + : super(errorType: 'AttemptToReceiveAlreadyOwnedErc721'); + + factory UpdateNftErrorAttemptToReceiveAlreadyOwnedErc721.fromJson( + dynamic json, + ) { + final map = _asJsonMap(json); + return UpdateNftErrorAttemptToReceiveAlreadyOwnedErc721( + txHash: _stringFromJson(map.value('tx_hash')), + ); + } + + final String txHash; + + @override + JsonMap toJson() => { + 'error_type': errorType, + 'error_data': {'tx_hash': txHash}, + }; +} + +final class UpdateNftErrorInvalidHexString extends UpdateNftError { + const UpdateNftErrorInvalidHexString(this.value) + : super(errorType: 'InvalidHexString'); + + factory UpdateNftErrorInvalidHexString.fromJson(dynamic json) { + return UpdateNftErrorInvalidHexString(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class UpdateNftErrorUpdateSpamPhishingError extends UpdateNftError { + const UpdateNftErrorUpdateSpamPhishingError(this.value) + : super(errorType: 'UpdateSpamPhishingError'); + + factory UpdateNftErrorUpdateSpamPhishingError.fromJson(dynamic json) { + return UpdateNftErrorUpdateSpamPhishingError( + UpdateSpamPhishingError.fromJson(json), + ); + } + + final UpdateSpamPhishingError value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value.toJson()}; +} + +final class UpdateNftErrorGetInfoFromUriError extends UpdateNftError { + const UpdateNftErrorGetInfoFromUriError(this.value) + : super(errorType: 'GetInfoFromUriError'); + + factory UpdateNftErrorGetInfoFromUriError.fromJson(dynamic json) { + return UpdateNftErrorGetInfoFromUriError( + GetInfoFromUriError.fromJson(json), + ); + } + + final GetInfoFromUriError value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value.toJson()}; +} + +final class UpdateNftErrorSerdeError extends UpdateNftError { + const UpdateNftErrorSerdeError(this.value) : super(errorType: 'SerdeError'); + + factory UpdateNftErrorSerdeError.fromJson(dynamic json) { + return UpdateNftErrorSerdeError(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class UpdateNftErrorProtectFromSpamError extends UpdateNftError { + const UpdateNftErrorProtectFromSpamError(this.value) + : super(errorType: 'ProtectFromSpamError'); + + factory UpdateNftErrorProtectFromSpamError.fromJson(dynamic json) { + return UpdateNftErrorProtectFromSpamError( + ProtectFromSpamError.fromJson(json), + ); + } + + final ProtectFromSpamError value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value.toJson()}; +} + +final class UpdateNftErrorNoSuchCoin extends UpdateNftError { + const UpdateNftErrorNoSuchCoin({required this.coin}) + : super(errorType: 'NoSuchCoin'); + + factory UpdateNftErrorNoSuchCoin.fromJson(dynamic json) { + final map = _asJsonMap(json); + return UpdateNftErrorNoSuchCoin( + coin: _stringFromJson(map.value('coin')), + ); + } + + final String coin; + + @override + JsonMap toJson() => { + 'error_type': errorType, + 'error_data': {'coin': coin}, + }; +} + +final class UpdateNftErrorCoinDoesntSupportNft extends UpdateNftError { + const UpdateNftErrorCoinDoesntSupportNft({required this.coin}) + : super(errorType: 'CoinDoesntSupportNft'); + + factory UpdateNftErrorCoinDoesntSupportNft.fromJson(dynamic json) { + final map = _asJsonMap(json); + return UpdateNftErrorCoinDoesntSupportNft( + coin: _stringFromJson(map.value('coin')), + ); + } + + final String coin; + + @override + JsonMap toJson() => { + 'error_type': errorType, + 'error_data': {'coin': coin}, + }; +} + +final class UpdateNftErrorPrivKeyPolicyNotAllowed extends UpdateNftError { + const UpdateNftErrorPrivKeyPolicyNotAllowed(this.value) + : super(errorType: 'PrivKeyPolicyNotAllowed'); + + factory UpdateNftErrorPrivKeyPolicyNotAllowed.fromJson(dynamic json) { + return UpdateNftErrorPrivKeyPolicyNotAllowed( + PrivKeyPolicyNotAllowed.fromJson(json), + ); + } + + final PrivKeyPolicyNotAllowed value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value.toJson()}; +} + +final class UpdateNftErrorUnexpectedDerivationMethod extends UpdateNftError { + const UpdateNftErrorUnexpectedDerivationMethod(this.value) + : super(errorType: 'UnexpectedDerivationMethod'); + + factory UpdateNftErrorUnexpectedDerivationMethod.fromJson(dynamic json) { + return UpdateNftErrorUnexpectedDerivationMethod( + UnexpectedDerivationMethod.fromJson(json), + ); + } + + final UnexpectedDerivationMethod value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value.toJson()}; +} + +final class UpdateNftErrorUnknown extends UpdateNftError { + const UpdateNftErrorUnknown({required this.data, this.rawErrorType}) + : super(errorType: 'unknown'); + + final String? rawErrorType; + final dynamic data; + + @override + JsonMap toJson() => { + 'error_type': rawErrorType ?? errorType, + 'error_data': data, + }; +} + +sealed class GetCurrentMtpError { + const GetCurrentMtpError({required this.errorType}); + + factory GetCurrentMtpError.fromJson(dynamic json) { + final map = _asJsonMap(json); + final type = map.valueOrNull('error_type'); + final data = map.valueOrNull('error_data'); + switch (type) { + case 'NoSuchCoin': + return GetCurrentMtpErrorNoSuchCoin.fromJson(data); + case 'NotSupportedCoin': + return GetCurrentMtpErrorNotSupportedCoin.fromJson(data); + case 'RpcError': + return GetCurrentMtpErrorRpcError.fromJson(data); + default: + return GetCurrentMtpErrorUnknown(rawErrorType: type, data: data); + } + } + + final String errorType; + + JsonMap toJson(); +} + +final class GetCurrentMtpErrorNoSuchCoin extends GetCurrentMtpError { + const GetCurrentMtpErrorNoSuchCoin(this.value) + : super(errorType: 'NoSuchCoin'); + + factory GetCurrentMtpErrorNoSuchCoin.fromJson(dynamic json) { + return GetCurrentMtpErrorNoSuchCoin(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class GetCurrentMtpErrorNotSupportedCoin extends GetCurrentMtpError { + const GetCurrentMtpErrorNotSupportedCoin(this.value) + : super(errorType: 'NotSupportedCoin'); + + factory GetCurrentMtpErrorNotSupportedCoin.fromJson(dynamic json) { + return GetCurrentMtpErrorNotSupportedCoin(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class GetCurrentMtpErrorRpcError extends GetCurrentMtpError { + const GetCurrentMtpErrorRpcError(this.value) : super(errorType: 'RpcError'); + + factory GetCurrentMtpErrorRpcError.fromJson(dynamic json) { + return GetCurrentMtpErrorRpcError(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class GetCurrentMtpErrorUnknown extends GetCurrentMtpError { + const GetCurrentMtpErrorUnknown({required this.data, this.rawErrorType}) + : super(errorType: 'unknown'); + + final String? rawErrorType; + final dynamic data; + + @override + JsonMap toJson() => { + 'error_type': rawErrorType ?? errorType, + 'error_data': data, + }; +} + +sealed class GetEnabledCoinsError { + const GetEnabledCoinsError({required this.errorType}); + + factory GetEnabledCoinsError.fromJson(dynamic json) { + final map = _asJsonMap(json); + final type = map.valueOrNull('error_type'); + final data = map.valueOrNull('error_data'); + switch (type) { + case 'Internal': + return GetEnabledCoinsErrorInternal.fromJson(data); + default: + return GetEnabledCoinsErrorUnknown(rawErrorType: type, data: data); + } + } + + final String errorType; + + JsonMap toJson(); +} + +final class GetEnabledCoinsErrorInternal extends GetEnabledCoinsError { + const GetEnabledCoinsErrorInternal(this.value) : super(errorType: 'Internal'); + + factory GetEnabledCoinsErrorInternal.fromJson(dynamic json) { + return GetEnabledCoinsErrorInternal(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class GetEnabledCoinsErrorUnknown extends GetEnabledCoinsError { + const GetEnabledCoinsErrorUnknown({required this.data, this.rawErrorType}) + : super(errorType: 'unknown'); + + final String? rawErrorType; + final dynamic data; + + @override + JsonMap toJson() => { + 'error_type': rawErrorType ?? errorType, + 'error_data': data, + }; +} + +sealed class GetNewAddressRpcError { + const GetNewAddressRpcError({required this.errorType}); + + factory GetNewAddressRpcError.fromJson(dynamic json) { + final map = _asJsonMap(json); + final type = map.valueOrNull('error_type'); + final data = map.valueOrNull('error_data'); + switch (type) { + case 'HwContextNotInitialized': + return GetNewAddressRpcErrorHwContextNotInitialized.fromJson(); + case 'NoSuchCoin': + return GetNewAddressRpcErrorNoSuchCoin.fromJson(data); + case 'UnexpectedUserAction': + return GetNewAddressRpcErrorUnexpectedUserAction.fromJson(data); + case 'CoinIsActivatedNotWithHDWallet': + return GetNewAddressRpcErrorCoinIsActivatedNotWithHDWallet.fromJson(); + case 'UnknownAccount': + return GetNewAddressRpcErrorUnknownAccount.fromJson(data); + case 'InvalidBip44Chain': + return GetNewAddressRpcErrorInvalidBip44Chain.fromJson(data); + case 'ErrorDerivingAddress': + return GetNewAddressRpcErrorErrorDerivingAddress.fromJson(data); + case 'AddressLimitReached': + return GetNewAddressRpcErrorAddressLimitReached.fromJson(data); + case 'EmptyAddressesLimitReached': + return GetNewAddressRpcErrorEmptyAddressesLimitReached.fromJson(data); + case 'RpcInvalidResponse': + return GetNewAddressRpcErrorRpcInvalidResponse.fromJson(data); + case 'WalletStorageError': + return GetNewAddressRpcErrorWalletStorageError.fromJson(data); + case 'FailedScripthashSubscription': + return GetNewAddressRpcErrorFailedScripthashSubscription.fromJson(data); + case 'Timeout': + return GetNewAddressRpcErrorTimeout.fromJson(data); + case 'HwError': + return GetNewAddressRpcErrorHwError.fromJson(data); + case 'Transport': + return GetNewAddressRpcErrorTransport.fromJson(data); + case 'Internal': + return GetNewAddressRpcErrorInternal.fromJson(data); + default: + return GetNewAddressRpcErrorUnknown(rawErrorType: type, data: data); + } + } + + final String errorType; + + JsonMap toJson(); +} + +final class GetNewAddressRpcErrorHwContextNotInitialized + extends GetNewAddressRpcError { + const GetNewAddressRpcErrorHwContextNotInitialized() + : super(errorType: 'HwContextNotInitialized'); + + factory GetNewAddressRpcErrorHwContextNotInitialized.fromJson() => + const GetNewAddressRpcErrorHwContextNotInitialized(); + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': null}; +} + +final class GetNewAddressRpcErrorNoSuchCoin extends GetNewAddressRpcError { + const GetNewAddressRpcErrorNoSuchCoin({required this.coin}) + : super(errorType: 'NoSuchCoin'); + + factory GetNewAddressRpcErrorNoSuchCoin.fromJson(dynamic json) { + final map = _asJsonMap(json); + return GetNewAddressRpcErrorNoSuchCoin( + coin: _stringFromJson(map.value('coin')), + ); + } + + final String coin; + + @override + JsonMap toJson() => { + 'error_type': errorType, + 'error_data': {'coin': coin}, + }; +} + +final class GetNewAddressRpcErrorUnexpectedUserAction + extends GetNewAddressRpcError { + const GetNewAddressRpcErrorUnexpectedUserAction({required this.expected}) + : super(errorType: 'UnexpectedUserAction'); + + factory GetNewAddressRpcErrorUnexpectedUserAction.fromJson(dynamic json) { + final map = _asJsonMap(json); + return GetNewAddressRpcErrorUnexpectedUserAction( + expected: _stringFromJson(map.value('expected')), + ); + } + + final String expected; + + @override + JsonMap toJson() => { + 'error_type': errorType, + 'error_data': {'expected': expected}, + }; +} + +final class GetNewAddressRpcErrorCoinIsActivatedNotWithHDWallet + extends GetNewAddressRpcError { + const GetNewAddressRpcErrorCoinIsActivatedNotWithHDWallet() + : super(errorType: 'CoinIsActivatedNotWithHDWallet'); + + factory GetNewAddressRpcErrorCoinIsActivatedNotWithHDWallet.fromJson() => + const GetNewAddressRpcErrorCoinIsActivatedNotWithHDWallet(); + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': null}; +} + +final class GetNewAddressRpcErrorUnknownAccount extends GetNewAddressRpcError { + const GetNewAddressRpcErrorUnknownAccount({required this.accountId}) + : super(errorType: 'UnknownAccount'); + + factory GetNewAddressRpcErrorUnknownAccount.fromJson(dynamic json) { + final map = _asJsonMap(json); + return GetNewAddressRpcErrorUnknownAccount( + accountId: _intFromJson(map.value('account_id')), + ); + } + + final int accountId; + + @override + JsonMap toJson() => { + 'error_type': errorType, + 'error_data': {'account_id': accountId}, + }; +} + +final class GetNewAddressRpcErrorInvalidBip44Chain + extends GetNewAddressRpcError { + const GetNewAddressRpcErrorInvalidBip44Chain({required this.chain}) + : super(errorType: 'InvalidBip44Chain'); + + factory GetNewAddressRpcErrorInvalidBip44Chain.fromJson(dynamic json) { + final map = _asJsonMap(json); + return GetNewAddressRpcErrorInvalidBip44Chain( + chain: Bip44Chain.fromJson(map.value('chain')), + ); + } + + final Bip44Chain chain; + + @override + JsonMap toJson() => { + 'error_type': errorType, + 'error_data': {'chain': chain.toJson()}, + }; +} + +final class GetNewAddressRpcErrorErrorDerivingAddress + extends GetNewAddressRpcError { + const GetNewAddressRpcErrorErrorDerivingAddress(this.value) + : super(errorType: 'ErrorDerivingAddress'); + + factory GetNewAddressRpcErrorErrorDerivingAddress.fromJson(dynamic json) { + return GetNewAddressRpcErrorErrorDerivingAddress(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class GetNewAddressRpcErrorAddressLimitReached + extends GetNewAddressRpcError { + const GetNewAddressRpcErrorAddressLimitReached({ + required this.maxAddressesNumber, + }) : super(errorType: 'AddressLimitReached'); + + factory GetNewAddressRpcErrorAddressLimitReached.fromJson(dynamic json) { + final map = _asJsonMap(json); + return GetNewAddressRpcErrorAddressLimitReached( + maxAddressesNumber: _intFromJson( + map.value('max_addresses_number'), + ), + ); + } + + final int maxAddressesNumber; + + @override + JsonMap toJson() => { + 'error_type': errorType, + 'error_data': {'max_addresses_number': maxAddressesNumber}, + }; +} + +final class GetNewAddressRpcErrorEmptyAddressesLimitReached + extends GetNewAddressRpcError { + const GetNewAddressRpcErrorEmptyAddressesLimitReached({ + required this.gapLimit, + }) : super(errorType: 'EmptyAddressesLimitReached'); + + factory GetNewAddressRpcErrorEmptyAddressesLimitReached.fromJson( + dynamic json, + ) { + final map = _asJsonMap(json); + return GetNewAddressRpcErrorEmptyAddressesLimitReached( + gapLimit: _intFromJson(map.value('gap_limit')), + ); + } + + final int gapLimit; + + @override + JsonMap toJson() => { + 'error_type': errorType, + 'error_data': {'gap_limit': gapLimit}, + }; +} + +final class GetNewAddressRpcErrorRpcInvalidResponse + extends GetNewAddressRpcError { + const GetNewAddressRpcErrorRpcInvalidResponse(this.value) + : super(errorType: 'RpcInvalidResponse'); + + factory GetNewAddressRpcErrorRpcInvalidResponse.fromJson(dynamic json) { + return GetNewAddressRpcErrorRpcInvalidResponse(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class GetNewAddressRpcErrorWalletStorageError + extends GetNewAddressRpcError { + const GetNewAddressRpcErrorWalletStorageError(this.value) + : super(errorType: 'WalletStorageError'); + + factory GetNewAddressRpcErrorWalletStorageError.fromJson(dynamic json) { + return GetNewAddressRpcErrorWalletStorageError(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class GetNewAddressRpcErrorFailedScripthashSubscription + extends GetNewAddressRpcError { + const GetNewAddressRpcErrorFailedScripthashSubscription(this.value) + : super(errorType: 'FailedScripthashSubscription'); + + factory GetNewAddressRpcErrorFailedScripthashSubscription.fromJson( + dynamic json, + ) { + return GetNewAddressRpcErrorFailedScripthashSubscription( + _stringFromJson(json), + ); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class GetNewAddressRpcErrorTimeout extends GetNewAddressRpcError { + const GetNewAddressRpcErrorTimeout(this.value) : super(errorType: 'Timeout'); + + factory GetNewAddressRpcErrorTimeout.fromJson(dynamic json) { + return GetNewAddressRpcErrorTimeout(Mm2Duration.fromJson(json)); + } + + final Mm2Duration value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value.toJson()}; +} + +final class GetNewAddressRpcErrorHwError extends GetNewAddressRpcError { + const GetNewAddressRpcErrorHwError(this.value) : super(errorType: 'HwError'); + + factory GetNewAddressRpcErrorHwError.fromJson(dynamic json) { + return GetNewAddressRpcErrorHwError(HwRpcError.fromJson(json)); + } + + final HwRpcError value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value.toJson()}; +} + +final class GetNewAddressRpcErrorTransport extends GetNewAddressRpcError { + const GetNewAddressRpcErrorTransport(this.value) + : super(errorType: 'Transport'); + + factory GetNewAddressRpcErrorTransport.fromJson(dynamic json) { + return GetNewAddressRpcErrorTransport(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class GetNewAddressRpcErrorInternal extends GetNewAddressRpcError { + const GetNewAddressRpcErrorInternal(this.value) + : super(errorType: 'Internal'); + + factory GetNewAddressRpcErrorInternal.fromJson(dynamic json) { + return GetNewAddressRpcErrorInternal(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class GetNewAddressRpcErrorUnknown extends GetNewAddressRpcError { + const GetNewAddressRpcErrorUnknown({required this.data, this.rawErrorType}) + : super(errorType: 'unknown'); + + final String? rawErrorType; + final dynamic data; + + @override + JsonMap toJson() => { + 'error_type': rawErrorType ?? errorType, + 'error_data': data, + }; +} + +sealed class HDAccountBalanceRpcError { + const HDAccountBalanceRpcError({required this.errorType}); + + factory HDAccountBalanceRpcError.fromJson(dynamic json) { + final map = _asJsonMap(json); + final type = map.valueOrNull('error_type'); + final data = map.valueOrNull('error_data'); + switch (type) { + case 'NoSuchCoin': + return HDAccountBalanceRpcErrorNoSuchCoin.fromJson(data); + case 'Timeout': + return HDAccountBalanceRpcErrorTimeout.fromJson(data); + case 'CoinIsActivatedNotWithHDWallet': + return HDAccountBalanceRpcErrorCoinIsActivatedNotWithHDWallet.fromJson(); + case 'UnknownAccount': + return HDAccountBalanceRpcErrorUnknownAccount.fromJson(data); + case 'InvalidBip44Chain': + return HDAccountBalanceRpcErrorInvalidBip44Chain.fromJson(data); + case 'ErrorDerivingAddress': + return HDAccountBalanceRpcErrorErrorDerivingAddress.fromJson(data); + case 'WalletStorageError': + return HDAccountBalanceRpcErrorWalletStorageError.fromJson(data); + case 'RpcInvalidResponse': + return HDAccountBalanceRpcErrorRpcInvalidResponse.fromJson(data); + case 'FailedScripthashSubscription': + return HDAccountBalanceRpcErrorFailedScripthashSubscription.fromJson( + data, + ); + case 'Transport': + return HDAccountBalanceRpcErrorTransport.fromJson(data); + case 'Internal': + return HDAccountBalanceRpcErrorInternal.fromJson(data); + default: + return HDAccountBalanceRpcErrorUnknown(rawErrorType: type, data: data); + } + } + + final String errorType; + + JsonMap toJson(); +} + +final class HDAccountBalanceRpcErrorNoSuchCoin + extends HDAccountBalanceRpcError { + const HDAccountBalanceRpcErrorNoSuchCoin({required this.coin}) + : super(errorType: 'NoSuchCoin'); + + factory HDAccountBalanceRpcErrorNoSuchCoin.fromJson(dynamic json) { + final map = _asJsonMap(json); + return HDAccountBalanceRpcErrorNoSuchCoin( + coin: _stringFromJson(map.value('coin')), + ); + } + + final String coin; + + @override + JsonMap toJson() => { + 'error_type': errorType, + 'error_data': {'coin': coin}, + }; +} + +final class HDAccountBalanceRpcErrorTimeout extends HDAccountBalanceRpcError { + const HDAccountBalanceRpcErrorTimeout(this.value) + : super(errorType: 'Timeout'); + + factory HDAccountBalanceRpcErrorTimeout.fromJson(dynamic json) { + return HDAccountBalanceRpcErrorTimeout(Mm2Duration.fromJson(json)); + } + + final Mm2Duration value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value.toJson()}; +} + +final class HDAccountBalanceRpcErrorCoinIsActivatedNotWithHDWallet + extends HDAccountBalanceRpcError { + const HDAccountBalanceRpcErrorCoinIsActivatedNotWithHDWallet() + : super(errorType: 'CoinIsActivatedNotWithHDWallet'); + + factory HDAccountBalanceRpcErrorCoinIsActivatedNotWithHDWallet.fromJson() => + const HDAccountBalanceRpcErrorCoinIsActivatedNotWithHDWallet(); + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': null}; +} + +final class HDAccountBalanceRpcErrorUnknownAccount + extends HDAccountBalanceRpcError { + const HDAccountBalanceRpcErrorUnknownAccount({required this.accountId}) + : super(errorType: 'UnknownAccount'); + + factory HDAccountBalanceRpcErrorUnknownAccount.fromJson(dynamic json) { + final map = _asJsonMap(json); + return HDAccountBalanceRpcErrorUnknownAccount( + accountId: _intFromJson(map.value('account_id')), + ); + } + + final int accountId; + + @override + JsonMap toJson() => { + 'error_type': errorType, + 'error_data': {'account_id': accountId}, + }; +} + +final class HDAccountBalanceRpcErrorInvalidBip44Chain + extends HDAccountBalanceRpcError { + const HDAccountBalanceRpcErrorInvalidBip44Chain({required this.chain}) + : super(errorType: 'InvalidBip44Chain'); + + factory HDAccountBalanceRpcErrorInvalidBip44Chain.fromJson(dynamic json) { + final map = _asJsonMap(json); + return HDAccountBalanceRpcErrorInvalidBip44Chain( + chain: Bip44Chain.fromJson(map.value('chain')), + ); + } + + final Bip44Chain chain; + + @override + JsonMap toJson() => { + 'error_type': errorType, + 'error_data': {'chain': chain.toJson()}, + }; +} + +final class HDAccountBalanceRpcErrorErrorDerivingAddress + extends HDAccountBalanceRpcError { + const HDAccountBalanceRpcErrorErrorDerivingAddress(this.value) + : super(errorType: 'ErrorDerivingAddress'); + + factory HDAccountBalanceRpcErrorErrorDerivingAddress.fromJson(dynamic json) { + return HDAccountBalanceRpcErrorErrorDerivingAddress(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class HDAccountBalanceRpcErrorWalletStorageError + extends HDAccountBalanceRpcError { + const HDAccountBalanceRpcErrorWalletStorageError(this.value) + : super(errorType: 'WalletStorageError'); + + factory HDAccountBalanceRpcErrorWalletStorageError.fromJson(dynamic json) { + return HDAccountBalanceRpcErrorWalletStorageError(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class HDAccountBalanceRpcErrorRpcInvalidResponse + extends HDAccountBalanceRpcError { + const HDAccountBalanceRpcErrorRpcInvalidResponse(this.value) + : super(errorType: 'RpcInvalidResponse'); + + factory HDAccountBalanceRpcErrorRpcInvalidResponse.fromJson(dynamic json) { + return HDAccountBalanceRpcErrorRpcInvalidResponse(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class HDAccountBalanceRpcErrorFailedScripthashSubscription + extends HDAccountBalanceRpcError { + const HDAccountBalanceRpcErrorFailedScripthashSubscription(this.value) + : super(errorType: 'FailedScripthashSubscription'); + + factory HDAccountBalanceRpcErrorFailedScripthashSubscription.fromJson( + dynamic json, + ) { + return HDAccountBalanceRpcErrorFailedScripthashSubscription( + _stringFromJson(json), + ); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class HDAccountBalanceRpcErrorTransport extends HDAccountBalanceRpcError { + const HDAccountBalanceRpcErrorTransport(this.value) + : super(errorType: 'Transport'); + + factory HDAccountBalanceRpcErrorTransport.fromJson(dynamic json) { + return HDAccountBalanceRpcErrorTransport(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class HDAccountBalanceRpcErrorInternal extends HDAccountBalanceRpcError { + const HDAccountBalanceRpcErrorInternal(this.value) + : super(errorType: 'Internal'); + + factory HDAccountBalanceRpcErrorInternal.fromJson(dynamic json) { + return HDAccountBalanceRpcErrorInternal(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class HDAccountBalanceRpcErrorUnknown extends HDAccountBalanceRpcError { + const HDAccountBalanceRpcErrorUnknown({required this.data, this.rawErrorType}) + : super(errorType: 'unknown'); + + final String? rawErrorType; + final dynamic data; + + @override + JsonMap toJson() => { + 'error_type': rawErrorType ?? errorType, + 'error_data': data, + }; +} + +sealed class CreateAccountRpcError { + const CreateAccountRpcError({required this.errorType}); + + factory CreateAccountRpcError.fromJson(dynamic json) { + final map = _asJsonMap(json); + final type = map.valueOrNull('error_type'); + final data = map.valueOrNull('error_data'); + switch (type) { + case 'HwContextNotInitialized': + return CreateAccountRpcErrorHwContextNotInitialized.fromJson(); + case 'NoSuchCoin': + return CreateAccountRpcErrorNoSuchCoin.fromJson(data); + case 'UnexpectedUserAction': + return CreateAccountRpcErrorUnexpectedUserAction.fromJson(data); + case 'Timeout': + return CreateAccountRpcErrorTimeout.fromJson(data); + case 'CoinIsActivatedNotWithHDWallet': + return CreateAccountRpcErrorCoinIsActivatedNotWithHDWallet.fromJson(); + case 'InvalidBip44Chain': + return CreateAccountRpcErrorInvalidBip44Chain.fromJson(data); + case 'AccountLimitReached': + return CreateAccountRpcErrorAccountLimitReached.fromJson(data); + case 'RpcInvalidResponse': + return CreateAccountRpcErrorRpcInvalidResponse.fromJson(data); + case 'WalletStorageError': + return CreateAccountRpcErrorWalletStorageError.fromJson(data); + case 'HwError': + return CreateAccountRpcErrorHwError.fromJson(data); + case 'Transport': + return CreateAccountRpcErrorTransport.fromJson(data); + case 'Internal': + return CreateAccountRpcErrorInternal.fromJson(data); + default: + return CreateAccountRpcErrorUnknown(rawErrorType: type, data: data); + } + } + + final String errorType; + + JsonMap toJson(); +} + +final class CreateAccountRpcErrorHwContextNotInitialized + extends CreateAccountRpcError { + const CreateAccountRpcErrorHwContextNotInitialized() + : super(errorType: 'HwContextNotInitialized'); + + factory CreateAccountRpcErrorHwContextNotInitialized.fromJson() => + const CreateAccountRpcErrorHwContextNotInitialized(); + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': null}; +} + +final class CreateAccountRpcErrorNoSuchCoin extends CreateAccountRpcError { + const CreateAccountRpcErrorNoSuchCoin({required this.coin}) + : super(errorType: 'NoSuchCoin'); + + factory CreateAccountRpcErrorNoSuchCoin.fromJson(dynamic json) { + final map = _asJsonMap(json); + return CreateAccountRpcErrorNoSuchCoin( + coin: _stringFromJson(map.value('coin')), + ); + } + + final String coin; + + @override + JsonMap toJson() => { + 'error_type': errorType, + 'error_data': {'coin': coin}, + }; +} + +final class CreateAccountRpcErrorUnexpectedUserAction + extends CreateAccountRpcError { + const CreateAccountRpcErrorUnexpectedUserAction({required this.expected}) + : super(errorType: 'UnexpectedUserAction'); + + factory CreateAccountRpcErrorUnexpectedUserAction.fromJson(dynamic json) { + final map = _asJsonMap(json); + return CreateAccountRpcErrorUnexpectedUserAction( + expected: _stringFromJson(map.value('expected')), + ); + } + + final String expected; + + @override + JsonMap toJson() => { + 'error_type': errorType, + 'error_data': {'expected': expected}, + }; +} + +final class CreateAccountRpcErrorTimeout extends CreateAccountRpcError { + const CreateAccountRpcErrorTimeout(this.value) : super(errorType: 'Timeout'); + + factory CreateAccountRpcErrorTimeout.fromJson(dynamic json) { + return CreateAccountRpcErrorTimeout(Mm2Duration.fromJson(json)); + } + + final Mm2Duration value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value.toJson()}; +} + +final class CreateAccountRpcErrorCoinIsActivatedNotWithHDWallet + extends CreateAccountRpcError { + const CreateAccountRpcErrorCoinIsActivatedNotWithHDWallet() + : super(errorType: 'CoinIsActivatedNotWithHDWallet'); + + factory CreateAccountRpcErrorCoinIsActivatedNotWithHDWallet.fromJson() => + const CreateAccountRpcErrorCoinIsActivatedNotWithHDWallet(); + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': null}; +} + +final class CreateAccountRpcErrorInvalidBip44Chain + extends CreateAccountRpcError { + const CreateAccountRpcErrorInvalidBip44Chain({required this.chain}) + : super(errorType: 'InvalidBip44Chain'); + + factory CreateAccountRpcErrorInvalidBip44Chain.fromJson(dynamic json) { + final map = _asJsonMap(json); + return CreateAccountRpcErrorInvalidBip44Chain( + chain: Bip44Chain.fromJson(map.value('chain')), + ); + } + + final Bip44Chain chain; + + @override + JsonMap toJson() => { + 'error_type': errorType, + 'error_data': {'chain': chain.toJson()}, + }; +} + +final class CreateAccountRpcErrorAccountLimitReached + extends CreateAccountRpcError { + const CreateAccountRpcErrorAccountLimitReached({ + required this.maxAccountsNumber, + }) : super(errorType: 'AccountLimitReached'); + + factory CreateAccountRpcErrorAccountLimitReached.fromJson(dynamic json) { + final map = _asJsonMap(json); + return CreateAccountRpcErrorAccountLimitReached( + maxAccountsNumber: _intFromJson( + map.value('max_accounts_number'), + ), + ); + } + + final int maxAccountsNumber; + + @override + JsonMap toJson() => { + 'error_type': errorType, + 'error_data': {'max_accounts_number': maxAccountsNumber}, + }; +} + +final class CreateAccountRpcErrorRpcInvalidResponse + extends CreateAccountRpcError { + const CreateAccountRpcErrorRpcInvalidResponse(this.value) + : super(errorType: 'RpcInvalidResponse'); + + factory CreateAccountRpcErrorRpcInvalidResponse.fromJson(dynamic json) { + return CreateAccountRpcErrorRpcInvalidResponse(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class CreateAccountRpcErrorWalletStorageError + extends CreateAccountRpcError { + const CreateAccountRpcErrorWalletStorageError(this.value) + : super(errorType: 'WalletStorageError'); + + factory CreateAccountRpcErrorWalletStorageError.fromJson(dynamic json) { + return CreateAccountRpcErrorWalletStorageError(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class CreateAccountRpcErrorHwError extends CreateAccountRpcError { + const CreateAccountRpcErrorHwError(this.value) : super(errorType: 'HwError'); + + factory CreateAccountRpcErrorHwError.fromJson(dynamic json) { + return CreateAccountRpcErrorHwError(HwRpcError.fromJson(json)); + } + + final HwRpcError value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value.toJson()}; +} + +final class CreateAccountRpcErrorTransport extends CreateAccountRpcError { + const CreateAccountRpcErrorTransport(this.value) + : super(errorType: 'Transport'); + + factory CreateAccountRpcErrorTransport.fromJson(dynamic json) { + return CreateAccountRpcErrorTransport(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class CreateAccountRpcErrorInternal extends CreateAccountRpcError { + const CreateAccountRpcErrorInternal(this.value) + : super(errorType: 'Internal'); + + factory CreateAccountRpcErrorInternal.fromJson(dynamic json) { + return CreateAccountRpcErrorInternal(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class CreateAccountRpcErrorUnknown extends CreateAccountRpcError { + const CreateAccountRpcErrorUnknown({required this.data, this.rawErrorType}) + : super(errorType: 'unknown'); + + final String? rawErrorType; + final dynamic data; + + @override + JsonMap toJson() => { + 'error_type': rawErrorType ?? errorType, + 'error_data': data, + }; +} + +sealed class CloseChannelError { + const CloseChannelError({required this.errorType}); + + factory CloseChannelError.fromJson(dynamic json) { + final map = _asJsonMap(json); + final type = map.valueOrNull('error_type'); + final data = map.valueOrNull('error_data'); + switch (type) { + case 'UnsupportedCoin': + return CloseChannelErrorUnsupportedCoin.fromJson(data); + case 'NoSuchCoin': + return CloseChannelErrorNoSuchCoin.fromJson(data); + case 'NoSuchChannel': + return CloseChannelErrorNoSuchChannel.fromJson(data); + case 'CloseChannelError': + return CloseChannelErrorCloseChannelError.fromJson(data); + default: + return CloseChannelErrorUnknown(rawErrorType: type, data: data); + } + } + + final String errorType; + + JsonMap toJson(); +} + +final class CloseChannelErrorUnsupportedCoin extends CloseChannelError { + const CloseChannelErrorUnsupportedCoin(this.value) + : super(errorType: 'UnsupportedCoin'); + + factory CloseChannelErrorUnsupportedCoin.fromJson(dynamic json) { + return CloseChannelErrorUnsupportedCoin(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class CloseChannelErrorNoSuchCoin extends CloseChannelError { + const CloseChannelErrorNoSuchCoin(this.value) + : super(errorType: 'NoSuchCoin'); + + factory CloseChannelErrorNoSuchCoin.fromJson(dynamic json) { + return CloseChannelErrorNoSuchCoin(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class CloseChannelErrorNoSuchChannel extends CloseChannelError { + const CloseChannelErrorNoSuchChannel(this.value) + : super(errorType: 'NoSuchChannel'); + + factory CloseChannelErrorNoSuchChannel.fromJson(dynamic json) { + return CloseChannelErrorNoSuchChannel(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class CloseChannelErrorCloseChannelError extends CloseChannelError { + const CloseChannelErrorCloseChannelError(this.value) + : super(errorType: 'CloseChannelError'); + + factory CloseChannelErrorCloseChannelError.fromJson(dynamic json) { + return CloseChannelErrorCloseChannelError(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class CloseChannelErrorUnknown extends CloseChannelError { + const CloseChannelErrorUnknown({required this.data, this.rawErrorType}) + : super(errorType: 'unknown'); + + final String? rawErrorType; + final dynamic data; + + @override + JsonMap toJson() => { + 'error_type': rawErrorType ?? errorType, + 'error_data': data, + }; +} + +sealed class ConnectToNodeError { + const ConnectToNodeError({required this.errorType}); + + factory ConnectToNodeError.fromJson(dynamic json) { + final map = _asJsonMap(json); + final type = map.valueOrNull('error_type'); + final data = map.valueOrNull('error_data'); + switch (type) { + case 'ParseError': + return ConnectToNodeErrorParseError.fromJson(data); + case 'ConnectionError': + return ConnectToNodeErrorConnectionError.fromJson(data); + case 'IOError': + return ConnectToNodeErrorIOError.fromJson(data); + case 'UnsupportedCoin': + return ConnectToNodeErrorUnsupportedCoin.fromJson(data); + case 'NoSuchCoin': + return ConnectToNodeErrorNoSuchCoin.fromJson(data); + default: + return ConnectToNodeErrorUnknown(rawErrorType: type, data: data); + } + } + + final String errorType; + + JsonMap toJson(); +} + +final class ConnectToNodeErrorParseError extends ConnectToNodeError { + const ConnectToNodeErrorParseError(this.value) + : super(errorType: 'ParseError'); + + factory ConnectToNodeErrorParseError.fromJson(dynamic json) { + return ConnectToNodeErrorParseError(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class ConnectToNodeErrorConnectionError extends ConnectToNodeError { + const ConnectToNodeErrorConnectionError(this.value) + : super(errorType: 'ConnectionError'); + + factory ConnectToNodeErrorConnectionError.fromJson(dynamic json) { + return ConnectToNodeErrorConnectionError(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class ConnectToNodeErrorIOError extends ConnectToNodeError { + const ConnectToNodeErrorIOError(this.value) : super(errorType: 'IOError'); + + factory ConnectToNodeErrorIOError.fromJson(dynamic json) { + return ConnectToNodeErrorIOError(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class ConnectToNodeErrorUnsupportedCoin extends ConnectToNodeError { + const ConnectToNodeErrorUnsupportedCoin(this.value) + : super(errorType: 'UnsupportedCoin'); + + factory ConnectToNodeErrorUnsupportedCoin.fromJson(dynamic json) { + return ConnectToNodeErrorUnsupportedCoin(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class ConnectToNodeErrorNoSuchCoin extends ConnectToNodeError { + const ConnectToNodeErrorNoSuchCoin(this.value) + : super(errorType: 'NoSuchCoin'); + + factory ConnectToNodeErrorNoSuchCoin.fromJson(dynamic json) { + return ConnectToNodeErrorNoSuchCoin(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class ConnectToNodeErrorUnknown extends ConnectToNodeError { + const ConnectToNodeErrorUnknown({required this.data, this.rawErrorType}) + : super(errorType: 'unknown'); + + final String? rawErrorType; + final dynamic data; + + @override + JsonMap toJson() => { + 'error_type': rawErrorType ?? errorType, + 'error_data': data, + }; +} + +sealed class GenerateInvoiceError { + const GenerateInvoiceError({required this.errorType}); + + factory GenerateInvoiceError.fromJson(dynamic json) { + final map = _asJsonMap(json); + final type = map.valueOrNull('error_type'); + final data = map.valueOrNull('error_data'); + switch (type) { + case 'UnsupportedCoin': + return GenerateInvoiceErrorUnsupportedCoin.fromJson(data); + case 'NoSuchCoin': + return GenerateInvoiceErrorNoSuchCoin.fromJson(data); + case 'SignOrCreationError': + return GenerateInvoiceErrorSignOrCreationError.fromJson(data); + case 'DbError': + return GenerateInvoiceErrorDbError.fromJson(data); + default: + return GenerateInvoiceErrorUnknown(rawErrorType: type, data: data); + } + } + + final String errorType; + + JsonMap toJson(); +} + +final class GenerateInvoiceErrorUnsupportedCoin extends GenerateInvoiceError { + const GenerateInvoiceErrorUnsupportedCoin(this.value) + : super(errorType: 'UnsupportedCoin'); + + factory GenerateInvoiceErrorUnsupportedCoin.fromJson(dynamic json) { + return GenerateInvoiceErrorUnsupportedCoin(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class GenerateInvoiceErrorNoSuchCoin extends GenerateInvoiceError { + const GenerateInvoiceErrorNoSuchCoin(this.value) + : super(errorType: 'NoSuchCoin'); + + factory GenerateInvoiceErrorNoSuchCoin.fromJson(dynamic json) { + return GenerateInvoiceErrorNoSuchCoin(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class GenerateInvoiceErrorSignOrCreationError + extends GenerateInvoiceError { + const GenerateInvoiceErrorSignOrCreationError(this.value) + : super(errorType: 'SignOrCreationError'); + + factory GenerateInvoiceErrorSignOrCreationError.fromJson(dynamic json) { + return GenerateInvoiceErrorSignOrCreationError(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class GenerateInvoiceErrorDbError extends GenerateInvoiceError { + const GenerateInvoiceErrorDbError(this.value) : super(errorType: 'DbError'); + + factory GenerateInvoiceErrorDbError.fromJson(dynamic json) { + return GenerateInvoiceErrorDbError(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class GenerateInvoiceErrorUnknown extends GenerateInvoiceError { + const GenerateInvoiceErrorUnknown({required this.data, this.rawErrorType}) + : super(errorType: 'unknown'); + + final String? rawErrorType; + final dynamic data; + + @override + JsonMap toJson() => { + 'error_type': rawErrorType ?? errorType, + 'error_data': data, + }; +} + +sealed class GetChannelDetailsError { + const GetChannelDetailsError({required this.errorType}); + + factory GetChannelDetailsError.fromJson(dynamic json) { + final map = _asJsonMap(json); + final type = map.valueOrNull('error_type'); + final data = map.valueOrNull('error_data'); + switch (type) { + case 'UnsupportedCoin': + return GetChannelDetailsErrorUnsupportedCoin.fromJson(data); + case 'NoSuchCoin': + return GetChannelDetailsErrorNoSuchCoin.fromJson(data); + case 'NoSuchChannel': + return GetChannelDetailsErrorNoSuchChannel.fromJson(data); + case 'DbError': + return GetChannelDetailsErrorDbError.fromJson(data); + default: + return GetChannelDetailsErrorUnknown(rawErrorType: type, data: data); + } + } + + final String errorType; + + JsonMap toJson(); +} + +final class GetChannelDetailsErrorUnsupportedCoin + extends GetChannelDetailsError { + const GetChannelDetailsErrorUnsupportedCoin(this.value) + : super(errorType: 'UnsupportedCoin'); + + factory GetChannelDetailsErrorUnsupportedCoin.fromJson(dynamic json) { + return GetChannelDetailsErrorUnsupportedCoin(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class GetChannelDetailsErrorNoSuchCoin extends GetChannelDetailsError { + const GetChannelDetailsErrorNoSuchCoin(this.value) + : super(errorType: 'NoSuchCoin'); + + factory GetChannelDetailsErrorNoSuchCoin.fromJson(dynamic json) { + return GetChannelDetailsErrorNoSuchCoin(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class GetChannelDetailsErrorNoSuchChannel extends GetChannelDetailsError { + const GetChannelDetailsErrorNoSuchChannel(this.value) + : super(errorType: 'NoSuchChannel'); + + factory GetChannelDetailsErrorNoSuchChannel.fromJson(dynamic json) { + return GetChannelDetailsErrorNoSuchChannel(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class GetChannelDetailsErrorDbError extends GetChannelDetailsError { + const GetChannelDetailsErrorDbError(this.value) : super(errorType: 'DbError'); + + factory GetChannelDetailsErrorDbError.fromJson(dynamic json) { + return GetChannelDetailsErrorDbError(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class GetChannelDetailsErrorUnknown extends GetChannelDetailsError { + const GetChannelDetailsErrorUnknown({required this.data, this.rawErrorType}) + : super(errorType: 'unknown'); + + final String? rawErrorType; + final dynamic data; + + @override + JsonMap toJson() => { + 'error_type': rawErrorType ?? errorType, + 'error_data': data, + }; +} + +sealed class ClaimableBalancesError { + const ClaimableBalancesError({required this.errorType}); + + factory ClaimableBalancesError.fromJson(dynamic json) { + final map = _asJsonMap(json); + final type = map.valueOrNull('error_type'); + final data = map.valueOrNull('error_data'); + switch (type) { + case 'UnsupportedCoin': + return ClaimableBalancesErrorUnsupportedCoin.fromJson(data); + case 'NoSuchCoin': + return ClaimableBalancesErrorNoSuchCoin.fromJson(data); + default: + return ClaimableBalancesErrorUnknown(rawErrorType: type, data: data); + } + } + + final String errorType; + + JsonMap toJson(); +} + +final class ClaimableBalancesErrorUnsupportedCoin + extends ClaimableBalancesError { + const ClaimableBalancesErrorUnsupportedCoin(this.value) + : super(errorType: 'UnsupportedCoin'); + + factory ClaimableBalancesErrorUnsupportedCoin.fromJson(dynamic json) { + return ClaimableBalancesErrorUnsupportedCoin(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class ClaimableBalancesErrorNoSuchCoin extends ClaimableBalancesError { + const ClaimableBalancesErrorNoSuchCoin(this.value) + : super(errorType: 'NoSuchCoin'); + + factory ClaimableBalancesErrorNoSuchCoin.fromJson(dynamic json) { + return ClaimableBalancesErrorNoSuchCoin(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class ClaimableBalancesErrorUnknown extends ClaimableBalancesError { + const ClaimableBalancesErrorUnknown({required this.data, this.rawErrorType}) + : super(errorType: 'unknown'); + + final String? rawErrorType; + final dynamic data; + + @override + JsonMap toJson() => { + 'error_type': rawErrorType ?? errorType, + 'error_data': data, + }; +} + +sealed class GetPaymentDetailsError { + const GetPaymentDetailsError({required this.errorType}); + + factory GetPaymentDetailsError.fromJson(dynamic json) { + final map = _asJsonMap(json); + final type = map.valueOrNull('error_type'); + final data = map.valueOrNull('error_data'); + switch (type) { + case 'UnsupportedCoin': + return GetPaymentDetailsErrorUnsupportedCoin.fromJson(data); + case 'NoSuchCoin': + return GetPaymentDetailsErrorNoSuchCoin.fromJson(data); + case 'NoSuchPayment': + return GetPaymentDetailsErrorNoSuchPayment.fromJson(data); + case 'DbError': + return GetPaymentDetailsErrorDbError.fromJson(data); + default: + return GetPaymentDetailsErrorUnknown(rawErrorType: type, data: data); + } + } + + final String errorType; + + JsonMap toJson(); +} + +final class GetPaymentDetailsErrorUnsupportedCoin + extends GetPaymentDetailsError { + const GetPaymentDetailsErrorUnsupportedCoin(this.value) + : super(errorType: 'UnsupportedCoin'); + + factory GetPaymentDetailsErrorUnsupportedCoin.fromJson(dynamic json) { + return GetPaymentDetailsErrorUnsupportedCoin(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class GetPaymentDetailsErrorNoSuchCoin extends GetPaymentDetailsError { + const GetPaymentDetailsErrorNoSuchCoin(this.value) + : super(errorType: 'NoSuchCoin'); + + factory GetPaymentDetailsErrorNoSuchCoin.fromJson(dynamic json) { + return GetPaymentDetailsErrorNoSuchCoin(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class GetPaymentDetailsErrorNoSuchPayment extends GetPaymentDetailsError { + const GetPaymentDetailsErrorNoSuchPayment(this.value) + : super(errorType: 'NoSuchPayment'); + + factory GetPaymentDetailsErrorNoSuchPayment.fromJson(dynamic json) { + return GetPaymentDetailsErrorNoSuchPayment(H256Json.fromJson(json)); + } + + final H256Json value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value.toJson()}; +} + +final class GetPaymentDetailsErrorDbError extends GetPaymentDetailsError { + const GetPaymentDetailsErrorDbError(this.value) : super(errorType: 'DbError'); + + factory GetPaymentDetailsErrorDbError.fromJson(dynamic json) { + return GetPaymentDetailsErrorDbError(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class GetPaymentDetailsErrorUnknown extends GetPaymentDetailsError { + const GetPaymentDetailsErrorUnknown({required this.data, this.rawErrorType}) + : super(errorType: 'unknown'); + + final String? rawErrorType; + final dynamic data; + + @override + JsonMap toJson() => { + 'error_type': rawErrorType ?? errorType, + 'error_data': data, + }; +} + +sealed class ListChannelsError { + const ListChannelsError({required this.errorType}); + + factory ListChannelsError.fromJson(dynamic json) { + final map = _asJsonMap(json); + final type = map.valueOrNull('error_type'); + final data = map.valueOrNull('error_data'); + switch (type) { + case 'UnsupportedCoin': + return ListChannelsErrorUnsupportedCoin.fromJson(data); + case 'NoSuchCoin': + return ListChannelsErrorNoSuchCoin.fromJson(data); + case 'DbError': + return ListChannelsErrorDbError.fromJson(data); + default: + return ListChannelsErrorUnknown(rawErrorType: type, data: data); + } + } + + final String errorType; + + JsonMap toJson(); +} + +final class ListChannelsErrorUnsupportedCoin extends ListChannelsError { + const ListChannelsErrorUnsupportedCoin(this.value) + : super(errorType: 'UnsupportedCoin'); + + factory ListChannelsErrorUnsupportedCoin.fromJson(dynamic json) { + return ListChannelsErrorUnsupportedCoin(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class ListChannelsErrorNoSuchCoin extends ListChannelsError { + const ListChannelsErrorNoSuchCoin(this.value) + : super(errorType: 'NoSuchCoin'); + + factory ListChannelsErrorNoSuchCoin.fromJson(dynamic json) { + return ListChannelsErrorNoSuchCoin(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class ListChannelsErrorDbError extends ListChannelsError { + const ListChannelsErrorDbError(this.value) : super(errorType: 'DbError'); + + factory ListChannelsErrorDbError.fromJson(dynamic json) { + return ListChannelsErrorDbError(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class ListChannelsErrorUnknown extends ListChannelsError { + const ListChannelsErrorUnknown({required this.data, this.rawErrorType}) + : super(errorType: 'unknown'); + + final String? rawErrorType; + final dynamic data; + + @override + JsonMap toJson() => { + 'error_type': rawErrorType ?? errorType, + 'error_data': data, + }; +} + +sealed class ListPaymentsError { + const ListPaymentsError({required this.errorType}); + + factory ListPaymentsError.fromJson(dynamic json) { + final map = _asJsonMap(json); + final type = map.valueOrNull('error_type'); + final data = map.valueOrNull('error_data'); + switch (type) { + case 'UnsupportedCoin': + return ListPaymentsErrorUnsupportedCoin.fromJson(data); + case 'NoSuchCoin': + return ListPaymentsErrorNoSuchCoin.fromJson(data); + case 'DbError': + return ListPaymentsErrorDbError.fromJson(data); + default: + return ListPaymentsErrorUnknown(rawErrorType: type, data: data); + } + } + + final String errorType; + + JsonMap toJson(); +} + +final class ListPaymentsErrorUnsupportedCoin extends ListPaymentsError { + const ListPaymentsErrorUnsupportedCoin(this.value) + : super(errorType: 'UnsupportedCoin'); + + factory ListPaymentsErrorUnsupportedCoin.fromJson(dynamic json) { + return ListPaymentsErrorUnsupportedCoin(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class ListPaymentsErrorNoSuchCoin extends ListPaymentsError { + const ListPaymentsErrorNoSuchCoin(this.value) + : super(errorType: 'NoSuchCoin'); + + factory ListPaymentsErrorNoSuchCoin.fromJson(dynamic json) { + return ListPaymentsErrorNoSuchCoin(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class ListPaymentsErrorDbError extends ListPaymentsError { + const ListPaymentsErrorDbError(this.value) : super(errorType: 'DbError'); + + factory ListPaymentsErrorDbError.fromJson(dynamic json) { + return ListPaymentsErrorDbError(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class ListPaymentsErrorUnknown extends ListPaymentsError { + const ListPaymentsErrorUnknown({required this.data, this.rawErrorType}) + : super(errorType: 'unknown'); + + final String? rawErrorType; + final dynamic data; + + @override + JsonMap toJson() => { + 'error_type': rawErrorType ?? errorType, + 'error_data': data, + }; +} + +sealed class OpenChannelError { + const OpenChannelError({required this.errorType}); + + factory OpenChannelError.fromJson(dynamic json) { + final map = _asJsonMap(json); + final type = map.valueOrNull('error_type'); + final data = map.valueOrNull('error_data'); + switch (type) { + case 'UnsupportedCoin': + return OpenChannelErrorUnsupportedCoin.fromJson(data); + case 'BalanceError': + return OpenChannelErrorBalanceError.fromJson(data); + case 'InvalidPath': + return OpenChannelErrorInvalidPath.fromJson(data); + case 'FailureToOpenChannel': + return OpenChannelErrorFailureToOpenChannel.fromJson(data); + case 'RpcError': + return OpenChannelErrorRpcError.fromJson(data); + case 'InternalError': + return OpenChannelErrorInternalError.fromJson(data); + case 'IOError': + return OpenChannelErrorIOError.fromJson(data); + case 'DbError': + return OpenChannelErrorDbError.fromJson(data); + case 'ConnectToNodeError': + return OpenChannelErrorConnectToNodeError.fromJson(data); + case 'NoSuchCoin': + return OpenChannelErrorNoSuchCoin.fromJson(data); + case 'GenerateTxErr': + return OpenChannelErrorGenerateTxErr.fromJson(data); + default: + return OpenChannelErrorUnknown(rawErrorType: type, data: data); + } + } + + final String errorType; + + JsonMap toJson(); +} + +final class OpenChannelErrorUnsupportedCoin extends OpenChannelError { + const OpenChannelErrorUnsupportedCoin(this.value) + : super(errorType: 'UnsupportedCoin'); + + factory OpenChannelErrorUnsupportedCoin.fromJson(dynamic json) { + return OpenChannelErrorUnsupportedCoin(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class OpenChannelErrorBalanceError extends OpenChannelError { + const OpenChannelErrorBalanceError(this.value) + : super(errorType: 'BalanceError'); + + factory OpenChannelErrorBalanceError.fromJson(dynamic json) { + return OpenChannelErrorBalanceError(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class OpenChannelErrorInvalidPath extends OpenChannelError { + const OpenChannelErrorInvalidPath(this.value) + : super(errorType: 'InvalidPath'); + + factory OpenChannelErrorInvalidPath.fromJson(dynamic json) { + return OpenChannelErrorInvalidPath(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class OpenChannelErrorFailureToOpenChannel extends OpenChannelError { + const OpenChannelErrorFailureToOpenChannel(this.value0, this.value1) + : super(errorType: 'FailureToOpenChannel'); + + factory OpenChannelErrorFailureToOpenChannel.fromJson(dynamic json) { + final list = _asJsonList(json); + return OpenChannelErrorFailureToOpenChannel( + _stringFromJson(list[0]), + _stringFromJson(list[1]), + ); + } + + final String value0; + final String value1; + + @override + JsonMap toJson() => { + 'error_type': errorType, + 'error_data': [value0, value1], + }; +} + +final class OpenChannelErrorRpcError extends OpenChannelError { + const OpenChannelErrorRpcError(this.value) : super(errorType: 'RpcError'); + + factory OpenChannelErrorRpcError.fromJson(dynamic json) { + return OpenChannelErrorRpcError(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class OpenChannelErrorInternalError extends OpenChannelError { + const OpenChannelErrorInternalError(this.value) + : super(errorType: 'InternalError'); + + factory OpenChannelErrorInternalError.fromJson(dynamic json) { + return OpenChannelErrorInternalError(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class OpenChannelErrorIOError extends OpenChannelError { + const OpenChannelErrorIOError(this.value) : super(errorType: 'IOError'); + + factory OpenChannelErrorIOError.fromJson(dynamic json) { + return OpenChannelErrorIOError(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class OpenChannelErrorDbError extends OpenChannelError { + const OpenChannelErrorDbError(this.value) : super(errorType: 'DbError'); + + factory OpenChannelErrorDbError.fromJson(dynamic json) { + return OpenChannelErrorDbError(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class OpenChannelErrorConnectToNodeError extends OpenChannelError { + const OpenChannelErrorConnectToNodeError(this.value) + : super(errorType: 'ConnectToNodeError'); + + factory OpenChannelErrorConnectToNodeError.fromJson(dynamic json) { + return OpenChannelErrorConnectToNodeError(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class OpenChannelErrorNoSuchCoin extends OpenChannelError { + const OpenChannelErrorNoSuchCoin(this.value) : super(errorType: 'NoSuchCoin'); + + factory OpenChannelErrorNoSuchCoin.fromJson(dynamic json) { + return OpenChannelErrorNoSuchCoin(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class OpenChannelErrorGenerateTxErr extends OpenChannelError { + const OpenChannelErrorGenerateTxErr(this.value) + : super(errorType: 'GenerateTxErr'); + + factory OpenChannelErrorGenerateTxErr.fromJson(dynamic json) { + return OpenChannelErrorGenerateTxErr(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class OpenChannelErrorUnknown extends OpenChannelError { + const OpenChannelErrorUnknown({required this.data, this.rawErrorType}) + : super(errorType: 'unknown'); + + final String? rawErrorType; + final dynamic data; + + @override + JsonMap toJson() => { + 'error_type': rawErrorType ?? errorType, + 'error_data': data, + }; +} + +sealed class SendPaymentError { + const SendPaymentError({required this.errorType}); + + factory SendPaymentError.fromJson(dynamic json) { + final map = _asJsonMap(json); + final type = map.valueOrNull('error_type'); + final data = map.valueOrNull('error_data'); + switch (type) { + case 'UnsupportedCoin': + return SendPaymentErrorUnsupportedCoin.fromJson(data); + case 'NoSuchCoin': + return SendPaymentErrorNoSuchCoin.fromJson(data); + case 'NoRouteFound': + return SendPaymentErrorNoRouteFound.fromJson(data); + case 'PaymentError': + return SendPaymentErrorPaymentError.fromJson(data); + case 'DbError': + return SendPaymentErrorDbError.fromJson(data); + default: + return SendPaymentErrorUnknown(rawErrorType: type, data: data); + } + } + + final String errorType; + + JsonMap toJson(); +} + +final class SendPaymentErrorUnsupportedCoin extends SendPaymentError { + const SendPaymentErrorUnsupportedCoin(this.value) + : super(errorType: 'UnsupportedCoin'); + + factory SendPaymentErrorUnsupportedCoin.fromJson(dynamic json) { + return SendPaymentErrorUnsupportedCoin(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class SendPaymentErrorNoSuchCoin extends SendPaymentError { + const SendPaymentErrorNoSuchCoin(this.value) : super(errorType: 'NoSuchCoin'); + + factory SendPaymentErrorNoSuchCoin.fromJson(dynamic json) { + return SendPaymentErrorNoSuchCoin(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class SendPaymentErrorNoRouteFound extends SendPaymentError { + const SendPaymentErrorNoRouteFound(this.value) + : super(errorType: 'NoRouteFound'); + + factory SendPaymentErrorNoRouteFound.fromJson(dynamic json) { + return SendPaymentErrorNoRouteFound(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class SendPaymentErrorPaymentError extends SendPaymentError { + const SendPaymentErrorPaymentError(this.value) + : super(errorType: 'PaymentError'); + + factory SendPaymentErrorPaymentError.fromJson(dynamic json) { + return SendPaymentErrorPaymentError(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class SendPaymentErrorDbError extends SendPaymentError { + const SendPaymentErrorDbError(this.value) : super(errorType: 'DbError'); + + factory SendPaymentErrorDbError.fromJson(dynamic json) { + return SendPaymentErrorDbError(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class SendPaymentErrorUnknown extends SendPaymentError { + const SendPaymentErrorUnknown({required this.data, this.rawErrorType}) + : super(errorType: 'unknown'); + + final String? rawErrorType; + final dynamic data; + + @override + JsonMap toJson() => { + 'error_type': rawErrorType ?? errorType, + 'error_data': data, + }; +} + +sealed class TrustedNodeError { + const TrustedNodeError({required this.errorType}); + + factory TrustedNodeError.fromJson(dynamic json) { + final map = _asJsonMap(json); + final type = map.valueOrNull('error_type'); + final data = map.valueOrNull('error_data'); + switch (type) { + case 'UnsupportedCoin': + return TrustedNodeErrorUnsupportedCoin.fromJson(data); + case 'NoSuchCoin': + return TrustedNodeErrorNoSuchCoin.fromJson(data); + case 'IOError': + return TrustedNodeErrorIOError.fromJson(data); + default: + return TrustedNodeErrorUnknown(rawErrorType: type, data: data); + } + } + + final String errorType; + + JsonMap toJson(); +} + +final class TrustedNodeErrorUnsupportedCoin extends TrustedNodeError { + const TrustedNodeErrorUnsupportedCoin(this.value) + : super(errorType: 'UnsupportedCoin'); + + factory TrustedNodeErrorUnsupportedCoin.fromJson(dynamic json) { + return TrustedNodeErrorUnsupportedCoin(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class TrustedNodeErrorNoSuchCoin extends TrustedNodeError { + const TrustedNodeErrorNoSuchCoin(this.value) : super(errorType: 'NoSuchCoin'); + + factory TrustedNodeErrorNoSuchCoin.fromJson(dynamic json) { + return TrustedNodeErrorNoSuchCoin(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class TrustedNodeErrorIOError extends TrustedNodeError { + const TrustedNodeErrorIOError(this.value) : super(errorType: 'IOError'); + + factory TrustedNodeErrorIOError.fromJson(dynamic json) { + return TrustedNodeErrorIOError(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class TrustedNodeErrorUnknown extends TrustedNodeError { + const TrustedNodeErrorUnknown({required this.data, this.rawErrorType}) + : super(errorType: 'unknown'); + + final String? rawErrorType; + final dynamic data; + + @override + JsonMap toJson() => { + 'error_type': rawErrorType ?? errorType, + 'error_data': data, + }; +} + +sealed class UpdateChannelError { + const UpdateChannelError({required this.errorType}); + + factory UpdateChannelError.fromJson(dynamic json) { + final map = _asJsonMap(json); + final type = map.valueOrNull('error_type'); + final data = map.valueOrNull('error_data'); + switch (type) { + case 'UnsupportedCoin': + return UpdateChannelErrorUnsupportedCoin.fromJson(data); + case 'NoSuchCoin': + return UpdateChannelErrorNoSuchCoin.fromJson(data); + case 'NoSuchChannel': + return UpdateChannelErrorNoSuchChannel.fromJson(data); + case 'FailureToUpdateChannel': + return UpdateChannelErrorFailureToUpdateChannel.fromJson(data); + default: + return UpdateChannelErrorUnknown(rawErrorType: type, data: data); + } + } + + final String errorType; + + JsonMap toJson(); +} + +final class UpdateChannelErrorUnsupportedCoin extends UpdateChannelError { + const UpdateChannelErrorUnsupportedCoin(this.value) + : super(errorType: 'UnsupportedCoin'); + + factory UpdateChannelErrorUnsupportedCoin.fromJson(dynamic json) { + return UpdateChannelErrorUnsupportedCoin(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class UpdateChannelErrorNoSuchCoin extends UpdateChannelError { + const UpdateChannelErrorNoSuchCoin(this.value) + : super(errorType: 'NoSuchCoin'); + + factory UpdateChannelErrorNoSuchCoin.fromJson(dynamic json) { + return UpdateChannelErrorNoSuchCoin(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class UpdateChannelErrorNoSuchChannel extends UpdateChannelError { + const UpdateChannelErrorNoSuchChannel(this.value) + : super(errorType: 'NoSuchChannel'); + + factory UpdateChannelErrorNoSuchChannel.fromJson(dynamic json) { + return UpdateChannelErrorNoSuchChannel(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class UpdateChannelErrorFailureToUpdateChannel + extends UpdateChannelError { + const UpdateChannelErrorFailureToUpdateChannel(this.value0, this.value1) + : super(errorType: 'FailureToUpdateChannel'); + + factory UpdateChannelErrorFailureToUpdateChannel.fromJson(dynamic json) { + final list = _asJsonList(json); + return UpdateChannelErrorFailureToUpdateChannel( + _stringFromJson(list[0]), + _stringFromJson(list[1]), + ); + } + + final String value0; + final String value1; + + @override + JsonMap toJson() => { + 'error_type': errorType, + 'error_data': [value0, value1], + }; +} + +final class UpdateChannelErrorUnknown extends UpdateChannelError { + const UpdateChannelErrorUnknown({required this.data, this.rawErrorType}) + : super(errorType: 'unknown'); + + final String? rawErrorType; + final dynamic data; + + @override + JsonMap toJson() => { + 'error_type': rawErrorType ?? errorType, + 'error_data': data, + }; +} + +sealed class OfflineKeysError { + const OfflineKeysError({required this.errorType}); + + factory OfflineKeysError.fromJson(dynamic json) { + final map = _asJsonMap(json); + final type = map.valueOrNull('error_type'); + final data = map.valueOrNull('error_data'); + switch (type) { + case 'Internal': + return OfflineKeysErrorInternal.fromJson(data); + case 'CoinConfigNotFound': + return OfflineKeysErrorCoinConfigNotFound.fromJson(data); + case 'ProtocolParseError': + return OfflineKeysErrorProtocolParseError.fromJson(data); + case 'KeyDerivationFailed': + return OfflineKeysErrorKeyDerivationFailed.fromJson(data); + case 'InvalidHdRange': + return OfflineKeysErrorInvalidHdRange.fromJson(data); + case 'HdRangeTooLarge': + return OfflineKeysErrorHdRangeTooLarge.fromJson(); + case 'MissingPrefixValue': + return OfflineKeysErrorMissingPrefixValue.fromJson(data); + case 'InvalidParametersForMode': + return OfflineKeysErrorInvalidParametersForMode.fromJson(); + default: + return OfflineKeysErrorUnknown(rawErrorType: type, data: data); + } + } + + final String errorType; + + JsonMap toJson(); +} + +final class OfflineKeysErrorInternal extends OfflineKeysError { + const OfflineKeysErrorInternal(this.value) : super(errorType: 'Internal'); + + factory OfflineKeysErrorInternal.fromJson(dynamic json) { + return OfflineKeysErrorInternal(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class OfflineKeysErrorCoinConfigNotFound extends OfflineKeysError { + const OfflineKeysErrorCoinConfigNotFound(this.value) + : super(errorType: 'CoinConfigNotFound'); + + factory OfflineKeysErrorCoinConfigNotFound.fromJson(dynamic json) { + return OfflineKeysErrorCoinConfigNotFound(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class OfflineKeysErrorProtocolParseError extends OfflineKeysError { + const OfflineKeysErrorProtocolParseError({ + required this.ticker, + required this.error, + }) : super(errorType: 'ProtocolParseError'); + + factory OfflineKeysErrorProtocolParseError.fromJson(dynamic json) { + final map = _asJsonMap(json); + return OfflineKeysErrorProtocolParseError( + ticker: _stringFromJson(map.value('ticker')), + error: _stringFromJson(map.value('error')), + ); + } + + final String ticker; + final String error; + + @override + JsonMap toJson() => { + 'error_type': errorType, + 'error_data': {'ticker': ticker, 'error': error}, + }; +} + +final class OfflineKeysErrorKeyDerivationFailed extends OfflineKeysError { + const OfflineKeysErrorKeyDerivationFailed({ + required this.ticker, + required this.error, + }) : super(errorType: 'KeyDerivationFailed'); + + factory OfflineKeysErrorKeyDerivationFailed.fromJson(dynamic json) { + final map = _asJsonMap(json); + return OfflineKeysErrorKeyDerivationFailed( + ticker: _stringFromJson(map.value('ticker')), + error: _stringFromJson(map.value('error')), + ); + } + + final String ticker; + final String error; + + @override + JsonMap toJson() => { + 'error_type': errorType, + 'error_data': {'ticker': ticker, 'error': error}, + }; +} + +final class OfflineKeysErrorInvalidHdRange extends OfflineKeysError { + const OfflineKeysErrorInvalidHdRange({ + required this.startIndex, + required this.endIndex, + }) : super(errorType: 'InvalidHdRange'); + + factory OfflineKeysErrorInvalidHdRange.fromJson(dynamic json) { + final map = _asJsonMap(json); + return OfflineKeysErrorInvalidHdRange( + startIndex: _intFromJson(map.value('start_index')), + endIndex: _intFromJson(map.value('end_index')), + ); + } + + final int startIndex; + final int endIndex; + + @override + JsonMap toJson() => { + 'error_type': errorType, + 'error_data': {'start_index': startIndex, 'end_index': endIndex}, + }; +} + +final class OfflineKeysErrorHdRangeTooLarge extends OfflineKeysError { + const OfflineKeysErrorHdRangeTooLarge() : super(errorType: 'HdRangeTooLarge'); + + factory OfflineKeysErrorHdRangeTooLarge.fromJson() => + const OfflineKeysErrorHdRangeTooLarge(); + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': null}; +} + +final class OfflineKeysErrorMissingPrefixValue extends OfflineKeysError { + const OfflineKeysErrorMissingPrefixValue({ + required this.ticker, + required this.prefixType, + }) : super(errorType: 'MissingPrefixValue'); + + factory OfflineKeysErrorMissingPrefixValue.fromJson(dynamic json) { + final map = _asJsonMap(json); + return OfflineKeysErrorMissingPrefixValue( + ticker: _stringFromJson(map.value('ticker')), + prefixType: _stringFromJson(map.value('prefix_type')), + ); + } + + final String ticker; + final String prefixType; + + @override + JsonMap toJson() => { + 'error_type': errorType, + 'error_data': {'ticker': ticker, 'prefix_type': prefixType}, + }; +} + +final class OfflineKeysErrorInvalidParametersForMode extends OfflineKeysError { + const OfflineKeysErrorInvalidParametersForMode() + : super(errorType: 'InvalidParametersForMode'); + + factory OfflineKeysErrorInvalidParametersForMode.fromJson() => + const OfflineKeysErrorInvalidParametersForMode(); + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': null}; +} + +final class OfflineKeysErrorUnknown extends OfflineKeysError { + const OfflineKeysErrorUnknown({required this.data, this.rawErrorType}) + : super(errorType: 'unknown'); + + final String? rawErrorType; + final dynamic data; + + @override + JsonMap toJson() => { + 'error_type': rawErrorType ?? errorType, + 'error_data': data, + }; +} + +sealed class TendermintCoinRpcError { + const TendermintCoinRpcError({required this.errorType}); + + factory TendermintCoinRpcError.fromJson(dynamic json) { + final map = _asJsonMap(json); + final type = map.valueOrNull('error_type'); + final data = map.valueOrNull('error_data'); + switch (type) { + case 'Prost': + return TendermintCoinRpcErrorProst.fromJson(data); + case 'InvalidResponse': + return TendermintCoinRpcErrorInvalidResponse.fromJson(data); + case 'PerformError': + return TendermintCoinRpcErrorPerformError.fromJson(data); + case 'RpcClientError': + return TendermintCoinRpcErrorRpcClientError.fromJson(data); + case 'InternalError': + return TendermintCoinRpcErrorInternalError.fromJson(data); + case 'UnexpectedAccountType': + return TendermintCoinRpcErrorUnexpectedAccountType.fromJson(data); + case 'NotFound': + return TendermintCoinRpcErrorNotFound.fromJson(data); + default: + return TendermintCoinRpcErrorUnknown(rawErrorType: type, data: data); + } + } + + final String errorType; + + JsonMap toJson(); +} + +final class TendermintCoinRpcErrorProst extends TendermintCoinRpcError { + const TendermintCoinRpcErrorProst(this.value) : super(errorType: 'Prost'); + + factory TendermintCoinRpcErrorProst.fromJson(dynamic json) { + return TendermintCoinRpcErrorProst(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class TendermintCoinRpcErrorInvalidResponse + extends TendermintCoinRpcError { + const TendermintCoinRpcErrorInvalidResponse(this.value) + : super(errorType: 'InvalidResponse'); + + factory TendermintCoinRpcErrorInvalidResponse.fromJson(dynamic json) { + return TendermintCoinRpcErrorInvalidResponse(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class TendermintCoinRpcErrorPerformError extends TendermintCoinRpcError { + const TendermintCoinRpcErrorPerformError(this.value) + : super(errorType: 'PerformError'); + + factory TendermintCoinRpcErrorPerformError.fromJson(dynamic json) { + return TendermintCoinRpcErrorPerformError(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class TendermintCoinRpcErrorRpcClientError + extends TendermintCoinRpcError { + const TendermintCoinRpcErrorRpcClientError(this.value) + : super(errorType: 'RpcClientError'); + + factory TendermintCoinRpcErrorRpcClientError.fromJson(dynamic json) { + return TendermintCoinRpcErrorRpcClientError(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class TendermintCoinRpcErrorInternalError extends TendermintCoinRpcError { + const TendermintCoinRpcErrorInternalError(this.value) + : super(errorType: 'InternalError'); + + factory TendermintCoinRpcErrorInternalError.fromJson(dynamic json) { + return TendermintCoinRpcErrorInternalError(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class TendermintCoinRpcErrorUnexpectedAccountType + extends TendermintCoinRpcError { + const TendermintCoinRpcErrorUnexpectedAccountType({required this.prefix}) + : super(errorType: 'UnexpectedAccountType'); + + factory TendermintCoinRpcErrorUnexpectedAccountType.fromJson(dynamic json) { + final map = _asJsonMap(json); + return TendermintCoinRpcErrorUnexpectedAccountType( + prefix: _stringFromJson(map.value('prefix')), + ); + } + + final String prefix; + + @override + JsonMap toJson() => { + 'error_type': errorType, + 'error_data': {'prefix': prefix}, + }; +} + +final class TendermintCoinRpcErrorNotFound extends TendermintCoinRpcError { + const TendermintCoinRpcErrorNotFound(this.value) + : super(errorType: 'NotFound'); + + factory TendermintCoinRpcErrorNotFound.fromJson(dynamic json) { + return TendermintCoinRpcErrorNotFound(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class TendermintCoinRpcErrorUnknown extends TendermintCoinRpcError { + const TendermintCoinRpcErrorUnknown({required this.data, this.rawErrorType}) + : super(errorType: 'unknown'); + + final String? rawErrorType; + final dynamic data; + + @override + JsonMap toJson() => { + 'error_type': rawErrorType ?? errorType, + 'error_data': data, + }; +} + +sealed class InitErc20Error { + const InitErc20Error({required this.errorType}); + + factory InitErc20Error.fromJson(dynamic json) { + final map = _asJsonMap(json); + final type = map.valueOrNull('error_type'); + final data = map.valueOrNull('error_data'); + switch (type) { + case 'HwError': + return InitErc20ErrorHwError.fromJson(data); + case 'TaskTimedOut': + return InitErc20ErrorTaskTimedOut.fromJson(data); + case 'TokenIsAlreadyActivated': + return InitErc20ErrorTokenIsAlreadyActivated.fromJson(data); + case 'TokenCreationError': + return InitErc20ErrorTokenCreationError.fromJson(data); + case 'CouldNotFetchBalance': + return InitErc20ErrorCouldNotFetchBalance.fromJson(data); + case 'Transport': + return InitErc20ErrorTransport.fromJson(data); + case 'Internal': + return InitErc20ErrorInternal.fromJson(data); + case 'CustomTokenError': + return InitErc20ErrorCustomTokenError.fromJson(data); + default: + return InitErc20ErrorUnknown(rawErrorType: type, data: data); + } + } + + final String errorType; + + JsonMap toJson(); +} + +final class InitErc20ErrorHwError extends InitErc20Error { + const InitErc20ErrorHwError(this.value) : super(errorType: 'HwError'); + + factory InitErc20ErrorHwError.fromJson(dynamic json) { + return InitErc20ErrorHwError(HwRpcError.fromJson(json)); + } + + final HwRpcError value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value.toJson()}; +} + +final class InitErc20ErrorTaskTimedOut extends InitErc20Error { + const InitErc20ErrorTaskTimedOut({required this.duration}) + : super(errorType: 'TaskTimedOut'); + + factory InitErc20ErrorTaskTimedOut.fromJson(dynamic json) { + final map = _asJsonMap(json); + return InitErc20ErrorTaskTimedOut( + duration: Mm2Duration.fromJson(map.value('duration')), + ); + } + + final Mm2Duration duration; + + @override + JsonMap toJson() => { + 'error_type': errorType, + 'error_data': {'duration': duration.toJson()}, + }; +} + +final class InitErc20ErrorTokenIsAlreadyActivated extends InitErc20Error { + const InitErc20ErrorTokenIsAlreadyActivated({required this.ticker}) + : super(errorType: 'TokenIsAlreadyActivated'); + + factory InitErc20ErrorTokenIsAlreadyActivated.fromJson(dynamic json) { + final map = _asJsonMap(json); + return InitErc20ErrorTokenIsAlreadyActivated( + ticker: _stringFromJson(map.value('ticker')), + ); + } + + final String ticker; + + @override + JsonMap toJson() => { + 'error_type': errorType, + 'error_data': {'ticker': ticker}, + }; +} + +final class InitErc20ErrorTokenCreationError extends InitErc20Error { + const InitErc20ErrorTokenCreationError({ + required this.ticker, + required this.error, + }) : super(errorType: 'TokenCreationError'); + + factory InitErc20ErrorTokenCreationError.fromJson(dynamic json) { + final map = _asJsonMap(json); + return InitErc20ErrorTokenCreationError( + ticker: _stringFromJson(map.value('ticker')), + error: _stringFromJson(map.value('error')), + ); + } + + final String ticker; + final String error; + + @override + JsonMap toJson() => { + 'error_type': errorType, + 'error_data': {'ticker': ticker, 'error': error}, + }; +} + +final class InitErc20ErrorCouldNotFetchBalance extends InitErc20Error { + const InitErc20ErrorCouldNotFetchBalance(this.value) + : super(errorType: 'CouldNotFetchBalance'); + + factory InitErc20ErrorCouldNotFetchBalance.fromJson(dynamic json) { + return InitErc20ErrorCouldNotFetchBalance(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class InitErc20ErrorTransport extends InitErc20Error { + const InitErc20ErrorTransport(this.value) : super(errorType: 'Transport'); + + factory InitErc20ErrorTransport.fromJson(dynamic json) { + return InitErc20ErrorTransport(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class InitErc20ErrorInternal extends InitErc20Error { + const InitErc20ErrorInternal(this.value) : super(errorType: 'Internal'); + + factory InitErc20ErrorInternal.fromJson(dynamic json) { + return InitErc20ErrorInternal(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class InitErc20ErrorCustomTokenError extends InitErc20Error { + const InitErc20ErrorCustomTokenError(this.value) + : super(errorType: 'CustomTokenError'); + + factory InitErc20ErrorCustomTokenError.fromJson(dynamic json) { + return InitErc20ErrorCustomTokenError(CustomTokenError.fromJson(json)); + } + + final CustomTokenError value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value.toJson()}; +} + +final class InitErc20ErrorUnknown extends InitErc20Error { + const InitErc20ErrorUnknown({required this.data, this.rawErrorType}) + : super(errorType: 'unknown'); + + final String? rawErrorType; + final dynamic data; + + @override + JsonMap toJson() => { + 'error_type': rawErrorType ?? errorType, + 'error_data': data, + }; +} + +sealed class InitTokenError { + const InitTokenError({required this.errorType}); + + factory InitTokenError.fromJson(dynamic json) { + final map = _asJsonMap(json); + final type = map.valueOrNull('error_type'); + final data = map.valueOrNull('error_data'); + switch (type) { + case 'NoSuchTask': + return InitTokenErrorNoSuchTask.fromJson(data); + case 'TaskTimedOut': + return InitTokenErrorTaskTimedOut.fromJson(data); + case 'TokenIsAlreadyActivated': + return InitTokenErrorTokenIsAlreadyActivated.fromJson(data); + case 'TokenConfigIsNotFound': + return InitTokenErrorTokenConfigIsNotFound.fromJson(data); + case 'TokenProtocolParseError': + return InitTokenErrorTokenProtocolParseError.fromJson(data); + case 'UnexpectedTokenProtocol': + return InitTokenErrorUnexpectedTokenProtocol.fromJson(data); + case 'TokenCreationError': + return InitTokenErrorTokenCreationError.fromJson(data); + case 'CouldNotFetchBalance': + return InitTokenErrorCouldNotFetchBalance.fromJson(data); + case 'PlatformCoinIsNotActivated': + return InitTokenErrorPlatformCoinIsNotActivated.fromJson(data); + case 'UnsupportedPlatformCoin': + return InitTokenErrorUnsupportedPlatformCoin.fromJson(data); + case 'CustomTokenError': + return InitTokenErrorCustomTokenError.fromJson(data); + case 'HwError': + return InitTokenErrorHwError.fromJson(data); + case 'Transport': + return InitTokenErrorTransport.fromJson(data); + case 'Internal': + return InitTokenErrorInternal.fromJson(data); + default: + return InitTokenErrorUnknown(rawErrorType: type, data: data); + } + } + + final String errorType; + + JsonMap toJson(); +} + +final class InitTokenErrorNoSuchTask extends InitTokenError { + const InitTokenErrorNoSuchTask(this.value) : super(errorType: 'NoSuchTask'); + + factory InitTokenErrorNoSuchTask.fromJson(dynamic json) { + return InitTokenErrorNoSuchTask(TaskId.fromJson(json)); + } + + final TaskId value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value.toJson()}; +} + +final class InitTokenErrorTaskTimedOut extends InitTokenError { + const InitTokenErrorTaskTimedOut({required this.duration}) + : super(errorType: 'TaskTimedOut'); + + factory InitTokenErrorTaskTimedOut.fromJson(dynamic json) { + final map = _asJsonMap(json); + return InitTokenErrorTaskTimedOut( + duration: Mm2Duration.fromJson(map.value('duration')), + ); + } + + final Mm2Duration duration; + + @override + JsonMap toJson() => { + 'error_type': errorType, + 'error_data': {'duration': duration.toJson()}, + }; +} + +final class InitTokenErrorTokenIsAlreadyActivated extends InitTokenError { + const InitTokenErrorTokenIsAlreadyActivated({required this.ticker}) + : super(errorType: 'TokenIsAlreadyActivated'); + + factory InitTokenErrorTokenIsAlreadyActivated.fromJson(dynamic json) { + final map = _asJsonMap(json); + return InitTokenErrorTokenIsAlreadyActivated( + ticker: _stringFromJson(map.value('ticker')), + ); + } + + final String ticker; + + @override + JsonMap toJson() => { + 'error_type': errorType, + 'error_data': {'ticker': ticker}, + }; +} + +final class InitTokenErrorTokenConfigIsNotFound extends InitTokenError { + const InitTokenErrorTokenConfigIsNotFound(this.value) + : super(errorType: 'TokenConfigIsNotFound'); + + factory InitTokenErrorTokenConfigIsNotFound.fromJson(dynamic json) { + return InitTokenErrorTokenConfigIsNotFound(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class InitTokenErrorTokenProtocolParseError extends InitTokenError { + const InitTokenErrorTokenProtocolParseError({ + required this.ticker, + required this.error, + }) : super(errorType: 'TokenProtocolParseError'); + + factory InitTokenErrorTokenProtocolParseError.fromJson(dynamic json) { + final map = _asJsonMap(json); + return InitTokenErrorTokenProtocolParseError( + ticker: _stringFromJson(map.value('ticker')), + error: _stringFromJson(map.value('error')), + ); + } + + final String ticker; + final String error; + + @override + JsonMap toJson() => { + 'error_type': errorType, + 'error_data': {'ticker': ticker, 'error': error}, + }; +} + +final class InitTokenErrorUnexpectedTokenProtocol extends InitTokenError { + const InitTokenErrorUnexpectedTokenProtocol({ + required this.ticker, + required this.protocol, + }) : super(errorType: 'UnexpectedTokenProtocol'); + + factory InitTokenErrorUnexpectedTokenProtocol.fromJson(dynamic json) { + final map = _asJsonMap(json); + return InitTokenErrorUnexpectedTokenProtocol( + ticker: _stringFromJson(map.value('ticker')), + protocol: JsonValue.fromJson(map.value('protocol')), + ); + } + + final String ticker; + final JsonValue protocol; + + @override + JsonMap toJson() => { + 'error_type': errorType, + 'error_data': {'ticker': ticker, 'protocol': protocol.toJson()}, + }; +} + +final class InitTokenErrorTokenCreationError extends InitTokenError { + const InitTokenErrorTokenCreationError({ + required this.ticker, + required this.error, + }) : super(errorType: 'TokenCreationError'); + + factory InitTokenErrorTokenCreationError.fromJson(dynamic json) { + final map = _asJsonMap(json); + return InitTokenErrorTokenCreationError( + ticker: _stringFromJson(map.value('ticker')), + error: _stringFromJson(map.value('error')), + ); + } + + final String ticker; + final String error; + + @override + JsonMap toJson() => { + 'error_type': errorType, + 'error_data': {'ticker': ticker, 'error': error}, + }; +} + +final class InitTokenErrorCouldNotFetchBalance extends InitTokenError { + const InitTokenErrorCouldNotFetchBalance(this.value) + : super(errorType: 'CouldNotFetchBalance'); + + factory InitTokenErrorCouldNotFetchBalance.fromJson(dynamic json) { + return InitTokenErrorCouldNotFetchBalance(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class InitTokenErrorPlatformCoinIsNotActivated extends InitTokenError { + const InitTokenErrorPlatformCoinIsNotActivated(this.value) + : super(errorType: 'PlatformCoinIsNotActivated'); + + factory InitTokenErrorPlatformCoinIsNotActivated.fromJson(dynamic json) { + return InitTokenErrorPlatformCoinIsNotActivated(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class InitTokenErrorUnsupportedPlatformCoin extends InitTokenError { + const InitTokenErrorUnsupportedPlatformCoin({ + required this.platformCoinTicker, + required this.tokenTicker, + }) : super(errorType: 'UnsupportedPlatformCoin'); + + factory InitTokenErrorUnsupportedPlatformCoin.fromJson(dynamic json) { + final map = _asJsonMap(json); + return InitTokenErrorUnsupportedPlatformCoin( + platformCoinTicker: _stringFromJson( + map.value('platform_coin_ticker'), + ), + tokenTicker: _stringFromJson(map.value('token_ticker')), + ); + } + + final String platformCoinTicker; + final String tokenTicker; + + @override + JsonMap toJson() => { + 'error_type': errorType, + 'error_data': { + 'platform_coin_ticker': platformCoinTicker, + 'token_ticker': tokenTicker, + }, + }; +} + +final class InitTokenErrorCustomTokenError extends InitTokenError { + const InitTokenErrorCustomTokenError(this.value) + : super(errorType: 'CustomTokenError'); + + factory InitTokenErrorCustomTokenError.fromJson(dynamic json) { + return InitTokenErrorCustomTokenError(CustomTokenError.fromJson(json)); + } + + final CustomTokenError value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value.toJson()}; +} + +final class InitTokenErrorHwError extends InitTokenError { + const InitTokenErrorHwError(this.value) : super(errorType: 'HwError'); + + factory InitTokenErrorHwError.fromJson(dynamic json) { + return InitTokenErrorHwError(HwRpcError.fromJson(json)); + } + + final HwRpcError value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value.toJson()}; +} + +final class InitTokenErrorTransport extends InitTokenError { + const InitTokenErrorTransport(this.value) : super(errorType: 'Transport'); + + factory InitTokenErrorTransport.fromJson(dynamic json) { + return InitTokenErrorTransport(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class InitTokenErrorInternal extends InitTokenError { + const InitTokenErrorInternal(this.value) : super(errorType: 'Internal'); + + factory InitTokenErrorInternal.fromJson(dynamic json) { + return InitTokenErrorInternal(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class InitTokenErrorUnknown extends InitTokenError { + const InitTokenErrorUnknown({required this.data, this.rawErrorType}) + : super(errorType: 'unknown'); + + final String? rawErrorType; + final dynamic data; + + @override + JsonMap toJson() => { + 'error_type': rawErrorType ?? errorType, + 'error_data': data, + }; +} + +sealed class InitL2Error { + const InitL2Error({required this.errorType}); + + factory InitL2Error.fromJson(dynamic json) { + final map = _asJsonMap(json); + final type = map.valueOrNull('error_type'); + final data = map.valueOrNull('error_data'); + switch (type) { + case 'L2IsAlreadyActivated': + return InitL2ErrorL2IsAlreadyActivated.fromJson(data); + case 'L2ConfigIsNotFound': + return InitL2ErrorL2ConfigIsNotFound.fromJson(data); + case 'L2ProtocolParseError': + return InitL2ErrorL2ProtocolParseError.fromJson(data); + case 'UnexpectedL2Protocol': + return InitL2ErrorUnexpectedL2Protocol.fromJson(data); + case 'PlatformCoinIsNotActivated': + return InitL2ErrorPlatformCoinIsNotActivated.fromJson(data); + case 'UnsupportedPlatformCoin': + return InitL2ErrorUnsupportedPlatformCoin.fromJson(data); + case 'InvalidPlatformConfiguration': + return InitL2ErrorInvalidPlatformConfiguration.fromJson(data); + case 'L2ConfigParseError': + return InitL2ErrorL2ConfigParseError.fromJson(data); + case 'TaskTimedOut': + return InitL2ErrorTaskTimedOut.fromJson(data); + case 'Transport': + return InitL2ErrorTransport.fromJson(data); + case 'Internal': + return InitL2ErrorInternal.fromJson(data); + default: + return InitL2ErrorUnknown(rawErrorType: type, data: data); + } + } + + final String errorType; + + JsonMap toJson(); +} + +final class InitL2ErrorL2IsAlreadyActivated extends InitL2Error { + const InitL2ErrorL2IsAlreadyActivated(this.value) + : super(errorType: 'L2IsAlreadyActivated'); + + factory InitL2ErrorL2IsAlreadyActivated.fromJson(dynamic json) { + return InitL2ErrorL2IsAlreadyActivated(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class InitL2ErrorL2ConfigIsNotFound extends InitL2Error { + const InitL2ErrorL2ConfigIsNotFound(this.value) + : super(errorType: 'L2ConfigIsNotFound'); + + factory InitL2ErrorL2ConfigIsNotFound.fromJson(dynamic json) { + return InitL2ErrorL2ConfigIsNotFound(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class InitL2ErrorL2ProtocolParseError extends InitL2Error { + const InitL2ErrorL2ProtocolParseError({ + required this.ticker, + required this.error, + }) : super(errorType: 'L2ProtocolParseError'); + + factory InitL2ErrorL2ProtocolParseError.fromJson(dynamic json) { + final map = _asJsonMap(json); + return InitL2ErrorL2ProtocolParseError( + ticker: _stringFromJson(map.value('ticker')), + error: _stringFromJson(map.value('error')), + ); + } + + final String ticker; + final String error; + + @override + JsonMap toJson() => { + 'error_type': errorType, + 'error_data': {'ticker': ticker, 'error': error}, + }; +} + +final class InitL2ErrorUnexpectedL2Protocol extends InitL2Error { + const InitL2ErrorUnexpectedL2Protocol({ + required this.ticker, + required this.protocol, + }) : super(errorType: 'UnexpectedL2Protocol'); + + factory InitL2ErrorUnexpectedL2Protocol.fromJson(dynamic json) { + final map = _asJsonMap(json); + return InitL2ErrorUnexpectedL2Protocol( + ticker: _stringFromJson(map.value('ticker')), + protocol: JsonValue.fromJson(map.value('protocol')), + ); + } + + final String ticker; + final JsonValue protocol; + + @override + JsonMap toJson() => { + 'error_type': errorType, + 'error_data': {'ticker': ticker, 'protocol': protocol.toJson()}, + }; +} + +final class InitL2ErrorPlatformCoinIsNotActivated extends InitL2Error { + const InitL2ErrorPlatformCoinIsNotActivated(this.value) + : super(errorType: 'PlatformCoinIsNotActivated'); + + factory InitL2ErrorPlatformCoinIsNotActivated.fromJson(dynamic json) { + return InitL2ErrorPlatformCoinIsNotActivated(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class InitL2ErrorUnsupportedPlatformCoin extends InitL2Error { + const InitL2ErrorUnsupportedPlatformCoin({ + required this.platformCoinTicker, + required this.l2Ticker, + }) : super(errorType: 'UnsupportedPlatformCoin'); + + factory InitL2ErrorUnsupportedPlatformCoin.fromJson(dynamic json) { + final map = _asJsonMap(json); + return InitL2ErrorUnsupportedPlatformCoin( + platformCoinTicker: _stringFromJson( + map.value('platform_coin_ticker'), + ), + l2Ticker: _stringFromJson(map.value('l2_ticker')), + ); + } + + final String platformCoinTicker; + final String l2Ticker; + + @override + JsonMap toJson() => { + 'error_type': errorType, + 'error_data': { + 'platform_coin_ticker': platformCoinTicker, + 'l2_ticker': l2Ticker, + }, + }; +} + +final class InitL2ErrorInvalidPlatformConfiguration extends InitL2Error { + const InitL2ErrorInvalidPlatformConfiguration({ + required this.platformCoinTicker, + required this.err, + }) : super(errorType: 'InvalidPlatformConfiguration'); + + factory InitL2ErrorInvalidPlatformConfiguration.fromJson(dynamic json) { + final map = _asJsonMap(json); + return InitL2ErrorInvalidPlatformConfiguration( + platformCoinTicker: _stringFromJson( + map.value('platform_coin_ticker'), + ), + err: _stringFromJson(map.value('err')), + ); + } + + final String platformCoinTicker; + final String err; + + @override + JsonMap toJson() => { + 'error_type': errorType, + 'error_data': {'platform_coin_ticker': platformCoinTicker, 'err': err}, + }; +} + +final class InitL2ErrorL2ConfigParseError extends InitL2Error { + const InitL2ErrorL2ConfigParseError(this.value) + : super(errorType: 'L2ConfigParseError'); + + factory InitL2ErrorL2ConfigParseError.fromJson(dynamic json) { + return InitL2ErrorL2ConfigParseError(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class InitL2ErrorTaskTimedOut extends InitL2Error { + const InitL2ErrorTaskTimedOut({required this.duration}) + : super(errorType: 'TaskTimedOut'); + + factory InitL2ErrorTaskTimedOut.fromJson(dynamic json) { + final map = _asJsonMap(json); + return InitL2ErrorTaskTimedOut( + duration: Mm2Duration.fromJson(map.value('duration')), + ); + } + + final Mm2Duration duration; + + @override + JsonMap toJson() => { + 'error_type': errorType, + 'error_data': {'duration': duration.toJson()}, + }; +} + +final class InitL2ErrorTransport extends InitL2Error { + const InitL2ErrorTransport(this.value) : super(errorType: 'Transport'); + + factory InitL2ErrorTransport.fromJson(dynamic json) { + return InitL2ErrorTransport(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class InitL2ErrorInternal extends InitL2Error { + const InitL2ErrorInternal(this.value) : super(errorType: 'Internal'); + + factory InitL2ErrorInternal.fromJson(dynamic json) { + return InitL2ErrorInternal(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class InitL2ErrorUnknown extends InitL2Error { + const InitL2ErrorUnknown({required this.data, this.rawErrorType}) + : super(errorType: 'unknown'); + + final String? rawErrorType; + final dynamic data; + + @override + JsonMap toJson() => { + 'error_type': rawErrorType ?? errorType, + 'error_data': data, + }; +} + +sealed class LightningInitError { + const LightningInitError({required this.errorType}); + + factory LightningInitError.fromJson(dynamic json) { + final map = _asJsonMap(json); + final type = map.valueOrNull('error_type'); + final data = map.valueOrNull('error_data'); + switch (type) { + case 'CoinIsAlreadyActivated': + return LightningInitErrorCoinIsAlreadyActivated.fromJson(data); + case 'InvalidConfiguration': + return LightningInitErrorInvalidConfiguration.fromJson(data); + case 'InvalidPlatformConfiguration': + return LightningInitErrorInvalidPlatformConfiguration.fromJson(data); + case 'EnableLightningError': + return LightningInitErrorEnableLightningError.fromJson(data); + case 'LightningValidationErr': + return LightningInitErrorLightningValidationErr.fromJson(data); + case 'MyBalanceError': + return LightningInitErrorMyBalanceError.fromJson(data); + case 'MyAddressError': + return LightningInitErrorMyAddressError.fromJson(data); + case 'Internal': + return LightningInitErrorInternal.fromJson(data); + default: + return LightningInitErrorUnknown(rawErrorType: type, data: data); + } + } + + final String errorType; + + JsonMap toJson(); +} + +final class LightningInitErrorCoinIsAlreadyActivated + extends LightningInitError { + const LightningInitErrorCoinIsAlreadyActivated({required this.ticker}) + : super(errorType: 'CoinIsAlreadyActivated'); + + factory LightningInitErrorCoinIsAlreadyActivated.fromJson(dynamic json) { + final map = _asJsonMap(json); + return LightningInitErrorCoinIsAlreadyActivated( + ticker: _stringFromJson(map.value('ticker')), + ); + } + + final String ticker; + + @override + JsonMap toJson() => { + 'error_type': errorType, + 'error_data': {'ticker': ticker}, + }; +} + +final class LightningInitErrorInvalidConfiguration extends LightningInitError { + const LightningInitErrorInvalidConfiguration(this.value) + : super(errorType: 'InvalidConfiguration'); + + factory LightningInitErrorInvalidConfiguration.fromJson(dynamic json) { + return LightningInitErrorInvalidConfiguration(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class LightningInitErrorInvalidPlatformConfiguration + extends LightningInitError { + const LightningInitErrorInvalidPlatformConfiguration({ + required this.platformCoinTicker, + required this.err, + }) : super(errorType: 'InvalidPlatformConfiguration'); + + factory LightningInitErrorInvalidPlatformConfiguration.fromJson( + dynamic json, + ) { + final map = _asJsonMap(json); + return LightningInitErrorInvalidPlatformConfiguration( + platformCoinTicker: _stringFromJson( + map.value('platform_coin_ticker'), + ), + err: _stringFromJson(map.value('err')), + ); + } + + final String platformCoinTicker; + final String err; + + @override + JsonMap toJson() => { + 'error_type': errorType, + 'error_data': {'platform_coin_ticker': platformCoinTicker, 'err': err}, + }; +} + +final class LightningInitErrorEnableLightningError extends LightningInitError { + const LightningInitErrorEnableLightningError(this.value) + : super(errorType: 'EnableLightningError'); + + factory LightningInitErrorEnableLightningError.fromJson(dynamic json) { + return LightningInitErrorEnableLightningError( + EnableLightningError.fromJson(json), + ); + } + + final EnableLightningError value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value.toJson()}; +} + +final class LightningInitErrorLightningValidationErr + extends LightningInitError { + const LightningInitErrorLightningValidationErr(this.value) + : super(errorType: 'LightningValidationErr'); + + factory LightningInitErrorLightningValidationErr.fromJson(dynamic json) { + return LightningInitErrorLightningValidationErr( + LightningValidationErr.fromJson(json), + ); + } + + final LightningValidationErr value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value.toJson()}; +} + +final class LightningInitErrorMyBalanceError extends LightningInitError { + const LightningInitErrorMyBalanceError(this.value) + : super(errorType: 'MyBalanceError'); + + factory LightningInitErrorMyBalanceError.fromJson(dynamic json) { + return LightningInitErrorMyBalanceError(BalanceError.fromJson(json)); + } + + final BalanceError value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value.toJson()}; +} + +final class LightningInitErrorMyAddressError extends LightningInitError { + const LightningInitErrorMyAddressError(this.value) + : super(errorType: 'MyAddressError'); + + factory LightningInitErrorMyAddressError.fromJson(dynamic json) { + return LightningInitErrorMyAddressError(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class LightningInitErrorInternal extends LightningInitError { + const LightningInitErrorInternal(this.value) : super(errorType: 'Internal'); + + factory LightningInitErrorInternal.fromJson(dynamic json) { + return LightningInitErrorInternal(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class LightningInitErrorUnknown extends LightningInitError { + const LightningInitErrorUnknown({required this.data, this.rawErrorType}) + : super(errorType: 'unknown'); + + final String? rawErrorType; + final dynamic data; + + @override + JsonMap toJson() => { + 'error_type': rawErrorType ?? errorType, + 'error_data': data, + }; +} + +sealed class LightningValidationErr { + const LightningValidationErr({required this.errorType}); + + factory LightningValidationErr.fromJson(dynamic json) { + final map = _asJsonMap(json); + final type = map.valueOrNull('error_type'); + final data = map.valueOrNull('error_data'); + switch (type) { + case 'UnexpectedMethod': + return LightningValidationErrUnexpectedMethod.fromJson(data); + case 'UnsupportedMode': + return LightningValidationErrUnsupportedMode.fromJson(data); + case 'InvalidRequest': + return LightningValidationErrInvalidRequest.fromJson(data); + case 'InvalidAddress': + return LightningValidationErrInvalidAddress.fromJson(data); + default: + return LightningValidationErrUnknown(rawErrorType: type, data: data); + } + } + + final String errorType; + + JsonMap toJson(); +} + +final class LightningValidationErrUnexpectedMethod + extends LightningValidationErr { + const LightningValidationErrUnexpectedMethod(this.value0, this.value1) + : super(errorType: 'UnexpectedMethod'); + + factory LightningValidationErrUnexpectedMethod.fromJson(dynamic json) { + final list = _asJsonList(json); + return LightningValidationErrUnexpectedMethod( + _stringFromJson(list[0]), + _stringFromJson(list[1]), + ); + } + + final String value0; + final String value1; + + @override + JsonMap toJson() => { + 'error_type': errorType, + 'error_data': [value0, value1], + }; +} + +final class LightningValidationErrUnsupportedMode + extends LightningValidationErr { + const LightningValidationErrUnsupportedMode(this.value0, this.value1) + : super(errorType: 'UnsupportedMode'); + + factory LightningValidationErrUnsupportedMode.fromJson(dynamic json) { + final list = _asJsonList(json); + return LightningValidationErrUnsupportedMode( + _stringFromJson(list[0]), + _stringFromJson(list[1]), + ); + } + + final String value0; + final String value1; + + @override + JsonMap toJson() => { + 'error_type': errorType, + 'error_data': [value0, value1], + }; +} + +final class LightningValidationErrInvalidRequest + extends LightningValidationErr { + const LightningValidationErrInvalidRequest(this.value) + : super(errorType: 'InvalidRequest'); + + factory LightningValidationErrInvalidRequest.fromJson(dynamic json) { + return LightningValidationErrInvalidRequest(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class LightningValidationErrInvalidAddress + extends LightningValidationErr { + const LightningValidationErrInvalidAddress(this.value) + : super(errorType: 'InvalidAddress'); + + factory LightningValidationErrInvalidAddress.fromJson(dynamic json) { + return LightningValidationErrInvalidAddress(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class LightningValidationErrUnknown extends LightningValidationErr { + const LightningValidationErrUnknown({required this.data, this.rawErrorType}) + : super(errorType: 'unknown'); + + final String? rawErrorType; + final dynamic data; + + @override + JsonMap toJson() => { + 'error_type': rawErrorType ?? errorType, + 'error_data': data, + }; +} + +sealed class EnablePlatformCoinWithTokensError { + const EnablePlatformCoinWithTokensError({required this.errorType}); + + factory EnablePlatformCoinWithTokensError.fromJson(dynamic json) { + final map = _asJsonMap(json); + final type = map.valueOrNull('error_type'); + final data = map.valueOrNull('error_data'); + switch (type) { + case 'PlatformIsAlreadyActivated': + return EnablePlatformCoinWithTokensErrorPlatformIsAlreadyActivated.fromJson( + data, + ); + case 'PlatformConfigIsNotFound': + return EnablePlatformCoinWithTokensErrorPlatformConfigIsNotFound.fromJson( + data, + ); + case 'CoinProtocolParseError': + return EnablePlatformCoinWithTokensErrorCoinProtocolParseError.fromJson( + data, + ); + case 'UnexpectedPlatformProtocol': + return EnablePlatformCoinWithTokensErrorUnexpectedPlatformProtocol.fromJson( + data, + ); + case 'TokenConfigIsNotFound': + return EnablePlatformCoinWithTokensErrorTokenConfigIsNotFound.fromJson( + data, + ); + case 'TokenProtocolParseError': + return EnablePlatformCoinWithTokensErrorTokenProtocolParseError.fromJson( + data, + ); + case 'UnexpectedTokenProtocol': + return EnablePlatformCoinWithTokensErrorUnexpectedTokenProtocol.fromJson( + data, + ); + case 'PlatformCoinCreationError': + return EnablePlatformCoinWithTokensErrorPlatformCoinCreationError.fromJson( + data, + ); + case 'PrivKeyPolicyNotAllowed': + return EnablePlatformCoinWithTokensErrorPrivKeyPolicyNotAllowed.fromJson( + data, + ); + case 'UnexpectedDerivationMethod': + return EnablePlatformCoinWithTokensErrorUnexpectedDerivationMethod.fromJson( + data, + ); + case 'Transport': + return EnablePlatformCoinWithTokensErrorTransport.fromJson(data); + case 'AtLeastOneNodeRequired': + return EnablePlatformCoinWithTokensErrorAtLeastOneNodeRequired.fromJson( + data, + ); + case 'InvalidPayload': + return EnablePlatformCoinWithTokensErrorInvalidPayload.fromJson(data); + case 'FailedSpawningBalanceEvents': + return EnablePlatformCoinWithTokensErrorFailedSpawningBalanceEvents.fromJson( + data, + ); + case 'Internal': + return EnablePlatformCoinWithTokensErrorInternal.fromJson(data); + case 'NoSuchTask': + return EnablePlatformCoinWithTokensErrorNoSuchTask.fromJson(data); + case 'TaskTimedOut': + return EnablePlatformCoinWithTokensErrorTaskTimedOut.fromJson(data); + case 'UnexpectedDeviceActivationPolicy': + return EnablePlatformCoinWithTokensErrorUnexpectedDeviceActivationPolicy.fromJson(); + case 'CustomTokenError': + return EnablePlatformCoinWithTokensErrorCustomTokenError.fromJson(data); + case 'WalletConnectError': + return EnablePlatformCoinWithTokensErrorWalletConnectError.fromJson( + data, + ); + default: + return EnablePlatformCoinWithTokensErrorUnknown( + rawErrorType: type, + data: data, + ); + } + } + + final String errorType; + + JsonMap toJson(); +} + +final class EnablePlatformCoinWithTokensErrorPlatformIsAlreadyActivated + extends EnablePlatformCoinWithTokensError { + const EnablePlatformCoinWithTokensErrorPlatformIsAlreadyActivated(this.value) + : super(errorType: 'PlatformIsAlreadyActivated'); + + factory EnablePlatformCoinWithTokensErrorPlatformIsAlreadyActivated.fromJson( + dynamic json, + ) { + return EnablePlatformCoinWithTokensErrorPlatformIsAlreadyActivated( + _stringFromJson(json), + ); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class EnablePlatformCoinWithTokensErrorPlatformConfigIsNotFound + extends EnablePlatformCoinWithTokensError { + const EnablePlatformCoinWithTokensErrorPlatformConfigIsNotFound(this.value) + : super(errorType: 'PlatformConfigIsNotFound'); + + factory EnablePlatformCoinWithTokensErrorPlatformConfigIsNotFound.fromJson( + dynamic json, + ) { + return EnablePlatformCoinWithTokensErrorPlatformConfigIsNotFound( + _stringFromJson(json), + ); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class EnablePlatformCoinWithTokensErrorCoinProtocolParseError + extends EnablePlatformCoinWithTokensError { + const EnablePlatformCoinWithTokensErrorCoinProtocolParseError({ + required this.ticker, + required this.error, + }) : super(errorType: 'CoinProtocolParseError'); + + factory EnablePlatformCoinWithTokensErrorCoinProtocolParseError.fromJson( + dynamic json, + ) { + final map = _asJsonMap(json); + return EnablePlatformCoinWithTokensErrorCoinProtocolParseError( + ticker: _stringFromJson(map.value('ticker')), + error: _stringFromJson(map.value('error')), + ); + } + + final String ticker; + final String error; + + @override + JsonMap toJson() => { + 'error_type': errorType, + 'error_data': {'ticker': ticker, 'error': error}, + }; +} + +final class EnablePlatformCoinWithTokensErrorUnexpectedPlatformProtocol + extends EnablePlatformCoinWithTokensError { + const EnablePlatformCoinWithTokensErrorUnexpectedPlatformProtocol({ + required this.ticker, + required this.protocol, + }) : super(errorType: 'UnexpectedPlatformProtocol'); + + factory EnablePlatformCoinWithTokensErrorUnexpectedPlatformProtocol.fromJson( + dynamic json, + ) { + final map = _asJsonMap(json); + return EnablePlatformCoinWithTokensErrorUnexpectedPlatformProtocol( + ticker: _stringFromJson(map.value('ticker')), + protocol: JsonValue.fromJson(map.value('protocol')), + ); + } + + final String ticker; + final JsonValue protocol; + + @override + JsonMap toJson() => { + 'error_type': errorType, + 'error_data': {'ticker': ticker, 'protocol': protocol.toJson()}, + }; +} + +final class EnablePlatformCoinWithTokensErrorTokenConfigIsNotFound + extends EnablePlatformCoinWithTokensError { + const EnablePlatformCoinWithTokensErrorTokenConfigIsNotFound(this.value) + : super(errorType: 'TokenConfigIsNotFound'); + + factory EnablePlatformCoinWithTokensErrorTokenConfigIsNotFound.fromJson( + dynamic json, + ) { + return EnablePlatformCoinWithTokensErrorTokenConfigIsNotFound( + _stringFromJson(json), + ); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class EnablePlatformCoinWithTokensErrorTokenProtocolParseError + extends EnablePlatformCoinWithTokensError { + const EnablePlatformCoinWithTokensErrorTokenProtocolParseError({ + required this.ticker, + required this.error, + }) : super(errorType: 'TokenProtocolParseError'); + + factory EnablePlatformCoinWithTokensErrorTokenProtocolParseError.fromJson( + dynamic json, + ) { + final map = _asJsonMap(json); + return EnablePlatformCoinWithTokensErrorTokenProtocolParseError( + ticker: _stringFromJson(map.value('ticker')), + error: _stringFromJson(map.value('error')), + ); + } + + final String ticker; + final String error; + + @override + JsonMap toJson() => { + 'error_type': errorType, + 'error_data': {'ticker': ticker, 'error': error}, + }; +} + +final class EnablePlatformCoinWithTokensErrorUnexpectedTokenProtocol + extends EnablePlatformCoinWithTokensError { + const EnablePlatformCoinWithTokensErrorUnexpectedTokenProtocol({ + required this.ticker, + required this.protocol, + }) : super(errorType: 'UnexpectedTokenProtocol'); + + factory EnablePlatformCoinWithTokensErrorUnexpectedTokenProtocol.fromJson( + dynamic json, + ) { + final map = _asJsonMap(json); + return EnablePlatformCoinWithTokensErrorUnexpectedTokenProtocol( + ticker: _stringFromJson(map.value('ticker')), + protocol: JsonValue.fromJson(map.value('protocol')), + ); + } + + final String ticker; + final JsonValue protocol; + + @override + JsonMap toJson() => { + 'error_type': errorType, + 'error_data': {'ticker': ticker, 'protocol': protocol.toJson()}, + }; +} + +final class EnablePlatformCoinWithTokensErrorPlatformCoinCreationError + extends EnablePlatformCoinWithTokensError { + const EnablePlatformCoinWithTokensErrorPlatformCoinCreationError({ + required this.ticker, + required this.error, + }) : super(errorType: 'PlatformCoinCreationError'); + + factory EnablePlatformCoinWithTokensErrorPlatformCoinCreationError.fromJson( + dynamic json, + ) { + final map = _asJsonMap(json); + return EnablePlatformCoinWithTokensErrorPlatformCoinCreationError( + ticker: _stringFromJson(map.value('ticker')), + error: _stringFromJson(map.value('error')), + ); + } + + final String ticker; + final String error; + + @override + JsonMap toJson() => { + 'error_type': errorType, + 'error_data': {'ticker': ticker, 'error': error}, + }; +} + +final class EnablePlatformCoinWithTokensErrorPrivKeyPolicyNotAllowed + extends EnablePlatformCoinWithTokensError { + const EnablePlatformCoinWithTokensErrorPrivKeyPolicyNotAllowed(this.value) + : super(errorType: 'PrivKeyPolicyNotAllowed'); + + factory EnablePlatformCoinWithTokensErrorPrivKeyPolicyNotAllowed.fromJson( + dynamic json, + ) { + return EnablePlatformCoinWithTokensErrorPrivKeyPolicyNotAllowed( + PrivKeyPolicyNotAllowed.fromJson(json), + ); + } + + final PrivKeyPolicyNotAllowed value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value.toJson()}; +} + +final class EnablePlatformCoinWithTokensErrorUnexpectedDerivationMethod + extends EnablePlatformCoinWithTokensError { + const EnablePlatformCoinWithTokensErrorUnexpectedDerivationMethod(this.value) + : super(errorType: 'UnexpectedDerivationMethod'); + + factory EnablePlatformCoinWithTokensErrorUnexpectedDerivationMethod.fromJson( + dynamic json, + ) { + return EnablePlatformCoinWithTokensErrorUnexpectedDerivationMethod( + _stringFromJson(json), + ); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class EnablePlatformCoinWithTokensErrorTransport + extends EnablePlatformCoinWithTokensError { + const EnablePlatformCoinWithTokensErrorTransport(this.value) + : super(errorType: 'Transport'); + + factory EnablePlatformCoinWithTokensErrorTransport.fromJson(dynamic json) { + return EnablePlatformCoinWithTokensErrorTransport(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class EnablePlatformCoinWithTokensErrorAtLeastOneNodeRequired + extends EnablePlatformCoinWithTokensError { + const EnablePlatformCoinWithTokensErrorAtLeastOneNodeRequired(this.value) + : super(errorType: 'AtLeastOneNodeRequired'); + + factory EnablePlatformCoinWithTokensErrorAtLeastOneNodeRequired.fromJson( + dynamic json, + ) { + return EnablePlatformCoinWithTokensErrorAtLeastOneNodeRequired( + _stringFromJson(json), + ); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class EnablePlatformCoinWithTokensErrorInvalidPayload + extends EnablePlatformCoinWithTokensError { + const EnablePlatformCoinWithTokensErrorInvalidPayload(this.value) + : super(errorType: 'InvalidPayload'); + + factory EnablePlatformCoinWithTokensErrorInvalidPayload.fromJson( + dynamic json, + ) { + return EnablePlatformCoinWithTokensErrorInvalidPayload( + _stringFromJson(json), + ); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class EnablePlatformCoinWithTokensErrorFailedSpawningBalanceEvents + extends EnablePlatformCoinWithTokensError { + const EnablePlatformCoinWithTokensErrorFailedSpawningBalanceEvents(this.value) + : super(errorType: 'FailedSpawningBalanceEvents'); + + factory EnablePlatformCoinWithTokensErrorFailedSpawningBalanceEvents.fromJson( + dynamic json, + ) { + return EnablePlatformCoinWithTokensErrorFailedSpawningBalanceEvents( + _stringFromJson(json), + ); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class EnablePlatformCoinWithTokensErrorInternal + extends EnablePlatformCoinWithTokensError { + const EnablePlatformCoinWithTokensErrorInternal(this.value) + : super(errorType: 'Internal'); + + factory EnablePlatformCoinWithTokensErrorInternal.fromJson(dynamic json) { + return EnablePlatformCoinWithTokensErrorInternal(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class EnablePlatformCoinWithTokensErrorNoSuchTask + extends EnablePlatformCoinWithTokensError { + const EnablePlatformCoinWithTokensErrorNoSuchTask(this.value) + : super(errorType: 'NoSuchTask'); + + factory EnablePlatformCoinWithTokensErrorNoSuchTask.fromJson(dynamic json) { + return EnablePlatformCoinWithTokensErrorNoSuchTask(TaskId.fromJson(json)); + } + + final TaskId value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value.toJson()}; +} + +final class EnablePlatformCoinWithTokensErrorTaskTimedOut + extends EnablePlatformCoinWithTokensError { + const EnablePlatformCoinWithTokensErrorTaskTimedOut({required this.duration}) + : super(errorType: 'TaskTimedOut'); + + factory EnablePlatformCoinWithTokensErrorTaskTimedOut.fromJson(dynamic json) { + final map = _asJsonMap(json); + return EnablePlatformCoinWithTokensErrorTaskTimedOut( + duration: Mm2Duration.fromJson(map.value('duration')), + ); + } + + final Mm2Duration duration; + + @override + JsonMap toJson() => { + 'error_type': errorType, + 'error_data': {'duration': duration.toJson()}, + }; +} + +final class EnablePlatformCoinWithTokensErrorUnexpectedDeviceActivationPolicy + extends EnablePlatformCoinWithTokensError { + const EnablePlatformCoinWithTokensErrorUnexpectedDeviceActivationPolicy() + : super(errorType: 'UnexpectedDeviceActivationPolicy'); + + factory EnablePlatformCoinWithTokensErrorUnexpectedDeviceActivationPolicy.fromJson() => + const EnablePlatformCoinWithTokensErrorUnexpectedDeviceActivationPolicy(); + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': null}; +} + +final class EnablePlatformCoinWithTokensErrorCustomTokenError + extends EnablePlatformCoinWithTokensError { + const EnablePlatformCoinWithTokensErrorCustomTokenError(this.value) + : super(errorType: 'CustomTokenError'); + + factory EnablePlatformCoinWithTokensErrorCustomTokenError.fromJson( + dynamic json, + ) { + return EnablePlatformCoinWithTokensErrorCustomTokenError( + CustomTokenError.fromJson(json), + ); + } + + final CustomTokenError value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value.toJson()}; +} + +final class EnablePlatformCoinWithTokensErrorWalletConnectError + extends EnablePlatformCoinWithTokensError { + const EnablePlatformCoinWithTokensErrorWalletConnectError(this.value) + : super(errorType: 'WalletConnectError'); + + factory EnablePlatformCoinWithTokensErrorWalletConnectError.fromJson( + dynamic json, + ) { + return EnablePlatformCoinWithTokensErrorWalletConnectError( + _stringFromJson(json), + ); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class EnablePlatformCoinWithTokensErrorUnknown + extends EnablePlatformCoinWithTokensError { + const EnablePlatformCoinWithTokensErrorUnknown({ + required this.data, + this.rawErrorType, + }) : super(errorType: 'unknown'); + + final String? rawErrorType; + final dynamic data; + + @override + JsonMap toJson() => { + 'error_type': rawErrorType ?? errorType, + 'error_data': data, + }; +} + +sealed class SiaCoinInitError { + const SiaCoinInitError({required this.errorType}); + + factory SiaCoinInitError.fromJson(dynamic json) { + final map = _asJsonMap(json); + final type = map.valueOrNull('error_type'); + final data = map.valueOrNull('error_data'); + switch (type) { + case 'CoinCreationError': + return SiaCoinInitErrorCoinCreationError.fromJson(data); + case 'CoinIsAlreadyActivated': + return SiaCoinInitErrorCoinIsAlreadyActivated.fromJson(data); + case 'HardwareWalletsAreNotSupportedYet': + return SiaCoinInitErrorHardwareWalletsAreNotSupportedYet.fromJson(); + case 'TaskTimedOut': + return SiaCoinInitErrorTaskTimedOut.fromJson(data); + case 'CouldNotGetBalance': + return SiaCoinInitErrorCouldNotGetBalance.fromJson(data); + case 'CouldNotGetBlockCount': + return SiaCoinInitErrorCouldNotGetBlockCount.fromJson(data); + case 'Internal': + return SiaCoinInitErrorInternal.fromJson(data); + default: + return SiaCoinInitErrorUnknown(rawErrorType: type, data: data); + } + } + + final String errorType; + + JsonMap toJson(); +} + +final class SiaCoinInitErrorCoinCreationError extends SiaCoinInitError { + const SiaCoinInitErrorCoinCreationError({ + required this.ticker, + required this.error, + }) : super(errorType: 'CoinCreationError'); + + factory SiaCoinInitErrorCoinCreationError.fromJson(dynamic json) { + final map = _asJsonMap(json); + return SiaCoinInitErrorCoinCreationError( + ticker: _stringFromJson(map.value('ticker')), + error: _stringFromJson(map.value('error')), + ); + } + + final String ticker; + final String error; + + @override + JsonMap toJson() => { + 'error_type': errorType, + 'error_data': {'ticker': ticker, 'error': error}, + }; +} + +final class SiaCoinInitErrorCoinIsAlreadyActivated extends SiaCoinInitError { + const SiaCoinInitErrorCoinIsAlreadyActivated({required this.ticker}) + : super(errorType: 'CoinIsAlreadyActivated'); + + factory SiaCoinInitErrorCoinIsAlreadyActivated.fromJson(dynamic json) { + final map = _asJsonMap(json); + return SiaCoinInitErrorCoinIsAlreadyActivated( + ticker: _stringFromJson(map.value('ticker')), + ); + } + + final String ticker; + + @override + JsonMap toJson() => { + 'error_type': errorType, + 'error_data': {'ticker': ticker}, + }; +} + +final class SiaCoinInitErrorHardwareWalletsAreNotSupportedYet + extends SiaCoinInitError { + const SiaCoinInitErrorHardwareWalletsAreNotSupportedYet() + : super(errorType: 'HardwareWalletsAreNotSupportedYet'); + + factory SiaCoinInitErrorHardwareWalletsAreNotSupportedYet.fromJson() => + const SiaCoinInitErrorHardwareWalletsAreNotSupportedYet(); + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': null}; +} + +final class SiaCoinInitErrorTaskTimedOut extends SiaCoinInitError { + const SiaCoinInitErrorTaskTimedOut({required this.duration}) + : super(errorType: 'TaskTimedOut'); + + factory SiaCoinInitErrorTaskTimedOut.fromJson(dynamic json) { + final map = _asJsonMap(json); + return SiaCoinInitErrorTaskTimedOut( + duration: Mm2Duration.fromJson(map.value('duration')), + ); + } + + final Mm2Duration duration; + + @override + JsonMap toJson() => { + 'error_type': errorType, + 'error_data': {'duration': duration.toJson()}, + }; +} + +final class SiaCoinInitErrorCouldNotGetBalance extends SiaCoinInitError { + const SiaCoinInitErrorCouldNotGetBalance(this.value) + : super(errorType: 'CouldNotGetBalance'); + + factory SiaCoinInitErrorCouldNotGetBalance.fromJson(dynamic json) { + return SiaCoinInitErrorCouldNotGetBalance(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class SiaCoinInitErrorCouldNotGetBlockCount extends SiaCoinInitError { + const SiaCoinInitErrorCouldNotGetBlockCount(this.value) + : super(errorType: 'CouldNotGetBlockCount'); + + factory SiaCoinInitErrorCouldNotGetBlockCount.fromJson(dynamic json) { + return SiaCoinInitErrorCouldNotGetBlockCount(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class SiaCoinInitErrorInternal extends SiaCoinInitError { + const SiaCoinInitErrorInternal(this.value) : super(errorType: 'Internal'); + + factory SiaCoinInitErrorInternal.fromJson(dynamic json) { + return SiaCoinInitErrorInternal(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class SiaCoinInitErrorUnknown extends SiaCoinInitError { + const SiaCoinInitErrorUnknown({required this.data, this.rawErrorType}) + : super(errorType: 'unknown'); + + final String? rawErrorType; + final dynamic data; + + @override + JsonMap toJson() => { + 'error_type': rawErrorType ?? errorType, + 'error_data': data, + }; +} + +sealed class InitStandaloneCoinError { + const InitStandaloneCoinError({required this.errorType}); + + factory InitStandaloneCoinError.fromJson(dynamic json) { + final map = _asJsonMap(json); + final type = map.valueOrNull('error_type'); + final data = map.valueOrNull('error_data'); + switch (type) { + case 'NoSuchTask': + return InitStandaloneCoinErrorNoSuchTask.fromJson(data); + case 'TaskTimedOut': + return InitStandaloneCoinErrorTaskTimedOut.fromJson(data); + case 'CoinIsAlreadyActivated': + return InitStandaloneCoinErrorCoinIsAlreadyActivated.fromJson(data); + case 'CoinConfigIsNotFound': + return InitStandaloneCoinErrorCoinConfigIsNotFound.fromJson(data); + case 'CoinProtocolParseError': + return InitStandaloneCoinErrorCoinProtocolParseError.fromJson(data); + case 'UnexpectedCoinProtocol': + return InitStandaloneCoinErrorUnexpectedCoinProtocol.fromJson(data); + case 'CoinCreationError': + return InitStandaloneCoinErrorCoinCreationError.fromJson(data); + case 'HwError': + return InitStandaloneCoinErrorHwError.fromJson(data); + case 'Transport': + return InitStandaloneCoinErrorTransport.fromJson(data); + case 'Internal': + return InitStandaloneCoinErrorInternal.fromJson(data); + default: + return InitStandaloneCoinErrorUnknown(rawErrorType: type, data: data); + } + } + + final String errorType; + + JsonMap toJson(); +} + +final class InitStandaloneCoinErrorNoSuchTask extends InitStandaloneCoinError { + const InitStandaloneCoinErrorNoSuchTask(this.value) + : super(errorType: 'NoSuchTask'); + + factory InitStandaloneCoinErrorNoSuchTask.fromJson(dynamic json) { + return InitStandaloneCoinErrorNoSuchTask(TaskId.fromJson(json)); + } + + final TaskId value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value.toJson()}; +} + +final class InitStandaloneCoinErrorTaskTimedOut + extends InitStandaloneCoinError { + const InitStandaloneCoinErrorTaskTimedOut({required this.duration}) + : super(errorType: 'TaskTimedOut'); + + factory InitStandaloneCoinErrorTaskTimedOut.fromJson(dynamic json) { + final map = _asJsonMap(json); + return InitStandaloneCoinErrorTaskTimedOut( + duration: Mm2Duration.fromJson(map.value('duration')), + ); + } + + final Mm2Duration duration; + + @override + JsonMap toJson() => { + 'error_type': errorType, + 'error_data': {'duration': duration.toJson()}, + }; +} + +final class InitStandaloneCoinErrorCoinIsAlreadyActivated + extends InitStandaloneCoinError { + const InitStandaloneCoinErrorCoinIsAlreadyActivated({required this.ticker}) + : super(errorType: 'CoinIsAlreadyActivated'); + + factory InitStandaloneCoinErrorCoinIsAlreadyActivated.fromJson(dynamic json) { + final map = _asJsonMap(json); + return InitStandaloneCoinErrorCoinIsAlreadyActivated( + ticker: _stringFromJson(map.value('ticker')), + ); + } + + final String ticker; + + @override + JsonMap toJson() => { + 'error_type': errorType, + 'error_data': {'ticker': ticker}, + }; +} + +final class InitStandaloneCoinErrorCoinConfigIsNotFound + extends InitStandaloneCoinError { + const InitStandaloneCoinErrorCoinConfigIsNotFound(this.value) + : super(errorType: 'CoinConfigIsNotFound'); + + factory InitStandaloneCoinErrorCoinConfigIsNotFound.fromJson(dynamic json) { + return InitStandaloneCoinErrorCoinConfigIsNotFound(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class InitStandaloneCoinErrorCoinProtocolParseError + extends InitStandaloneCoinError { + const InitStandaloneCoinErrorCoinProtocolParseError({ + required this.ticker, + required this.error, + }) : super(errorType: 'CoinProtocolParseError'); + + factory InitStandaloneCoinErrorCoinProtocolParseError.fromJson(dynamic json) { + final map = _asJsonMap(json); + return InitStandaloneCoinErrorCoinProtocolParseError( + ticker: _stringFromJson(map.value('ticker')), + error: _stringFromJson(map.value('error')), + ); + } + + final String ticker; + final String error; + + @override + JsonMap toJson() => { + 'error_type': errorType, + 'error_data': {'ticker': ticker, 'error': error}, + }; +} + +final class InitStandaloneCoinErrorUnexpectedCoinProtocol + extends InitStandaloneCoinError { + const InitStandaloneCoinErrorUnexpectedCoinProtocol({ + required this.ticker, + required this.protocol, + }) : super(errorType: 'UnexpectedCoinProtocol'); + + factory InitStandaloneCoinErrorUnexpectedCoinProtocol.fromJson(dynamic json) { + final map = _asJsonMap(json); + return InitStandaloneCoinErrorUnexpectedCoinProtocol( + ticker: _stringFromJson(map.value('ticker')), + protocol: JsonValue.fromJson(map.value('protocol')), + ); + } + + final String ticker; + final JsonValue protocol; + + @override + JsonMap toJson() => { + 'error_type': errorType, + 'error_data': {'ticker': ticker, 'protocol': protocol.toJson()}, + }; +} + +final class InitStandaloneCoinErrorCoinCreationError + extends InitStandaloneCoinError { + const InitStandaloneCoinErrorCoinCreationError({ + required this.ticker, + required this.error, + }) : super(errorType: 'CoinCreationError'); + + factory InitStandaloneCoinErrorCoinCreationError.fromJson(dynamic json) { + final map = _asJsonMap(json); + return InitStandaloneCoinErrorCoinCreationError( + ticker: _stringFromJson(map.value('ticker')), + error: _stringFromJson(map.value('error')), + ); + } + + final String ticker; + final String error; + + @override + JsonMap toJson() => { + 'error_type': errorType, + 'error_data': {'ticker': ticker, 'error': error}, + }; +} + +final class InitStandaloneCoinErrorHwError extends InitStandaloneCoinError { + const InitStandaloneCoinErrorHwError(this.value) + : super(errorType: 'HwError'); + + factory InitStandaloneCoinErrorHwError.fromJson(dynamic json) { + return InitStandaloneCoinErrorHwError(HwRpcError.fromJson(json)); + } + + final HwRpcError value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value.toJson()}; +} + +final class InitStandaloneCoinErrorTransport extends InitStandaloneCoinError { + const InitStandaloneCoinErrorTransport(this.value) + : super(errorType: 'Transport'); + + factory InitStandaloneCoinErrorTransport.fromJson(dynamic json) { + return InitStandaloneCoinErrorTransport(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class InitStandaloneCoinErrorInternal extends InitStandaloneCoinError { + const InitStandaloneCoinErrorInternal(this.value) + : super(errorType: 'Internal'); + + factory InitStandaloneCoinErrorInternal.fromJson(dynamic json) { + return InitStandaloneCoinErrorInternal(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class InitStandaloneCoinErrorUnknown extends InitStandaloneCoinError { + const InitStandaloneCoinErrorUnknown({required this.data, this.rawErrorType}) + : super(errorType: 'unknown'); + + final String? rawErrorType; + final dynamic data; + + @override + JsonMap toJson() => { + 'error_type': rawErrorType ?? errorType, + 'error_data': data, + }; +} + +sealed class EnableTokenError { + const EnableTokenError({required this.errorType}); + + factory EnableTokenError.fromJson(dynamic json) { + final map = _asJsonMap(json); + final type = map.valueOrNull('error_type'); + final data = map.valueOrNull('error_data'); + switch (type) { + case 'TokenIsAlreadyActivated': + return EnableTokenErrorTokenIsAlreadyActivated.fromJson(data); + case 'TokenConfigIsNotFound': + return EnableTokenErrorTokenConfigIsNotFound.fromJson(data); + case 'TokenProtocolParseError': + return EnableTokenErrorTokenProtocolParseError.fromJson(data); + case 'UnexpectedTokenProtocol': + return EnableTokenErrorUnexpectedTokenProtocol.fromJson(data); + case 'PlatformCoinIsNotActivated': + return EnableTokenErrorPlatformCoinIsNotActivated.fromJson(data); + case 'UnsupportedPlatformCoin': + return EnableTokenErrorUnsupportedPlatformCoin.fromJson(data); + case 'UnexpectedDerivationMethod': + return EnableTokenErrorUnexpectedDerivationMethod.fromJson(data); + case 'CouldNotFetchBalance': + return EnableTokenErrorCouldNotFetchBalance.fromJson(data); + case 'InvalidConfig': + return EnableTokenErrorInvalidConfig.fromJson(data); + case 'Transport': + return EnableTokenErrorTransport.fromJson(data); + case 'Internal': + return EnableTokenErrorInternal.fromJson(data); + case 'InvalidPayload': + return EnableTokenErrorInvalidPayload.fromJson(data); + case 'PrivKeyPolicyNotAllowed': + return EnableTokenErrorPrivKeyPolicyNotAllowed.fromJson(data); + case 'CustomTokenError': + return EnableTokenErrorCustomTokenError.fromJson(data); + default: + return EnableTokenErrorUnknown(rawErrorType: type, data: data); + } + } + + final String errorType; + + JsonMap toJson(); +} + +final class EnableTokenErrorTokenIsAlreadyActivated extends EnableTokenError { + const EnableTokenErrorTokenIsAlreadyActivated(this.value) + : super(errorType: 'TokenIsAlreadyActivated'); + + factory EnableTokenErrorTokenIsAlreadyActivated.fromJson(dynamic json) { + return EnableTokenErrorTokenIsAlreadyActivated(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class EnableTokenErrorTokenConfigIsNotFound extends EnableTokenError { + const EnableTokenErrorTokenConfigIsNotFound(this.value) + : super(errorType: 'TokenConfigIsNotFound'); + + factory EnableTokenErrorTokenConfigIsNotFound.fromJson(dynamic json) { + return EnableTokenErrorTokenConfigIsNotFound(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class EnableTokenErrorTokenProtocolParseError extends EnableTokenError { + const EnableTokenErrorTokenProtocolParseError({ + required this.ticker, + required this.error, + }) : super(errorType: 'TokenProtocolParseError'); + + factory EnableTokenErrorTokenProtocolParseError.fromJson(dynamic json) { + final map = _asJsonMap(json); + return EnableTokenErrorTokenProtocolParseError( + ticker: _stringFromJson(map.value('ticker')), + error: _stringFromJson(map.value('error')), + ); + } + + final String ticker; + final String error; + + @override + JsonMap toJson() => { + 'error_type': errorType, + 'error_data': {'ticker': ticker, 'error': error}, + }; +} + +final class EnableTokenErrorUnexpectedTokenProtocol extends EnableTokenError { + const EnableTokenErrorUnexpectedTokenProtocol({ + required this.ticker, + required this.protocol, + }) : super(errorType: 'UnexpectedTokenProtocol'); + + factory EnableTokenErrorUnexpectedTokenProtocol.fromJson(dynamic json) { + final map = _asJsonMap(json); + return EnableTokenErrorUnexpectedTokenProtocol( + ticker: _stringFromJson(map.value('ticker')), + protocol: JsonValue.fromJson(map.value('protocol')), + ); + } + + final String ticker; + final JsonValue protocol; + + @override + JsonMap toJson() => { + 'error_type': errorType, + 'error_data': {'ticker': ticker, 'protocol': protocol.toJson()}, + }; +} + +final class EnableTokenErrorPlatformCoinIsNotActivated + extends EnableTokenError { + const EnableTokenErrorPlatformCoinIsNotActivated(this.value) + : super(errorType: 'PlatformCoinIsNotActivated'); + + factory EnableTokenErrorPlatformCoinIsNotActivated.fromJson(dynamic json) { + return EnableTokenErrorPlatformCoinIsNotActivated(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class EnableTokenErrorUnsupportedPlatformCoin extends EnableTokenError { + const EnableTokenErrorUnsupportedPlatformCoin({ + required this.platformCoinTicker, + required this.tokenTicker, + }) : super(errorType: 'UnsupportedPlatformCoin'); + + factory EnableTokenErrorUnsupportedPlatformCoin.fromJson(dynamic json) { + final map = _asJsonMap(json); + return EnableTokenErrorUnsupportedPlatformCoin( + platformCoinTicker: _stringFromJson( + map.value('platform_coin_ticker'), + ), + tokenTicker: _stringFromJson(map.value('token_ticker')), + ); + } + + final String platformCoinTicker; + final String tokenTicker; + + @override + JsonMap toJson() => { + 'error_type': errorType, + 'error_data': { + 'platform_coin_ticker': platformCoinTicker, + 'token_ticker': tokenTicker, + }, + }; +} + +final class EnableTokenErrorUnexpectedDerivationMethod + extends EnableTokenError { + const EnableTokenErrorUnexpectedDerivationMethod(this.value) + : super(errorType: 'UnexpectedDerivationMethod'); + + factory EnableTokenErrorUnexpectedDerivationMethod.fromJson(dynamic json) { + return EnableTokenErrorUnexpectedDerivationMethod( + UnexpectedDerivationMethod.fromJson(json), + ); + } + + final UnexpectedDerivationMethod value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value.toJson()}; +} + +final class EnableTokenErrorCouldNotFetchBalance extends EnableTokenError { + const EnableTokenErrorCouldNotFetchBalance(this.value) + : super(errorType: 'CouldNotFetchBalance'); + + factory EnableTokenErrorCouldNotFetchBalance.fromJson(dynamic json) { + return EnableTokenErrorCouldNotFetchBalance(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class EnableTokenErrorInvalidConfig extends EnableTokenError { + const EnableTokenErrorInvalidConfig(this.value) + : super(errorType: 'InvalidConfig'); + + factory EnableTokenErrorInvalidConfig.fromJson(dynamic json) { + return EnableTokenErrorInvalidConfig(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class EnableTokenErrorTransport extends EnableTokenError { + const EnableTokenErrorTransport(this.value) : super(errorType: 'Transport'); + + factory EnableTokenErrorTransport.fromJson(dynamic json) { + return EnableTokenErrorTransport(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class EnableTokenErrorInternal extends EnableTokenError { + const EnableTokenErrorInternal(this.value) : super(errorType: 'Internal'); + + factory EnableTokenErrorInternal.fromJson(dynamic json) { + return EnableTokenErrorInternal(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class EnableTokenErrorInvalidPayload extends EnableTokenError { + const EnableTokenErrorInvalidPayload(this.value) + : super(errorType: 'InvalidPayload'); + + factory EnableTokenErrorInvalidPayload.fromJson(dynamic json) { + return EnableTokenErrorInvalidPayload(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class EnableTokenErrorPrivKeyPolicyNotAllowed extends EnableTokenError { + const EnableTokenErrorPrivKeyPolicyNotAllowed(this.value) + : super(errorType: 'PrivKeyPolicyNotAllowed'); + + factory EnableTokenErrorPrivKeyPolicyNotAllowed.fromJson(dynamic json) { + return EnableTokenErrorPrivKeyPolicyNotAllowed( + PrivKeyPolicyNotAllowed.fromJson(json), + ); + } + + final PrivKeyPolicyNotAllowed value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value.toJson()}; +} + +final class EnableTokenErrorCustomTokenError extends EnableTokenError { + const EnableTokenErrorCustomTokenError(this.value) + : super(errorType: 'CustomTokenError'); + + factory EnableTokenErrorCustomTokenError.fromJson(dynamic json) { + return EnableTokenErrorCustomTokenError(CustomTokenError.fromJson(json)); + } + + final CustomTokenError value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value.toJson()}; +} + +final class EnableTokenErrorUnknown extends EnableTokenError { + const EnableTokenErrorUnknown({required this.data, this.rawErrorType}) + : super(errorType: 'unknown'); + + final String? rawErrorType; + final dynamic data; + + @override + JsonMap toJson() => { + 'error_type': rawErrorType ?? errorType, + 'error_data': data, + }; +} + +sealed class CancelEnableCoinUnifiedError { + const CancelEnableCoinUnifiedError({required this.errorType}); + + factory CancelEnableCoinUnifiedError.fromJson(dynamic json) { + final map = _asJsonMap(json); + final type = map.valueOrNull('error_type'); + final data = map.valueOrNull('error_data'); + switch (type) { + case 'NoSuchTask': + return CancelEnableCoinUnifiedErrorNoSuchTask.fromJson(data); + case 'Internal': + return CancelEnableCoinUnifiedErrorInternal.fromJson(data); + default: + return CancelEnableCoinUnifiedErrorUnknown( + rawErrorType: type, + data: data, + ); + } + } + + final String errorType; + + JsonMap toJson(); +} + +final class CancelEnableCoinUnifiedErrorNoSuchTask + extends CancelEnableCoinUnifiedError { + const CancelEnableCoinUnifiedErrorNoSuchTask(this.value) + : super(errorType: 'NoSuchTask'); + + factory CancelEnableCoinUnifiedErrorNoSuchTask.fromJson(dynamic json) { + return CancelEnableCoinUnifiedErrorNoSuchTask(_intFromJson(json)); + } + + final int value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class CancelEnableCoinUnifiedErrorInternal + extends CancelEnableCoinUnifiedError { + const CancelEnableCoinUnifiedErrorInternal(this.value) + : super(errorType: 'Internal'); + + factory CancelEnableCoinUnifiedErrorInternal.fromJson(dynamic json) { + return CancelEnableCoinUnifiedErrorInternal(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class CancelEnableCoinUnifiedErrorUnknown + extends CancelEnableCoinUnifiedError { + const CancelEnableCoinUnifiedErrorUnknown({ + required this.data, + this.rawErrorType, + }) : super(errorType: 'unknown'); + + final String? rawErrorType; + final dynamic data; + + @override + JsonMap toJson() => { + 'error_type': rawErrorType ?? errorType, + 'error_data': data, + }; +} + +sealed class EnableCoinUnifiedError { + const EnableCoinUnifiedError({required this.errorType}); + + factory EnableCoinUnifiedError.fromJson(dynamic json) { + final map = _asJsonMap(json); + final type = map.valueOrNull('error_type'); + final data = map.valueOrNull('error_data'); + switch (type) { + case 'UnsupportedProtocol': + return EnableCoinUnifiedErrorUnsupportedProtocol.fromJson(data); + case 'MissingActivationParams': + return EnableCoinUnifiedErrorMissingActivationParams.fromJson(data); + case 'InvalidActivationRequest': + return EnableCoinUnifiedErrorInvalidActivationRequest.fromJson(data); + case 'Internal': + return EnableCoinUnifiedErrorInternal.fromJson(data); + case 'UtxoInitError': + return EnableCoinUnifiedErrorUtxoInitError.fromJson(data); + case 'QtumInitError': + return EnableCoinUnifiedErrorQtumInitError.fromJson(data); + case 'ZcoinInitError': + return EnableCoinUnifiedErrorZcoinInitError.fromJson(data); + case 'EthInitError': + return EnableCoinUnifiedErrorEthInitError.fromJson(data); + case 'Erc20InitError': + return EnableCoinUnifiedErrorErc20InitError.fromJson(data); + case 'TendermintInitError': + return EnableCoinUnifiedErrorTendermintInitError.fromJson(data); + default: + return EnableCoinUnifiedErrorUnknown(rawErrorType: type, data: data); + } + } + + final String errorType; + + JsonMap toJson(); +} + +final class EnableCoinUnifiedErrorUnsupportedProtocol + extends EnableCoinUnifiedError { + const EnableCoinUnifiedErrorUnsupportedProtocol({required this.ticker}) + : super(errorType: 'UnsupportedProtocol'); + + factory EnableCoinUnifiedErrorUnsupportedProtocol.fromJson(dynamic json) { + final map = _asJsonMap(json); + return EnableCoinUnifiedErrorUnsupportedProtocol( + ticker: _stringFromJson(map.value('ticker')), + ); + } + + final String ticker; + + @override + JsonMap toJson() => { + 'error_type': errorType, + 'error_data': {'ticker': ticker}, + }; +} + +final class EnableCoinUnifiedErrorMissingActivationParams + extends EnableCoinUnifiedError { + const EnableCoinUnifiedErrorMissingActivationParams(this.value) + : super(errorType: 'MissingActivationParams'); + + factory EnableCoinUnifiedErrorMissingActivationParams.fromJson(dynamic json) { + return EnableCoinUnifiedErrorMissingActivationParams(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class EnableCoinUnifiedErrorInvalidActivationRequest + extends EnableCoinUnifiedError { + const EnableCoinUnifiedErrorInvalidActivationRequest(this.value) + : super(errorType: 'InvalidActivationRequest'); + + factory EnableCoinUnifiedErrorInvalidActivationRequest.fromJson( + dynamic json, + ) { + return EnableCoinUnifiedErrorInvalidActivationRequest( + _stringFromJson(json), + ); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class EnableCoinUnifiedErrorInternal extends EnableCoinUnifiedError { + const EnableCoinUnifiedErrorInternal(this.value) + : super(errorType: 'Internal'); + + factory EnableCoinUnifiedErrorInternal.fromJson(dynamic json) { + return EnableCoinUnifiedErrorInternal(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class EnableCoinUnifiedErrorUtxoInitError extends EnableCoinUnifiedError { + const EnableCoinUnifiedErrorUtxoInitError(this.value) + : super(errorType: 'UtxoInitError'); + + factory EnableCoinUnifiedErrorUtxoInitError.fromJson(dynamic json) { + return EnableCoinUnifiedErrorUtxoInitError(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class EnableCoinUnifiedErrorQtumInitError extends EnableCoinUnifiedError { + const EnableCoinUnifiedErrorQtumInitError(this.value) + : super(errorType: 'QtumInitError'); + + factory EnableCoinUnifiedErrorQtumInitError.fromJson(dynamic json) { + return EnableCoinUnifiedErrorQtumInitError(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class EnableCoinUnifiedErrorZcoinInitError + extends EnableCoinUnifiedError { + const EnableCoinUnifiedErrorZcoinInitError(this.value) + : super(errorType: 'ZcoinInitError'); + + factory EnableCoinUnifiedErrorZcoinInitError.fromJson(dynamic json) { + return EnableCoinUnifiedErrorZcoinInitError(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class EnableCoinUnifiedErrorEthInitError extends EnableCoinUnifiedError { + const EnableCoinUnifiedErrorEthInitError(this.value) + : super(errorType: 'EthInitError'); + + factory EnableCoinUnifiedErrorEthInitError.fromJson(dynamic json) { + return EnableCoinUnifiedErrorEthInitError(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class EnableCoinUnifiedErrorErc20InitError + extends EnableCoinUnifiedError { + const EnableCoinUnifiedErrorErc20InitError(this.value) + : super(errorType: 'Erc20InitError'); + + factory EnableCoinUnifiedErrorErc20InitError.fromJson(dynamic json) { + return EnableCoinUnifiedErrorErc20InitError(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class EnableCoinUnifiedErrorTendermintInitError + extends EnableCoinUnifiedError { + const EnableCoinUnifiedErrorTendermintInitError(this.value) + : super(errorType: 'TendermintInitError'); + + factory EnableCoinUnifiedErrorTendermintInitError.fromJson(dynamic json) { + return EnableCoinUnifiedErrorTendermintInitError(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class EnableCoinUnifiedErrorUnknown extends EnableCoinUnifiedError { + const EnableCoinUnifiedErrorUnknown({required this.data, this.rawErrorType}) + : super(errorType: 'unknown'); + + final String? rawErrorType; + final dynamic data; + + @override + JsonMap toJson() => { + 'error_type': rawErrorType ?? errorType, + 'error_data': data, + }; +} + +sealed class EnableCoinUnifiedStatusError { + const EnableCoinUnifiedStatusError({required this.errorType}); + + factory EnableCoinUnifiedStatusError.fromJson(dynamic json) { + final map = _asJsonMap(json); + final type = map.valueOrNull('error_type'); + final data = map.valueOrNull('error_data'); + switch (type) { + case 'NoSuchTask': + return EnableCoinUnifiedStatusErrorNoSuchTask.fromJson(data); + case 'Internal': + return EnableCoinUnifiedStatusErrorInternal.fromJson(data); + default: + return EnableCoinUnifiedStatusErrorUnknown( + rawErrorType: type, + data: data, + ); + } + } + + final String errorType; + + JsonMap toJson(); +} + +final class EnableCoinUnifiedStatusErrorNoSuchTask + extends EnableCoinUnifiedStatusError { + const EnableCoinUnifiedStatusErrorNoSuchTask(this.value) + : super(errorType: 'NoSuchTask'); + + factory EnableCoinUnifiedStatusErrorNoSuchTask.fromJson(dynamic json) { + return EnableCoinUnifiedStatusErrorNoSuchTask(_intFromJson(json)); + } + + final int value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class EnableCoinUnifiedStatusErrorInternal + extends EnableCoinUnifiedStatusError { + const EnableCoinUnifiedStatusErrorInternal(this.value) + : super(errorType: 'Internal'); + + factory EnableCoinUnifiedStatusErrorInternal.fromJson(dynamic json) { + return EnableCoinUnifiedStatusErrorInternal(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class EnableCoinUnifiedStatusErrorUnknown + extends EnableCoinUnifiedStatusError { + const EnableCoinUnifiedStatusErrorUnknown({ + required this.data, + this.rawErrorType, + }) : super(errorType: 'unknown'); + + final String? rawErrorType; + final dynamic data; + + @override + JsonMap toJson() => { + 'error_type': rawErrorType ?? errorType, + 'error_data': data, + }; +} + +sealed class InitUtxoStandardError { + const InitUtxoStandardError({required this.errorType}); + + factory InitUtxoStandardError.fromJson(dynamic json) { + final map = _asJsonMap(json); + final type = map.valueOrNull('error_type'); + final data = map.valueOrNull('error_data'); + switch (type) { + case 'HwError': + return InitUtxoStandardErrorHwError.fromJson(data); + case 'TaskTimedOut': + return InitUtxoStandardErrorTaskTimedOut.fromJson(data); + case 'CoinIsAlreadyActivated': + return InitUtxoStandardErrorCoinIsAlreadyActivated.fromJson(data); + case 'CoinCreationError': + return InitUtxoStandardErrorCoinCreationError.fromJson(data); + case 'Transport': + return InitUtxoStandardErrorTransport.fromJson(data); + case 'Internal': + return InitUtxoStandardErrorInternal.fromJson(data); + default: + return InitUtxoStandardErrorUnknown(rawErrorType: type, data: data); + } + } + + final String errorType; + + JsonMap toJson(); +} + +final class InitUtxoStandardErrorHwError extends InitUtxoStandardError { + const InitUtxoStandardErrorHwError(this.value) : super(errorType: 'HwError'); + + factory InitUtxoStandardErrorHwError.fromJson(dynamic json) { + return InitUtxoStandardErrorHwError(HwRpcError.fromJson(json)); + } + + final HwRpcError value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value.toJson()}; +} + +final class InitUtxoStandardErrorTaskTimedOut extends InitUtxoStandardError { + const InitUtxoStandardErrorTaskTimedOut({required this.duration}) + : super(errorType: 'TaskTimedOut'); + + factory InitUtxoStandardErrorTaskTimedOut.fromJson(dynamic json) { + final map = _asJsonMap(json); + return InitUtxoStandardErrorTaskTimedOut( + duration: Mm2Duration.fromJson(map.value('duration')), + ); + } + + final Mm2Duration duration; + + @override + JsonMap toJson() => { + 'error_type': errorType, + 'error_data': {'duration': duration.toJson()}, + }; +} + +final class InitUtxoStandardErrorCoinIsAlreadyActivated + extends InitUtxoStandardError { + const InitUtxoStandardErrorCoinIsAlreadyActivated({required this.ticker}) + : super(errorType: 'CoinIsAlreadyActivated'); + + factory InitUtxoStandardErrorCoinIsAlreadyActivated.fromJson(dynamic json) { + final map = _asJsonMap(json); + return InitUtxoStandardErrorCoinIsAlreadyActivated( + ticker: _stringFromJson(map.value('ticker')), + ); + } + + final String ticker; + + @override + JsonMap toJson() => { + 'error_type': errorType, + 'error_data': {'ticker': ticker}, + }; +} + +final class InitUtxoStandardErrorCoinCreationError + extends InitUtxoStandardError { + const InitUtxoStandardErrorCoinCreationError({ + required this.ticker, + required this.error, + }) : super(errorType: 'CoinCreationError'); + + factory InitUtxoStandardErrorCoinCreationError.fromJson(dynamic json) { + final map = _asJsonMap(json); + return InitUtxoStandardErrorCoinCreationError( + ticker: _stringFromJson(map.value('ticker')), + error: _stringFromJson(map.value('error')), + ); + } + + final String ticker; + final String error; + + @override + JsonMap toJson() => { + 'error_type': errorType, + 'error_data': {'ticker': ticker, 'error': error}, + }; +} + +final class InitUtxoStandardErrorTransport extends InitUtxoStandardError { + const InitUtxoStandardErrorTransport(this.value) + : super(errorType: 'Transport'); + + factory InitUtxoStandardErrorTransport.fromJson(dynamic json) { + return InitUtxoStandardErrorTransport(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class InitUtxoStandardErrorInternal extends InitUtxoStandardError { + const InitUtxoStandardErrorInternal(this.value) + : super(errorType: 'Internal'); + + factory InitUtxoStandardErrorInternal.fromJson(dynamic json) { + return InitUtxoStandardErrorInternal(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class InitUtxoStandardErrorUnknown extends InitUtxoStandardError { + const InitUtxoStandardErrorUnknown({required this.data, this.rawErrorType}) + : super(errorType: 'unknown'); + + final String? rawErrorType; + final dynamic data; + + @override + JsonMap toJson() => { + 'error_type': rawErrorType ?? errorType, + 'error_data': data, + }; +} + +sealed class ZcoinInitError { + const ZcoinInitError({required this.errorType}); + + factory ZcoinInitError.fromJson(dynamic json) { + final map = _asJsonMap(json); + final type = map.valueOrNull('error_type'); + final data = map.valueOrNull('error_data'); + switch (type) { + case 'CoinCreationError': + return ZcoinInitErrorCoinCreationError.fromJson(data); + case 'CoinIsAlreadyActivated': + return ZcoinInitErrorCoinIsAlreadyActivated.fromJson(data); + case 'HardwareWalletsAreNotSupportedYet': + return ZcoinInitErrorHardwareWalletsAreNotSupportedYet.fromJson(); + case 'TaskTimedOut': + return ZcoinInitErrorTaskTimedOut.fromJson(data); + case 'CouldNotGetBalance': + return ZcoinInitErrorCouldNotGetBalance.fromJson(data); + case 'CouldNotGetBlockCount': + return ZcoinInitErrorCouldNotGetBlockCount.fromJson(data); + case 'Internal': + return ZcoinInitErrorInternal.fromJson(data); + default: + return ZcoinInitErrorUnknown(rawErrorType: type, data: data); + } + } + + final String errorType; + + JsonMap toJson(); +} + +final class ZcoinInitErrorCoinCreationError extends ZcoinInitError { + const ZcoinInitErrorCoinCreationError({ + required this.ticker, + required this.error, + }) : super(errorType: 'CoinCreationError'); + + factory ZcoinInitErrorCoinCreationError.fromJson(dynamic json) { + final map = _asJsonMap(json); + return ZcoinInitErrorCoinCreationError( + ticker: _stringFromJson(map.value('ticker')), + error: _stringFromJson(map.value('error')), + ); + } + + final String ticker; + final String error; + + @override + JsonMap toJson() => { + 'error_type': errorType, + 'error_data': {'ticker': ticker, 'error': error}, + }; +} + +final class ZcoinInitErrorCoinIsAlreadyActivated extends ZcoinInitError { + const ZcoinInitErrorCoinIsAlreadyActivated({required this.ticker}) + : super(errorType: 'CoinIsAlreadyActivated'); + + factory ZcoinInitErrorCoinIsAlreadyActivated.fromJson(dynamic json) { + final map = _asJsonMap(json); + return ZcoinInitErrorCoinIsAlreadyActivated( + ticker: _stringFromJson(map.value('ticker')), + ); + } + + final String ticker; + + @override + JsonMap toJson() => { + 'error_type': errorType, + 'error_data': {'ticker': ticker}, + }; +} + +final class ZcoinInitErrorHardwareWalletsAreNotSupportedYet + extends ZcoinInitError { + const ZcoinInitErrorHardwareWalletsAreNotSupportedYet() + : super(errorType: 'HardwareWalletsAreNotSupportedYet'); + + factory ZcoinInitErrorHardwareWalletsAreNotSupportedYet.fromJson() => + const ZcoinInitErrorHardwareWalletsAreNotSupportedYet(); + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': null}; +} + +final class ZcoinInitErrorTaskTimedOut extends ZcoinInitError { + const ZcoinInitErrorTaskTimedOut({required this.duration}) + : super(errorType: 'TaskTimedOut'); + + factory ZcoinInitErrorTaskTimedOut.fromJson(dynamic json) { + final map = _asJsonMap(json); + return ZcoinInitErrorTaskTimedOut( + duration: Mm2Duration.fromJson(map.value('duration')), + ); + } + + final Mm2Duration duration; + + @override + JsonMap toJson() => { + 'error_type': errorType, + 'error_data': {'duration': duration.toJson()}, + }; +} + +final class ZcoinInitErrorCouldNotGetBalance extends ZcoinInitError { + const ZcoinInitErrorCouldNotGetBalance(this.value) + : super(errorType: 'CouldNotGetBalance'); + + factory ZcoinInitErrorCouldNotGetBalance.fromJson(dynamic json) { + return ZcoinInitErrorCouldNotGetBalance(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class ZcoinInitErrorCouldNotGetBlockCount extends ZcoinInitError { + const ZcoinInitErrorCouldNotGetBlockCount(this.value) + : super(errorType: 'CouldNotGetBlockCount'); + + factory ZcoinInitErrorCouldNotGetBlockCount.fromJson(dynamic json) { + return ZcoinInitErrorCouldNotGetBlockCount(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class ZcoinInitErrorInternal extends ZcoinInitError { + const ZcoinInitErrorInternal(this.value) : super(errorType: 'Internal'); + + factory ZcoinInitErrorInternal.fromJson(dynamic json) { + return ZcoinInitErrorInternal(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class ZcoinInitErrorUnknown extends ZcoinInitError { + const ZcoinInitErrorUnknown({required this.data, this.rawErrorType}) + : super(errorType: 'unknown'); + + final String? rawErrorType; + final dynamic data; + + @override + JsonMap toJson() => { + 'error_type': rawErrorType ?? errorType, + 'error_data': data, + }; +} + +sealed class SerializationError { + const SerializationError({required this.errorType}); + + factory SerializationError.fromJson(dynamic json) { + final map = _asJsonMap(json); + final type = map.valueOrNull('error_type'); + final data = map.valueOrNull('error_data'); + switch (type) { + case 'InternalError': + return SerializationErrorInternalError.fromJson(data); + default: + return SerializationErrorUnknown(rawErrorType: type, data: data); + } + } + + final String errorType; + + JsonMap toJson(); +} + +final class SerializationErrorInternalError extends SerializationError { + const SerializationErrorInternalError(this.value) + : super(errorType: 'InternalError'); + + factory SerializationErrorInternalError.fromJson(dynamic json) { + return SerializationErrorInternalError(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class SerializationErrorUnknown extends SerializationError { + const SerializationErrorUnknown({required this.data, this.rawErrorType}) + : super(errorType: 'unknown'); + + final String? rawErrorType; + final dynamic data; + + @override + JsonMap toJson() => { + 'error_type': rawErrorType ?? errorType, + 'error_data': data, + }; +} + +sealed class SendAskedDataError { + const SendAskedDataError({required this.errorType}); + + factory SendAskedDataError.fromJson(dynamic json) { + final map = _asJsonMap(json); + final type = map.valueOrNull('error_type'); + final data = map.valueOrNull('error_data'); + switch (type) { + case 'NotFound': + return SendAskedDataErrorNotFound.fromJson(data); + case 'Internal': + return SendAskedDataErrorInternal.fromJson(data); + default: + return SendAskedDataErrorUnknown(rawErrorType: type, data: data); + } + } + + final String errorType; + + JsonMap toJson(); +} + +final class SendAskedDataErrorNotFound extends SendAskedDataError { + const SendAskedDataErrorNotFound(this.value) : super(errorType: 'NotFound'); + + factory SendAskedDataErrorNotFound.fromJson(dynamic json) { + return SendAskedDataErrorNotFound(_intFromJson(json)); + } + + final int value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class SendAskedDataErrorInternal extends SendAskedDataError { + const SendAskedDataErrorInternal(this.value) : super(errorType: 'Internal'); + + factory SendAskedDataErrorInternal.fromJson(dynamic json) { + return SendAskedDataErrorInternal(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class SendAskedDataErrorUnknown extends SendAskedDataError { + const SendAskedDataErrorUnknown({required this.data, this.rawErrorType}) + : super(errorType: 'unknown'); + + final String? rawErrorType; + final dynamic data; + + @override + JsonMap toJson() => { + 'error_type': rawErrorType ?? errorType, + 'error_data': data, + }; +} + +sealed class ForwardedError { + const ForwardedError({required this.errorType}); + + factory ForwardedError.fromJson(dynamic json) { + final map = _asJsonMap(json); + final type = map.valueOrNull('error_type'); + final data = map.valueOrNull('error_data'); + switch (type) { + case 'NotSufficientBalance': + return ForwardedErrorNotSufficientBalance.fromJson(data); + default: + return ForwardedErrorUnknown(rawErrorType: type, data: data); + } + } + + final String errorType; + + JsonMap toJson(); +} + +final class ForwardedErrorNotSufficientBalance extends ForwardedError { + const ForwardedErrorNotSufficientBalance({required this.missing}) + : super(errorType: 'NotSufficientBalance'); + + factory ForwardedErrorNotSufficientBalance.fromJson(dynamic json) { + final map = _asJsonMap(json); + return ForwardedErrorNotSufficientBalance( + missing: _intFromJson(map.value('missing')), + ); + } + + final int missing; + + @override + JsonMap toJson() => { + 'error_type': errorType, + 'error_data': {'missing': missing}, + }; +} + +final class ForwardedErrorUnknown extends ForwardedError { + const ForwardedErrorUnknown({required this.data, this.rawErrorType}) + : super(errorType: 'unknown'); + + final String? rawErrorType; + final dynamic data; + + @override + JsonMap toJson() => { + 'error_type': rawErrorType ?? errorType, + 'error_data': data, + }; +} + +sealed class AccountRpcError { + const AccountRpcError({required this.errorType}); + + factory AccountRpcError.fromJson(dynamic json) { + final map = _asJsonMap(json); + final type = map.valueOrNull('error_type'); + final data = map.valueOrNull('error_data'); + switch (type) { + case 'NameTooLong': + return AccountRpcErrorNameTooLong.fromJson(data); + case 'DescriptionTooLong': + return AccountRpcErrorDescriptionTooLong.fromJson(data); + case 'TickerTooLong': + return AccountRpcErrorTickerTooLong.fromJson(data); + case 'NoSuchAccount': + return AccountRpcErrorNoSuchAccount.fromJson(data); + case 'NoEnabledAccount': + return AccountRpcErrorNoEnabledAccount.fromJson(); + case 'AccountExistsAlready': + return AccountRpcErrorAccountExistsAlready.fromJson(data); + case 'ErrorLoadingAccount': + return AccountRpcErrorErrorLoadingAccount.fromJson(data); + case 'ErrorSavingAccount': + return AccountRpcErrorErrorSavingAccount.fromJson(data); + case 'Internal': + return AccountRpcErrorInternal.fromJson(data); + default: + return AccountRpcErrorUnknown(rawErrorType: type, data: data); + } + } + + final String errorType; + + JsonMap toJson(); +} + +final class AccountRpcErrorNameTooLong extends AccountRpcError { + const AccountRpcErrorNameTooLong({required this.maxLen}) + : super(errorType: 'NameTooLong'); + + factory AccountRpcErrorNameTooLong.fromJson(dynamic json) { + final map = _asJsonMap(json); + return AccountRpcErrorNameTooLong( + maxLen: _intFromJson(map.value('max_len')), + ); + } + + final int maxLen; + + @override + JsonMap toJson() => { + 'error_type': errorType, + 'error_data': {'max_len': maxLen}, + }; +} + +final class AccountRpcErrorDescriptionTooLong extends AccountRpcError { + const AccountRpcErrorDescriptionTooLong({required this.maxLen}) + : super(errorType: 'DescriptionTooLong'); + + factory AccountRpcErrorDescriptionTooLong.fromJson(dynamic json) { + final map = _asJsonMap(json); + return AccountRpcErrorDescriptionTooLong( + maxLen: _intFromJson(map.value('max_len')), + ); + } + + final int maxLen; + + @override + JsonMap toJson() => { + 'error_type': errorType, + 'error_data': {'max_len': maxLen}, + }; +} + +final class AccountRpcErrorTickerTooLong extends AccountRpcError { + const AccountRpcErrorTickerTooLong({required this.maxLen}) + : super(errorType: 'TickerTooLong'); + + factory AccountRpcErrorTickerTooLong.fromJson(dynamic json) { + final map = _asJsonMap(json); + return AccountRpcErrorTickerTooLong( + maxLen: _intFromJson(map.value('max_len')), + ); + } + + final int maxLen; + + @override + JsonMap toJson() => { + 'error_type': errorType, + 'error_data': {'max_len': maxLen}, + }; +} + +final class AccountRpcErrorNoSuchAccount extends AccountRpcError { + const AccountRpcErrorNoSuchAccount(this.value) + : super(errorType: 'NoSuchAccount'); + + factory AccountRpcErrorNoSuchAccount.fromJson(dynamic json) { + return AccountRpcErrorNoSuchAccount(AccountId.fromJson(json)); + } + + final AccountId value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value.toJson()}; +} + +final class AccountRpcErrorNoEnabledAccount extends AccountRpcError { + const AccountRpcErrorNoEnabledAccount() + : super(errorType: 'NoEnabledAccount'); + + factory AccountRpcErrorNoEnabledAccount.fromJson() => + const AccountRpcErrorNoEnabledAccount(); + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': null}; +} + +final class AccountRpcErrorAccountExistsAlready extends AccountRpcError { + const AccountRpcErrorAccountExistsAlready(this.value) + : super(errorType: 'AccountExistsAlready'); + + factory AccountRpcErrorAccountExistsAlready.fromJson(dynamic json) { + return AccountRpcErrorAccountExistsAlready(AccountId.fromJson(json)); + } + + final AccountId value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value.toJson()}; +} + +final class AccountRpcErrorErrorLoadingAccount extends AccountRpcError { + const AccountRpcErrorErrorLoadingAccount(this.value) + : super(errorType: 'ErrorLoadingAccount'); + + factory AccountRpcErrorErrorLoadingAccount.fromJson(dynamic json) { + return AccountRpcErrorErrorLoadingAccount(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class AccountRpcErrorErrorSavingAccount extends AccountRpcError { + const AccountRpcErrorErrorSavingAccount(this.value) + : super(errorType: 'ErrorSavingAccount'); + + factory AccountRpcErrorErrorSavingAccount.fromJson(dynamic json) { + return AccountRpcErrorErrorSavingAccount(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class AccountRpcErrorInternal extends AccountRpcError { + const AccountRpcErrorInternal(this.value) : super(errorType: 'Internal'); + + factory AccountRpcErrorInternal.fromJson(dynamic json) { + return AccountRpcErrorInternal(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class AccountRpcErrorUnknown extends AccountRpcError { + const AccountRpcErrorUnknown({required this.data, this.rawErrorType}) + : super(errorType: 'unknown'); + + final String? rawErrorType; + final dynamic data; + + @override + JsonMap toJson() => { + 'error_type': rawErrorType ?? errorType, + 'error_data': data, + }; +} + +sealed class HealthcheckRpcError { + const HealthcheckRpcError({required this.errorType}); + + factory HealthcheckRpcError.fromJson(dynamic json) { + final map = _asJsonMap(json); + final type = map.valueOrNull('error_type'); + final data = map.valueOrNull('error_data'); + switch (type) { + case 'MessageGenerationFailed': + return HealthcheckRpcErrorMessageGenerationFailed.fromJson(data); + case 'MessageEncodingFailed': + return HealthcheckRpcErrorMessageEncodingFailed.fromJson(data); + case 'Internal': + return HealthcheckRpcErrorInternal.fromJson(data); + default: + return HealthcheckRpcErrorUnknown(rawErrorType: type, data: data); + } + } + + final String errorType; + + JsonMap toJson(); +} + +final class HealthcheckRpcErrorMessageGenerationFailed + extends HealthcheckRpcError { + const HealthcheckRpcErrorMessageGenerationFailed({required this.reason}) + : super(errorType: 'MessageGenerationFailed'); + + factory HealthcheckRpcErrorMessageGenerationFailed.fromJson(dynamic json) { + final map = _asJsonMap(json); + return HealthcheckRpcErrorMessageGenerationFailed( + reason: _stringFromJson(map.value('reason')), + ); + } + + final String reason; + + @override + JsonMap toJson() => { + 'error_type': errorType, + 'error_data': {'reason': reason}, + }; +} + +final class HealthcheckRpcErrorMessageEncodingFailed + extends HealthcheckRpcError { + const HealthcheckRpcErrorMessageEncodingFailed({required this.reason}) + : super(errorType: 'MessageEncodingFailed'); + + factory HealthcheckRpcErrorMessageEncodingFailed.fromJson(dynamic json) { + final map = _asJsonMap(json); + return HealthcheckRpcErrorMessageEncodingFailed( + reason: _stringFromJson(map.value('reason')), + ); + } + + final String reason; + + @override + JsonMap toJson() => { + 'error_type': errorType, + 'error_data': {'reason': reason}, + }; +} + +final class HealthcheckRpcErrorInternal extends HealthcheckRpcError { + const HealthcheckRpcErrorInternal({required this.reason}) + : super(errorType: 'Internal'); + + factory HealthcheckRpcErrorInternal.fromJson(dynamic json) { + final map = _asJsonMap(json); + return HealthcheckRpcErrorInternal( + reason: _stringFromJson(map.value('reason')), + ); + } + + final String reason; + + @override + JsonMap toJson() => { + 'error_type': errorType, + 'error_data': {'reason': reason}, + }; +} + +final class HealthcheckRpcErrorUnknown extends HealthcheckRpcError { + const HealthcheckRpcErrorUnknown({required this.data, this.rawErrorType}) + : super(errorType: 'unknown'); + + final String? rawErrorType; + final dynamic data; + + @override + JsonMap toJson() => { + 'error_type': rawErrorType ?? errorType, + 'error_data': data, + }; +} + +sealed class InitHwError { + const InitHwError({required this.errorType}); + + factory InitHwError.fromJson(dynamic json) { + final map = _asJsonMap(json); + final type = map.valueOrNull('error_type'); + final data = map.valueOrNull('error_data'); + switch (type) { + case 'HwContextInitializingAlready': + return InitHwErrorHwContextInitializingAlready.fromJson(); + case 'HwError': + return InitHwErrorHwError.fromJson(data); + case 'UnexpectedUserAction': + return InitHwErrorUnexpectedUserAction.fromJson(data); + case 'Timeout': + return InitHwErrorTimeout.fromJson(data); + case 'Internal': + return InitHwErrorInternal.fromJson(data); + default: + return InitHwErrorUnknown(rawErrorType: type, data: data); + } + } + + final String errorType; + + JsonMap toJson(); +} + +final class InitHwErrorHwContextInitializingAlready extends InitHwError { + const InitHwErrorHwContextInitializingAlready() + : super(errorType: 'HwContextInitializingAlready'); + + factory InitHwErrorHwContextInitializingAlready.fromJson() => + const InitHwErrorHwContextInitializingAlready(); + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': null}; +} + +final class InitHwErrorHwError extends InitHwError { + const InitHwErrorHwError(this.value) : super(errorType: 'HwError'); + + factory InitHwErrorHwError.fromJson(dynamic json) { + return InitHwErrorHwError(HwRpcError.fromJson(json)); + } + + final HwRpcError value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value.toJson()}; +} + +final class InitHwErrorUnexpectedUserAction extends InitHwError { + const InitHwErrorUnexpectedUserAction({required this.expected}) + : super(errorType: 'UnexpectedUserAction'); + + factory InitHwErrorUnexpectedUserAction.fromJson(dynamic json) { + final map = _asJsonMap(json); + return InitHwErrorUnexpectedUserAction( + expected: _stringFromJson(map.value('expected')), + ); + } + + final String expected; + + @override + JsonMap toJson() => { + 'error_type': errorType, + 'error_data': {'expected': expected}, + }; +} + +final class InitHwErrorTimeout extends InitHwError { + const InitHwErrorTimeout(this.value) : super(errorType: 'Timeout'); + + factory InitHwErrorTimeout.fromJson(dynamic json) { + return InitHwErrorTimeout(Mm2Duration.fromJson(json)); + } + + final Mm2Duration value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value.toJson()}; +} + +final class InitHwErrorInternal extends InitHwError { + const InitHwErrorInternal(this.value) : super(errorType: 'Internal'); + + factory InitHwErrorInternal.fromJson(dynamic json) { + return InitHwErrorInternal(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class InitHwErrorUnknown extends InitHwError { + const InitHwErrorUnknown({required this.data, this.rawErrorType}) + : super(errorType: 'unknown'); + + final String? rawErrorType; + final dynamic data; + + @override + JsonMap toJson() => { + 'error_type': rawErrorType ?? errorType, + 'error_data': data, + }; +} + +sealed class InitMetamaskError { + const InitMetamaskError({required this.errorType}); + + factory InitMetamaskError.fromJson(dynamic json) { + final map = _asJsonMap(json); + final type = map.valueOrNull('error_type'); + final data = map.valueOrNull('error_data'); + switch (type) { + case 'MetamaskInitializingAlready': + return InitMetamaskErrorMetamaskInitializingAlready.fromJson(); + case 'MetamaskError': + return InitMetamaskErrorMetamaskError.fromJson(data); + case 'Timeout': + return InitMetamaskErrorTimeout.fromJson(data); + case 'Internal': + return InitMetamaskErrorInternal.fromJson(data); + default: + return InitMetamaskErrorUnknown(rawErrorType: type, data: data); + } + } + + final String errorType; + + JsonMap toJson(); +} + +final class InitMetamaskErrorMetamaskInitializingAlready + extends InitMetamaskError { + const InitMetamaskErrorMetamaskInitializingAlready() + : super(errorType: 'MetamaskInitializingAlready'); + + factory InitMetamaskErrorMetamaskInitializingAlready.fromJson() => + const InitMetamaskErrorMetamaskInitializingAlready(); + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': null}; +} + +final class InitMetamaskErrorMetamaskError extends InitMetamaskError { + const InitMetamaskErrorMetamaskError(this.value) + : super(errorType: 'MetamaskError'); + + factory InitMetamaskErrorMetamaskError.fromJson(dynamic json) { + return InitMetamaskErrorMetamaskError(MetamaskRpcError.fromJson(json)); + } + + final MetamaskRpcError value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value.toJson()}; +} + +final class InitMetamaskErrorTimeout extends InitMetamaskError { + const InitMetamaskErrorTimeout(this.value) : super(errorType: 'Timeout'); + + factory InitMetamaskErrorTimeout.fromJson(dynamic json) { + return InitMetamaskErrorTimeout(Mm2Duration.fromJson(json)); + } + + final Mm2Duration value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value.toJson()}; +} + +final class InitMetamaskErrorInternal extends InitMetamaskError { + const InitMetamaskErrorInternal(this.value) : super(errorType: 'Internal'); + + factory InitMetamaskErrorInternal.fromJson(dynamic json) { + return InitMetamaskErrorInternal(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class InitMetamaskErrorUnknown extends InitMetamaskError { + const InitMetamaskErrorUnknown({required this.data, this.rawErrorType}) + : super(errorType: 'unknown'); + + final String? rawErrorType; + final dynamic data; + + @override + JsonMap toJson() => { + 'error_type': rawErrorType ?? errorType, + 'error_data': data, + }; +} + +sealed class MessageError { + const MessageError({required this.errorType}); + + factory MessageError.fromJson(dynamic json) { + final map = _asJsonMap(json); + final type = map.valueOrNull('error_type'); + final data = map.valueOrNull('error_data'); + switch (type) { + case 'TelegramError': + return MessageErrorTelegramError.fromJson(data); + default: + return MessageErrorUnknown(rawErrorType: type, data: data); + } + } + + final String errorType; + + JsonMap toJson(); +} + +final class MessageErrorTelegramError extends MessageError { + const MessageErrorTelegramError(this.value) + : super(errorType: 'TelegramError'); + + factory MessageErrorTelegramError.fromJson(dynamic json) { + return MessageErrorTelegramError(TelegramError.fromJson(json)); + } + + final TelegramError value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value.toJson()}; +} + +final class MessageErrorUnknown extends MessageError { + const MessageErrorUnknown({required this.data, this.rawErrorType}) + : super(errorType: 'unknown'); + + final String? rawErrorType; + final dynamic data; + + @override + JsonMap toJson() => { + 'error_type': rawErrorType ?? errorType, + 'error_data': data, + }; +} + +sealed class MmInitError { + const MmInitError({required this.errorType}); + + factory MmInitError.fromJson(dynamic json) { + final map = _asJsonMap(json); + final type = map.valueOrNull('error_type'); + final data = map.valueOrNull('error_data'); + switch (type) { + case 'Cancelled': + return MmInitErrorCancelled.fromJson(); + case 'Timeout': + return MmInitErrorTimeout.fromJson(data); + case 'ErrorDeserializingConfig': + return MmInitErrorErrorDeserializingConfig.fromJson(data); + case 'FieldNotFoundInConfig': + return MmInitErrorFieldNotFoundInConfig.fromJson(data); + case 'FieldWrongValueInConfig': + return MmInitErrorFieldWrongValueInConfig.fromJson(data); + case 'P2PError': + return MmInitErrorP2PError.fromJson(data); + case 'ErrorCreatingDbDir': + return MmInitErrorErrorCreatingDbDir.fromJson(data); + case 'DbDirectoryIsNotWritable': + return MmInitErrorDbDirectoryIsNotWritable.fromJson(data); + case 'DbFileIsNotWritable': + return MmInitErrorDbFileIsNotWritable.fromJson(data); + case 'ErrorSqliteInitializing': + return MmInitErrorErrorSqliteInitializing.fromJson(data); + case 'ErrorDbMigrating': + return MmInitErrorErrorDbMigrating.fromJson(data); + case 'SwapsKickStartError': + return MmInitErrorSwapsKickStartError.fromJson(data); + case 'OrdersKickStartError': + return MmInitErrorOrdersKickStartError.fromJson(data); + case 'WalletInitError': + return MmInitErrorWalletInitError.fromJson(data); + case 'EventStreamerInitFailed': + return MmInitErrorEventStreamerInitFailed.fromJson(data); + case 'HwError': + return MmInitErrorHwError.fromJson(data); + case 'Internal': + return MmInitErrorInternal.fromJson(data); + default: + return MmInitErrorUnknown(rawErrorType: type, data: data); + } + } + + final String errorType; + + JsonMap toJson(); +} + +final class MmInitErrorCancelled extends MmInitError { + const MmInitErrorCancelled() : super(errorType: 'Cancelled'); + + factory MmInitErrorCancelled.fromJson() => const MmInitErrorCancelled(); + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': null}; +} + +final class MmInitErrorTimeout extends MmInitError { + const MmInitErrorTimeout(this.value) : super(errorType: 'Timeout'); + + factory MmInitErrorTimeout.fromJson(dynamic json) { + return MmInitErrorTimeout(Mm2Duration.fromJson(json)); + } + + final Mm2Duration value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value.toJson()}; +} + +final class MmInitErrorErrorDeserializingConfig extends MmInitError { + const MmInitErrorErrorDeserializingConfig({ + required this.field, + required this.error, + }) : super(errorType: 'ErrorDeserializingConfig'); + + factory MmInitErrorErrorDeserializingConfig.fromJson(dynamic json) { + final map = _asJsonMap(json); + return MmInitErrorErrorDeserializingConfig( + field: _stringFromJson(map.value('field')), + error: _stringFromJson(map.value('error')), + ); + } + + final String field; + final String error; + + @override + JsonMap toJson() => { + 'error_type': errorType, + 'error_data': {'field': field, 'error': error}, + }; +} + +final class MmInitErrorFieldNotFoundInConfig extends MmInitError { + const MmInitErrorFieldNotFoundInConfig({required this.field}) + : super(errorType: 'FieldNotFoundInConfig'); + + factory MmInitErrorFieldNotFoundInConfig.fromJson(dynamic json) { + final map = _asJsonMap(json); + return MmInitErrorFieldNotFoundInConfig( + field: _stringFromJson(map.value('field')), + ); + } + + final String field; + + @override + JsonMap toJson() => { + 'error_type': errorType, + 'error_data': {'field': field}, + }; +} + +final class MmInitErrorFieldWrongValueInConfig extends MmInitError { + const MmInitErrorFieldWrongValueInConfig({ + required this.field, + required this.error, + }) : super(errorType: 'FieldWrongValueInConfig'); + + factory MmInitErrorFieldWrongValueInConfig.fromJson(dynamic json) { + final map = _asJsonMap(json); + return MmInitErrorFieldWrongValueInConfig( + field: _stringFromJson(map.value('field')), + error: _stringFromJson(map.value('error')), + ); + } + + final String field; + final String error; + + @override + JsonMap toJson() => { + 'error_type': errorType, + 'error_data': {'field': field, 'error': error}, + }; +} + +final class MmInitErrorP2PError extends MmInitError { + const MmInitErrorP2PError(this.value) : super(errorType: 'P2PError'); + + factory MmInitErrorP2PError.fromJson(dynamic json) { + return MmInitErrorP2PError(P2PInitError.fromJson(json)); + } + + final P2PInitError value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value.toJson()}; +} + +final class MmInitErrorErrorCreatingDbDir extends MmInitError { + const MmInitErrorErrorCreatingDbDir({ + required this.pathData, + required this.error, + }) : super(errorType: 'ErrorCreatingDbDir'); + + factory MmInitErrorErrorCreatingDbDir.fromJson(dynamic json) { + final map = _asJsonMap(json); + return MmInitErrorErrorCreatingDbDir( + pathData: PathBuf.fromJson(map.value('path')), + error: _stringFromJson(map.value('error')), + ); + } + + final PathBuf pathData; + final String error; + + @override + JsonMap toJson() => { + 'error_type': errorType, + 'error_data': {'path': pathData.toJson(), 'error': error}, + }; +} + +final class MmInitErrorDbDirectoryIsNotWritable extends MmInitError { + const MmInitErrorDbDirectoryIsNotWritable({required this.pathData}) + : super(errorType: 'DbDirectoryIsNotWritable'); + + factory MmInitErrorDbDirectoryIsNotWritable.fromJson(dynamic json) { + final map = _asJsonMap(json); + return MmInitErrorDbDirectoryIsNotWritable( + pathData: _stringFromJson(map.value('path')), + ); + } + + final String pathData; + + @override + JsonMap toJson() => { + 'error_type': errorType, + 'error_data': {'path': pathData}, + }; +} + +final class MmInitErrorDbFileIsNotWritable extends MmInitError { + const MmInitErrorDbFileIsNotWritable({required this.pathData}) + : super(errorType: 'DbFileIsNotWritable'); + + factory MmInitErrorDbFileIsNotWritable.fromJson(dynamic json) { + final map = _asJsonMap(json); + return MmInitErrorDbFileIsNotWritable( + pathData: _stringFromJson(map.value('path')), + ); + } + + final String pathData; + + @override + JsonMap toJson() => { + 'error_type': errorType, + 'error_data': {'path': pathData}, + }; +} + +final class MmInitErrorErrorSqliteInitializing extends MmInitError { + const MmInitErrorErrorSqliteInitializing(this.value) + : super(errorType: 'ErrorSqliteInitializing'); + + factory MmInitErrorErrorSqliteInitializing.fromJson(dynamic json) { + return MmInitErrorErrorSqliteInitializing(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class MmInitErrorErrorDbMigrating extends MmInitError { + const MmInitErrorErrorDbMigrating(this.value) + : super(errorType: 'ErrorDbMigrating'); + + factory MmInitErrorErrorDbMigrating.fromJson(dynamic json) { + return MmInitErrorErrorDbMigrating(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class MmInitErrorSwapsKickStartError extends MmInitError { + const MmInitErrorSwapsKickStartError(this.value) + : super(errorType: 'SwapsKickStartError'); + + factory MmInitErrorSwapsKickStartError.fromJson(dynamic json) { + return MmInitErrorSwapsKickStartError(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class MmInitErrorOrdersKickStartError extends MmInitError { + const MmInitErrorOrdersKickStartError(this.value) + : super(errorType: 'OrdersKickStartError'); + + factory MmInitErrorOrdersKickStartError.fromJson(dynamic json) { + return MmInitErrorOrdersKickStartError(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class MmInitErrorWalletInitError extends MmInitError { + const MmInitErrorWalletInitError(this.value) + : super(errorType: 'WalletInitError'); + + factory MmInitErrorWalletInitError.fromJson(dynamic json) { + return MmInitErrorWalletInitError(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class MmInitErrorEventStreamerInitFailed extends MmInitError { + const MmInitErrorEventStreamerInitFailed(this.value) + : super(errorType: 'EventStreamerInitFailed'); + + factory MmInitErrorEventStreamerInitFailed.fromJson(dynamic json) { + return MmInitErrorEventStreamerInitFailed(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class MmInitErrorHwError extends MmInitError { + const MmInitErrorHwError(this.value) : super(errorType: 'HwError'); + + factory MmInitErrorHwError.fromJson(dynamic json) { + return MmInitErrorHwError(HwRpcError.fromJson(json)); + } + + final HwRpcError value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value.toJson()}; +} + +final class MmInitErrorInternal extends MmInitError { + const MmInitErrorInternal(this.value) : super(errorType: 'Internal'); + + factory MmInitErrorInternal.fromJson(dynamic json) { + return MmInitErrorInternal(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class MmInitErrorUnknown extends MmInitError { + const MmInitErrorUnknown({required this.data, this.rawErrorType}) + : super(errorType: 'unknown'); + + final String? rawErrorType; + final dynamic data; + + @override + JsonMap toJson() => { + 'error_type': rawErrorType ?? errorType, + 'error_data': data, + }; +} + +sealed class CancelAllOrdersError { + const CancelAllOrdersError({required this.errorType}); + + factory CancelAllOrdersError.fromJson(dynamic json) { + final map = _asJsonMap(json); + final type = map.valueOrNull('error_type'); + final data = map.valueOrNull('error_data'); + switch (type) { + case 'LegacyError': + return CancelAllOrdersErrorLegacyError.fromJson(data); + default: + return CancelAllOrdersErrorUnknown(rawErrorType: type, data: data); + } + } + + final String errorType; + + JsonMap toJson(); +} + +final class CancelAllOrdersErrorLegacyError extends CancelAllOrdersError { + const CancelAllOrdersErrorLegacyError(this.value) + : super(errorType: 'LegacyError'); + + factory CancelAllOrdersErrorLegacyError.fromJson(dynamic json) { + return CancelAllOrdersErrorLegacyError(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class CancelAllOrdersErrorUnknown extends CancelAllOrdersError { + const CancelAllOrdersErrorUnknown({required this.data, this.rawErrorType}) + : super(errorType: 'unknown'); + + final String? rawErrorType; + final dynamic data; + + @override + JsonMap toJson() => { + 'error_type': rawErrorType ?? errorType, + 'error_data': data, + }; +} + +sealed class CancelOrderError { + const CancelOrderError({required this.errorType}); + + factory CancelOrderError.fromJson(dynamic json) { + final map = _asJsonMap(json); + final type = map.valueOrNull('error_type'); + final data = map.valueOrNull('error_data'); + switch (type) { + case 'CannotRetrieveOrderMatchContext': + return CancelOrderErrorCannotRetrieveOrderMatchContext.fromJson(); + case 'OrderBeingMatched': + return CancelOrderErrorOrderBeingMatched.fromJson(data); + case 'UUIDNotFound': + return CancelOrderErrorUUIDNotFound.fromJson(data); + default: + return CancelOrderErrorUnknown(rawErrorType: type, data: data); + } + } + + final String errorType; + + JsonMap toJson(); +} + +final class CancelOrderErrorCannotRetrieveOrderMatchContext + extends CancelOrderError { + const CancelOrderErrorCannotRetrieveOrderMatchContext() + : super(errorType: 'CannotRetrieveOrderMatchContext'); + + factory CancelOrderErrorCannotRetrieveOrderMatchContext.fromJson() => + const CancelOrderErrorCannotRetrieveOrderMatchContext(); + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': null}; +} + +final class CancelOrderErrorOrderBeingMatched extends CancelOrderError { + const CancelOrderErrorOrderBeingMatched({required this.uuid}) + : super(errorType: 'OrderBeingMatched'); + + factory CancelOrderErrorOrderBeingMatched.fromJson(dynamic json) { + final map = _asJsonMap(json); + return CancelOrderErrorOrderBeingMatched( + uuid: _stringFromJson(map.value('uuid')), + ); + } + + final String uuid; + + @override + JsonMap toJson() => { + 'error_type': errorType, + 'error_data': {'uuid': uuid}, + }; +} + +final class CancelOrderErrorUUIDNotFound extends CancelOrderError { + const CancelOrderErrorUUIDNotFound({required this.uuid}) + : super(errorType: 'UUIDNotFound'); + + factory CancelOrderErrorUUIDNotFound.fromJson(dynamic json) { + final map = _asJsonMap(json); + return CancelOrderErrorUUIDNotFound( + uuid: _stringFromJson(map.value('uuid')), + ); + } + + final String uuid; + + @override + JsonMap toJson() => { + 'error_type': errorType, + 'error_data': {'uuid': uuid}, + }; +} + +final class CancelOrderErrorUnknown extends CancelOrderError { + const CancelOrderErrorUnknown({required this.data, this.rawErrorType}) + : super(errorType: 'unknown'); + + final String? rawErrorType; + final dynamic data; + + @override + JsonMap toJson() => { + 'error_type': rawErrorType ?? errorType, + 'error_data': data, + }; +} + +sealed class BestOrdersRpcError { + const BestOrdersRpcError({required this.errorType}); + + factory BestOrdersRpcError.fromJson(dynamic json) { + final map = _asJsonMap(json); + final type = map.valueOrNull('error_type'); + final data = map.valueOrNull('error_data'); + switch (type) { + case 'CoinIsWalletOnly': + return BestOrdersRpcErrorCoinIsWalletOnly.fromJson(data); + case 'P2PError': + return BestOrdersRpcErrorP2PError.fromJson(data); + case 'CtxError': + return BestOrdersRpcErrorCtxError.fromJson(data); + default: + return BestOrdersRpcErrorUnknown(rawErrorType: type, data: data); + } + } + + final String errorType; + + JsonMap toJson(); +} + +final class BestOrdersRpcErrorCoinIsWalletOnly extends BestOrdersRpcError { + const BestOrdersRpcErrorCoinIsWalletOnly(this.value) + : super(errorType: 'CoinIsWalletOnly'); + + factory BestOrdersRpcErrorCoinIsWalletOnly.fromJson(dynamic json) { + return BestOrdersRpcErrorCoinIsWalletOnly(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class BestOrdersRpcErrorP2PError extends BestOrdersRpcError { + const BestOrdersRpcErrorP2PError(this.value) : super(errorType: 'P2PError'); + + factory BestOrdersRpcErrorP2PError.fromJson(dynamic json) { + return BestOrdersRpcErrorP2PError(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class BestOrdersRpcErrorCtxError extends BestOrdersRpcError { + const BestOrdersRpcErrorCtxError(this.value) : super(errorType: 'CtxError'); + + factory BestOrdersRpcErrorCtxError.fromJson(dynamic json) { + return BestOrdersRpcErrorCtxError(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class BestOrdersRpcErrorUnknown extends BestOrdersRpcError { + const BestOrdersRpcErrorUnknown({required this.data, this.rawErrorType}) + : super(errorType: 'unknown'); + + final String? rawErrorType; + final dynamic data; + + @override + JsonMap toJson() => { + 'error_type': rawErrorType ?? errorType, + 'error_data': data, + }; +} + +sealed class OrderbookRpcError { + const OrderbookRpcError({required this.errorType}); + + factory OrderbookRpcError.fromJson(dynamic json) { + final map = _asJsonMap(json); + final type = map.valueOrNull('error_type'); + final data = map.valueOrNull('error_data'); + switch (type) { + case 'BaseRelSame': + return OrderbookRpcErrorBaseRelSame.fromJson(); + case 'BaseRelSameOrderbookTickersAndProtocols': + return OrderbookRpcErrorBaseRelSameOrderbookTickersAndProtocols.fromJson(); + case 'CoinConfigNotFound': + return OrderbookRpcErrorCoinConfigNotFound.fromJson(data); + case 'CoinIsWalletOnly': + return OrderbookRpcErrorCoinIsWalletOnly.fromJson(data); + case 'P2PSubscribeError': + return OrderbookRpcErrorP2PSubscribeError.fromJson(data); + case 'Internal': + return OrderbookRpcErrorInternal.fromJson(data); + default: + return OrderbookRpcErrorUnknown(rawErrorType: type, data: data); + } + } + + final String errorType; + + JsonMap toJson(); +} + +final class OrderbookRpcErrorBaseRelSame extends OrderbookRpcError { + const OrderbookRpcErrorBaseRelSame() : super(errorType: 'BaseRelSame'); + + factory OrderbookRpcErrorBaseRelSame.fromJson() => + const OrderbookRpcErrorBaseRelSame(); + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': null}; +} + +final class OrderbookRpcErrorBaseRelSameOrderbookTickersAndProtocols + extends OrderbookRpcError { + const OrderbookRpcErrorBaseRelSameOrderbookTickersAndProtocols() + : super(errorType: 'BaseRelSameOrderbookTickersAndProtocols'); + + factory OrderbookRpcErrorBaseRelSameOrderbookTickersAndProtocols.fromJson() => + const OrderbookRpcErrorBaseRelSameOrderbookTickersAndProtocols(); + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': null}; +} + +final class OrderbookRpcErrorCoinConfigNotFound extends OrderbookRpcError { + const OrderbookRpcErrorCoinConfigNotFound(this.value) + : super(errorType: 'CoinConfigNotFound'); + + factory OrderbookRpcErrorCoinConfigNotFound.fromJson(dynamic json) { + return OrderbookRpcErrorCoinConfigNotFound(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class OrderbookRpcErrorCoinIsWalletOnly extends OrderbookRpcError { + const OrderbookRpcErrorCoinIsWalletOnly(this.value) + : super(errorType: 'CoinIsWalletOnly'); + + factory OrderbookRpcErrorCoinIsWalletOnly.fromJson(dynamic json) { + return OrderbookRpcErrorCoinIsWalletOnly(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class OrderbookRpcErrorP2PSubscribeError extends OrderbookRpcError { + const OrderbookRpcErrorP2PSubscribeError(this.value) + : super(errorType: 'P2PSubscribeError'); + + factory OrderbookRpcErrorP2PSubscribeError.fromJson(dynamic json) { + return OrderbookRpcErrorP2PSubscribeError(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class OrderbookRpcErrorInternal extends OrderbookRpcError { + const OrderbookRpcErrorInternal(this.value) : super(errorType: 'Internal'); + + factory OrderbookRpcErrorInternal.fromJson(dynamic json) { + return OrderbookRpcErrorInternal(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class OrderbookRpcErrorUnknown extends OrderbookRpcError { + const OrderbookRpcErrorUnknown({required this.data, this.rawErrorType}) + : super(errorType: 'unknown'); + + final String? rawErrorType; + final dynamic data; + + @override + JsonMap toJson() => { + 'error_type': rawErrorType ?? errorType, + 'error_data': data, + }; +} + +sealed class OrderProcessingError { + const OrderProcessingError({required this.errorType}); + + factory OrderProcessingError.fromJson(dynamic json) { + final map = _asJsonMap(json); + final type = map.valueOrNull('error_type'); + final data = map.valueOrNull('error_data'); + switch (type) { + case 'ProviderUnknown': + return OrderProcessingErrorProviderUnknown.fromJson(data); + case 'PriceIsZero': + return OrderProcessingErrorPriceIsZero.fromJson(data); + case 'LastUpdatedTimestampInvalid': + return OrderProcessingErrorLastUpdatedTimestampInvalid.fromJson(data); + case 'PriceElapsedValidityExpired': + return OrderProcessingErrorPriceElapsedValidityExpired.fromJson(data); + case 'PriceElapsedValidityUntreatable': + return OrderProcessingErrorPriceElapsedValidityUntreatable.fromJson( + data, + ); + case 'PriceBelowMinBasePrice': + return OrderProcessingErrorPriceBelowMinBasePrice.fromJson(data); + case 'PriceBelowMinRelPrice': + return OrderProcessingErrorPriceBelowMinRelPrice.fromJson(data); + case 'PriceBelowPairPrice': + return OrderProcessingErrorPriceBelowPairPrice.fromJson(data); + case 'AssetNotEnabled': + return OrderProcessingErrorAssetNotEnabled.fromJson(); + case 'InternalCoinFindError': + return OrderProcessingErrorInternalCoinFindError.fromJson(); + case 'BalanceInternalError': + return OrderProcessingErrorBalanceInternalError.fromJson(); + case 'BalanceIsZero': + return OrderProcessingErrorBalanceIsZero.fromJson(); + case 'OrderCreationError': + return OrderProcessingErrorOrderCreationError.fromJson(data); + case 'OrderUpdateError': + return OrderProcessingErrorOrderUpdateError.fromJson(data); + case 'MyRecentSwapsError': + return OrderProcessingErrorMyRecentSwapsError.fromJson(data); + case 'MinVolUsdAboveBalanceUsd': + return OrderProcessingErrorMinVolUsdAboveBalanceUsd.fromJson(); + case 'LegacyError': + return OrderProcessingErrorLegacyError.fromJson(data); + default: + return OrderProcessingErrorUnknown(rawErrorType: type, data: data); + } + } + + final String errorType; + + JsonMap toJson(); +} + +final class OrderProcessingErrorProviderUnknown extends OrderProcessingError { + const OrderProcessingErrorProviderUnknown({required this.keyTradePair}) + : super(errorType: 'ProviderUnknown'); + + factory OrderProcessingErrorProviderUnknown.fromJson(dynamic json) { + final map = _asJsonMap(json); + return OrderProcessingErrorProviderUnknown( + keyTradePair: _stringFromJson(map.value('key_trade_pair')), + ); + } + + final String keyTradePair; + + @override + JsonMap toJson() => { + 'error_type': errorType, + 'error_data': {'key_trade_pair': keyTradePair}, + }; +} + +final class OrderProcessingErrorPriceIsZero extends OrderProcessingError { + const OrderProcessingErrorPriceIsZero({required this.keyTradePair}) + : super(errorType: 'PriceIsZero'); + + factory OrderProcessingErrorPriceIsZero.fromJson(dynamic json) { + final map = _asJsonMap(json); + return OrderProcessingErrorPriceIsZero( + keyTradePair: _stringFromJson(map.value('key_trade_pair')), + ); + } + + final String keyTradePair; + + @override + JsonMap toJson() => { + 'error_type': errorType, + 'error_data': {'key_trade_pair': keyTradePair}, + }; +} + +final class OrderProcessingErrorLastUpdatedTimestampInvalid + extends OrderProcessingError { + const OrderProcessingErrorLastUpdatedTimestampInvalid({ + required this.keyTradePair, + }) : super(errorType: 'LastUpdatedTimestampInvalid'); + + factory OrderProcessingErrorLastUpdatedTimestampInvalid.fromJson( + dynamic json, + ) { + final map = _asJsonMap(json); + return OrderProcessingErrorLastUpdatedTimestampInvalid( + keyTradePair: _stringFromJson(map.value('key_trade_pair')), + ); + } + + final String keyTradePair; + + @override + JsonMap toJson() => { + 'error_type': errorType, + 'error_data': {'key_trade_pair': keyTradePair}, + }; +} + +final class OrderProcessingErrorPriceElapsedValidityExpired + extends OrderProcessingError { + const OrderProcessingErrorPriceElapsedValidityExpired({ + required this.elapsed, + required this.elapsedValidity, + required this.keyTradePair, + }) : super(errorType: 'PriceElapsedValidityExpired'); + + factory OrderProcessingErrorPriceElapsedValidityExpired.fromJson( + dynamic json, + ) { + final map = _asJsonMap(json); + return OrderProcessingErrorPriceElapsedValidityExpired( + elapsed: _doubleFromJson(map.value('elapsed')), + elapsedValidity: _doubleFromJson(map.value('elapsed_validity')), + keyTradePair: _stringFromJson(map.value('key_trade_pair')), + ); + } + + final double elapsed; + final double elapsedValidity; + final String keyTradePair; + + @override + JsonMap toJson() => { + 'error_type': errorType, + 'error_data': { + 'elapsed': elapsed, + 'elapsed_validity': elapsedValidity, + 'key_trade_pair': keyTradePair, + }, + }; +} + +final class OrderProcessingErrorPriceElapsedValidityUntreatable + extends OrderProcessingError { + const OrderProcessingErrorPriceElapsedValidityUntreatable(this.value) + : super(errorType: 'PriceElapsedValidityUntreatable'); + + factory OrderProcessingErrorPriceElapsedValidityUntreatable.fromJson( + dynamic json, + ) { + return OrderProcessingErrorPriceElapsedValidityUntreatable( + _stringFromJson(json), + ); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class OrderProcessingErrorPriceBelowMinBasePrice + extends OrderProcessingError { + const OrderProcessingErrorPriceBelowMinBasePrice({ + required this.basePrice, + required this.minBasePrice, + }) : super(errorType: 'PriceBelowMinBasePrice'); + + factory OrderProcessingErrorPriceBelowMinBasePrice.fromJson(dynamic json) { + final map = _asJsonMap(json); + return OrderProcessingErrorPriceBelowMinBasePrice( + basePrice: _stringFromJson(map.value('base_price')), + minBasePrice: _stringFromJson(map.value('min_base_price')), + ); + } + + final String basePrice; + final String minBasePrice; + + @override + JsonMap toJson() => { + 'error_type': errorType, + 'error_data': {'base_price': basePrice, 'min_base_price': minBasePrice}, + }; +} + +final class OrderProcessingErrorPriceBelowMinRelPrice + extends OrderProcessingError { + const OrderProcessingErrorPriceBelowMinRelPrice({ + required this.relPrice, + required this.minRelPrice, + }) : super(errorType: 'PriceBelowMinRelPrice'); + + factory OrderProcessingErrorPriceBelowMinRelPrice.fromJson(dynamic json) { + final map = _asJsonMap(json); + return OrderProcessingErrorPriceBelowMinRelPrice( + relPrice: _stringFromJson(map.value('rel_price')), + minRelPrice: _stringFromJson(map.value('min_rel_price')), + ); + } + + final String relPrice; + final String minRelPrice; + + @override + JsonMap toJson() => { + 'error_type': errorType, + 'error_data': {'rel_price': relPrice, 'min_rel_price': minRelPrice}, + }; +} + +final class OrderProcessingErrorPriceBelowPairPrice + extends OrderProcessingError { + const OrderProcessingErrorPriceBelowPairPrice({ + required this.pair, + required this.pairPrice, + required this.minPairPrice, + }) : super(errorType: 'PriceBelowPairPrice'); + + factory OrderProcessingErrorPriceBelowPairPrice.fromJson(dynamic json) { + final map = _asJsonMap(json); + return OrderProcessingErrorPriceBelowPairPrice( + pair: _stringFromJson(map.value('pair')), + pairPrice: _stringFromJson(map.value('pair_price')), + minPairPrice: _stringFromJson(map.value('min_pair_price')), + ); + } + + final String pair; + final String pairPrice; + final String minPairPrice; + + @override + JsonMap toJson() => { + 'error_type': errorType, + 'error_data': { + 'pair': pair, + 'pair_price': pairPrice, + 'min_pair_price': minPairPrice, + }, + }; +} + +final class OrderProcessingErrorAssetNotEnabled extends OrderProcessingError { + const OrderProcessingErrorAssetNotEnabled() + : super(errorType: 'AssetNotEnabled'); + + factory OrderProcessingErrorAssetNotEnabled.fromJson() => + const OrderProcessingErrorAssetNotEnabled(); + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': null}; +} + +final class OrderProcessingErrorInternalCoinFindError + extends OrderProcessingError { + const OrderProcessingErrorInternalCoinFindError() + : super(errorType: 'InternalCoinFindError'); + + factory OrderProcessingErrorInternalCoinFindError.fromJson() => + const OrderProcessingErrorInternalCoinFindError(); + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': null}; +} + +final class OrderProcessingErrorBalanceInternalError + extends OrderProcessingError { + const OrderProcessingErrorBalanceInternalError() + : super(errorType: 'BalanceInternalError'); + + factory OrderProcessingErrorBalanceInternalError.fromJson() => + const OrderProcessingErrorBalanceInternalError(); + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': null}; +} + +final class OrderProcessingErrorBalanceIsZero extends OrderProcessingError { + const OrderProcessingErrorBalanceIsZero() : super(errorType: 'BalanceIsZero'); + + factory OrderProcessingErrorBalanceIsZero.fromJson() => + const OrderProcessingErrorBalanceIsZero(); + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': null}; +} + +final class OrderProcessingErrorOrderCreationError + extends OrderProcessingError { + const OrderProcessingErrorOrderCreationError(this.value) + : super(errorType: 'OrderCreationError'); + + factory OrderProcessingErrorOrderCreationError.fromJson(dynamic json) { + return OrderProcessingErrorOrderCreationError(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class OrderProcessingErrorOrderUpdateError extends OrderProcessingError { + const OrderProcessingErrorOrderUpdateError(this.value) + : super(errorType: 'OrderUpdateError'); + + factory OrderProcessingErrorOrderUpdateError.fromJson(dynamic json) { + return OrderProcessingErrorOrderUpdateError(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class OrderProcessingErrorMyRecentSwapsError + extends OrderProcessingError { + const OrderProcessingErrorMyRecentSwapsError(this.value) + : super(errorType: 'MyRecentSwapsError'); + + factory OrderProcessingErrorMyRecentSwapsError.fromJson(dynamic json) { + return OrderProcessingErrorMyRecentSwapsError(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class OrderProcessingErrorMinVolUsdAboveBalanceUsd + extends OrderProcessingError { + const OrderProcessingErrorMinVolUsdAboveBalanceUsd() + : super(errorType: 'MinVolUsdAboveBalanceUsd'); + + factory OrderProcessingErrorMinVolUsdAboveBalanceUsd.fromJson() => + const OrderProcessingErrorMinVolUsdAboveBalanceUsd(); + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': null}; +} + +final class OrderProcessingErrorLegacyError extends OrderProcessingError { + const OrderProcessingErrorLegacyError(this.value) + : super(errorType: 'LegacyError'); + + factory OrderProcessingErrorLegacyError.fromJson(dynamic json) { + return OrderProcessingErrorLegacyError(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class OrderProcessingErrorUnknown extends OrderProcessingError { + const OrderProcessingErrorUnknown({required this.data, this.rawErrorType}) + : super(errorType: 'unknown'); + + final String? rawErrorType; + final dynamic data; + + @override + JsonMap toJson() => { + 'error_type': rawErrorType ?? errorType, + 'error_data': data, + }; +} + +sealed class StartSimpleMakerBotError { + const StartSimpleMakerBotError({required this.errorType}); + + factory StartSimpleMakerBotError.fromJson(dynamic json) { + final map = _asJsonMap(json); + final type = map.valueOrNull('error_type'); + final data = map.valueOrNull('error_data'); + switch (type) { + case 'AlreadyStarted': + return StartSimpleMakerBotErrorAlreadyStarted.fromJson(); + case 'InvalidBotConfiguration': + return StartSimpleMakerBotErrorInvalidBotConfiguration.fromJson(); + case 'Transport': + return StartSimpleMakerBotErrorTransport.fromJson(data); + case 'CannotStartFromStopping': + return StartSimpleMakerBotErrorCannotStartFromStopping.fromJson(); + case 'InternalError': + return StartSimpleMakerBotErrorInternalError.fromJson(data); + default: + return StartSimpleMakerBotErrorUnknown(rawErrorType: type, data: data); + } + } + + final String errorType; + + JsonMap toJson(); +} + +final class StartSimpleMakerBotErrorAlreadyStarted + extends StartSimpleMakerBotError { + const StartSimpleMakerBotErrorAlreadyStarted() + : super(errorType: 'AlreadyStarted'); + + factory StartSimpleMakerBotErrorAlreadyStarted.fromJson() => + const StartSimpleMakerBotErrorAlreadyStarted(); + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': null}; +} + +final class StartSimpleMakerBotErrorInvalidBotConfiguration + extends StartSimpleMakerBotError { + const StartSimpleMakerBotErrorInvalidBotConfiguration() + : super(errorType: 'InvalidBotConfiguration'); + + factory StartSimpleMakerBotErrorInvalidBotConfiguration.fromJson() => + const StartSimpleMakerBotErrorInvalidBotConfiguration(); + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': null}; +} + +final class StartSimpleMakerBotErrorTransport extends StartSimpleMakerBotError { + const StartSimpleMakerBotErrorTransport(this.value) + : super(errorType: 'Transport'); + + factory StartSimpleMakerBotErrorTransport.fromJson(dynamic json) { + return StartSimpleMakerBotErrorTransport(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class StartSimpleMakerBotErrorCannotStartFromStopping + extends StartSimpleMakerBotError { + const StartSimpleMakerBotErrorCannotStartFromStopping() + : super(errorType: 'CannotStartFromStopping'); + + factory StartSimpleMakerBotErrorCannotStartFromStopping.fromJson() => + const StartSimpleMakerBotErrorCannotStartFromStopping(); + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': null}; +} + +final class StartSimpleMakerBotErrorInternalError + extends StartSimpleMakerBotError { + const StartSimpleMakerBotErrorInternalError(this.value) + : super(errorType: 'InternalError'); + + factory StartSimpleMakerBotErrorInternalError.fromJson(dynamic json) { + return StartSimpleMakerBotErrorInternalError(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class StartSimpleMakerBotErrorUnknown extends StartSimpleMakerBotError { + const StartSimpleMakerBotErrorUnknown({required this.data, this.rawErrorType}) + : super(errorType: 'unknown'); + + final String? rawErrorType; + final dynamic data; + + @override + JsonMap toJson() => { + 'error_type': rawErrorType ?? errorType, + 'error_data': data, + }; +} + +sealed class StopSimpleMakerBotError { + const StopSimpleMakerBotError({required this.errorType}); + + factory StopSimpleMakerBotError.fromJson(dynamic json) { + final map = _asJsonMap(json); + final type = map.valueOrNull('error_type'); + final data = map.valueOrNull('error_data'); + switch (type) { + case 'AlreadyStopped': + return StopSimpleMakerBotErrorAlreadyStopped.fromJson(); + case 'AlreadyStopping': + return StopSimpleMakerBotErrorAlreadyStopping.fromJson(); + case 'Transport': + return StopSimpleMakerBotErrorTransport.fromJson(data); + case 'InternalError': + return StopSimpleMakerBotErrorInternalError.fromJson(data); + default: + return StopSimpleMakerBotErrorUnknown(rawErrorType: type, data: data); + } + } + + final String errorType; + + JsonMap toJson(); +} + +final class StopSimpleMakerBotErrorAlreadyStopped + extends StopSimpleMakerBotError { + const StopSimpleMakerBotErrorAlreadyStopped() + : super(errorType: 'AlreadyStopped'); + + factory StopSimpleMakerBotErrorAlreadyStopped.fromJson() => + const StopSimpleMakerBotErrorAlreadyStopped(); + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': null}; +} + +final class StopSimpleMakerBotErrorAlreadyStopping + extends StopSimpleMakerBotError { + const StopSimpleMakerBotErrorAlreadyStopping() + : super(errorType: 'AlreadyStopping'); + + factory StopSimpleMakerBotErrorAlreadyStopping.fromJson() => + const StopSimpleMakerBotErrorAlreadyStopping(); + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': null}; +} + +final class StopSimpleMakerBotErrorTransport extends StopSimpleMakerBotError { + const StopSimpleMakerBotErrorTransport(this.value) + : super(errorType: 'Transport'); + + factory StopSimpleMakerBotErrorTransport.fromJson(dynamic json) { + return StopSimpleMakerBotErrorTransport(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class StopSimpleMakerBotErrorInternalError + extends StopSimpleMakerBotError { + const StopSimpleMakerBotErrorInternalError(this.value) + : super(errorType: 'InternalError'); + + factory StopSimpleMakerBotErrorInternalError.fromJson(dynamic json) { + return StopSimpleMakerBotErrorInternalError(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class StopSimpleMakerBotErrorUnknown extends StopSimpleMakerBotError { + const StopSimpleMakerBotErrorUnknown({required this.data, this.rawErrorType}) + : super(errorType: 'unknown'); + + final String? rawErrorType; + final dynamic data; + + @override + JsonMap toJson() => { + 'error_type': rawErrorType ?? errorType, + 'error_data': data, + }; +} + +sealed class SwapUpdateNotificationError { + const SwapUpdateNotificationError({required this.errorType}); + + factory SwapUpdateNotificationError.fromJson(dynamic json) { + final map = _asJsonMap(json); + final type = map.valueOrNull('error_type'); + final data = map.valueOrNull('error_data'); + switch (type) { + case 'MyRecentSwapsError': + return SwapUpdateNotificationErrorMyRecentSwapsError.fromJson(data); + case 'SwapInfoNotAvailable': + return SwapUpdateNotificationErrorSwapInfoNotAvailable.fromJson(); + default: + return SwapUpdateNotificationErrorUnknown( + rawErrorType: type, + data: data, + ); + } + } + + final String errorType; + + JsonMap toJson(); +} + +final class SwapUpdateNotificationErrorMyRecentSwapsError + extends SwapUpdateNotificationError { + const SwapUpdateNotificationErrorMyRecentSwapsError(this.value) + : super(errorType: 'MyRecentSwapsError'); + + factory SwapUpdateNotificationErrorMyRecentSwapsError.fromJson(dynamic json) { + return SwapUpdateNotificationErrorMyRecentSwapsError( + LatestSwapsErr.fromJson(json), + ); + } + + final LatestSwapsErr value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value.toJson()}; +} + +final class SwapUpdateNotificationErrorSwapInfoNotAvailable + extends SwapUpdateNotificationError { + const SwapUpdateNotificationErrorSwapInfoNotAvailable() + : super(errorType: 'SwapInfoNotAvailable'); + + factory SwapUpdateNotificationErrorSwapInfoNotAvailable.fromJson() => + const SwapUpdateNotificationErrorSwapInfoNotAvailable(); + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': null}; +} + +final class SwapUpdateNotificationErrorUnknown + extends SwapUpdateNotificationError { + const SwapUpdateNotificationErrorUnknown({ + required this.data, + this.rawErrorType, + }) : super(errorType: 'unknown'); + + final String? rawErrorType; + final dynamic data; + + @override + JsonMap toJson() => { + 'error_type': rawErrorType ?? errorType, + 'error_data': data, + }; +} + +sealed class NodeVersionError { + const NodeVersionError({required this.errorType}); + + factory NodeVersionError.fromJson(dynamic json) { + final map = _asJsonMap(json); + final type = map.valueOrNull('error_type'); + final data = map.valueOrNull('error_data'); + switch (type) { + case 'InvalidRequest': + return NodeVersionErrorInvalidRequest.fromJson(data); + case 'DatabaseError': + return NodeVersionErrorDatabaseError.fromJson(data); + case 'InvalidAddress': + return NodeVersionErrorInvalidAddress.fromJson(data); + case 'PeerIdParseError': + return NodeVersionErrorPeerIdParseError.fromJson(data); + case 'UnsupportedMode': + return NodeVersionErrorUnsupportedMode.fromJson(data); + case 'AlreadyRunning': + return NodeVersionErrorAlreadyRunning.fromJson(); + case 'CurrentlyStopping': + return NodeVersionErrorCurrentlyStopping.fromJson(); + case 'NotRunning': + return NodeVersionErrorNotRunning.fromJson(); + default: + return NodeVersionErrorUnknown(rawErrorType: type, data: data); + } + } + + final String errorType; + + JsonMap toJson(); +} + +final class NodeVersionErrorInvalidRequest extends NodeVersionError { + const NodeVersionErrorInvalidRequest(this.value) + : super(errorType: 'InvalidRequest'); + + factory NodeVersionErrorInvalidRequest.fromJson(dynamic json) { + return NodeVersionErrorInvalidRequest(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class NodeVersionErrorDatabaseError extends NodeVersionError { + const NodeVersionErrorDatabaseError(this.value) + : super(errorType: 'DatabaseError'); + + factory NodeVersionErrorDatabaseError.fromJson(dynamic json) { + return NodeVersionErrorDatabaseError(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class NodeVersionErrorInvalidAddress extends NodeVersionError { + const NodeVersionErrorInvalidAddress(this.value) + : super(errorType: 'InvalidAddress'); + + factory NodeVersionErrorInvalidAddress.fromJson(dynamic json) { + return NodeVersionErrorInvalidAddress(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class NodeVersionErrorPeerIdParseError extends NodeVersionError { + const NodeVersionErrorPeerIdParseError(this.value0, this.value1) + : super(errorType: 'PeerIdParseError'); + + factory NodeVersionErrorPeerIdParseError.fromJson(dynamic json) { + final list = _asJsonList(json); + return NodeVersionErrorPeerIdParseError( + _stringFromJson(list[0]), + _stringFromJson(list[1]), + ); + } + + final String value0; + final String value1; + + @override + JsonMap toJson() => { + 'error_type': errorType, + 'error_data': [value0, value1], + }; +} + +final class NodeVersionErrorUnsupportedMode extends NodeVersionError { + const NodeVersionErrorUnsupportedMode(this.value) + : super(errorType: 'UnsupportedMode'); + + factory NodeVersionErrorUnsupportedMode.fromJson(dynamic json) { + return NodeVersionErrorUnsupportedMode(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class NodeVersionErrorAlreadyRunning extends NodeVersionError { + const NodeVersionErrorAlreadyRunning() : super(errorType: 'AlreadyRunning'); + + factory NodeVersionErrorAlreadyRunning.fromJson() => + const NodeVersionErrorAlreadyRunning(); + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': null}; +} + +final class NodeVersionErrorCurrentlyStopping extends NodeVersionError { + const NodeVersionErrorCurrentlyStopping() + : super(errorType: 'CurrentlyStopping'); + + factory NodeVersionErrorCurrentlyStopping.fromJson() => + const NodeVersionErrorCurrentlyStopping(); + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': null}; +} + +final class NodeVersionErrorNotRunning extends NodeVersionError { + const NodeVersionErrorNotRunning() : super(errorType: 'NotRunning'); + + factory NodeVersionErrorNotRunning.fromJson() => + const NodeVersionErrorNotRunning(); + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': null}; +} + +final class NodeVersionErrorUnknown extends NodeVersionError { + const NodeVersionErrorUnknown({required this.data, this.rawErrorType}) + : super(errorType: 'unknown'); + + final String? rawErrorType; + final dynamic data; + + @override + JsonMap toJson() => { + 'error_type': rawErrorType ?? errorType, + 'error_data': data, + }; +} + +sealed class GetLockedAmountRpcError { + const GetLockedAmountRpcError({required this.errorType}); + + factory GetLockedAmountRpcError.fromJson(dynamic json) { + final map = _asJsonMap(json); + final type = map.valueOrNull('error_type'); + final data = map.valueOrNull('error_data'); + switch (type) { + case 'NoSuchCoin': + return GetLockedAmountRpcErrorNoSuchCoin.fromJson(data); + default: + return GetLockedAmountRpcErrorUnknown(rawErrorType: type, data: data); + } + } + + final String errorType; + + JsonMap toJson(); +} + +final class GetLockedAmountRpcErrorNoSuchCoin extends GetLockedAmountRpcError { + const GetLockedAmountRpcErrorNoSuchCoin({required this.coin}) + : super(errorType: 'NoSuchCoin'); + + factory GetLockedAmountRpcErrorNoSuchCoin.fromJson(dynamic json) { + final map = _asJsonMap(json); + return GetLockedAmountRpcErrorNoSuchCoin( + coin: _stringFromJson(map.value('coin')), + ); + } + + final String coin; + + @override + JsonMap toJson() => { + 'error_type': errorType, + 'error_data': {'coin': coin}, + }; +} + +final class GetLockedAmountRpcErrorUnknown extends GetLockedAmountRpcError { + const GetLockedAmountRpcErrorUnknown({required this.data, this.rawErrorType}) + : super(errorType: 'unknown'); + + final String? rawErrorType; + final dynamic data; + + @override + JsonMap toJson() => { + 'error_type': rawErrorType ?? errorType, + 'error_data': data, + }; +} + +sealed class LatestSwapsErr { + const LatestSwapsErr({required this.errorType}); + + factory LatestSwapsErr.fromJson(dynamic json) { + final map = _asJsonMap(json); + final type = map.valueOrNull('error_type'); + final data = map.valueOrNull('error_data'); + switch (type) { + case 'UUIDNotPresentInDb': + return LatestSwapsErrUUIDNotPresentInDb.fromJson(data); + case 'UnableToLoadSavedSwaps': + return LatestSwapsErrUnableToLoadSavedSwaps.fromJson(data); + case 'UnableToQuerySwapStorage': + return LatestSwapsErrUnableToQuerySwapStorage.fromJson(); + default: + return LatestSwapsErrUnknown(rawErrorType: type, data: data); + } + } + + final String errorType; + + JsonMap toJson(); +} + +final class LatestSwapsErrUUIDNotPresentInDb extends LatestSwapsErr { + const LatestSwapsErrUUIDNotPresentInDb(this.value) + : super(errorType: 'UUIDNotPresentInDb'); + + factory LatestSwapsErrUUIDNotPresentInDb.fromJson(dynamic json) { + return LatestSwapsErrUUIDNotPresentInDb(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class LatestSwapsErrUnableToLoadSavedSwaps extends LatestSwapsErr { + const LatestSwapsErrUnableToLoadSavedSwaps(this.value) + : super(errorType: 'UnableToLoadSavedSwaps'); + + factory LatestSwapsErrUnableToLoadSavedSwaps.fromJson(dynamic json) { + return LatestSwapsErrUnableToLoadSavedSwaps(SavedSwapError.fromJson(json)); + } + + final SavedSwapError value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value.toJson()}; +} + +final class LatestSwapsErrUnableToQuerySwapStorage extends LatestSwapsErr { + const LatestSwapsErrUnableToQuerySwapStorage() + : super(errorType: 'UnableToQuerySwapStorage'); + + factory LatestSwapsErrUnableToQuerySwapStorage.fromJson() => + const LatestSwapsErrUnableToQuerySwapStorage(); + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': null}; +} + +final class LatestSwapsErrUnknown extends LatestSwapsErr { + const LatestSwapsErrUnknown({required this.data, this.rawErrorType}) + : super(errorType: 'unknown'); + + final String? rawErrorType; + final dynamic data; + + @override + JsonMap toJson() => { + 'error_type': rawErrorType ?? errorType, + 'error_data': data, + }; +} + +sealed class CheckBalanceError { + const CheckBalanceError({required this.errorType}); + + factory CheckBalanceError.fromJson(dynamic json) { + final map = _asJsonMap(json); + final type = map.valueOrNull('error_type'); + final data = map.valueOrNull('error_data'); + switch (type) { + case 'NotSufficientBalance': + return CheckBalanceErrorNotSufficientBalance.fromJson(data); + case 'NotSufficientBaseCoinBalance': + return CheckBalanceErrorNotSufficientBaseCoinBalance.fromJson(data); + case 'VolumeTooLow': + return CheckBalanceErrorVolumeTooLow.fromJson(data); + case 'Transport': + return CheckBalanceErrorTransport.fromJson(data); + case 'InternalError': + return CheckBalanceErrorInternalError.fromJson(data); + default: + return CheckBalanceErrorUnknown(rawErrorType: type, data: data); + } + } + + final String errorType; + + JsonMap toJson(); +} + +final class CheckBalanceErrorNotSufficientBalance extends CheckBalanceError { + const CheckBalanceErrorNotSufficientBalance({ + required this.coin, + required this.available, + required this.required, + this.lockedBySwaps, + }) : super(errorType: 'NotSufficientBalance'); + + factory CheckBalanceErrorNotSufficientBalance.fromJson(dynamic json) { + final map = _asJsonMap(json); + return CheckBalanceErrorNotSufficientBalance( + coin: _stringFromJson(map.value('coin')), + available: BigDecimal.fromJson(map.value('available')), + required: BigDecimal.fromJson(map.value('required')), + lockedBySwaps: map.valueOrNull('locked_by_swaps') == null + ? null + : BigDecimal.fromJson(map.valueOrNull('locked_by_swaps')), + ); + } + + final String coin; + final BigDecimal available; + final BigDecimal required; + final BigDecimal? lockedBySwaps; + + @override + JsonMap toJson() => { + 'error_type': errorType, + 'error_data': { + 'coin': coin, + 'available': available.toJson(), + 'required': required.toJson(), + 'locked_by_swaps': lockedBySwaps?.toJson(), + }, + }; +} + +final class CheckBalanceErrorNotSufficientBaseCoinBalance + extends CheckBalanceError { + const CheckBalanceErrorNotSufficientBaseCoinBalance({ + required this.coin, + required this.available, + required this.required, + this.lockedBySwaps, + }) : super(errorType: 'NotSufficientBaseCoinBalance'); + + factory CheckBalanceErrorNotSufficientBaseCoinBalance.fromJson(dynamic json) { + final map = _asJsonMap(json); + return CheckBalanceErrorNotSufficientBaseCoinBalance( + coin: _stringFromJson(map.value('coin')), + available: BigDecimal.fromJson(map.value('available')), + required: BigDecimal.fromJson(map.value('required')), + lockedBySwaps: map.valueOrNull('locked_by_swaps') == null + ? null + : BigDecimal.fromJson(map.valueOrNull('locked_by_swaps')), + ); + } + + final String coin; + final BigDecimal available; + final BigDecimal required; + final BigDecimal? lockedBySwaps; + + @override + JsonMap toJson() => { + 'error_type': errorType, + 'error_data': { + 'coin': coin, + 'available': available.toJson(), + 'required': required.toJson(), + 'locked_by_swaps': lockedBySwaps?.toJson(), + }, + }; +} + +final class CheckBalanceErrorVolumeTooLow extends CheckBalanceError { + const CheckBalanceErrorVolumeTooLow({ + required this.coin, + required this.volume, + required this.threshold, + }) : super(errorType: 'VolumeTooLow'); + + factory CheckBalanceErrorVolumeTooLow.fromJson(dynamic json) { + final map = _asJsonMap(json); + return CheckBalanceErrorVolumeTooLow( + coin: _stringFromJson(map.value('coin')), + volume: BigDecimal.fromJson(map.value('volume')), + threshold: BigDecimal.fromJson(map.value('threshold')), + ); + } + + final String coin; + final BigDecimal volume; + final BigDecimal threshold; + + @override + JsonMap toJson() => { + 'error_type': errorType, + 'error_data': { + 'coin': coin, + 'volume': volume.toJson(), + 'threshold': threshold.toJson(), + }, + }; +} + +final class CheckBalanceErrorTransport extends CheckBalanceError { + const CheckBalanceErrorTransport(this.value) : super(errorType: 'Transport'); + + factory CheckBalanceErrorTransport.fromJson(dynamic json) { + return CheckBalanceErrorTransport(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class CheckBalanceErrorInternalError extends CheckBalanceError { + const CheckBalanceErrorInternalError(this.value) + : super(errorType: 'InternalError'); + + factory CheckBalanceErrorInternalError.fromJson(dynamic json) { + return CheckBalanceErrorInternalError(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class CheckBalanceErrorUnknown extends CheckBalanceError { + const CheckBalanceErrorUnknown({required this.data, this.rawErrorType}) + : super(errorType: 'unknown'); + + final String? rawErrorType; + final dynamic data; + + @override + JsonMap toJson() => { + 'error_type': rawErrorType ?? errorType, + 'error_data': data, + }; +} + +sealed class MaxMakerVolRpcError { + const MaxMakerVolRpcError({required this.errorType}); + + factory MaxMakerVolRpcError.fromJson(dynamic json) { + final map = _asJsonMap(json); + final type = map.valueOrNull('error_type'); + final data = map.valueOrNull('error_data'); + switch (type) { + case 'NotSufficientBalance': + return MaxMakerVolRpcErrorNotSufficientBalance.fromJson(data); + case 'NotSufficientBaseCoinBalance': + return MaxMakerVolRpcErrorNotSufficientBaseCoinBalance.fromJson(data); + case 'VolumeTooLow': + return MaxMakerVolRpcErrorVolumeTooLow.fromJson(data); + case 'NoSuchCoin': + return MaxMakerVolRpcErrorNoSuchCoin.fromJson(data); + case 'Transport': + return MaxMakerVolRpcErrorTransport.fromJson(data); + case 'InternalError': + return MaxMakerVolRpcErrorInternalError.fromJson(data); + default: + return MaxMakerVolRpcErrorUnknown(rawErrorType: type, data: data); + } + } + + final String errorType; + + JsonMap toJson(); +} + +final class MaxMakerVolRpcErrorNotSufficientBalance + extends MaxMakerVolRpcError { + const MaxMakerVolRpcErrorNotSufficientBalance({ + required this.coin, + required this.available, + required this.required, + this.lockedBySwaps, + }) : super(errorType: 'NotSufficientBalance'); + + factory MaxMakerVolRpcErrorNotSufficientBalance.fromJson(dynamic json) { + final map = _asJsonMap(json); + return MaxMakerVolRpcErrorNotSufficientBalance( + coin: _stringFromJson(map.value('coin')), + available: BigDecimal.fromJson(map.value('available')), + required: BigDecimal.fromJson(map.value('required')), + lockedBySwaps: map.valueOrNull('locked_by_swaps') == null + ? null + : BigDecimal.fromJson(map.valueOrNull('locked_by_swaps')), + ); + } + + final String coin; + final BigDecimal available; + final BigDecimal required; + final BigDecimal? lockedBySwaps; + + @override + JsonMap toJson() => { + 'error_type': errorType, + 'error_data': { + 'coin': coin, + 'available': available.toJson(), + 'required': required.toJson(), + 'locked_by_swaps': lockedBySwaps?.toJson(), + }, + }; +} + +final class MaxMakerVolRpcErrorNotSufficientBaseCoinBalance + extends MaxMakerVolRpcError { + const MaxMakerVolRpcErrorNotSufficientBaseCoinBalance({ + required this.coin, + required this.available, + required this.required, + this.lockedBySwaps, + }) : super(errorType: 'NotSufficientBaseCoinBalance'); + + factory MaxMakerVolRpcErrorNotSufficientBaseCoinBalance.fromJson( + dynamic json, + ) { + final map = _asJsonMap(json); + return MaxMakerVolRpcErrorNotSufficientBaseCoinBalance( + coin: _stringFromJson(map.value('coin')), + available: BigDecimal.fromJson(map.value('available')), + required: BigDecimal.fromJson(map.value('required')), + lockedBySwaps: map.valueOrNull('locked_by_swaps') == null + ? null + : BigDecimal.fromJson(map.valueOrNull('locked_by_swaps')), + ); + } + + final String coin; + final BigDecimal available; + final BigDecimal required; + final BigDecimal? lockedBySwaps; + + @override + JsonMap toJson() => { + 'error_type': errorType, + 'error_data': { + 'coin': coin, + 'available': available.toJson(), + 'required': required.toJson(), + 'locked_by_swaps': lockedBySwaps?.toJson(), + }, + }; +} + +final class MaxMakerVolRpcErrorVolumeTooLow extends MaxMakerVolRpcError { + const MaxMakerVolRpcErrorVolumeTooLow({ + required this.coin, + required this.volume, + required this.threshold, + }) : super(errorType: 'VolumeTooLow'); + + factory MaxMakerVolRpcErrorVolumeTooLow.fromJson(dynamic json) { + final map = _asJsonMap(json); + return MaxMakerVolRpcErrorVolumeTooLow( + coin: _stringFromJson(map.value('coin')), + volume: BigDecimal.fromJson(map.value('volume')), + threshold: BigDecimal.fromJson(map.value('threshold')), + ); + } + + final String coin; + final BigDecimal volume; + final BigDecimal threshold; + + @override + JsonMap toJson() => { + 'error_type': errorType, + 'error_data': { + 'coin': coin, + 'volume': volume.toJson(), + 'threshold': threshold.toJson(), + }, + }; +} + +final class MaxMakerVolRpcErrorNoSuchCoin extends MaxMakerVolRpcError { + const MaxMakerVolRpcErrorNoSuchCoin({required this.coin}) + : super(errorType: 'NoSuchCoin'); + + factory MaxMakerVolRpcErrorNoSuchCoin.fromJson(dynamic json) { + final map = _asJsonMap(json); + return MaxMakerVolRpcErrorNoSuchCoin( + coin: _stringFromJson(map.value('coin')), + ); + } + + final String coin; + + @override + JsonMap toJson() => { + 'error_type': errorType, + 'error_data': {'coin': coin}, + }; +} + +final class MaxMakerVolRpcErrorTransport extends MaxMakerVolRpcError { + const MaxMakerVolRpcErrorTransport(this.value) + : super(errorType: 'Transport'); + + factory MaxMakerVolRpcErrorTransport.fromJson(dynamic json) { + return MaxMakerVolRpcErrorTransport(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class MaxMakerVolRpcErrorInternalError extends MaxMakerVolRpcError { + const MaxMakerVolRpcErrorInternalError(this.value) + : super(errorType: 'InternalError'); + + factory MaxMakerVolRpcErrorInternalError.fromJson(dynamic json) { + return MaxMakerVolRpcErrorInternalError(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class MaxMakerVolRpcErrorUnknown extends MaxMakerVolRpcError { + const MaxMakerVolRpcErrorUnknown({required this.data, this.rawErrorType}) + : super(errorType: 'unknown'); + + final String? rawErrorType; + final dynamic data; + + @override + JsonMap toJson() => { + 'error_type': rawErrorType ?? errorType, + 'error_data': data, + }; +} + +sealed class RecreateSwapError { + const RecreateSwapError({required this.errorType}); + + factory RecreateSwapError.fromJson(dynamic json) { + final map = _asJsonMap(json); + final type = map.valueOrNull('error_type'); + final data = map.valueOrNull('error_data'); + switch (type) { + case 'SwapIsNotStarted': + return RecreateSwapErrorSwapIsNotStarted.fromJson(); + case 'SwapIsNotNegotiated': + return RecreateSwapErrorSwapIsNotNegotiated.fromJson(); + case 'UnexpectedEvent': + return RecreateSwapErrorUnexpectedEvent.fromJson(data); + case 'NoSuchCoin': + return RecreateSwapErrorNoSuchCoin.fromJson(data); + case 'NoSecretHash': + return RecreateSwapErrorNoSecretHash.fromJson(); + case 'Internal': + return RecreateSwapErrorInternal.fromJson(data); + default: + return RecreateSwapErrorUnknown(rawErrorType: type, data: data); + } + } + + final String errorType; + + JsonMap toJson(); +} + +final class RecreateSwapErrorSwapIsNotStarted extends RecreateSwapError { + const RecreateSwapErrorSwapIsNotStarted() + : super(errorType: 'SwapIsNotStarted'); + + factory RecreateSwapErrorSwapIsNotStarted.fromJson() => + const RecreateSwapErrorSwapIsNotStarted(); + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': null}; +} + +final class RecreateSwapErrorSwapIsNotNegotiated extends RecreateSwapError { + const RecreateSwapErrorSwapIsNotNegotiated() + : super(errorType: 'SwapIsNotNegotiated'); + + factory RecreateSwapErrorSwapIsNotNegotiated.fromJson() => + const RecreateSwapErrorSwapIsNotNegotiated(); + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': null}; +} + +final class RecreateSwapErrorUnexpectedEvent extends RecreateSwapError { + const RecreateSwapErrorUnexpectedEvent({ + required this.expected, + required this.found, + }) : super(errorType: 'UnexpectedEvent'); + + factory RecreateSwapErrorUnexpectedEvent.fromJson(dynamic json) { + final map = _asJsonMap(json); + return RecreateSwapErrorUnexpectedEvent( + expected: _stringFromJson(map.value('expected')), + found: _stringFromJson(map.value('found')), + ); + } + + final String expected; + final String found; + + @override + JsonMap toJson() => { + 'error_type': errorType, + 'error_data': {'expected': expected, 'found': found}, + }; +} + +final class RecreateSwapErrorNoSuchCoin extends RecreateSwapError { + const RecreateSwapErrorNoSuchCoin({required this.coin}) + : super(errorType: 'NoSuchCoin'); + + factory RecreateSwapErrorNoSuchCoin.fromJson(dynamic json) { + final map = _asJsonMap(json); + return RecreateSwapErrorNoSuchCoin( + coin: _stringFromJson(map.value('coin')), + ); + } + + final String coin; + + @override + JsonMap toJson() => { + 'error_type': errorType, + 'error_data': {'coin': coin}, + }; +} + +final class RecreateSwapErrorNoSecretHash extends RecreateSwapError { + const RecreateSwapErrorNoSecretHash() : super(errorType: 'NoSecretHash'); + + factory RecreateSwapErrorNoSecretHash.fromJson() => + const RecreateSwapErrorNoSecretHash(); + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': null}; +} + +final class RecreateSwapErrorInternal extends RecreateSwapError { + const RecreateSwapErrorInternal(this.value) : super(errorType: 'Internal'); + + factory RecreateSwapErrorInternal.fromJson(dynamic json) { + return RecreateSwapErrorInternal(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class RecreateSwapErrorUnknown extends RecreateSwapError { + const RecreateSwapErrorUnknown({required this.data, this.rawErrorType}) + : super(errorType: 'unknown'); + + final String? rawErrorType; + final dynamic data; + + @override + JsonMap toJson() => { + 'error_type': rawErrorType ?? errorType, + 'error_data': data, + }; +} + +sealed class ActiveSwapsErr { + const ActiveSwapsErr({required this.errorType}); + + factory ActiveSwapsErr.fromJson(dynamic json) { + final map = _asJsonMap(json); + final type = map.valueOrNull('error_type'); + final data = map.valueOrNull('error_data'); + switch (type) { + case 'Internal': + return ActiveSwapsErrInternal.fromJson(data); + default: + return ActiveSwapsErrUnknown(rawErrorType: type, data: data); + } + } + + final String errorType; + + JsonMap toJson(); +} + +final class ActiveSwapsErrInternal extends ActiveSwapsErr { + const ActiveSwapsErrInternal(this.value) : super(errorType: 'Internal'); + + factory ActiveSwapsErrInternal.fromJson(dynamic json) { + return ActiveSwapsErrInternal(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class ActiveSwapsErrUnknown extends ActiveSwapsErr { + const ActiveSwapsErrUnknown({required this.data, this.rawErrorType}) + : super(errorType: 'unknown'); + + final String? rawErrorType; + final dynamic data; + + @override + JsonMap toJson() => { + 'error_type': rawErrorType ?? errorType, + 'error_data': data, + }; +} + +sealed class MyRecentSwapsErr { + const MyRecentSwapsErr({required this.errorType}); + + factory MyRecentSwapsErr.fromJson(dynamic json) { + final map = _asJsonMap(json); + final type = map.valueOrNull('error_type'); + final data = map.valueOrNull('error_data'); + switch (type) { + case 'FromUuidSwapNotFound': + return MyRecentSwapsErrFromUuidSwapNotFound.fromJson(data); + case 'InvalidTimeStampRange': + return MyRecentSwapsErrInvalidTimeStampRange.fromJson(); + case 'DbError': + return MyRecentSwapsErrDbError.fromJson(data); + default: + return MyRecentSwapsErrUnknown(rawErrorType: type, data: data); + } + } + + final String errorType; + + JsonMap toJson(); +} + +final class MyRecentSwapsErrFromUuidSwapNotFound extends MyRecentSwapsErr { + const MyRecentSwapsErrFromUuidSwapNotFound(this.value) + : super(errorType: 'FromUuidSwapNotFound'); + + factory MyRecentSwapsErrFromUuidSwapNotFound.fromJson(dynamic json) { + return MyRecentSwapsErrFromUuidSwapNotFound(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class MyRecentSwapsErrInvalidTimeStampRange extends MyRecentSwapsErr { + const MyRecentSwapsErrInvalidTimeStampRange() + : super(errorType: 'InvalidTimeStampRange'); + + factory MyRecentSwapsErrInvalidTimeStampRange.fromJson() => + const MyRecentSwapsErrInvalidTimeStampRange(); + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': null}; +} + +final class MyRecentSwapsErrDbError extends MyRecentSwapsErr { + const MyRecentSwapsErrDbError(this.value) : super(errorType: 'DbError'); + + factory MyRecentSwapsErrDbError.fromJson(dynamic json) { + return MyRecentSwapsErrDbError(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class MyRecentSwapsErrUnknown extends MyRecentSwapsErr { + const MyRecentSwapsErrUnknown({required this.data, this.rawErrorType}) + : super(errorType: 'unknown'); + + final String? rawErrorType; + final dynamic data; + + @override + JsonMap toJson() => { + 'error_type': rawErrorType ?? errorType, + 'error_data': data, + }; +} + +sealed class MySwapStatusError { + const MySwapStatusError({required this.errorType}); + + factory MySwapStatusError.fromJson(dynamic json) { + final map = _asJsonMap(json); + final type = map.valueOrNull('error_type'); + final data = map.valueOrNull('error_data'); + switch (type) { + case 'NoSwapWithUuid': + return MySwapStatusErrorNoSwapWithUuid.fromJson(data); + case 'UnsupportedSwapType': + return MySwapStatusErrorUnsupportedSwapType.fromJson(data); + case 'DbError': + return MySwapStatusErrorDbError.fromJson(data); + default: + return MySwapStatusErrorUnknown(rawErrorType: type, data: data); + } + } + + final String errorType; + + JsonMap toJson(); +} + +final class MySwapStatusErrorNoSwapWithUuid extends MySwapStatusError { + const MySwapStatusErrorNoSwapWithUuid(this.value) + : super(errorType: 'NoSwapWithUuid'); + + factory MySwapStatusErrorNoSwapWithUuid.fromJson(dynamic json) { + return MySwapStatusErrorNoSwapWithUuid(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class MySwapStatusErrorUnsupportedSwapType extends MySwapStatusError { + const MySwapStatusErrorUnsupportedSwapType(this.value) + : super(errorType: 'UnsupportedSwapType'); + + factory MySwapStatusErrorUnsupportedSwapType.fromJson(dynamic json) { + return MySwapStatusErrorUnsupportedSwapType(_intFromJson(json)); + } + + final int value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class MySwapStatusErrorDbError extends MySwapStatusError { + const MySwapStatusErrorDbError(this.value) : super(errorType: 'DbError'); + + factory MySwapStatusErrorDbError.fromJson(dynamic json) { + return MySwapStatusErrorDbError(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class MySwapStatusErrorUnknown extends MySwapStatusError { + const MySwapStatusErrorUnknown({required this.data, this.rawErrorType}) + : super(errorType: 'unknown'); + + final String? rawErrorType; + final dynamic data; + + @override + JsonMap toJson() => { + 'error_type': rawErrorType ?? errorType, + 'error_data': data, + }; +} + +sealed class TradePreimageRpcError { + const TradePreimageRpcError({required this.errorType}); + + factory TradePreimageRpcError.fromJson(dynamic json) { + final map = _asJsonMap(json); + final type = map.valueOrNull('error_type'); + final data = map.valueOrNull('error_data'); + switch (type) { + case 'NotSufficientBalance': + return TradePreimageRpcErrorNotSufficientBalance.fromJson(data); + case 'NotSufficientBaseCoinBalance': + return TradePreimageRpcErrorNotSufficientBaseCoinBalance.fromJson(data); + case 'VolumeTooLow': + return TradePreimageRpcErrorVolumeTooLow.fromJson(data); + case 'NoSuchCoin': + return TradePreimageRpcErrorNoSuchCoin.fromJson(data); + case 'CoinIsWalletOnly': + return TradePreimageRpcErrorCoinIsWalletOnly.fromJson(data); + case 'BaseEqualRel': + return TradePreimageRpcErrorBaseEqualRel.fromJson(); + case 'InvalidParam': + return TradePreimageRpcErrorInvalidParam.fromJson(data); + case 'PriceTooLow': + return TradePreimageRpcErrorPriceTooLow.fromJson(data); + case 'Transport': + return TradePreimageRpcErrorTransport.fromJson(data); + case 'InternalError': + return TradePreimageRpcErrorInternalError.fromJson(data); + default: + return TradePreimageRpcErrorUnknown(rawErrorType: type, data: data); + } + } + + final String errorType; + + JsonMap toJson(); +} + +final class TradePreimageRpcErrorNotSufficientBalance + extends TradePreimageRpcError { + const TradePreimageRpcErrorNotSufficientBalance({ + required this.coin, + required this.available, + required this.required, + this.lockedBySwaps, + }) : super(errorType: 'NotSufficientBalance'); + + factory TradePreimageRpcErrorNotSufficientBalance.fromJson(dynamic json) { + final map = _asJsonMap(json); + return TradePreimageRpcErrorNotSufficientBalance( + coin: _stringFromJson(map.value('coin')), + available: BigDecimal.fromJson(map.value('available')), + required: BigDecimal.fromJson(map.value('required')), + lockedBySwaps: map.valueOrNull('locked_by_swaps') == null + ? null + : BigDecimal.fromJson(map.valueOrNull('locked_by_swaps')), + ); + } + + final String coin; + final BigDecimal available; + final BigDecimal required; + final BigDecimal? lockedBySwaps; + + @override + JsonMap toJson() => { + 'error_type': errorType, + 'error_data': { + 'coin': coin, + 'available': available.toJson(), + 'required': required.toJson(), + 'locked_by_swaps': lockedBySwaps?.toJson(), + }, + }; +} + +final class TradePreimageRpcErrorNotSufficientBaseCoinBalance + extends TradePreimageRpcError { + const TradePreimageRpcErrorNotSufficientBaseCoinBalance({ + required this.coin, + required this.available, + required this.required, + this.lockedBySwaps, + }) : super(errorType: 'NotSufficientBaseCoinBalance'); + + factory TradePreimageRpcErrorNotSufficientBaseCoinBalance.fromJson( + dynamic json, + ) { + final map = _asJsonMap(json); + return TradePreimageRpcErrorNotSufficientBaseCoinBalance( + coin: _stringFromJson(map.value('coin')), + available: BigDecimal.fromJson(map.value('available')), + required: BigDecimal.fromJson(map.value('required')), + lockedBySwaps: map.valueOrNull('locked_by_swaps') == null + ? null + : BigDecimal.fromJson(map.valueOrNull('locked_by_swaps')), + ); + } + + final String coin; + final BigDecimal available; + final BigDecimal required; + final BigDecimal? lockedBySwaps; + + @override + JsonMap toJson() => { + 'error_type': errorType, + 'error_data': { + 'coin': coin, + 'available': available.toJson(), + 'required': required.toJson(), + 'locked_by_swaps': lockedBySwaps?.toJson(), + }, + }; +} + +final class TradePreimageRpcErrorVolumeTooLow extends TradePreimageRpcError { + const TradePreimageRpcErrorVolumeTooLow({ + required this.coin, + required this.volume, + required this.threshold, + }) : super(errorType: 'VolumeTooLow'); + + factory TradePreimageRpcErrorVolumeTooLow.fromJson(dynamic json) { + final map = _asJsonMap(json); + return TradePreimageRpcErrorVolumeTooLow( + coin: _stringFromJson(map.value('coin')), + volume: BigDecimal.fromJson(map.value('volume')), + threshold: BigDecimal.fromJson(map.value('threshold')), + ); + } + + final String coin; + final BigDecimal volume; + final BigDecimal threshold; + + @override + JsonMap toJson() => { + 'error_type': errorType, + 'error_data': { + 'coin': coin, + 'volume': volume.toJson(), + 'threshold': threshold.toJson(), + }, + }; +} + +final class TradePreimageRpcErrorNoSuchCoin extends TradePreimageRpcError { + const TradePreimageRpcErrorNoSuchCoin({required this.coin}) + : super(errorType: 'NoSuchCoin'); + + factory TradePreimageRpcErrorNoSuchCoin.fromJson(dynamic json) { + final map = _asJsonMap(json); + return TradePreimageRpcErrorNoSuchCoin( + coin: _stringFromJson(map.value('coin')), + ); + } + + final String coin; + + @override + JsonMap toJson() => { + 'error_type': errorType, + 'error_data': {'coin': coin}, + }; +} + +final class TradePreimageRpcErrorCoinIsWalletOnly + extends TradePreimageRpcError { + const TradePreimageRpcErrorCoinIsWalletOnly({required this.coin}) + : super(errorType: 'CoinIsWalletOnly'); + + factory TradePreimageRpcErrorCoinIsWalletOnly.fromJson(dynamic json) { + final map = _asJsonMap(json); + return TradePreimageRpcErrorCoinIsWalletOnly( + coin: _stringFromJson(map.value('coin')), + ); + } + + final String coin; + + @override + JsonMap toJson() => { + 'error_type': errorType, + 'error_data': {'coin': coin}, + }; +} + +final class TradePreimageRpcErrorBaseEqualRel extends TradePreimageRpcError { + const TradePreimageRpcErrorBaseEqualRel() : super(errorType: 'BaseEqualRel'); + + factory TradePreimageRpcErrorBaseEqualRel.fromJson() => + const TradePreimageRpcErrorBaseEqualRel(); + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': null}; +} + +final class TradePreimageRpcErrorInvalidParam extends TradePreimageRpcError { + const TradePreimageRpcErrorInvalidParam({ + required this.param, + required this.reason, + }) : super(errorType: 'InvalidParam'); + + factory TradePreimageRpcErrorInvalidParam.fromJson(dynamic json) { + final map = _asJsonMap(json); + return TradePreimageRpcErrorInvalidParam( + param: _stringFromJson(map.value('param')), + reason: _stringFromJson(map.value('reason')), + ); + } + + final String param; + final String reason; + + @override + JsonMap toJson() => { + 'error_type': errorType, + 'error_data': {'param': param, 'reason': reason}, + }; +} + +final class TradePreimageRpcErrorPriceTooLow extends TradePreimageRpcError { + const TradePreimageRpcErrorPriceTooLow({ + required this.price, + required this.threshold, + }) : super(errorType: 'PriceTooLow'); + + factory TradePreimageRpcErrorPriceTooLow.fromJson(dynamic json) { + final map = _asJsonMap(json); + return TradePreimageRpcErrorPriceTooLow( + price: BigDecimal.fromJson(map.value('price')), + threshold: BigDecimal.fromJson(map.value('threshold')), + ); + } + + final BigDecimal price; + final BigDecimal threshold; + + @override + JsonMap toJson() => { + 'error_type': errorType, + 'error_data': {'price': price.toJson(), 'threshold': threshold.toJson()}, + }; +} + +final class TradePreimageRpcErrorTransport extends TradePreimageRpcError { + const TradePreimageRpcErrorTransport(this.value) + : super(errorType: 'Transport'); + + factory TradePreimageRpcErrorTransport.fromJson(dynamic json) { + return TradePreimageRpcErrorTransport(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class TradePreimageRpcErrorInternalError extends TradePreimageRpcError { + const TradePreimageRpcErrorInternalError(this.value) + : super(errorType: 'InternalError'); + + factory TradePreimageRpcErrorInternalError.fromJson(dynamic json) { + return TradePreimageRpcErrorInternalError(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class TradePreimageRpcErrorUnknown extends TradePreimageRpcError { + const TradePreimageRpcErrorUnknown({required this.data, this.rawErrorType}) + : super(errorType: 'unknown'); + + final String? rawErrorType; + final dynamic data; + + @override + JsonMap toJson() => { + 'error_type': rawErrorType ?? errorType, + 'error_data': data, + }; +} + +sealed class MnemonicRpcError { + const MnemonicRpcError({required this.errorType}); + + factory MnemonicRpcError.fromJson(dynamic json) { + final map = _asJsonMap(json); + final type = map.valueOrNull('error_type'); + final data = map.valueOrNull('error_data'); + switch (type) { + case 'InvalidRequest': + return MnemonicRpcErrorInvalidRequest.fromJson(data); + case 'WalletsStorageError': + return MnemonicRpcErrorWalletsStorageError.fromJson(data); + case 'Internal': + return MnemonicRpcErrorInternal.fromJson(data); + case 'InvalidPassword': + return MnemonicRpcErrorInvalidPassword.fromJson(data); + case 'PasswordPolicyViolation': + return MnemonicRpcErrorPasswordPolicyViolation.fromJson(data); + default: + return MnemonicRpcErrorUnknown(rawErrorType: type, data: data); + } + } + + final String errorType; + + JsonMap toJson(); +} + +final class MnemonicRpcErrorInvalidRequest extends MnemonicRpcError { + const MnemonicRpcErrorInvalidRequest(this.value) + : super(errorType: 'InvalidRequest'); + + factory MnemonicRpcErrorInvalidRequest.fromJson(dynamic json) { + return MnemonicRpcErrorInvalidRequest(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class MnemonicRpcErrorWalletsStorageError extends MnemonicRpcError { + const MnemonicRpcErrorWalletsStorageError(this.value) + : super(errorType: 'WalletsStorageError'); + + factory MnemonicRpcErrorWalletsStorageError.fromJson(dynamic json) { + return MnemonicRpcErrorWalletsStorageError(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class MnemonicRpcErrorInternal extends MnemonicRpcError { + const MnemonicRpcErrorInternal(this.value) : super(errorType: 'Internal'); + + factory MnemonicRpcErrorInternal.fromJson(dynamic json) { + return MnemonicRpcErrorInternal(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class MnemonicRpcErrorInvalidPassword extends MnemonicRpcError { + const MnemonicRpcErrorInvalidPassword(this.value) + : super(errorType: 'InvalidPassword'); + + factory MnemonicRpcErrorInvalidPassword.fromJson(dynamic json) { + return MnemonicRpcErrorInvalidPassword(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class MnemonicRpcErrorPasswordPolicyViolation extends MnemonicRpcError { + const MnemonicRpcErrorPasswordPolicyViolation(this.value) + : super(errorType: 'PasswordPolicyViolation'); + + factory MnemonicRpcErrorPasswordPolicyViolation.fromJson(dynamic json) { + return MnemonicRpcErrorPasswordPolicyViolation(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class MnemonicRpcErrorUnknown extends MnemonicRpcError { + const MnemonicRpcErrorUnknown({required this.data, this.rawErrorType}) + : super(errorType: 'unknown'); + + final String? rawErrorType; + final dynamic data; + + @override + JsonMap toJson() => { + 'error_type': rawErrorType ?? errorType, + 'error_data': data, + }; +} + +sealed class TelegramError { + const TelegramError({required this.errorType}); + + factory TelegramError.fromJson(dynamic json) { + final map = _asJsonMap(json); + final type = map.valueOrNull('error_type'); + final data = map.valueOrNull('error_data'); + switch (type) { + case 'RequestError': + return TelegramErrorRequestError.fromJson(data); + default: + return TelegramErrorUnknown(rawErrorType: type, data: data); + } + } + + final String errorType; + + JsonMap toJson(); +} + +final class TelegramErrorRequestError extends TelegramError { + const TelegramErrorRequestError(this.value) + : super(errorType: 'RequestError'); + + factory TelegramErrorRequestError.fromJson(dynamic json) { + return TelegramErrorRequestError(SlurpError.fromJson(json)); + } + + final SlurpError value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value.toJson()}; +} + +final class TelegramErrorUnknown extends TelegramError { + const TelegramErrorUnknown({required this.data, this.rawErrorType}) + : super(errorType: 'unknown'); + + final String? rawErrorType; + final dynamic data; + + @override + JsonMap toJson() => { + 'error_type': rawErrorType ?? errorType, + 'error_data': data, + }; +} + +sealed class DispatcherError { + const DispatcherError({required this.errorType}); + + factory DispatcherError.fromJson(dynamic json) { + final map = _asJsonMap(json); + final type = map.valueOrNull('error_type'); + final data = map.valueOrNull('error_data'); + switch (type) { + case 'Banned': + return DispatcherErrorBanned.fromJson(); + case 'NoSuchMethod': + return DispatcherErrorNoSuchMethod.fromJson(); + case 'InvalidRequest': + return DispatcherErrorInvalidRequest.fromJson(data); + case 'LocalHostOnly': + return DispatcherErrorLocalHostOnly.fromJson(); + case 'UserpassIsNotSet': + return DispatcherErrorUserpassIsNotSet.fromJson(); + case 'UserpassIsInvalid': + return DispatcherErrorUserpassIsInvalid.fromJson(data); + case 'InvalidMmRpcVersion': + return DispatcherErrorInvalidMmRpcVersion.fromJson(data); + default: + return DispatcherErrorUnknown(rawErrorType: type, data: data); + } + } + + final String errorType; + + JsonMap toJson(); +} + +final class DispatcherErrorBanned extends DispatcherError { + const DispatcherErrorBanned() : super(errorType: 'Banned'); + + factory DispatcherErrorBanned.fromJson() => const DispatcherErrorBanned(); + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': null}; +} + +final class DispatcherErrorNoSuchMethod extends DispatcherError { + const DispatcherErrorNoSuchMethod() : super(errorType: 'NoSuchMethod'); + + factory DispatcherErrorNoSuchMethod.fromJson() => + const DispatcherErrorNoSuchMethod(); + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': null}; +} + +final class DispatcherErrorInvalidRequest extends DispatcherError { + const DispatcherErrorInvalidRequest(this.value) + : super(errorType: 'InvalidRequest'); + + factory DispatcherErrorInvalidRequest.fromJson(dynamic json) { + return DispatcherErrorInvalidRequest(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class DispatcherErrorLocalHostOnly extends DispatcherError { + const DispatcherErrorLocalHostOnly() : super(errorType: 'LocalHostOnly'); + + factory DispatcherErrorLocalHostOnly.fromJson() => + const DispatcherErrorLocalHostOnly(); + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': null}; +} + +final class DispatcherErrorUserpassIsNotSet extends DispatcherError { + const DispatcherErrorUserpassIsNotSet() + : super(errorType: 'UserpassIsNotSet'); + + factory DispatcherErrorUserpassIsNotSet.fromJson() => + const DispatcherErrorUserpassIsNotSet(); + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': null}; +} + +final class DispatcherErrorUserpassIsInvalid extends DispatcherError { + const DispatcherErrorUserpassIsInvalid(this.value) + : super(errorType: 'UserpassIsInvalid'); + + factory DispatcherErrorUserpassIsInvalid.fromJson(dynamic json) { + return DispatcherErrorUserpassIsInvalid(RateLimitError.fromJson(json)); + } + + final RateLimitError value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value.toJson()}; +} + +final class DispatcherErrorInvalidMmRpcVersion extends DispatcherError { + const DispatcherErrorInvalidMmRpcVersion(this.value) + : super(errorType: 'InvalidMmRpcVersion'); + + factory DispatcherErrorInvalidMmRpcVersion.fromJson(dynamic json) { + return DispatcherErrorInvalidMmRpcVersion(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class DispatcherErrorUnknown extends DispatcherError { + const DispatcherErrorUnknown({required this.data, this.rawErrorType}) + : super(errorType: 'unknown'); + + final String? rawErrorType; + final dynamic data; + + @override + JsonMap toJson() => { + 'error_type': rawErrorType ?? errorType, + 'error_data': data, + }; +} + +sealed class ApiIntegrationRpcError { + const ApiIntegrationRpcError({required this.errorType}); + + factory ApiIntegrationRpcError.fromJson(dynamic json) { + final map = _asJsonMap(json); + final type = map.valueOrNull('error_type'); + final data = map.valueOrNull('error_data'); + switch (type) { + case 'NoSuchCoin': + return ApiIntegrationRpcErrorNoSuchCoin.fromJson(data); + case 'CoinTypeError': + return ApiIntegrationRpcErrorCoinTypeError.fromJson(); + case 'NftProtocolNotSupported': + return ApiIntegrationRpcErrorNftProtocolNotSupported.fromJson(); + case 'ChainNotSupported': + return ApiIntegrationRpcErrorChainNotSupported.fromJson(); + case 'DifferentChains': + return ApiIntegrationRpcErrorDifferentChains.fromJson(); + case 'MyAddressError': + return ApiIntegrationRpcErrorMyAddressError.fromJson(data); + case 'NumberError': + return ApiIntegrationRpcErrorNumberError.fromJson(data); + case 'InvalidParam': + return ApiIntegrationRpcErrorInvalidParam.fromJson(data); + case 'OutOfBounds': + return ApiIntegrationRpcErrorOutOfBounds.fromJson(data); + case 'OneInchAllowanceNotEnough': + return ApiIntegrationRpcErrorOneInchAllowanceNotEnough.fromJson(data); + case 'OneInchError': + return ApiIntegrationRpcErrorOneInchError.fromJson(data); + case 'ApiDataError': + return ApiIntegrationRpcErrorApiDataError.fromJson(data); + case 'InternalError': + return ApiIntegrationRpcErrorInternalError.fromJson(data); + case 'BestLrSwapNotFound': + return ApiIntegrationRpcErrorBestLrSwapNotFound.fromJson(); + default: + return ApiIntegrationRpcErrorUnknown(rawErrorType: type, data: data); + } + } + + final String errorType; + + JsonMap toJson(); +} + +final class ApiIntegrationRpcErrorNoSuchCoin extends ApiIntegrationRpcError { + const ApiIntegrationRpcErrorNoSuchCoin({required this.coin}) + : super(errorType: 'NoSuchCoin'); + + factory ApiIntegrationRpcErrorNoSuchCoin.fromJson(dynamic json) { + final map = _asJsonMap(json); + return ApiIntegrationRpcErrorNoSuchCoin( + coin: _stringFromJson(map.value('coin')), + ); + } + + final String coin; + + @override + JsonMap toJson() => { + 'error_type': errorType, + 'error_data': {'coin': coin}, + }; +} + +final class ApiIntegrationRpcErrorCoinTypeError extends ApiIntegrationRpcError { + const ApiIntegrationRpcErrorCoinTypeError() + : super(errorType: 'CoinTypeError'); + + factory ApiIntegrationRpcErrorCoinTypeError.fromJson() => + const ApiIntegrationRpcErrorCoinTypeError(); + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': null}; +} + +final class ApiIntegrationRpcErrorNftProtocolNotSupported + extends ApiIntegrationRpcError { + const ApiIntegrationRpcErrorNftProtocolNotSupported() + : super(errorType: 'NftProtocolNotSupported'); + + factory ApiIntegrationRpcErrorNftProtocolNotSupported.fromJson() => + const ApiIntegrationRpcErrorNftProtocolNotSupported(); + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': null}; +} + +final class ApiIntegrationRpcErrorChainNotSupported + extends ApiIntegrationRpcError { + const ApiIntegrationRpcErrorChainNotSupported() + : super(errorType: 'ChainNotSupported'); + + factory ApiIntegrationRpcErrorChainNotSupported.fromJson() => + const ApiIntegrationRpcErrorChainNotSupported(); + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': null}; +} + +final class ApiIntegrationRpcErrorDifferentChains + extends ApiIntegrationRpcError { + const ApiIntegrationRpcErrorDifferentChains() + : super(errorType: 'DifferentChains'); + + factory ApiIntegrationRpcErrorDifferentChains.fromJson() => + const ApiIntegrationRpcErrorDifferentChains(); + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': null}; +} + +final class ApiIntegrationRpcErrorMyAddressError + extends ApiIntegrationRpcError { + const ApiIntegrationRpcErrorMyAddressError(this.value) + : super(errorType: 'MyAddressError'); + + factory ApiIntegrationRpcErrorMyAddressError.fromJson(dynamic json) { + return ApiIntegrationRpcErrorMyAddressError(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class ApiIntegrationRpcErrorNumberError extends ApiIntegrationRpcError { + const ApiIntegrationRpcErrorNumberError(this.value) + : super(errorType: 'NumberError'); + + factory ApiIntegrationRpcErrorNumberError.fromJson(dynamic json) { + return ApiIntegrationRpcErrorNumberError(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class ApiIntegrationRpcErrorInvalidParam extends ApiIntegrationRpcError { + const ApiIntegrationRpcErrorInvalidParam(this.value) + : super(errorType: 'InvalidParam'); + + factory ApiIntegrationRpcErrorInvalidParam.fromJson(dynamic json) { + return ApiIntegrationRpcErrorInvalidParam(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class ApiIntegrationRpcErrorOutOfBounds extends ApiIntegrationRpcError { + const ApiIntegrationRpcErrorOutOfBounds({ + required this.param, + required this.value, + required this.min, + required this.max, + }) : super(errorType: 'OutOfBounds'); + + factory ApiIntegrationRpcErrorOutOfBounds.fromJson(dynamic json) { + final map = _asJsonMap(json); + return ApiIntegrationRpcErrorOutOfBounds( + param: _stringFromJson(map.value('param')), + value: _stringFromJson(map.value('value')), + min: _stringFromJson(map.value('min')), + max: _stringFromJson(map.value('max')), + ); + } + + final String param; + final String value; + final String min; + final String max; + + @override + JsonMap toJson() => { + 'error_type': errorType, + 'error_data': {'param': param, 'value': value, 'min': min, 'max': max}, + }; +} + +final class ApiIntegrationRpcErrorOneInchAllowanceNotEnough + extends ApiIntegrationRpcError { + const ApiIntegrationRpcErrorOneInchAllowanceNotEnough({ + required this.allowance, + required this.amount, + }) : super(errorType: 'OneInchAllowanceNotEnough'); + + factory ApiIntegrationRpcErrorOneInchAllowanceNotEnough.fromJson( + dynamic json, + ) { + final map = _asJsonMap(json); + return ApiIntegrationRpcErrorOneInchAllowanceNotEnough( + allowance: U256.fromJson(map.value('allowance')), + amount: U256.fromJson(map.value('amount')), + ); + } + + final U256 allowance; + final U256 amount; + + @override + JsonMap toJson() => { + 'error_type': errorType, + 'error_data': {'allowance': allowance.toJson(), 'amount': amount.toJson()}, + }; +} + +final class ApiIntegrationRpcErrorOneInchError extends ApiIntegrationRpcError { + const ApiIntegrationRpcErrorOneInchError(this.value) + : super(errorType: 'OneInchError'); + + factory ApiIntegrationRpcErrorOneInchError.fromJson(dynamic json) { + return ApiIntegrationRpcErrorOneInchError(ApiClientError.fromJson(json)); + } + + final ApiClientError value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value.toJson()}; +} + +final class ApiIntegrationRpcErrorApiDataError extends ApiIntegrationRpcError { + const ApiIntegrationRpcErrorApiDataError(this.value) + : super(errorType: 'ApiDataError'); + + factory ApiIntegrationRpcErrorApiDataError.fromJson(dynamic json) { + return ApiIntegrationRpcErrorApiDataError(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class ApiIntegrationRpcErrorInternalError extends ApiIntegrationRpcError { + const ApiIntegrationRpcErrorInternalError(this.value) + : super(errorType: 'InternalError'); + + factory ApiIntegrationRpcErrorInternalError.fromJson(dynamic json) { + return ApiIntegrationRpcErrorInternalError(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class ApiIntegrationRpcErrorBestLrSwapNotFound + extends ApiIntegrationRpcError { + const ApiIntegrationRpcErrorBestLrSwapNotFound() + : super(errorType: 'BestLrSwapNotFound'); + + factory ApiIntegrationRpcErrorBestLrSwapNotFound.fromJson() => + const ApiIntegrationRpcErrorBestLrSwapNotFound(); + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': null}; +} + +final class ApiIntegrationRpcErrorUnknown extends ApiIntegrationRpcError { + const ApiIntegrationRpcErrorUnknown({required this.data, this.rawErrorType}) + : super(errorType: 'unknown'); + + final String? rawErrorType; + final dynamic data; + + @override + JsonMap toJson() => { + 'error_type': rawErrorType ?? errorType, + 'error_data': data, + }; +} + +sealed class GetPublicKeyError { + const GetPublicKeyError({required this.errorType}); + + factory GetPublicKeyError.fromJson(dynamic json) { + final map = _asJsonMap(json); + final type = map.valueOrNull('error_type'); + final data = map.valueOrNull('error_data'); + switch (type) { + case 'Internal': + return GetPublicKeyErrorInternal.fromJson(data); + default: + return GetPublicKeyErrorUnknown(rawErrorType: type, data: data); + } + } + + final String errorType; + + JsonMap toJson(); +} + +final class GetPublicKeyErrorInternal extends GetPublicKeyError { + const GetPublicKeyErrorInternal(this.value) : super(errorType: 'Internal'); + + factory GetPublicKeyErrorInternal.fromJson(dynamic json) { + return GetPublicKeyErrorInternal(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class GetPublicKeyErrorUnknown extends GetPublicKeyError { + const GetPublicKeyErrorUnknown({required this.data, this.rawErrorType}) + : super(errorType: 'unknown'); + + final String? rawErrorType; + final dynamic data; + + @override + JsonMap toJson() => { + 'error_type': rawErrorType ?? errorType, + 'error_data': data, + }; +} + +sealed class Erc20CallError { + const Erc20CallError({required this.errorType}); + + factory Erc20CallError.fromJson(dynamic json) { + final map = _asJsonMap(json); + final type = map.valueOrNull('error_type'); + final data = map.valueOrNull('error_data'); + switch (type) { + case 'NoSuchCoin': + return Erc20CallErrorNoSuchCoin.fromJson(data); + case 'CoinNotSupported': + return Erc20CallErrorCoinNotSupported.fromJson(data); + case 'InvalidParam': + return Erc20CallErrorInvalidParam.fromJson(data); + case 'TransactionError': + return Erc20CallErrorTransactionError.fromJson(data); + case 'Web3RpcError': + return Erc20CallErrorWeb3RpcError.fromJson(data); + default: + return Erc20CallErrorUnknown(rawErrorType: type, data: data); + } + } + + final String errorType; + + JsonMap toJson(); +} + +final class Erc20CallErrorNoSuchCoin extends Erc20CallError { + const Erc20CallErrorNoSuchCoin({required this.coin}) + : super(errorType: 'NoSuchCoin'); + + factory Erc20CallErrorNoSuchCoin.fromJson(dynamic json) { + final map = _asJsonMap(json); + return Erc20CallErrorNoSuchCoin( + coin: _stringFromJson(map.value('coin')), + ); + } + + final String coin; + + @override + JsonMap toJson() => { + 'error_type': errorType, + 'error_data': {'coin': coin}, + }; +} + +final class Erc20CallErrorCoinNotSupported extends Erc20CallError { + const Erc20CallErrorCoinNotSupported({required this.coin}) + : super(errorType: 'CoinNotSupported'); + + factory Erc20CallErrorCoinNotSupported.fromJson(dynamic json) { + final map = _asJsonMap(json); + return Erc20CallErrorCoinNotSupported( + coin: _stringFromJson(map.value('coin')), + ); + } + + final String coin; + + @override + JsonMap toJson() => { + 'error_type': errorType, + 'error_data': {'coin': coin}, + }; +} + +final class Erc20CallErrorInvalidParam extends Erc20CallError { + const Erc20CallErrorInvalidParam(this.value) + : super(errorType: 'InvalidParam'); + + factory Erc20CallErrorInvalidParam.fromJson(dynamic json) { + return Erc20CallErrorInvalidParam(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class Erc20CallErrorTransactionError extends Erc20CallError { + const Erc20CallErrorTransactionError(this.value) + : super(errorType: 'TransactionError'); + + factory Erc20CallErrorTransactionError.fromJson(dynamic json) { + return Erc20CallErrorTransactionError(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class Erc20CallErrorWeb3RpcError extends Erc20CallError { + const Erc20CallErrorWeb3RpcError(this.value) + : super(errorType: 'Web3RpcError'); + + factory Erc20CallErrorWeb3RpcError.fromJson(dynamic json) { + return Erc20CallErrorWeb3RpcError(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class Erc20CallErrorUnknown extends Erc20CallError { + const Erc20CallErrorUnknown({required this.data, this.rawErrorType}) + : super(errorType: 'unknown'); + + final String? rawErrorType; + final dynamic data; + + @override + JsonMap toJson() => { + 'error_type': rawErrorType ?? errorType, + 'error_data': data, + }; +} + +sealed class TokenInfoError { + const TokenInfoError({required this.errorType}); + + factory TokenInfoError.fromJson(dynamic json) { + final map = _asJsonMap(json); + final type = map.valueOrNull('error_type'); + final data = map.valueOrNull('error_data'); + switch (type) { + case 'NoSuchCoin': + return TokenInfoErrorNoSuchCoin.fromJson(data); + case 'UnsupportedTokenProtocol': + return TokenInfoErrorUnsupportedTokenProtocol.fromJson(data); + case 'InvalidRequest': + return TokenInfoErrorInvalidRequest.fromJson(data); + case 'RetrieveInfoError': + return TokenInfoErrorRetrieveInfoError.fromJson(data); + default: + return TokenInfoErrorUnknown(rawErrorType: type, data: data); + } + } + + final String errorType; + + JsonMap toJson(); +} + +final class TokenInfoErrorNoSuchCoin extends TokenInfoError { + const TokenInfoErrorNoSuchCoin({required this.coin}) + : super(errorType: 'NoSuchCoin'); + + factory TokenInfoErrorNoSuchCoin.fromJson(dynamic json) { + final map = _asJsonMap(json); + return TokenInfoErrorNoSuchCoin( + coin: _stringFromJson(map.value('coin')), + ); + } + + final String coin; + + @override + JsonMap toJson() => { + 'error_type': errorType, + 'error_data': {'coin': coin}, + }; +} + +final class TokenInfoErrorUnsupportedTokenProtocol extends TokenInfoError { + const TokenInfoErrorUnsupportedTokenProtocol({required this.protocol}) + : super(errorType: 'UnsupportedTokenProtocol'); + + factory TokenInfoErrorUnsupportedTokenProtocol.fromJson(dynamic json) { + final map = _asJsonMap(json); + return TokenInfoErrorUnsupportedTokenProtocol( + protocol: _stringFromJson(map.value('protocol')), + ); + } + + final String protocol; + + @override + JsonMap toJson() => { + 'error_type': errorType, + 'error_data': {'protocol': protocol}, + }; +} + +final class TokenInfoErrorInvalidRequest extends TokenInfoError { + const TokenInfoErrorInvalidRequest(this.value) + : super(errorType: 'InvalidRequest'); + + factory TokenInfoErrorInvalidRequest.fromJson(dynamic json) { + return TokenInfoErrorInvalidRequest(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class TokenInfoErrorRetrieveInfoError extends TokenInfoError { + const TokenInfoErrorRetrieveInfoError(this.value) + : super(errorType: 'RetrieveInfoError'); + + factory TokenInfoErrorRetrieveInfoError.fromJson(dynamic json) { + return TokenInfoErrorRetrieveInfoError(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class TokenInfoErrorUnknown extends TokenInfoError { + const TokenInfoErrorUnknown({required this.data, this.rawErrorType}) + : super(errorType: 'unknown'); + + final String? rawErrorType; + final dynamic data; + + @override + JsonMap toJson() => { + 'error_type': rawErrorType ?? errorType, + 'error_data': data, + }; +} + +sealed class TrezorConnectionError { + const TrezorConnectionError({required this.errorType}); + + factory TrezorConnectionError.fromJson(dynamic json) { + final map = _asJsonMap(json); + final type = map.valueOrNull('error_type'); + final data = map.valueOrNull('error_data'); + switch (type) { + case 'TrezorNotInitialized': + return TrezorConnectionErrorTrezorNotInitialized.fromJson(); + case 'FoundUnexpectedDevice': + return TrezorConnectionErrorFoundUnexpectedDevice.fromJson(); + case 'Internal': + return TrezorConnectionErrorInternal.fromJson(data); + default: + return TrezorConnectionErrorUnknown(rawErrorType: type, data: data); + } + } + + final String errorType; + + JsonMap toJson(); +} + +final class TrezorConnectionErrorTrezorNotInitialized + extends TrezorConnectionError { + const TrezorConnectionErrorTrezorNotInitialized() + : super(errorType: 'TrezorNotInitialized'); + + factory TrezorConnectionErrorTrezorNotInitialized.fromJson() => + const TrezorConnectionErrorTrezorNotInitialized(); + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': null}; +} + +final class TrezorConnectionErrorFoundUnexpectedDevice + extends TrezorConnectionError { + const TrezorConnectionErrorFoundUnexpectedDevice() + : super(errorType: 'FoundUnexpectedDevice'); + + factory TrezorConnectionErrorFoundUnexpectedDevice.fromJson() => + const TrezorConnectionErrorFoundUnexpectedDevice(); + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': null}; +} + +final class TrezorConnectionErrorInternal extends TrezorConnectionError { + const TrezorConnectionErrorInternal(this.value) + : super(errorType: 'Internal'); + + factory TrezorConnectionErrorInternal.fromJson(dynamic json) { + return TrezorConnectionErrorInternal(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class TrezorConnectionErrorUnknown extends TrezorConnectionError { + const TrezorConnectionErrorUnknown({required this.data, this.rawErrorType}) + : super(errorType: 'unknown'); + + final String? rawErrorType; + final dynamic data; + + @override + JsonMap toJson() => { + 'error_type': rawErrorType ?? errorType, + 'error_data': data, + }; +} + +sealed class RateLimitError { + const RateLimitError({required this.errorType}); + + factory RateLimitError.fromJson(dynamic json) { + final map = _asJsonMap(json); + final type = map.valueOrNull('error_type'); + final data = map.valueOrNull('error_data'); + switch (type) { + case 'NbAttemptsLeft': + return RateLimitErrorNbAttemptsLeft.fromJson(data); + default: + return RateLimitErrorUnknown(rawErrorType: type, data: data); + } + } + + final String errorType; + + JsonMap toJson(); +} + +final class RateLimitErrorNbAttemptsLeft extends RateLimitError { + const RateLimitErrorNbAttemptsLeft(this.value) + : super(errorType: 'NbAttemptsLeft'); + + factory RateLimitErrorNbAttemptsLeft.fromJson(dynamic json) { + return RateLimitErrorNbAttemptsLeft(_intFromJson(json)); + } + + final int value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class RateLimitErrorUnknown extends RateLimitError { + const RateLimitErrorUnknown({required this.data, this.rawErrorType}) + : super(errorType: 'unknown'); + + final String? rawErrorType; + final dynamic data; + + @override + JsonMap toJson() => { + 'error_type': rawErrorType ?? errorType, + 'error_data': data, + }; +} + +sealed class BalanceStreamingRequestError { + const BalanceStreamingRequestError({required this.errorType}); + + factory BalanceStreamingRequestError.fromJson(dynamic json) { + final map = _asJsonMap(json); + final type = map.valueOrNull('error_type'); + final data = map.valueOrNull('error_data'); + switch (type) { + case 'EnableError': + return BalanceStreamingRequestErrorEnableError.fromJson(data); + case 'CoinNotFound': + return BalanceStreamingRequestErrorCoinNotFound.fromJson(); + case 'CoinNotSupported': + return BalanceStreamingRequestErrorCoinNotSupported.fromJson(); + case 'Internal': + return BalanceStreamingRequestErrorInternal.fromJson(data); + default: + return BalanceStreamingRequestErrorUnknown( + rawErrorType: type, + data: data, + ); + } + } + + final String errorType; + + JsonMap toJson(); +} + +final class BalanceStreamingRequestErrorEnableError + extends BalanceStreamingRequestError { + const BalanceStreamingRequestErrorEnableError(this.value) + : super(errorType: 'EnableError'); + + factory BalanceStreamingRequestErrorEnableError.fromJson(dynamic json) { + return BalanceStreamingRequestErrorEnableError(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class BalanceStreamingRequestErrorCoinNotFound + extends BalanceStreamingRequestError { + const BalanceStreamingRequestErrorCoinNotFound() + : super(errorType: 'CoinNotFound'); + + factory BalanceStreamingRequestErrorCoinNotFound.fromJson() => + const BalanceStreamingRequestErrorCoinNotFound(); + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': null}; +} + +final class BalanceStreamingRequestErrorCoinNotSupported + extends BalanceStreamingRequestError { + const BalanceStreamingRequestErrorCoinNotSupported() + : super(errorType: 'CoinNotSupported'); + + factory BalanceStreamingRequestErrorCoinNotSupported.fromJson() => + const BalanceStreamingRequestErrorCoinNotSupported(); + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': null}; +} + +final class BalanceStreamingRequestErrorInternal + extends BalanceStreamingRequestError { + const BalanceStreamingRequestErrorInternal(this.value) + : super(errorType: 'Internal'); + + factory BalanceStreamingRequestErrorInternal.fromJson(dynamic json) { + return BalanceStreamingRequestErrorInternal(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class BalanceStreamingRequestErrorUnknown + extends BalanceStreamingRequestError { + const BalanceStreamingRequestErrorUnknown({ + required this.data, + this.rawErrorType, + }) : super(errorType: 'unknown'); + + final String? rawErrorType; + final dynamic data; + + @override + JsonMap toJson() => { + 'error_type': rawErrorType ?? errorType, + 'error_data': data, + }; +} + +sealed class DisableStreamingRequestError { + const DisableStreamingRequestError({required this.errorType}); + + factory DisableStreamingRequestError.fromJson(dynamic json) { + final map = _asJsonMap(json); + final type = map.valueOrNull('error_type'); + final data = map.valueOrNull('error_data'); + switch (type) { + case 'DisableError': + return DisableStreamingRequestErrorDisableError.fromJson(data); + default: + return DisableStreamingRequestErrorUnknown( + rawErrorType: type, + data: data, + ); + } + } + + final String errorType; + + JsonMap toJson(); +} + +final class DisableStreamingRequestErrorDisableError + extends DisableStreamingRequestError { + const DisableStreamingRequestErrorDisableError(this.value) + : super(errorType: 'DisableError'); + + factory DisableStreamingRequestErrorDisableError.fromJson(dynamic json) { + return DisableStreamingRequestErrorDisableError(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class DisableStreamingRequestErrorUnknown + extends DisableStreamingRequestError { + const DisableStreamingRequestErrorUnknown({ + required this.data, + this.rawErrorType, + }) : super(errorType: 'unknown'); + + final String? rawErrorType; + final dynamic data; + + @override + JsonMap toJson() => { + 'error_type': rawErrorType ?? errorType, + 'error_data': data, + }; +} + +sealed class FeeStreamingRequestError { + const FeeStreamingRequestError({required this.errorType}); + + factory FeeStreamingRequestError.fromJson(dynamic json) { + final map = _asJsonMap(json); + final type = map.valueOrNull('error_type'); + final data = map.valueOrNull('error_data'); + switch (type) { + case 'EnableError': + return FeeStreamingRequestErrorEnableError.fromJson(data); + case 'CoinNotFound': + return FeeStreamingRequestErrorCoinNotFound.fromJson(); + case 'CoinNotSupported': + return FeeStreamingRequestErrorCoinNotSupported.fromJson(); + case 'Internal': + return FeeStreamingRequestErrorInternal.fromJson(data); + default: + return FeeStreamingRequestErrorUnknown(rawErrorType: type, data: data); + } + } + + final String errorType; + + JsonMap toJson(); +} + +final class FeeStreamingRequestErrorEnableError + extends FeeStreamingRequestError { + const FeeStreamingRequestErrorEnableError(this.value) + : super(errorType: 'EnableError'); + + factory FeeStreamingRequestErrorEnableError.fromJson(dynamic json) { + return FeeStreamingRequestErrorEnableError(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class FeeStreamingRequestErrorCoinNotFound + extends FeeStreamingRequestError { + const FeeStreamingRequestErrorCoinNotFound() + : super(errorType: 'CoinNotFound'); + + factory FeeStreamingRequestErrorCoinNotFound.fromJson() => + const FeeStreamingRequestErrorCoinNotFound(); + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': null}; +} + +final class FeeStreamingRequestErrorCoinNotSupported + extends FeeStreamingRequestError { + const FeeStreamingRequestErrorCoinNotSupported() + : super(errorType: 'CoinNotSupported'); + + factory FeeStreamingRequestErrorCoinNotSupported.fromJson() => + const FeeStreamingRequestErrorCoinNotSupported(); + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': null}; +} + +final class FeeStreamingRequestErrorInternal extends FeeStreamingRequestError { + const FeeStreamingRequestErrorInternal(this.value) + : super(errorType: 'Internal'); + + factory FeeStreamingRequestErrorInternal.fromJson(dynamic json) { + return FeeStreamingRequestErrorInternal(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class FeeStreamingRequestErrorUnknown extends FeeStreamingRequestError { + const FeeStreamingRequestErrorUnknown({required this.data, this.rawErrorType}) + : super(errorType: 'unknown'); + + final String? rawErrorType; + final dynamic data; + + @override + JsonMap toJson() => { + 'error_type': rawErrorType ?? errorType, + 'error_data': data, + }; +} + +sealed class HeartbeatRequestError { + const HeartbeatRequestError({required this.errorType}); + + factory HeartbeatRequestError.fromJson(dynamic json) { + final map = _asJsonMap(json); + final type = map.valueOrNull('error_type'); + final data = map.valueOrNull('error_data'); + switch (type) { + case 'EnableError': + return HeartbeatRequestErrorEnableError.fromJson(data); + default: + return HeartbeatRequestErrorUnknown(rawErrorType: type, data: data); + } + } + + final String errorType; + + JsonMap toJson(); +} + +final class HeartbeatRequestErrorEnableError extends HeartbeatRequestError { + const HeartbeatRequestErrorEnableError(this.value) + : super(errorType: 'EnableError'); + + factory HeartbeatRequestErrorEnableError.fromJson(dynamic json) { + return HeartbeatRequestErrorEnableError(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class HeartbeatRequestErrorUnknown extends HeartbeatRequestError { + const HeartbeatRequestErrorUnknown({required this.data, this.rawErrorType}) + : super(errorType: 'unknown'); + + final String? rawErrorType; + final dynamic data; + + @override + JsonMap toJson() => { + 'error_type': rawErrorType ?? errorType, + 'error_data': data, + }; +} + +sealed class NetworkStreamingRequestError { + const NetworkStreamingRequestError({required this.errorType}); + + factory NetworkStreamingRequestError.fromJson(dynamic json) { + final map = _asJsonMap(json); + final type = map.valueOrNull('error_type'); + final data = map.valueOrNull('error_data'); + switch (type) { + case 'EnableError': + return NetworkStreamingRequestErrorEnableError.fromJson(data); + default: + return NetworkStreamingRequestErrorUnknown( + rawErrorType: type, + data: data, + ); + } + } + + final String errorType; + + JsonMap toJson(); +} + +final class NetworkStreamingRequestErrorEnableError + extends NetworkStreamingRequestError { + const NetworkStreamingRequestErrorEnableError(this.value) + : super(errorType: 'EnableError'); + + factory NetworkStreamingRequestErrorEnableError.fromJson(dynamic json) { + return NetworkStreamingRequestErrorEnableError(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class NetworkStreamingRequestErrorUnknown + extends NetworkStreamingRequestError { + const NetworkStreamingRequestErrorUnknown({ + required this.data, + this.rawErrorType, + }) : super(errorType: 'unknown'); + + final String? rawErrorType; + final dynamic data; + + @override + JsonMap toJson() => { + 'error_type': rawErrorType ?? errorType, + 'error_data': data, + }; +} + +sealed class OrderbookStreamingRequestError { + const OrderbookStreamingRequestError({required this.errorType}); + + factory OrderbookStreamingRequestError.fromJson(dynamic json) { + final map = _asJsonMap(json); + final type = map.valueOrNull('error_type'); + final data = map.valueOrNull('error_data'); + switch (type) { + case 'EnableError': + return OrderbookStreamingRequestErrorEnableError.fromJson(data); + default: + return OrderbookStreamingRequestErrorUnknown( + rawErrorType: type, + data: data, + ); + } + } + + final String errorType; + + JsonMap toJson(); +} + +final class OrderbookStreamingRequestErrorEnableError + extends OrderbookStreamingRequestError { + const OrderbookStreamingRequestErrorEnableError(this.value) + : super(errorType: 'EnableError'); + + factory OrderbookStreamingRequestErrorEnableError.fromJson(dynamic json) { + return OrderbookStreamingRequestErrorEnableError(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class OrderbookStreamingRequestErrorUnknown + extends OrderbookStreamingRequestError { + const OrderbookStreamingRequestErrorUnknown({ + required this.data, + this.rawErrorType, + }) : super(errorType: 'unknown'); + + final String? rawErrorType; + final dynamic data; + + @override + JsonMap toJson() => { + 'error_type': rawErrorType ?? errorType, + 'error_data': data, + }; +} + +sealed class OrderStatusStreamingRequestError { + const OrderStatusStreamingRequestError({required this.errorType}); + + factory OrderStatusStreamingRequestError.fromJson(dynamic json) { + final map = _asJsonMap(json); + final type = map.valueOrNull('error_type'); + final data = map.valueOrNull('error_data'); + switch (type) { + case 'EnableError': + return OrderStatusStreamingRequestErrorEnableError.fromJson(data); + default: + return OrderStatusStreamingRequestErrorUnknown( + rawErrorType: type, + data: data, + ); + } + } + + final String errorType; + + JsonMap toJson(); +} + +final class OrderStatusStreamingRequestErrorEnableError + extends OrderStatusStreamingRequestError { + const OrderStatusStreamingRequestErrorEnableError(this.value) + : super(errorType: 'EnableError'); + + factory OrderStatusStreamingRequestErrorEnableError.fromJson(dynamic json) { + return OrderStatusStreamingRequestErrorEnableError(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class OrderStatusStreamingRequestErrorUnknown + extends OrderStatusStreamingRequestError { + const OrderStatusStreamingRequestErrorUnknown({ + required this.data, + this.rawErrorType, + }) : super(errorType: 'unknown'); + + final String? rawErrorType; + final dynamic data; + + @override + JsonMap toJson() => { + 'error_type': rawErrorType ?? errorType, + 'error_data': data, + }; +} + +sealed class SwapStatusStreamingRequestError { + const SwapStatusStreamingRequestError({required this.errorType}); + + factory SwapStatusStreamingRequestError.fromJson(dynamic json) { + final map = _asJsonMap(json); + final type = map.valueOrNull('error_type'); + final data = map.valueOrNull('error_data'); + switch (type) { + case 'EnableError': + return SwapStatusStreamingRequestErrorEnableError.fromJson(data); + default: + return SwapStatusStreamingRequestErrorUnknown( + rawErrorType: type, + data: data, + ); + } + } + + final String errorType; + + JsonMap toJson(); +} + +final class SwapStatusStreamingRequestErrorEnableError + extends SwapStatusStreamingRequestError { + const SwapStatusStreamingRequestErrorEnableError(this.value) + : super(errorType: 'EnableError'); + + factory SwapStatusStreamingRequestErrorEnableError.fromJson(dynamic json) { + return SwapStatusStreamingRequestErrorEnableError(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class SwapStatusStreamingRequestErrorUnknown + extends SwapStatusStreamingRequestError { + const SwapStatusStreamingRequestErrorUnknown({ + required this.data, + this.rawErrorType, + }) : super(errorType: 'unknown'); + + final String? rawErrorType; + final dynamic data; + + @override + JsonMap toJson() => { + 'error_type': rawErrorType ?? errorType, + 'error_data': data, + }; +} + +sealed class TxHistoryStreamingRequestError { + const TxHistoryStreamingRequestError({required this.errorType}); + + factory TxHistoryStreamingRequestError.fromJson(dynamic json) { + final map = _asJsonMap(json); + final type = map.valueOrNull('error_type'); + final data = map.valueOrNull('error_data'); + switch (type) { + case 'EnableError': + return TxHistoryStreamingRequestErrorEnableError.fromJson(data); + case 'CoinNotFound': + return TxHistoryStreamingRequestErrorCoinNotFound.fromJson(); + case 'CoinNotSupported': + return TxHistoryStreamingRequestErrorCoinNotSupported.fromJson(); + case 'Internal': + return TxHistoryStreamingRequestErrorInternal.fromJson(data); + default: + return TxHistoryStreamingRequestErrorUnknown( + rawErrorType: type, + data: data, + ); + } + } + + final String errorType; + + JsonMap toJson(); +} + +final class TxHistoryStreamingRequestErrorEnableError + extends TxHistoryStreamingRequestError { + const TxHistoryStreamingRequestErrorEnableError(this.value) + : super(errorType: 'EnableError'); + + factory TxHistoryStreamingRequestErrorEnableError.fromJson(dynamic json) { + return TxHistoryStreamingRequestErrorEnableError(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class TxHistoryStreamingRequestErrorCoinNotFound + extends TxHistoryStreamingRequestError { + const TxHistoryStreamingRequestErrorCoinNotFound() + : super(errorType: 'CoinNotFound'); + + factory TxHistoryStreamingRequestErrorCoinNotFound.fromJson() => + const TxHistoryStreamingRequestErrorCoinNotFound(); + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': null}; +} + +final class TxHistoryStreamingRequestErrorCoinNotSupported + extends TxHistoryStreamingRequestError { + const TxHistoryStreamingRequestErrorCoinNotSupported() + : super(errorType: 'CoinNotSupported'); + + factory TxHistoryStreamingRequestErrorCoinNotSupported.fromJson() => + const TxHistoryStreamingRequestErrorCoinNotSupported(); + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': null}; +} + +final class TxHistoryStreamingRequestErrorInternal + extends TxHistoryStreamingRequestError { + const TxHistoryStreamingRequestErrorInternal(this.value) + : super(errorType: 'Internal'); + + factory TxHistoryStreamingRequestErrorInternal.fromJson(dynamic json) { + return TxHistoryStreamingRequestErrorInternal(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class TxHistoryStreamingRequestErrorUnknown + extends TxHistoryStreamingRequestError { + const TxHistoryStreamingRequestErrorUnknown({ + required this.data, + this.rawErrorType, + }) : super(errorType: 'unknown'); + + final String? rawErrorType; + final dynamic data; + + @override + JsonMap toJson() => { + 'error_type': rawErrorType ?? errorType, + 'error_data': data, + }; +} + +sealed class WalletConnectRpcError { + const WalletConnectRpcError({required this.errorType}); + + factory WalletConnectRpcError.fromJson(dynamic json) { + final map = _asJsonMap(json); + final type = map.valueOrNull('error_type'); + final data = map.valueOrNull('error_data'); + switch (type) { + case 'InternalError': + return WalletConnectRpcErrorInternalError.fromJson(data); + case 'InitializationError': + return WalletConnectRpcErrorInitializationError.fromJson(data); + case 'SessionRequestError': + return WalletConnectRpcErrorSessionRequestError.fromJson(data); + default: + return WalletConnectRpcErrorUnknown(rawErrorType: type, data: data); + } + } + + final String errorType; + + JsonMap toJson(); +} + +final class WalletConnectRpcErrorInternalError extends WalletConnectRpcError { + const WalletConnectRpcErrorInternalError(this.value) + : super(errorType: 'InternalError'); + + factory WalletConnectRpcErrorInternalError.fromJson(dynamic json) { + return WalletConnectRpcErrorInternalError(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class WalletConnectRpcErrorInitializationError + extends WalletConnectRpcError { + const WalletConnectRpcErrorInitializationError(this.value) + : super(errorType: 'InitializationError'); + + factory WalletConnectRpcErrorInitializationError.fromJson(dynamic json) { + return WalletConnectRpcErrorInitializationError(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class WalletConnectRpcErrorSessionRequestError + extends WalletConnectRpcError { + const WalletConnectRpcErrorSessionRequestError(this.value) + : super(errorType: 'SessionRequestError'); + + factory WalletConnectRpcErrorSessionRequestError.fromJson(dynamic json) { + return WalletConnectRpcErrorSessionRequestError(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class WalletConnectRpcErrorUnknown extends WalletConnectRpcError { + const WalletConnectRpcErrorUnknown({required this.data, this.rawErrorType}) + : super(errorType: 'unknown'); + + final String? rawErrorType; + final dynamic data; + + @override + JsonMap toJson() => { + 'error_type': rawErrorType ?? errorType, + 'error_data': data, + }; +} + +sealed class AnError { + const AnError({required this.errorType}); + + factory AnError.fromJson(dynamic json) { + final map = _asJsonMap(json); + final type = map.valueOrNull('error_type'); + final data = map.valueOrNull('error_data'); + switch (type) { + case 'NotSufficientBalance': + return AnErrorNotSufficientBalance.fromJson(data); + default: + return AnErrorUnknown(rawErrorType: type, data: data); + } + } + + final String errorType; + + JsonMap toJson(); +} + +final class AnErrorNotSufficientBalance extends AnError { + const AnErrorNotSufficientBalance({required this.missing}) + : super(errorType: 'NotSufficientBalance'); + + factory AnErrorNotSufficientBalance.fromJson(dynamic json) { + final map = _asJsonMap(json); + return AnErrorNotSufficientBalance( + missing: _intFromJson(map.value('missing')), + ); + } + + final int missing; + + @override + JsonMap toJson() => { + 'error_type': errorType, + 'error_data': {'missing': missing}, + }; +} + +final class AnErrorUnknown extends AnError { + const AnErrorUnknown({required this.data, this.rawErrorType}) + : super(errorType: 'unknown'); + + final String? rawErrorType; + final dynamic data; + + @override + JsonMap toJson() => { + 'error_type': rawErrorType ?? errorType, + 'error_data': data, + }; +} + +sealed class CancelRpcTaskError { + const CancelRpcTaskError({required this.errorType}); + + factory CancelRpcTaskError.fromJson(dynamic json) { + final map = _asJsonMap(json); + final type = map.valueOrNull('error_type'); + final data = map.valueOrNull('error_data'); + switch (type) { + case 'NoSuchTask': + return CancelRpcTaskErrorNoSuchTask.fromJson(data); + case 'TaskFinished': + return CancelRpcTaskErrorTaskFinished.fromJson(data); + case 'Internal': + return CancelRpcTaskErrorInternal.fromJson(data); + default: + return CancelRpcTaskErrorUnknown(rawErrorType: type, data: data); + } + } + + final String errorType; + + JsonMap toJson(); +} + +final class CancelRpcTaskErrorNoSuchTask extends CancelRpcTaskError { + const CancelRpcTaskErrorNoSuchTask(this.value) + : super(errorType: 'NoSuchTask'); + + factory CancelRpcTaskErrorNoSuchTask.fromJson(dynamic json) { + return CancelRpcTaskErrorNoSuchTask(TaskId.fromJson(json)); + } + + final TaskId value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value.toJson()}; +} + +final class CancelRpcTaskErrorTaskFinished extends CancelRpcTaskError { + const CancelRpcTaskErrorTaskFinished(this.value) + : super(errorType: 'TaskFinished'); + + factory CancelRpcTaskErrorTaskFinished.fromJson(dynamic json) { + return CancelRpcTaskErrorTaskFinished(TaskId.fromJson(json)); + } + + final TaskId value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value.toJson()}; +} + +final class CancelRpcTaskErrorInternal extends CancelRpcTaskError { + const CancelRpcTaskErrorInternal(this.value) : super(errorType: 'Internal'); + + factory CancelRpcTaskErrorInternal.fromJson(dynamic json) { + return CancelRpcTaskErrorInternal(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class CancelRpcTaskErrorUnknown extends CancelRpcTaskError { + const CancelRpcTaskErrorUnknown({required this.data, this.rawErrorType}) + : super(errorType: 'unknown'); + + final String? rawErrorType; + final dynamic data; + + @override + JsonMap toJson() => { + 'error_type': rawErrorType ?? errorType, + 'error_data': data, + }; +} + +sealed class RpcTaskStatusError { + const RpcTaskStatusError({required this.errorType}); + + factory RpcTaskStatusError.fromJson(dynamic json) { + final map = _asJsonMap(json); + final type = map.valueOrNull('error_type'); + final data = map.valueOrNull('error_data'); + switch (type) { + case 'NoSuchTask': + return RpcTaskStatusErrorNoSuchTask.fromJson(data); + case 'Internal': + return RpcTaskStatusErrorInternal.fromJson(data); + default: + return RpcTaskStatusErrorUnknown(rawErrorType: type, data: data); + } + } + + final String errorType; + + JsonMap toJson(); +} + +final class RpcTaskStatusErrorNoSuchTask extends RpcTaskStatusError { + const RpcTaskStatusErrorNoSuchTask(this.value) + : super(errorType: 'NoSuchTask'); + + factory RpcTaskStatusErrorNoSuchTask.fromJson(dynamic json) { + return RpcTaskStatusErrorNoSuchTask(TaskId.fromJson(json)); + } + + final TaskId value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value.toJson()}; +} + +final class RpcTaskStatusErrorInternal extends RpcTaskStatusError { + const RpcTaskStatusErrorInternal(this.value) : super(errorType: 'Internal'); + + factory RpcTaskStatusErrorInternal.fromJson(dynamic json) { + return RpcTaskStatusErrorInternal(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class RpcTaskStatusErrorUnknown extends RpcTaskStatusError { + const RpcTaskStatusErrorUnknown({required this.data, this.rawErrorType}) + : super(errorType: 'unknown'); + + final String? rawErrorType; + final dynamic data; + + @override + JsonMap toJson() => { + 'error_type': rawErrorType ?? errorType, + 'error_data': data, + }; +} + +sealed class RpcTaskUserActionError { + const RpcTaskUserActionError({required this.errorType}); + + factory RpcTaskUserActionError.fromJson(dynamic json) { + final map = _asJsonMap(json); + final type = map.valueOrNull('error_type'); + final data = map.valueOrNull('error_data'); + switch (type) { + case 'NoSuchTask': + return RpcTaskUserActionErrorNoSuchTask.fromJson(data); + case 'Internal': + return RpcTaskUserActionErrorInternal.fromJson(data); + default: + return RpcTaskUserActionErrorUnknown(rawErrorType: type, data: data); + } + } + + final String errorType; + + JsonMap toJson(); +} + +final class RpcTaskUserActionErrorNoSuchTask extends RpcTaskUserActionError { + const RpcTaskUserActionErrorNoSuchTask(this.value) + : super(errorType: 'NoSuchTask'); + + factory RpcTaskUserActionErrorNoSuchTask.fromJson(dynamic json) { + return RpcTaskUserActionErrorNoSuchTask(TaskId.fromJson(json)); + } + + final TaskId value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value.toJson()}; +} + +final class RpcTaskUserActionErrorInternal extends RpcTaskUserActionError { + const RpcTaskUserActionErrorInternal(this.value) + : super(errorType: 'Internal'); + + factory RpcTaskUserActionErrorInternal.fromJson(dynamic json) { + return RpcTaskUserActionErrorInternal(_stringFromJson(json)); + } + + final String value; + + @override + JsonMap toJson() => {'error_type': errorType, 'error_data': value}; +} + +final class RpcTaskUserActionErrorUnknown extends RpcTaskUserActionError { + const RpcTaskUserActionErrorUnknown({required this.data, this.rawErrorType}) + : super(errorType: 'unknown'); + + final String? rawErrorType; + final dynamic data; + + @override + JsonMap toJson() => { + 'error_type': rawErrorType ?? errorType, + 'error_data': data, + }; +} + +sealed class MmRpcException implements Exception { + const MmRpcException({ + required this.errorType, + this.message, + this.path, + this.trace, + }); + + final String errorType; + final String? message; + final String? path; + final String? trace; + + @override + String toString() => + 'MmRpcException(type: $errorType, message: $message, path: $path)'; +} + +sealed class Web3RpcErrorException extends MmRpcException { + const Web3RpcErrorException({ + required super.errorType, + super.message, + super.path, + super.trace, + }); +} + +final class Web3RpcErrorTransportException extends Web3RpcErrorException { + const Web3RpcErrorTransportException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'Transport'); + final String value; +} + +final class Web3RpcErrorInvalidResponseException extends Web3RpcErrorException { + const Web3RpcErrorInvalidResponseException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'InvalidResponse'); + final String value; +} + +final class Web3RpcErrorTimeoutException extends Web3RpcErrorException { + const Web3RpcErrorTimeoutException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'Timeout'); + final String value; +} + +final class Web3RpcErrorInternalException extends Web3RpcErrorException { + const Web3RpcErrorInternalException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'Internal'); + final String value; +} + +final class Web3RpcErrorInvalidGasApiConfigException + extends Web3RpcErrorException { + const Web3RpcErrorInvalidGasApiConfigException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'InvalidGasApiConfig'); + final String value; +} + +final class Web3RpcErrorNftProtocolNotSupportedException + extends Web3RpcErrorException { + const Web3RpcErrorNftProtocolNotSupportedException({ + super.message, + super.path, + super.trace, + }) : super(errorType: 'NftProtocolNotSupported'); +} + +final class Web3RpcErrorNumConversErrorException extends Web3RpcErrorException { + const Web3RpcErrorNumConversErrorException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'NumConversError'); + final String value; +} + +sealed class GetFeeEstimationRequestErrorException extends MmRpcException { + const GetFeeEstimationRequestErrorException({ + required super.errorType, + super.message, + super.path, + super.trace, + }); +} + +final class GetFeeEstimationRequestErrorCoinNotFoundException + extends GetFeeEstimationRequestErrorException { + const GetFeeEstimationRequestErrorCoinNotFoundException({ + super.message, + super.path, + super.trace, + }) : super(errorType: 'CoinNotFound'); +} + +final class GetFeeEstimationRequestErrorInternalException + extends GetFeeEstimationRequestErrorException { + const GetFeeEstimationRequestErrorInternalException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'Internal'); + final String value; +} + +final class GetFeeEstimationRequestErrorCoinNotSupportedException + extends GetFeeEstimationRequestErrorException { + const GetFeeEstimationRequestErrorCoinNotSupportedException({ + super.message, + super.path, + super.trace, + }) : super(errorType: 'CoinNotSupported'); +} + +sealed class EthActivationV2ErrorException extends MmRpcException { + const EthActivationV2ErrorException({ + required super.errorType, + super.message, + super.path, + super.trace, + }); +} + +final class EthActivationV2ErrorInvalidPayloadException + extends EthActivationV2ErrorException { + const EthActivationV2ErrorInvalidPayloadException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'InvalidPayload'); + final String value; +} + +final class EthActivationV2ErrorInvalidSwapContractAddrException + extends EthActivationV2ErrorException { + const EthActivationV2ErrorInvalidSwapContractAddrException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'InvalidSwapContractAddr'); + final String value; +} + +final class EthActivationV2ErrorInvalidFallbackSwapContractException + extends EthActivationV2ErrorException { + const EthActivationV2ErrorInvalidFallbackSwapContractException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'InvalidFallbackSwapContract'); + final String value; +} + +final class EthActivationV2ErrorInvalidPathToAddressException + extends EthActivationV2ErrorException { + const EthActivationV2ErrorInvalidPathToAddressException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'InvalidPathToAddress'); + final String value; +} + +final class EthActivationV2ErrorChainIdNotSetException + extends EthActivationV2ErrorException { + const EthActivationV2ErrorChainIdNotSetException({ + super.message, + super.path, + super.trace, + }) : super(errorType: 'ChainIdNotSet'); +} + +final class EthActivationV2ErrorUnsupportedChainException + extends EthActivationV2ErrorException { + const EthActivationV2ErrorUnsupportedChainException({ + required this.chain, + required this.feature, + super.message, + super.path, + super.trace, + }) : super(errorType: 'UnsupportedChain'); + final String chain; + final String feature; +} + +final class EthActivationV2ErrorActivationFailedException + extends EthActivationV2ErrorException { + const EthActivationV2ErrorActivationFailedException({ + required this.ticker, + required this.error, + super.message, + super.path, + super.trace, + }) : super(errorType: 'ActivationFailed'); + final String ticker; + final String error; +} + +final class EthActivationV2ErrorCouldNotFetchBalanceException + extends EthActivationV2ErrorException { + const EthActivationV2ErrorCouldNotFetchBalanceException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'CouldNotFetchBalance'); + final String value; +} + +final class EthActivationV2ErrorUnreachableNodesException + extends EthActivationV2ErrorException { + const EthActivationV2ErrorUnreachableNodesException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'UnreachableNodes'); + final String value; +} + +final class EthActivationV2ErrorAtLeastOneNodeRequiredException + extends EthActivationV2ErrorException { + const EthActivationV2ErrorAtLeastOneNodeRequiredException({ + super.message, + super.path, + super.trace, + }) : super(errorType: 'AtLeastOneNodeRequired'); +} + +final class EthActivationV2ErrorErrorDeserializingDerivationPathException + extends EthActivationV2ErrorException { + const EthActivationV2ErrorErrorDeserializingDerivationPathException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'ErrorDeserializingDerivationPath'); + final String value; +} + +final class EthActivationV2ErrorPrivKeyPolicyNotAllowedException + extends EthActivationV2ErrorException { + const EthActivationV2ErrorPrivKeyPolicyNotAllowedException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'PrivKeyPolicyNotAllowed'); + final PrivKeyPolicyNotAllowed value; +} + +final class EthActivationV2ErrorFailedSpawningBalanceEventsException + extends EthActivationV2ErrorException { + const EthActivationV2ErrorFailedSpawningBalanceEventsException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'FailedSpawningBalanceEvents'); + final String value; +} + +final class EthActivationV2ErrorHDWalletStorageErrorException + extends EthActivationV2ErrorException { + const EthActivationV2ErrorHDWalletStorageErrorException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'HDWalletStorageError'); + final String value; +} + +final class EthActivationV2ErrorMetamaskErrorException + extends EthActivationV2ErrorException { + const EthActivationV2ErrorMetamaskErrorException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'MetamaskError'); + final MetamaskRpcError value; +} + +final class EthActivationV2ErrorInternalErrorException + extends EthActivationV2ErrorException { + const EthActivationV2ErrorInternalErrorException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'InternalError'); + final String value; +} + +final class EthActivationV2ErrorTransportException + extends EthActivationV2ErrorException { + const EthActivationV2ErrorTransportException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'Transport'); + final String value; +} + +final class EthActivationV2ErrorUnexpectedDerivationMethodException + extends EthActivationV2ErrorException { + const EthActivationV2ErrorUnexpectedDerivationMethodException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'UnexpectedDerivationMethod'); + final UnexpectedDerivationMethod value; +} + +final class EthActivationV2ErrorCoinDoesntSupportTrezorException + extends EthActivationV2ErrorException { + const EthActivationV2ErrorCoinDoesntSupportTrezorException({ + super.message, + super.path, + super.trace, + }) : super(errorType: 'CoinDoesntSupportTrezor'); +} + +final class EthActivationV2ErrorHwContextNotInitializedException + extends EthActivationV2ErrorException { + const EthActivationV2ErrorHwContextNotInitializedException({ + super.message, + super.path, + super.trace, + }) : super(errorType: 'HwContextNotInitialized'); +} + +final class EthActivationV2ErrorTaskTimedOutException + extends EthActivationV2ErrorException { + const EthActivationV2ErrorTaskTimedOutException({ + required this.duration, + super.message, + super.path, + super.trace, + }) : super(errorType: 'TaskTimedOut'); + final Mm2Duration duration; +} + +final class EthActivationV2ErrorHwErrorException + extends EthActivationV2ErrorException { + const EthActivationV2ErrorHwErrorException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'HwError'); + final HwRpcError value; +} + +final class EthActivationV2ErrorInvalidHardwareWalletCallException + extends EthActivationV2ErrorException { + const EthActivationV2ErrorInvalidHardwareWalletCallException({ + super.message, + super.path, + super.trace, + }) : super(errorType: 'InvalidHardwareWalletCall'); +} + +final class EthActivationV2ErrorCustomTokenErrorException + extends EthActivationV2ErrorException { + const EthActivationV2ErrorCustomTokenErrorException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'CustomTokenError'); + final CustomTokenError value; +} + +final class EthActivationV2ErrorWalletConnectErrorException + extends EthActivationV2ErrorException { + const EthActivationV2ErrorWalletConnectErrorException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'WalletConnectError'); + final String value; +} + +sealed class EthTokenActivationErrorException extends MmRpcException { + const EthTokenActivationErrorException({ + required super.errorType, + super.message, + super.path, + super.trace, + }); +} + +final class EthTokenActivationErrorInternalErrorException + extends EthTokenActivationErrorException { + const EthTokenActivationErrorInternalErrorException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'InternalError'); + final String value; +} + +final class EthTokenActivationErrorClientConnectionFailedException + extends EthTokenActivationErrorException { + const EthTokenActivationErrorClientConnectionFailedException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'ClientConnectionFailed'); + final String value; +} + +final class EthTokenActivationErrorCouldNotFetchBalanceException + extends EthTokenActivationErrorException { + const EthTokenActivationErrorCouldNotFetchBalanceException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'CouldNotFetchBalance'); + final String value; +} + +final class EthTokenActivationErrorInvalidPayloadException + extends EthTokenActivationErrorException { + const EthTokenActivationErrorInvalidPayloadException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'InvalidPayload'); + final String value; +} + +final class EthTokenActivationErrorTransportException + extends EthTokenActivationErrorException { + const EthTokenActivationErrorTransportException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'Transport'); + final String value; +} + +final class EthTokenActivationErrorUnexpectedDerivationMethodException + extends EthTokenActivationErrorException { + const EthTokenActivationErrorUnexpectedDerivationMethodException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'UnexpectedDerivationMethod'); + final UnexpectedDerivationMethod value; +} + +final class EthTokenActivationErrorPrivKeyPolicyNotAllowedException + extends EthTokenActivationErrorException { + const EthTokenActivationErrorPrivKeyPolicyNotAllowedException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'PrivKeyPolicyNotAllowed'); + final PrivKeyPolicyNotAllowed value; +} + +final class EthTokenActivationErrorCustomTokenErrorException + extends EthTokenActivationErrorException { + const EthTokenActivationErrorCustomTokenErrorException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'CustomTokenError'); + final CustomTokenError value; +} + +sealed class AddressDerivingErrorException extends MmRpcException { + const AddressDerivingErrorException({ + required super.errorType, + super.message, + super.path, + super.trace, + }); +} + +final class AddressDerivingErrorInvalidBip44ChainException + extends AddressDerivingErrorException { + const AddressDerivingErrorInvalidBip44ChainException({ + required this.chain, + super.message, + super.path, + super.trace, + }) : super(errorType: 'InvalidBip44Chain'); + final Bip44Chain chain; +} + +final class AddressDerivingErrorBip32ErrorException + extends AddressDerivingErrorException { + const AddressDerivingErrorBip32ErrorException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'Bip32Error'); + final String value; +} + +final class AddressDerivingErrorInternalException + extends AddressDerivingErrorException { + const AddressDerivingErrorInternalException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'Internal'); + final String value; +} + +sealed class EnableLightningErrorException extends MmRpcException { + const EnableLightningErrorException({ + required super.errorType, + super.message, + super.path, + super.trace, + }); +} + +final class EnableLightningErrorInvalidRequestException + extends EnableLightningErrorException { + const EnableLightningErrorInvalidRequestException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'InvalidRequest'); + final String value; +} + +final class EnableLightningErrorInvalidConfigurationException + extends EnableLightningErrorException { + const EnableLightningErrorInvalidConfigurationException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'InvalidConfiguration'); + final String value; +} + +final class EnableLightningErrorUnsupportedModeException + extends EnableLightningErrorException { + const EnableLightningErrorUnsupportedModeException( + this.value0, + this.value1, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'UnsupportedMode'); + final String value0; + final String value1; +} + +final class EnableLightningErrorIOErrorException + extends EnableLightningErrorException { + const EnableLightningErrorIOErrorException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'IOError'); + final String value; +} + +final class EnableLightningErrorInvalidAddressException + extends EnableLightningErrorException { + const EnableLightningErrorInvalidAddressException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'InvalidAddress'); + final String value; +} + +final class EnableLightningErrorInvalidPathException + extends EnableLightningErrorException { + const EnableLightningErrorInvalidPathException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'InvalidPath'); + final String value; +} + +final class EnableLightningErrorPrivKeyPolicyNotAllowedException + extends EnableLightningErrorException { + const EnableLightningErrorPrivKeyPolicyNotAllowedException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'PrivKeyPolicyNotAllowed'); + final PrivKeyPolicyNotAllowed value; +} + +final class EnableLightningErrorSystemTimeErrorException + extends EnableLightningErrorException { + const EnableLightningErrorSystemTimeErrorException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'SystemTimeError'); + final String value; +} + +final class EnableLightningErrorRpcErrorException + extends EnableLightningErrorException { + const EnableLightningErrorRpcErrorException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'RpcError'); + final String value; +} + +final class EnableLightningErrorDbErrorException + extends EnableLightningErrorException { + const EnableLightningErrorDbErrorException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'DbError'); + final String value; +} + +final class EnableLightningErrorRpcTaskErrorException + extends EnableLightningErrorException { + const EnableLightningErrorRpcTaskErrorException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'RpcTaskError'); + final String value; +} + +final class EnableLightningErrorConnectToNodeErrorException + extends EnableLightningErrorException { + const EnableLightningErrorConnectToNodeErrorException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'ConnectToNodeError'); + final String value; +} + +final class EnableLightningErrorInternalException + extends EnableLightningErrorException { + const EnableLightningErrorInternalException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'Internal'); + final String value; +} + +sealed class BalanceErrorException extends MmRpcException { + const BalanceErrorException({ + required super.errorType, + super.message, + super.path, + super.trace, + }); +} + +final class BalanceErrorTransportException extends BalanceErrorException { + const BalanceErrorTransportException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'Transport'); + final String value; +} + +final class BalanceErrorInvalidResponseException extends BalanceErrorException { + const BalanceErrorInvalidResponseException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'InvalidResponse'); + final String value; +} + +final class BalanceErrorUnexpectedDerivationMethodException + extends BalanceErrorException { + const BalanceErrorUnexpectedDerivationMethodException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'UnexpectedDerivationMethod'); + final UnexpectedDerivationMethod value; +} + +final class BalanceErrorWalletStorageErrorException + extends BalanceErrorException { + const BalanceErrorWalletStorageErrorException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'WalletStorageError'); + final String value; +} + +final class BalanceErrorInternalException extends BalanceErrorException { + const BalanceErrorInternalException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'Internal'); + final String value; +} + +sealed class DelegationErrorException extends MmRpcException { + const DelegationErrorException({ + required super.errorType, + super.message, + super.path, + super.trace, + }); +} + +final class DelegationErrorNotSufficientBalanceException + extends DelegationErrorException { + const DelegationErrorNotSufficientBalanceException({ + required this.coin, + required this.available, + required this.required, + super.message, + super.path, + super.trace, + }) : super(errorType: 'NotSufficientBalance'); + final String coin; + final BigDecimal available; + final BigDecimal required; +} + +final class DelegationErrorAmountTooLowException + extends DelegationErrorException { + const DelegationErrorAmountTooLowException({ + required this.amount, + required this.threshold, + super.message, + super.path, + super.trace, + }) : super(errorType: 'AmountTooLow'); + final BigDecimal amount; + final BigDecimal threshold; +} + +final class DelegationErrorCoinDoesntSupportDelegationException + extends DelegationErrorException { + const DelegationErrorCoinDoesntSupportDelegationException({ + required this.coin, + super.message, + super.path, + super.trace, + }) : super(errorType: 'CoinDoesntSupportDelegation'); + final String coin; +} + +final class DelegationErrorNoSuchCoinException + extends DelegationErrorException { + const DelegationErrorNoSuchCoinException({ + required this.coin, + super.message, + super.path, + super.trace, + }) : super(errorType: 'NoSuchCoin'); + final String coin; +} + +final class DelegationErrorCanNotUndelegateException + extends DelegationErrorException { + const DelegationErrorCanNotUndelegateException({ + required this.delegatorAddr, + required this.validatorAddr, + super.message, + super.path, + super.trace, + }) : super(errorType: 'CanNotUndelegate'); + final String delegatorAddr; + final String validatorAddr; +} + +final class DelegationErrorTooMuchToUndelegateException + extends DelegationErrorException { + const DelegationErrorTooMuchToUndelegateException({ + required this.available, + required this.requested, + super.message, + super.path, + super.trace, + }) : super(errorType: 'TooMuchToUndelegate'); + final BigDecimal available; + final BigDecimal requested; +} + +final class DelegationErrorUnprofitableRewardException + extends DelegationErrorException { + const DelegationErrorUnprofitableRewardException({ + required this.reward, + required this.fee, + super.message, + super.path, + super.trace, + }) : super(errorType: 'UnprofitableReward'); + final BigDecimal reward; + final BigDecimal fee; +} + +final class DelegationErrorNothingToClaimException + extends DelegationErrorException { + const DelegationErrorNothingToClaimException({ + required this.coin, + super.message, + super.path, + super.trace, + }) : super(errorType: 'NothingToClaim'); + final String coin; +} + +final class DelegationErrorCannotInteractWithSmartContractException + extends DelegationErrorException { + const DelegationErrorCannotInteractWithSmartContractException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'CannotInteractWithSmartContract'); + final String value; +} + +final class DelegationErrorAddressErrorException + extends DelegationErrorException { + const DelegationErrorAddressErrorException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'AddressError'); + final String value; +} + +final class DelegationErrorAlreadyDelegatingException + extends DelegationErrorException { + const DelegationErrorAlreadyDelegatingException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'AlreadyDelegating'); + final String value; +} + +final class DelegationErrorDelegationOpsNotSupportedException + extends DelegationErrorException { + const DelegationErrorDelegationOpsNotSupportedException({ + required this.reason, + super.message, + super.path, + super.trace, + }) : super(errorType: 'DelegationOpsNotSupported'); + final String reason; +} + +final class DelegationErrorTransportException extends DelegationErrorException { + const DelegationErrorTransportException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'Transport'); + final String value; +} + +final class DelegationErrorInvalidPayloadException + extends DelegationErrorException { + const DelegationErrorInvalidPayloadException({ + required this.reason, + super.message, + super.path, + super.trace, + }) : super(errorType: 'InvalidPayload'); + final String reason; +} + +final class DelegationErrorInternalErrorException + extends DelegationErrorException { + const DelegationErrorInternalErrorException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'InternalError'); + final String value; +} + +sealed class GetMyAddressErrorException extends MmRpcException { + const GetMyAddressErrorException({ + required super.errorType, + super.message, + super.path, + super.trace, + }); +} + +final class GetMyAddressErrorCoinsConfCheckErrorException + extends GetMyAddressErrorException { + const GetMyAddressErrorCoinsConfCheckErrorException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'CoinsConfCheckError'); + final String value; +} + +final class GetMyAddressErrorCoinIsNotSupportedException + extends GetMyAddressErrorException { + const GetMyAddressErrorCoinIsNotSupportedException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'CoinIsNotSupported'); + final String value; +} + +final class GetMyAddressErrorInternalException + extends GetMyAddressErrorException { + const GetMyAddressErrorInternalException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'Internal'); + final String value; +} + +final class GetMyAddressErrorInvalidRequestException + extends GetMyAddressErrorException { + const GetMyAddressErrorInvalidRequestException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'InvalidRequest'); + final String value; +} + +final class GetMyAddressErrorGetEthAddressErrorException + extends GetMyAddressErrorException { + const GetMyAddressErrorGetEthAddressErrorException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'GetEthAddressError'); + final GetEthAddressError value; +} + +sealed class RawTransactionErrorException extends MmRpcException { + const RawTransactionErrorException({ + required super.errorType, + super.message, + super.path, + super.trace, + }); +} + +final class RawTransactionErrorNoSuchCoinException + extends RawTransactionErrorException { + const RawTransactionErrorNoSuchCoinException({ + required this.coin, + super.message, + super.path, + super.trace, + }) : super(errorType: 'NoSuchCoin'); + final String coin; +} + +final class RawTransactionErrorInvalidHashErrorException + extends RawTransactionErrorException { + const RawTransactionErrorInvalidHashErrorException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'InvalidHashError'); + final String value; +} + +final class RawTransactionErrorTransportException + extends RawTransactionErrorException { + const RawTransactionErrorTransportException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'Transport'); + final String value; +} + +final class RawTransactionErrorHashNotExistException + extends RawTransactionErrorException { + const RawTransactionErrorHashNotExistException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'HashNotExist'); + final String value; +} + +final class RawTransactionErrorInternalErrorException + extends RawTransactionErrorException { + const RawTransactionErrorInternalErrorException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'InternalError'); + final String value; +} + +final class RawTransactionErrorDecodeErrorException + extends RawTransactionErrorException { + const RawTransactionErrorDecodeErrorException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'DecodeError'); + final String value; +} + +final class RawTransactionErrorInvalidParamException + extends RawTransactionErrorException { + const RawTransactionErrorInvalidParamException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'InvalidParam'); + final String value; +} + +final class RawTransactionErrorNonExistentPrevOutputErrorException + extends RawTransactionErrorException { + const RawTransactionErrorNonExistentPrevOutputErrorException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'NonExistentPrevOutputError'); + final String value; +} + +final class RawTransactionErrorSigningErrorException + extends RawTransactionErrorException { + const RawTransactionErrorSigningErrorException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'SigningError'); + final String value; +} + +final class RawTransactionErrorNotImplementedException + extends RawTransactionErrorException { + const RawTransactionErrorNotImplementedException({ + required this.coin, + super.message, + super.path, + super.trace, + }) : super(errorType: 'NotImplemented'); + final String coin; +} + +final class RawTransactionErrorTransactionErrorException + extends RawTransactionErrorException { + const RawTransactionErrorTransactionErrorException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'TransactionError'); + final String value; +} + +sealed class SignatureErrorException extends MmRpcException { + const SignatureErrorException({ + required super.errorType, + super.message, + super.path, + super.trace, + }); +} + +final class SignatureErrorInvalidRequestException + extends SignatureErrorException { + const SignatureErrorInvalidRequestException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'InvalidRequest'); + final String value; +} + +final class SignatureErrorInternalErrorException + extends SignatureErrorException { + const SignatureErrorInternalErrorException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'InternalError'); + final String value; +} + +final class SignatureErrorCoinIsNotFoundException + extends SignatureErrorException { + const SignatureErrorCoinIsNotFoundException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'CoinIsNotFound'); + final String value; +} + +final class SignatureErrorPrefixNotFoundException + extends SignatureErrorException { + const SignatureErrorPrefixNotFoundException({ + super.message, + super.path, + super.trace, + }) : super(errorType: 'PrefixNotFound'); +} + +sealed class StakingInfoErrorException extends MmRpcException { + const StakingInfoErrorException({ + required super.errorType, + super.message, + super.path, + super.trace, + }); +} + +final class StakingInfoErrorNoSuchCoinException + extends StakingInfoErrorException { + const StakingInfoErrorNoSuchCoinException({ + required this.coin, + super.message, + super.path, + super.trace, + }) : super(errorType: 'NoSuchCoin'); + final String coin; +} + +final class StakingInfoErrorUnexpectedDerivationMethodException + extends StakingInfoErrorException { + const StakingInfoErrorUnexpectedDerivationMethodException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'UnexpectedDerivationMethod'); + final String value; +} + +final class StakingInfoErrorInvalidPayloadException + extends StakingInfoErrorException { + const StakingInfoErrorInvalidPayloadException({ + required this.reason, + super.message, + super.path, + super.trace, + }) : super(errorType: 'InvalidPayload'); + final String reason; +} + +final class StakingInfoErrorTransportException + extends StakingInfoErrorException { + const StakingInfoErrorTransportException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'Transport'); + final String value; +} + +final class StakingInfoErrorInternalException + extends StakingInfoErrorException { + const StakingInfoErrorInternalException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'Internal'); + final String value; +} + +sealed class SwapTxFeePolicyErrorException extends MmRpcException { + const SwapTxFeePolicyErrorException({ + required super.errorType, + super.message, + super.path, + super.trace, + }); +} + +final class SwapTxFeePolicyErrorNoSuchCoinException + extends SwapTxFeePolicyErrorException { + const SwapTxFeePolicyErrorNoSuchCoinException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'NoSuchCoin'); + final String value; +} + +final class SwapTxFeePolicyErrorNotSupportedException + extends SwapTxFeePolicyErrorException { + const SwapTxFeePolicyErrorNotSupportedException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'NotSupported'); + final String value; +} + +sealed class VerificationErrorException extends MmRpcException { + const VerificationErrorException({ + required super.errorType, + super.message, + super.path, + super.trace, + }); +} + +final class VerificationErrorInvalidRequestException + extends VerificationErrorException { + const VerificationErrorInvalidRequestException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'InvalidRequest'); + final String value; +} + +final class VerificationErrorInternalErrorException + extends VerificationErrorException { + const VerificationErrorInternalErrorException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'InternalError'); + final String value; +} + +final class VerificationErrorSignatureDecodingErrorException + extends VerificationErrorException { + const VerificationErrorSignatureDecodingErrorException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'SignatureDecodingError'); + final String value; +} + +final class VerificationErrorAddressDecodingErrorException + extends VerificationErrorException { + const VerificationErrorAddressDecodingErrorException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'AddressDecodingError'); + final String value; +} + +final class VerificationErrorCoinIsNotFoundException + extends VerificationErrorException { + const VerificationErrorCoinIsNotFoundException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'CoinIsNotFound'); + final String value; +} + +final class VerificationErrorPrefixNotFoundException + extends VerificationErrorException { + const VerificationErrorPrefixNotFoundException({ + super.message, + super.path, + super.trace, + }) : super(errorType: 'PrefixNotFound'); +} + +sealed class WithdrawErrorException extends MmRpcException { + const WithdrawErrorException({ + required super.errorType, + super.message, + super.path, + super.trace, + }); +} + +final class WithdrawErrorCoinDoesntSupportInitWithdrawException + extends WithdrawErrorException { + const WithdrawErrorCoinDoesntSupportInitWithdrawException({ + required this.coin, + super.message, + super.path, + super.trace, + }) : super(errorType: 'CoinDoesntSupportInitWithdraw'); + final String coin; +} + +final class WithdrawErrorNotSufficientBalanceException + extends WithdrawErrorException { + const WithdrawErrorNotSufficientBalanceException({ + required this.coin, + required this.available, + required this.required, + super.message, + super.path, + super.trace, + }) : super(errorType: 'NotSufficientBalance'); + final String coin; + final BigDecimal available; + final BigDecimal required; +} + +final class WithdrawErrorNotSufficientPlatformBalanceForFeeException + extends WithdrawErrorException { + const WithdrawErrorNotSufficientPlatformBalanceForFeeException({ + required this.coin, + required this.available, + required this.required, + super.message, + super.path, + super.trace, + }) : super(errorType: 'NotSufficientPlatformBalanceForFee'); + final String coin; + final BigDecimal available; + final BigDecimal required; +} + +final class WithdrawErrorZeroBalanceToWithdrawMaxException + extends WithdrawErrorException { + const WithdrawErrorZeroBalanceToWithdrawMaxException({ + super.message, + super.path, + super.trace, + }) : super(errorType: 'ZeroBalanceToWithdrawMax'); +} + +final class WithdrawErrorAmountTooLowException extends WithdrawErrorException { + const WithdrawErrorAmountTooLowException({ + required this.amount, + required this.threshold, + super.message, + super.path, + super.trace, + }) : super(errorType: 'AmountTooLow'); + final BigDecimal amount; + final BigDecimal threshold; +} + +final class WithdrawErrorInvalidAddressException + extends WithdrawErrorException { + const WithdrawErrorInvalidAddressException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'InvalidAddress'); + final String value; +} + +final class WithdrawErrorInvalidFeePolicyException + extends WithdrawErrorException { + const WithdrawErrorInvalidFeePolicyException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'InvalidFeePolicy'); + final String value; +} + +final class WithdrawErrorInvalidFeeException extends WithdrawErrorException { + const WithdrawErrorInvalidFeeException({ + required this.reason, + this.details, + super.message, + super.path, + super.trace, + }) : super(errorType: 'InvalidFee'); + final String reason; + final JsonValue? details; +} + +final class WithdrawErrorInvalidMemoException extends WithdrawErrorException { + const WithdrawErrorInvalidMemoException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'InvalidMemo'); + final String value; +} + +final class WithdrawErrorNoSuchCoinException extends WithdrawErrorException { + const WithdrawErrorNoSuchCoinException({ + required this.coin, + super.message, + super.path, + super.trace, + }) : super(errorType: 'NoSuchCoin'); + final String coin; +} + +final class WithdrawErrorTimeoutException extends WithdrawErrorException { + const WithdrawErrorTimeoutException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'Timeout'); + final Mm2Duration value; +} + +final class WithdrawErrorFromAddressNotFoundException + extends WithdrawErrorException { + const WithdrawErrorFromAddressNotFoundException({ + super.message, + super.path, + super.trace, + }) : super(errorType: 'FromAddressNotFound'); +} + +final class WithdrawErrorUnexpectedFromAddressException + extends WithdrawErrorException { + const WithdrawErrorUnexpectedFromAddressException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'UnexpectedFromAddress'); + final String value; +} + +final class WithdrawErrorUnknownAccountException + extends WithdrawErrorException { + const WithdrawErrorUnknownAccountException({ + required this.accountId, + super.message, + super.path, + super.trace, + }) : super(errorType: 'UnknownAccount'); + final int accountId; +} + +final class WithdrawErrorUnexpectedUserActionException + extends WithdrawErrorException { + const WithdrawErrorUnexpectedUserActionException({ + required this.expected, + super.message, + super.path, + super.trace, + }) : super(errorType: 'UnexpectedUserAction'); + final String expected; +} + +final class WithdrawErrorHwErrorException extends WithdrawErrorException { + const WithdrawErrorHwErrorException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'HwError'); + final HwRpcError value; +} + +final class WithdrawErrorBroadcastExpectedException + extends WithdrawErrorException { + const WithdrawErrorBroadcastExpectedException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'BroadcastExpected'); + final String value; +} + +final class WithdrawErrorTransportException extends WithdrawErrorException { + const WithdrawErrorTransportException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'Transport'); + final String value; +} + +final class WithdrawErrorInternalErrorException extends WithdrawErrorException { + const WithdrawErrorInternalErrorException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'InternalError'); + final String value; +} + +final class WithdrawErrorUnsupportedErrorException + extends WithdrawErrorException { + const WithdrawErrorUnsupportedErrorException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'UnsupportedError'); + final String value; +} + +final class WithdrawErrorCoinDoesntSupportNftWithdrawException + extends WithdrawErrorException { + const WithdrawErrorCoinDoesntSupportNftWithdrawException({ + required this.coin, + super.message, + super.path, + super.trace, + }) : super(errorType: 'CoinDoesntSupportNftWithdraw'); + final String coin; +} + +final class WithdrawErrorContractTypeDoesntSupportNftWithdrawingException + extends WithdrawErrorException { + const WithdrawErrorContractTypeDoesntSupportNftWithdrawingException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'ContractTypeDoesntSupportNftWithdrawing'); + final String value; +} + +final class WithdrawErrorActionNotAllowedException + extends WithdrawErrorException { + const WithdrawErrorActionNotAllowedException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'ActionNotAllowed'); + final String value; +} + +final class WithdrawErrorGetNftInfoErrorException + extends WithdrawErrorException { + const WithdrawErrorGetNftInfoErrorException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'GetNftInfoError'); + final GetNftInfoError value; +} + +final class WithdrawErrorNotEnoughNftsAmountException + extends WithdrawErrorException { + const WithdrawErrorNotEnoughNftsAmountException({ + required this.tokenAddress, + required this.tokenId, + required this.available, + required this.required, + super.message, + super.path, + super.trace, + }) : super(errorType: 'NotEnoughNftsAmount'); + final String tokenAddress; + final String tokenId; + final BigUint available; + final BigUint required; +} + +final class WithdrawErrorDbErrorException extends WithdrawErrorException { + const WithdrawErrorDbErrorException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'DbError'); + final String value; +} + +final class WithdrawErrorMyAddressNotNftOwnerException + extends WithdrawErrorException { + const WithdrawErrorMyAddressNotNftOwnerException({ + required this.myAddress, + required this.tokenOwner, + super.message, + super.path, + super.trace, + }) : super(errorType: 'MyAddressNotNftOwner'); + final String myAddress; + final String tokenOwner; +} + +final class WithdrawErrorNftProtocolNotSupportedException + extends WithdrawErrorException { + const WithdrawErrorNftProtocolNotSupportedException({ + super.message, + super.path, + super.trace, + }) : super(errorType: 'NftProtocolNotSupported'); +} + +final class WithdrawErrorNoChainIdSetException extends WithdrawErrorException { + const WithdrawErrorNoChainIdSetException({ + required this.coin, + super.message, + super.path, + super.trace, + }) : super(errorType: 'NoChainIdSet'); + final String coin; +} + +final class WithdrawErrorSigningErrorException extends WithdrawErrorException { + const WithdrawErrorSigningErrorException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'SigningError'); + final String value; +} + +final class WithdrawErrorTxTypeNotSupportedException + extends WithdrawErrorException { + const WithdrawErrorTxTypeNotSupportedException({ + super.message, + super.path, + super.trace, + }) : super(errorType: 'TxTypeNotSupported'); +} + +final class WithdrawErrorIBCErrorException extends WithdrawErrorException { + const WithdrawErrorIBCErrorException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'IBCError'); + final TendermintIBCError value; +} + +sealed class MyTxHistoryErrorV2Exception extends MmRpcException { + const MyTxHistoryErrorV2Exception({ + required super.errorType, + super.message, + super.path, + super.trace, + }); +} + +final class MyTxHistoryErrorV2CoinIsNotActiveException + extends MyTxHistoryErrorV2Exception { + const MyTxHistoryErrorV2CoinIsNotActiveException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'CoinIsNotActive'); + final String value; +} + +final class MyTxHistoryErrorV2InvalidTargetException + extends MyTxHistoryErrorV2Exception { + const MyTxHistoryErrorV2InvalidTargetException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'InvalidTarget'); + final String value; +} + +final class MyTxHistoryErrorV2StorageIsNotInitializedException + extends MyTxHistoryErrorV2Exception { + const MyTxHistoryErrorV2StorageIsNotInitializedException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'StorageIsNotInitialized'); + final String value; +} + +final class MyTxHistoryErrorV2StorageErrorException + extends MyTxHistoryErrorV2Exception { + const MyTxHistoryErrorV2StorageErrorException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'StorageError'); + final String value; +} + +final class MyTxHistoryErrorV2RpcErrorException + extends MyTxHistoryErrorV2Exception { + const MyTxHistoryErrorV2RpcErrorException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'RpcError'); + final String value; +} + +final class MyTxHistoryErrorV2NotSupportedForException + extends MyTxHistoryErrorV2Exception { + const MyTxHistoryErrorV2NotSupportedForException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'NotSupportedFor'); + final String value; +} + +final class MyTxHistoryErrorV2InternalException + extends MyTxHistoryErrorV2Exception { + const MyTxHistoryErrorV2InternalException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'Internal'); + final String value; +} + +sealed class ClearNftDbErrorException extends MmRpcException { + const ClearNftDbErrorException({ + required super.errorType, + super.message, + super.path, + super.trace, + }); +} + +final class ClearNftDbErrorDbErrorException extends ClearNftDbErrorException { + const ClearNftDbErrorDbErrorException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'DbError'); + final String value; +} + +final class ClearNftDbErrorInternalException extends ClearNftDbErrorException { + const ClearNftDbErrorInternalException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'Internal'); + final String value; +} + +final class ClearNftDbErrorInvalidRequestException + extends ClearNftDbErrorException { + const ClearNftDbErrorInvalidRequestException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'InvalidRequest'); + final String value; +} + +sealed class GetNftInfoErrorException extends MmRpcException { + const GetNftInfoErrorException({ + required super.errorType, + super.message, + super.path, + super.trace, + }); +} + +final class GetNftInfoErrorInvalidRequestException + extends GetNftInfoErrorException { + const GetNftInfoErrorInvalidRequestException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'InvalidRequest'); + final String value; +} + +final class GetNftInfoErrorTransportException extends GetNftInfoErrorException { + const GetNftInfoErrorTransportException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'Transport'); + final String value; +} + +final class GetNftInfoErrorInvalidResponseException + extends GetNftInfoErrorException { + const GetNftInfoErrorInvalidResponseException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'InvalidResponse'); + final String value; +} + +final class GetNftInfoErrorInternalException extends GetNftInfoErrorException { + const GetNftInfoErrorInternalException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'Internal'); + final String value; +} + +final class GetNftInfoErrorGetEthAddressErrorException + extends GetNftInfoErrorException { + const GetNftInfoErrorGetEthAddressErrorException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'GetEthAddressError'); + final GetEthAddressError value; +} + +final class GetNftInfoErrorTokenNotFoundInWalletException + extends GetNftInfoErrorException { + const GetNftInfoErrorTokenNotFoundInWalletException({ + required this.tokenAddress, + required this.tokenId, + super.message, + super.path, + super.trace, + }) : super(errorType: 'TokenNotFoundInWallet'); + final String tokenAddress; + final String tokenId; +} + +final class GetNftInfoErrorDbErrorException extends GetNftInfoErrorException { + const GetNftInfoErrorDbErrorException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'DbError'); + final String value; +} + +final class GetNftInfoErrorParseRfc3339ErrException + extends GetNftInfoErrorException { + const GetNftInfoErrorParseRfc3339ErrException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'ParseRfc3339Err'); + final ParseRfc3339Err value; +} + +final class GetNftInfoErrorContractTypeIsNullException + extends GetNftInfoErrorException { + const GetNftInfoErrorContractTypeIsNullException({ + super.message, + super.path, + super.trace, + }) : super(errorType: 'ContractTypeIsNull'); +} + +final class GetNftInfoErrorProtectFromSpamErrorException + extends GetNftInfoErrorException { + const GetNftInfoErrorProtectFromSpamErrorException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'ProtectFromSpamError'); + final ProtectFromSpamError value; +} + +final class GetNftInfoErrorTransferConfirmationsErrorException + extends GetNftInfoErrorException { + const GetNftInfoErrorTransferConfirmationsErrorException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'TransferConfirmationsError'); + final TransferConfirmationsError value; +} + +final class GetNftInfoErrorNumConversErrorException + extends GetNftInfoErrorException { + const GetNftInfoErrorNumConversErrorException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'NumConversError'); + final String value; +} + +sealed class UpdateNftErrorException extends MmRpcException { + const UpdateNftErrorException({ + required super.errorType, + super.message, + super.path, + super.trace, + }); +} + +final class UpdateNftErrorDbErrorException extends UpdateNftErrorException { + const UpdateNftErrorDbErrorException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'DbError'); + final String value; +} + +final class UpdateNftErrorInternalException extends UpdateNftErrorException { + const UpdateNftErrorInternalException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'Internal'); + final String value; +} + +final class UpdateNftErrorGetNftInfoErrorException + extends UpdateNftErrorException { + const UpdateNftErrorGetNftInfoErrorException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'GetNftInfoError'); + final GetNftInfoError value; +} + +final class UpdateNftErrorGetMyAddressErrorException + extends UpdateNftErrorException { + const UpdateNftErrorGetMyAddressErrorException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'GetMyAddressError'); + final GetMyAddressError value; +} + +final class UpdateNftErrorTokenNotFoundInWalletException + extends UpdateNftErrorException { + const UpdateNftErrorTokenNotFoundInWalletException({ + required this.tokenAddress, + required this.tokenId, + super.message, + super.path, + super.trace, + }) : super(errorType: 'TokenNotFoundInWallet'); + final String tokenAddress; + final String tokenId; +} + +final class UpdateNftErrorInsufficientAmountInCacheException + extends UpdateNftErrorException { + const UpdateNftErrorInsufficientAmountInCacheException({ + required this.amountList, + required this.amountHistory, + super.message, + super.path, + super.trace, + }) : super(errorType: 'InsufficientAmountInCache'); + final String amountList; + final String amountHistory; +} + +final class UpdateNftErrorInvalidBlockOrderException + extends UpdateNftErrorException { + const UpdateNftErrorInvalidBlockOrderException({ + required this.lastScannedBlock, + required this.lastNftBlock, + super.message, + super.path, + super.trace, + }) : super(errorType: 'InvalidBlockOrder'); + final String lastScannedBlock; + final String lastNftBlock; +} + +final class UpdateNftErrorLastScannedBlockNotFoundException + extends UpdateNftErrorException { + const UpdateNftErrorLastScannedBlockNotFoundException({ + required this.lastNftBlock, + super.message, + super.path, + super.trace, + }) : super(errorType: 'LastScannedBlockNotFound'); + final String lastNftBlock; +} + +final class UpdateNftErrorAttemptToReceiveAlreadyOwnedErc721Exception + extends UpdateNftErrorException { + const UpdateNftErrorAttemptToReceiveAlreadyOwnedErc721Exception({ + required this.txHash, + super.message, + super.path, + super.trace, + }) : super(errorType: 'AttemptToReceiveAlreadyOwnedErc721'); + final String txHash; +} + +final class UpdateNftErrorInvalidHexStringException + extends UpdateNftErrorException { + const UpdateNftErrorInvalidHexStringException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'InvalidHexString'); + final String value; +} + +final class UpdateNftErrorUpdateSpamPhishingErrorException + extends UpdateNftErrorException { + const UpdateNftErrorUpdateSpamPhishingErrorException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'UpdateSpamPhishingError'); + final UpdateSpamPhishingError value; +} + +final class UpdateNftErrorGetInfoFromUriErrorException + extends UpdateNftErrorException { + const UpdateNftErrorGetInfoFromUriErrorException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'GetInfoFromUriError'); + final GetInfoFromUriError value; +} + +final class UpdateNftErrorSerdeErrorException extends UpdateNftErrorException { + const UpdateNftErrorSerdeErrorException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'SerdeError'); + final String value; +} + +final class UpdateNftErrorProtectFromSpamErrorException + extends UpdateNftErrorException { + const UpdateNftErrorProtectFromSpamErrorException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'ProtectFromSpamError'); + final ProtectFromSpamError value; +} + +final class UpdateNftErrorNoSuchCoinException extends UpdateNftErrorException { + const UpdateNftErrorNoSuchCoinException({ + required this.coin, + super.message, + super.path, + super.trace, + }) : super(errorType: 'NoSuchCoin'); + final String coin; +} + +final class UpdateNftErrorCoinDoesntSupportNftException + extends UpdateNftErrorException { + const UpdateNftErrorCoinDoesntSupportNftException({ + required this.coin, + super.message, + super.path, + super.trace, + }) : super(errorType: 'CoinDoesntSupportNft'); + final String coin; +} + +final class UpdateNftErrorPrivKeyPolicyNotAllowedException + extends UpdateNftErrorException { + const UpdateNftErrorPrivKeyPolicyNotAllowedException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'PrivKeyPolicyNotAllowed'); + final PrivKeyPolicyNotAllowed value; +} + +final class UpdateNftErrorUnexpectedDerivationMethodException + extends UpdateNftErrorException { + const UpdateNftErrorUnexpectedDerivationMethodException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'UnexpectedDerivationMethod'); + final UnexpectedDerivationMethod value; +} + +sealed class GetCurrentMtpErrorException extends MmRpcException { + const GetCurrentMtpErrorException({ + required super.errorType, + super.message, + super.path, + super.trace, + }); +} + +final class GetCurrentMtpErrorNoSuchCoinException + extends GetCurrentMtpErrorException { + const GetCurrentMtpErrorNoSuchCoinException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'NoSuchCoin'); + final String value; +} + +final class GetCurrentMtpErrorNotSupportedCoinException + extends GetCurrentMtpErrorException { + const GetCurrentMtpErrorNotSupportedCoinException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'NotSupportedCoin'); + final String value; +} + +final class GetCurrentMtpErrorRpcErrorException + extends GetCurrentMtpErrorException { + const GetCurrentMtpErrorRpcErrorException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'RpcError'); + final String value; +} + +sealed class GetEnabledCoinsErrorException extends MmRpcException { + const GetEnabledCoinsErrorException({ + required super.errorType, + super.message, + super.path, + super.trace, + }); +} + +final class GetEnabledCoinsErrorInternalException + extends GetEnabledCoinsErrorException { + const GetEnabledCoinsErrorInternalException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'Internal'); + final String value; +} + +sealed class GetNewAddressRpcErrorException extends MmRpcException { + const GetNewAddressRpcErrorException({ + required super.errorType, + super.message, + super.path, + super.trace, + }); +} + +final class GetNewAddressRpcErrorHwContextNotInitializedException + extends GetNewAddressRpcErrorException { + const GetNewAddressRpcErrorHwContextNotInitializedException({ + super.message, + super.path, + super.trace, + }) : super(errorType: 'HwContextNotInitialized'); +} + +final class GetNewAddressRpcErrorNoSuchCoinException + extends GetNewAddressRpcErrorException { + const GetNewAddressRpcErrorNoSuchCoinException({ + required this.coin, + super.message, + super.path, + super.trace, + }) : super(errorType: 'NoSuchCoin'); + final String coin; +} + +final class GetNewAddressRpcErrorUnexpectedUserActionException + extends GetNewAddressRpcErrorException { + const GetNewAddressRpcErrorUnexpectedUserActionException({ + required this.expected, + super.message, + super.path, + super.trace, + }) : super(errorType: 'UnexpectedUserAction'); + final String expected; +} + +final class GetNewAddressRpcErrorCoinIsActivatedNotWithHDWalletException + extends GetNewAddressRpcErrorException { + const GetNewAddressRpcErrorCoinIsActivatedNotWithHDWalletException({ + super.message, + super.path, + super.trace, + }) : super(errorType: 'CoinIsActivatedNotWithHDWallet'); +} + +final class GetNewAddressRpcErrorUnknownAccountException + extends GetNewAddressRpcErrorException { + const GetNewAddressRpcErrorUnknownAccountException({ + required this.accountId, + super.message, + super.path, + super.trace, + }) : super(errorType: 'UnknownAccount'); + final int accountId; +} + +final class GetNewAddressRpcErrorInvalidBip44ChainException + extends GetNewAddressRpcErrorException { + const GetNewAddressRpcErrorInvalidBip44ChainException({ + required this.chain, + super.message, + super.path, + super.trace, + }) : super(errorType: 'InvalidBip44Chain'); + final Bip44Chain chain; +} + +final class GetNewAddressRpcErrorErrorDerivingAddressException + extends GetNewAddressRpcErrorException { + const GetNewAddressRpcErrorErrorDerivingAddressException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'ErrorDerivingAddress'); + final String value; +} + +final class GetNewAddressRpcErrorAddressLimitReachedException + extends GetNewAddressRpcErrorException { + const GetNewAddressRpcErrorAddressLimitReachedException({ + required this.maxAddressesNumber, + super.message, + super.path, + super.trace, + }) : super(errorType: 'AddressLimitReached'); + final int maxAddressesNumber; +} + +final class GetNewAddressRpcErrorEmptyAddressesLimitReachedException + extends GetNewAddressRpcErrorException { + const GetNewAddressRpcErrorEmptyAddressesLimitReachedException({ + required this.gapLimit, + super.message, + super.path, + super.trace, + }) : super(errorType: 'EmptyAddressesLimitReached'); + final int gapLimit; +} + +final class GetNewAddressRpcErrorRpcInvalidResponseException + extends GetNewAddressRpcErrorException { + const GetNewAddressRpcErrorRpcInvalidResponseException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'RpcInvalidResponse'); + final String value; +} + +final class GetNewAddressRpcErrorWalletStorageErrorException + extends GetNewAddressRpcErrorException { + const GetNewAddressRpcErrorWalletStorageErrorException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'WalletStorageError'); + final String value; +} + +final class GetNewAddressRpcErrorFailedScripthashSubscriptionException + extends GetNewAddressRpcErrorException { + const GetNewAddressRpcErrorFailedScripthashSubscriptionException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'FailedScripthashSubscription'); + final String value; +} + +final class GetNewAddressRpcErrorTimeoutException + extends GetNewAddressRpcErrorException { + const GetNewAddressRpcErrorTimeoutException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'Timeout'); + final Mm2Duration value; +} + +final class GetNewAddressRpcErrorHwErrorException + extends GetNewAddressRpcErrorException { + const GetNewAddressRpcErrorHwErrorException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'HwError'); + final HwRpcError value; +} + +final class GetNewAddressRpcErrorTransportException + extends GetNewAddressRpcErrorException { + const GetNewAddressRpcErrorTransportException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'Transport'); + final String value; +} + +final class GetNewAddressRpcErrorInternalException + extends GetNewAddressRpcErrorException { + const GetNewAddressRpcErrorInternalException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'Internal'); + final String value; +} + +sealed class HDAccountBalanceRpcErrorException extends MmRpcException { + const HDAccountBalanceRpcErrorException({ + required super.errorType, + super.message, + super.path, + super.trace, + }); +} + +final class HDAccountBalanceRpcErrorNoSuchCoinException + extends HDAccountBalanceRpcErrorException { + const HDAccountBalanceRpcErrorNoSuchCoinException({ + required this.coin, + super.message, + super.path, + super.trace, + }) : super(errorType: 'NoSuchCoin'); + final String coin; +} + +final class HDAccountBalanceRpcErrorTimeoutException + extends HDAccountBalanceRpcErrorException { + const HDAccountBalanceRpcErrorTimeoutException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'Timeout'); + final Mm2Duration value; +} + +final class HDAccountBalanceRpcErrorCoinIsActivatedNotWithHDWalletException + extends HDAccountBalanceRpcErrorException { + const HDAccountBalanceRpcErrorCoinIsActivatedNotWithHDWalletException({ + super.message, + super.path, + super.trace, + }) : super(errorType: 'CoinIsActivatedNotWithHDWallet'); +} + +final class HDAccountBalanceRpcErrorUnknownAccountException + extends HDAccountBalanceRpcErrorException { + const HDAccountBalanceRpcErrorUnknownAccountException({ + required this.accountId, + super.message, + super.path, + super.trace, + }) : super(errorType: 'UnknownAccount'); + final int accountId; +} + +final class HDAccountBalanceRpcErrorInvalidBip44ChainException + extends HDAccountBalanceRpcErrorException { + const HDAccountBalanceRpcErrorInvalidBip44ChainException({ + required this.chain, + super.message, + super.path, + super.trace, + }) : super(errorType: 'InvalidBip44Chain'); + final Bip44Chain chain; +} + +final class HDAccountBalanceRpcErrorErrorDerivingAddressException + extends HDAccountBalanceRpcErrorException { + const HDAccountBalanceRpcErrorErrorDerivingAddressException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'ErrorDerivingAddress'); + final String value; +} + +final class HDAccountBalanceRpcErrorWalletStorageErrorException + extends HDAccountBalanceRpcErrorException { + const HDAccountBalanceRpcErrorWalletStorageErrorException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'WalletStorageError'); + final String value; +} + +final class HDAccountBalanceRpcErrorRpcInvalidResponseException + extends HDAccountBalanceRpcErrorException { + const HDAccountBalanceRpcErrorRpcInvalidResponseException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'RpcInvalidResponse'); + final String value; +} + +final class HDAccountBalanceRpcErrorFailedScripthashSubscriptionException + extends HDAccountBalanceRpcErrorException { + const HDAccountBalanceRpcErrorFailedScripthashSubscriptionException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'FailedScripthashSubscription'); + final String value; +} + +final class HDAccountBalanceRpcErrorTransportException + extends HDAccountBalanceRpcErrorException { + const HDAccountBalanceRpcErrorTransportException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'Transport'); + final String value; +} + +final class HDAccountBalanceRpcErrorInternalException + extends HDAccountBalanceRpcErrorException { + const HDAccountBalanceRpcErrorInternalException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'Internal'); + final String value; +} + +sealed class CreateAccountRpcErrorException extends MmRpcException { + const CreateAccountRpcErrorException({ + required super.errorType, + super.message, + super.path, + super.trace, + }); +} + +final class CreateAccountRpcErrorHwContextNotInitializedException + extends CreateAccountRpcErrorException { + const CreateAccountRpcErrorHwContextNotInitializedException({ + super.message, + super.path, + super.trace, + }) : super(errorType: 'HwContextNotInitialized'); +} + +final class CreateAccountRpcErrorNoSuchCoinException + extends CreateAccountRpcErrorException { + const CreateAccountRpcErrorNoSuchCoinException({ + required this.coin, + super.message, + super.path, + super.trace, + }) : super(errorType: 'NoSuchCoin'); + final String coin; +} + +final class CreateAccountRpcErrorUnexpectedUserActionException + extends CreateAccountRpcErrorException { + const CreateAccountRpcErrorUnexpectedUserActionException({ + required this.expected, + super.message, + super.path, + super.trace, + }) : super(errorType: 'UnexpectedUserAction'); + final String expected; +} + +final class CreateAccountRpcErrorTimeoutException + extends CreateAccountRpcErrorException { + const CreateAccountRpcErrorTimeoutException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'Timeout'); + final Mm2Duration value; +} + +final class CreateAccountRpcErrorCoinIsActivatedNotWithHDWalletException + extends CreateAccountRpcErrorException { + const CreateAccountRpcErrorCoinIsActivatedNotWithHDWalletException({ + super.message, + super.path, + super.trace, + }) : super(errorType: 'CoinIsActivatedNotWithHDWallet'); +} + +final class CreateAccountRpcErrorInvalidBip44ChainException + extends CreateAccountRpcErrorException { + const CreateAccountRpcErrorInvalidBip44ChainException({ + required this.chain, + super.message, + super.path, + super.trace, + }) : super(errorType: 'InvalidBip44Chain'); + final Bip44Chain chain; +} + +final class CreateAccountRpcErrorAccountLimitReachedException + extends CreateAccountRpcErrorException { + const CreateAccountRpcErrorAccountLimitReachedException({ + required this.maxAccountsNumber, + super.message, + super.path, + super.trace, + }) : super(errorType: 'AccountLimitReached'); + final int maxAccountsNumber; +} + +final class CreateAccountRpcErrorRpcInvalidResponseException + extends CreateAccountRpcErrorException { + const CreateAccountRpcErrorRpcInvalidResponseException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'RpcInvalidResponse'); + final String value; +} + +final class CreateAccountRpcErrorWalletStorageErrorException + extends CreateAccountRpcErrorException { + const CreateAccountRpcErrorWalletStorageErrorException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'WalletStorageError'); + final String value; +} + +final class CreateAccountRpcErrorHwErrorException + extends CreateAccountRpcErrorException { + const CreateAccountRpcErrorHwErrorException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'HwError'); + final HwRpcError value; +} + +final class CreateAccountRpcErrorTransportException + extends CreateAccountRpcErrorException { + const CreateAccountRpcErrorTransportException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'Transport'); + final String value; +} + +final class CreateAccountRpcErrorInternalException + extends CreateAccountRpcErrorException { + const CreateAccountRpcErrorInternalException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'Internal'); + final String value; +} + +sealed class CloseChannelErrorException extends MmRpcException { + const CloseChannelErrorException({ + required super.errorType, + super.message, + super.path, + super.trace, + }); +} + +final class CloseChannelErrorUnsupportedCoinException + extends CloseChannelErrorException { + const CloseChannelErrorUnsupportedCoinException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'UnsupportedCoin'); + final String value; +} + +final class CloseChannelErrorNoSuchCoinException + extends CloseChannelErrorException { + const CloseChannelErrorNoSuchCoinException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'NoSuchCoin'); + final String value; +} + +final class CloseChannelErrorNoSuchChannelException + extends CloseChannelErrorException { + const CloseChannelErrorNoSuchChannelException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'NoSuchChannel'); + final String value; +} + +final class CloseChannelErrorCloseChannelErrorException + extends CloseChannelErrorException { + const CloseChannelErrorCloseChannelErrorException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'CloseChannelError'); + final String value; +} + +sealed class ConnectToNodeErrorException extends MmRpcException { + const ConnectToNodeErrorException({ + required super.errorType, + super.message, + super.path, + super.trace, + }); +} + +final class ConnectToNodeErrorParseErrorException + extends ConnectToNodeErrorException { + const ConnectToNodeErrorParseErrorException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'ParseError'); + final String value; +} + +final class ConnectToNodeErrorConnectionErrorException + extends ConnectToNodeErrorException { + const ConnectToNodeErrorConnectionErrorException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'ConnectionError'); + final String value; +} + +final class ConnectToNodeErrorIOErrorException + extends ConnectToNodeErrorException { + const ConnectToNodeErrorIOErrorException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'IOError'); + final String value; +} + +final class ConnectToNodeErrorUnsupportedCoinException + extends ConnectToNodeErrorException { + const ConnectToNodeErrorUnsupportedCoinException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'UnsupportedCoin'); + final String value; +} + +final class ConnectToNodeErrorNoSuchCoinException + extends ConnectToNodeErrorException { + const ConnectToNodeErrorNoSuchCoinException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'NoSuchCoin'); + final String value; +} + +sealed class GenerateInvoiceErrorException extends MmRpcException { + const GenerateInvoiceErrorException({ + required super.errorType, + super.message, + super.path, + super.trace, + }); +} + +final class GenerateInvoiceErrorUnsupportedCoinException + extends GenerateInvoiceErrorException { + const GenerateInvoiceErrorUnsupportedCoinException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'UnsupportedCoin'); + final String value; +} + +final class GenerateInvoiceErrorNoSuchCoinException + extends GenerateInvoiceErrorException { + const GenerateInvoiceErrorNoSuchCoinException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'NoSuchCoin'); + final String value; +} + +final class GenerateInvoiceErrorSignOrCreationErrorException + extends GenerateInvoiceErrorException { + const GenerateInvoiceErrorSignOrCreationErrorException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'SignOrCreationError'); + final String value; +} + +final class GenerateInvoiceErrorDbErrorException + extends GenerateInvoiceErrorException { + const GenerateInvoiceErrorDbErrorException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'DbError'); + final String value; +} + +sealed class GetChannelDetailsErrorException extends MmRpcException { + const GetChannelDetailsErrorException({ + required super.errorType, + super.message, + super.path, + super.trace, + }); +} + +final class GetChannelDetailsErrorUnsupportedCoinException + extends GetChannelDetailsErrorException { + const GetChannelDetailsErrorUnsupportedCoinException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'UnsupportedCoin'); + final String value; +} + +final class GetChannelDetailsErrorNoSuchCoinException + extends GetChannelDetailsErrorException { + const GetChannelDetailsErrorNoSuchCoinException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'NoSuchCoin'); + final String value; +} + +final class GetChannelDetailsErrorNoSuchChannelException + extends GetChannelDetailsErrorException { + const GetChannelDetailsErrorNoSuchChannelException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'NoSuchChannel'); + final String value; +} + +final class GetChannelDetailsErrorDbErrorException + extends GetChannelDetailsErrorException { + const GetChannelDetailsErrorDbErrorException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'DbError'); + final String value; +} + +sealed class ClaimableBalancesErrorException extends MmRpcException { + const ClaimableBalancesErrorException({ + required super.errorType, + super.message, + super.path, + super.trace, + }); +} + +final class ClaimableBalancesErrorUnsupportedCoinException + extends ClaimableBalancesErrorException { + const ClaimableBalancesErrorUnsupportedCoinException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'UnsupportedCoin'); + final String value; +} + +final class ClaimableBalancesErrorNoSuchCoinException + extends ClaimableBalancesErrorException { + const ClaimableBalancesErrorNoSuchCoinException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'NoSuchCoin'); + final String value; +} + +sealed class GetPaymentDetailsErrorException extends MmRpcException { + const GetPaymentDetailsErrorException({ + required super.errorType, + super.message, + super.path, + super.trace, + }); +} + +final class GetPaymentDetailsErrorUnsupportedCoinException + extends GetPaymentDetailsErrorException { + const GetPaymentDetailsErrorUnsupportedCoinException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'UnsupportedCoin'); + final String value; +} + +final class GetPaymentDetailsErrorNoSuchCoinException + extends GetPaymentDetailsErrorException { + const GetPaymentDetailsErrorNoSuchCoinException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'NoSuchCoin'); + final String value; +} + +final class GetPaymentDetailsErrorNoSuchPaymentException + extends GetPaymentDetailsErrorException { + const GetPaymentDetailsErrorNoSuchPaymentException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'NoSuchPayment'); + final H256Json value; +} + +final class GetPaymentDetailsErrorDbErrorException + extends GetPaymentDetailsErrorException { + const GetPaymentDetailsErrorDbErrorException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'DbError'); + final String value; +} + +sealed class ListChannelsErrorException extends MmRpcException { + const ListChannelsErrorException({ + required super.errorType, + super.message, + super.path, + super.trace, + }); +} + +final class ListChannelsErrorUnsupportedCoinException + extends ListChannelsErrorException { + const ListChannelsErrorUnsupportedCoinException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'UnsupportedCoin'); + final String value; +} + +final class ListChannelsErrorNoSuchCoinException + extends ListChannelsErrorException { + const ListChannelsErrorNoSuchCoinException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'NoSuchCoin'); + final String value; +} + +final class ListChannelsErrorDbErrorException + extends ListChannelsErrorException { + const ListChannelsErrorDbErrorException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'DbError'); + final String value; +} + +sealed class ListPaymentsErrorException extends MmRpcException { + const ListPaymentsErrorException({ + required super.errorType, + super.message, + super.path, + super.trace, + }); +} + +final class ListPaymentsErrorUnsupportedCoinException + extends ListPaymentsErrorException { + const ListPaymentsErrorUnsupportedCoinException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'UnsupportedCoin'); + final String value; +} + +final class ListPaymentsErrorNoSuchCoinException + extends ListPaymentsErrorException { + const ListPaymentsErrorNoSuchCoinException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'NoSuchCoin'); + final String value; +} + +final class ListPaymentsErrorDbErrorException + extends ListPaymentsErrorException { + const ListPaymentsErrorDbErrorException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'DbError'); + final String value; +} + +sealed class OpenChannelErrorException extends MmRpcException { + const OpenChannelErrorException({ + required super.errorType, + super.message, + super.path, + super.trace, + }); +} + +final class OpenChannelErrorUnsupportedCoinException + extends OpenChannelErrorException { + const OpenChannelErrorUnsupportedCoinException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'UnsupportedCoin'); + final String value; +} + +final class OpenChannelErrorBalanceErrorException + extends OpenChannelErrorException { + const OpenChannelErrorBalanceErrorException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'BalanceError'); + final String value; +} + +final class OpenChannelErrorInvalidPathException + extends OpenChannelErrorException { + const OpenChannelErrorInvalidPathException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'InvalidPath'); + final String value; +} + +final class OpenChannelErrorFailureToOpenChannelException + extends OpenChannelErrorException { + const OpenChannelErrorFailureToOpenChannelException( + this.value0, + this.value1, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'FailureToOpenChannel'); + final String value0; + final String value1; +} + +final class OpenChannelErrorRpcErrorException + extends OpenChannelErrorException { + const OpenChannelErrorRpcErrorException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'RpcError'); + final String value; +} + +final class OpenChannelErrorInternalErrorException + extends OpenChannelErrorException { + const OpenChannelErrorInternalErrorException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'InternalError'); + final String value; +} + +final class OpenChannelErrorIOErrorException extends OpenChannelErrorException { + const OpenChannelErrorIOErrorException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'IOError'); + final String value; +} + +final class OpenChannelErrorDbErrorException extends OpenChannelErrorException { + const OpenChannelErrorDbErrorException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'DbError'); + final String value; +} + +final class OpenChannelErrorConnectToNodeErrorException + extends OpenChannelErrorException { + const OpenChannelErrorConnectToNodeErrorException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'ConnectToNodeError'); + final String value; +} + +final class OpenChannelErrorNoSuchCoinException + extends OpenChannelErrorException { + const OpenChannelErrorNoSuchCoinException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'NoSuchCoin'); + final String value; +} + +final class OpenChannelErrorGenerateTxErrException + extends OpenChannelErrorException { + const OpenChannelErrorGenerateTxErrException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'GenerateTxErr'); + final String value; +} + +sealed class SendPaymentErrorException extends MmRpcException { + const SendPaymentErrorException({ + required super.errorType, + super.message, + super.path, + super.trace, + }); +} + +final class SendPaymentErrorUnsupportedCoinException + extends SendPaymentErrorException { + const SendPaymentErrorUnsupportedCoinException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'UnsupportedCoin'); + final String value; +} + +final class SendPaymentErrorNoSuchCoinException + extends SendPaymentErrorException { + const SendPaymentErrorNoSuchCoinException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'NoSuchCoin'); + final String value; +} + +final class SendPaymentErrorNoRouteFoundException + extends SendPaymentErrorException { + const SendPaymentErrorNoRouteFoundException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'NoRouteFound'); + final String value; +} + +final class SendPaymentErrorPaymentErrorException + extends SendPaymentErrorException { + const SendPaymentErrorPaymentErrorException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'PaymentError'); + final String value; +} + +final class SendPaymentErrorDbErrorException extends SendPaymentErrorException { + const SendPaymentErrorDbErrorException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'DbError'); + final String value; +} + +sealed class TrustedNodeErrorException extends MmRpcException { + const TrustedNodeErrorException({ + required super.errorType, + super.message, + super.path, + super.trace, + }); +} + +final class TrustedNodeErrorUnsupportedCoinException + extends TrustedNodeErrorException { + const TrustedNodeErrorUnsupportedCoinException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'UnsupportedCoin'); + final String value; +} + +final class TrustedNodeErrorNoSuchCoinException + extends TrustedNodeErrorException { + const TrustedNodeErrorNoSuchCoinException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'NoSuchCoin'); + final String value; +} + +final class TrustedNodeErrorIOErrorException extends TrustedNodeErrorException { + const TrustedNodeErrorIOErrorException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'IOError'); + final String value; +} + +sealed class UpdateChannelErrorException extends MmRpcException { + const UpdateChannelErrorException({ + required super.errorType, + super.message, + super.path, + super.trace, + }); +} + +final class UpdateChannelErrorUnsupportedCoinException + extends UpdateChannelErrorException { + const UpdateChannelErrorUnsupportedCoinException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'UnsupportedCoin'); + final String value; +} + +final class UpdateChannelErrorNoSuchCoinException + extends UpdateChannelErrorException { + const UpdateChannelErrorNoSuchCoinException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'NoSuchCoin'); + final String value; +} + +final class UpdateChannelErrorNoSuchChannelException + extends UpdateChannelErrorException { + const UpdateChannelErrorNoSuchChannelException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'NoSuchChannel'); + final String value; +} + +final class UpdateChannelErrorFailureToUpdateChannelException + extends UpdateChannelErrorException { + const UpdateChannelErrorFailureToUpdateChannelException( + this.value0, + this.value1, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'FailureToUpdateChannel'); + final String value0; + final String value1; +} + +sealed class OfflineKeysErrorException extends MmRpcException { + const OfflineKeysErrorException({ + required super.errorType, + super.message, + super.path, + super.trace, + }); +} + +final class OfflineKeysErrorInternalException + extends OfflineKeysErrorException { + const OfflineKeysErrorInternalException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'Internal'); + final String value; +} + +final class OfflineKeysErrorCoinConfigNotFoundException + extends OfflineKeysErrorException { + const OfflineKeysErrorCoinConfigNotFoundException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'CoinConfigNotFound'); + final String value; +} + +final class OfflineKeysErrorProtocolParseErrorException + extends OfflineKeysErrorException { + const OfflineKeysErrorProtocolParseErrorException({ + required this.ticker, + required this.error, + super.message, + super.path, + super.trace, + }) : super(errorType: 'ProtocolParseError'); + final String ticker; + final String error; +} + +final class OfflineKeysErrorKeyDerivationFailedException + extends OfflineKeysErrorException { + const OfflineKeysErrorKeyDerivationFailedException({ + required this.ticker, + required this.error, + super.message, + super.path, + super.trace, + }) : super(errorType: 'KeyDerivationFailed'); + final String ticker; + final String error; +} + +final class OfflineKeysErrorInvalidHdRangeException + extends OfflineKeysErrorException { + const OfflineKeysErrorInvalidHdRangeException({ + required this.startIndex, + required this.endIndex, + super.message, + super.path, + super.trace, + }) : super(errorType: 'InvalidHdRange'); + final int startIndex; + final int endIndex; +} + +final class OfflineKeysErrorHdRangeTooLargeException + extends OfflineKeysErrorException { + const OfflineKeysErrorHdRangeTooLargeException({ + super.message, + super.path, + super.trace, + }) : super(errorType: 'HdRangeTooLarge'); +} + +final class OfflineKeysErrorMissingPrefixValueException + extends OfflineKeysErrorException { + const OfflineKeysErrorMissingPrefixValueException({ + required this.ticker, + required this.prefixType, + super.message, + super.path, + super.trace, + }) : super(errorType: 'MissingPrefixValue'); + final String ticker; + final String prefixType; +} + +final class OfflineKeysErrorInvalidParametersForModeException + extends OfflineKeysErrorException { + const OfflineKeysErrorInvalidParametersForModeException({ + super.message, + super.path, + super.trace, + }) : super(errorType: 'InvalidParametersForMode'); +} + +sealed class TendermintCoinRpcErrorException extends MmRpcException { + const TendermintCoinRpcErrorException({ + required super.errorType, + super.message, + super.path, + super.trace, + }); +} + +final class TendermintCoinRpcErrorProstException + extends TendermintCoinRpcErrorException { + const TendermintCoinRpcErrorProstException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'Prost'); + final String value; +} + +final class TendermintCoinRpcErrorInvalidResponseException + extends TendermintCoinRpcErrorException { + const TendermintCoinRpcErrorInvalidResponseException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'InvalidResponse'); + final String value; +} + +final class TendermintCoinRpcErrorPerformErrorException + extends TendermintCoinRpcErrorException { + const TendermintCoinRpcErrorPerformErrorException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'PerformError'); + final String value; +} + +final class TendermintCoinRpcErrorRpcClientErrorException + extends TendermintCoinRpcErrorException { + const TendermintCoinRpcErrorRpcClientErrorException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'RpcClientError'); + final String value; +} + +final class TendermintCoinRpcErrorInternalErrorException + extends TendermintCoinRpcErrorException { + const TendermintCoinRpcErrorInternalErrorException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'InternalError'); + final String value; +} + +final class TendermintCoinRpcErrorUnexpectedAccountTypeException + extends TendermintCoinRpcErrorException { + const TendermintCoinRpcErrorUnexpectedAccountTypeException({ + required this.prefix, + super.message, + super.path, + super.trace, + }) : super(errorType: 'UnexpectedAccountType'); + final String prefix; +} + +final class TendermintCoinRpcErrorNotFoundException + extends TendermintCoinRpcErrorException { + const TendermintCoinRpcErrorNotFoundException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'NotFound'); + final String value; +} + +sealed class InitErc20ErrorException extends MmRpcException { + const InitErc20ErrorException({ + required super.errorType, + super.message, + super.path, + super.trace, + }); +} + +final class InitErc20ErrorHwErrorException extends InitErc20ErrorException { + const InitErc20ErrorHwErrorException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'HwError'); + final HwRpcError value; +} + +final class InitErc20ErrorTaskTimedOutException + extends InitErc20ErrorException { + const InitErc20ErrorTaskTimedOutException({ + required this.duration, + super.message, + super.path, + super.trace, + }) : super(errorType: 'TaskTimedOut'); + final Mm2Duration duration; +} + +final class InitErc20ErrorTokenIsAlreadyActivatedException + extends InitErc20ErrorException { + const InitErc20ErrorTokenIsAlreadyActivatedException({ + required this.ticker, + super.message, + super.path, + super.trace, + }) : super(errorType: 'TokenIsAlreadyActivated'); + final String ticker; +} + +final class InitErc20ErrorTokenCreationErrorException + extends InitErc20ErrorException { + const InitErc20ErrorTokenCreationErrorException({ + required this.ticker, + required this.error, + super.message, + super.path, + super.trace, + }) : super(errorType: 'TokenCreationError'); + final String ticker; + final String error; +} + +final class InitErc20ErrorCouldNotFetchBalanceException + extends InitErc20ErrorException { + const InitErc20ErrorCouldNotFetchBalanceException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'CouldNotFetchBalance'); + final String value; +} + +final class InitErc20ErrorTransportException extends InitErc20ErrorException { + const InitErc20ErrorTransportException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'Transport'); + final String value; +} + +final class InitErc20ErrorInternalException extends InitErc20ErrorException { + const InitErc20ErrorInternalException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'Internal'); + final String value; +} + +final class InitErc20ErrorCustomTokenErrorException + extends InitErc20ErrorException { + const InitErc20ErrorCustomTokenErrorException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'CustomTokenError'); + final CustomTokenError value; +} + +sealed class InitTokenErrorException extends MmRpcException { + const InitTokenErrorException({ + required super.errorType, + super.message, + super.path, + super.trace, + }); +} + +final class InitTokenErrorNoSuchTaskException extends InitTokenErrorException { + const InitTokenErrorNoSuchTaskException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'NoSuchTask'); + final TaskId value; +} + +final class InitTokenErrorTaskTimedOutException + extends InitTokenErrorException { + const InitTokenErrorTaskTimedOutException({ + required this.duration, + super.message, + super.path, + super.trace, + }) : super(errorType: 'TaskTimedOut'); + final Mm2Duration duration; +} + +final class InitTokenErrorTokenIsAlreadyActivatedException + extends InitTokenErrorException { + const InitTokenErrorTokenIsAlreadyActivatedException({ + required this.ticker, + super.message, + super.path, + super.trace, + }) : super(errorType: 'TokenIsAlreadyActivated'); + final String ticker; +} + +final class InitTokenErrorTokenConfigIsNotFoundException + extends InitTokenErrorException { + const InitTokenErrorTokenConfigIsNotFoundException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'TokenConfigIsNotFound'); + final String value; +} + +final class InitTokenErrorTokenProtocolParseErrorException + extends InitTokenErrorException { + const InitTokenErrorTokenProtocolParseErrorException({ + required this.ticker, + required this.error, + super.message, + super.path, + super.trace, + }) : super(errorType: 'TokenProtocolParseError'); + final String ticker; + final String error; +} + +final class InitTokenErrorUnexpectedTokenProtocolException + extends InitTokenErrorException { + const InitTokenErrorUnexpectedTokenProtocolException({ + required this.ticker, + required this.protocol, + super.message, + super.path, + super.trace, + }) : super(errorType: 'UnexpectedTokenProtocol'); + final String ticker; + final JsonValue protocol; +} + +final class InitTokenErrorTokenCreationErrorException + extends InitTokenErrorException { + const InitTokenErrorTokenCreationErrorException({ + required this.ticker, + required this.error, + super.message, + super.path, + super.trace, + }) : super(errorType: 'TokenCreationError'); + final String ticker; + final String error; +} + +final class InitTokenErrorCouldNotFetchBalanceException + extends InitTokenErrorException { + const InitTokenErrorCouldNotFetchBalanceException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'CouldNotFetchBalance'); + final String value; +} + +final class InitTokenErrorPlatformCoinIsNotActivatedException + extends InitTokenErrorException { + const InitTokenErrorPlatformCoinIsNotActivatedException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'PlatformCoinIsNotActivated'); + final String value; +} + +final class InitTokenErrorUnsupportedPlatformCoinException + extends InitTokenErrorException { + const InitTokenErrorUnsupportedPlatformCoinException({ + required this.platformCoinTicker, + required this.tokenTicker, + super.message, + super.path, + super.trace, + }) : super(errorType: 'UnsupportedPlatformCoin'); + final String platformCoinTicker; + final String tokenTicker; +} + +final class InitTokenErrorCustomTokenErrorException + extends InitTokenErrorException { + const InitTokenErrorCustomTokenErrorException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'CustomTokenError'); + final CustomTokenError value; +} + +final class InitTokenErrorHwErrorException extends InitTokenErrorException { + const InitTokenErrorHwErrorException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'HwError'); + final HwRpcError value; +} + +final class InitTokenErrorTransportException extends InitTokenErrorException { + const InitTokenErrorTransportException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'Transport'); + final String value; +} + +final class InitTokenErrorInternalException extends InitTokenErrorException { + const InitTokenErrorInternalException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'Internal'); + final String value; +} + +sealed class InitL2ErrorException extends MmRpcException { + const InitL2ErrorException({ + required super.errorType, + super.message, + super.path, + super.trace, + }); +} + +final class InitL2ErrorL2IsAlreadyActivatedException + extends InitL2ErrorException { + const InitL2ErrorL2IsAlreadyActivatedException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'L2IsAlreadyActivated'); + final String value; +} + +final class InitL2ErrorL2ConfigIsNotFoundException + extends InitL2ErrorException { + const InitL2ErrorL2ConfigIsNotFoundException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'L2ConfigIsNotFound'); + final String value; +} + +final class InitL2ErrorL2ProtocolParseErrorException + extends InitL2ErrorException { + const InitL2ErrorL2ProtocolParseErrorException({ + required this.ticker, + required this.error, + super.message, + super.path, + super.trace, + }) : super(errorType: 'L2ProtocolParseError'); + final String ticker; + final String error; +} + +final class InitL2ErrorUnexpectedL2ProtocolException + extends InitL2ErrorException { + const InitL2ErrorUnexpectedL2ProtocolException({ + required this.ticker, + required this.protocol, + super.message, + super.path, + super.trace, + }) : super(errorType: 'UnexpectedL2Protocol'); + final String ticker; + final JsonValue protocol; +} + +final class InitL2ErrorPlatformCoinIsNotActivatedException + extends InitL2ErrorException { + const InitL2ErrorPlatformCoinIsNotActivatedException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'PlatformCoinIsNotActivated'); + final String value; +} + +final class InitL2ErrorUnsupportedPlatformCoinException + extends InitL2ErrorException { + const InitL2ErrorUnsupportedPlatformCoinException({ + required this.platformCoinTicker, + required this.l2Ticker, + super.message, + super.path, + super.trace, + }) : super(errorType: 'UnsupportedPlatformCoin'); + final String platformCoinTicker; + final String l2Ticker; +} + +final class InitL2ErrorInvalidPlatformConfigurationException + extends InitL2ErrorException { + const InitL2ErrorInvalidPlatformConfigurationException({ + required this.platformCoinTicker, + required this.err, + super.message, + super.path, + super.trace, + }) : super(errorType: 'InvalidPlatformConfiguration'); + final String platformCoinTicker; + final String err; +} + +final class InitL2ErrorL2ConfigParseErrorException + extends InitL2ErrorException { + const InitL2ErrorL2ConfigParseErrorException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'L2ConfigParseError'); + final String value; +} + +final class InitL2ErrorTaskTimedOutException extends InitL2ErrorException { + const InitL2ErrorTaskTimedOutException({ + required this.duration, + super.message, + super.path, + super.trace, + }) : super(errorType: 'TaskTimedOut'); + final Mm2Duration duration; +} + +final class InitL2ErrorTransportException extends InitL2ErrorException { + const InitL2ErrorTransportException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'Transport'); + final String value; +} + +final class InitL2ErrorInternalException extends InitL2ErrorException { + const InitL2ErrorInternalException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'Internal'); + final String value; +} + +sealed class LightningInitErrorException extends MmRpcException { + const LightningInitErrorException({ + required super.errorType, + super.message, + super.path, + super.trace, + }); +} + +final class LightningInitErrorCoinIsAlreadyActivatedException + extends LightningInitErrorException { + const LightningInitErrorCoinIsAlreadyActivatedException({ + required this.ticker, + super.message, + super.path, + super.trace, + }) : super(errorType: 'CoinIsAlreadyActivated'); + final String ticker; +} + +final class LightningInitErrorInvalidConfigurationException + extends LightningInitErrorException { + const LightningInitErrorInvalidConfigurationException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'InvalidConfiguration'); + final String value; +} + +final class LightningInitErrorInvalidPlatformConfigurationException + extends LightningInitErrorException { + const LightningInitErrorInvalidPlatformConfigurationException({ + required this.platformCoinTicker, + required this.err, + super.message, + super.path, + super.trace, + }) : super(errorType: 'InvalidPlatformConfiguration'); + final String platformCoinTicker; + final String err; +} + +final class LightningInitErrorEnableLightningErrorException + extends LightningInitErrorException { + const LightningInitErrorEnableLightningErrorException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'EnableLightningError'); + final EnableLightningError value; +} + +final class LightningInitErrorLightningValidationErrException + extends LightningInitErrorException { + const LightningInitErrorLightningValidationErrException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'LightningValidationErr'); + final LightningValidationErr value; +} + +final class LightningInitErrorMyBalanceErrorException + extends LightningInitErrorException { + const LightningInitErrorMyBalanceErrorException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'MyBalanceError'); + final BalanceError value; +} + +final class LightningInitErrorMyAddressErrorException + extends LightningInitErrorException { + const LightningInitErrorMyAddressErrorException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'MyAddressError'); + final String value; +} + +final class LightningInitErrorInternalException + extends LightningInitErrorException { + const LightningInitErrorInternalException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'Internal'); + final String value; +} + +sealed class LightningValidationErrException extends MmRpcException { + const LightningValidationErrException({ + required super.errorType, + super.message, + super.path, + super.trace, + }); +} + +final class LightningValidationErrUnexpectedMethodException + extends LightningValidationErrException { + const LightningValidationErrUnexpectedMethodException( + this.value0, + this.value1, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'UnexpectedMethod'); + final String value0; + final String value1; +} + +final class LightningValidationErrUnsupportedModeException + extends LightningValidationErrException { + const LightningValidationErrUnsupportedModeException( + this.value0, + this.value1, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'UnsupportedMode'); + final String value0; + final String value1; +} + +final class LightningValidationErrInvalidRequestException + extends LightningValidationErrException { + const LightningValidationErrInvalidRequestException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'InvalidRequest'); + final String value; +} + +final class LightningValidationErrInvalidAddressException + extends LightningValidationErrException { + const LightningValidationErrInvalidAddressException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'InvalidAddress'); + final String value; +} + +sealed class EnablePlatformCoinWithTokensErrorException extends MmRpcException { + const EnablePlatformCoinWithTokensErrorException({ + required super.errorType, + super.message, + super.path, + super.trace, + }); +} + +final class EnablePlatformCoinWithTokensErrorPlatformIsAlreadyActivatedException + extends EnablePlatformCoinWithTokensErrorException { + const EnablePlatformCoinWithTokensErrorPlatformIsAlreadyActivatedException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'PlatformIsAlreadyActivated'); + final String value; +} + +final class EnablePlatformCoinWithTokensErrorPlatformConfigIsNotFoundException + extends EnablePlatformCoinWithTokensErrorException { + const EnablePlatformCoinWithTokensErrorPlatformConfigIsNotFoundException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'PlatformConfigIsNotFound'); + final String value; +} + +final class EnablePlatformCoinWithTokensErrorCoinProtocolParseErrorException + extends EnablePlatformCoinWithTokensErrorException { + const EnablePlatformCoinWithTokensErrorCoinProtocolParseErrorException({ + required this.ticker, + required this.error, + super.message, + super.path, + super.trace, + }) : super(errorType: 'CoinProtocolParseError'); + final String ticker; + final String error; +} + +final class EnablePlatformCoinWithTokensErrorUnexpectedPlatformProtocolException + extends EnablePlatformCoinWithTokensErrorException { + const EnablePlatformCoinWithTokensErrorUnexpectedPlatformProtocolException({ + required this.ticker, + required this.protocol, + super.message, + super.path, + super.trace, + }) : super(errorType: 'UnexpectedPlatformProtocol'); + final String ticker; + final JsonValue protocol; +} + +final class EnablePlatformCoinWithTokensErrorTokenConfigIsNotFoundException + extends EnablePlatformCoinWithTokensErrorException { + const EnablePlatformCoinWithTokensErrorTokenConfigIsNotFoundException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'TokenConfigIsNotFound'); + final String value; +} + +final class EnablePlatformCoinWithTokensErrorTokenProtocolParseErrorException + extends EnablePlatformCoinWithTokensErrorException { + const EnablePlatformCoinWithTokensErrorTokenProtocolParseErrorException({ + required this.ticker, + required this.error, + super.message, + super.path, + super.trace, + }) : super(errorType: 'TokenProtocolParseError'); + final String ticker; + final String error; +} + +final class EnablePlatformCoinWithTokensErrorUnexpectedTokenProtocolException + extends EnablePlatformCoinWithTokensErrorException { + const EnablePlatformCoinWithTokensErrorUnexpectedTokenProtocolException({ + required this.ticker, + required this.protocol, + super.message, + super.path, + super.trace, + }) : super(errorType: 'UnexpectedTokenProtocol'); + final String ticker; + final JsonValue protocol; +} + +final class EnablePlatformCoinWithTokensErrorPlatformCoinCreationErrorException + extends EnablePlatformCoinWithTokensErrorException { + const EnablePlatformCoinWithTokensErrorPlatformCoinCreationErrorException({ + required this.ticker, + required this.error, + super.message, + super.path, + super.trace, + }) : super(errorType: 'PlatformCoinCreationError'); + final String ticker; + final String error; +} + +final class EnablePlatformCoinWithTokensErrorPrivKeyPolicyNotAllowedException + extends EnablePlatformCoinWithTokensErrorException { + const EnablePlatformCoinWithTokensErrorPrivKeyPolicyNotAllowedException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'PrivKeyPolicyNotAllowed'); + final PrivKeyPolicyNotAllowed value; +} + +final class EnablePlatformCoinWithTokensErrorUnexpectedDerivationMethodException + extends EnablePlatformCoinWithTokensErrorException { + const EnablePlatformCoinWithTokensErrorUnexpectedDerivationMethodException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'UnexpectedDerivationMethod'); + final String value; +} + +final class EnablePlatformCoinWithTokensErrorTransportException + extends EnablePlatformCoinWithTokensErrorException { + const EnablePlatformCoinWithTokensErrorTransportException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'Transport'); + final String value; +} + +final class EnablePlatformCoinWithTokensErrorAtLeastOneNodeRequiredException + extends EnablePlatformCoinWithTokensErrorException { + const EnablePlatformCoinWithTokensErrorAtLeastOneNodeRequiredException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'AtLeastOneNodeRequired'); + final String value; +} + +final class EnablePlatformCoinWithTokensErrorInvalidPayloadException + extends EnablePlatformCoinWithTokensErrorException { + const EnablePlatformCoinWithTokensErrorInvalidPayloadException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'InvalidPayload'); + final String value; +} + +final class EnablePlatformCoinWithTokensErrorFailedSpawningBalanceEventsException + extends EnablePlatformCoinWithTokensErrorException { + const EnablePlatformCoinWithTokensErrorFailedSpawningBalanceEventsException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'FailedSpawningBalanceEvents'); + final String value; +} + +final class EnablePlatformCoinWithTokensErrorInternalException + extends EnablePlatformCoinWithTokensErrorException { + const EnablePlatformCoinWithTokensErrorInternalException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'Internal'); + final String value; +} + +final class EnablePlatformCoinWithTokensErrorNoSuchTaskException + extends EnablePlatformCoinWithTokensErrorException { + const EnablePlatformCoinWithTokensErrorNoSuchTaskException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'NoSuchTask'); + final TaskId value; +} + +final class EnablePlatformCoinWithTokensErrorTaskTimedOutException + extends EnablePlatformCoinWithTokensErrorException { + const EnablePlatformCoinWithTokensErrorTaskTimedOutException({ + required this.duration, + super.message, + super.path, + super.trace, + }) : super(errorType: 'TaskTimedOut'); + final Mm2Duration duration; +} + +final class EnablePlatformCoinWithTokensErrorUnexpectedDeviceActivationPolicyException + extends EnablePlatformCoinWithTokensErrorException { + const EnablePlatformCoinWithTokensErrorUnexpectedDeviceActivationPolicyException({ + super.message, + super.path, + super.trace, + }) : super(errorType: 'UnexpectedDeviceActivationPolicy'); +} + +final class EnablePlatformCoinWithTokensErrorCustomTokenErrorException + extends EnablePlatformCoinWithTokensErrorException { + const EnablePlatformCoinWithTokensErrorCustomTokenErrorException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'CustomTokenError'); + final CustomTokenError value; +} + +final class EnablePlatformCoinWithTokensErrorWalletConnectErrorException + extends EnablePlatformCoinWithTokensErrorException { + const EnablePlatformCoinWithTokensErrorWalletConnectErrorException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'WalletConnectError'); + final String value; +} + +sealed class SiaCoinInitErrorException extends MmRpcException { + const SiaCoinInitErrorException({ + required super.errorType, + super.message, + super.path, + super.trace, + }); +} + +final class SiaCoinInitErrorCoinCreationErrorException + extends SiaCoinInitErrorException { + const SiaCoinInitErrorCoinCreationErrorException({ + required this.ticker, + required this.error, + super.message, + super.path, + super.trace, + }) : super(errorType: 'CoinCreationError'); + final String ticker; + final String error; +} + +final class SiaCoinInitErrorCoinIsAlreadyActivatedException + extends SiaCoinInitErrorException { + const SiaCoinInitErrorCoinIsAlreadyActivatedException({ + required this.ticker, + super.message, + super.path, + super.trace, + }) : super(errorType: 'CoinIsAlreadyActivated'); + final String ticker; +} + +final class SiaCoinInitErrorHardwareWalletsAreNotSupportedYetException + extends SiaCoinInitErrorException { + const SiaCoinInitErrorHardwareWalletsAreNotSupportedYetException({ + super.message, + super.path, + super.trace, + }) : super(errorType: 'HardwareWalletsAreNotSupportedYet'); +} + +final class SiaCoinInitErrorTaskTimedOutException + extends SiaCoinInitErrorException { + const SiaCoinInitErrorTaskTimedOutException({ + required this.duration, + super.message, + super.path, + super.trace, + }) : super(errorType: 'TaskTimedOut'); + final Mm2Duration duration; +} + +final class SiaCoinInitErrorCouldNotGetBalanceException + extends SiaCoinInitErrorException { + const SiaCoinInitErrorCouldNotGetBalanceException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'CouldNotGetBalance'); + final String value; +} + +final class SiaCoinInitErrorCouldNotGetBlockCountException + extends SiaCoinInitErrorException { + const SiaCoinInitErrorCouldNotGetBlockCountException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'CouldNotGetBlockCount'); + final String value; +} + +final class SiaCoinInitErrorInternalException + extends SiaCoinInitErrorException { + const SiaCoinInitErrorInternalException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'Internal'); + final String value; +} + +sealed class InitStandaloneCoinErrorException extends MmRpcException { + const InitStandaloneCoinErrorException({ + required super.errorType, + super.message, + super.path, + super.trace, + }); +} + +final class InitStandaloneCoinErrorNoSuchTaskException + extends InitStandaloneCoinErrorException { + const InitStandaloneCoinErrorNoSuchTaskException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'NoSuchTask'); + final TaskId value; +} + +final class InitStandaloneCoinErrorTaskTimedOutException + extends InitStandaloneCoinErrorException { + const InitStandaloneCoinErrorTaskTimedOutException({ + required this.duration, + super.message, + super.path, + super.trace, + }) : super(errorType: 'TaskTimedOut'); + final Mm2Duration duration; +} + +final class InitStandaloneCoinErrorCoinIsAlreadyActivatedException + extends InitStandaloneCoinErrorException { + const InitStandaloneCoinErrorCoinIsAlreadyActivatedException({ + required this.ticker, + super.message, + super.path, + super.trace, + }) : super(errorType: 'CoinIsAlreadyActivated'); + final String ticker; +} + +final class InitStandaloneCoinErrorCoinConfigIsNotFoundException + extends InitStandaloneCoinErrorException { + const InitStandaloneCoinErrorCoinConfigIsNotFoundException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'CoinConfigIsNotFound'); + final String value; +} + +final class InitStandaloneCoinErrorCoinProtocolParseErrorException + extends InitStandaloneCoinErrorException { + const InitStandaloneCoinErrorCoinProtocolParseErrorException({ + required this.ticker, + required this.error, + super.message, + super.path, + super.trace, + }) : super(errorType: 'CoinProtocolParseError'); + final String ticker; + final String error; +} + +final class InitStandaloneCoinErrorUnexpectedCoinProtocolException + extends InitStandaloneCoinErrorException { + const InitStandaloneCoinErrorUnexpectedCoinProtocolException({ + required this.ticker, + required this.protocol, + super.message, + super.path, + super.trace, + }) : super(errorType: 'UnexpectedCoinProtocol'); + final String ticker; + final JsonValue protocol; +} + +final class InitStandaloneCoinErrorCoinCreationErrorException + extends InitStandaloneCoinErrorException { + const InitStandaloneCoinErrorCoinCreationErrorException({ + required this.ticker, + required this.error, + super.message, + super.path, + super.trace, + }) : super(errorType: 'CoinCreationError'); + final String ticker; + final String error; +} + +final class InitStandaloneCoinErrorHwErrorException + extends InitStandaloneCoinErrorException { + const InitStandaloneCoinErrorHwErrorException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'HwError'); + final HwRpcError value; +} + +final class InitStandaloneCoinErrorTransportException + extends InitStandaloneCoinErrorException { + const InitStandaloneCoinErrorTransportException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'Transport'); + final String value; +} + +final class InitStandaloneCoinErrorInternalException + extends InitStandaloneCoinErrorException { + const InitStandaloneCoinErrorInternalException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'Internal'); + final String value; +} + +sealed class EnableTokenErrorException extends MmRpcException { + const EnableTokenErrorException({ + required super.errorType, + super.message, + super.path, + super.trace, + }); +} + +final class EnableTokenErrorTokenIsAlreadyActivatedException + extends EnableTokenErrorException { + const EnableTokenErrorTokenIsAlreadyActivatedException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'TokenIsAlreadyActivated'); + final String value; +} + +final class EnableTokenErrorTokenConfigIsNotFoundException + extends EnableTokenErrorException { + const EnableTokenErrorTokenConfigIsNotFoundException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'TokenConfigIsNotFound'); + final String value; +} + +final class EnableTokenErrorTokenProtocolParseErrorException + extends EnableTokenErrorException { + const EnableTokenErrorTokenProtocolParseErrorException({ + required this.ticker, + required this.error, + super.message, + super.path, + super.trace, + }) : super(errorType: 'TokenProtocolParseError'); + final String ticker; + final String error; +} + +final class EnableTokenErrorUnexpectedTokenProtocolException + extends EnableTokenErrorException { + const EnableTokenErrorUnexpectedTokenProtocolException({ + required this.ticker, + required this.protocol, + super.message, + super.path, + super.trace, + }) : super(errorType: 'UnexpectedTokenProtocol'); + final String ticker; + final JsonValue protocol; +} + +final class EnableTokenErrorPlatformCoinIsNotActivatedException + extends EnableTokenErrorException { + const EnableTokenErrorPlatformCoinIsNotActivatedException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'PlatformCoinIsNotActivated'); + final String value; +} + +final class EnableTokenErrorUnsupportedPlatformCoinException + extends EnableTokenErrorException { + const EnableTokenErrorUnsupportedPlatformCoinException({ + required this.platformCoinTicker, + required this.tokenTicker, + super.message, + super.path, + super.trace, + }) : super(errorType: 'UnsupportedPlatformCoin'); + final String platformCoinTicker; + final String tokenTicker; +} + +final class EnableTokenErrorUnexpectedDerivationMethodException + extends EnableTokenErrorException { + const EnableTokenErrorUnexpectedDerivationMethodException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'UnexpectedDerivationMethod'); + final UnexpectedDerivationMethod value; +} + +final class EnableTokenErrorCouldNotFetchBalanceException + extends EnableTokenErrorException { + const EnableTokenErrorCouldNotFetchBalanceException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'CouldNotFetchBalance'); + final String value; +} + +final class EnableTokenErrorInvalidConfigException + extends EnableTokenErrorException { + const EnableTokenErrorInvalidConfigException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'InvalidConfig'); + final String value; +} + +final class EnableTokenErrorTransportException + extends EnableTokenErrorException { + const EnableTokenErrorTransportException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'Transport'); + final String value; +} + +final class EnableTokenErrorInternalException + extends EnableTokenErrorException { + const EnableTokenErrorInternalException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'Internal'); + final String value; +} + +final class EnableTokenErrorInvalidPayloadException + extends EnableTokenErrorException { + const EnableTokenErrorInvalidPayloadException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'InvalidPayload'); + final String value; +} + +final class EnableTokenErrorPrivKeyPolicyNotAllowedException + extends EnableTokenErrorException { + const EnableTokenErrorPrivKeyPolicyNotAllowedException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'PrivKeyPolicyNotAllowed'); + final PrivKeyPolicyNotAllowed value; +} + +final class EnableTokenErrorCustomTokenErrorException + extends EnableTokenErrorException { + const EnableTokenErrorCustomTokenErrorException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'CustomTokenError'); + final CustomTokenError value; +} + +sealed class CancelEnableCoinUnifiedErrorException extends MmRpcException { + const CancelEnableCoinUnifiedErrorException({ + required super.errorType, + super.message, + super.path, + super.trace, + }); +} + +final class CancelEnableCoinUnifiedErrorNoSuchTaskException + extends CancelEnableCoinUnifiedErrorException { + const CancelEnableCoinUnifiedErrorNoSuchTaskException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'NoSuchTask'); + final int value; +} + +final class CancelEnableCoinUnifiedErrorInternalException + extends CancelEnableCoinUnifiedErrorException { + const CancelEnableCoinUnifiedErrorInternalException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'Internal'); + final String value; +} + +sealed class EnableCoinUnifiedErrorException extends MmRpcException { + const EnableCoinUnifiedErrorException({ + required super.errorType, + super.message, + super.path, + super.trace, + }); +} + +final class EnableCoinUnifiedErrorUnsupportedProtocolException + extends EnableCoinUnifiedErrorException { + const EnableCoinUnifiedErrorUnsupportedProtocolException({ + required this.ticker, + super.message, + super.path, + super.trace, + }) : super(errorType: 'UnsupportedProtocol'); + final String ticker; +} + +final class EnableCoinUnifiedErrorMissingActivationParamsException + extends EnableCoinUnifiedErrorException { + const EnableCoinUnifiedErrorMissingActivationParamsException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'MissingActivationParams'); + final String value; +} + +final class EnableCoinUnifiedErrorInvalidActivationRequestException + extends EnableCoinUnifiedErrorException { + const EnableCoinUnifiedErrorInvalidActivationRequestException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'InvalidActivationRequest'); + final String value; +} + +final class EnableCoinUnifiedErrorInternalException + extends EnableCoinUnifiedErrorException { + const EnableCoinUnifiedErrorInternalException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'Internal'); + final String value; +} + +final class EnableCoinUnifiedErrorUtxoInitErrorException + extends EnableCoinUnifiedErrorException { + const EnableCoinUnifiedErrorUtxoInitErrorException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'UtxoInitError'); + final String value; +} + +final class EnableCoinUnifiedErrorQtumInitErrorException + extends EnableCoinUnifiedErrorException { + const EnableCoinUnifiedErrorQtumInitErrorException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'QtumInitError'); + final String value; +} + +final class EnableCoinUnifiedErrorZcoinInitErrorException + extends EnableCoinUnifiedErrorException { + const EnableCoinUnifiedErrorZcoinInitErrorException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'ZcoinInitError'); + final String value; +} + +final class EnableCoinUnifiedErrorEthInitErrorException + extends EnableCoinUnifiedErrorException { + const EnableCoinUnifiedErrorEthInitErrorException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'EthInitError'); + final String value; +} + +final class EnableCoinUnifiedErrorErc20InitErrorException + extends EnableCoinUnifiedErrorException { + const EnableCoinUnifiedErrorErc20InitErrorException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'Erc20InitError'); + final String value; +} + +final class EnableCoinUnifiedErrorTendermintInitErrorException + extends EnableCoinUnifiedErrorException { + const EnableCoinUnifiedErrorTendermintInitErrorException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'TendermintInitError'); + final String value; +} + +sealed class EnableCoinUnifiedStatusErrorException extends MmRpcException { + const EnableCoinUnifiedStatusErrorException({ + required super.errorType, + super.message, + super.path, + super.trace, + }); +} + +final class EnableCoinUnifiedStatusErrorNoSuchTaskException + extends EnableCoinUnifiedStatusErrorException { + const EnableCoinUnifiedStatusErrorNoSuchTaskException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'NoSuchTask'); + final int value; +} + +final class EnableCoinUnifiedStatusErrorInternalException + extends EnableCoinUnifiedStatusErrorException { + const EnableCoinUnifiedStatusErrorInternalException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'Internal'); + final String value; +} + +sealed class InitUtxoStandardErrorException extends MmRpcException { + const InitUtxoStandardErrorException({ + required super.errorType, + super.message, + super.path, + super.trace, + }); +} + +final class InitUtxoStandardErrorHwErrorException + extends InitUtxoStandardErrorException { + const InitUtxoStandardErrorHwErrorException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'HwError'); + final HwRpcError value; +} + +final class InitUtxoStandardErrorTaskTimedOutException + extends InitUtxoStandardErrorException { + const InitUtxoStandardErrorTaskTimedOutException({ + required this.duration, + super.message, + super.path, + super.trace, + }) : super(errorType: 'TaskTimedOut'); + final Mm2Duration duration; +} + +final class InitUtxoStandardErrorCoinIsAlreadyActivatedException + extends InitUtxoStandardErrorException { + const InitUtxoStandardErrorCoinIsAlreadyActivatedException({ + required this.ticker, + super.message, + super.path, + super.trace, + }) : super(errorType: 'CoinIsAlreadyActivated'); + final String ticker; +} + +final class InitUtxoStandardErrorCoinCreationErrorException + extends InitUtxoStandardErrorException { + const InitUtxoStandardErrorCoinCreationErrorException({ + required this.ticker, + required this.error, + super.message, + super.path, + super.trace, + }) : super(errorType: 'CoinCreationError'); + final String ticker; + final String error; +} + +final class InitUtxoStandardErrorTransportException + extends InitUtxoStandardErrorException { + const InitUtxoStandardErrorTransportException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'Transport'); + final String value; +} + +final class InitUtxoStandardErrorInternalException + extends InitUtxoStandardErrorException { + const InitUtxoStandardErrorInternalException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'Internal'); + final String value; +} + +sealed class ZcoinInitErrorException extends MmRpcException { + const ZcoinInitErrorException({ + required super.errorType, + super.message, + super.path, + super.trace, + }); +} + +final class ZcoinInitErrorCoinCreationErrorException + extends ZcoinInitErrorException { + const ZcoinInitErrorCoinCreationErrorException({ + required this.ticker, + required this.error, + super.message, + super.path, + super.trace, + }) : super(errorType: 'CoinCreationError'); + final String ticker; + final String error; +} + +final class ZcoinInitErrorCoinIsAlreadyActivatedException + extends ZcoinInitErrorException { + const ZcoinInitErrorCoinIsAlreadyActivatedException({ + required this.ticker, + super.message, + super.path, + super.trace, + }) : super(errorType: 'CoinIsAlreadyActivated'); + final String ticker; +} + +final class ZcoinInitErrorHardwareWalletsAreNotSupportedYetException + extends ZcoinInitErrorException { + const ZcoinInitErrorHardwareWalletsAreNotSupportedYetException({ + super.message, + super.path, + super.trace, + }) : super(errorType: 'HardwareWalletsAreNotSupportedYet'); +} + +final class ZcoinInitErrorTaskTimedOutException + extends ZcoinInitErrorException { + const ZcoinInitErrorTaskTimedOutException({ + required this.duration, + super.message, + super.path, + super.trace, + }) : super(errorType: 'TaskTimedOut'); + final Mm2Duration duration; +} + +final class ZcoinInitErrorCouldNotGetBalanceException + extends ZcoinInitErrorException { + const ZcoinInitErrorCouldNotGetBalanceException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'CouldNotGetBalance'); + final String value; +} + +final class ZcoinInitErrorCouldNotGetBlockCountException + extends ZcoinInitErrorException { + const ZcoinInitErrorCouldNotGetBlockCountException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'CouldNotGetBlockCount'); + final String value; +} + +final class ZcoinInitErrorInternalException extends ZcoinInitErrorException { + const ZcoinInitErrorInternalException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'Internal'); + final String value; +} + +sealed class SerializationErrorException extends MmRpcException { + const SerializationErrorException({ + required super.errorType, + super.message, + super.path, + super.trace, + }); +} + +final class SerializationErrorInternalErrorException + extends SerializationErrorException { + const SerializationErrorInternalErrorException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'InternalError'); + final String value; +} + +sealed class SendAskedDataErrorException extends MmRpcException { + const SendAskedDataErrorException({ + required super.errorType, + super.message, + super.path, + super.trace, + }); +} + +final class SendAskedDataErrorNotFoundException + extends SendAskedDataErrorException { + const SendAskedDataErrorNotFoundException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'NotFound'); + final int value; +} + +final class SendAskedDataErrorInternalException + extends SendAskedDataErrorException { + const SendAskedDataErrorInternalException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'Internal'); + final String value; +} + +sealed class ForwardedErrorException extends MmRpcException { + const ForwardedErrorException({ + required super.errorType, + super.message, + super.path, + super.trace, + }); +} + +final class ForwardedErrorNotSufficientBalanceException + extends ForwardedErrorException { + const ForwardedErrorNotSufficientBalanceException({ + required this.missing, + super.message, + super.path, + super.trace, + }) : super(errorType: 'NotSufficientBalance'); + final int missing; +} + +sealed class AccountRpcErrorException extends MmRpcException { + const AccountRpcErrorException({ + required super.errorType, + super.message, + super.path, + super.trace, + }); +} + +final class AccountRpcErrorNameTooLongException + extends AccountRpcErrorException { + const AccountRpcErrorNameTooLongException({ + required this.maxLen, + super.message, + super.path, + super.trace, + }) : super(errorType: 'NameTooLong'); + final int maxLen; +} + +final class AccountRpcErrorDescriptionTooLongException + extends AccountRpcErrorException { + const AccountRpcErrorDescriptionTooLongException({ + required this.maxLen, + super.message, + super.path, + super.trace, + }) : super(errorType: 'DescriptionTooLong'); + final int maxLen; +} + +final class AccountRpcErrorTickerTooLongException + extends AccountRpcErrorException { + const AccountRpcErrorTickerTooLongException({ + required this.maxLen, + super.message, + super.path, + super.trace, + }) : super(errorType: 'TickerTooLong'); + final int maxLen; +} + +final class AccountRpcErrorNoSuchAccountException + extends AccountRpcErrorException { + const AccountRpcErrorNoSuchAccountException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'NoSuchAccount'); + final AccountId value; +} + +final class AccountRpcErrorNoEnabledAccountException + extends AccountRpcErrorException { + const AccountRpcErrorNoEnabledAccountException({ + super.message, + super.path, + super.trace, + }) : super(errorType: 'NoEnabledAccount'); +} + +final class AccountRpcErrorAccountExistsAlreadyException + extends AccountRpcErrorException { + const AccountRpcErrorAccountExistsAlreadyException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'AccountExistsAlready'); + final AccountId value; +} + +final class AccountRpcErrorErrorLoadingAccountException + extends AccountRpcErrorException { + const AccountRpcErrorErrorLoadingAccountException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'ErrorLoadingAccount'); + final String value; +} + +final class AccountRpcErrorErrorSavingAccountException + extends AccountRpcErrorException { + const AccountRpcErrorErrorSavingAccountException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'ErrorSavingAccount'); + final String value; +} + +final class AccountRpcErrorInternalException extends AccountRpcErrorException { + const AccountRpcErrorInternalException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'Internal'); + final String value; +} + +sealed class HealthcheckRpcErrorException extends MmRpcException { + const HealthcheckRpcErrorException({ + required super.errorType, + super.message, + super.path, + super.trace, + }); +} + +final class HealthcheckRpcErrorMessageGenerationFailedException + extends HealthcheckRpcErrorException { + const HealthcheckRpcErrorMessageGenerationFailedException({ + required this.reason, + super.message, + super.path, + super.trace, + }) : super(errorType: 'MessageGenerationFailed'); + final String reason; +} + +final class HealthcheckRpcErrorMessageEncodingFailedException + extends HealthcheckRpcErrorException { + const HealthcheckRpcErrorMessageEncodingFailedException({ + required this.reason, + super.message, + super.path, + super.trace, + }) : super(errorType: 'MessageEncodingFailed'); + final String reason; +} + +final class HealthcheckRpcErrorInternalException + extends HealthcheckRpcErrorException { + const HealthcheckRpcErrorInternalException({ + required this.reason, + super.message, + super.path, + super.trace, + }) : super(errorType: 'Internal'); + final String reason; +} + +sealed class InitHwErrorException extends MmRpcException { + const InitHwErrorException({ + required super.errorType, + super.message, + super.path, + super.trace, + }); +} + +final class InitHwErrorHwContextInitializingAlreadyException + extends InitHwErrorException { + const InitHwErrorHwContextInitializingAlreadyException({ + super.message, + super.path, + super.trace, + }) : super(errorType: 'HwContextInitializingAlready'); +} + +final class InitHwErrorHwErrorException extends InitHwErrorException { + const InitHwErrorHwErrorException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'HwError'); + final HwRpcError value; +} + +final class InitHwErrorUnexpectedUserActionException + extends InitHwErrorException { + const InitHwErrorUnexpectedUserActionException({ + required this.expected, + super.message, + super.path, + super.trace, + }) : super(errorType: 'UnexpectedUserAction'); + final String expected; +} + +final class InitHwErrorTimeoutException extends InitHwErrorException { + const InitHwErrorTimeoutException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'Timeout'); + final Mm2Duration value; +} + +final class InitHwErrorInternalException extends InitHwErrorException { + const InitHwErrorInternalException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'Internal'); + final String value; +} + +sealed class InitMetamaskErrorException extends MmRpcException { + const InitMetamaskErrorException({ + required super.errorType, + super.message, + super.path, + super.trace, + }); +} + +final class InitMetamaskErrorMetamaskInitializingAlreadyException + extends InitMetamaskErrorException { + const InitMetamaskErrorMetamaskInitializingAlreadyException({ + super.message, + super.path, + super.trace, + }) : super(errorType: 'MetamaskInitializingAlready'); +} + +final class InitMetamaskErrorMetamaskErrorException + extends InitMetamaskErrorException { + const InitMetamaskErrorMetamaskErrorException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'MetamaskError'); + final MetamaskRpcError value; +} + +final class InitMetamaskErrorTimeoutException + extends InitMetamaskErrorException { + const InitMetamaskErrorTimeoutException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'Timeout'); + final Mm2Duration value; +} + +final class InitMetamaskErrorInternalException + extends InitMetamaskErrorException { + const InitMetamaskErrorInternalException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'Internal'); + final String value; +} + +sealed class MessageErrorException extends MmRpcException { + const MessageErrorException({ + required super.errorType, + super.message, + super.path, + super.trace, + }); +} + +final class MessageErrorTelegramErrorException extends MessageErrorException { + const MessageErrorTelegramErrorException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'TelegramError'); + final TelegramError value; +} + +sealed class MmInitErrorException extends MmRpcException { + const MmInitErrorException({ + required super.errorType, + super.message, + super.path, + super.trace, + }); +} + +final class MmInitErrorCancelledException extends MmInitErrorException { + const MmInitErrorCancelledException({super.message, super.path, super.trace}) + : super(errorType: 'Cancelled'); +} + +final class MmInitErrorTimeoutException extends MmInitErrorException { + const MmInitErrorTimeoutException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'Timeout'); + final Mm2Duration value; +} + +final class MmInitErrorErrorDeserializingConfigException + extends MmInitErrorException { + const MmInitErrorErrorDeserializingConfigException({ + required this.field, + required this.error, + super.message, + super.path, + super.trace, + }) : super(errorType: 'ErrorDeserializingConfig'); + final String field; + final String error; +} + +final class MmInitErrorFieldNotFoundInConfigException + extends MmInitErrorException { + const MmInitErrorFieldNotFoundInConfigException({ + required this.field, + super.message, + super.path, + super.trace, + }) : super(errorType: 'FieldNotFoundInConfig'); + final String field; +} + +final class MmInitErrorFieldWrongValueInConfigException + extends MmInitErrorException { + const MmInitErrorFieldWrongValueInConfigException({ + required this.field, + required this.error, + super.message, + super.path, + super.trace, + }) : super(errorType: 'FieldWrongValueInConfig'); + final String field; + final String error; +} + +final class MmInitErrorP2PErrorException extends MmInitErrorException { + const MmInitErrorP2PErrorException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'P2PError'); + final P2PInitError value; +} + +final class MmInitErrorErrorCreatingDbDirException + extends MmInitErrorException { + const MmInitErrorErrorCreatingDbDirException({ + required this.pathData, + required this.error, + super.message, + super.path, + super.trace, + }) : super(errorType: 'ErrorCreatingDbDir'); + final PathBuf pathData; + final String error; +} + +final class MmInitErrorDbDirectoryIsNotWritableException + extends MmInitErrorException { + const MmInitErrorDbDirectoryIsNotWritableException({ + required this.pathData, + super.message, + super.path, + super.trace, + }) : super(errorType: 'DbDirectoryIsNotWritable'); + final String pathData; +} + +final class MmInitErrorDbFileIsNotWritableException + extends MmInitErrorException { + const MmInitErrorDbFileIsNotWritableException({ + required this.pathData, + super.message, + super.path, + super.trace, + }) : super(errorType: 'DbFileIsNotWritable'); + final String pathData; +} + +final class MmInitErrorErrorSqliteInitializingException + extends MmInitErrorException { + const MmInitErrorErrorSqliteInitializingException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'ErrorSqliteInitializing'); + final String value; +} + +final class MmInitErrorErrorDbMigratingException extends MmInitErrorException { + const MmInitErrorErrorDbMigratingException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'ErrorDbMigrating'); + final String value; +} + +final class MmInitErrorSwapsKickStartErrorException + extends MmInitErrorException { + const MmInitErrorSwapsKickStartErrorException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'SwapsKickStartError'); + final String value; +} + +final class MmInitErrorOrdersKickStartErrorException + extends MmInitErrorException { + const MmInitErrorOrdersKickStartErrorException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'OrdersKickStartError'); + final String value; +} + +final class MmInitErrorWalletInitErrorException extends MmInitErrorException { + const MmInitErrorWalletInitErrorException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'WalletInitError'); + final String value; +} + +final class MmInitErrorEventStreamerInitFailedException + extends MmInitErrorException { + const MmInitErrorEventStreamerInitFailedException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'EventStreamerInitFailed'); + final String value; +} + +final class MmInitErrorHwErrorException extends MmInitErrorException { + const MmInitErrorHwErrorException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'HwError'); + final HwRpcError value; +} + +final class MmInitErrorInternalException extends MmInitErrorException { + const MmInitErrorInternalException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'Internal'); + final String value; +} + +sealed class CancelAllOrdersErrorException extends MmRpcException { + const CancelAllOrdersErrorException({ + required super.errorType, + super.message, + super.path, + super.trace, + }); +} + +final class CancelAllOrdersErrorLegacyErrorException + extends CancelAllOrdersErrorException { + const CancelAllOrdersErrorLegacyErrorException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'LegacyError'); + final String value; +} + +sealed class CancelOrderErrorException extends MmRpcException { + const CancelOrderErrorException({ + required super.errorType, + super.message, + super.path, + super.trace, + }); +} + +final class CancelOrderErrorCannotRetrieveOrderMatchContextException + extends CancelOrderErrorException { + const CancelOrderErrorCannotRetrieveOrderMatchContextException({ + super.message, + super.path, + super.trace, + }) : super(errorType: 'CannotRetrieveOrderMatchContext'); +} + +final class CancelOrderErrorOrderBeingMatchedException + extends CancelOrderErrorException { + const CancelOrderErrorOrderBeingMatchedException({ + required this.uuid, + super.message, + super.path, + super.trace, + }) : super(errorType: 'OrderBeingMatched'); + final String uuid; +} + +final class CancelOrderErrorUUIDNotFoundException + extends CancelOrderErrorException { + const CancelOrderErrorUUIDNotFoundException({ + required this.uuid, + super.message, + super.path, + super.trace, + }) : super(errorType: 'UUIDNotFound'); + final String uuid; +} + +sealed class BestOrdersRpcErrorException extends MmRpcException { + const BestOrdersRpcErrorException({ + required super.errorType, + super.message, + super.path, + super.trace, + }); +} + +final class BestOrdersRpcErrorCoinIsWalletOnlyException + extends BestOrdersRpcErrorException { + const BestOrdersRpcErrorCoinIsWalletOnlyException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'CoinIsWalletOnly'); + final String value; +} + +final class BestOrdersRpcErrorP2PErrorException + extends BestOrdersRpcErrorException { + const BestOrdersRpcErrorP2PErrorException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'P2PError'); + final String value; +} + +final class BestOrdersRpcErrorCtxErrorException + extends BestOrdersRpcErrorException { + const BestOrdersRpcErrorCtxErrorException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'CtxError'); + final String value; +} + +sealed class OrderbookRpcErrorException extends MmRpcException { + const OrderbookRpcErrorException({ + required super.errorType, + super.message, + super.path, + super.trace, + }); +} + +final class OrderbookRpcErrorBaseRelSameException + extends OrderbookRpcErrorException { + const OrderbookRpcErrorBaseRelSameException({ + super.message, + super.path, + super.trace, + }) : super(errorType: 'BaseRelSame'); +} + +final class OrderbookRpcErrorBaseRelSameOrderbookTickersAndProtocolsException + extends OrderbookRpcErrorException { + const OrderbookRpcErrorBaseRelSameOrderbookTickersAndProtocolsException({ + super.message, + super.path, + super.trace, + }) : super(errorType: 'BaseRelSameOrderbookTickersAndProtocols'); +} + +final class OrderbookRpcErrorCoinConfigNotFoundException + extends OrderbookRpcErrorException { + const OrderbookRpcErrorCoinConfigNotFoundException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'CoinConfigNotFound'); + final String value; +} + +final class OrderbookRpcErrorCoinIsWalletOnlyException + extends OrderbookRpcErrorException { + const OrderbookRpcErrorCoinIsWalletOnlyException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'CoinIsWalletOnly'); + final String value; +} + +final class OrderbookRpcErrorP2PSubscribeErrorException + extends OrderbookRpcErrorException { + const OrderbookRpcErrorP2PSubscribeErrorException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'P2PSubscribeError'); + final String value; +} + +final class OrderbookRpcErrorInternalException + extends OrderbookRpcErrorException { + const OrderbookRpcErrorInternalException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'Internal'); + final String value; +} + +sealed class OrderProcessingErrorException extends MmRpcException { + const OrderProcessingErrorException({ + required super.errorType, + super.message, + super.path, + super.trace, + }); +} + +final class OrderProcessingErrorProviderUnknownException + extends OrderProcessingErrorException { + const OrderProcessingErrorProviderUnknownException({ + required this.keyTradePair, + super.message, + super.path, + super.trace, + }) : super(errorType: 'ProviderUnknown'); + final String keyTradePair; +} + +final class OrderProcessingErrorPriceIsZeroException + extends OrderProcessingErrorException { + const OrderProcessingErrorPriceIsZeroException({ + required this.keyTradePair, + super.message, + super.path, + super.trace, + }) : super(errorType: 'PriceIsZero'); + final String keyTradePair; +} + +final class OrderProcessingErrorLastUpdatedTimestampInvalidException + extends OrderProcessingErrorException { + const OrderProcessingErrorLastUpdatedTimestampInvalidException({ + required this.keyTradePair, + super.message, + super.path, + super.trace, + }) : super(errorType: 'LastUpdatedTimestampInvalid'); + final String keyTradePair; +} + +final class OrderProcessingErrorPriceElapsedValidityExpiredException + extends OrderProcessingErrorException { + const OrderProcessingErrorPriceElapsedValidityExpiredException({ + required this.elapsed, + required this.elapsedValidity, + required this.keyTradePair, + super.message, + super.path, + super.trace, + }) : super(errorType: 'PriceElapsedValidityExpired'); + final double elapsed; + final double elapsedValidity; + final String keyTradePair; +} + +final class OrderProcessingErrorPriceElapsedValidityUntreatableException + extends OrderProcessingErrorException { + const OrderProcessingErrorPriceElapsedValidityUntreatableException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'PriceElapsedValidityUntreatable'); + final String value; +} + +final class OrderProcessingErrorPriceBelowMinBasePriceException + extends OrderProcessingErrorException { + const OrderProcessingErrorPriceBelowMinBasePriceException({ + required this.basePrice, + required this.minBasePrice, + super.message, + super.path, + super.trace, + }) : super(errorType: 'PriceBelowMinBasePrice'); + final String basePrice; + final String minBasePrice; +} + +final class OrderProcessingErrorPriceBelowMinRelPriceException + extends OrderProcessingErrorException { + const OrderProcessingErrorPriceBelowMinRelPriceException({ + required this.relPrice, + required this.minRelPrice, + super.message, + super.path, + super.trace, + }) : super(errorType: 'PriceBelowMinRelPrice'); + final String relPrice; + final String minRelPrice; +} + +final class OrderProcessingErrorPriceBelowPairPriceException + extends OrderProcessingErrorException { + const OrderProcessingErrorPriceBelowPairPriceException({ + required this.pair, + required this.pairPrice, + required this.minPairPrice, + super.message, + super.path, + super.trace, + }) : super(errorType: 'PriceBelowPairPrice'); + final String pair; + final String pairPrice; + final String minPairPrice; +} + +final class OrderProcessingErrorAssetNotEnabledException + extends OrderProcessingErrorException { + const OrderProcessingErrorAssetNotEnabledException({ + super.message, + super.path, + super.trace, + }) : super(errorType: 'AssetNotEnabled'); +} + +final class OrderProcessingErrorInternalCoinFindErrorException + extends OrderProcessingErrorException { + const OrderProcessingErrorInternalCoinFindErrorException({ + super.message, + super.path, + super.trace, + }) : super(errorType: 'InternalCoinFindError'); +} + +final class OrderProcessingErrorBalanceInternalErrorException + extends OrderProcessingErrorException { + const OrderProcessingErrorBalanceInternalErrorException({ + super.message, + super.path, + super.trace, + }) : super(errorType: 'BalanceInternalError'); +} + +final class OrderProcessingErrorBalanceIsZeroException + extends OrderProcessingErrorException { + const OrderProcessingErrorBalanceIsZeroException({ + super.message, + super.path, + super.trace, + }) : super(errorType: 'BalanceIsZero'); +} + +final class OrderProcessingErrorOrderCreationErrorException + extends OrderProcessingErrorException { + const OrderProcessingErrorOrderCreationErrorException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'OrderCreationError'); + final String value; +} + +final class OrderProcessingErrorOrderUpdateErrorException + extends OrderProcessingErrorException { + const OrderProcessingErrorOrderUpdateErrorException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'OrderUpdateError'); + final String value; +} + +final class OrderProcessingErrorMyRecentSwapsErrorException + extends OrderProcessingErrorException { + const OrderProcessingErrorMyRecentSwapsErrorException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'MyRecentSwapsError'); + final String value; +} + +final class OrderProcessingErrorMinVolUsdAboveBalanceUsdException + extends OrderProcessingErrorException { + const OrderProcessingErrorMinVolUsdAboveBalanceUsdException({ + super.message, + super.path, + super.trace, + }) : super(errorType: 'MinVolUsdAboveBalanceUsd'); +} + +final class OrderProcessingErrorLegacyErrorException + extends OrderProcessingErrorException { + const OrderProcessingErrorLegacyErrorException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'LegacyError'); + final String value; +} + +sealed class StartSimpleMakerBotErrorException extends MmRpcException { + const StartSimpleMakerBotErrorException({ + required super.errorType, + super.message, + super.path, + super.trace, + }); +} + +final class StartSimpleMakerBotErrorAlreadyStartedException + extends StartSimpleMakerBotErrorException { + const StartSimpleMakerBotErrorAlreadyStartedException({ + super.message, + super.path, + super.trace, + }) : super(errorType: 'AlreadyStarted'); +} + +final class StartSimpleMakerBotErrorInvalidBotConfigurationException + extends StartSimpleMakerBotErrorException { + const StartSimpleMakerBotErrorInvalidBotConfigurationException({ + super.message, + super.path, + super.trace, + }) : super(errorType: 'InvalidBotConfiguration'); +} + +final class StartSimpleMakerBotErrorTransportException + extends StartSimpleMakerBotErrorException { + const StartSimpleMakerBotErrorTransportException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'Transport'); + final String value; +} + +final class StartSimpleMakerBotErrorCannotStartFromStoppingException + extends StartSimpleMakerBotErrorException { + const StartSimpleMakerBotErrorCannotStartFromStoppingException({ + super.message, + super.path, + super.trace, + }) : super(errorType: 'CannotStartFromStopping'); +} + +final class StartSimpleMakerBotErrorInternalErrorException + extends StartSimpleMakerBotErrorException { + const StartSimpleMakerBotErrorInternalErrorException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'InternalError'); + final String value; +} + +sealed class StopSimpleMakerBotErrorException extends MmRpcException { + const StopSimpleMakerBotErrorException({ + required super.errorType, + super.message, + super.path, + super.trace, + }); +} + +final class StopSimpleMakerBotErrorAlreadyStoppedException + extends StopSimpleMakerBotErrorException { + const StopSimpleMakerBotErrorAlreadyStoppedException({ + super.message, + super.path, + super.trace, + }) : super(errorType: 'AlreadyStopped'); +} + +final class StopSimpleMakerBotErrorAlreadyStoppingException + extends StopSimpleMakerBotErrorException { + const StopSimpleMakerBotErrorAlreadyStoppingException({ + super.message, + super.path, + super.trace, + }) : super(errorType: 'AlreadyStopping'); +} + +final class StopSimpleMakerBotErrorTransportException + extends StopSimpleMakerBotErrorException { + const StopSimpleMakerBotErrorTransportException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'Transport'); + final String value; +} + +final class StopSimpleMakerBotErrorInternalErrorException + extends StopSimpleMakerBotErrorException { + const StopSimpleMakerBotErrorInternalErrorException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'InternalError'); + final String value; +} + +sealed class SwapUpdateNotificationErrorException extends MmRpcException { + const SwapUpdateNotificationErrorException({ + required super.errorType, + super.message, + super.path, + super.trace, + }); +} + +final class SwapUpdateNotificationErrorMyRecentSwapsErrorException + extends SwapUpdateNotificationErrorException { + const SwapUpdateNotificationErrorMyRecentSwapsErrorException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'MyRecentSwapsError'); + final LatestSwapsErr value; +} + +final class SwapUpdateNotificationErrorSwapInfoNotAvailableException + extends SwapUpdateNotificationErrorException { + const SwapUpdateNotificationErrorSwapInfoNotAvailableException({ + super.message, + super.path, + super.trace, + }) : super(errorType: 'SwapInfoNotAvailable'); +} + +sealed class NodeVersionErrorException extends MmRpcException { + const NodeVersionErrorException({ + required super.errorType, + super.message, + super.path, + super.trace, + }); +} + +final class NodeVersionErrorInvalidRequestException + extends NodeVersionErrorException { + const NodeVersionErrorInvalidRequestException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'InvalidRequest'); + final String value; +} + +final class NodeVersionErrorDatabaseErrorException + extends NodeVersionErrorException { + const NodeVersionErrorDatabaseErrorException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'DatabaseError'); + final String value; +} + +final class NodeVersionErrorInvalidAddressException + extends NodeVersionErrorException { + const NodeVersionErrorInvalidAddressException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'InvalidAddress'); + final String value; +} + +final class NodeVersionErrorPeerIdParseErrorException + extends NodeVersionErrorException { + const NodeVersionErrorPeerIdParseErrorException( + this.value0, + this.value1, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'PeerIdParseError'); + final String value0; + final String value1; +} + +final class NodeVersionErrorUnsupportedModeException + extends NodeVersionErrorException { + const NodeVersionErrorUnsupportedModeException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'UnsupportedMode'); + final String value; +} + +final class NodeVersionErrorAlreadyRunningException + extends NodeVersionErrorException { + const NodeVersionErrorAlreadyRunningException({ + super.message, + super.path, + super.trace, + }) : super(errorType: 'AlreadyRunning'); +} + +final class NodeVersionErrorCurrentlyStoppingException + extends NodeVersionErrorException { + const NodeVersionErrorCurrentlyStoppingException({ + super.message, + super.path, + super.trace, + }) : super(errorType: 'CurrentlyStopping'); +} + +final class NodeVersionErrorNotRunningException + extends NodeVersionErrorException { + const NodeVersionErrorNotRunningException({ + super.message, + super.path, + super.trace, + }) : super(errorType: 'NotRunning'); +} + +sealed class GetLockedAmountRpcErrorException extends MmRpcException { + const GetLockedAmountRpcErrorException({ + required super.errorType, + super.message, + super.path, + super.trace, + }); +} + +final class GetLockedAmountRpcErrorNoSuchCoinException + extends GetLockedAmountRpcErrorException { + const GetLockedAmountRpcErrorNoSuchCoinException({ + required this.coin, + super.message, + super.path, + super.trace, + }) : super(errorType: 'NoSuchCoin'); + final String coin; +} + +sealed class LatestSwapsErrException extends MmRpcException { + const LatestSwapsErrException({ + required super.errorType, + super.message, + super.path, + super.trace, + }); +} + +final class LatestSwapsErrUUIDNotPresentInDbException + extends LatestSwapsErrException { + const LatestSwapsErrUUIDNotPresentInDbException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'UUIDNotPresentInDb'); + final String value; +} + +final class LatestSwapsErrUnableToLoadSavedSwapsException + extends LatestSwapsErrException { + const LatestSwapsErrUnableToLoadSavedSwapsException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'UnableToLoadSavedSwaps'); + final SavedSwapError value; +} + +final class LatestSwapsErrUnableToQuerySwapStorageException + extends LatestSwapsErrException { + const LatestSwapsErrUnableToQuerySwapStorageException({ + super.message, + super.path, + super.trace, + }) : super(errorType: 'UnableToQuerySwapStorage'); +} + +sealed class CheckBalanceErrorException extends MmRpcException { + const CheckBalanceErrorException({ + required super.errorType, + super.message, + super.path, + super.trace, + }); +} + +final class CheckBalanceErrorNotSufficientBalanceException + extends CheckBalanceErrorException { + const CheckBalanceErrorNotSufficientBalanceException({ + required this.coin, + required this.available, + required this.required, + this.lockedBySwaps, + super.message, + super.path, + super.trace, + }) : super(errorType: 'NotSufficientBalance'); + final String coin; + final BigDecimal available; + final BigDecimal required; + final BigDecimal? lockedBySwaps; +} + +final class CheckBalanceErrorNotSufficientBaseCoinBalanceException + extends CheckBalanceErrorException { + const CheckBalanceErrorNotSufficientBaseCoinBalanceException({ + required this.coin, + required this.available, + required this.required, + this.lockedBySwaps, + super.message, + super.path, + super.trace, + }) : super(errorType: 'NotSufficientBaseCoinBalance'); + final String coin; + final BigDecimal available; + final BigDecimal required; + final BigDecimal? lockedBySwaps; +} + +final class CheckBalanceErrorVolumeTooLowException + extends CheckBalanceErrorException { + const CheckBalanceErrorVolumeTooLowException({ + required this.coin, + required this.volume, + required this.threshold, + super.message, + super.path, + super.trace, + }) : super(errorType: 'VolumeTooLow'); + final String coin; + final BigDecimal volume; + final BigDecimal threshold; +} + +final class CheckBalanceErrorTransportException + extends CheckBalanceErrorException { + const CheckBalanceErrorTransportException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'Transport'); + final String value; +} + +final class CheckBalanceErrorInternalErrorException + extends CheckBalanceErrorException { + const CheckBalanceErrorInternalErrorException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'InternalError'); + final String value; +} + +sealed class MaxMakerVolRpcErrorException extends MmRpcException { + const MaxMakerVolRpcErrorException({ + required super.errorType, + super.message, + super.path, + super.trace, + }); +} + +final class MaxMakerVolRpcErrorNotSufficientBalanceException + extends MaxMakerVolRpcErrorException { + const MaxMakerVolRpcErrorNotSufficientBalanceException({ + required this.coin, + required this.available, + required this.required, + this.lockedBySwaps, + super.message, + super.path, + super.trace, + }) : super(errorType: 'NotSufficientBalance'); + final String coin; + final BigDecimal available; + final BigDecimal required; + final BigDecimal? lockedBySwaps; +} + +final class MaxMakerVolRpcErrorNotSufficientBaseCoinBalanceException + extends MaxMakerVolRpcErrorException { + const MaxMakerVolRpcErrorNotSufficientBaseCoinBalanceException({ + required this.coin, + required this.available, + required this.required, + this.lockedBySwaps, + super.message, + super.path, + super.trace, + }) : super(errorType: 'NotSufficientBaseCoinBalance'); + final String coin; + final BigDecimal available; + final BigDecimal required; + final BigDecimal? lockedBySwaps; +} + +final class MaxMakerVolRpcErrorVolumeTooLowException + extends MaxMakerVolRpcErrorException { + const MaxMakerVolRpcErrorVolumeTooLowException({ + required this.coin, + required this.volume, + required this.threshold, + super.message, + super.path, + super.trace, + }) : super(errorType: 'VolumeTooLow'); + final String coin; + final BigDecimal volume; + final BigDecimal threshold; +} + +final class MaxMakerVolRpcErrorNoSuchCoinException + extends MaxMakerVolRpcErrorException { + const MaxMakerVolRpcErrorNoSuchCoinException({ + required this.coin, + super.message, + super.path, + super.trace, + }) : super(errorType: 'NoSuchCoin'); + final String coin; +} + +final class MaxMakerVolRpcErrorTransportException + extends MaxMakerVolRpcErrorException { + const MaxMakerVolRpcErrorTransportException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'Transport'); + final String value; +} + +final class MaxMakerVolRpcErrorInternalErrorException + extends MaxMakerVolRpcErrorException { + const MaxMakerVolRpcErrorInternalErrorException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'InternalError'); + final String value; +} + +sealed class RecreateSwapErrorException extends MmRpcException { + const RecreateSwapErrorException({ + required super.errorType, + super.message, + super.path, + super.trace, + }); +} + +final class RecreateSwapErrorSwapIsNotStartedException + extends RecreateSwapErrorException { + const RecreateSwapErrorSwapIsNotStartedException({ + super.message, + super.path, + super.trace, + }) : super(errorType: 'SwapIsNotStarted'); +} + +final class RecreateSwapErrorSwapIsNotNegotiatedException + extends RecreateSwapErrorException { + const RecreateSwapErrorSwapIsNotNegotiatedException({ + super.message, + super.path, + super.trace, + }) : super(errorType: 'SwapIsNotNegotiated'); +} + +final class RecreateSwapErrorUnexpectedEventException + extends RecreateSwapErrorException { + const RecreateSwapErrorUnexpectedEventException({ + required this.expected, + required this.found, + super.message, + super.path, + super.trace, + }) : super(errorType: 'UnexpectedEvent'); + final String expected; + final String found; +} + +final class RecreateSwapErrorNoSuchCoinException + extends RecreateSwapErrorException { + const RecreateSwapErrorNoSuchCoinException({ + required this.coin, + super.message, + super.path, + super.trace, + }) : super(errorType: 'NoSuchCoin'); + final String coin; +} + +final class RecreateSwapErrorNoSecretHashException + extends RecreateSwapErrorException { + const RecreateSwapErrorNoSecretHashException({ + super.message, + super.path, + super.trace, + }) : super(errorType: 'NoSecretHash'); +} + +final class RecreateSwapErrorInternalException + extends RecreateSwapErrorException { + const RecreateSwapErrorInternalException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'Internal'); + final String value; +} + +sealed class ActiveSwapsErrException extends MmRpcException { + const ActiveSwapsErrException({ + required super.errorType, + super.message, + super.path, + super.trace, + }); +} + +final class ActiveSwapsErrInternalException extends ActiveSwapsErrException { + const ActiveSwapsErrInternalException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'Internal'); + final String value; +} + +sealed class MyRecentSwapsErrException extends MmRpcException { + const MyRecentSwapsErrException({ + required super.errorType, + super.message, + super.path, + super.trace, + }); +} + +final class MyRecentSwapsErrFromUuidSwapNotFoundException + extends MyRecentSwapsErrException { + const MyRecentSwapsErrFromUuidSwapNotFoundException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'FromUuidSwapNotFound'); + final String value; +} + +final class MyRecentSwapsErrInvalidTimeStampRangeException + extends MyRecentSwapsErrException { + const MyRecentSwapsErrInvalidTimeStampRangeException({ + super.message, + super.path, + super.trace, + }) : super(errorType: 'InvalidTimeStampRange'); +} + +final class MyRecentSwapsErrDbErrorException extends MyRecentSwapsErrException { + const MyRecentSwapsErrDbErrorException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'DbError'); + final String value; +} + +sealed class MySwapStatusErrorException extends MmRpcException { + const MySwapStatusErrorException({ + required super.errorType, + super.message, + super.path, + super.trace, + }); +} + +final class MySwapStatusErrorNoSwapWithUuidException + extends MySwapStatusErrorException { + const MySwapStatusErrorNoSwapWithUuidException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'NoSwapWithUuid'); + final String value; +} + +final class MySwapStatusErrorUnsupportedSwapTypeException + extends MySwapStatusErrorException { + const MySwapStatusErrorUnsupportedSwapTypeException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'UnsupportedSwapType'); + final int value; +} + +final class MySwapStatusErrorDbErrorException + extends MySwapStatusErrorException { + const MySwapStatusErrorDbErrorException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'DbError'); + final String value; +} + +sealed class TradePreimageRpcErrorException extends MmRpcException { + const TradePreimageRpcErrorException({ + required super.errorType, + super.message, + super.path, + super.trace, + }); +} + +final class TradePreimageRpcErrorNotSufficientBalanceException + extends TradePreimageRpcErrorException { + const TradePreimageRpcErrorNotSufficientBalanceException({ + required this.coin, + required this.available, + required this.required, + this.lockedBySwaps, + super.message, + super.path, + super.trace, + }) : super(errorType: 'NotSufficientBalance'); + final String coin; + final BigDecimal available; + final BigDecimal required; + final BigDecimal? lockedBySwaps; +} + +final class TradePreimageRpcErrorNotSufficientBaseCoinBalanceException + extends TradePreimageRpcErrorException { + const TradePreimageRpcErrorNotSufficientBaseCoinBalanceException({ + required this.coin, + required this.available, + required this.required, + this.lockedBySwaps, + super.message, + super.path, + super.trace, + }) : super(errorType: 'NotSufficientBaseCoinBalance'); + final String coin; + final BigDecimal available; + final BigDecimal required; + final BigDecimal? lockedBySwaps; +} + +final class TradePreimageRpcErrorVolumeTooLowException + extends TradePreimageRpcErrorException { + const TradePreimageRpcErrorVolumeTooLowException({ + required this.coin, + required this.volume, + required this.threshold, + super.message, + super.path, + super.trace, + }) : super(errorType: 'VolumeTooLow'); + final String coin; + final BigDecimal volume; + final BigDecimal threshold; +} + +final class TradePreimageRpcErrorNoSuchCoinException + extends TradePreimageRpcErrorException { + const TradePreimageRpcErrorNoSuchCoinException({ + required this.coin, + super.message, + super.path, + super.trace, + }) : super(errorType: 'NoSuchCoin'); + final String coin; +} + +final class TradePreimageRpcErrorCoinIsWalletOnlyException + extends TradePreimageRpcErrorException { + const TradePreimageRpcErrorCoinIsWalletOnlyException({ + required this.coin, + super.message, + super.path, + super.trace, + }) : super(errorType: 'CoinIsWalletOnly'); + final String coin; +} + +final class TradePreimageRpcErrorBaseEqualRelException + extends TradePreimageRpcErrorException { + const TradePreimageRpcErrorBaseEqualRelException({ + super.message, + super.path, + super.trace, + }) : super(errorType: 'BaseEqualRel'); +} + +final class TradePreimageRpcErrorInvalidParamException + extends TradePreimageRpcErrorException { + const TradePreimageRpcErrorInvalidParamException({ + required this.param, + required this.reason, + super.message, + super.path, + super.trace, + }) : super(errorType: 'InvalidParam'); + final String param; + final String reason; +} + +final class TradePreimageRpcErrorPriceTooLowException + extends TradePreimageRpcErrorException { + const TradePreimageRpcErrorPriceTooLowException({ + required this.price, + required this.threshold, + super.message, + super.path, + super.trace, + }) : super(errorType: 'PriceTooLow'); + final BigDecimal price; + final BigDecimal threshold; +} + +final class TradePreimageRpcErrorTransportException + extends TradePreimageRpcErrorException { + const TradePreimageRpcErrorTransportException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'Transport'); + final String value; +} + +final class TradePreimageRpcErrorInternalErrorException + extends TradePreimageRpcErrorException { + const TradePreimageRpcErrorInternalErrorException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'InternalError'); + final String value; +} + +sealed class MnemonicRpcErrorException extends MmRpcException { + const MnemonicRpcErrorException({ + required super.errorType, + super.message, + super.path, + super.trace, + }); +} + +final class MnemonicRpcErrorInvalidRequestException + extends MnemonicRpcErrorException { + const MnemonicRpcErrorInvalidRequestException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'InvalidRequest'); + final String value; +} + +final class MnemonicRpcErrorWalletsStorageErrorException + extends MnemonicRpcErrorException { + const MnemonicRpcErrorWalletsStorageErrorException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'WalletsStorageError'); + final String value; +} + +final class MnemonicRpcErrorInternalException + extends MnemonicRpcErrorException { + const MnemonicRpcErrorInternalException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'Internal'); + final String value; +} + +final class MnemonicRpcErrorInvalidPasswordException + extends MnemonicRpcErrorException { + const MnemonicRpcErrorInvalidPasswordException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'InvalidPassword'); + final String value; +} + +final class MnemonicRpcErrorPasswordPolicyViolationException + extends MnemonicRpcErrorException { + const MnemonicRpcErrorPasswordPolicyViolationException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'PasswordPolicyViolation'); + final String value; +} + +sealed class TelegramErrorException extends MmRpcException { + const TelegramErrorException({ + required super.errorType, + super.message, + super.path, + super.trace, + }); +} + +final class TelegramErrorRequestErrorException extends TelegramErrorException { + const TelegramErrorRequestErrorException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'RequestError'); + final SlurpError value; +} + +sealed class DispatcherErrorException extends MmRpcException { + const DispatcherErrorException({ + required super.errorType, + super.message, + super.path, + super.trace, + }); +} + +final class DispatcherErrorBannedException extends DispatcherErrorException { + const DispatcherErrorBannedException({super.message, super.path, super.trace}) + : super(errorType: 'Banned'); +} + +final class DispatcherErrorNoSuchMethodException + extends DispatcherErrorException { + const DispatcherErrorNoSuchMethodException({ + super.message, + super.path, + super.trace, + }) : super(errorType: 'NoSuchMethod'); +} + +final class DispatcherErrorInvalidRequestException + extends DispatcherErrorException { + const DispatcherErrorInvalidRequestException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'InvalidRequest'); + final String value; +} + +final class DispatcherErrorLocalHostOnlyException + extends DispatcherErrorException { + const DispatcherErrorLocalHostOnlyException({ + super.message, + super.path, + super.trace, + }) : super(errorType: 'LocalHostOnly'); +} + +final class DispatcherErrorUserpassIsNotSetException + extends DispatcherErrorException { + const DispatcherErrorUserpassIsNotSetException({ + super.message, + super.path, + super.trace, + }) : super(errorType: 'UserpassIsNotSet'); +} + +final class DispatcherErrorUserpassIsInvalidException + extends DispatcherErrorException { + const DispatcherErrorUserpassIsInvalidException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'UserpassIsInvalid'); + final RateLimitError value; +} + +final class DispatcherErrorInvalidMmRpcVersionException + extends DispatcherErrorException { + const DispatcherErrorInvalidMmRpcVersionException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'InvalidMmRpcVersion'); + final String value; +} + +sealed class ApiIntegrationRpcErrorException extends MmRpcException { + const ApiIntegrationRpcErrorException({ + required super.errorType, + super.message, + super.path, + super.trace, + }); +} + +final class ApiIntegrationRpcErrorNoSuchCoinException + extends ApiIntegrationRpcErrorException { + const ApiIntegrationRpcErrorNoSuchCoinException({ + required this.coin, + super.message, + super.path, + super.trace, + }) : super(errorType: 'NoSuchCoin'); + final String coin; +} + +final class ApiIntegrationRpcErrorCoinTypeErrorException + extends ApiIntegrationRpcErrorException { + const ApiIntegrationRpcErrorCoinTypeErrorException({ + super.message, + super.path, + super.trace, + }) : super(errorType: 'CoinTypeError'); +} + +final class ApiIntegrationRpcErrorNftProtocolNotSupportedException + extends ApiIntegrationRpcErrorException { + const ApiIntegrationRpcErrorNftProtocolNotSupportedException({ + super.message, + super.path, + super.trace, + }) : super(errorType: 'NftProtocolNotSupported'); +} + +final class ApiIntegrationRpcErrorChainNotSupportedException + extends ApiIntegrationRpcErrorException { + const ApiIntegrationRpcErrorChainNotSupportedException({ + super.message, + super.path, + super.trace, + }) : super(errorType: 'ChainNotSupported'); +} + +final class ApiIntegrationRpcErrorDifferentChainsException + extends ApiIntegrationRpcErrorException { + const ApiIntegrationRpcErrorDifferentChainsException({ + super.message, + super.path, + super.trace, + }) : super(errorType: 'DifferentChains'); +} + +final class ApiIntegrationRpcErrorMyAddressErrorException + extends ApiIntegrationRpcErrorException { + const ApiIntegrationRpcErrorMyAddressErrorException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'MyAddressError'); + final String value; +} + +final class ApiIntegrationRpcErrorNumberErrorException + extends ApiIntegrationRpcErrorException { + const ApiIntegrationRpcErrorNumberErrorException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'NumberError'); + final String value; +} + +final class ApiIntegrationRpcErrorInvalidParamException + extends ApiIntegrationRpcErrorException { + const ApiIntegrationRpcErrorInvalidParamException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'InvalidParam'); + final String value; +} + +final class ApiIntegrationRpcErrorOutOfBoundsException + extends ApiIntegrationRpcErrorException { + const ApiIntegrationRpcErrorOutOfBoundsException({ + required this.param, + required this.value, + required this.min, + required this.max, + super.message, + super.path, + super.trace, + }) : super(errorType: 'OutOfBounds'); + final String param; + final String value; + final String min; + final String max; +} + +final class ApiIntegrationRpcErrorOneInchAllowanceNotEnoughException + extends ApiIntegrationRpcErrorException { + const ApiIntegrationRpcErrorOneInchAllowanceNotEnoughException({ + required this.allowance, + required this.amount, + super.message, + super.path, + super.trace, + }) : super(errorType: 'OneInchAllowanceNotEnough'); + final U256 allowance; + final U256 amount; +} + +final class ApiIntegrationRpcErrorOneInchErrorException + extends ApiIntegrationRpcErrorException { + const ApiIntegrationRpcErrorOneInchErrorException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'OneInchError'); + final ApiClientError value; +} + +final class ApiIntegrationRpcErrorApiDataErrorException + extends ApiIntegrationRpcErrorException { + const ApiIntegrationRpcErrorApiDataErrorException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'ApiDataError'); + final String value; +} + +final class ApiIntegrationRpcErrorInternalErrorException + extends ApiIntegrationRpcErrorException { + const ApiIntegrationRpcErrorInternalErrorException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'InternalError'); + final String value; +} + +final class ApiIntegrationRpcErrorBestLrSwapNotFoundException + extends ApiIntegrationRpcErrorException { + const ApiIntegrationRpcErrorBestLrSwapNotFoundException({ + super.message, + super.path, + super.trace, + }) : super(errorType: 'BestLrSwapNotFound'); +} + +sealed class GetPublicKeyErrorException extends MmRpcException { + const GetPublicKeyErrorException({ + required super.errorType, + super.message, + super.path, + super.trace, + }); +} + +final class GetPublicKeyErrorInternalException + extends GetPublicKeyErrorException { + const GetPublicKeyErrorInternalException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'Internal'); + final String value; +} + +sealed class Erc20CallErrorException extends MmRpcException { + const Erc20CallErrorException({ + required super.errorType, + super.message, + super.path, + super.trace, + }); +} + +final class Erc20CallErrorNoSuchCoinException extends Erc20CallErrorException { + const Erc20CallErrorNoSuchCoinException({ + required this.coin, + super.message, + super.path, + super.trace, + }) : super(errorType: 'NoSuchCoin'); + final String coin; +} + +final class Erc20CallErrorCoinNotSupportedException + extends Erc20CallErrorException { + const Erc20CallErrorCoinNotSupportedException({ + required this.coin, + super.message, + super.path, + super.trace, + }) : super(errorType: 'CoinNotSupported'); + final String coin; +} + +final class Erc20CallErrorInvalidParamException + extends Erc20CallErrorException { + const Erc20CallErrorInvalidParamException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'InvalidParam'); + final String value; +} + +final class Erc20CallErrorTransactionErrorException + extends Erc20CallErrorException { + const Erc20CallErrorTransactionErrorException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'TransactionError'); + final String value; +} + +final class Erc20CallErrorWeb3RpcErrorException + extends Erc20CallErrorException { + const Erc20CallErrorWeb3RpcErrorException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'Web3RpcError'); + final String value; +} + +sealed class TokenInfoErrorException extends MmRpcException { + const TokenInfoErrorException({ + required super.errorType, + super.message, + super.path, + super.trace, + }); +} + +final class TokenInfoErrorNoSuchCoinException extends TokenInfoErrorException { + const TokenInfoErrorNoSuchCoinException({ + required this.coin, + super.message, + super.path, + super.trace, + }) : super(errorType: 'NoSuchCoin'); + final String coin; +} + +final class TokenInfoErrorUnsupportedTokenProtocolException + extends TokenInfoErrorException { + const TokenInfoErrorUnsupportedTokenProtocolException({ + required this.protocol, + super.message, + super.path, + super.trace, + }) : super(errorType: 'UnsupportedTokenProtocol'); + final String protocol; +} + +final class TokenInfoErrorInvalidRequestException + extends TokenInfoErrorException { + const TokenInfoErrorInvalidRequestException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'InvalidRequest'); + final String value; +} + +final class TokenInfoErrorRetrieveInfoErrorException + extends TokenInfoErrorException { + const TokenInfoErrorRetrieveInfoErrorException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'RetrieveInfoError'); + final String value; +} + +sealed class TrezorConnectionErrorException extends MmRpcException { + const TrezorConnectionErrorException({ + required super.errorType, + super.message, + super.path, + super.trace, + }); +} + +final class TrezorConnectionErrorTrezorNotInitializedException + extends TrezorConnectionErrorException { + const TrezorConnectionErrorTrezorNotInitializedException({ + super.message, + super.path, + super.trace, + }) : super(errorType: 'TrezorNotInitialized'); +} + +final class TrezorConnectionErrorFoundUnexpectedDeviceException + extends TrezorConnectionErrorException { + const TrezorConnectionErrorFoundUnexpectedDeviceException({ + super.message, + super.path, + super.trace, + }) : super(errorType: 'FoundUnexpectedDevice'); +} + +final class TrezorConnectionErrorInternalException + extends TrezorConnectionErrorException { + const TrezorConnectionErrorInternalException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'Internal'); + final String value; +} + +sealed class RateLimitErrorException extends MmRpcException { + const RateLimitErrorException({ + required super.errorType, + super.message, + super.path, + super.trace, + }); +} + +final class RateLimitErrorNbAttemptsLeftException + extends RateLimitErrorException { + const RateLimitErrorNbAttemptsLeftException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'NbAttemptsLeft'); + final int value; +} + +sealed class BalanceStreamingRequestErrorException extends MmRpcException { + const BalanceStreamingRequestErrorException({ + required super.errorType, + super.message, + super.path, + super.trace, + }); +} + +final class BalanceStreamingRequestErrorEnableErrorException + extends BalanceStreamingRequestErrorException { + const BalanceStreamingRequestErrorEnableErrorException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'EnableError'); + final String value; +} + +final class BalanceStreamingRequestErrorCoinNotFoundException + extends BalanceStreamingRequestErrorException { + const BalanceStreamingRequestErrorCoinNotFoundException({ + super.message, + super.path, + super.trace, + }) : super(errorType: 'CoinNotFound'); +} + +final class BalanceStreamingRequestErrorCoinNotSupportedException + extends BalanceStreamingRequestErrorException { + const BalanceStreamingRequestErrorCoinNotSupportedException({ + super.message, + super.path, + super.trace, + }) : super(errorType: 'CoinNotSupported'); +} + +final class BalanceStreamingRequestErrorInternalException + extends BalanceStreamingRequestErrorException { + const BalanceStreamingRequestErrorInternalException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'Internal'); + final String value; +} + +sealed class DisableStreamingRequestErrorException extends MmRpcException { + const DisableStreamingRequestErrorException({ + required super.errorType, + super.message, + super.path, + super.trace, + }); +} + +final class DisableStreamingRequestErrorDisableErrorException + extends DisableStreamingRequestErrorException { + const DisableStreamingRequestErrorDisableErrorException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'DisableError'); + final String value; +} + +sealed class FeeStreamingRequestErrorException extends MmRpcException { + const FeeStreamingRequestErrorException({ + required super.errorType, + super.message, + super.path, + super.trace, + }); +} + +final class FeeStreamingRequestErrorEnableErrorException + extends FeeStreamingRequestErrorException { + const FeeStreamingRequestErrorEnableErrorException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'EnableError'); + final String value; +} + +final class FeeStreamingRequestErrorCoinNotFoundException + extends FeeStreamingRequestErrorException { + const FeeStreamingRequestErrorCoinNotFoundException({ + super.message, + super.path, + super.trace, + }) : super(errorType: 'CoinNotFound'); +} + +final class FeeStreamingRequestErrorCoinNotSupportedException + extends FeeStreamingRequestErrorException { + const FeeStreamingRequestErrorCoinNotSupportedException({ + super.message, + super.path, + super.trace, + }) : super(errorType: 'CoinNotSupported'); +} + +final class FeeStreamingRequestErrorInternalException + extends FeeStreamingRequestErrorException { + const FeeStreamingRequestErrorInternalException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'Internal'); + final String value; +} + +sealed class HeartbeatRequestErrorException extends MmRpcException { + const HeartbeatRequestErrorException({ + required super.errorType, + super.message, + super.path, + super.trace, + }); +} + +final class HeartbeatRequestErrorEnableErrorException + extends HeartbeatRequestErrorException { + const HeartbeatRequestErrorEnableErrorException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'EnableError'); + final String value; +} + +sealed class NetworkStreamingRequestErrorException extends MmRpcException { + const NetworkStreamingRequestErrorException({ + required super.errorType, + super.message, + super.path, + super.trace, + }); +} + +final class NetworkStreamingRequestErrorEnableErrorException + extends NetworkStreamingRequestErrorException { + const NetworkStreamingRequestErrorEnableErrorException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'EnableError'); + final String value; +} + +sealed class OrderbookStreamingRequestErrorException extends MmRpcException { + const OrderbookStreamingRequestErrorException({ + required super.errorType, + super.message, + super.path, + super.trace, + }); +} + +final class OrderbookStreamingRequestErrorEnableErrorException + extends OrderbookStreamingRequestErrorException { + const OrderbookStreamingRequestErrorEnableErrorException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'EnableError'); + final String value; +} + +sealed class OrderStatusStreamingRequestErrorException extends MmRpcException { + const OrderStatusStreamingRequestErrorException({ + required super.errorType, + super.message, + super.path, + super.trace, + }); +} + +final class OrderStatusStreamingRequestErrorEnableErrorException + extends OrderStatusStreamingRequestErrorException { + const OrderStatusStreamingRequestErrorEnableErrorException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'EnableError'); + final String value; +} + +sealed class SwapStatusStreamingRequestErrorException extends MmRpcException { + const SwapStatusStreamingRequestErrorException({ + required super.errorType, + super.message, + super.path, + super.trace, + }); +} + +final class SwapStatusStreamingRequestErrorEnableErrorException + extends SwapStatusStreamingRequestErrorException { + const SwapStatusStreamingRequestErrorEnableErrorException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'EnableError'); + final String value; +} + +sealed class TxHistoryStreamingRequestErrorException extends MmRpcException { + const TxHistoryStreamingRequestErrorException({ + required super.errorType, + super.message, + super.path, + super.trace, + }); +} + +final class TxHistoryStreamingRequestErrorEnableErrorException + extends TxHistoryStreamingRequestErrorException { + const TxHistoryStreamingRequestErrorEnableErrorException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'EnableError'); + final String value; +} + +final class TxHistoryStreamingRequestErrorCoinNotFoundException + extends TxHistoryStreamingRequestErrorException { + const TxHistoryStreamingRequestErrorCoinNotFoundException({ + super.message, + super.path, + super.trace, + }) : super(errorType: 'CoinNotFound'); +} + +final class TxHistoryStreamingRequestErrorCoinNotSupportedException + extends TxHistoryStreamingRequestErrorException { + const TxHistoryStreamingRequestErrorCoinNotSupportedException({ + super.message, + super.path, + super.trace, + }) : super(errorType: 'CoinNotSupported'); +} + +final class TxHistoryStreamingRequestErrorInternalException + extends TxHistoryStreamingRequestErrorException { + const TxHistoryStreamingRequestErrorInternalException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'Internal'); + final String value; +} + +sealed class WalletConnectRpcErrorException extends MmRpcException { + const WalletConnectRpcErrorException({ + required super.errorType, + super.message, + super.path, + super.trace, + }); +} + +final class WalletConnectRpcErrorInternalErrorException + extends WalletConnectRpcErrorException { + const WalletConnectRpcErrorInternalErrorException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'InternalError'); + final String value; +} + +final class WalletConnectRpcErrorInitializationErrorException + extends WalletConnectRpcErrorException { + const WalletConnectRpcErrorInitializationErrorException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'InitializationError'); + final String value; +} + +final class WalletConnectRpcErrorSessionRequestErrorException + extends WalletConnectRpcErrorException { + const WalletConnectRpcErrorSessionRequestErrorException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'SessionRequestError'); + final String value; +} + +sealed class AnErrorException extends MmRpcException { + const AnErrorException({ + required super.errorType, + super.message, + super.path, + super.trace, + }); +} + +final class AnErrorNotSufficientBalanceException extends AnErrorException { + const AnErrorNotSufficientBalanceException({ + required this.missing, + super.message, + super.path, + super.trace, + }) : super(errorType: 'NotSufficientBalance'); + final int missing; +} + +sealed class CancelRpcTaskErrorException extends MmRpcException { + const CancelRpcTaskErrorException({ + required super.errorType, + super.message, + super.path, + super.trace, + }); +} + +final class CancelRpcTaskErrorNoSuchTaskException + extends CancelRpcTaskErrorException { + const CancelRpcTaskErrorNoSuchTaskException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'NoSuchTask'); + final TaskId value; +} + +final class CancelRpcTaskErrorTaskFinishedException + extends CancelRpcTaskErrorException { + const CancelRpcTaskErrorTaskFinishedException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'TaskFinished'); + final TaskId value; +} + +final class CancelRpcTaskErrorInternalException + extends CancelRpcTaskErrorException { + const CancelRpcTaskErrorInternalException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'Internal'); + final String value; +} + +sealed class RpcTaskStatusErrorException extends MmRpcException { + const RpcTaskStatusErrorException({ + required super.errorType, + super.message, + super.path, + super.trace, + }); +} + +final class RpcTaskStatusErrorNoSuchTaskException + extends RpcTaskStatusErrorException { + const RpcTaskStatusErrorNoSuchTaskException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'NoSuchTask'); + final TaskId value; +} + +final class RpcTaskStatusErrorInternalException + extends RpcTaskStatusErrorException { + const RpcTaskStatusErrorInternalException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'Internal'); + final String value; +} + +sealed class RpcTaskUserActionErrorException extends MmRpcException { + const RpcTaskUserActionErrorException({ + required super.errorType, + super.message, + super.path, + super.trace, + }); +} + +final class RpcTaskUserActionErrorNoSuchTaskException + extends RpcTaskUserActionErrorException { + const RpcTaskUserActionErrorNoSuchTaskException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'NoSuchTask'); + final TaskId value; +} + +final class RpcTaskUserActionErrorInternalException + extends RpcTaskUserActionErrorException { + const RpcTaskUserActionErrorInternalException( + this.value, { + super.message, + super.path, + super.trace, + }) : super(errorType: 'Internal'); + final String value; +} + +// Auto-generated from SiaCoinBuildError unit variant UnsupportedPrivKeyPolicy mapped through stringified error field +final class SiaCoinInitErrorUnsupportedPrivKeyPolicyException + extends SiaCoinInitErrorException { + const SiaCoinInitErrorUnsupportedPrivKeyPolicyException({ + required this.ticker, + super.message, + super.path, + super.trace, + }) : super(errorType: 'CoinCreationError'); + final String ticker; +} + +// Auto-generated from HwError unit variant NoTrezorDeviceAvailable mapped through stringified error field +final class InitUtxoStandardErrorNoTrezorDeviceAvailableException + extends InitUtxoStandardErrorException { + const InitUtxoStandardErrorNoTrezorDeviceAvailableException({ + required this.ticker, + super.message, + super.path, + super.trace, + }) : super(errorType: 'CoinCreationError'); + final String ticker; +} + +// Auto-generated from HwError unit variant FoundUnexpectedDevice mapped through stringified error field +final class InitUtxoStandardErrorFoundUnexpectedDeviceException + extends InitUtxoStandardErrorException { + const InitUtxoStandardErrorFoundUnexpectedDeviceException({ + required this.ticker, + super.message, + super.path, + super.trace, + }) : super(errorType: 'CoinCreationError'); + final String ticker; +} + +// Auto-generated from HwError unit variant DeviceDisconnected mapped through stringified error field +final class InitUtxoStandardErrorDeviceDisconnectedException + extends InitUtxoStandardErrorException { + const InitUtxoStandardErrorDeviceDisconnectedException({ + required this.ticker, + super.message, + super.path, + super.trace, + }) : super(errorType: 'CoinCreationError'); + final String ticker; +} + +// Auto-generated from HwError unit variant InvalidPin mapped through stringified error field +final class InitUtxoStandardErrorInvalidPinException + extends InitUtxoStandardErrorException { + const InitUtxoStandardErrorInvalidPinException({ + required this.ticker, + super.message, + super.path, + super.trace, + }) : super(errorType: 'CoinCreationError'); + final String ticker; +} + +// Auto-generated from HwError unit variant UnexpectedMessage mapped through stringified error field +final class InitUtxoStandardErrorUnexpectedMessageException + extends InitUtxoStandardErrorException { + const InitUtxoStandardErrorUnexpectedMessageException({ + required this.ticker, + super.message, + super.path, + super.trace, + }) : super(errorType: 'CoinCreationError'); + final String ticker; +} + +// Auto-generated from HwError unit variant ButtonExpected mapped through stringified error field +final class InitUtxoStandardErrorButtonExpectedException + extends InitUtxoStandardErrorException { + const InitUtxoStandardErrorButtonExpectedException({ + required this.ticker, + super.message, + super.path, + super.trace, + }) : super(errorType: 'CoinCreationError'); + final String ticker; +} + +// Auto-generated from HwError unit variant DataError mapped through stringified error field +final class InitUtxoStandardErrorDataErrorException + extends InitUtxoStandardErrorException { + const InitUtxoStandardErrorDataErrorException({ + required this.ticker, + super.message, + super.path, + super.trace, + }) : super(errorType: 'CoinCreationError'); + final String ticker; +} + +// Auto-generated from HwError unit variant PinExpected mapped through stringified error field +final class InitUtxoStandardErrorPinExpectedException + extends InitUtxoStandardErrorException { + const InitUtxoStandardErrorPinExpectedException({ + required this.ticker, + super.message, + super.path, + super.trace, + }) : super(errorType: 'CoinCreationError'); + final String ticker; +} + +// Auto-generated from HwError unit variant InvalidSignature mapped through stringified error field +final class InitUtxoStandardErrorInvalidSignatureException + extends InitUtxoStandardErrorException { + const InitUtxoStandardErrorInvalidSignatureException({ + required this.ticker, + super.message, + super.path, + super.trace, + }) : super(errorType: 'CoinCreationError'); + final String ticker; +} + +// Auto-generated from HwError unit variant ProcessError mapped through stringified error field +final class InitUtxoStandardErrorProcessErrorException + extends InitUtxoStandardErrorException { + const InitUtxoStandardErrorProcessErrorException({ + required this.ticker, + super.message, + super.path, + super.trace, + }) : super(errorType: 'CoinCreationError'); + final String ticker; +} + +// Auto-generated from HwError unit variant NotEnoughFunds mapped through stringified error field +final class InitUtxoStandardErrorNotEnoughFundsException + extends InitUtxoStandardErrorException { + const InitUtxoStandardErrorNotEnoughFundsException({ + required this.ticker, + super.message, + super.path, + super.trace, + }) : super(errorType: 'CoinCreationError'); + final String ticker; +} + +// Auto-generated from HwError unit variant NotInitialized mapped through stringified error field +final class InitUtxoStandardErrorNotInitializedException + extends InitUtxoStandardErrorException { + const InitUtxoStandardErrorNotInitializedException({ + required this.ticker, + super.message, + super.path, + super.trace, + }) : super(errorType: 'CoinCreationError'); + final String ticker; +} + +// Auto-generated from HwError unit variant WipeCodeMismatch mapped through stringified error field +final class InitUtxoStandardErrorWipeCodeMismatchException + extends InitUtxoStandardErrorException { + const InitUtxoStandardErrorWipeCodeMismatchException({ + required this.ticker, + super.message, + super.path, + super.trace, + }) : super(errorType: 'CoinCreationError'); + final String ticker; +} + +// Auto-generated from HwError unit variant InvalidSession mapped through stringified error field +final class InitUtxoStandardErrorInvalidSessionException + extends InitUtxoStandardErrorException { + const InitUtxoStandardErrorInvalidSessionException({ + required this.ticker, + super.message, + super.path, + super.trace, + }) : super(errorType: 'CoinCreationError'); + final String ticker; +} + +// Auto-generated from HwError unit variant FirmwareError mapped through stringified error field +final class InitUtxoStandardErrorFirmwareErrorException + extends InitUtxoStandardErrorException { + const InitUtxoStandardErrorFirmwareErrorException({ + required this.ticker, + super.message, + super.path, + super.trace, + }) : super(errorType: 'CoinCreationError'); + final String ticker; +} + +// Auto-generated from HwError unit variant FailureMessageNotFound mapped through stringified error field +final class InitUtxoStandardErrorFailureMessageNotFoundException + extends InitUtxoStandardErrorException { + const InitUtxoStandardErrorFailureMessageNotFoundException({ + required this.ticker, + super.message, + super.path, + super.trace, + }) : super(errorType: 'CoinCreationError'); + final String ticker; +} + +// Auto-generated from HwError unit variant UserCancelled mapped through stringified error field +final class InitUtxoStandardErrorUserCancelledException + extends InitUtxoStandardErrorException { + const InitUtxoStandardErrorUserCancelledException({ + required this.ticker, + super.message, + super.path, + super.trace, + }) : super(errorType: 'CoinCreationError'); + final String ticker; +} + +// Auto-generated from HwError unit variant PongMessageMismatch mapped through stringified error field +final class InitUtxoStandardErrorPongMessageMismatchException + extends InitUtxoStandardErrorException { + const InitUtxoStandardErrorPongMessageMismatchException({ + required this.ticker, + super.message, + super.path, + super.trace, + }) : super(errorType: 'CoinCreationError'); + final String ticker; +} + +// Auto-generated from UtxoCoinBuildError unit variant NativeRpcNotSupportedInWasm mapped through stringified error field +final class InitUtxoStandardErrorNativeRpcNotSupportedInWasmException + extends InitUtxoStandardErrorException { + const InitUtxoStandardErrorNativeRpcNotSupportedInWasmException({ + required this.ticker, + super.message, + super.path, + super.trace, + }) : super(errorType: 'CoinCreationError'); + final String ticker; +} + +// Auto-generated from UtxoCoinBuildError unit variant RpcPortIsNotSet mapped through stringified error field +final class InitUtxoStandardErrorRpcPortIsNotSetException + extends InitUtxoStandardErrorException { + const InitUtxoStandardErrorRpcPortIsNotSetException({ + required this.ticker, + super.message, + super.path, + super.trace, + }) : super(errorType: 'CoinCreationError'); + final String ticker; +} + +// Auto-generated from UtxoCoinBuildError unit variant CantDetectUserHome mapped through stringified error field +final class InitUtxoStandardErrorCantDetectUserHomeException + extends InitUtxoStandardErrorException { + const InitUtxoStandardErrorCantDetectUserHomeException({ + required this.ticker, + super.message, + super.path, + super.trace, + }) : super(errorType: 'CoinCreationError'); + final String ticker; +} + +// Auto-generated from UtxoCoinBuildError unit variant HwContextNotInitialized mapped through stringified error field +final class InitUtxoStandardErrorHwContextNotInitializedException + extends InitUtxoStandardErrorException { + const InitUtxoStandardErrorHwContextNotInitializedException({ + required this.ticker, + super.message, + super.path, + super.trace, + }) : super(errorType: 'CoinCreationError'); + final String ticker; +} + +// Auto-generated from UtxoCoinBuildError unit variant CoinDoesntSupportTrezor mapped through stringified error field +final class InitUtxoStandardErrorCoinDoesntSupportTrezorException + extends InitUtxoStandardErrorException { + const InitUtxoStandardErrorCoinDoesntSupportTrezorException({ + required this.ticker, + super.message, + super.path, + super.trace, + }) : super(errorType: 'CoinCreationError'); + final String ticker; +} + +// Auto-generated from ZCoinBuildError unit variant GetAddressError mapped through stringified error field +final class ZcoinInitErrorGetAddressErrorException + extends ZcoinInitErrorException { + const ZcoinInitErrorGetAddressErrorException({ + required this.ticker, + super.message, + super.path, + super.trace, + }) : super(errorType: 'CoinCreationError'); + final String ticker; +} + +// Auto-generated from ZCoinBuildError unit variant ZCashParamsNotFound mapped through stringified error field +final class ZcoinInitErrorZCashParamsNotFoundException + extends ZcoinInitErrorException { + const ZcoinInitErrorZCashParamsNotFoundException({ + required this.ticker, + super.message, + super.path, + super.trace, + }) : super(errorType: 'CoinCreationError'); + final String ticker; +} + +// Auto-generated from ZCoinBuildError unit variant ZDerivationPathNotSet mapped through stringified error field +final class ZcoinInitErrorZDerivationPathNotSetException + extends ZcoinInitErrorException { + const ZcoinInitErrorZDerivationPathNotSetException({ + required this.ticker, + super.message, + super.path, + super.trace, + }) : super(errorType: 'CoinCreationError'); + final String ticker; +} + +// Auto-generated from ZCoinBuildError unit variant SaplingParamsInvalidChecksum mapped through stringified error field +final class ZcoinInitErrorSaplingParamsInvalidChecksumException + extends ZcoinInitErrorException { + const ZcoinInitErrorSaplingParamsInvalidChecksumException({ + required this.ticker, + super.message, + super.path, + super.trace, + }) : super(errorType: 'CoinCreationError'); + final String ticker; +} + +/// Registry for parsing KDF RPC error responses into typed exceptions. +/// +/// This class provides automatic conversion of error responses to typed +/// [MmRpcException] subclasses based on the `error_type` field. +abstract final class KdfErrorRegistry { + KdfErrorRegistry._(); + + /// Attempts to parse a JSON error response into a typed [MmRpcException]. + /// + /// Returns `null` if the error type is not recognized or if the JSON + /// does not contain an `error_type` field. + static MmRpcException? tryParse(JsonMap json) { + final errorType = json['error_type'] as String?; + if (errorType == null) return null; + if (_ambiguousErrorTypes.contains(errorType)) return null; + + final errorData = json['error_data']; + final message = json['error'] as String? ?? json['message'] as String?; + final path = json['error_path'] as String?; + final trace = json['error_trace'] as String?; + + final parser = _errorParsers[errorType]; + if (parser == null) return null; + + try { + return parser(errorData, message, path, trace); + } catch (_) { + // Malformed or unexpected error_data shape — fall back to null so + // callers can degrade gracefully to GeneralErrorResponse. + return null; + } + } + + /// Checks if the given error type string is a known KDF error type. + static bool isKnownErrorType(String errorType) { + return _errorParsers.containsKey(errorType); + } + + /// Returns all known error type strings. + static Iterable get knownErrorTypes => _errorParsers.keys; + + /// Error types emitted by multiple RPC namespaces. + /// + /// Excluded from automatic typed parsing because `error_type` alone + /// is not enough to disambiguate them. + static const Set _ambiguousErrorTypes = { + 'AmountTooLow', + 'AtLeastOneNodeRequired', + 'CoinConfigNotFound', + 'CoinCreationError', + 'CoinIsActivatedNotWithHDWallet', + 'CoinIsAlreadyActivated', + 'CoinIsNotFound', + 'CoinIsWalletOnly', + 'CoinNotFound', + 'CoinNotSupported', + 'CoinProtocolParseError', + 'ConnectToNodeError', + 'CouldNotFetchBalance', + 'CouldNotGetBalance', + 'CouldNotGetBlockCount', + 'CustomTokenError', + 'DbError', + 'EnableError', + 'ErrorDerivingAddress', + 'FailedScripthashSubscription', + 'FailedSpawningBalanceEvents', + 'GetEthAddressError', + 'GetNftInfoError', + 'HardwareWalletsAreNotSupportedYet', + 'HwContextNotInitialized', + 'HwError', + 'IOError', + 'Internal', + 'InternalError', + 'InvalidAddress', + 'InvalidBip44Chain', + 'InvalidConfiguration', + 'InvalidParam', + 'InvalidPath', + 'InvalidPayload', + 'InvalidPlatformConfiguration', + 'InvalidRequest', + 'InvalidResponse', + 'LegacyError', + 'MetamaskError', + 'MyAddressError', + 'MyRecentSwapsError', + 'NftProtocolNotSupported', + 'NoSuchChannel', + 'NoSuchCoin', + 'NoSuchTask', + 'NotFound', + 'NotSufficientBalance', + 'NotSufficientBaseCoinBalance', + 'NumConversError', + 'P2PError', + 'PlatformCoinIsNotActivated', + 'PrefixNotFound', + 'PrivKeyPolicyNotAllowed', + 'ProtectFromSpamError', + 'RpcError', + 'RpcInvalidResponse', + 'SigningError', + 'TaskTimedOut', + 'Timeout', + 'TokenConfigIsNotFound', + 'TokenCreationError', + 'TokenIsAlreadyActivated', + 'TokenNotFoundInWallet', + 'TokenProtocolParseError', + 'TransactionError', + 'Transport', + 'UnexpectedDerivationMethod', + 'UnexpectedTokenProtocol', + 'UnexpectedUserAction', + 'UnknownAccount', + 'UnsupportedCoin', + 'UnsupportedMode', + 'UnsupportedPlatformCoin', + 'VolumeTooLow', + 'WalletConnectError', + 'WalletStorageError', + }; + + static final _errorParsers = + < + String, + MmRpcException Function( + dynamic errorData, + String? message, + String? path, + String? trace, + ) + >{ + 'AccountExistsAlready': (errorData, message, path, trace) => + AccountRpcErrorAccountExistsAlreadyException( + AccountId.fromJson(errorData), + message: message, + path: path, + trace: trace, + ), + 'AccountLimitReached': (errorData, message, path, trace) { + final map = _asJsonMap(errorData); + return CreateAccountRpcErrorAccountLimitReachedException( + maxAccountsNumber: _intFromJson( + map.value('max_accounts_number'), + ), + message: message, + path: path, + trace: trace, + ); + }, + 'ActionNotAllowed': (errorData, message, path, trace) => + WithdrawErrorActionNotAllowedException( + _stringFromJson(errorData), + message: message, + path: path, + trace: trace, + ), + 'ActivationFailed': (errorData, message, path, trace) { + final map = _asJsonMap(errorData); + return EthActivationV2ErrorActivationFailedException( + ticker: _stringFromJson(map.value('ticker')), + error: _stringFromJson(map.value('error')), + message: message, + path: path, + trace: trace, + ); + }, + 'AddressDecodingError': (errorData, message, path, trace) => + VerificationErrorAddressDecodingErrorException( + _stringFromJson(errorData), + message: message, + path: path, + trace: trace, + ), + 'AddressError': (errorData, message, path, trace) => + DelegationErrorAddressErrorException( + _stringFromJson(errorData), + message: message, + path: path, + trace: trace, + ), + 'AddressLimitReached': (errorData, message, path, trace) { + final map = _asJsonMap(errorData); + return GetNewAddressRpcErrorAddressLimitReachedException( + maxAddressesNumber: _intFromJson( + map.value('max_addresses_number'), + ), + message: message, + path: path, + trace: trace, + ); + }, + 'AlreadyDelegating': (errorData, message, path, trace) => + DelegationErrorAlreadyDelegatingException( + _stringFromJson(errorData), + message: message, + path: path, + trace: trace, + ), + 'AlreadyRunning': (errorData, message, path, trace) => + NodeVersionErrorAlreadyRunningException( + message: message, + path: path, + trace: trace, + ), + 'AlreadyStarted': (errorData, message, path, trace) => + StartSimpleMakerBotErrorAlreadyStartedException( + message: message, + path: path, + trace: trace, + ), + 'AlreadyStopped': (errorData, message, path, trace) => + StopSimpleMakerBotErrorAlreadyStoppedException( + message: message, + path: path, + trace: trace, + ), + 'AlreadyStopping': (errorData, message, path, trace) => + StopSimpleMakerBotErrorAlreadyStoppingException( + message: message, + path: path, + trace: trace, + ), + 'ApiDataError': (errorData, message, path, trace) => + ApiIntegrationRpcErrorApiDataErrorException( + _stringFromJson(errorData), + message: message, + path: path, + trace: trace, + ), + 'AssetNotEnabled': (errorData, message, path, trace) => + OrderProcessingErrorAssetNotEnabledException( + message: message, + path: path, + trace: trace, + ), + 'AttemptToReceiveAlreadyOwnedErc721': + (errorData, message, path, trace) { + final map = _asJsonMap(errorData); + return UpdateNftErrorAttemptToReceiveAlreadyOwnedErc721Exception( + txHash: _stringFromJson(map.value('tx_hash')), + message: message, + path: path, + trace: trace, + ); + }, + 'BalanceError': (errorData, message, path, trace) => + OpenChannelErrorBalanceErrorException( + _stringFromJson(errorData), + message: message, + path: path, + trace: trace, + ), + 'BalanceInternalError': (errorData, message, path, trace) => + OrderProcessingErrorBalanceInternalErrorException( + message: message, + path: path, + trace: trace, + ), + 'BalanceIsZero': (errorData, message, path, trace) => + OrderProcessingErrorBalanceIsZeroException( + message: message, + path: path, + trace: trace, + ), + 'Banned': (errorData, message, path, trace) => + DispatcherErrorBannedException( + message: message, + path: path, + trace: trace, + ), + 'BaseEqualRel': (errorData, message, path, trace) => + TradePreimageRpcErrorBaseEqualRelException( + message: message, + path: path, + trace: trace, + ), + 'BaseRelSame': (errorData, message, path, trace) => + OrderbookRpcErrorBaseRelSameException( + message: message, + path: path, + trace: trace, + ), + 'BaseRelSameOrderbookTickersAndProtocols': + (errorData, message, path, trace) => + OrderbookRpcErrorBaseRelSameOrderbookTickersAndProtocolsException( + message: message, + path: path, + trace: trace, + ), + 'BestLrSwapNotFound': (errorData, message, path, trace) => + ApiIntegrationRpcErrorBestLrSwapNotFoundException( + message: message, + path: path, + trace: trace, + ), + 'Bip32Error': (errorData, message, path, trace) => + AddressDerivingErrorBip32ErrorException( + _stringFromJson(errorData), + message: message, + path: path, + trace: trace, + ), + 'BroadcastExpected': (errorData, message, path, trace) => + WithdrawErrorBroadcastExpectedException( + _stringFromJson(errorData), + message: message, + path: path, + trace: trace, + ), + 'CanNotUndelegate': (errorData, message, path, trace) { + final map = _asJsonMap(errorData); + return DelegationErrorCanNotUndelegateException( + delegatorAddr: _stringFromJson( + map.value('delegator_addr'), + ), + validatorAddr: _stringFromJson( + map.value('validator_addr'), + ), + message: message, + path: path, + trace: trace, + ); + }, + 'Cancelled': (errorData, message, path, trace) => + MmInitErrorCancelledException( + message: message, + path: path, + trace: trace, + ), + 'CannotInteractWithSmartContract': (errorData, message, path, trace) => + DelegationErrorCannotInteractWithSmartContractException( + _stringFromJson(errorData), + message: message, + path: path, + trace: trace, + ), + 'CannotRetrieveOrderMatchContext': (errorData, message, path, trace) => + CancelOrderErrorCannotRetrieveOrderMatchContextException( + message: message, + path: path, + trace: trace, + ), + 'CannotStartFromStopping': (errorData, message, path, trace) => + StartSimpleMakerBotErrorCannotStartFromStoppingException( + message: message, + path: path, + trace: trace, + ), + 'ChainIdNotSet': (errorData, message, path, trace) => + EthActivationV2ErrorChainIdNotSetException( + message: message, + path: path, + trace: trace, + ), + 'ChainNotSupported': (errorData, message, path, trace) => + ApiIntegrationRpcErrorChainNotSupportedException( + message: message, + path: path, + trace: trace, + ), + 'ClientConnectionFailed': (errorData, message, path, trace) => + EthTokenActivationErrorClientConnectionFailedException( + _stringFromJson(errorData), + message: message, + path: path, + trace: trace, + ), + 'CloseChannelError': (errorData, message, path, trace) => + CloseChannelErrorCloseChannelErrorException( + _stringFromJson(errorData), + message: message, + path: path, + trace: trace, + ), + 'CoinConfigIsNotFound': (errorData, message, path, trace) => + InitStandaloneCoinErrorCoinConfigIsNotFoundException( + _stringFromJson(errorData), + message: message, + path: path, + trace: trace, + ), + 'CoinDoesntSupportDelegation': (errorData, message, path, trace) { + final map = _asJsonMap(errorData); + return DelegationErrorCoinDoesntSupportDelegationException( + coin: _stringFromJson(map.value('coin')), + message: message, + path: path, + trace: trace, + ); + }, + 'CoinDoesntSupportInitWithdraw': (errorData, message, path, trace) { + final map = _asJsonMap(errorData); + return WithdrawErrorCoinDoesntSupportInitWithdrawException( + coin: _stringFromJson(map.value('coin')), + message: message, + path: path, + trace: trace, + ); + }, + 'CoinDoesntSupportNft': (errorData, message, path, trace) { + final map = _asJsonMap(errorData); + return UpdateNftErrorCoinDoesntSupportNftException( + coin: _stringFromJson(map.value('coin')), + message: message, + path: path, + trace: trace, + ); + }, + 'CoinDoesntSupportNftWithdraw': (errorData, message, path, trace) { + final map = _asJsonMap(errorData); + return WithdrawErrorCoinDoesntSupportNftWithdrawException( + coin: _stringFromJson(map.value('coin')), + message: message, + path: path, + trace: trace, + ); + }, + 'CoinDoesntSupportTrezor': (errorData, message, path, trace) => + EthActivationV2ErrorCoinDoesntSupportTrezorException( + message: message, + path: path, + trace: trace, + ), + 'CoinIsNotActive': (errorData, message, path, trace) => + MyTxHistoryErrorV2CoinIsNotActiveException( + _stringFromJson(errorData), + message: message, + path: path, + trace: trace, + ), + 'CoinIsNotSupported': (errorData, message, path, trace) => + GetMyAddressErrorCoinIsNotSupportedException( + _stringFromJson(errorData), + message: message, + path: path, + trace: trace, + ), + 'CoinTypeError': (errorData, message, path, trace) => + ApiIntegrationRpcErrorCoinTypeErrorException( + message: message, + path: path, + trace: trace, + ), + 'CoinsConfCheckError': (errorData, message, path, trace) => + GetMyAddressErrorCoinsConfCheckErrorException( + _stringFromJson(errorData), + message: message, + path: path, + trace: trace, + ), + 'ConnectionError': (errorData, message, path, trace) => + ConnectToNodeErrorConnectionErrorException( + _stringFromJson(errorData), + message: message, + path: path, + trace: trace, + ), + 'ContractTypeDoesntSupportNftWithdrawing': + (errorData, message, path, trace) => + WithdrawErrorContractTypeDoesntSupportNftWithdrawingException( + _stringFromJson(errorData), + message: message, + path: path, + trace: trace, + ), + 'ContractTypeIsNull': (errorData, message, path, trace) => + GetNftInfoErrorContractTypeIsNullException( + message: message, + path: path, + trace: trace, + ), + 'CtxError': (errorData, message, path, trace) => + BestOrdersRpcErrorCtxErrorException( + _stringFromJson(errorData), + message: message, + path: path, + trace: trace, + ), + 'CurrentlyStopping': (errorData, message, path, trace) => + NodeVersionErrorCurrentlyStoppingException( + message: message, + path: path, + trace: trace, + ), + 'DatabaseError': (errorData, message, path, trace) => + NodeVersionErrorDatabaseErrorException( + _stringFromJson(errorData), + message: message, + path: path, + trace: trace, + ), + 'DbDirectoryIsNotWritable': (errorData, message, path, trace) { + final map = _asJsonMap(errorData); + return MmInitErrorDbDirectoryIsNotWritableException( + pathData: _stringFromJson(map.value('path')), + message: message, + path: path, + trace: trace, + ); + }, + 'DbFileIsNotWritable': (errorData, message, path, trace) { + final map = _asJsonMap(errorData); + return MmInitErrorDbFileIsNotWritableException( + pathData: _stringFromJson(map.value('path')), + message: message, + path: path, + trace: trace, + ); + }, + 'DecodeError': (errorData, message, path, trace) => + RawTransactionErrorDecodeErrorException( + _stringFromJson(errorData), + message: message, + path: path, + trace: trace, + ), + 'DelegationOpsNotSupported': (errorData, message, path, trace) { + final map = _asJsonMap(errorData); + return DelegationErrorDelegationOpsNotSupportedException( + reason: _stringFromJson(map.value('reason')), + message: message, + path: path, + trace: trace, + ); + }, + 'DescriptionTooLong': (errorData, message, path, trace) { + final map = _asJsonMap(errorData); + return AccountRpcErrorDescriptionTooLongException( + maxLen: _intFromJson(map.value('max_len')), + message: message, + path: path, + trace: trace, + ); + }, + 'DifferentChains': (errorData, message, path, trace) => + ApiIntegrationRpcErrorDifferentChainsException( + message: message, + path: path, + trace: trace, + ), + 'DisableError': (errorData, message, path, trace) => + DisableStreamingRequestErrorDisableErrorException( + _stringFromJson(errorData), + message: message, + path: path, + trace: trace, + ), + 'EmptyAddressesLimitReached': (errorData, message, path, trace) { + final map = _asJsonMap(errorData); + return GetNewAddressRpcErrorEmptyAddressesLimitReachedException( + gapLimit: _intFromJson(map.value('gap_limit')), + message: message, + path: path, + trace: trace, + ); + }, + 'EnableLightningError': (errorData, message, path, trace) => + LightningInitErrorEnableLightningErrorException( + EnableLightningError.fromJson(errorData), + message: message, + path: path, + trace: trace, + ), + 'Erc20InitError': (errorData, message, path, trace) => + EnableCoinUnifiedErrorErc20InitErrorException( + _stringFromJson(errorData), + message: message, + path: path, + trace: trace, + ), + 'ErrorCreatingDbDir': (errorData, message, path, trace) { + final map = _asJsonMap(errorData); + return MmInitErrorErrorCreatingDbDirException( + pathData: PathBuf.fromJson(map.value('path')), + error: _stringFromJson(map.value('error')), + message: message, + path: path, + trace: trace, + ); + }, + 'ErrorDbMigrating': (errorData, message, path, trace) => + MmInitErrorErrorDbMigratingException( + _stringFromJson(errorData), + message: message, + path: path, + trace: trace, + ), + 'ErrorDeserializingConfig': (errorData, message, path, trace) { + final map = _asJsonMap(errorData); + return MmInitErrorErrorDeserializingConfigException( + field: _stringFromJson(map.value('field')), + error: _stringFromJson(map.value('error')), + message: message, + path: path, + trace: trace, + ); + }, + 'ErrorDeserializingDerivationPath': (errorData, message, path, trace) => + EthActivationV2ErrorErrorDeserializingDerivationPathException( + _stringFromJson(errorData), + message: message, + path: path, + trace: trace, + ), + 'ErrorLoadingAccount': (errorData, message, path, trace) => + AccountRpcErrorErrorLoadingAccountException( + _stringFromJson(errorData), + message: message, + path: path, + trace: trace, + ), + 'ErrorSavingAccount': (errorData, message, path, trace) => + AccountRpcErrorErrorSavingAccountException( + _stringFromJson(errorData), + message: message, + path: path, + trace: trace, + ), + 'ErrorSqliteInitializing': (errorData, message, path, trace) => + MmInitErrorErrorSqliteInitializingException( + _stringFromJson(errorData), + message: message, + path: path, + trace: trace, + ), + 'EthInitError': (errorData, message, path, trace) => + EnableCoinUnifiedErrorEthInitErrorException( + _stringFromJson(errorData), + message: message, + path: path, + trace: trace, + ), + 'EventStreamerInitFailed': (errorData, message, path, trace) => + MmInitErrorEventStreamerInitFailedException( + _stringFromJson(errorData), + message: message, + path: path, + trace: trace, + ), + 'FailureToOpenChannel': (errorData, message, path, trace) { + final list = _asJsonList(errorData); + return OpenChannelErrorFailureToOpenChannelException( + _stringFromJson(list[0]), + _stringFromJson(list[1]), + message: message, + path: path, + trace: trace, + ); + }, + 'FailureToUpdateChannel': (errorData, message, path, trace) { + final list = _asJsonList(errorData); + return UpdateChannelErrorFailureToUpdateChannelException( + _stringFromJson(list[0]), + _stringFromJson(list[1]), + message: message, + path: path, + trace: trace, + ); + }, + 'FieldNotFoundInConfig': (errorData, message, path, trace) { + final map = _asJsonMap(errorData); + return MmInitErrorFieldNotFoundInConfigException( + field: _stringFromJson(map.value('field')), + message: message, + path: path, + trace: trace, + ); + }, + 'FieldWrongValueInConfig': (errorData, message, path, trace) { + final map = _asJsonMap(errorData); + return MmInitErrorFieldWrongValueInConfigException( + field: _stringFromJson(map.value('field')), + error: _stringFromJson(map.value('error')), + message: message, + path: path, + trace: trace, + ); + }, + 'FoundUnexpectedDevice': (errorData, message, path, trace) => + TrezorConnectionErrorFoundUnexpectedDeviceException( + message: message, + path: path, + trace: trace, + ), + 'FromAddressNotFound': (errorData, message, path, trace) => + WithdrawErrorFromAddressNotFoundException( + message: message, + path: path, + trace: trace, + ), + 'FromUuidSwapNotFound': (errorData, message, path, trace) => + MyRecentSwapsErrFromUuidSwapNotFoundException( + _stringFromJson(errorData), + message: message, + path: path, + trace: trace, + ), + 'GenerateTxErr': (errorData, message, path, trace) => + OpenChannelErrorGenerateTxErrException( + _stringFromJson(errorData), + message: message, + path: path, + trace: trace, + ), + 'GetInfoFromUriError': (errorData, message, path, trace) => + UpdateNftErrorGetInfoFromUriErrorException( + GetInfoFromUriError.fromJson(errorData), + message: message, + path: path, + trace: trace, + ), + 'GetMyAddressError': (errorData, message, path, trace) => + UpdateNftErrorGetMyAddressErrorException( + GetMyAddressError.fromJson(errorData), + message: message, + path: path, + trace: trace, + ), + 'HDWalletStorageError': (errorData, message, path, trace) => + EthActivationV2ErrorHDWalletStorageErrorException( + _stringFromJson(errorData), + message: message, + path: path, + trace: trace, + ), + 'HashNotExist': (errorData, message, path, trace) => + RawTransactionErrorHashNotExistException( + _stringFromJson(errorData), + message: message, + path: path, + trace: trace, + ), + 'HdRangeTooLarge': (errorData, message, path, trace) => + OfflineKeysErrorHdRangeTooLargeException( + message: message, + path: path, + trace: trace, + ), + 'HwContextInitializingAlready': (errorData, message, path, trace) => + InitHwErrorHwContextInitializingAlreadyException( + message: message, + path: path, + trace: trace, + ), + 'IBCError': (errorData, message, path, trace) => + WithdrawErrorIBCErrorException( + TendermintIBCError.fromJson(errorData), + message: message, + path: path, + trace: trace, + ), + 'InitializationError': (errorData, message, path, trace) => + WalletConnectRpcErrorInitializationErrorException( + _stringFromJson(errorData), + message: message, + path: path, + trace: trace, + ), + 'InsufficientAmountInCache': (errorData, message, path, trace) { + final map = _asJsonMap(errorData); + return UpdateNftErrorInsufficientAmountInCacheException( + amountList: _stringFromJson(map.value('amount_list')), + amountHistory: _stringFromJson( + map.value('amount_history'), + ), + message: message, + path: path, + trace: trace, + ); + }, + 'InternalCoinFindError': (errorData, message, path, trace) => + OrderProcessingErrorInternalCoinFindErrorException( + message: message, + path: path, + trace: trace, + ), + 'InvalidActivationRequest': (errorData, message, path, trace) => + EnableCoinUnifiedErrorInvalidActivationRequestException( + _stringFromJson(errorData), + message: message, + path: path, + trace: trace, + ), + 'InvalidBlockOrder': (errorData, message, path, trace) { + final map = _asJsonMap(errorData); + return UpdateNftErrorInvalidBlockOrderException( + lastScannedBlock: _stringFromJson( + map.value('last_scanned_block'), + ), + lastNftBlock: _stringFromJson(map.value('last_nft_block')), + message: message, + path: path, + trace: trace, + ); + }, + 'InvalidBotConfiguration': (errorData, message, path, trace) => + StartSimpleMakerBotErrorInvalidBotConfigurationException( + message: message, + path: path, + trace: trace, + ), + 'InvalidConfig': (errorData, message, path, trace) => + EnableTokenErrorInvalidConfigException( + _stringFromJson(errorData), + message: message, + path: path, + trace: trace, + ), + 'InvalidFallbackSwapContract': (errorData, message, path, trace) => + EthActivationV2ErrorInvalidFallbackSwapContractException( + _stringFromJson(errorData), + message: message, + path: path, + trace: trace, + ), + 'InvalidFee': (errorData, message, path, trace) { + final map = _asJsonMap(errorData); + return WithdrawErrorInvalidFeeException( + reason: _stringFromJson(map.value('reason')), + details: map.valueOrNull('details') == null + ? null + : JsonValue.fromJson(map.valueOrNull('details')), + message: message, + path: path, + trace: trace, + ); + }, + 'InvalidFeePolicy': (errorData, message, path, trace) => + WithdrawErrorInvalidFeePolicyException( + _stringFromJson(errorData), + message: message, + path: path, + trace: trace, + ), + 'InvalidGasApiConfig': (errorData, message, path, trace) => + Web3RpcErrorInvalidGasApiConfigException( + _stringFromJson(errorData), + message: message, + path: path, + trace: trace, + ), + 'InvalidHardwareWalletCall': (errorData, message, path, trace) => + EthActivationV2ErrorInvalidHardwareWalletCallException( + message: message, + path: path, + trace: trace, + ), + 'InvalidHashError': (errorData, message, path, trace) => + RawTransactionErrorInvalidHashErrorException( + _stringFromJson(errorData), + message: message, + path: path, + trace: trace, + ), + 'InvalidHdRange': (errorData, message, path, trace) { + final map = _asJsonMap(errorData); + return OfflineKeysErrorInvalidHdRangeException( + startIndex: _intFromJson(map.value('start_index')), + endIndex: _intFromJson(map.value('end_index')), + message: message, + path: path, + trace: trace, + ); + }, + 'InvalidHexString': (errorData, message, path, trace) => + UpdateNftErrorInvalidHexStringException( + _stringFromJson(errorData), + message: message, + path: path, + trace: trace, + ), + 'InvalidMemo': (errorData, message, path, trace) => + WithdrawErrorInvalidMemoException( + _stringFromJson(errorData), + message: message, + path: path, + trace: trace, + ), + 'InvalidMmRpcVersion': (errorData, message, path, trace) => + DispatcherErrorInvalidMmRpcVersionException( + _stringFromJson(errorData), + message: message, + path: path, + trace: trace, + ), + 'InvalidParametersForMode': (errorData, message, path, trace) => + OfflineKeysErrorInvalidParametersForModeException( + message: message, + path: path, + trace: trace, + ), + 'InvalidPassword': (errorData, message, path, trace) => + MnemonicRpcErrorInvalidPasswordException( + _stringFromJson(errorData), + message: message, + path: path, + trace: trace, + ), + 'InvalidPathToAddress': (errorData, message, path, trace) => + EthActivationV2ErrorInvalidPathToAddressException( + _stringFromJson(errorData), + message: message, + path: path, + trace: trace, + ), + 'InvalidSwapContractAddr': (errorData, message, path, trace) => + EthActivationV2ErrorInvalidSwapContractAddrException( + _stringFromJson(errorData), + message: message, + path: path, + trace: trace, + ), + 'InvalidTarget': (errorData, message, path, trace) => + MyTxHistoryErrorV2InvalidTargetException( + _stringFromJson(errorData), + message: message, + path: path, + trace: trace, + ), + 'InvalidTimeStampRange': (errorData, message, path, trace) => + MyRecentSwapsErrInvalidTimeStampRangeException( + message: message, + path: path, + trace: trace, + ), + 'KeyDerivationFailed': (errorData, message, path, trace) { + final map = _asJsonMap(errorData); + return OfflineKeysErrorKeyDerivationFailedException( + ticker: _stringFromJson(map.value('ticker')), + error: _stringFromJson(map.value('error')), + message: message, + path: path, + trace: trace, + ); + }, + 'L2ConfigIsNotFound': (errorData, message, path, trace) => + InitL2ErrorL2ConfigIsNotFoundException( + _stringFromJson(errorData), + message: message, + path: path, + trace: trace, + ), + 'L2ConfigParseError': (errorData, message, path, trace) => + InitL2ErrorL2ConfigParseErrorException( + _stringFromJson(errorData), + message: message, + path: path, + trace: trace, + ), + 'L2IsAlreadyActivated': (errorData, message, path, trace) => + InitL2ErrorL2IsAlreadyActivatedException( + _stringFromJson(errorData), + message: message, + path: path, + trace: trace, + ), + 'L2ProtocolParseError': (errorData, message, path, trace) { + final map = _asJsonMap(errorData); + return InitL2ErrorL2ProtocolParseErrorException( + ticker: _stringFromJson(map.value('ticker')), + error: _stringFromJson(map.value('error')), + message: message, + path: path, + trace: trace, + ); + }, + 'LastScannedBlockNotFound': (errorData, message, path, trace) { + final map = _asJsonMap(errorData); + return UpdateNftErrorLastScannedBlockNotFoundException( + lastNftBlock: _stringFromJson(map.value('last_nft_block')), + message: message, + path: path, + trace: trace, + ); + }, + 'LastUpdatedTimestampInvalid': (errorData, message, path, trace) { + final map = _asJsonMap(errorData); + return OrderProcessingErrorLastUpdatedTimestampInvalidException( + keyTradePair: _stringFromJson(map.value('key_trade_pair')), + message: message, + path: path, + trace: trace, + ); + }, + 'LightningValidationErr': (errorData, message, path, trace) => + LightningInitErrorLightningValidationErrException( + LightningValidationErr.fromJson(errorData), + message: message, + path: path, + trace: trace, + ), + 'LocalHostOnly': (errorData, message, path, trace) => + DispatcherErrorLocalHostOnlyException( + message: message, + path: path, + trace: trace, + ), + 'MessageEncodingFailed': (errorData, message, path, trace) { + final map = _asJsonMap(errorData); + return HealthcheckRpcErrorMessageEncodingFailedException( + reason: _stringFromJson(map.value('reason')), + message: message, + path: path, + trace: trace, + ); + }, + 'MessageGenerationFailed': (errorData, message, path, trace) { + final map = _asJsonMap(errorData); + return HealthcheckRpcErrorMessageGenerationFailedException( + reason: _stringFromJson(map.value('reason')), + message: message, + path: path, + trace: trace, + ); + }, + 'MetamaskInitializingAlready': (errorData, message, path, trace) => + InitMetamaskErrorMetamaskInitializingAlreadyException( + message: message, + path: path, + trace: trace, + ), + 'MinVolUsdAboveBalanceUsd': (errorData, message, path, trace) => + OrderProcessingErrorMinVolUsdAboveBalanceUsdException( + message: message, + path: path, + trace: trace, + ), + 'MissingActivationParams': (errorData, message, path, trace) => + EnableCoinUnifiedErrorMissingActivationParamsException( + _stringFromJson(errorData), + message: message, + path: path, + trace: trace, + ), + 'MissingPrefixValue': (errorData, message, path, trace) { + final map = _asJsonMap(errorData); + return OfflineKeysErrorMissingPrefixValueException( + ticker: _stringFromJson(map.value('ticker')), + prefixType: _stringFromJson(map.value('prefix_type')), + message: message, + path: path, + trace: trace, + ); + }, + 'MyAddressNotNftOwner': (errorData, message, path, trace) { + final map = _asJsonMap(errorData); + return WithdrawErrorMyAddressNotNftOwnerException( + myAddress: _stringFromJson(map.value('my_address')), + tokenOwner: _stringFromJson(map.value('token_owner')), + message: message, + path: path, + trace: trace, + ); + }, + 'MyBalanceError': (errorData, message, path, trace) => + LightningInitErrorMyBalanceErrorException( + BalanceError.fromJson(errorData), + message: message, + path: path, + trace: trace, + ), + 'NameTooLong': (errorData, message, path, trace) { + final map = _asJsonMap(errorData); + return AccountRpcErrorNameTooLongException( + maxLen: _intFromJson(map.value('max_len')), + message: message, + path: path, + trace: trace, + ); + }, + 'NbAttemptsLeft': (errorData, message, path, trace) => + RateLimitErrorNbAttemptsLeftException( + _intFromJson(errorData), + message: message, + path: path, + trace: trace, + ), + 'NoChainIdSet': (errorData, message, path, trace) { + final map = _asJsonMap(errorData); + return WithdrawErrorNoChainIdSetException( + coin: _stringFromJson(map.value('coin')), + message: message, + path: path, + trace: trace, + ); + }, + 'NoEnabledAccount': (errorData, message, path, trace) => + AccountRpcErrorNoEnabledAccountException( + message: message, + path: path, + trace: trace, + ), + 'NoRouteFound': (errorData, message, path, trace) => + SendPaymentErrorNoRouteFoundException( + _stringFromJson(errorData), + message: message, + path: path, + trace: trace, + ), + 'NoSecretHash': (errorData, message, path, trace) => + RecreateSwapErrorNoSecretHashException( + message: message, + path: path, + trace: trace, + ), + 'NoSuchAccount': (errorData, message, path, trace) => + AccountRpcErrorNoSuchAccountException( + AccountId.fromJson(errorData), + message: message, + path: path, + trace: trace, + ), + 'NoSuchMethod': (errorData, message, path, trace) => + DispatcherErrorNoSuchMethodException( + message: message, + path: path, + trace: trace, + ), + 'NoSuchPayment': (errorData, message, path, trace) => + GetPaymentDetailsErrorNoSuchPaymentException( + H256Json.fromJson(errorData), + message: message, + path: path, + trace: trace, + ), + 'NoSwapWithUuid': (errorData, message, path, trace) => + MySwapStatusErrorNoSwapWithUuidException( + _stringFromJson(errorData), + message: message, + path: path, + trace: trace, + ), + 'NonExistentPrevOutputError': (errorData, message, path, trace) => + RawTransactionErrorNonExistentPrevOutputErrorException( + _stringFromJson(errorData), + message: message, + path: path, + trace: trace, + ), + 'NotEnoughNftsAmount': (errorData, message, path, trace) { + final map = _asJsonMap(errorData); + return WithdrawErrorNotEnoughNftsAmountException( + tokenAddress: _stringFromJson(map.value('token_address')), + tokenId: _stringFromJson(map.value('token_id')), + available: BigUint.fromJson(map.value('available')), + required: BigUint.fromJson(map.value('required')), + message: message, + path: path, + trace: trace, + ); + }, + 'NotImplemented': (errorData, message, path, trace) { + final map = _asJsonMap(errorData); + return RawTransactionErrorNotImplementedException( + coin: _stringFromJson(map.value('coin')), + message: message, + path: path, + trace: trace, + ); + }, + 'NotRunning': (errorData, message, path, trace) => + NodeVersionErrorNotRunningException( + message: message, + path: path, + trace: trace, + ), + 'NotSufficientPlatformBalanceForFee': + (errorData, message, path, trace) { + final map = _asJsonMap(errorData); + return WithdrawErrorNotSufficientPlatformBalanceForFeeException( + coin: _stringFromJson(map.value('coin')), + available: BigDecimal.fromJson(map.value('available')), + required: BigDecimal.fromJson(map.value('required')), + message: message, + path: path, + trace: trace, + ); + }, + 'NotSupported': (errorData, message, path, trace) => + SwapTxFeePolicyErrorNotSupportedException( + _stringFromJson(errorData), + message: message, + path: path, + trace: trace, + ), + 'NotSupportedCoin': (errorData, message, path, trace) => + GetCurrentMtpErrorNotSupportedCoinException( + _stringFromJson(errorData), + message: message, + path: path, + trace: trace, + ), + 'NotSupportedFor': (errorData, message, path, trace) => + MyTxHistoryErrorV2NotSupportedForException( + _stringFromJson(errorData), + message: message, + path: path, + trace: trace, + ), + 'NothingToClaim': (errorData, message, path, trace) { + final map = _asJsonMap(errorData); + return DelegationErrorNothingToClaimException( + coin: _stringFromJson(map.value('coin')), + message: message, + path: path, + trace: trace, + ); + }, + 'NumberError': (errorData, message, path, trace) => + ApiIntegrationRpcErrorNumberErrorException( + _stringFromJson(errorData), + message: message, + path: path, + trace: trace, + ), + 'OneInchAllowanceNotEnough': (errorData, message, path, trace) { + final map = _asJsonMap(errorData); + return ApiIntegrationRpcErrorOneInchAllowanceNotEnoughException( + allowance: U256.fromJson(map.value('allowance')), + amount: U256.fromJson(map.value('amount')), + message: message, + path: path, + trace: trace, + ); + }, + 'OneInchError': (errorData, message, path, trace) => + ApiIntegrationRpcErrorOneInchErrorException( + ApiClientError.fromJson(errorData), + message: message, + path: path, + trace: trace, + ), + 'OrderBeingMatched': (errorData, message, path, trace) { + final map = _asJsonMap(errorData); + return CancelOrderErrorOrderBeingMatchedException( + uuid: _stringFromJson(map.value('uuid')), + message: message, + path: path, + trace: trace, + ); + }, + 'OrderCreationError': (errorData, message, path, trace) => + OrderProcessingErrorOrderCreationErrorException( + _stringFromJson(errorData), + message: message, + path: path, + trace: trace, + ), + 'OrderUpdateError': (errorData, message, path, trace) => + OrderProcessingErrorOrderUpdateErrorException( + _stringFromJson(errorData), + message: message, + path: path, + trace: trace, + ), + 'OrdersKickStartError': (errorData, message, path, trace) => + MmInitErrorOrdersKickStartErrorException( + _stringFromJson(errorData), + message: message, + path: path, + trace: trace, + ), + 'OutOfBounds': (errorData, message, path, trace) { + final map = _asJsonMap(errorData); + return ApiIntegrationRpcErrorOutOfBoundsException( + param: _stringFromJson(map.value('param')), + value: _stringFromJson(map.value('value')), + min: _stringFromJson(map.value('min')), + max: _stringFromJson(map.value('max')), + message: message, + path: path, + trace: trace, + ); + }, + 'P2PSubscribeError': (errorData, message, path, trace) => + OrderbookRpcErrorP2PSubscribeErrorException( + _stringFromJson(errorData), + message: message, + path: path, + trace: trace, + ), + 'ParseError': (errorData, message, path, trace) => + ConnectToNodeErrorParseErrorException( + _stringFromJson(errorData), + message: message, + path: path, + trace: trace, + ), + 'ParseRfc3339Err': (errorData, message, path, trace) => + GetNftInfoErrorParseRfc3339ErrException( + ParseRfc3339Err.fromJson(errorData), + message: message, + path: path, + trace: trace, + ), + 'PasswordPolicyViolation': (errorData, message, path, trace) => + MnemonicRpcErrorPasswordPolicyViolationException( + _stringFromJson(errorData), + message: message, + path: path, + trace: trace, + ), + 'PaymentError': (errorData, message, path, trace) => + SendPaymentErrorPaymentErrorException( + _stringFromJson(errorData), + message: message, + path: path, + trace: trace, + ), + 'PeerIdParseError': (errorData, message, path, trace) { + final list = _asJsonList(errorData); + return NodeVersionErrorPeerIdParseErrorException( + _stringFromJson(list[0]), + _stringFromJson(list[1]), + message: message, + path: path, + trace: trace, + ); + }, + 'PerformError': (errorData, message, path, trace) => + TendermintCoinRpcErrorPerformErrorException( + _stringFromJson(errorData), + message: message, + path: path, + trace: trace, + ), + 'PlatformCoinCreationError': (errorData, message, path, trace) { + final map = _asJsonMap(errorData); + return EnablePlatformCoinWithTokensErrorPlatformCoinCreationErrorException( + ticker: _stringFromJson(map.value('ticker')), + error: _stringFromJson(map.value('error')), + message: message, + path: path, + trace: trace, + ); + }, + 'PlatformConfigIsNotFound': (errorData, message, path, trace) => + EnablePlatformCoinWithTokensErrorPlatformConfigIsNotFoundException( + _stringFromJson(errorData), + message: message, + path: path, + trace: trace, + ), + 'PlatformIsAlreadyActivated': (errorData, message, path, trace) => + EnablePlatformCoinWithTokensErrorPlatformIsAlreadyActivatedException( + _stringFromJson(errorData), + message: message, + path: path, + trace: trace, + ), + 'PriceBelowMinBasePrice': (errorData, message, path, trace) { + final map = _asJsonMap(errorData); + return OrderProcessingErrorPriceBelowMinBasePriceException( + basePrice: _stringFromJson(map.value('base_price')), + minBasePrice: _stringFromJson(map.value('min_base_price')), + message: message, + path: path, + trace: trace, + ); + }, + 'PriceBelowMinRelPrice': (errorData, message, path, trace) { + final map = _asJsonMap(errorData); + return OrderProcessingErrorPriceBelowMinRelPriceException( + relPrice: _stringFromJson(map.value('rel_price')), + minRelPrice: _stringFromJson(map.value('min_rel_price')), + message: message, + path: path, + trace: trace, + ); + }, + 'PriceBelowPairPrice': (errorData, message, path, trace) { + final map = _asJsonMap(errorData); + return OrderProcessingErrorPriceBelowPairPriceException( + pair: _stringFromJson(map.value('pair')), + pairPrice: _stringFromJson(map.value('pair_price')), + minPairPrice: _stringFromJson(map.value('min_pair_price')), + message: message, + path: path, + trace: trace, + ); + }, + 'PriceElapsedValidityExpired': (errorData, message, path, trace) { + final map = _asJsonMap(errorData); + return OrderProcessingErrorPriceElapsedValidityExpiredException( + elapsed: _doubleFromJson(map.value('elapsed')), + elapsedValidity: _doubleFromJson( + map.value('elapsed_validity'), + ), + keyTradePair: _stringFromJson(map.value('key_trade_pair')), + message: message, + path: path, + trace: trace, + ); + }, + 'PriceElapsedValidityUntreatable': (errorData, message, path, trace) => + OrderProcessingErrorPriceElapsedValidityUntreatableException( + _stringFromJson(errorData), + message: message, + path: path, + trace: trace, + ), + 'PriceIsZero': (errorData, message, path, trace) { + final map = _asJsonMap(errorData); + return OrderProcessingErrorPriceIsZeroException( + keyTradePair: _stringFromJson(map.value('key_trade_pair')), + message: message, + path: path, + trace: trace, + ); + }, + 'PriceTooLow': (errorData, message, path, trace) { + final map = _asJsonMap(errorData); + return TradePreimageRpcErrorPriceTooLowException( + price: BigDecimal.fromJson(map.value('price')), + threshold: BigDecimal.fromJson(map.value('threshold')), + message: message, + path: path, + trace: trace, + ); + }, + 'Prost': (errorData, message, path, trace) => + TendermintCoinRpcErrorProstException( + _stringFromJson(errorData), + message: message, + path: path, + trace: trace, + ), + 'ProtocolParseError': (errorData, message, path, trace) { + final map = _asJsonMap(errorData); + return OfflineKeysErrorProtocolParseErrorException( + ticker: _stringFromJson(map.value('ticker')), + error: _stringFromJson(map.value('error')), + message: message, + path: path, + trace: trace, + ); + }, + 'ProviderUnknown': (errorData, message, path, trace) { + final map = _asJsonMap(errorData); + return OrderProcessingErrorProviderUnknownException( + keyTradePair: _stringFromJson(map.value('key_trade_pair')), + message: message, + path: path, + trace: trace, + ); + }, + 'QtumInitError': (errorData, message, path, trace) => + EnableCoinUnifiedErrorQtumInitErrorException( + _stringFromJson(errorData), + message: message, + path: path, + trace: trace, + ), + 'RequestError': (errorData, message, path, trace) => + TelegramErrorRequestErrorException( + SlurpError.fromJson(errorData), + message: message, + path: path, + trace: trace, + ), + 'RetrieveInfoError': (errorData, message, path, trace) => + TokenInfoErrorRetrieveInfoErrorException( + _stringFromJson(errorData), + message: message, + path: path, + trace: trace, + ), + 'RpcClientError': (errorData, message, path, trace) => + TendermintCoinRpcErrorRpcClientErrorException( + _stringFromJson(errorData), + message: message, + path: path, + trace: trace, + ), + 'RpcTaskError': (errorData, message, path, trace) => + EnableLightningErrorRpcTaskErrorException( + _stringFromJson(errorData), + message: message, + path: path, + trace: trace, + ), + 'SerdeError': (errorData, message, path, trace) => + UpdateNftErrorSerdeErrorException( + _stringFromJson(errorData), + message: message, + path: path, + trace: trace, + ), + 'SessionRequestError': (errorData, message, path, trace) => + WalletConnectRpcErrorSessionRequestErrorException( + _stringFromJson(errorData), + message: message, + path: path, + trace: trace, + ), + 'SignOrCreationError': (errorData, message, path, trace) => + GenerateInvoiceErrorSignOrCreationErrorException( + _stringFromJson(errorData), + message: message, + path: path, + trace: trace, + ), + 'SignatureDecodingError': (errorData, message, path, trace) => + VerificationErrorSignatureDecodingErrorException( + _stringFromJson(errorData), + message: message, + path: path, + trace: trace, + ), + 'StorageError': (errorData, message, path, trace) => + MyTxHistoryErrorV2StorageErrorException( + _stringFromJson(errorData), + message: message, + path: path, + trace: trace, + ), + 'StorageIsNotInitialized': (errorData, message, path, trace) => + MyTxHistoryErrorV2StorageIsNotInitializedException( + _stringFromJson(errorData), + message: message, + path: path, + trace: trace, + ), + 'SwapInfoNotAvailable': (errorData, message, path, trace) => + SwapUpdateNotificationErrorSwapInfoNotAvailableException( + message: message, + path: path, + trace: trace, + ), + 'SwapIsNotNegotiated': (errorData, message, path, trace) => + RecreateSwapErrorSwapIsNotNegotiatedException( + message: message, + path: path, + trace: trace, + ), + 'SwapIsNotStarted': (errorData, message, path, trace) => + RecreateSwapErrorSwapIsNotStartedException( + message: message, + path: path, + trace: trace, + ), + 'SwapsKickStartError': (errorData, message, path, trace) => + MmInitErrorSwapsKickStartErrorException( + _stringFromJson(errorData), + message: message, + path: path, + trace: trace, + ), + 'SystemTimeError': (errorData, message, path, trace) => + EnableLightningErrorSystemTimeErrorException( + _stringFromJson(errorData), + message: message, + path: path, + trace: trace, + ), + 'TaskFinished': (errorData, message, path, trace) => + CancelRpcTaskErrorTaskFinishedException( + TaskId.fromJson(errorData), + message: message, + path: path, + trace: trace, + ), + 'TelegramError': (errorData, message, path, trace) => + MessageErrorTelegramErrorException( + TelegramError.fromJson(errorData), + message: message, + path: path, + trace: trace, + ), + 'TendermintInitError': (errorData, message, path, trace) => + EnableCoinUnifiedErrorTendermintInitErrorException( + _stringFromJson(errorData), + message: message, + path: path, + trace: trace, + ), + 'TickerTooLong': (errorData, message, path, trace) { + final map = _asJsonMap(errorData); + return AccountRpcErrorTickerTooLongException( + maxLen: _intFromJson(map.value('max_len')), + message: message, + path: path, + trace: trace, + ); + }, + 'TooMuchToUndelegate': (errorData, message, path, trace) { + final map = _asJsonMap(errorData); + return DelegationErrorTooMuchToUndelegateException( + available: BigDecimal.fromJson(map.value('available')), + requested: BigDecimal.fromJson(map.value('requested')), + message: message, + path: path, + trace: trace, + ); + }, + 'TransferConfirmationsError': (errorData, message, path, trace) => + GetNftInfoErrorTransferConfirmationsErrorException( + TransferConfirmationsError.fromJson(errorData), + message: message, + path: path, + trace: trace, + ), + 'TrezorNotInitialized': (errorData, message, path, trace) => + TrezorConnectionErrorTrezorNotInitializedException( + message: message, + path: path, + trace: trace, + ), + 'TxTypeNotSupported': (errorData, message, path, trace) => + WithdrawErrorTxTypeNotSupportedException( + message: message, + path: path, + trace: trace, + ), + 'UUIDNotFound': (errorData, message, path, trace) { + final map = _asJsonMap(errorData); + return CancelOrderErrorUUIDNotFoundException( + uuid: _stringFromJson(map.value('uuid')), + message: message, + path: path, + trace: trace, + ); + }, + 'UUIDNotPresentInDb': (errorData, message, path, trace) => + LatestSwapsErrUUIDNotPresentInDbException( + _stringFromJson(errorData), + message: message, + path: path, + trace: trace, + ), + 'UnableToLoadSavedSwaps': (errorData, message, path, trace) => + LatestSwapsErrUnableToLoadSavedSwapsException( + SavedSwapError.fromJson(errorData), + message: message, + path: path, + trace: trace, + ), + 'UnableToQuerySwapStorage': (errorData, message, path, trace) => + LatestSwapsErrUnableToQuerySwapStorageException( + message: message, + path: path, + trace: trace, + ), + 'UnexpectedAccountType': (errorData, message, path, trace) { + final map = _asJsonMap(errorData); + return TendermintCoinRpcErrorUnexpectedAccountTypeException( + prefix: _stringFromJson(map.value('prefix')), + message: message, + path: path, + trace: trace, + ); + }, + 'UnexpectedCoinProtocol': (errorData, message, path, trace) { + final map = _asJsonMap(errorData); + return InitStandaloneCoinErrorUnexpectedCoinProtocolException( + ticker: _stringFromJson(map.value('ticker')), + protocol: JsonValue.fromJson(map.value('protocol')), + message: message, + path: path, + trace: trace, + ); + }, + 'UnexpectedDeviceActivationPolicy': (errorData, message, path, trace) => + EnablePlatformCoinWithTokensErrorUnexpectedDeviceActivationPolicyException( + message: message, + path: path, + trace: trace, + ), + 'UnexpectedEvent': (errorData, message, path, trace) { + final map = _asJsonMap(errorData); + return RecreateSwapErrorUnexpectedEventException( + expected: _stringFromJson(map.value('expected')), + found: _stringFromJson(map.value('found')), + message: message, + path: path, + trace: trace, + ); + }, + 'UnexpectedFromAddress': (errorData, message, path, trace) => + WithdrawErrorUnexpectedFromAddressException( + _stringFromJson(errorData), + message: message, + path: path, + trace: trace, + ), + 'UnexpectedL2Protocol': (errorData, message, path, trace) { + final map = _asJsonMap(errorData); + return InitL2ErrorUnexpectedL2ProtocolException( + ticker: _stringFromJson(map.value('ticker')), + protocol: JsonValue.fromJson(map.value('protocol')), + message: message, + path: path, + trace: trace, + ); + }, + 'UnexpectedMethod': (errorData, message, path, trace) { + final list = _asJsonList(errorData); + return LightningValidationErrUnexpectedMethodException( + _stringFromJson(list[0]), + _stringFromJson(list[1]), + message: message, + path: path, + trace: trace, + ); + }, + 'UnexpectedPlatformProtocol': (errorData, message, path, trace) { + final map = _asJsonMap(errorData); + return EnablePlatformCoinWithTokensErrorUnexpectedPlatformProtocolException( + ticker: _stringFromJson(map.value('ticker')), + protocol: JsonValue.fromJson(map.value('protocol')), + message: message, + path: path, + trace: trace, + ); + }, + 'UnprofitableReward': (errorData, message, path, trace) { + final map = _asJsonMap(errorData); + return DelegationErrorUnprofitableRewardException( + reward: BigDecimal.fromJson(map.value('reward')), + fee: BigDecimal.fromJson(map.value('fee')), + message: message, + path: path, + trace: trace, + ); + }, + 'UnreachableNodes': (errorData, message, path, trace) => + EthActivationV2ErrorUnreachableNodesException( + _stringFromJson(errorData), + message: message, + path: path, + trace: trace, + ), + 'UnsupportedChain': (errorData, message, path, trace) { + final map = _asJsonMap(errorData); + return EthActivationV2ErrorUnsupportedChainException( + chain: _stringFromJson(map.value('chain')), + feature: _stringFromJson(map.value('feature')), + message: message, + path: path, + trace: trace, + ); + }, + 'UnsupportedError': (errorData, message, path, trace) => + WithdrawErrorUnsupportedErrorException( + _stringFromJson(errorData), + message: message, + path: path, + trace: trace, + ), + 'UnsupportedProtocol': (errorData, message, path, trace) { + final map = _asJsonMap(errorData); + return EnableCoinUnifiedErrorUnsupportedProtocolException( + ticker: _stringFromJson(map.value('ticker')), + message: message, + path: path, + trace: trace, + ); + }, + 'UnsupportedSwapType': (errorData, message, path, trace) => + MySwapStatusErrorUnsupportedSwapTypeException( + _intFromJson(errorData), + message: message, + path: path, + trace: trace, + ), + 'UnsupportedTokenProtocol': (errorData, message, path, trace) { + final map = _asJsonMap(errorData); + return TokenInfoErrorUnsupportedTokenProtocolException( + protocol: _stringFromJson(map.value('protocol')), + message: message, + path: path, + trace: trace, + ); + }, + 'UpdateSpamPhishingError': (errorData, message, path, trace) => + UpdateNftErrorUpdateSpamPhishingErrorException( + UpdateSpamPhishingError.fromJson(errorData), + message: message, + path: path, + trace: trace, + ), + 'UserpassIsInvalid': (errorData, message, path, trace) => + DispatcherErrorUserpassIsInvalidException( + RateLimitError.fromJson(errorData), + message: message, + path: path, + trace: trace, + ), + 'UserpassIsNotSet': (errorData, message, path, trace) => + DispatcherErrorUserpassIsNotSetException( + message: message, + path: path, + trace: trace, + ), + 'UtxoInitError': (errorData, message, path, trace) => + EnableCoinUnifiedErrorUtxoInitErrorException( + _stringFromJson(errorData), + message: message, + path: path, + trace: trace, + ), + 'WalletInitError': (errorData, message, path, trace) => + MmInitErrorWalletInitErrorException( + _stringFromJson(errorData), + message: message, + path: path, + trace: trace, + ), + 'WalletsStorageError': (errorData, message, path, trace) => + MnemonicRpcErrorWalletsStorageErrorException( + _stringFromJson(errorData), + message: message, + path: path, + trace: trace, + ), + 'Web3RpcError': (errorData, message, path, trace) => + Erc20CallErrorWeb3RpcErrorException( + _stringFromJson(errorData), + message: message, + path: path, + trace: trace, + ), + 'ZcoinInitError': (errorData, message, path, trace) => + EnableCoinUnifiedErrorZcoinInitErrorException( + _stringFromJson(errorData), + message: message, + path: path, + trace: trace, + ), + 'ZeroBalanceToWithdrawMax': (errorData, message, path, trace) => + WithdrawErrorZeroBalanceToWithdrawMaxException( + message: message, + path: path, + trace: trace, + ), + }; +} diff --git a/packages/komodo_defi_rpc_methods/lib/src/models/models.dart b/packages/komodo_defi_rpc_methods/lib/src/models/models.dart index 419f31053..30f41f83b 100644 --- a/packages/komodo_defi_rpc_methods/lib/src/models/models.dart +++ b/packages/komodo_defi_rpc_methods/lib/src/models/models.dart @@ -6,6 +6,8 @@ library models; export 'base_request.dart'; export 'base_response.dart'; export 'error_response.dart'; +export 'kdf_error_messages.dart'; +export 'mm2_rpc_exceptions.dart'; export 'new_task.dart'; export 'new_task_response.dart'; export 'params.dart'; diff --git a/packages/komodo_defi_rpc_methods/lib/src/models/task_response_details.dart b/packages/komodo_defi_rpc_methods/lib/src/models/task_response_details.dart index 5b851868b..5c89670f9 100644 --- a/packages/komodo_defi_rpc_methods/lib/src/models/task_response_details.dart +++ b/packages/komodo_defi_rpc_methods/lib/src/models/task_response_details.dart @@ -1,9 +1,10 @@ import 'dart:convert'; import 'package:komodo_defi_rpc_methods/src/internal_exports.dart'; +import 'package:komodo_defi_types/komodo_defi_type_utils.dart'; /// Generic response details wrapper for task status responses -class ResponseDetails { +class ResponseDetails { ResponseDetails({required this.data, required this.error, this.description}) : assert( [data, error, description].where((e) => e != null).length == 1, @@ -11,7 +12,7 @@ class ResponseDetails { ); final T? data; - final R? error; + final E? error; // Usually only non-null for in-progress tasks /// Additional status information for in-progress tasks @@ -28,10 +29,59 @@ class ResponseDetails { Map toJson() { return { if (data != null) 'data': jsonEncode(data), - if (error != null) 'error': jsonEncode(error), + if (error != null) 'error': error.toString(), if (description != null) - 'description': - description is String ? description : jsonEncode(description), + 'description': description is String + ? description + : jsonEncode(description), }; } } + +/// Parses task error details into a typed [Exception]. +Exception parseTaskErrorDetails(dynamic detailsJson) { + if (detailsJson is Map) { + try { + final detailsMap = convertToJsonMap(detailsJson); + final typedError = KdfErrorRegistry.tryParse(detailsMap); + if (typedError != null) { + return typedError; + } + return GeneralErrorResponse.parse(detailsMap); + } catch (_) { + // Fall back to string-based exception extraction below. + } + } + + final message = detailsJson?.toString().trim(); + if (message == null || message.isEmpty) { + return Exception('Unknown error'); + } + return Exception(message); +} + +/// Extracts a human-readable message from [Exception]. +String? exceptionMessage(Exception? error) { + if (error == null) { + return null; + } + + if (error is MmRpcException) { + return error.displayMessage; + } + + if (error is GeneralErrorResponse) { + return error.error; + } + + final raw = error.toString().trim(); + if (raw.isEmpty) { + return null; + } + const exceptionPrefix = 'Exception: '; + if (raw.startsWith(exceptionPrefix)) { + final message = raw.substring(exceptionPrefix.length).trim(); + return message.isEmpty ? null : message; + } + return raw; +} diff --git a/packages/komodo_defi_rpc_methods/lib/src/rpc_methods/eth/task_enable_eth_init.dart b/packages/komodo_defi_rpc_methods/lib/src/rpc_methods/eth/task_enable_eth_init.dart index b79825e4f..b0abc57f3 100644 --- a/packages/komodo_defi_rpc_methods/lib/src/rpc_methods/eth/task_enable_eth_init.dart +++ b/packages/komodo_defi_rpc_methods/lib/src/rpc_methods/eth/task_enable_eth_init.dart @@ -1,5 +1,4 @@ import 'package:komodo_defi_rpc_methods/src/internal_exports.dart'; -import 'package:komodo_defi_types/komodo_defi_type_utils.dart'; class TaskEnableEthInit extends BaseRequest { @@ -22,19 +21,6 @@ class TaskEnableEthInit }; @override - NewTaskResponse parseResponse(String responseBody) { - final json = jsonFromString(responseBody); - if (GeneralErrorResponse.isErrorResponse(json)) { - throw GeneralErrorResponse.parse(json); - } - return NewTaskResponse.parse(json); - } - - @override - NewTaskResponse parse(Map json) { - if (GeneralErrorResponse.isErrorResponse(json)) { - throw GeneralErrorResponse.parse(json); - } - return NewTaskResponse.parse(json); - } + NewTaskResponse parse(Map json) => + NewTaskResponse.parse(json); } diff --git a/packages/komodo_defi_rpc_methods/lib/src/rpc_methods/hd_wallet/account_balance.dart b/packages/komodo_defi_rpc_methods/lib/src/rpc_methods/hd_wallet/account_balance.dart index e6728b2cd..e72fe21c9 100644 --- a/packages/komodo_defi_rpc_methods/lib/src/rpc_methods/hd_wallet/account_balance.dart +++ b/packages/komodo_defi_rpc_methods/lib/src/rpc_methods/hd_wallet/account_balance.dart @@ -83,35 +83,27 @@ class AccountBalanceStatusResponse extends BaseResponse { factory AccountBalanceStatusResponse.parse(JsonMap json) { final result = json.value('result'); final status = _statusFromTaskStatus(result.value('status')); + final detailsJson = result['details']; return AccountBalanceStatusResponse( mmrpc: json.value('mmrpc'), status: status!, - // details: status == 'Ok' ? AccountBalanceInfo.fromJson(details) : details, - details: ResponseDetails< - AccountBalanceInfo, - GeneralErrorResponse, - String - >( - data: - status == SyncStatusEnum.success - ? AccountBalanceInfo.fromJson(result.value('details')) - : null, - error: - status == SyncStatusEnum.error - ? GeneralErrorResponse.parse(result.value('details')) - : null, - description: - status == SyncStatusEnum.inProgress - ? result.value('details') - : null, + details: ResponseDetails( + data: status == SyncStatusEnum.success + ? AccountBalanceInfo.fromJson(detailsJson as JsonMap) + : null, + error: status == SyncStatusEnum.error + ? parseTaskErrorDetails(detailsJson) + : null, + description: status == SyncStatusEnum.inProgress + ? detailsJson?.toString() + : null, ), ); } final SyncStatusEnum status; - final ResponseDetails - details; + final ResponseDetails details; @override JsonMap toJson() { diff --git a/packages/komodo_defi_rpc_methods/lib/src/rpc_methods/hd_wallet/get_new_address_task.dart b/packages/komodo_defi_rpc_methods/lib/src/rpc_methods/hd_wallet/get_new_address_task.dart index cba82acf0..25fb93657 100644 --- a/packages/komodo_defi_rpc_methods/lib/src/rpc_methods/hd_wallet/get_new_address_task.dart +++ b/packages/komodo_defi_rpc_methods/lib/src/rpc_methods/hd_wallet/get_new_address_task.dart @@ -103,14 +103,14 @@ class GetNewAddressTaskStatusResponse extends BaseResponse { final detailsJson = result['details']; Object? description; NewAddressInfo? data; - GeneralErrorResponse? error; + Exception? error; if (status == SyncStatusEnum.success) { data = NewAddressInfo.fromJson( (detailsJson as JsonMap).value('new_address'), ); } else if (status == SyncStatusEnum.error) { - error = GeneralErrorResponse.parse(detailsJson as JsonMap); + error = parseTaskErrorDetails(detailsJson); } else if (status == SyncStatusEnum.inProgress) { description = TaskDescriptionParserFactory.parseDescription(detailsJson); } @@ -118,7 +118,7 @@ class GetNewAddressTaskStatusResponse extends BaseResponse { return GetNewAddressTaskStatusResponse( mmrpc: json.value('mmrpc'), status: status, - details: ResponseDetails( + details: ResponseDetails( data: data, error: error, description: description, @@ -127,7 +127,7 @@ class GetNewAddressTaskStatusResponse extends BaseResponse { } final SyncStatusEnum status; - final ResponseDetails details; + final ResponseDetails details; @override JsonMap toJson() { @@ -158,7 +158,7 @@ class GetNewAddressTaskStatusResponse extends BaseResponse { case SyncStatusEnum.error: return NewAddressState( status: NewAddressStatus.error, - error: details.error?.error ?? 'Unknown error', + error: exceptionMessage(details.error) ?? 'Unknown error', taskId: taskId, ); case SyncStatusEnum.inProgress: diff --git a/packages/komodo_defi_rpc_methods/lib/src/rpc_methods/hd_wallet/scan_for_new_addresses_status.dart b/packages/komodo_defi_rpc_methods/lib/src/rpc_methods/hd_wallet/scan_for_new_addresses_status.dart index a897cc94d..347f7d05a 100644 --- a/packages/komodo_defi_rpc_methods/lib/src/rpc_methods/hd_wallet/scan_for_new_addresses_status.dart +++ b/packages/komodo_defi_rpc_methods/lib/src/rpc_methods/hd_wallet/scan_for_new_addresses_status.dart @@ -34,24 +34,41 @@ class ScanForNewAddressesStatusResponse extends BaseResponse { required super.mmrpc, required this.status, this.details, + this.error, + this.statusDescription, }); @override factory ScanForNewAddressesStatusResponse.parse(Map json) { + final status = json.value('result', 'status'); + final rawDetails = json.valueOrNull('result', 'details'); + + ScanAddressesInfo? details; + Exception? error; + String? statusDescription; + + if (status == 'Ok' && rawDetails is Map) { + details = ScanAddressesInfo.fromJson(rawDetails); + } else if (status == 'Error' && rawDetails != null) { + error = parseTaskErrorDetails(rawDetails); + statusDescription = exceptionMessage(error); + } else if (rawDetails != null) { + statusDescription = rawDetails.toString(); + } + return ScanForNewAddressesStatusResponse( mmrpc: json.value('mmrpc'), - status: json.value('result', 'status'), - details: - json.valueOrNull>('result', 'details') != null - ? ScanAddressesInfo.fromJson( - json.value>('result', 'details'), - ) - : null, + status: status, + details: details, + error: error, + statusDescription: statusDescription, ); } final String status; final ScanAddressesInfo? details; + final Exception? error; + final String? statusDescription; @override Map toJson() { @@ -60,6 +77,8 @@ class ScanForNewAddressesStatusResponse extends BaseResponse { 'result': { 'status': status, if (details != null) 'details': details!.toJson(), + if (error != null) 'error': error.toString(), + if (statusDescription != null) 'description': statusDescription, }, }; } diff --git a/packages/komodo_defi_rpc_methods/lib/src/rpc_methods/trezor/trezor_rpc_namespace.dart b/packages/komodo_defi_rpc_methods/lib/src/rpc_methods/trezor/trezor_rpc_namespace.dart index 4c98b548f..9efbd9961 100644 --- a/packages/komodo_defi_rpc_methods/lib/src/rpc_methods/trezor/trezor_rpc_namespace.dart +++ b/packages/komodo_defi_rpc_methods/lib/src/rpc_methods/trezor/trezor_rpc_namespace.dart @@ -252,10 +252,10 @@ class TrezorStatusResponse extends BaseResponse { return null; } - /// Returns error info if status is 'Error' - GeneralErrorResponse? get errorInfo { - if (status == 'Error' && details is JsonMap) { - return GeneralErrorResponse.parse(details as JsonMap); + /// Returns typed error info if status is 'Error' + Exception? get errorInfo { + if (status == 'Error') { + return parseTaskErrorDetails(details); } return null; } diff --git a/packages/komodo_defi_rpc_methods/lib/src/rpc_methods/wallet/change_mnemonic_password.dart b/packages/komodo_defi_rpc_methods/lib/src/rpc_methods/wallet/change_mnemonic_password.dart index 4e6b66253..30df50068 100644 --- a/packages/komodo_defi_rpc_methods/lib/src/rpc_methods/wallet/change_mnemonic_password.dart +++ b/packages/komodo_defi_rpc_methods/lib/src/rpc_methods/wallet/change_mnemonic_password.dart @@ -2,14 +2,13 @@ import 'package:komodo_defi_rpc_methods/src/internal_exports.dart'; import 'package:komodo_defi_types/komodo_defi_type_utils.dart'; -import 'package:komodo_defi_types/komodo_defi_types.dart'; +/// Request to change the mnemonic password. +/// +/// Errors are automatically parsed into typed [MmRpcException] subclasses +/// by the [KdfErrorRegistry]. class ChangeMnemonicPasswordRequest - extends - BaseRequest< - ChangeMnemonicPasswordResponse, - ChangeMnemonicIncorrectPasswordErrorResponse - > { + extends BaseRequest { ChangeMnemonicPasswordRequest({ required super.rpcPass, required this.currentPassword, @@ -31,63 +30,11 @@ class ChangeMnemonicPasswordRequest }, }; - @override - ChangeMnemonicIncorrectPasswordErrorResponse? parseCustomErrorResponse( - JsonMap json, - ) { - if (ChangeMnemonicIncorrectPasswordErrorResponse.isWrongPasswordError( - json, - )) { - return ChangeMnemonicIncorrectPasswordErrorResponse.parse(json); - } - - return null; // Let the base implementation handle other types of errors - } - @override ChangeMnemonicPasswordResponse parse(Map json) => ChangeMnemonicPasswordResponse.parse(json); } -/// Specific error response for wrong password in change_mnemonic_password -class ChangeMnemonicIncorrectPasswordErrorResponse - extends GeneralErrorResponse { - ChangeMnemonicIncorrectPasswordErrorResponse({ - required super.mmrpc, - required super.error, - required super.errorPath, - required super.errorTrace, - required super.errorType, - required super.errorData, - required super.object, - }); - - /// Parse error response from JSON - factory ChangeMnemonicIncorrectPasswordErrorResponse.parse( - Map json, - ) { - return ChangeMnemonicIncorrectPasswordErrorResponse( - mmrpc: json.valueOrNull('mmrpc') ?? '2.0', - error: json.valueOrNull('error'), - errorPath: json.valueOrNull('error_path'), - errorTrace: json.valueOrNull('error_trace'), - errorType: json.valueOrNull('error_type'), - errorData: json.valueOrNull('error_data'), - object: json, - ); - } - - /// Check if the error response is a wrong password error - static bool isWrongPasswordError(Map json) { - final errorMessage = json.valueOrNull('error') ?? ''; - final didFindWrongPasswordError = AuthException.findExceptionsInLog( - errorMessage, - ).any((e) => e.type == AuthExceptionType.incorrectPassword); - - return didFindWrongPasswordError; - } -} - class ChangeMnemonicPasswordResponse extends BaseResponse { ChangeMnemonicPasswordResponse({required super.mmrpc}); diff --git a/packages/komodo_defi_rpc_methods/lib/src/rpc_methods/wallet/delete_wallet.dart b/packages/komodo_defi_rpc_methods/lib/src/rpc_methods/wallet/delete_wallet.dart index d5efa6426..6f56cd22f 100644 --- a/packages/komodo_defi_rpc_methods/lib/src/rpc_methods/wallet/delete_wallet.dart +++ b/packages/komodo_defi_rpc_methods/lib/src/rpc_methods/wallet/delete_wallet.dart @@ -1,8 +1,18 @@ import 'package:komodo_defi_rpc_methods/src/internal_exports.dart'; import 'package:komodo_defi_types/komodo_defi_type_utils.dart'; +/// Request to delete a wallet. +/// +/// Errors are automatically parsed into typed [MmRpcException] subclasses +/// by the [KdfErrorRegistry]. Common error types include: +/// - `InvalidRequest` - The request was malformed +/// - `WalletNotFound` - The specified wallet does not exist +/// - `InvalidPassword` - The provided password is incorrect +/// - `CannotDeleteActiveWallet` - Cannot delete the currently active wallet +/// - `WalletsStorageError` - Error accessing wallet storage +/// - `InternalError` - An internal error occurred class DeleteWalletRequest - extends BaseRequest { + extends BaseRequest { DeleteWalletRequest({ required this.walletName, required this.password, @@ -21,26 +31,6 @@ class DeleteWalletRequest 'params': {'wallet_name': walletName, 'password': password}, }; - @override - DeleteWalletErrorResponse? parseCustomErrorResponse(JsonMap json) { - final type = json.valueOrNull('error_type'); - switch (type) { - case 'InvalidRequest': - return DeleteWalletInvalidRequestErrorResponse.parse(json); - case 'WalletNotFound': - return DeleteWalletWalletNotFoundErrorResponse.parse(json); - case 'InvalidPassword': - return DeleteWalletInvalidPasswordErrorResponse.parse(json); - case 'CannotDeleteActiveWallet': - return DeleteWalletCannotDeleteActiveWalletErrorResponse.parse(json); - case 'WalletsStorageError': - return DeleteWalletWalletsStorageErrorResponse.parse(json); - case 'InternalError': - return DeleteWalletInternalErrorResponse.parse(json); - } - return null; - } - @override DeleteWalletResponse parse(Map json) => DeleteWalletResponse.parse(json); @@ -56,170 +46,3 @@ class DeleteWalletResponse extends BaseResponse { @override Map toJson() => {'mmrpc': mmrpc, 'result': null}; } - -abstract class DeleteWalletErrorResponse extends GeneralErrorResponse { - DeleteWalletErrorResponse({ - required super.mmrpc, - required super.error, - required super.errorPath, - required super.errorTrace, - required super.errorType, - required super.errorData, - required super.object, - }); - - factory DeleteWalletErrorResponse.parse(Map json) { - return DeleteWalletInvalidRequestErrorResponse.parse(json); - } -} - -class DeleteWalletInvalidRequestErrorResponse - extends DeleteWalletErrorResponse { - DeleteWalletInvalidRequestErrorResponse({ - required super.mmrpc, - required super.error, - required super.errorPath, - required super.errorTrace, - required super.errorType, - required super.errorData, - required super.object, - }); - - factory DeleteWalletInvalidRequestErrorResponse.parse(JsonMap json) { - return DeleteWalletInvalidRequestErrorResponse( - mmrpc: json.valueOrNull('mmrpc') ?? '2.0', - error: json.valueOrNull('error'), - errorPath: json.valueOrNull('error_path'), - errorTrace: json.valueOrNull('error_trace'), - errorType: json.valueOrNull('error_type'), - errorData: json.valueOrNull('error_data'), - object: json, - ); - } -} - -class DeleteWalletWalletNotFoundErrorResponse - extends DeleteWalletErrorResponse { - DeleteWalletWalletNotFoundErrorResponse({ - required super.mmrpc, - required super.error, - required super.errorPath, - required super.errorTrace, - required super.errorType, - required super.errorData, - required super.object, - }); - - factory DeleteWalletWalletNotFoundErrorResponse.parse(JsonMap json) { - return DeleteWalletWalletNotFoundErrorResponse( - mmrpc: json.valueOrNull('mmrpc') ?? '2.0', - error: json.valueOrNull('error'), - errorPath: json.valueOrNull('error_path'), - errorTrace: json.valueOrNull('error_trace'), - errorType: json.valueOrNull('error_type'), - errorData: json.valueOrNull('error_data'), - object: json, - ); - } -} - -class DeleteWalletInvalidPasswordErrorResponse - extends DeleteWalletErrorResponse { - DeleteWalletInvalidPasswordErrorResponse({ - required super.mmrpc, - required super.error, - required super.errorPath, - required super.errorTrace, - required super.errorType, - required super.errorData, - required super.object, - }); - - factory DeleteWalletInvalidPasswordErrorResponse.parse(JsonMap json) { - return DeleteWalletInvalidPasswordErrorResponse( - mmrpc: json.valueOrNull('mmrpc') ?? '2.0', - error: json.valueOrNull('error'), - errorPath: json.valueOrNull('error_path'), - errorTrace: json.valueOrNull('error_trace'), - errorType: json.valueOrNull('error_type'), - errorData: json.valueOrNull('error_data'), - object: json, - ); - } -} - -class DeleteWalletCannotDeleteActiveWalletErrorResponse - extends DeleteWalletErrorResponse { - DeleteWalletCannotDeleteActiveWalletErrorResponse({ - required super.mmrpc, - required super.error, - required super.errorPath, - required super.errorTrace, - required super.errorType, - required super.errorData, - required super.object, - }); - - factory DeleteWalletCannotDeleteActiveWalletErrorResponse.parse( - JsonMap json, - ) { - return DeleteWalletCannotDeleteActiveWalletErrorResponse( - mmrpc: json.valueOrNull('mmrpc') ?? '2.0', - error: json.valueOrNull('error'), - errorPath: json.valueOrNull('error_path'), - errorTrace: json.valueOrNull('error_trace'), - errorType: json.valueOrNull('error_type'), - errorData: json.valueOrNull('error_data'), - object: json, - ); - } -} - -class DeleteWalletWalletsStorageErrorResponse - extends DeleteWalletErrorResponse { - DeleteWalletWalletsStorageErrorResponse({ - required super.mmrpc, - required super.error, - required super.errorPath, - required super.errorTrace, - required super.errorType, - required super.errorData, - required super.object, - }); - - factory DeleteWalletWalletsStorageErrorResponse.parse(JsonMap json) { - return DeleteWalletWalletsStorageErrorResponse( - mmrpc: json.valueOrNull('mmrpc') ?? '2.0', - error: json.valueOrNull('error'), - errorPath: json.valueOrNull('error_path'), - errorTrace: json.valueOrNull('error_trace'), - errorType: json.valueOrNull('error_type'), - errorData: json.valueOrNull('error_data'), - object: json, - ); - } -} - -class DeleteWalletInternalErrorResponse extends DeleteWalletErrorResponse { - DeleteWalletInternalErrorResponse({ - required super.mmrpc, - required super.error, - required super.errorPath, - required super.errorTrace, - required super.errorType, - required super.errorData, - required super.object, - }); - - factory DeleteWalletInternalErrorResponse.parse(JsonMap json) { - return DeleteWalletInternalErrorResponse( - mmrpc: json.valueOrNull('mmrpc') ?? '2.0', - error: json.valueOrNull('error'), - errorPath: json.valueOrNull('error_path'), - errorTrace: json.valueOrNull('error_trace'), - errorType: json.valueOrNull('error_type'), - errorData: json.valueOrNull('error_data'), - object: json, - ); - } -} diff --git a/packages/komodo_defi_rpc_methods/lib/src/strategies/balance/hd_wallet_balance_strategy.dart b/packages/komodo_defi_rpc_methods/lib/src/strategies/balance/hd_wallet_balance_strategy.dart index e80418c2f..337e081c6 100644 --- a/packages/komodo_defi_rpc_methods/lib/src/strategies/balance/hd_wallet_balance_strategy.dart +++ b/packages/komodo_defi_rpc_methods/lib/src/strategies/balance/hd_wallet_balance_strategy.dart @@ -54,8 +54,9 @@ class HDWalletBalanceStrategy extends BalanceStrategy { retryCount++; // Calculate jittered backoff delay - final baseDelay = - math.min(500 * math.pow(2, retryCount), 2000).toInt(); + final baseDelay = math + .min(500 * math.pow(2, retryCount), 2000) + .toInt(); final jitter = _random.nextInt(baseDelay ~/ 2); final delay = Duration(milliseconds: baseDelay + jitter); @@ -150,11 +151,10 @@ class HDWalletBalanceStrategy extends BalanceStrategy { } else if (status.status == SyncStatusEnum.error) { timer.cancel(); if (!completer.isCompleted) { + final taskError = + exceptionMessage(status.details.error) ?? 'Unknown error'; completer.completeError( - Exception( - 'HD wallet balance task failed: ' - '${status.details.error ?? "Unknown error"}', - ), + Exception('HD wallet balance task failed: $taskError'), ); } } diff --git a/packages/komodo_defi_rpc_methods/lib/src/strategies/pubkey/hd_multi_address_strategy.dart b/packages/komodo_defi_rpc_methods/lib/src/strategies/pubkey/hd_multi_address_strategy.dart index c3e4da8df..859e82ea2 100644 --- a/packages/komodo_defi_rpc_methods/lib/src/strategies/pubkey/hd_multi_address_strategy.dart +++ b/packages/komodo_defi_rpc_methods/lib/src/strategies/pubkey/hd_multi_address_strategy.dart @@ -8,6 +8,8 @@ mixin HDWalletMixin on PubkeyStrategy { KdfUser get kdfUser; int get _gapLimit => 20; + Duration get _scanPollInterval => const Duration(milliseconds: 250); + Duration get _scanTimeout => const Duration(seconds: 20); @override bool get supportsMultipleAddresses => true; @@ -28,7 +30,40 @@ mixin HDWalletMixin on PubkeyStrategy { @override Future scanForNewAddresses(AssetId assetId, ApiClient client) async { - await getAccountBalance(assetId, client); + final initResponse = await client.rpc.hdWallet.scanForNewAddressesInit( + assetId.id, + accountId: 0, + gapLimit: _gapLimit, + ); + + final startedAt = DateTime.now(); + while (true) { + final status = await client.rpc.hdWallet.scanForNewAddressesStatus( + initResponse.taskId, + forgetIfFinished: false, + ); + + if (status.status == 'Ok') { + return; + } + + if (status.status == 'Error') { + if (status.error != null) { + throw status.error!; + } + throw Exception( + status.statusDescription ?? 'Failed to scan for new addresses', + ); + } + + if (DateTime.now().difference(startedAt) >= _scanTimeout) { + throw TimeoutException( + 'Timed out scanning for new addresses for ${assetId.id}', + ); + } + + await Future.delayed(_scanPollInterval); + } } Future getAccountBalance( @@ -57,18 +92,17 @@ mixin HDWalletMixin on PubkeyStrategy { AssetId assetId, AccountBalanceInfo balanceInfo, ) async { - final addresses = - balanceInfo.addresses - .map( - (addr) => PubkeyInfo( - address: addr.address, - derivationPath: addr.derivationPath, - chain: addr.chain, - balance: addr.balance.balanceOf(assetId.id), - coinTicker: assetId.id, - ), - ) - .toList(); + final addresses = balanceInfo.addresses + .map( + (addr) => PubkeyInfo( + address: addr.address, + derivationPath: addr.derivationPath, + chain: addr.chain, + balance: addr.balance.balanceOf(assetId.id), + coinTicker: assetId.id, + ), + ) + .toList(); return AssetPubkeys( assetId: assetId, @@ -102,13 +136,12 @@ class ContextPrivKeyHDWalletStrategy extends PubkeyStrategy with HDWalletMixin { // TODO: Refactor to create a domain model with onlt a single balance entry. // Currently we are bound to the RPC response data structure. Future getNewAddress(AssetId assetId, ApiClient client) async { - final newAddress = - (await client.rpc.hdWallet.getNewAddress( - assetId.id, - accountId: 0, - chain: 'External', - gapLimit: _gapLimit, - )).newAddress; + final newAddress = (await client.rpc.hdWallet.getNewAddress( + assetId.id, + accountId: 0, + chain: 'External', + gapLimit: _gapLimit, + )).newAddress; // Get the balance for the specific coin, or use the first balance if not // found diff --git a/packages/komodo_defi_rpc_methods/tool/generate_mm2_rpc_exceptions.py b/packages/komodo_defi_rpc_methods/tool/generate_mm2_rpc_exceptions.py new file mode 100644 index 000000000..9755a46fa --- /dev/null +++ b/packages/komodo_defi_rpc_methods/tool/generate_mm2_rpc_exceptions.py @@ -0,0 +1,2268 @@ +#!/usr/bin/env python3 +import argparse +import json +import os +import re +import shutil +import subprocess +import sys +import tempfile +from dataclasses import dataclass, field +from pathlib import Path +from typing import Optional, Union + +DEFAULT_REPO_URL = "https://github.com/KomodoPlatform/komodo-defi-framework.git" + + +@dataclass +class SerdeAttrs: + tag: Optional[str] = None + content: Optional[str] = None + rename_all: Optional[str] = None + rename: Optional[str] = None + other: bool = False + + +@dataclass +class FieldDef: + name: str + rust_type: str + json_name: Optional[str] = None + serde_rename: Optional[str] = None + + +@dataclass +class EnumVariantDef: + name: str + fields: list[FieldDef] + kind: str # unit | tuple | struct + serde_rename: Optional[str] = None + serde_other: bool = False + + +@dataclass +class EnumDef: + rust_name: str + dart_name: str + serde: SerdeAttrs + variants: list[EnumVariantDef] = field(default_factory=list) + + +@dataclass +class StructDef: + rust_name: str + dart_name: str + serde: SerdeAttrs + fields: list[FieldDef] + kind: str # unit | tuple | struct + + +@dataclass +class AliasDef: + rust_name: str + dart_name: str + target_type: str + + +@dataclass +class ExternalDef: + rust_name: str + dart_name: str + kind: str # wrapper or alias + base_type: str + + +def split_top_level(value: str) -> list[str]: + items: list[str] = [] + current: list[str] = [] + depth_angle = depth_paren = depth_brace = depth_bracket = 0 + in_string = False + string_char = "" + escape = False + raw_hashes: Optional[int] = None + + i = 0 + while i < len(value): + ch = value[i] + + if in_string: + current.append(ch) + if raw_hashes is not None: + if ch == '"' and value[i + 1 : i + 1 + raw_hashes] == "#" * raw_hashes: + if raw_hashes: + current.append("#" * raw_hashes) + i += raw_hashes + in_string = False + raw_hashes = None + else: + if escape: + escape = False + elif ch == "\\": + escape = True + elif ch == string_char: + in_string = False + i += 1 + continue + + if ch == "r": + j = i + 1 + hash_count = 0 + while j < len(value) and value[j] == "#": + hash_count += 1 + j += 1 + if j < len(value) and value[j] == '"': + in_string = True + string_char = '"' + raw_hashes = hash_count + current.append(value[i : j + 1]) + i = j + 1 + continue + + if ch in ("\"", "'"): + in_string = True + string_char = ch + raw_hashes = None + current.append(ch) + i += 1 + continue + + if ch == "<": + depth_angle += 1 + elif ch == ">": + depth_angle = max(0, depth_angle - 1) + elif ch == "(": + depth_paren += 1 + elif ch == ")": + depth_paren = max(0, depth_paren - 1) + elif ch == "{": + depth_brace += 1 + elif ch == "}": + depth_brace = max(0, depth_brace - 1) + elif ch == "[": + depth_bracket += 1 + elif ch == "]": + depth_bracket = max(0, depth_bracket - 1) + + if ( + ch == "," + and depth_angle == 0 + and depth_paren == 0 + and depth_brace == 0 + and depth_bracket == 0 + ): + item = "".join(current).strip() + if item: + items.append(item) + current = [] + i += 1 + continue + current.append(ch) + i += 1 + tail = "".join(current).strip() + if tail: + items.append(tail) + return items + + +def strip_line_comment(value: str) -> str: + if "//" in value: + return value.split("//", 1)[0].rstrip() + return value.rstrip() + + +def extract_preceding_attrs(source: str, start_idx: int) -> str: + head = source[:start_idx].splitlines() + attrs: list[str] = [] + i = len(head) - 1 + while i >= 0: + line = head[i].strip() + if not line: + break + if line.startswith("#["): + attrs.insert(0, line) + i -= 1 + continue + break + return "\n".join(attrs) + + +def parse_serde_attrs(attr_text: str) -> SerdeAttrs: + attrs = SerdeAttrs() + if not attr_text: + return attrs + if "serde" not in attr_text: + return attrs + tag_match = re.search(r'tag\s*=\s*"([^"]+)"', attr_text) + if tag_match: + attrs.tag = tag_match.group(1) + content_match = re.search(r'content\s*=\s*"([^"]+)"', attr_text) + if content_match: + attrs.content = content_match.group(1) + rename_all_match = re.search(r'rename_all\s*=\s*"([^"]+)"', attr_text) + if rename_all_match: + attrs.rename_all = rename_all_match.group(1) + rename_match = re.search(r'rename\s*=\s*"([^"]+)"', attr_text) + if rename_match: + attrs.rename = rename_match.group(1) + if re.search(r'\bother\b', attr_text): + attrs.other = True + return attrs + + +def split_attr_lines(lines: list[str]) -> tuple[list[str], list[str]]: + attr_lines: list[str] = [] + rest_lines: list[str] = [] + in_attr = False + bracket_depth = 0 + for line in lines: + stripped = line.strip() + if stripped.startswith("#[") or in_attr: + attr_lines.append(line) + bracket_depth += line.count("[") + bracket_depth -= line.count("]") + if bracket_depth <= 0: + in_attr = False + bracket_depth = 0 + else: + in_attr = True + continue + rest_lines.append(line) + return attr_lines, rest_lines + + +def words_from_identifier(name: str) -> list[str]: + spaced = re.sub(r"([a-z0-9])([A-Z])", r"\1 \2", name) + spaced = re.sub(r"[^0-9A-Za-z]+", " ", spaced) + return [part for part in spaced.strip().split() if part] + + +def apply_rename_all(name: str, rename_all: Optional[str]) -> str: + if not rename_all: + return name + words = words_from_identifier(name) + if rename_all == "lowercase": + return "".join(words).lower() + if rename_all == "UPPERCASE": + return "".join(words).upper() + if rename_all == "snake_case": + return "_".join(word.lower() for word in words) + if rename_all == "SCREAMING_SNAKE_CASE": + return "_".join(word.upper() for word in words) + if rename_all == "kebab-case": + return "-".join(word.lower() for word in words) + if rename_all == "camelCase": + if not words: + return name + return words[0].lower() + "".join(w.title() for w in words[1:]) + if rename_all == "PascalCase": + return "".join(w.title() for w in words) + return name + + +def extract_block(source: str, start_idx: int, open_ch: str, close_ch: str) -> tuple[str, int]: + depth = 0 + i = start_idx + body_chars: list[str] = [] + while i < len(source): + ch = source[i] + if ch == open_ch: + depth += 1 + if depth == 1: + i += 1 + continue + elif ch == close_ch: + depth -= 1 + if depth == 0: + return "".join(body_chars), i + if depth >= 1: + body_chars.append(ch) + i += 1 + return "".join(body_chars), i + + +def parse_named_fields(body: str, rename_all: Optional[str]) -> list[FieldDef]: + fields: list[FieldDef] = [] + for raw in split_top_level(body): + raw = strip_line_comment(raw).strip() + if not raw: + continue + lines = [line for line in raw.splitlines() if line.strip()] + attr_lines, rest_lines = split_attr_lines(lines) + attr_text = "\n".join(attr_lines) + rest = " ".join(rest_lines).strip() + serde = parse_serde_attrs(attr_text) + m = re.match(r"(?:pub(?:\([^)]*\))?\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*:\s*(.+)", rest) + if not m: + continue + name = m.group(1) + ty = m.group(2).strip() + json_name = serde.rename or apply_rename_all(name, rename_all) + fields.append( + FieldDef( + name=name, + rust_type=ty, + json_name=json_name, + serde_rename=serde.rename, + ) + ) + return fields + + +def parse_enum_variants(body: str, rename_all: Optional[str]) -> list[EnumVariantDef]: + variants: list[EnumVariantDef] = [] + for raw in split_top_level(body): + raw = strip_line_comment(raw).strip() + if not raw: + continue + lines = [line for line in raw.splitlines() if line.strip()] + attr_lines, rest_lines = split_attr_lines(lines) + attr_text = "\n".join(attr_lines) + rest = " ".join(rest_lines).strip() + serde = parse_serde_attrs(attr_text) + if "{" in rest: + name = rest.split("{", 1)[0].strip() + block_body, _ = extract_block(rest, rest.index("{"), "{", "}") + fields = parse_named_fields(block_body, None) + variants.append( + EnumVariantDef( + name=name, + fields=fields, + kind="struct", + serde_rename=serde.rename, + serde_other=serde.other, + ) + ) + elif "(" in rest: + name = rest.split("(", 1)[0].strip() + block_body, _ = extract_block(rest, rest.index("("), "(", ")") + tuple_fields = [ + FieldDef(name=str(idx), rust_type=ty.strip()) + for idx, ty in enumerate(split_top_level(block_body)) + ] + variants.append( + EnumVariantDef( + name=name, + fields=tuple_fields, + kind="tuple", + serde_rename=serde.rename, + serde_other=serde.other, + ) + ) + else: + name = rest.strip() + if "=" in name: + name = name.split("=", 1)[0].strip() + if not name: + continue + variants.append( + EnumVariantDef( + name=name, + fields=[], + kind="unit", + serde_rename=serde.rename, + serde_other=serde.other, + ) + ) + return variants + + +def find_type_definition( + rust_name: str, + dart_name: str, + rust_files: list[Path], + preferred_segments: Optional[list[str]] = None, +) -> Optional[Union[EnumDef, StructDef, AliasDef]]: + name_pattern = re.compile(rf"\b(enum|struct|type)\s+{re.escape(rust_name)}\b") + + def score_path(path: Path) -> int: + if not preferred_segments: + return 0 + parts = path.as_posix().split("/") + score = 0 + for seg in preferred_segments: + if seg in parts: + score += 1 + return score + + matches: list[tuple[int, Path, re.Match[str], str]] = [] + for path in rust_files: + try: + text = path.read_text(encoding="utf-8") + except Exception: + continue + match = name_pattern.search(text) + if match: + matches.append((score_path(path), path, match, text)) + + if not matches: + return None + + matches.sort(key=lambda item: item[0], reverse=True) + _score, _path, match, text = matches[0] + + kind = match.group(1) + attr_text = extract_preceding_attrs(text, match.start()) + serde = parse_serde_attrs(attr_text) + + # Find body start (skip generics) + i = match.end() + depth_angle = 0 + body_start = None + while i < len(text): + ch = text[i] + if ch == "<": + depth_angle += 1 + elif ch == ">": + depth_angle = max(0, depth_angle - 1) + elif depth_angle == 0 and ch in "{(;": + body_start = i + break + i += 1 + + if kind == "type": + if body_start is None: + return None + rhs_start = text.find("=", match.end()) + if rhs_start == -1: + return None + rhs_end = text.find(";", rhs_start) + if rhs_end == -1: + return None + target = text[rhs_start + 1 : rhs_end].strip() + return AliasDef(rust_name=rust_name, dart_name=dart_name, target_type=target) + + if body_start is None: + return None + + if text[body_start] == "{": + body, _ = extract_block(text, body_start, "{", "}") + if kind == "struct": + fields = parse_named_fields(body, serde.rename_all) + return StructDef( + rust_name=rust_name, + dart_name=dart_name, + serde=serde, + fields=fields, + kind="struct", + ) + variants = parse_enum_variants(body, serde.rename_all) + return EnumDef( + rust_name=rust_name, + dart_name=dart_name, + serde=serde, + variants=variants, + ) + + if text[body_start] == "(" and kind == "struct": + body, _ = extract_block(text, body_start, "(", ")") + tuple_fields = [ + FieldDef(name=str(idx), rust_type=ty.strip()) + for idx, ty in enumerate(split_top_level(body)) + ] + return StructDef( + rust_name=rust_name, + dart_name=dart_name, + serde=serde, + fields=tuple_fields, + kind="tuple", + ) + + if text[body_start] == ";" and kind == "struct": + return StructDef( + rust_name=rust_name, + dart_name=dart_name, + serde=serde, + fields=[], + kind="unit", + ) + + return None + + + +def parse_args() -> argparse.Namespace: + package_root = Path(__file__).resolve().parent.parent + default_out = package_root / "lib" / "src" / "models" / "mm2_rpc_exceptions.dart" + + parser = argparse.ArgumentParser( + description=( + "Generate Mm2 RPC exceptions from the Komodo DeFi Framework source." + ) + ) + parser.add_argument( + "--mm2-repo", + "-r", + help="Path to a local komodo-defi-framework checkout.", + ) + parser.add_argument( + "--repo-url", + default=DEFAULT_REPO_URL, + help="Git URL used when cloning the API repository.", + ) + parser.add_argument( + "--out", + default=str(default_out), + help="Output path for generated Dart exceptions.", + ) + parser.add_argument( + "--no-clone", + action="store_true", + help="Require --mm2-repo instead of cloning into a temp directory.", + ) + parser.add_argument( + "--fix", + dest="fix", + action="store_true", + help="Run `dart fix --apply` on the generated file.", + ) + parser.add_argument( + "--no-fix", + dest="fix", + action="store_false", + help="Skip running `dart fix --apply` on the generated file.", + ) + parser.set_defaults(fix=True) + return parser.parse_args() + + +def run(cmd: list[str], cwd: Optional[Path] = None) -> None: + subprocess.run(cmd, check=True, cwd=str(cwd) if cwd else None) + + +def resolve_mm2_repo(args: argparse.Namespace) -> tuple[Path, Optional[tempfile.TemporaryDirectory]]: + if args.mm2_repo: + repo_path = Path(args.mm2_repo).expanduser().resolve() + if not repo_path.exists(): + raise SystemExit(f"--mm2-repo path does not exist: {repo_path}") + return repo_path, None + + if args.no_clone: + raise SystemExit("Provide --mm2-repo or allow cloning by removing --no-clone.") + + tmp_dir = tempfile.TemporaryDirectory(prefix="kdf-mm2-") + repo_path = Path(tmp_dir.name) / "komodo-defi-framework" + run(["git", "clone", "--depth", "1", args.repo_url, str(repo_path)]) + return repo_path, tmp_dir + + +def generate_lines(enums: list[dict], mm2_repo_root: Path) -> list[str]: + + def sanitize_type_name(name: str) -> str: + name = name.replace("::", " ") + name = re.sub(r"[^0-9A-Za-z]+", " ", name) + parts = [part for part in name.split() if part] + if not parts: + return "Unknown" + formatted_parts = [] + for part in parts: + if len(part) == 1: + formatted_parts.append(part.upper()) + else: + formatted_parts.append(part[0].upper() + part[1:]) + return "".join(formatted_parts) + + def split_generic(s: str) -> tuple[str, str]: + depth = 0 + current: list[str] = [] + parts: list[str] = [] + for ch in s: + if ch in "<({[": + depth += 1 + elif ch in ">)}]": + depth -= 1 + elif ch == "," and depth == 0: + parts.append("".join(current).strip()) + current = [] + continue + current.append(ch) + parts.append("".join(current).strip()) + if len(parts) != 2: + raise ValueError(f"Expected two generic arguments, got {parts}") + return parts[0], parts[1] + + def pascal_case(name: str) -> str: + name = re.sub(r"([a-z0-9])([A-Z])", r"\1 \2", name) + name = re.sub(r"[^0-9A-Za-z]+", " ", name) + parts = [part for part in name.split() if part] + if not parts: + return "Unknown" + formatted = [] + for part in parts: + if len(part) == 1: + formatted.append(part.upper()) + else: + formatted.append(part[0].upper() + part[1:]) + return "".join(formatted) + + def camel_case(name: str) -> str: + base = pascal_case(name) + return base[0].lower() + base[1:] if base else "value" + + # Names reserved by the base MmRpcException and Dart keywords we avoid colliding with. + reserved_field_names = { + "message", + "path", + "trace", + "errorType", + } + reserved_enum_members = { + "as", + "assert", + "async", + "await", + "break", + "case", + "catch", + "class", + "const", + "continue", + "default", + "deferred", + "do", + "dynamic", + "else", + "enum", + "export", + "extends", + "extension", + "external", + "factory", + "false", + "final", + "finally", + "for", + "Function", + "get", + "hashCode", + "hide", + "if", + "implements", + "import", + "in", + "index", + "interface", + "is", + "late", + "library", + "mixin", + "name", + "new", + "noSuchMethod", + "null", + "operator", + "part", + "required", + "rethrow", + "return", + "runtimeType", + "set", + "show", + "static", + "super", + "switch", + "this", + "throw", + "toString", + "true", + "try", + "typedef", + "var", + "void", + "while", + "with", + "yield", + "values", + } + + def enum_member_name(name: str) -> str: + candidate = camel_case(name) + candidate = re.sub(r'[^0-9A-Za-z_]', '_', candidate) + if not candidate: + candidate = 'unknown' + if candidate[0].isdigit(): + candidate = f'v{candidate}' + if candidate in reserved_enum_members: + candidate = f"{candidate}Value" + return candidate + + def make_safe_field_name(original_name: str, used_names: set[str]) -> str: + candidate = camel_case(original_name) + if candidate in reserved_field_names: + candidate = candidate + "Data" + while candidate in used_names: + candidate = candidate + "_" + used_names.add(candidate) + return candidate + + primitive_rust_types = { + "bool", + "char", + "u8", + "u16", + "u32", + "u64", + "usize", + "i8", + "i16", + "i32", + "i64", + "isize", + "f32", + "f64", + "String", + "str", + } + + external_type_map: dict[str, ExternalDef] = { + "BigDecimal": ExternalDef( + rust_name="BigDecimal", + dart_name="BigDecimal", + kind="wrapper", + base_type="String", + ), + "BigUint": ExternalDef( + rust_name="BigUint", + dart_name="BigUint", + kind="wrapper", + base_type="String", + ), + "U256": ExternalDef( + rust_name="U256", dart_name="U256", kind="wrapper", base_type="String" + ), + "PathBuf": ExternalDef( + rust_name="PathBuf", + dart_name="PathBuf", + kind="wrapper", + base_type="String", + ), + "H256Json": ExternalDef( + rust_name="H256Json", + dart_name="H256Json", + kind="wrapper", + base_type="String", + ), + "H160Json": ExternalDef( + rust_name="H160Json", + dart_name="H160Json", + kind="wrapper", + base_type="String", + ), + "Duration": ExternalDef( + rust_name="Duration", + dart_name="Mm2Duration", + kind="duration", + base_type="Duration", + ), + "Json": ExternalDef( + rust_name="Json", + dart_name="JsonValue", + kind="json", + base_type="dynamic", + ), + "serde_json::Value": ExternalDef( + rust_name="serde_json::Value", + dart_name="JsonValue", + kind="json", + base_type="dynamic", + ), + "serde_json::value::Value": ExternalDef( + rust_name="serde_json::value::Value", + dart_name="JsonValue", + kind="json", + base_type="dynamic", + ), + } + + def strip_wrappers(type_str: str) -> str: + type_str = type_str.strip() + if type_str.startswith("&"): + type_str = type_str.lstrip("&").strip() + wrapper_prefixes = [ + "Option<", + "Vec<", + "VecDeque<", + "HashMap<", + "BTreeMap<", + "HashSet<", + "BTreeSet<", + "MmRpcResult<", + ] + for prefix in wrapper_prefixes: + if type_str.startswith(prefix) and type_str.endswith(">"): + return strip_wrappers(type_str[len(prefix) : -1]) + return type_str + + def base_rust_name(type_str: str) -> str: + return strip_wrappers(type_str) + + error_enum_names = {enum["enum_name"] for enum in enums} + error_type_enum_names = { + f"{sanitize_type_name(name)}Type" for name in error_enum_names + } + + # Build referenced types from error enums + referenced_types: set[str] = set() + type_references: dict[str, set[str]] = {} + for enum in enums: + for variant in enum["variants"]: + for field in variant["fields"]: + base = base_rust_name(field["type"]) + referenced_types.add(base) + type_references.setdefault(base, set()).add(enum.get("file", "")) + + def preferred_segments_for_type(rust_type: str) -> Optional[list[str]]: + refs = type_references.get(rust_type) + if not refs: + return None + # Use the first reference as a heuristic for file location + ref = sorted(refs)[0] + parts = Path(ref).parts + return list(parts) + + # Index Rust files for type discovery + mm2src = mm2_repo_root / "mm2src" + skip_dirs = {"mm2_test_helpers", "adex_cli", "target", "tests"} + rust_files: list[Path] = [] + for path in mm2src.rglob("*.rs"): + if any(part in skip_dirs for part in path.parts): + continue + rust_files.append(path) + + rust_to_dart: dict[str, str] = {} + enum_defs: dict[str, EnumDef] = {} + struct_defs: dict[str, StructDef] = {} + external_defs: dict[str, ExternalDef] = {} + alias_defs: dict[str, str] = {} + + # Register error enums and externals + for name in error_enum_names: + rust_to_dart[name] = sanitize_type_name(name) + for key, ext in external_type_map.items(): + rust_to_dart[key] = ext.dart_name + external_defs[ext.dart_name] = ext + + def register_type(rust_type: str) -> None: + rust_type = rust_type.strip() + if not rust_type or rust_type in primitive_rust_types: + return + if rust_type == "Uuid": + rust_to_dart[rust_type] = "String" + return + if rust_type in rust_to_dart: + return + dart_name = sanitize_type_name(rust_type) if "::" in rust_type else rust_type + rust_to_dart[rust_type] = dart_name + + if rust_type in error_enum_names: + return + if rust_type in external_type_map: + external_defs[dart_name] = external_type_map[rust_type] + return + + rust_base = rust_type.split("::")[-1] + defn = find_type_definition( + rust_base, + dart_name, + rust_files, + preferred_segments_for_type(rust_type), + ) + if defn is None: + alias_defs[dart_name] = "Map" + return + + if isinstance(defn, AliasDef): + # Resolve aliases to primitives as wrapper classes + target = defn.target_type.strip() + dart_target = None + if target in primitive_rust_types: + dart_target = "int" if target not in ["String", "str", "bool"] else ("String" if target in ["String", "str"] else "bool") + if target in ["f32", "f64"]: + dart_target = "double" + if dart_target is None: + register_type(target) + dart_target = rust_to_dart.get(target, "dynamic") + external_defs[dart_name] = ExternalDef( + rust_name=defn.rust_name, + dart_name=defn.dart_name, + kind="alias", + base_type=dart_target, + ) + return + + if isinstance(defn, StructDef): + struct_defs[dart_name] = defn + for field in defn.fields: + register_type(base_rust_name(field.rust_type)) + return + + if isinstance(defn, EnumDef): + enum_defs[dart_name] = defn + for variant in defn.variants: + for field in variant.fields: + register_type(base_rust_name(field.rust_type)) + return + + for rust_type in referenced_types: + register_type(rust_type) + + def convert_type(type_str: str) -> dict[str, str]: + type_str = type_str.strip() + if type_str.startswith("&"): + return convert_type(type_str.lstrip("&").strip()) + if type_str.startswith("Option<") and type_str.endswith(">"): + inner = convert_type(type_str[7:-1]) + dart_t = inner["dart_type"] + if not dart_t.endswith("?"): + dart_t = f"{dart_t}?" + return {**inner, "dart_type": dart_t} + if type_str.startswith("Vec<") and type_str.endswith(">"): + inner = convert_type(type_str[4:-1]) + return {**inner, "dart_type": f"List<{inner['dart_type']}>"} + if type_str.startswith("VecDeque<") and type_str.endswith(">"): + inner = convert_type(type_str[9:-1]) + return {**inner, "dart_type": f"List<{inner['dart_type']}>"} + if type_str.startswith("HashMap<") and type_str.endswith(">"): + key, value = split_generic(type_str[8:-1]) + key_type = convert_type(key) + value_type = convert_type(value) + return { + **value_type, + "dart_type": f"Map<{key_type['dart_type']}, {value_type['dart_type']}>", + } + if type_str.startswith("BTreeMap<") and type_str.endswith(">"): + key, value = split_generic(type_str[9:-1]) + key_type = convert_type(key) + value_type = convert_type(value) + return { + **value_type, + "dart_type": f"Map<{key_type['dart_type']}, {value_type['dart_type']}>", + } + if type_str.startswith("BTreeSet<") and type_str.endswith(">"): + inner = convert_type(type_str[9:-1]) + return {**inner, "dart_type": f"List<{inner['dart_type']}>"} + if type_str.startswith("HashSet<") and type_str.endswith(">"): + inner = convert_type(type_str[8:-1]) + return {**inner, "dart_type": f"List<{inner['dart_type']}>"} + + if type_str in primitive_rust_types: + if type_str in ["String", "str"]: + return {"dart_type": "String"} + if type_str in ["bool"]: + return {"dart_type": "bool"} + if type_str in ["f32", "f64"]: + return {"dart_type": "double"} + return {"dart_type": "int"} + + if type_str in ["MmNumber", "Number"]: + return {"dart_type": "String"} + + if type_str == "Uuid": + return {"dart_type": "String"} + + if type_str.startswith("MmRpcResult<") and type_str.endswith(">"): + inner = convert_type(type_str[12:-1]) + return {**inner, "dart_type": inner["dart_type"]} + + if type_str == "RpcError": + return {"dart_type": "String"} + + if type_str == "UserAction": + return {"dart_type": "dynamic"} + + if type_str in ["PathBuf", "Path"]: + dart_name = rust_to_dart.get(type_str, "PathBuf") + return {"dart_type": dart_name} + + if type_str in ["serde_json::Value", "Json", "serde_json::value::Value"]: + dart_name = rust_to_dart.get(type_str, "JsonValue") + return {"dart_type": dart_name} + + if "::" in type_str: + clean = sanitize_type_name(type_str) + return {"dart_type": rust_to_dart.get(type_str, clean)} + + return {"dart_type": rust_to_dart.get(type_str, type_str)} + + custom_types: set[str] = set() + custom_types.update(enum_defs.keys()) + custom_types.update(struct_defs.keys()) + custom_types.update(external_defs.keys()) + custom_types.update(sanitize_type_name(name) for name in error_enum_names) + custom_types.update(error_type_enum_names) + + def is_custom_type(dart_type: str) -> bool: + return dart_type in custom_types + + def dart_value_from_json(dart_type: str, json_expr: str) -> str: + if dart_type.endswith("?"): + inner = dart_type[:-1] + inner_expr = dart_value_from_json(inner, json_expr) + return f"{json_expr} == null ? null : {inner_expr}" + if dart_type.startswith("List<") and dart_type.endswith(">"): + inner = dart_type[5:-1] + if is_custom_type(inner): + return f"({json_expr} as List).map((e) => {inner}.fromJson(e)).toList()" + return f"List<{inner}>.from({json_expr} as List)" + if dart_type.startswith("Map<"): + return f"{json_expr} as {dart_type}" + if is_custom_type(dart_type): + return f"{dart_type}.fromJson({json_expr})" + if dart_type == "int": + return f"_intFromJson({json_expr})" + if dart_type == "double": + return f"_doubleFromJson({json_expr})" + if dart_type == "String": + return f"_stringFromJson({json_expr})" + if dart_type == "bool": + return f"{json_expr} as bool" + return f"{json_expr} as {dart_type}" + + def dart_value_to_json(dart_type: str, value_expr: str) -> str: + if dart_type.endswith("?"): + inner = dart_type[:-1] + inner_expr = dart_value_to_json(inner, f"{value_expr}!") + return f"{value_expr} == null ? null : {inner_expr}" + if dart_type.startswith("List<") and dart_type.endswith(">"): + inner = dart_type[5:-1] + if is_custom_type(inner): + return f"{value_expr}.map((e) => e.toJson()).toList()" + return value_expr + if dart_type.startswith("Map<"): + return value_expr + if is_custom_type(dart_type): + return f"{value_expr}.toJson()" + return value_expr + + # Extract unit variants from a Rust enum declaration within a source file. + def extract_unit_variants_from_rust_enum(file_path: str, enum_name: str) -> list[str]: + try: + with open(file_path, "r", encoding="utf-8") as rf: + source = rf.read() + except FileNotFoundError: + return [] + + start = source.find(f"pub enum {enum_name}") + if start == -1: + return [] + brace_open = source.find("{", start) + if brace_open == -1: + return [] + depth = 1 + i = brace_open + 1 + body_chars: list[str] = [] + while i < len(source) and depth > 0: + ch = source[i] + if ch == "{": + depth += 1 + elif ch == "}": + depth -= 1 + if depth > 0: + body_chars.append(ch) + i += 1 + body = "".join(body_chars) + + unit_variants: list[str] = [] + for raw_line in body.splitlines(): + line = raw_line.strip() + if not line or line.startswith("#"): + continue + if "(" in line or "{" in line: + continue + m_name = re.match(r"^([A-Za-z_][A-Za-z0-9_]*)\s*,?$", line) + if m_name: + unit_variants.append(m_name.group(1)) + + return unit_variants + + # Heuristic: find stringified error parameter types inside `impl ` blocks + # that construct variants with `error: .to_string()`. + def find_stringified_error_param_types(enum_name: str, file_path: str) -> set[str]: + try: + with open(file_path, "r", encoding="utf-8") as f: + content = f.read() + except FileNotFoundError: + return set() + + param_types: set[str] = set() + + for m_err in re.finditer( + r"error\s*:\s*([A-Za-z_]\w*)\.to_string\(\)", content + ): + ident = m_err.group(1) + fn_start = content.rfind("fn ", 0, m_err.start()) + if fn_start == -1: + continue + params_open = content.find("(", fn_start) + if params_open == -1 or params_open > m_err.start(): + continue + depth = 1 + i = params_open + 1 + while i < len(content) and depth > 0: + ch = content[i] + if ch == "(": + depth += 1 + elif ch == ")": + depth -= 1 + i += 1 + params_close = i - 1 if depth == 0 else -1 + if params_close == -1: + continue + params_src = content[params_open + 1 : params_close] + m_param = re.search( + rf"\b{re.escape(ident)}\s*:\s*([A-Za-z_][A-Za-z0-9_:]*)", + params_src, + ) + if m_param: + param_types.add(m_param.group(1)) + + return param_types + + def find_enum_file(enum_name: str) -> Optional[str]: + for path in rust_files: + try: + txt = path.read_text(encoding="utf-8") + except Exception: + continue + if f"pub enum {enum_name}" in txt: + return str(path) + return None + + lines: list[str] = [] + lines.append("// GENERATED CODE - DO NOT MODIFY BY HAND.") + lines.append("// Generated by tool/generate_mm2_rpc_exceptions.py.") + lines.append("// ignore_for_file: public_member_api_docs, constant_identifier_names") + lines.append("") + lines.append("library mm2_rpc_exceptions;") + lines.append("") + lines.append("import 'package:komodo_defi_types/komodo_defi_type_utils.dart';") + lines.append("") + lines.append("int _intFromJson(dynamic value) {") + lines.append(" if (value is int) return value;") + lines.append(" if (value is num) return value.toInt();") + lines.append(" if (value is String) return int.tryParse(value) ?? 0;") + lines.append(" return 0;") + lines.append("}") + lines.append("") + lines.append("double _doubleFromJson(dynamic value) {") + lines.append(" if (value is double) return value;") + lines.append(" if (value is num) return value.toDouble();") + lines.append(" if (value is String) return double.tryParse(value) ?? 0;") + lines.append(" return 0;") + lines.append("}") + lines.append("") + lines.append("String _stringFromJson(dynamic value) {") + lines.append(" if (value == null) {") + lines.append(" return '';") + lines.append(" }") + lines.append(" return value.toString();") + lines.append("}") + lines.append("") + lines.append("JsonMap _asJsonMap(dynamic json) {") + lines.append(" if (json is JsonMap) {") + lines.append(" return json;") + lines.append(" }") + lines.append(" if (json is Map) {") + lines.append(" try {") + lines.append(" return convertToJsonMap(json);") + lines.append(" } catch (_) {") + lines.append(" return json.map(") + lines.append(" (key, value) => MapEntry(key.toString(), value),") + lines.append(" );") + lines.append(" }") + lines.append(" }") + lines.append(" return {};") + lines.append("}") + lines.append("") + lines.append("List _asJsonList(dynamic json) {") + lines.append(" if (json is List) {") + lines.append(" return List.from(json);") + lines.append(" }") + lines.append(" return [];") + lines.append("}") + lines.append("") + + # External / alias wrappers + for ext in sorted(external_defs.values(), key=lambda e: e.dart_name): + if ext.kind == "duration": + lines.append(f"final class {ext.dart_name} {{") + lines.append(f" const {ext.dart_name}(this.value);") + lines.append(f"") + lines.append(f" factory {ext.dart_name}.fromJson(dynamic json) {{") + lines.append(" if (json is Map) {") + lines.append(" final secs = json['secs'] ?? json['seconds'] ?? 0;") + lines.append(" final nanos = json['nanos'] ?? 0;") + lines.append(" final seconds = _intFromJson(secs);") + lines.append(" final nanoseconds = _intFromJson(nanos);") + lines.append( + " return Mm2Duration(Duration(microseconds: seconds * 1000000 + (nanoseconds ~/ 1000)));" + ) + lines.append(" }") + lines.append(" if (json is int || json is num || json is String) {") + lines.append(" return Mm2Duration(Duration(seconds: _intFromJson(json)));") + lines.append(" }") + lines.append(" return const Mm2Duration(Duration.zero);") + lines.append(" }") + lines.append("") + lines.append(" final Duration value;") + lines.append("") + lines.append(" Map toJson() {") + lines.append(" final seconds = value.inSeconds;") + lines.append( + " final nanoseconds = (value.inMicroseconds - seconds * 1000000) * 1000;" + ) + lines.append(" return {'secs': seconds, 'nanos': nanoseconds};") + lines.append(" }") + lines.append("}") + lines.append("") + continue + + if ext.kind == "json": + lines.append(f"final class {ext.dart_name} {{") + lines.append(f" const {ext.dart_name}(this.value);") + lines.append("") + lines.append(f" factory {ext.dart_name}.fromJson(dynamic json) {{") + lines.append(f" return {ext.dart_name}(json);") + lines.append(" }") + lines.append("") + lines.append(" final dynamic value;") + lines.append("") + lines.append(" dynamic toJson() => value;") + lines.append("}") + lines.append("") + continue + + lines.append(f"final class {ext.dart_name} {{") + lines.append(f" const {ext.dart_name}(this.value);") + lines.append("") + lines.append(f" factory {ext.dart_name}.fromJson(dynamic json) {{") + from_expr = dart_value_from_json(ext.base_type, "json") + lines.append(f" return {ext.dart_name}({from_expr});") + lines.append(" }") + lines.append("") + lines.append(f" final {ext.base_type} value;") + lines.append("") + to_expr = dart_value_to_json(ext.base_type, "value") + lines.append(f" dynamic toJson() => {to_expr};") + lines.append("}") + lines.append("") + + # Struct models + for struct in sorted(struct_defs.values(), key=lambda s: s.dart_name): + class_name = struct.dart_name + lines.append(f"final class {class_name} {{") + if struct.kind == "tuple": + tuple_fields: list[tuple[str, str]] = [] + for idx, field in enumerate(struct.fields): + dart_t = convert_type(field.rust_type)["dart_type"] + name = "value" if len(struct.fields) == 1 else f"value{idx}" + tuple_fields.append((name, dart_t)) + params = ", ".join([f"this.{name}" for name, _ in tuple_fields]) + lines.append(f" const {class_name}({params});") + lines.append("") + lines.append(f" factory {class_name}.fromJson(dynamic json) {{") + if len(tuple_fields) == 1: + name, dart_t = tuple_fields[0] + expr = dart_value_from_json(dart_t, "json") + lines.append(f" return {class_name}({expr});") + else: + lines.append(" final list = _asJsonList(json);") + args = ", ".join( + dart_value_from_json(dart_t, f"list[{idx}]") + for idx, (_name, dart_t) in enumerate(tuple_fields) + ) + lines.append(f" return {class_name}({args});") + lines.append(" }") + lines.append("") + for name, dart_t in tuple_fields: + lines.append(f" final {dart_t} {name};") + lines.append("") + lines.append(" dynamic toJson() {") + if len(tuple_fields) == 1: + expr = dart_value_to_json(tuple_fields[0][1], tuple_fields[0][0]) + lines.append(f" return {expr};") + else: + expr_list = ", ".join( + dart_value_to_json(t, n) for n, t in tuple_fields + ) + lines.append(f" return [{expr_list}];") + lines.append(" }") + lines.append("}") + lines.append("") + continue + + if struct.kind == "unit": + lines.append(f" const {class_name}();") + lines.append("") + lines.append(f" factory {class_name}.fromJson() => const {class_name}();") + lines.append("") + lines.append(" dynamic toJson() => null;") + lines.append("}") + lines.append("") + continue + + required_fields: list[tuple[FieldDef, str]] = [] + optional_fields: list[tuple[FieldDef, str]] = [] + for field in struct.fields: + dart_t = convert_type(field.rust_type)["dart_type"] + if dart_t.endswith("?"): + optional_fields.append((field, dart_t)) + else: + required_fields.append((field, dart_t)) + ctor_parts = [f"required this.{f.name}" for f, _ in required_fields] + [ + f"this.{f.name}" for f, _ in optional_fields + ] + ctor_params = ", ".join(ctor_parts) + lines.append(f" const {class_name}({{{ctor_params}}});") + lines.append("") + lines.append(f" factory {class_name}.fromJson(JsonMap json) {{") + lines.append(f" return {class_name}(") + for field, dart_t in required_fields + optional_fields: + json_key = field.json_name or field.name + accessor = ( + f"json.value('{json_key}')" + if not dart_t.endswith("?") + else f"json.valueOrNull('{json_key}')" + ) + expr = dart_value_from_json(dart_t, accessor) + lines.append(f" {field.name}: {expr},") + lines.append(" );") + lines.append(" }") + lines.append("") + for field, dart_t in required_fields + optional_fields: + lines.append(f" final {dart_t} {field.name};") + lines.append("") + lines.append(" JsonMap toJson() => {") + for field, dart_t in required_fields + optional_fields: + json_key = field.json_name or field.name + expr = dart_value_to_json(dart_t, field.name) + lines.append(f" '{json_key}': {expr},") + lines.append(" };") + lines.append("}") + lines.append("") + + # Enum models (non-error enums) + for enum_def in sorted(enum_defs.values(), key=lambda e: e.dart_name): + def is_unit_only(enum_def): + return all(v.kind == 'unit' and not v.fields for v in enum_def.variants) + + if is_unit_only(enum_def) and not enum_def.serde.tag and not enum_def.serde.content: + rename_all = enum_def.serde.rename_all + enum_name = enum_def.dart_name + variants = [] + json_to_member = [] + for variant in enum_def.variants: + json_name = variant.serde_rename or apply_rename_all(variant.name, rename_all) + member_name = enum_member_name(variant.name) + variants.append(member_name) + json_to_member.append((json_name, member_name)) + + if 'unknown' not in variants: + variants.append('unknown') + + lines.append(f"enum {enum_name} {{") + lines.append(f" {', '.join(variants)};" ) + lines.append("") + lines.append(f" static {enum_name} fromJson(dynamic json) {{") + lines.append(" String? value;") + lines.append(" if (json is Map && json.isNotEmpty) {") + lines.append(" value = json.keys.first.toString();") + lines.append(" } else {") + lines.append(" value = _stringFromJson(json);") + lines.append(" }") + lines.append(" switch (value) {") + for json_name, member_name in json_to_member: + lines.append(f" case '{json_name}':") + lines.append(f" return {enum_name}.{member_name};") + lines.append(" default:") + lines.append(f" return {enum_name}.unknown;") + lines.append(" }") + lines.append(" }") + lines.append("") + lines.append(" String toJson() {") + lines.append(" switch (this) {") + for json_name, member_name in json_to_member: + lines.append(f" case {enum_name}.{member_name}:") + lines.append(f" return '{json_name}';") + lines.append(f" case {enum_name}.unknown:") + lines.append(" return 'unknown';") + lines.append(" }") + lines.append(" }") + lines.append("}") + lines.append("") + continue + + + class_name = enum_def.dart_name + tag = enum_def.serde.tag + content = enum_def.serde.content + rename_all = enum_def.serde.rename_all + + lines.append(f"sealed class {class_name} {{") + lines.append(f" const {class_name}({{required this.type}});") + lines.append("") + lines.append(" final String type;") + lines.append("") + lines.append(f" factory {class_name}.fromJson(dynamic json) {{") + if tag and content: + lines.append(" final map = _asJsonMap(json);") + lines.append(f" final type = map.valueOrNull('{tag}');") + lines.append(f" final data = map.valueOrNull('{content}');") + lines.append(" switch (type) {") + for variant in enum_def.variants: + variant_name = variant.serde_rename or apply_rename_all( + variant.name, rename_all + ) + variant_class = f"{class_name}{pascal_case(variant.name)}" + lines.append(f" case '{variant_name}':") + call = f"{variant_class}.fromJson(data)" + if variant.kind == "unit": + call = f"{variant_class}.fromJson()" + lines.append(f" return {call};") + lines.append(" }") + lines.append(f" return {class_name}Unknown(type: type, data: data);") + elif tag: + lines.append(" final map = _asJsonMap(json);") + lines.append(f" final type = map.valueOrNull('{tag}');") + lines.append(" switch (type) {") + for variant in enum_def.variants: + variant_name = variant.serde_rename or apply_rename_all( + variant.name, rename_all + ) + variant_class = f"{class_name}{pascal_case(variant.name)}" + lines.append(f" case '{variant_name}':") + call = f"{variant_class}.fromJson(map)" + if variant.kind == "unit": + call = f"{variant_class}.fromJson()" + lines.append(f" return {call};") + lines.append(" }") + lines.append(f" return {class_name}Unknown(type: type, data: map);") + else: + lines.append(" if (json is String) {") + lines.append(" switch (json) {") + for variant in enum_def.variants: + variant_name = variant.serde_rename or apply_rename_all( + variant.name, rename_all + ) + variant_class = f"{class_name}{pascal_case(variant.name)}" + lines.append(f" case '{variant_name}':") + call = f"{variant_class}.fromJson(null)" + if variant.kind == "unit": + call = f"{variant_class}.fromJson()" + lines.append(f" return {call};") + lines.append(" }") + lines.append(f" return {class_name}Unknown(type: json, data: json);") + lines.append(" }") + lines.append(" if (json is Map && json.isNotEmpty) {") + lines.append(" final entry = json.entries.first;") + lines.append(" switch (entry.key) {") + for variant in enum_def.variants: + variant_name = variant.serde_rename or apply_rename_all( + variant.name, rename_all + ) + variant_class = f"{class_name}{pascal_case(variant.name)}" + lines.append(f" case '{variant_name}':") + call = f"{variant_class}.fromJson(entry.value)" + if variant.kind == "unit": + call = f"{variant_class}.fromJson()" + lines.append(f" return {call};") + lines.append(" }") + lines.append( + f" return {class_name}Unknown(type: entry.key, data: entry.value);" + ) + lines.append(" }") + lines.append(f" return {class_name}Unknown(type: null, data: json);") + lines.append(" }") + lines.append("") + lines.append(" JsonMap toJson();") + lines.append("}") + lines.append("") + + # Variant classes + for variant in enum_def.variants: + variant_class = f"{class_name}{pascal_case(variant.name)}" + variant_name = variant.serde_rename or apply_rename_all( + variant.name, rename_all + ) + fields = variant.fields + lines.append(f"final class {variant_class} extends {class_name} {{") + if variant.kind == "struct": + required_fields = [] + optional_fields = [] + for field in fields: + dart_t = convert_type(field.rust_type)["dart_type"] + if dart_t.endswith("?"): + optional_fields.append((field, dart_t)) + else: + required_fields.append((field, dart_t)) + ctor_parts = [f"required this.{f.name}" for f, _ in required_fields] + [ + f"this.{f.name}" for f, _ in optional_fields + ] + ctor_params = ", ".join(ctor_parts) + lines.append( + f" const {variant_class}({{{ctor_params}}}) : super(type: '{variant_name}');" + ) + lines.append("") + lines.append(f" factory {variant_class}.fromJson(dynamic json) {{") + lines.append(" final map = _asJsonMap(json);") + lines.append(f" return {variant_class}(") + for field, dart_t in required_fields + optional_fields: + json_key = field.json_name or field.name + accessor = ( + f"map.valueOrNull('{json_key}')" + if dart_t.endswith("?") + else f"map.value('{json_key}')" + ) + expr = dart_value_from_json(dart_t, accessor) + lines.append(f" {field.name}: {expr},") + lines.append(" );") + lines.append(" }") + lines.append("") + for field, dart_t in required_fields + optional_fields: + lines.append(f" final {dart_t} {field.name};") + lines.append("") + lines.append(" @override") + lines.append(" JsonMap toJson() => {") + if tag and content: + lines.append(f" '{tag}': '{variant_name}',") + lines.append(f" '{content}': {{") + for field, dart_t in required_fields + optional_fields: + json_key = field.json_name or field.name + expr = dart_value_to_json(dart_t, field.name) + lines.append(f" '{json_key}': {expr},") + lines.append(" },") + elif tag: + lines.append(f" '{tag}': '{variant_name}',") + for field, dart_t in required_fields + optional_fields: + json_key = field.json_name or field.name + expr = dart_value_to_json(dart_t, field.name) + lines.append(f" '{json_key}': {expr},") + else: + lines.append(f" '{variant_name}': {{") + for field, dart_t in required_fields + optional_fields: + json_key = field.json_name or field.name + expr = dart_value_to_json(dart_t, field.name) + lines.append(f" '{json_key}': {expr},") + lines.append(" },") + lines.append(" };") + lines.append("}") + lines.append("") + elif variant.kind == "tuple": + tuple_fields: list[tuple[str, str]] = [] + for idx, field in enumerate(fields): + dart_t = convert_type(field.rust_type)["dart_type"] + tuple_fields.append( + (f"value{idx}" if len(fields) > 1 else "value", dart_t) + ) + params = ", ".join([f"this.{name}" for name, _ in tuple_fields]) + lines.append( + f" const {variant_class}({params}) : super(type: '{variant_name}');" + ) + lines.append("") + lines.append(f" factory {variant_class}.fromJson(dynamic json) {{") + if len(fields) == 1: + name, dart_t = tuple_fields[0] + expr = dart_value_from_json(dart_t, "json") + lines.append(f" return {variant_class}({expr});") + else: + lines.append(" final list = _asJsonList(json);") + args = ", ".join( + dart_value_from_json(dart_t, f"list[{idx}]") + for idx, (_name, dart_t) in enumerate(tuple_fields) + ) + lines.append(f" return {variant_class}({args});") + lines.append(" }") + lines.append("") + for name, dart_t in tuple_fields: + lines.append(f" final {dart_t} {name};") + lines.append("") + lines.append(" @override") + lines.append(" JsonMap toJson() => {") + if tag and content: + lines.append(f" '{tag}': '{variant_name}',") + if len(fields) == 1: + expr = dart_value_to_json( + tuple_fields[0][1], tuple_fields[0][0] + ) + lines.append(f" '{content}': {expr},") + else: + expr_list = ", ".join( + dart_value_to_json(t, n) for n, t in tuple_fields + ) + lines.append(f" '{content}': [{expr_list}],") + elif tag: + lines.append(f" '{tag}': '{variant_name}',") + else: + if len(fields) == 1: + expr = dart_value_to_json( + tuple_fields[0][1], tuple_fields[0][0] + ) + lines.append(f" '{variant_name}': {expr},") + else: + expr_list = ", ".join( + dart_value_to_json(t, n) for n, t in tuple_fields + ) + lines.append(f" '{variant_name}': [{expr_list}],") + lines.append(" };") + lines.append("}") + lines.append("") + else: + lines.append( + f" const {variant_class}() : super(type: '{variant_name}');" + ) + lines.append("") + lines.append( + f" factory {variant_class}.fromJson() => const {variant_class}();" + ) + lines.append("") + lines.append(" @override") + lines.append(" JsonMap toJson() => {") + if tag: + lines.append(f" '{tag}': '{variant_name}',") + if content: + lines.append(f" '{content}': null,") + else: + lines.append(f" '{variant_name}': null,") + lines.append(" };") + lines.append("}") + lines.append("") + + # Unknown variant + lines.append(f"final class {class_name}Unknown extends {class_name} {{") + lines.append( + f" const {class_name}Unknown({{required String? type, required this.data}})" + f" : super(type: type ?? 'unknown');" + ) + lines.append("") + lines.append(" final dynamic data;") + lines.append("") + lines.append(" @override") + lines.append(" JsonMap toJson() => {") + if tag and content: + lines.append(f" '{tag}': type,") + lines.append(f" '{content}': data,") + elif tag: + lines.append(f" '{tag}': type,") + lines.append(" 'data': data,") + else: + lines.append(" 'type': type,") + lines.append(" 'data': data,") + lines.append(" };") + lines.append("}") + lines.append("") + + # Error enum data models (string-backed discriminators) + for enum in enums: + enum_name = sanitize_type_name(enum["enum_name"]) + lines.append(f"sealed class {enum_name} {{") + lines.append(f" const {enum_name}({{required this.errorType}});") + lines.append("") + lines.append(" final String errorType;") + lines.append("") + lines.append(f" factory {enum_name}.fromJson(dynamic json) {{") + lines.append(" final map = _asJsonMap(json);") + lines.append(" final type = map.valueOrNull('error_type');") + lines.append(" final data = map.valueOrNull('error_data');") + lines.append(" switch (type) {") + for variant in enum["variants"]: + if variant.get("serde_other"): + continue + variant_class = f"{enum_name}{pascal_case(variant['name'])}" + variant_type = variant.get("serde_rename") or variant["name"] + lines.append(f" case '{variant_type}':") + if not variant["fields"]: + lines.append(f" return {variant_class}.fromJson();") + else: + lines.append(f" return {variant_class}.fromJson(data);") + lines.append(" default:") + lines.append( + f" return {enum_name}Unknown(rawErrorType: type, data: data);" + ) + lines.append(" }") + lines.append(" }") + lines.append("") + lines.append(" JsonMap toJson();") + lines.append("}") + lines.append("") + + for variant in enum["variants"]: + variant_type = variant.get("serde_rename") or variant["name"] + if variant.get("serde_other"): + variant_type = "unknown" + variant_class = f"{enum_name}{pascal_case(variant['name'])}" + fields = variant["fields"] + is_struct = bool(fields) and not all(field["name"].isdigit() for field in fields) + is_tuple = bool(fields) and all(field["name"].isdigit() for field in fields) + + lines.append(f"final class {variant_class} extends {enum_name} {{") + if is_struct: + field_defs: list[tuple[str, str, str]] = [] + used_param_names: set[str] = set(reserved_field_names) + for field in fields: + field_type = convert_type(field["type"])["dart_type"] + json_key = field.get("serde_rename") or field["name"] + field_name = make_safe_field_name(field["name"], used_param_names) + field_defs.append((field_name, field_type, json_key)) + + required_params: list[str] = [] + optional_params: list[str] = [] + for fname, ftype, _json_key in field_defs: + if ftype.endswith("?"): + optional_params.append(f"this.{fname}") + else: + required_params.append(f"required this.{fname}") + params_parts = required_params + optional_params + params = ", ".join(params_parts) + lines.append( + f" const {variant_class}({{{params}}}) : super(errorType: '{variant_type}');" + ) + lines.append("") + lines.append(f" factory {variant_class}.fromJson(dynamic json) {{") + lines.append(" final map = _asJsonMap(json);") + lines.append(f" return {variant_class}(") + for fname, ftype, json_key in field_defs: + accessor = ( + f"map.valueOrNull('{json_key}')" + if ftype.endswith("?") + else f"map.value('{json_key}')" + ) + expr = dart_value_from_json(ftype, accessor) + lines.append(f" {fname}: {expr},") + lines.append(" );") + lines.append(" }") + lines.append("") + for fname, ftype, _json_key in field_defs: + lines.append(f" final {ftype} {fname};") + lines.append("") + lines.append(" @override") + lines.append(" JsonMap toJson() => {") + lines.append(" 'error_type': errorType,") + lines.append(" 'error_data': {") + for fname, ftype, json_key in field_defs: + expr = dart_value_to_json(ftype, fname) + lines.append(f" '{json_key}': {expr},") + lines.append(" },") + lines.append(" };") + elif is_tuple: + if len(fields) == 1: + field_type = convert_type(fields[0]["type"])["dart_type"] + lines.append( + f" const {variant_class}(this.value) : super(errorType: '{variant_type}');" + ) + lines.append("") + lines.append(f" factory {variant_class}.fromJson(dynamic json) {{") + expr = dart_value_from_json(field_type, "json") + lines.append(f" return {variant_class}({expr});") + lines.append(" }") + lines.append("") + lines.append(f" final {field_type} value;") + lines.append("") + lines.append(" @override") + lines.append(" JsonMap toJson() => {") + lines.append(" 'error_type': errorType,") + expr = dart_value_to_json(field_type, "value") + lines.append(f" 'error_data': {expr},") + lines.append(" };") + else: + params = ", ".join(f"this.value{idx}" for idx in range(len(fields))) + lines.append( + f" const {variant_class}({params}) : super(errorType: '{variant_type}');" + ) + lines.append("") + lines.append(f" factory {variant_class}.fromJson(dynamic json) {{") + lines.append(" final list = _asJsonList(json);") + args = ", ".join( + dart_value_from_json( + convert_type(field["type"])["dart_type"], f"list[{idx}]" + ) + for idx, field in enumerate(fields) + ) + lines.append(f" return {variant_class}({args});") + lines.append(" }") + lines.append("") + for idx, field in enumerate(fields): + field_type = convert_type(field["type"])["dart_type"] + lines.append(f" final {field_type} value{idx};") + lines.append("") + lines.append(" @override") + lines.append(" JsonMap toJson() => {") + lines.append(" 'error_type': errorType,") + expr_list = ", ".join( + dart_value_to_json( + convert_type(field["type"])["dart_type"], f"value{idx}" + ) + for idx, field in enumerate(fields) + ) + lines.append(f" 'error_data': [{expr_list}],") + lines.append(" };") + else: + lines.append( + f" const {variant_class}() : super(errorType: '{variant_type}');" + ) + lines.append("") + lines.append( + f" factory {variant_class}.fromJson() => const {variant_class}();" + ) + lines.append("") + lines.append(" @override") + lines.append(" JsonMap toJson() => {") + lines.append(" 'error_type': errorType,") + lines.append(" 'error_data': null,") + lines.append(" };") + + lines.append("}") + lines.append("") + + lines.append(f"final class {enum_name}Unknown extends {enum_name} {{") + lines.append( + f" const {enum_name}Unknown({{required this.data, this.rawErrorType}})" + " : super(errorType: 'unknown');" + ) + lines.append("") + lines.append(" final String? rawErrorType;") + lines.append(" final dynamic data;") + lines.append("") + lines.append(" @override") + lines.append(" JsonMap toJson() => {") + lines.append(" 'error_type': rawErrorType ?? errorType,") + lines.append(" 'error_data': data,") + lines.append(" };") + lines.append("}") + lines.append("") + + # Alias typedefs for unresolved types + if alias_defs: + for alias, base in sorted(alias_defs.items()): + lines.append(f"typedef {alias} = {base};") + lines.append("") + + lines.append("sealed class MmRpcException implements Exception {") + lines.append(" const MmRpcException({") + lines.append(" required this.errorType,") + lines.append(" this.message,") + lines.append(" this.path,") + lines.append(" this.trace,") + lines.append(" });") + lines.append("") + lines.append(" final String errorType;") + lines.append(" final String? message;") + lines.append(" final String? path;") + lines.append(" final String? trace;") + lines.append("") + lines.append(" @override") + lines.append( + r" String toString() => 'MmRpcException(type: ' '\$errorType, message: \$message, path: \$path)';" + ) + lines.append("}") + lines.append("") + + for enum in enums: + enum_name = sanitize_type_name(enum["enum_name"]) + base_name = pascal_case(enum["enum_name"]) + "Exception" + lines.append(f"sealed class {base_name} extends MmRpcException {{") + lines.append( + f" const {base_name}({{required super.errorType, super.message, super.path, super.trace}});" + ) + lines.append("}") + lines.append("") + + for variant in enum["variants"]: + variant_type = variant.get("serde_rename") or variant["name"] + if variant.get("serde_other"): + variant_type = "unknown" + class_name = ( + f"{pascal_case(enum['enum_name'])}{pascal_case(variant['name'])}Exception" + ) + fields = variant["fields"] + is_struct = bool(fields) and not all( + field["name"].isdigit() for field in fields + ) + is_tuple = bool(fields) and all( + field["name"].isdigit() for field in fields + ) + + lines.append(f"final class {class_name} extends {base_name} {{") + + if is_struct: + field_defs: list[tuple[str, str]] = [] + used_param_names: set[str] = set(reserved_field_names) + for field in fields: + field_type = convert_type(field["type"])["dart_type"] + field_name = make_safe_field_name(field["name"], used_param_names) + field_defs.append((field_name, field_type)) + + required_params: list[str] = [] + optional_params: list[str] = [] + for fname, ftype in field_defs: + if ftype.endswith("?"): + optional_params.append(f"this.{fname}") + else: + required_params.append(f"required this.{fname}") + params_parts = required_params + optional_params + [ + "super.message", + "super.path", + "super.trace", + ] + params = ", ".join(params_parts) + lines.append( + f" const {class_name}({{{params}}}) : super(errorType: '{variant_type}');" + ) + + for fname, ftype in field_defs: + lines.append(f" final {ftype} {fname};") + elif is_tuple: + if len(fields) == 1: + field_type = convert_type(fields[0]["type"])["dart_type"] + lines.append( + f" const {class_name}(this.value, {{super.message, super.path, super.trace}})" + f" : super(errorType: '{variant_type}');" + ) + lines.append(f" final {field_type} value;") + else: + params = ", ".join(f"this.value{idx}" for idx in range(len(fields))) + lines.append( + f" const {class_name}({params}, {{super.message, super.path, super.trace}})" + f" : super(errorType: '{variant_type}');" + ) + for idx, field in enumerate(fields): + field_type = convert_type(field["type"])["dart_type"] + lines.append(f" final {field_type} value{idx};") + else: + lines.append( + f" const {class_name}({{super.message, super.path, super.trace}})" + f" : super(errorType: '{variant_type}');" + ) + + lines.append("}") + lines.append("") + + emitted_extra_classes: set[str] = set() + + # Generic support: for any RPC enum where a variant contains an `error: String` + # field and its impl converts a typed error to string, synthesize specific + # subclasses for unit variants of that typed error. + for enum in enums: + enum_name = sanitize_type_name(enum["enum_name"]) + stringified_mappings = enum.get("stringified_from") or [] + if stringified_mappings: + name_to_variant = {v["name"]: v for v in enum["variants"]} + + for mapping in stringified_mappings: + target_variant_name = mapping.get("target_variant") + source_type_name = mapping.get("source_type") + unit_variants = mapping.get("unit_variants") or [] + if not target_variant_name or target_variant_name not in name_to_variant: + continue + + target_variant = name_to_variant[target_variant_name] + if not any( + f["name"] == "error" and f["type"] == "String" + for f in target_variant["fields"] + ): + continue + + variant_type_name = ( + target_variant.get("serde_rename") or target_variant["name"] + ) + if target_variant.get("serde_other"): + variant_type_name = "unknown" + + for unit in unit_variants: + class_name = ( + f"{pascal_case(enum['enum_name'])}{pascal_case(unit)}Exception" + ) + if class_name in emitted_extra_classes: + continue + + lines.append( + f"// Auto-generated from {source_type_name} unit variant {unit} mapped through stringified error field" + ) + lines.append( + f"final class {class_name} extends {pascal_case(enum['enum_name']) + 'Exception'} {{" + ) + + field_defs: list[tuple[str, str]] = [] + ctor_required: list[str] = [] + ctor_optional: list[str] = [] + used_names: set[str] = set(reserved_field_names) + for fld in target_variant["fields"]: + if fld["name"] == "error": + continue + dart_t = convert_type(fld["type"])["dart_type"] + dart_n = make_safe_field_name(fld["name"], used_names) + field_defs.append((dart_n, dart_t)) + if dart_t.endswith("?"): + ctor_optional.append(f"this.{dart_n}") + else: + ctor_required.append(f"required this.{dart_n}") + + ctor_params = ", ".join( + ctor_required + + ctor_optional + + ["super.message", "super.path", "super.trace"] + ) + lines.append( + f" const {class_name}({{{ctor_params}}}) : super(errorType: '{variant_type_name}');" + ) + + for dart_n, dart_t in field_defs: + lines.append(f" final {dart_t} {dart_n};") + + lines.append("}") + lines.append("") + emitted_extra_classes.add(class_name) + continue + + file_path = os.path.join(mm2_repo_root, enum.get("file", "")) + stringified_param_types = find_stringified_error_param_types( + enum["enum_name"], file_path + ) + if not stringified_param_types: + continue + + variants_with_error_field = [] + for v in enum["variants"]: + if any( + f["name"] == "error" and f["type"] == "String" for f in v["fields"] + ): + variants_with_error_field.append(v) + + if not variants_with_error_field: + continue + + for rust_type_name in sorted(stringified_param_types): + enum_file = find_enum_file(rust_type_name) + if not enum_file: + continue + unit_variants = extract_unit_variants_from_rust_enum(enum_file, rust_type_name) + if not unit_variants: + continue + + for unit in unit_variants: + class_name = ( + f"{pascal_case(enum['enum_name'])}{pascal_case(unit)}Exception" + ) + if class_name in emitted_extra_classes: + continue + + target_variant = variants_with_error_field[0] + variant_type_name = ( + target_variant.get("serde_rename") or target_variant["name"] + ) + if target_variant.get("serde_other"): + variant_type_name = "unknown" + + lines.append( + f"// Auto-generated from {rust_type_name} unit variant {unit} mapped through stringified error field" + ) + lines.append( + f"final class {class_name} extends {pascal_case(enum['enum_name']) + 'Exception'} {{" + ) + + field_defs: list[tuple[str, str]] = [] + ctor_required: list[str] = [] + ctor_optional: list[str] = [] + used_names: set[str] = set(reserved_field_names) + for fld in variants_with_error_field[0]["fields"]: + if fld["name"] == "error": + continue + dart_t = convert_type(fld["type"])["dart_type"] + dart_n = make_safe_field_name(fld["name"], used_names) + field_defs.append((dart_n, dart_t)) + if dart_t.endswith("?"): + ctor_optional.append(f"this.{dart_n}") + else: + ctor_required.append(f"required this.{dart_n}") + + ctor_params = ", ".join( + ctor_required + + ctor_optional + + ["super.message", "super.path", "super.trace"] + ) + lines.append( + f" const {class_name}({{{ctor_params}}}) : super(errorType: '{variant_type_name}');" + ) + + for dart_n, dart_t in field_defs: + lines.append(f" final {dart_t} {dart_n};") + + lines.append("}") + lines.append("") + emitted_extra_classes.add(class_name) + + # Generate KdfErrorRegistry class for automatic error parsing + lines.append("/// Registry for parsing KDF RPC error responses into typed exceptions.") + lines.append("///") + lines.append("/// This class provides automatic conversion of error responses to typed") + lines.append("/// [MmRpcException] subclasses based on the `error_type` field.") + lines.append("abstract final class KdfErrorRegistry {") + lines.append(" KdfErrorRegistry._();") + lines.append("") + lines.append(" /// Attempts to parse a JSON error response into a typed [MmRpcException].") + lines.append(" ///") + lines.append(" /// Returns `null` if the error type is not recognized or if the JSON") + lines.append(" /// does not contain an `error_type` field.") + lines.append(" ///") + lines.append(" /// Example:") + lines.append(" /// ```dart") + lines.append(" /// final exception = KdfErrorRegistry.tryParse(errorJson);") + lines.append(" /// if (exception != null) {") + lines.append(" /// throw exception;") + lines.append(" /// }") + lines.append(" /// ```") + lines.append(" static MmRpcException? tryParse(JsonMap json) {") + lines.append(" final errorType = json['error_type'] as String?;") + lines.append(" if (errorType == null) return null;") + lines.append("") + lines.append(" final errorData = json['error_data'];") + lines.append(" final message = json['error'] as String? ?? json['message'] as String?;") + lines.append(" final path = json['error_path'] as String?;") + lines.append(" final trace = json['error_trace'] as String?;") + lines.append("") + lines.append(" final parser = _errorParsers[errorType];") + lines.append(" if (parser == null) return null;") + lines.append("") + lines.append(" try {") + lines.append(" return parser(errorData, message, path, trace);") + lines.append(" } catch (_) {") + lines.append(" // Malformed or unexpected error_data shape \u2014 fall back to null so") + lines.append(" // callers can degrade gracefully to GeneralErrorResponse.") + lines.append(" return null;") + lines.append(" }") + lines.append(" }") + lines.append("") + lines.append(" /// Checks if the given error type string is a known KDF error type.") + lines.append(" static bool isKnownErrorType(String errorType) {") + lines.append(" return _errorParsers.containsKey(errorType);") + lines.append(" }") + lines.append("") + lines.append(" /// Returns all known error type strings.") + lines.append(" static Iterable get knownErrorTypes => _errorParsers.keys;") + lines.append("") + + # Build the error parsers map + lines.append(" static final _errorParsers = (() {") + lines.append(" final parsers = {};") + + for enum in enums: + enum_name = sanitize_type_name(enum["enum_name"]) + for variant in enum["variants"]: + if variant.get("serde_other"): + continue + variant_type = variant.get("serde_rename") or variant["name"] + exception_class_name = f"{pascal_case(enum['enum_name'])}{pascal_case(variant['name'])}Exception" + fields = variant["fields"] + is_struct = bool(fields) and not all(field["name"].isdigit() for field in fields) + is_tuple = bool(fields) and all(field["name"].isdigit() for field in fields) + + if is_struct: + # For struct variants, we need to parse the error_data as a map + field_defs: list[tuple[str, str, str]] = [] + used_param_names: set[str] = set(reserved_field_names) + for field in fields: + field_type = convert_type(field["type"])["dart_type"] + json_key = field.get("serde_rename") or field["name"] + field_name = make_safe_field_name(field["name"], used_param_names) + field_defs.append((field_name, field_type, json_key)) + + lines.append( + f" parsers['{variant_type}'] = (errorData, message, path, trace) {{" + ) + lines.append(" final map = _asJsonMap(errorData);") + lines.append(f" return {exception_class_name}(") + for fname, ftype, json_key in field_defs: + accessor = ( + f"map.valueOrNull('{json_key}')" + if ftype.endswith("?") + else f"map.value('{json_key}')" + ) + expr = dart_value_from_json(ftype, accessor) + lines.append(f" {fname}: {expr},") + lines.append(" message: message,") + lines.append(" path: path,") + lines.append(" trace: trace,") + lines.append(" );") + lines.append(" };") + elif is_tuple: + if len(fields) == 1: + field_type = convert_type(fields[0]["type"])["dart_type"] + expr = dart_value_from_json(field_type, "errorData") + lines.append( + f" parsers['{variant_type}'] = (errorData, message, path, trace) => {exception_class_name}({expr}, message: message, path: path, trace: trace);" + ) + else: + lines.append( + f" parsers['{variant_type}'] = (errorData, message, path, trace) {{" + ) + lines.append(" final list = _asJsonList(errorData);") + args = ", ".join( + dart_value_from_json( + convert_type(field["type"])["dart_type"], f"list[{idx}]" + ) + for idx, field in enumerate(fields) + ) + lines.append( + f" return {exception_class_name}({args}, message: message, path: path, trace: trace);" + ) + lines.append(" };") + else: + # Unit variant - no error_data + lines.append( + f" parsers['{variant_type}'] = (errorData, message, path, trace) => {exception_class_name}(message: message, path: path, trace: trace);" + ) + + lines.append(" return parsers;") + lines.append(" })();") + lines.append("}") + lines.append("") + + return lines + + +def run_dart_tools(target: str, apply_fix: bool) -> None: + dart = shutil.which("dart") + if not dart: + return + + try: + subprocess.run([dart, "format", target], check=False) + except Exception: + return + + if not apply_fix: + return + + try: + subprocess.run([dart, "fix", "--apply", target], check=False) + except Exception: + return + + +def main() -> None: + args = parse_args() + mm2_repo_root, tmp_dir = resolve_mm2_repo(args) + + try: + error_enums_path = mm2_repo_root / "tools" / "error_enums.json" + if not error_enums_path.exists(): + raise SystemExit( + f"error_enums.json not found at {error_enums_path}. " + "Ensure the komodo-defi-framework repository is available." + ) + + with error_enums_path.open("r", encoding="utf-8") as f: + enums = json.load(f) + + lines = generate_lines(enums, mm2_repo_root) + + output_file = Path(args.out).expanduser().resolve() + output_file.parent.mkdir(parents=True, exist_ok=True) + output_file.write_text("\n".join(lines), encoding="utf-8") + + run_dart_tools(str(output_file), apply_fix=args.fix) + finally: + if tmp_dir is not None: + tmp_dir.cleanup() + + +if __name__ == "__main__": + main() diff --git a/packages/komodo_defi_sdk/lib/komodo_defi_sdk.dart b/packages/komodo_defi_sdk/lib/komodo_defi_sdk.dart index 054afc075..22ea5671b 100644 --- a/packages/komodo_defi_sdk/lib/komodo_defi_sdk.dart +++ b/packages/komodo_defi_sdk/lib/komodo_defi_sdk.dart @@ -8,7 +8,18 @@ library; export 'package:komodo_cex_market_data/komodo_cex_market_data.dart' show Commodity, Cryptocurrency, FiatCurrency, QuoteCurrency, Stablecoin; export 'package:komodo_defi_framework/komodo_defi_framework.dart' - show IKdfHostConfig, LocalConfig, RemoteConfig; + show + BalanceEvent, + HeartbeatEvent, + IKdfHostConfig, + LocalConfig, + NetworkEvent, + OrderStatusEvent, + OrderbookEvent, + RemoteConfig, + ShutdownSignalEvent, + SwapStatusEvent, + TxHistoryEvent; export 'package:komodo_defi_local_auth/komodo_defi_local_auth.dart' show AuthenticationState, AuthenticationStatus; // ZHTLC sync parameters @@ -21,7 +32,10 @@ export 'package:komodo_defi_sdk/src/balances/balance_manager.dart' export 'package:komodo_defi_sdk/src/sdk/komodo_defi_sdk_config.dart'; export 'package:komodo_defi_sdk/src/security/security_manager.dart' show SecurityManager; +export 'package:komodo_defi_sdk/src/trading/trading_manager.dart' + show TradingManager; +export 'src/activation/nft_activation_service.dart' show NftActivationService; export 'src/activation_config/activation_config_service.dart' show ActivationConfigRepository, @@ -34,9 +48,8 @@ export 'src/activation_config/activation_config_service.dart' ZhtlcUserConfig; export 'src/activation_config/hive_activation_config_repository.dart' show HiveActivationConfigRepository; -export 'src/activation/nft_activation_service.dart' show NftActivationService; export 'src/assets/_assets_index.dart' - show AssetHdWalletAddressesExtension, ActivatedAssetsCache; + show ActivatedAssetsCache, AssetHdWalletAddressesExtension; export 'src/assets/asset_extensions.dart' show AssetFaucetExtension, diff --git a/packages/komodo_defi_sdk/lib/src/activation/activation_manager.dart b/packages/komodo_defi_sdk/lib/src/activation/activation_manager.dart index 632a38cc2..09091da0b 100644 --- a/packages/komodo_defi_sdk/lib/src/activation/activation_manager.dart +++ b/packages/komodo_defi_sdk/lib/src/activation/activation_manager.dart @@ -8,6 +8,7 @@ import 'package:komodo_defi_rpc_methods/komodo_defi_rpc_methods.dart'; import 'package:komodo_defi_sdk/src/_internal_exports.dart'; import 'package:komodo_defi_sdk/src/activation_config/activation_config_service.dart'; import 'package:komodo_defi_sdk/src/balances/balance_manager.dart'; +import 'package:komodo_defi_sdk/src/errors/sdk_error_mapper.dart'; import 'package:komodo_defi_types/komodo_defi_types.dart'; import 'package:mutex/mutex.dart'; @@ -35,6 +36,7 @@ class ActivationManager { final ActivatedAssetsCache _activatedAssetsCache; final _activationMutex = Mutex(); static const _operationTimeout = Duration(seconds: 30); + static const SdkErrorMapper _errorMapper = SdkErrorMapper(); final Map> _activationCompleters = {}; bool _isDisposed = false; @@ -115,7 +117,7 @@ class ActivationManager { parentAsset ?? group.primary, group.children?.toList(), )) { - yield progress; + yield _attachSdkError(progress, group.primary.id); if (progress.isComplete) { await _handleActivationComplete(group, progress, primaryCompleter); @@ -123,10 +125,11 @@ class ActivationManager { } } catch (e) { debugPrint('Activation failed: $e'); + final mappedError = _mapError(e, group.primary.id); if (!primaryCompleter.isCompleted) { - primaryCompleter.completeError(e); + primaryCompleter.completeError(mappedError); } - rethrow; + throw mappedError; } finally { try { await _cleanupActivation(group.primary.id); @@ -137,6 +140,36 @@ class ActivationManager { } } + ActivationProgress _attachSdkError( + ActivationProgress progress, + AssetId assetId, + ) { + if (!progress.isError || progress.sdkError != null) { + return progress; + } + + final errorMessage = progress.errorMessage ?? 'Activation failed'; + final sdkError = _mapError( + errorMessage, + assetId, + ); + + return progress.copyWith( + errorMessage: sdkError.fallbackMessage, + sdkError: sdkError, + ); + } + + SdkError _mapError(Object error, AssetId assetId) { + return _errorMapper.map( + error, + context: SdkErrorContext( + operation: 'activation', + assetId: assetId.id, + ), + ); + } + /// Check if asset and its children are already activated Future _checkActivationStatus(_AssetGroup group) async { try { diff --git a/packages/komodo_defi_sdk/lib/src/activation/base_strategies/activation_strategy_base.dart b/packages/komodo_defi_sdk/lib/src/activation/base_strategies/activation_strategy_base.dart index 3343e94ac..9af28a4ab 100644 --- a/packages/komodo_defi_sdk/lib/src/activation/base_strategies/activation_strategy_base.dart +++ b/packages/komodo_defi_sdk/lib/src/activation/base_strategies/activation_strategy_base.dart @@ -1,5 +1,7 @@ import 'package:komodo_defi_sdk/src/assets/activated_assets_cache.dart'; +import 'package:komodo_defi_sdk/src/errors/sdk_error_mapper.dart'; import 'package:komodo_defi_types/komodo_defi_types.dart'; +import 'package:meta/meta.dart'; abstract class AssetActivator { const AssetActivator(this.client); @@ -151,4 +153,33 @@ abstract class ProtocolActivationStrategy extends BatchCapableActivator { supportedProtocols.contains(asset.protocol.subClass); Set get supportedProtocols; -} \ No newline at end of file + + @protected + ActivationProgress buildErrorProgress({ + required Asset asset, + required Object error, + StackTrace? stackTrace, + required String errorCode, + int stepCount = 1, + String status = 'Activation failed', + }) { + const mapper = SdkErrorMapper(); + final sdkError = mapper.map( + error, + context: SdkErrorContext(operation: 'activation', assetId: asset.id.id), + ); + return ActivationProgress( + status: status, + errorMessage: sdkError.fallbackMessage, + sdkError: sdkError, + isComplete: true, + progressDetails: ActivationProgressDetails( + currentStep: ActivationStep.error, + stepCount: stepCount, + errorCode: errorCode, + errorDetails: sdkError.fallbackMessage, + stackTrace: stackTrace?.toString(), + ), + ); + } +} diff --git a/packages/komodo_defi_sdk/lib/src/activation/protocol_strategies/bch_activation_strategy.dart b/packages/komodo_defi_sdk/lib/src/activation/protocol_strategies/bch_activation_strategy.dart index 612713c1e..f3ca2c7fa 100644 --- a/packages/komodo_defi_sdk/lib/src/activation/protocol_strategies/bch_activation_strategy.dart +++ b/packages/komodo_defi_sdk/lib/src/activation/protocol_strategies/bch_activation_strategy.dart @@ -18,9 +18,9 @@ class BchActivationStrategy extends ProtocolActivationStrategy { @override Set get supportedProtocols => { - CoinSubClass.utxo, - CoinSubClass.slp, - }; + CoinSubClass.utxo, + CoinSubClass.slp, + }; @override bool get supportsBatchActivation => true; @@ -86,13 +86,12 @@ class BchActivationStrategy extends ProtocolActivationStrategy { ), ); - final slpTokensRequests = children - ?.map( - (child) => TokensRequest(ticker: child.id.id), - ) + final slpTokensRequests = + children + ?.map((child) => TokensRequest(ticker: child.id.id)) .toList() ?? []; - + // Debug logging for BCH activation if (KdfLoggingConfig.verboseLogging) { log( @@ -100,13 +99,7 @@ class BchActivationStrategy extends ProtocolActivationStrategy { name: 'BchActivationStrategy', ); log( - '[RPC] Activation parameters: ${jsonEncode({ - 'ticker': asset.id.id, - 'protocol': asset.protocol.subClass.formatted, - 'slp_token_count': children?.length ?? 0, - 'slp_tokens': children?.map((e) => e.id.id).toList() ?? [], - 'activation_params': bchConfig.toRpcParams(), - })}', + '[RPC] Activation parameters: ${jsonEncode({'ticker': asset.id.id, 'protocol': asset.protocol.subClass.formatted, 'slp_token_count': children?.length ?? 0, 'slp_tokens': children?.map((e) => e.id.id).toList() ?? [], 'activation_params': bchConfig.toRpcParams()})}', name: 'BchActivationStrategy', ); } @@ -117,7 +110,7 @@ class BchActivationStrategy extends ProtocolActivationStrategy { params: bchConfig, slpTokensRequests: slpTokensRequests, ); - + if (KdfLoggingConfig.verboseLogging) { log( '[RPC] Successfully activated BCH with ${children?.length ?? 0} SLP tokens', @@ -167,11 +160,7 @@ class BchActivationStrategy extends ProtocolActivationStrategy { name: 'BchActivationStrategy', ); log( - '[RPC] Activation parameters: ${jsonEncode({ - 'ticker': asset.id.id, - 'protocol': asset.protocol.subClass.formatted, - 'parent_id': asset.id.parentId?.id, - })}', + '[RPC] Activation parameters: ${jsonEncode({'ticker': asset.id.id, 'protocol': asset.protocol.subClass.formatted, 'parent_id': asset.id.parentId?.id})}', name: 'BchActivationStrategy', ); } @@ -180,7 +169,7 @@ class BchActivationStrategy extends ProtocolActivationStrategy { ticker: asset.id.id, params: SlpActivationParams(), ); - + if (KdfLoggingConfig.verboseLogging) { log( '[RPC] Successfully activated SLP token: ${asset.id.id}', @@ -200,17 +189,12 @@ class BchActivationStrategy extends ProtocolActivationStrategy { ); } } catch (e, stack) { - yield ActivationProgress( - status: 'Activation failed', - errorMessage: e.toString(), - isComplete: true, - progressDetails: ActivationProgressDetails( - currentStep: ActivationStep.error, - stepCount: 4, - errorCode: isBch ? 'BCH_ACTIVATION_ERROR' : 'SLP_ACTIVATION_ERROR', - errorDetails: e.toString(), - stackTrace: stack.toString(), - ), + yield buildErrorProgress( + asset: asset, + error: e, + stackTrace: stack, + errorCode: isBch ? 'BCH_ACTIVATION_ERROR' : 'SLP_ACTIVATION_ERROR', + stepCount: 4, ); } } diff --git a/packages/komodo_defi_sdk/lib/src/activation/protocol_strategies/custom_erc20_activation_strategy.dart b/packages/komodo_defi_sdk/lib/src/activation/protocol_strategies/custom_erc20_activation_strategy.dart index 2e957ba79..1eb19f170 100644 --- a/packages/komodo_defi_sdk/lib/src/activation/protocol_strategies/custom_erc20_activation_strategy.dart +++ b/packages/komodo_defi_sdk/lib/src/activation/protocol_strategies/custom_erc20_activation_strategy.dart @@ -15,6 +15,7 @@ class CustomErc20ActivationStrategy extends ProtocolActivationStrategy { @override Set get supportedProtocols => { CoinSubClass.erc20, + CoinSubClass.grc20, CoinSubClass.bep20, CoinSubClass.ftm20, CoinSubClass.matic, @@ -70,7 +71,7 @@ class CustomErc20ActivationStrategy extends ProtocolActivationStrategy { ); final platform = protocolData.value('platform'); final contractAddress = protocolData.value('contract_address'); - + // Debug logging for custom ERC20 token activation if (KdfLoggingConfig.verboseLogging) { log( @@ -78,13 +79,7 @@ class CustomErc20ActivationStrategy extends ProtocolActivationStrategy { name: 'CustomErc20ActivationStrategy', ); log( - '[RPC] Activation parameters: ${jsonEncode({ - 'ticker': asset.id.id, - 'protocol': asset.protocol.subClass.formatted, - 'platform': platform, - 'contract_address': contractAddress, - 'activation_params': activationParams.toRpcParams(), - })}', + '[RPC] Activation parameters: ${jsonEncode({'ticker': asset.id.id, 'protocol': asset.protocol.subClass.formatted, 'platform': platform, 'contract_address': contractAddress, 'activation_params': activationParams.toRpcParams()})}', name: 'CustomErc20ActivationStrategy', ); } @@ -95,7 +90,7 @@ class CustomErc20ActivationStrategy extends ProtocolActivationStrategy { platform: platform, contractAddress: contractAddress, ); - + if (KdfLoggingConfig.verboseLogging) { log( '[RPC] Successfully activated custom ERC20 token: ${asset.id.id}', @@ -115,17 +110,12 @@ class CustomErc20ActivationStrategy extends ProtocolActivationStrategy { ), ); } catch (e, stack) { - yield ActivationProgress( - status: 'Activation failed', - errorMessage: e.toString(), - isComplete: true, - progressDetails: ActivationProgressDetails( - currentStep: ActivationStep.error, - stepCount: 2, - errorCode: 'ERC20_ACTIVATION_ERROR', - errorDetails: e.toString(), - stackTrace: stack.toString(), - ), + yield buildErrorProgress( + asset: asset, + error: e, + stackTrace: stack, + errorCode: 'ERC20_ACTIVATION_ERROR', + stepCount: 2, ); } } diff --git a/packages/komodo_defi_sdk/lib/src/activation/protocol_strategies/erc20_activation_strategy.dart b/packages/komodo_defi_sdk/lib/src/activation/protocol_strategies/erc20_activation_strategy.dart index a74be015b..e35bab0d1 100644 --- a/packages/komodo_defi_sdk/lib/src/activation/protocol_strategies/erc20_activation_strategy.dart +++ b/packages/komodo_defi_sdk/lib/src/activation/protocol_strategies/erc20_activation_strategy.dart @@ -16,6 +16,7 @@ class Erc20ActivationStrategy extends ProtocolActivationStrategy { @override Set get supportedProtocols => { CoinSubClass.erc20, + CoinSubClass.grc20, CoinSubClass.bep20, CoinSubClass.ftm20, CoinSubClass.matic, @@ -71,7 +72,7 @@ class Erc20ActivationStrategy extends ProtocolActivationStrategy { final activationParams = Erc20ActivationParams.fromJsonConfig( asset.protocol.config, ); - + // Debug logging for ERC20 token activation if (KdfLoggingConfig.verboseLogging) { log( @@ -79,22 +80,16 @@ class Erc20ActivationStrategy extends ProtocolActivationStrategy { name: 'Erc20ActivationStrategy', ); log( - '[RPC] Activation parameters: ${jsonEncode({ - 'ticker': asset.id.id, - 'protocol': asset.protocol.subClass.formatted, - 'parent_id': asset.id.parentId?.id, - 'activation_params': activationParams.toRpcParams(), - 'priv_key_policy': privKeyPolicy.toJson(), - })}', + '[RPC] Activation parameters: ${jsonEncode({'ticker': asset.id.id, 'protocol': asset.protocol.subClass.formatted, 'parent_id': asset.id.parentId?.id, 'activation_params': activationParams.toRpcParams(), 'priv_key_policy': privKeyPolicy.toJson()})}', name: 'Erc20ActivationStrategy', ); } - + await client.rpc.erc20.enableErc20( ticker: asset.id.id, activationParams: activationParams, ); - + if (KdfLoggingConfig.verboseLogging) { log( '[RPC] Successfully activated ERC20 token: ${asset.id.id}', @@ -114,17 +109,12 @@ class Erc20ActivationStrategy extends ProtocolActivationStrategy { ), ); } catch (e, stack) { - yield ActivationProgress( - status: 'Activation failed', - errorMessage: e.toString(), - isComplete: true, - progressDetails: ActivationProgressDetails( - currentStep: ActivationStep.error, - stepCount: 2, - errorCode: 'ERC20_ACTIVATION_ERROR', - errorDetails: e.toString(), - stackTrace: stack.toString(), - ), + yield buildErrorProgress( + asset: asset, + error: e, + stackTrace: stack, + errorCode: 'ERC20_ACTIVATION_ERROR', + stepCount: 2, ); } } diff --git a/packages/komodo_defi_sdk/lib/src/activation/protocol_strategies/eth_task_activation_strategy.dart b/packages/komodo_defi_sdk/lib/src/activation/protocol_strategies/eth_task_activation_strategy.dart index a61664a35..d323b07a9 100644 --- a/packages/komodo_defi_sdk/lib/src/activation/protocol_strategies/eth_task_activation_strategy.dart +++ b/packages/komodo_defi_sdk/lib/src/activation/protocol_strategies/eth_task_activation_strategy.dart @@ -18,6 +18,7 @@ class EthTaskActivationStrategy extends ProtocolActivationStrategy { @override Set get supportedProtocols => { CoinSubClass.erc20, + CoinSubClass.grc20, CoinSubClass.bep20, CoinSubClass.ftm20, CoinSubClass.matic, @@ -152,16 +153,11 @@ class EthTaskActivationStrategy extends ProtocolActivationStrategy { ), ); } else { - yield ActivationProgress( - status: 'Activation failed: ${status.details}', - errorMessage: status.details, - isComplete: true, - progressDetails: ActivationProgressDetails( - currentStep: ActivationStep.error, - stepCount: 5, - errorCode: 'ETH_TASK_ACTIVATION_ERROR', - errorDetails: status.details, - ), + yield buildErrorProgress( + asset: asset, + error: status.details, + errorCode: 'ETH_TASK_ACTIVATION_ERROR', + stepCount: 5, ); } isComplete = true; @@ -180,17 +176,12 @@ class EthTaskActivationStrategy extends ProtocolActivationStrategy { } } } catch (e, stack) { - yield ActivationProgress( - status: 'Activation failed', - errorMessage: e.toString(), - isComplete: true, - progressDetails: ActivationProgressDetails( - currentStep: ActivationStep.error, - stepCount: 5, - errorCode: 'ETH_TASK_ACTIVATION_ERROR', - errorDetails: e.toString(), - stackTrace: stack.toString(), - ), + yield buildErrorProgress( + asset: asset, + error: e, + stackTrace: stack, + errorCode: 'ETH_TASK_ACTIVATION_ERROR', + stepCount: 5, ); } } diff --git a/packages/komodo_defi_sdk/lib/src/activation/protocol_strategies/eth_with_tokens_activation_strategy.dart b/packages/komodo_defi_sdk/lib/src/activation/protocol_strategies/eth_with_tokens_activation_strategy.dart index 0b7d6f376..da4d7600e 100644 --- a/packages/komodo_defi_sdk/lib/src/activation/protocol_strategies/eth_with_tokens_activation_strategy.dart +++ b/packages/komodo_defi_sdk/lib/src/activation/protocol_strategies/eth_with_tokens_activation_strategy.dart @@ -18,6 +18,7 @@ class EthWithTokensActivationStrategy extends ProtocolActivationStrategy { @override Set get supportedProtocols => { CoinSubClass.erc20, + CoinSubClass.grc20, CoinSubClass.bep20, CoinSubClass.ftm20, CoinSubClass.matic, @@ -118,15 +119,15 @@ class EthWithTokensActivationStrategy extends ProtocolActivationStrategy { // Debug logging for ETH platform activation if (KdfLoggingConfig.verboseLogging) { log( - '[RPC] Activating ETH platform: ${asset.id.id}', - name: 'EthWithTokensActivationStrategy', - ); + '[RPC] Activating ETH platform: ${asset.id.id}', + name: 'EthWithTokensActivationStrategy', + ); } if (KdfLoggingConfig.verboseLogging) { log( - '[RPC] Activation parameters: ${jsonEncode({'ticker': asset.id.id, 'protocol': asset.protocol.subClass.formatted, 'token_count': children?.length ?? 0, 'tokens': children?.map((e) => e.id.id).toList() ?? [], 'activation_params': activationParams.toRpcParams(), 'priv_key_policy': privKeyPolicy.toJson()})}', - name: 'EthWithTokensActivationStrategy', - ); + '[RPC] Activation parameters: ${jsonEncode({'ticker': asset.id.id, 'protocol': asset.protocol.subClass.formatted, 'token_count': children?.length ?? 0, 'tokens': children?.map((e) => e.id.id).toList() ?? [], 'activation_params': activationParams.toRpcParams(), 'priv_key_policy': privKeyPolicy.toJson()})}', + name: 'EthWithTokensActivationStrategy', + ); } await client.rpc.erc20.enableEthWithTokens( @@ -136,9 +137,9 @@ class EthWithTokensActivationStrategy extends ProtocolActivationStrategy { if (KdfLoggingConfig.verboseLogging) { log( - '[RPC] Successfully activated ETH platform: ${asset.id.id} with ${children?.length ?? 0} tokens', - name: 'EthWithTokensActivationStrategy', - ); + '[RPC] Successfully activated ETH platform: ${asset.id.id} with ${children?.length ?? 0} tokens', + name: 'EthWithTokensActivationStrategy', + ); } yield const ActivationProgress( @@ -163,17 +164,12 @@ class EthWithTokensActivationStrategy extends ProtocolActivationStrategy { ), ); } catch (e, stack) { - yield ActivationProgress( - status: 'Activation failed', - errorMessage: e.toString(), - isComplete: true, - progressDetails: ActivationProgressDetails( - currentStep: ActivationStep.error, - stepCount: 3, - errorCode: 'ETH_WITH_TOKENS_ACTIVATION_ERROR', - errorDetails: e.toString(), - stackTrace: stack.toString(), - ), + yield buildErrorProgress( + asset: asset, + error: e, + stackTrace: stack, + errorCode: 'ETH_WITH_TOKENS_ACTIVATION_ERROR', + stepCount: 3, ); } } diff --git a/packages/komodo_defi_sdk/lib/src/activation/protocol_strategies/qtum_activation_strategy.dart b/packages/komodo_defi_sdk/lib/src/activation/protocol_strategies/qtum_activation_strategy.dart index 49d796660..19f398144 100644 --- a/packages/komodo_defi_sdk/lib/src/activation/protocol_strategies/qtum_activation_strategy.dart +++ b/packages/komodo_defi_sdk/lib/src/activation/protocol_strategies/qtum_activation_strategy.dart @@ -44,36 +44,31 @@ class QtumActivationStrategy extends ProtocolActivationStrategy { final activationParams = asset.protocol.defaultActivationParams( privKeyPolicy: privKeyPolicy, ); - + // Debug logging for QTUM activation if (KdfLoggingConfig.verboseLogging) { log( - '[RPC] Activating QTUM coin: ${asset.id.id}', - name: 'QtumActivationStrategy', - ); + '[RPC] Activating QTUM coin: ${asset.id.id}', + name: 'QtumActivationStrategy', + ); } if (KdfLoggingConfig.verboseLogging) { log( - '[RPC] Activation parameters: ${jsonEncode({ - 'ticker': asset.id.id, - 'protocol': asset.protocol.subClass.formatted, - 'activation_params': activationParams.toRpcParams(), - 'priv_key_policy': privKeyPolicy.toJson(), - })}', - name: 'QtumActivationStrategy', - ); + '[RPC] Activation parameters: ${jsonEncode({'ticker': asset.id.id, 'protocol': asset.protocol.subClass.formatted, 'activation_params': activationParams.toRpcParams(), 'priv_key_policy': privKeyPolicy.toJson()})}', + name: 'QtumActivationStrategy', + ); } - + final taskResponse = await client.rpc.qtum.enableQtumInit( ticker: asset.id.id, params: activationParams, ); - + if (KdfLoggingConfig.verboseLogging) { log( - '[RPC] Task initiated for ${asset.id.id}, task_id: ${taskResponse.taskId}', - name: 'QtumActivationStrategy', - ); + '[RPC] Task initiated for ${asset.id.id}, task_id: ${taskResponse.taskId}', + name: 'QtumActivationStrategy', + ); } var isComplete = false; @@ -95,16 +90,11 @@ class QtumActivationStrategy extends ProtocolActivationStrategy { ), ); } else { - yield ActivationProgress( - status: 'Activation failed: ${status.details}', - errorMessage: status.details, - isComplete: true, - progressDetails: ActivationProgressDetails( - currentStep: ActivationStep.error, - stepCount: 4, - errorCode: 'QTUM_ACTIVATION_ERROR', - errorDetails: status.details, - ), + yield buildErrorProgress( + asset: asset, + error: status.details, + errorCode: 'QTUM_ACTIVATION_ERROR', + stepCount: 4, ); } isComplete = true; @@ -123,17 +113,12 @@ class QtumActivationStrategy extends ProtocolActivationStrategy { } } } catch (e, stack) { - yield ActivationProgress( - status: 'Activation failed', - errorMessage: e.toString(), - isComplete: true, - progressDetails: ActivationProgressDetails( - currentStep: ActivationStep.error, - stepCount: 4, - errorCode: 'QTUM_ACTIVATION_ERROR', - errorDetails: e.toString(), - stackTrace: stack.toString(), - ), + yield buildErrorProgress( + asset: asset, + error: e, + stackTrace: stack, + errorCode: 'QTUM_ACTIVATION_ERROR', + stepCount: 4, ); } } diff --git a/packages/komodo_defi_sdk/lib/src/activation/protocol_strategies/sia_activation_strategy.dart b/packages/komodo_defi_sdk/lib/src/activation/protocol_strategies/sia_activation_strategy.dart index 3ece5c8bf..52a954b42 100644 --- a/packages/komodo_defi_sdk/lib/src/activation/protocol_strategies/sia_activation_strategy.dart +++ b/packages/komodo_defi_sdk/lib/src/activation/protocol_strategies/sia_activation_strategy.dart @@ -37,18 +37,14 @@ class SiaActivationStrategy extends ProtocolActivationStrategy { progressDetails: ActivationProgressDetails( currentStep: ActivationStep.initialization, stepCount: 3, - additionalInfo: { - 'assetType': 'platform', - 'protocol': 'SIA', - }, + additionalInfo: {'assetType': 'platform', 'protocol': 'SIA'}, ), ); try { - final init = await KomodoDefiRpcMethods(client).sia.enableSiaInit( - ticker: asset.id.id, - params: params, - ); + final init = await KomodoDefiRpcMethods( + client, + ).sia.enableSiaInit(ticker: asset.id.id, params: params); final taskId = init.taskId; yield ActivationProgress( @@ -61,8 +57,9 @@ class SiaActivationStrategy extends ProtocolActivationStrategy { ); while (true) { - final status = - await KomodoDefiRpcMethods(client).sia.enableSiaStatus(taskId); + final status = await KomodoDefiRpcMethods( + client, + ).sia.enableSiaStatus(taskId); yield ActivationProgress( status: 'SIA activation in progress', @@ -86,12 +83,15 @@ class SiaActivationStrategy extends ProtocolActivationStrategy { ), ); } else { - yield ActivationProgress( + final errorProgress = buildErrorProgress( + asset: asset, + error: status.details ?? 'SIA activation failed', + errorCode: 'SIA_ACTIVATION_ERROR', + stepCount: 3, status: 'SIA activation failed', - isComplete: true, - progressDetails: ActivationProgressDetails( - currentStep: ActivationStep.error, - stepCount: 3, + ); + yield errorProgress.copyWith( + progressDetails: errorProgress.progressDetails?.copyWith( additionalInfo: { 'taskId': taskId, 'status': status.status, @@ -115,13 +115,17 @@ class SiaActivationStrategy extends ProtocolActivationStrategy { await Future.delayed(kPollInterval); } - } on Exception catch (e) { - yield ActivationProgress( + } on Exception catch (e, stack) { + final errorProgress = buildErrorProgress( + asset: asset, + error: e, + stackTrace: stack, + errorCode: 'SIA_ACTIVATION_ERROR', + stepCount: 3, status: 'SIA activation failed', - isComplete: true, - progressDetails: ActivationProgressDetails( - currentStep: ActivationStep.error, - stepCount: 3, + ); + yield errorProgress.copyWith( + progressDetails: errorProgress.progressDetails?.copyWith( additionalInfo: {'error': e.toString()}, ), ); @@ -129,4 +133,3 @@ class SiaActivationStrategy extends ProtocolActivationStrategy { } } } - diff --git a/packages/komodo_defi_sdk/lib/src/activation/protocol_strategies/slp_activation_strategy.dart b/packages/komodo_defi_sdk/lib/src/activation/protocol_strategies/slp_activation_strategy.dart index a627360bc..5721276b7 100644 --- a/packages/komodo_defi_sdk/lib/src/activation/protocol_strategies/slp_activation_strategy.dart +++ b/packages/komodo_defi_sdk/lib/src/activation/protocol_strategies/slp_activation_strategy.dart @@ -55,10 +55,9 @@ class SlpActivationStrategy extends ProtocolActivationStrategy { await client.rpc.slp.enableBchWithTokens( ticker: asset.id.id, params: BchActivationParams.fromJson(protocol.config), - slpTokensRequests: children - ?.map( - (child) => TokensRequest(ticker: child.id.id), - ) + slpTokensRequests: + children + ?.map((child) => TokensRequest(ticker: child.id.id)) .toList() ?? [], ); @@ -88,17 +87,12 @@ class SlpActivationStrategy extends ProtocolActivationStrategy { ), ); } catch (e, stack) { - yield ActivationProgress( - status: 'Activation failed', - errorMessage: e.toString(), - isComplete: true, - progressDetails: ActivationProgressDetails( - currentStep: ActivationStep.error, - stepCount: 3, - errorCode: 'SLP_ACTIVATION_ERROR', - errorDetails: e.toString(), - stackTrace: stack.toString(), - ), + yield buildErrorProgress( + asset: asset, + error: e, + stackTrace: stack, + errorCode: 'SLP_ACTIVATION_ERROR', + stepCount: 3, ); } } diff --git a/packages/komodo_defi_sdk/lib/src/activation/protocol_strategies/tendermint_activation_strategy.dart b/packages/komodo_defi_sdk/lib/src/activation/protocol_strategies/tendermint_activation_strategy.dart index 2df74655a..282824cb9 100644 --- a/packages/komodo_defi_sdk/lib/src/activation/protocol_strategies/tendermint_activation_strategy.dart +++ b/packages/komodo_defi_sdk/lib/src/activation/protocol_strategies/tendermint_activation_strategy.dart @@ -100,33 +100,25 @@ class TendermintWithTokensActivationStrategy ), ); - final tokensParams = children - ?.map((child) => TendermintTokenParams(ticker: child.id.id)) - .toList() ?? + final tokensParams = + children + ?.map((child) => TendermintTokenParams(ticker: child.id.id)) + .toList() ?? []; final nodes = protocol.rpcUrlsMap.map(TendermintNode.fromJson).toList(); - + // Debug logging for Tendermint activation if (KdfLoggingConfig.verboseLogging) { log( - '[RPC] Activating Tendermint platform: ${asset.id.id}', - name: 'TendermintWithTokensActivationStrategy', - ); + '[RPC] Activating Tendermint platform: ${asset.id.id}', + name: 'TendermintWithTokensActivationStrategy', + ); } if (KdfLoggingConfig.verboseLogging) { log( - '[RPC] Activation parameters: ${jsonEncode({ - 'ticker': asset.id.id, - 'protocol': asset.protocol.subClass.formatted, - 'chain_id': protocol.chainId, - 'account_prefix': protocol.accountPrefix, - 'token_count': children?.length ?? 0, - 'tokens': children?.map((e) => e.id.id).toList() ?? [], - 'rpc_nodes': nodes.map((n) => n.toJson()).toList(), - 'priv_key_policy': privKeyPolicy.toJson(), - })}', - name: 'TendermintWithTokensActivationStrategy', - ); + '[RPC] Activation parameters: ${jsonEncode({'ticker': asset.id.id, 'protocol': asset.protocol.subClass.formatted, 'chain_id': protocol.chainId, 'account_prefix': protocol.accountPrefix, 'token_count': children?.length ?? 0, 'tokens': children?.map((e) => e.id.id).toList() ?? [], 'rpc_nodes': nodes.map((n) => n.toJson()).toList(), 'priv_key_policy': privKeyPolicy.toJson()})}', + name: 'TendermintWithTokensActivationStrategy', + ); } final taskResponse = await client.rpc.tendermint.taskEnableTendermintInit( @@ -134,12 +126,12 @@ class TendermintWithTokensActivationStrategy tokensParams: tokensParams, nodes: nodes, ); - + if (KdfLoggingConfig.verboseLogging) { log( - '[RPC] Task initiated for ${asset.id.id}, task_id: ${taskResponse.taskId}', - name: 'TendermintWithTokensActivationStrategy', - ); + '[RPC] Task initiated for ${asset.id.id}, task_id: ${taskResponse.taskId}', + name: 'TendermintWithTokensActivationStrategy', + ); } yield ActivationProgress( @@ -180,16 +172,11 @@ class TendermintWithTokensActivationStrategy ); isComplete = true; } else if (status.status == SyncStatusEnum.error) { - yield ActivationProgress( - status: 'Activation failed: ${status.details.error}', - errorMessage: status.details.error ?? 'Unknown error', - isComplete: true, - progressDetails: ActivationProgressDetails( - currentStep: ActivationStep.error, - stepCount: 5, - errorCode: 'TENDERMINT_TASK_ACTIVATION_ERROR', - errorDetails: status.details.error, - ), + yield buildErrorProgress( + asset: asset, + error: status.details.error ?? 'Unknown error', + errorCode: 'TENDERMINT_TASK_ACTIVATION_ERROR', + stepCount: 5, ); isComplete = true; } else { @@ -207,17 +194,12 @@ class TendermintWithTokensActivationStrategy } } } catch (e, stack) { - yield ActivationProgress( - status: 'Activation failed', - errorMessage: e.toString(), - isComplete: true, - progressDetails: ActivationProgressDetails( - currentStep: ActivationStep.error, - stepCount: 5, - errorCode: 'TENDERMINT_WITH_TOKENS_ACTIVATION_ERROR', - errorDetails: e.toString(), - stackTrace: stack.toString(), - ), + yield buildErrorProgress( + asset: asset, + error: e, + stackTrace: stack, + errorCode: 'TENDERMINT_WITH_TOKENS_ACTIVATION_ERROR', + stepCount: 5, ); } } diff --git a/packages/komodo_defi_sdk/lib/src/activation/protocol_strategies/tendermint_task_activation_strategy.dart b/packages/komodo_defi_sdk/lib/src/activation/protocol_strategies/tendermint_task_activation_strategy.dart index 7beff48b1..c00f52c44 100644 --- a/packages/komodo_defi_sdk/lib/src/activation/protocol_strategies/tendermint_task_activation_strategy.dart +++ b/packages/komodo_defi_sdk/lib/src/activation/protocol_strategies/tendermint_task_activation_strategy.dart @@ -109,16 +109,11 @@ class TendermintTaskActivationStrategy extends ProtocolActivationStrategy { ); isComplete = true; } else if (status.status == SyncStatusEnum.error) { - yield ActivationProgress( - status: 'Activation failed: ${status.details.error}', - errorMessage: status.details.error ?? 'Unknown error', - isComplete: true, - progressDetails: ActivationProgressDetails( - currentStep: ActivationStep.error, - stepCount: 5, - errorCode: 'TENDERMINT_TASK_ACTIVATION_ERROR', - errorDetails: status.details.error, - ), + yield buildErrorProgress( + asset: asset, + error: status.details.error ?? 'Unknown error', + errorCode: 'TENDERMINT_TASK_ACTIVATION_ERROR', + stepCount: 5, ); isComplete = true; } else { @@ -136,17 +131,12 @@ class TendermintTaskActivationStrategy extends ProtocolActivationStrategy { } } } catch (e, stack) { - yield ActivationProgress( - status: 'Activation failed', - errorMessage: e.toString(), - isComplete: true, - progressDetails: ActivationProgressDetails( - currentStep: ActivationStep.error, - stepCount: 5, - errorCode: 'TENDERMINT_TASK_ACTIVATION_ERROR', - errorDetails: e.toString(), - stackTrace: stack.toString(), - ), + yield buildErrorProgress( + asset: asset, + error: e, + stackTrace: stack, + errorCode: 'TENDERMINT_TASK_ACTIVATION_ERROR', + stepCount: 5, ); } } diff --git a/packages/komodo_defi_sdk/lib/src/activation/protocol_strategies/tendermint_token_activation_strategy.dart b/packages/komodo_defi_sdk/lib/src/activation/protocol_strategies/tendermint_token_activation_strategy.dart index 4ce8da5dc..98c3d0dde 100644 --- a/packages/komodo_defi_sdk/lib/src/activation/protocol_strategies/tendermint_token_activation_strategy.dart +++ b/packages/komodo_defi_sdk/lib/src/activation/protocol_strategies/tendermint_token_activation_strategy.dart @@ -96,17 +96,12 @@ class TendermintTokenActivationStrategy extends ProtocolActivationStrategy { ), ); } catch (e, stack) { - yield ActivationProgress( - status: 'Activation failed', - errorMessage: e.toString(), - isComplete: true, - progressDetails: ActivationProgressDetails( - currentStep: ActivationStep.error, - stepCount: 3, - errorCode: 'TENDERMINT_TOKEN_ACTIVATION_ERROR', - errorDetails: e.toString(), - stackTrace: stack.toString(), - ), + yield buildErrorProgress( + asset: asset, + error: e, + stackTrace: stack, + errorCode: 'TENDERMINT_TOKEN_ACTIVATION_ERROR', + stepCount: 3, ); } } diff --git a/packages/komodo_defi_sdk/lib/src/activation/protocol_strategies/utxo_activation_strategy.dart b/packages/komodo_defi_sdk/lib/src/activation/protocol_strategies/utxo_activation_strategy.dart index e3d6b33c3..aa4534ebf 100644 --- a/packages/komodo_defi_sdk/lib/src/activation/protocol_strategies/utxo_activation_strategy.dart +++ b/packages/komodo_defi_sdk/lib/src/activation/protocol_strategies/utxo_activation_strategy.dart @@ -123,16 +123,11 @@ class UtxoActivationStrategy extends ProtocolActivationStrategy { ), ); } else { - yield ActivationProgress( - status: 'Activation failed: ${status.details}', - errorMessage: status.details, - isComplete: true, - progressDetails: ActivationProgressDetails( - currentStep: ActivationStep.error, - stepCount: 5, - errorCode: 'UTXO_ACTIVATION_ERROR', - errorDetails: status.details, - ), + yield buildErrorProgress( + asset: asset, + error: status.details, + errorCode: 'UTXO_ACTIVATION_ERROR', + stepCount: 5, ); } isComplete = true; @@ -151,17 +146,12 @@ class UtxoActivationStrategy extends ProtocolActivationStrategy { } } } catch (e, stack) { - yield ActivationProgress( - status: 'Activation failed', - errorMessage: e.toString(), - isComplete: true, - progressDetails: ActivationProgressDetails( - currentStep: ActivationStep.error, - stepCount: 5, - errorCode: 'UTXO_ACTIVATION_ERROR', - errorDetails: e.toString(), - stackTrace: stack.toString(), - ), + yield buildErrorProgress( + asset: asset, + error: e, + stackTrace: stack, + errorCode: 'UTXO_ACTIVATION_ERROR', + stepCount: 5, ); } } diff --git a/packages/komodo_defi_sdk/lib/src/activation/protocol_strategies/zhtlc_activation_progress.dart b/packages/komodo_defi_sdk/lib/src/activation/protocol_strategies/zhtlc_activation_progress.dart index 70e094ab0..89ea8fdba 100644 --- a/packages/komodo_defi_sdk/lib/src/activation/protocol_strategies/zhtlc_activation_progress.dart +++ b/packages/komodo_defi_sdk/lib/src/activation/protocol_strategies/zhtlc_activation_progress.dart @@ -1,6 +1,7 @@ // TODO(komodo-team): Allow passing the start sync mode; currently hard-coded // to sync from the time of activation. +import 'package:komodo_defi_sdk/src/errors/sdk_error_mapper.dart'; import 'package:komodo_defi_types/komodo_defi_types.dart'; /// Convenience wrapper around [ActivationProgress] that exposes the canonical @@ -13,6 +14,7 @@ class ZhtlcActivationProgress extends ActivationProgress { super.isComplete, super.errorMessage, super.progressDetails, + super.sdkError, }); /// Creates the initial "starting activation" progress update. @@ -44,15 +46,18 @@ class ZhtlcActivationProgress extends ActivationProgress { /// Emits a terminal failure progress snapshot for unexpected exceptions. factory ZhtlcActivationProgress.failure(Object error, StackTrace stack) { + const mapper = SdkErrorMapper(); + final sdkError = mapper.map(error); return ZhtlcActivationProgress._( status: 'Activation failed', - errorMessage: error.toString(), + errorMessage: sdkError.fallbackMessage, isComplete: true, + sdkError: sdkError, progressDetails: ActivationProgressDetails( currentStep: ActivationStep.error, stepCount: 6, errorCode: ZhtlcActivationProgress.errorCode, - errorDetails: error.toString(), + errorDetails: sdkError.fallbackMessage, stackTrace: stack.toString(), additionalInfo: { 'errorType': error.runtimeType.toString(), diff --git a/packages/komodo_defi_sdk/lib/src/activation/protocol_strategies/zhtlc_activation_progress_estimator.dart b/packages/komodo_defi_sdk/lib/src/activation/protocol_strategies/zhtlc_activation_progress_estimator.dart index f60420c0c..df1a9a335 100644 --- a/packages/komodo_defi_sdk/lib/src/activation/protocol_strategies/zhtlc_activation_progress_estimator.dart +++ b/packages/komodo_defi_sdk/lib/src/activation/protocol_strategies/zhtlc_activation_progress_estimator.dart @@ -3,6 +3,7 @@ import 'dart:math' as math; import 'package:komodo_defi_rpc_methods/komodo_defi_rpc_methods.dart'; import 'package:komodo_defi_sdk/src/activation/protocol_strategies/zhtlc_activation_progress.dart'; import 'package:komodo_defi_types/komodo_defi_type_utils.dart'; +import 'package:komodo_defi_sdk/src/errors/sdk_error_mapper.dart'; import 'package:komodo_defi_types/komodo_defi_types.dart'; /// High-level phases emitted by the ZHTLC activation task engine. @@ -166,6 +167,11 @@ class ZhtlcActivationProgressEstimator { ZhtlcStatusDetail? detail, int? currentBlock, }) { + const mapper = SdkErrorMapper(); + final context = SdkErrorContext( + operation: 'activation', + assetId: asset.id.id, + ); final parsedDetail = detail ?? parse(status.details); final baseInfo = _buildAdditionalInfo( asset, @@ -178,15 +184,17 @@ class ZhtlcActivationProgressEstimator { if (parsedDetail.hasError) { final message = _extractErrorMessage(parsedDetail.error) ?? 'Unknown error'; + final sdkError = mapper.map(message, context: context); return ActivationProgress( status: 'Activation failed', - errorMessage: message, + errorMessage: sdkError.fallbackMessage, isComplete: true, + sdkError: sdkError, progressDetails: ActivationProgressDetails( currentStep: ActivationStep.error, stepCount: stepCount, errorCode: ZhtlcActivationProgress.errorCode, - errorDetails: message, + errorDetails: sdkError.fallbackMessage, additionalInfo: baseInfo, ), ); @@ -204,17 +212,19 @@ class ZhtlcActivationProgressEstimator { if (status.status == 'Error' || parsedDetail.phase == ZhtlcActivationPhase.error) { final message = parsedDetail.message ?? status.details; + final sdkError = mapper.map(message, context: context); return ActivationProgress( status: 'Activation failed', - errorMessage: message, + errorMessage: sdkError.fallbackMessage, isComplete: true, + sdkError: sdkError, progressDetails: ActivationProgressDetails( currentStep: ActivationStep.error, stepCount: stepCount, errorCode: ZhtlcActivationProgress.errorCode, errorDetails: parsedDetail.error != null ? jsonToString(parsedDetail.error) - : message, + : sdkError.fallbackMessage, additionalInfo: baseInfo, ), ); diff --git a/packages/komodo_defi_sdk/lib/src/bootstrap.dart b/packages/komodo_defi_sdk/lib/src/bootstrap.dart index 3ea76836f..3ffeceb91 100644 --- a/packages/komodo_defi_sdk/lib/src/bootstrap.dart +++ b/packages/komodo_defi_sdk/lib/src/bootstrap.dart @@ -267,6 +267,16 @@ Future bootstrap({ return FeeManager(client); }, dependsOn: [ApiClient]); + container.registerSingletonAsync(() async { + final client = await container.getAsync(); + final eventStreamingManager = await container + .getAsync(); + return TradingManager( + client: client, + eventStreamingManager: eventStreamingManager, + ); + }, dependsOn: [ApiClient, EventStreamingManager]); + container.registerSingletonAsync(() async { final client = await container.getAsync(); return LegacyWithdrawalManager(client); diff --git a/packages/komodo_defi_sdk/lib/src/errors/sdk_error_mapper.dart b/packages/komodo_defi_sdk/lib/src/errors/sdk_error_mapper.dart new file mode 100644 index 000000000..dd7cede59 --- /dev/null +++ b/packages/komodo_defi_sdk/lib/src/errors/sdk_error_mapper.dart @@ -0,0 +1,1406 @@ +import 'dart:async'; +import 'dart:developer' show log; + +import 'package:komodo_defi_rpc_methods/komodo_defi_rpc_methods.dart'; +import 'package:komodo_defi_sdk/src/activation/activation_exceptions.dart'; +import 'package:komodo_defi_types/komodo_defi_types.dart'; + +abstract class SdkErrorHandler { + const SdkErrorHandler(); + + bool canHandle(Object error); + + SdkError handle(Object error, {SdkErrorContext? context}); +} + +class SdkErrorMapper { + const SdkErrorMapper({List? handlers}) + : _handlers = handlers ?? _defaultHandlers; + + final List _handlers; + + static const List _defaultHandlers = [ + _SdkErrorPassthroughHandler(), + _WithdrawalExceptionHandler(), + _AuthExceptionHandler(), + _ActivationExceptionHandler(), + _GeneralErrorResponseHandler(), + _MmRpcExceptionHandler(), + _TimeoutExceptionHandler(), + _UnsupportedErrorHandler(), + _StringErrorHandler(), + _FallbackHandler(), + ]; + + SdkError map(Object error, {SdkErrorContext? context}) { + for (final handler in _handlers) { + if (handler.canHandle(error)) { + return handler.handle(error, context: context); + } + } + return const _FallbackHandler().handle(error, context: context); + } +} + +class _SdkErrorPassthroughHandler extends SdkErrorHandler { + const _SdkErrorPassthroughHandler(); + + @override + bool canHandle(Object error) => error is SdkError; + + @override + SdkError handle(Object error, {SdkErrorContext? context}) => + error as SdkError; +} + +class _GeneralErrorResponseHandler extends SdkErrorHandler { + const _GeneralErrorResponseHandler(); + + @override + bool canHandle(Object error) => error is GeneralErrorResponse; + + @override + SdkError handle(Object error, {SdkErrorContext? context}) { + final response = error as GeneralErrorResponse; + final typedException = response.toTypedException(); + if (typedException != null) { + return const _MmRpcExceptionHandler().handle( + typedException, + context: context, + ); + } + + return _build( + code: SdkErrorCode.general, + category: SdkErrorCategory.unknown, + messageKey: _keyGeneral, + fallbackMessage: _fallbackGeneral(response.error ?? response), + detail: response.error, + retryable: false, + context: context, + source: response, + ); + } +} + +class _WithdrawalExceptionHandler extends SdkErrorHandler { + const _WithdrawalExceptionHandler(); + + @override + bool canHandle(Object error) => error is WithdrawalException; + + @override + SdkError handle(Object error, {SdkErrorContext? context}) { + final withdrawalError = error as WithdrawalException; + final detail = _detailFromSimpleMessage(withdrawalError.message); + switch (withdrawalError.code) { + case WithdrawalErrorCode.insufficientFunds: + return _build( + code: SdkErrorCode.insufficientFunds, + category: SdkErrorCategory.funds, + messageKey: _keyInsufficientFunds, + fallbackMessage: _fallbackInsufficientFunds, + detail: detail, + retryable: false, + context: context, + source: withdrawalError, + ); + case WithdrawalErrorCode.invalidAddress: + return _build( + code: SdkErrorCode.invalidAddress, + category: SdkErrorCategory.validation, + messageKey: _keyInvalidAddress, + fallbackMessage: _fallbackInvalidAddress, + detail: detail, + retryable: false, + context: context, + source: withdrawalError, + ); + case WithdrawalErrorCode.networkError: + return _build( + code: SdkErrorCode.networkUnavailable, + category: SdkErrorCategory.network, + messageKey: _keyNetworkUnavailable, + fallbackMessage: _fallbackNetworkUnavailable, + detail: detail, + retryable: true, + context: context, + source: withdrawalError, + ); + case WithdrawalErrorCode.userCancelled: + return _build( + code: SdkErrorCode.userCancelled, + category: SdkErrorCategory.validation, + messageKey: _keyUserCancelled, + fallbackMessage: _fallbackUserCancelled, + detail: detail, + retryable: false, + context: context, + source: withdrawalError, + ); + case WithdrawalErrorCode.gasEstimateFailed: + return _build( + code: SdkErrorCode.insufficientGas, + category: SdkErrorCategory.funds, + messageKey: _keyInsufficientGas, + fallbackMessage: _fallbackInsufficientGas, + detail: detail, + retryable: false, + context: context, + source: withdrawalError, + ); + case WithdrawalErrorCode.transactionFailed: + case WithdrawalErrorCode.contractError: + case WithdrawalErrorCode.unknownError: + return _build( + code: SdkErrorCode.general, + category: SdkErrorCategory.unknown, + messageKey: _keyGeneral, + fallbackMessage: _fallbackGeneral(withdrawalError), + detail: detail, + retryable: false, + context: context, + source: withdrawalError, + ); + } + } +} + +class _AuthExceptionHandler extends SdkErrorHandler { + const _AuthExceptionHandler(); + + @override + bool canHandle(Object error) => error is AuthException; + + @override + SdkError handle(Object error, {SdkErrorContext? context}) { + final authError = error as AuthException; + final detail = _detailFromSimpleMessage(authError.message); + switch (authError.type) { + case AuthExceptionType.incorrectPassword: + return _build( + code: SdkErrorCode.authInvalidCredentials, + category: SdkErrorCategory.auth, + messageKey: _keyAuthInvalidCredentials, + fallbackMessage: _fallbackAuthInvalidCredentials, + detail: detail, + retryable: false, + context: context, + source: authError, + ); + case AuthExceptionType.walletNotFound: + return _build( + code: SdkErrorCode.authWalletNotFound, + category: SdkErrorCategory.auth, + messageKey: _keyAuthWalletNotFound, + fallbackMessage: _fallbackAuthWalletNotFound, + detail: detail, + retryable: false, + context: context, + source: authError, + ); + case AuthExceptionType.unauthorized: + return _build( + code: SdkErrorCode.authUnauthorized, + category: SdkErrorCategory.auth, + messageKey: _keyAuthUnauthorized, + fallbackMessage: _fallbackAuthUnauthorized, + detail: detail, + retryable: false, + context: context, + source: authError, + ); + case AuthExceptionType.apiConnectionError: + return _build( + code: SdkErrorCode.networkUnavailable, + category: SdkErrorCategory.network, + messageKey: _keyNetworkUnavailable, + fallbackMessage: _fallbackNetworkUnavailable, + detail: detail, + retryable: true, + context: context, + source: authError, + ); + case AuthExceptionType.invalidBip39Mnemonic: + case AuthExceptionType.walletAlreadyExists: + case AuthExceptionType.walletAlreadyRunning: + case AuthExceptionType.walletStartFailed: + case AuthExceptionType.generalAuthError: + case AuthExceptionType.alreadySignedIn: + case AuthExceptionType.registrationNotAllowed: + case AuthExceptionType.internalError: + return _build( + code: SdkErrorCode.general, + category: SdkErrorCategory.auth, + messageKey: _keyGeneral, + fallbackMessage: _fallbackGeneral(authError), + detail: detail, + retryable: false, + context: context, + source: authError, + ); + } + } +} + +class _ActivationExceptionHandler extends SdkErrorHandler { + const _ActivationExceptionHandler(); + + @override + bool canHandle(Object error) => error is ActivationFailedException; + + @override + SdkError handle(Object error, {SdkErrorContext? context}) { + final activationError = error as ActivationFailedException; + final detail = _detailFromSimpleMessage(activationError.message); + if (activationError is ActivationTimeoutException) { + return _build( + code: SdkErrorCode.timeout, + category: SdkErrorCategory.network, + messageKey: _keyTimeout, + fallbackMessage: _fallbackTimeout, + detail: detail, + retryable: true, + context: context, + source: activationError, + ); + } + + if (activationError is ActivationNetworkException) { + return _build( + code: SdkErrorCode.networkUnavailable, + category: SdkErrorCategory.network, + messageKey: _keyNetworkUnavailable, + fallbackMessage: _fallbackNetworkUnavailable, + detail: detail, + retryable: true, + context: context, + source: activationError, + ); + } + + if (activationError is ActivationNotSupportedException) { + return _build( + code: SdkErrorCode.notSupported, + category: SdkErrorCategory.unsupported, + messageKey: _keyNotSupported, + fallbackMessage: _fallbackNotSupported, + detail: detail, + retryable: false, + context: context, + source: activationError, + ); + } + + return _build( + code: SdkErrorCode.activationFailed, + category: SdkErrorCategory.activation, + messageKey: _keyActivationFailed, + fallbackMessage: _fallbackActivationFailed, + detail: detail, + retryable: true, + context: context, + source: activationError, + ); + } +} + +class _MmRpcExceptionHandler extends SdkErrorHandler { + const _MmRpcExceptionHandler(); + + @override + bool canHandle(Object error) => error is MmRpcException; + + @override + SdkError handle(Object error, {SdkErrorContext? context}) { + final rpcError = error as MmRpcException; + final detail = _detailFromMmRpcException(rpcError, context); + if (rpcError is WithdrawErrorException) { + return _mapWithdrawError(rpcError, context, detail); + } + + if (rpcError is Web3RpcErrorException) { + return _mapWeb3Error(rpcError, context, detail); + } + + if (rpcError is EthActivationV2ErrorException) { + return _mapEthActivationError(rpcError, context, detail); + } + + if (rpcError is EthTokenActivationErrorException) { + return _mapEthTokenActivationError(rpcError, context, detail); + } + + final fallback = _mapByTypeName(rpcError, context, detail); + if (fallback != null) return fallback; + + log( + 'Unhandled MmRpcException type: ${rpcError.runtimeType} (${rpcError.errorType})', + name: 'SdkErrorMapper', + ); + + return _build( + code: SdkErrorCode.general, + category: SdkErrorCategory.unknown, + messageKey: _keyGeneral, + fallbackMessage: _fallbackGeneral(rpcError), + detail: detail, + retryable: false, + context: context, + source: rpcError, + ); + } + + SdkError _mapWithdrawError( + WithdrawErrorException error, + SdkErrorContext? context, + String? detail, + ) { + final assetId = context?.assetId ?? ''; + switch (error) { + case WithdrawErrorNotSufficientBalanceException(): + return _build( + code: SdkErrorCode.insufficientFunds, + category: SdkErrorCategory.funds, + messageKey: _keyWithdrawNotSufficientBalance, + messageArgs: [ + error.coin, + error.available.value, + error.required.value, + ], + fallbackMessage: _fallbackInsufficientFunds, + detail: detail, + retryable: false, + context: context, + source: error, + ); + case WithdrawErrorNotSufficientPlatformBalanceForFeeException(): + return _build( + code: SdkErrorCode.insufficientGas, + category: SdkErrorCategory.funds, + messageKey: _keyWithdrawNotEnoughBalanceForGas, + messageArgs: [error.coin], + fallbackMessage: _fallbackInsufficientGas, + detail: detail, + retryable: false, + context: context, + source: error, + ); + case WithdrawErrorZeroBalanceToWithdrawMaxException(): + return _build( + code: SdkErrorCode.zeroBalance, + category: SdkErrorCategory.funds, + messageKey: _keyWithdrawZeroBalance, + messageArgs: [assetId], + fallbackMessage: _fallbackZeroBalance, + detail: detail, + retryable: false, + context: context, + source: error, + ); + case WithdrawErrorAmountTooLowException(): + return _build( + code: SdkErrorCode.amountTooLow, + category: SdkErrorCategory.validation, + messageKey: _keyWithdrawAmountTooLow, + messageArgs: [ + error.amount.value, + assetId, + error.threshold.value, + assetId, + ], + fallbackMessage: _fallbackAmountTooLow, + detail: detail, + retryable: false, + context: context, + source: error, + ); + case WithdrawErrorInvalidAddressException(): + return _build( + code: SdkErrorCode.invalidAddress, + category: SdkErrorCategory.validation, + messageKey: _keyInvalidAddressApp, + messageArgs: [assetId], + fallbackMessage: _fallbackInvalidAddress, + detail: detail, + retryable: false, + context: context, + source: error, + ); + case WithdrawErrorInvalidFeePolicyException(): + case WithdrawErrorInvalidFeeException(): + return _build( + code: SdkErrorCode.invalidFee, + category: SdkErrorCategory.validation, + messageKey: _keyInvalidFee, + fallbackMessage: _fallbackInvalidFee, + detail: detail, + retryable: false, + context: context, + source: error, + ); + case WithdrawErrorInvalidMemoException(): + return _build( + code: SdkErrorCode.invalidMemo, + category: SdkErrorCategory.validation, + messageKey: _keyInvalidMemo, + fallbackMessage: _fallbackInvalidMemo, + detail: detail, + retryable: false, + context: context, + source: error, + ); + case WithdrawErrorNoSuchCoinException(): + return _build( + code: SdkErrorCode.assetNotActivated, + category: SdkErrorCategory.activation, + messageKey: _keyWithdrawNoSuchCoin, + messageArgs: [error.coin], + fallbackMessage: _fallbackAssetNotActivated, + detail: detail, + retryable: true, + context: context, + source: error, + ); + case WithdrawErrorTimeoutException(): + return _build( + code: SdkErrorCode.timeout, + category: SdkErrorCategory.network, + messageKey: _keyTimeout, + fallbackMessage: _fallbackTimeout, + detail: detail, + retryable: true, + context: context, + source: error, + ); + case WithdrawErrorTransportException(): + return _build( + code: SdkErrorCode.networkUnavailable, + category: SdkErrorCategory.network, + messageKey: _keyNetworkUnavailable, + fallbackMessage: _fallbackNetworkUnavailable, + detail: detail, + retryable: true, + context: context, + source: error, + ); + case WithdrawErrorUnexpectedUserActionException(): + return _build( + code: SdkErrorCode.userCancelled, + category: SdkErrorCategory.validation, + messageKey: _keyUserCancelled, + fallbackMessage: _fallbackUserCancelled, + detail: detail, + retryable: false, + context: context, + source: error, + ); + case WithdrawErrorHwErrorException(): + return _build( + code: SdkErrorCode.hardwareFailure, + category: SdkErrorCategory.hardware, + messageKey: _keyHardwareFailure, + fallbackMessage: _fallbackHardwareFailure, + detail: detail, + retryable: true, + context: context, + source: error, + ); + case WithdrawErrorCoinDoesntSupportInitWithdrawException(): + case WithdrawErrorCoinDoesntSupportNftWithdrawException(): + case WithdrawErrorContractTypeDoesntSupportNftWithdrawingException(): + case WithdrawErrorNftProtocolNotSupportedException(): + case WithdrawErrorTxTypeNotSupportedException(): + case WithdrawErrorUnsupportedErrorException(): + return _build( + code: SdkErrorCode.notSupported, + category: SdkErrorCategory.unsupported, + messageKey: _keyNotSupported, + fallbackMessage: _fallbackNotSupported, + detail: detail, + retryable: false, + context: context, + source: error, + ); + default: + return _build( + code: SdkErrorCode.general, + category: SdkErrorCategory.unknown, + messageKey: _keyGeneral, + fallbackMessage: _fallbackGeneral(error), + detail: detail, + retryable: false, + context: context, + source: error, + ); + } + } + + SdkError _mapWeb3Error( + Web3RpcErrorException error, + SdkErrorContext? context, + String? detail, + ) { + switch (error) { + case Web3RpcErrorTimeoutException(): + return _build( + code: SdkErrorCode.timeout, + category: SdkErrorCategory.network, + messageKey: _keyTimeout, + fallbackMessage: _fallbackTimeout, + detail: detail, + retryable: true, + context: context, + source: error, + ); + case Web3RpcErrorTransportException(): + return _build( + code: SdkErrorCode.networkUnavailable, + category: SdkErrorCategory.network, + messageKey: _keyNetworkUnavailable, + fallbackMessage: _fallbackNetworkUnavailable, + detail: detail, + retryable: true, + context: context, + source: error, + ); + case Web3RpcErrorInvalidResponseException(): + return _build( + code: SdkErrorCode.invalidResponse, + category: SdkErrorCategory.network, + messageKey: _keyInvalidResponse, + fallbackMessage: _fallbackInvalidResponse, + detail: detail, + retryable: true, + context: context, + source: error, + ); + default: + return _build( + code: SdkErrorCode.general, + category: SdkErrorCategory.unknown, + messageKey: _keyGeneral, + fallbackMessage: _fallbackGeneral(error), + detail: detail, + retryable: false, + context: context, + source: error, + ); + } + } + + SdkError _mapEthActivationError( + EthActivationV2ErrorException error, + SdkErrorContext? context, + String? detail, + ) { + switch (error) { + case EthActivationV2ErrorUnreachableNodesException(): + case EthActivationV2ErrorAtLeastOneNodeRequiredException(): + case EthActivationV2ErrorTransportException(): + return _build( + code: SdkErrorCode.networkUnavailable, + category: SdkErrorCategory.network, + messageKey: _keyNetworkUnavailable, + fallbackMessage: _fallbackNetworkUnavailable, + detail: detail, + retryable: true, + context: context, + source: error, + ); + case EthActivationV2ErrorTaskTimedOutException(): + return _build( + code: SdkErrorCode.timeout, + category: SdkErrorCategory.network, + messageKey: _keyTimeout, + fallbackMessage: _fallbackTimeout, + detail: detail, + retryable: true, + context: context, + source: error, + ); + case EthActivationV2ErrorUnsupportedChainException(): + case EthActivationV2ErrorCoinDoesntSupportTrezorException(): + return _build( + code: SdkErrorCode.notSupported, + category: SdkErrorCategory.unsupported, + messageKey: _keyNotSupported, + fallbackMessage: _fallbackNotSupported, + detail: detail, + retryable: false, + context: context, + source: error, + ); + case EthActivationV2ErrorHwErrorException(): + case EthActivationV2ErrorInvalidHardwareWalletCallException(): + case EthActivationV2ErrorHwContextNotInitializedException(): + return _build( + code: SdkErrorCode.hardwareFailure, + category: SdkErrorCategory.hardware, + messageKey: _keyHardwareFailure, + fallbackMessage: _fallbackHardwareFailure, + detail: detail, + retryable: true, + context: context, + source: error, + ); + case EthActivationV2ErrorPrivKeyPolicyNotAllowedException(): + return _build( + code: SdkErrorCode.authUnauthorized, + category: SdkErrorCategory.auth, + messageKey: _keyAuthUnauthorized, + fallbackMessage: _fallbackAuthUnauthorized, + detail: detail, + retryable: false, + context: context, + source: error, + ); + case EthActivationV2ErrorActivationFailedException(): + case EthActivationV2ErrorCouldNotFetchBalanceException(): + case EthActivationV2ErrorFailedSpawningBalanceEventsException(): + return _build( + code: SdkErrorCode.activationFailed, + category: SdkErrorCategory.activation, + messageKey: _keyActivationFailed, + fallbackMessage: _fallbackActivationFailed, + detail: detail, + retryable: true, + context: context, + source: error, + ); + case EthActivationV2ErrorInvalidPayloadException(): + case EthActivationV2ErrorInvalidSwapContractAddrException(): + case EthActivationV2ErrorInvalidFallbackSwapContractException(): + case EthActivationV2ErrorInvalidPathToAddressException(): + case EthActivationV2ErrorErrorDeserializingDerivationPathException(): + case EthActivationV2ErrorUnexpectedDerivationMethodException(): + case EthActivationV2ErrorChainIdNotSetException(): + return _build( + code: SdkErrorCode.invalidResponse, + category: SdkErrorCategory.validation, + messageKey: _keyInvalidResponse, + fallbackMessage: _fallbackInvalidResponse, + detail: detail, + retryable: false, + context: context, + source: error, + ); + default: + return _build( + code: SdkErrorCode.general, + category: SdkErrorCategory.unknown, + messageKey: _keyGeneral, + fallbackMessage: _fallbackGeneral(error), + detail: detail, + retryable: false, + context: context, + source: error, + ); + } + } + + SdkError _mapEthTokenActivationError( + EthTokenActivationErrorException error, + SdkErrorContext? context, + String? detail, + ) { + switch (error) { + case EthTokenActivationErrorClientConnectionFailedException(): + case EthTokenActivationErrorTransportException(): + return _build( + code: SdkErrorCode.networkUnavailable, + category: SdkErrorCategory.network, + messageKey: _keyNetworkUnavailable, + fallbackMessage: _fallbackNetworkUnavailable, + detail: detail, + retryable: true, + context: context, + source: error, + ); + case EthTokenActivationErrorCouldNotFetchBalanceException(): + return _build( + code: SdkErrorCode.activationFailed, + category: SdkErrorCategory.activation, + messageKey: _keyActivationFailed, + fallbackMessage: _fallbackActivationFailed, + detail: detail, + retryable: true, + context: context, + source: error, + ); + case EthTokenActivationErrorInvalidPayloadException(): + return _build( + code: SdkErrorCode.invalidResponse, + category: SdkErrorCategory.validation, + messageKey: _keyInvalidResponse, + fallbackMessage: _fallbackInvalidResponse, + detail: detail, + retryable: false, + context: context, + source: error, + ); + case EthTokenActivationErrorPrivKeyPolicyNotAllowedException(): + case EthTokenActivationErrorUnexpectedDerivationMethodException(): + return _build( + code: SdkErrorCode.authUnauthorized, + category: SdkErrorCategory.auth, + messageKey: _keyAuthUnauthorized, + fallbackMessage: _fallbackAuthUnauthorized, + detail: detail, + retryable: false, + context: context, + source: error, + ); + default: + return _build( + code: SdkErrorCode.general, + category: SdkErrorCategory.unknown, + messageKey: _keyGeneral, + fallbackMessage: _fallbackGeneral(error), + detail: detail, + retryable: false, + context: context, + source: error, + ); + } + } + + SdkError? _mapByTypeName( + MmRpcException error, + SdkErrorContext? context, + String? detail, + ) { + final typeName = + '${error.runtimeType} ${error.errorType} ${error.message ?? ''}' + .toLowerCase(); + if (_containsAny(typeName, ['timeout', 'timedout'])) { + return _build( + code: SdkErrorCode.timeout, + category: SdkErrorCategory.network, + messageKey: _keyTimeout, + fallbackMessage: _fallbackTimeout, + detail: detail, + retryable: true, + context: context, + source: error, + ); + } + + if (_containsAny(typeName, ['transport', 'unreachable', 'connection'])) { + return _build( + code: SdkErrorCode.networkUnavailable, + category: SdkErrorCategory.network, + messageKey: _keyNetworkUnavailable, + fallbackMessage: _fallbackNetworkUnavailable, + detail: detail, + retryable: true, + context: context, + source: error, + ); + } + + if (_containsAny(typeName, ['insufficient', 'notsufficient'])) { + return _build( + code: _containsAny(typeName, ['fee', 'gas']) + ? SdkErrorCode.insufficientGas + : SdkErrorCode.insufficientFunds, + category: SdkErrorCategory.funds, + messageKey: _containsAny(typeName, ['fee', 'gas']) + ? _keyInsufficientGas + : _keyInsufficientFunds, + fallbackMessage: _containsAny(typeName, ['fee', 'gas']) + ? _fallbackInsufficientGas + : _fallbackInsufficientFunds, + detail: detail, + retryable: false, + context: context, + source: error, + ); + } + + if (typeName.contains('invalidaddress')) { + return _build( + code: SdkErrorCode.invalidAddress, + category: SdkErrorCategory.validation, + messageKey: _keyInvalidAddress, + fallbackMessage: _fallbackInvalidAddress, + detail: detail, + retryable: false, + context: context, + source: error, + ); + } + + if (_containsAny(typeName, ['invalidfee', 'invalidfeepolicy'])) { + return _build( + code: SdkErrorCode.invalidFee, + category: SdkErrorCategory.validation, + messageKey: _keyInvalidFee, + fallbackMessage: _fallbackInvalidFee, + detail: detail, + retryable: false, + context: context, + source: error, + ); + } + + if (typeName.contains('invalidmemo')) { + return _build( + code: SdkErrorCode.invalidMemo, + category: SdkErrorCategory.validation, + messageKey: _keyInvalidMemo, + fallbackMessage: _fallbackInvalidMemo, + detail: detail, + retryable: false, + context: context, + source: error, + ); + } + + if (_containsAny(typeName, ['amounttoolow', 'dust'])) { + return _build( + code: SdkErrorCode.amountTooLow, + category: SdkErrorCategory.validation, + messageKey: _keyAmountTooLow, + fallbackMessage: _fallbackAmountTooLow, + detail: detail, + retryable: false, + context: context, + source: error, + ); + } + + if (typeName.contains('zerobalance')) { + return _build( + code: SdkErrorCode.zeroBalance, + category: SdkErrorCategory.funds, + messageKey: _keyZeroBalance, + fallbackMessage: _fallbackZeroBalance, + detail: detail, + retryable: false, + context: context, + source: error, + ); + } + + if (_containsAny(typeName, ['nosuchcoin', 'coinisnotfound'])) { + return _build( + code: SdkErrorCode.assetNotActivated, + category: SdkErrorCategory.activation, + messageKey: _keyAssetNotActivated, + fallbackMessage: _fallbackAssetNotActivated, + detail: detail, + retryable: true, + context: context, + source: error, + ); + } + + if (_containsAny(typeName, [ + 'notsupported', + 'doesntsupport', + 'unsupported', + ])) { + return _build( + code: SdkErrorCode.notSupported, + category: SdkErrorCategory.unsupported, + messageKey: _keyNotSupported, + fallbackMessage: _fallbackNotSupported, + detail: detail, + retryable: false, + context: context, + source: error, + ); + } + + if (typeName.contains('activationfailed')) { + return _build( + code: SdkErrorCode.activationFailed, + category: SdkErrorCategory.activation, + messageKey: _keyActivationFailed, + fallbackMessage: _fallbackActivationFailed, + detail: detail, + retryable: true, + context: context, + source: error, + ); + } + + if (_containsAny(typeName, ['useraction', 'cancelled', 'canceled'])) { + return _build( + code: SdkErrorCode.userCancelled, + category: SdkErrorCategory.validation, + messageKey: _keyUserCancelled, + fallbackMessage: _fallbackUserCancelled, + detail: detail, + retryable: false, + context: context, + source: error, + ); + } + + if (_containsAny(typeName, ['hw', 'hardware', 'trezor'])) { + return _build( + code: SdkErrorCode.hardwareFailure, + category: SdkErrorCategory.hardware, + messageKey: _keyHardwareFailure, + fallbackMessage: _fallbackHardwareFailure, + detail: detail, + retryable: true, + context: context, + source: error, + ); + } + + if (_containsAny(typeName, ['unauthorized', 'privkeypolicynotallowed'])) { + return _build( + code: SdkErrorCode.authUnauthorized, + category: SdkErrorCategory.auth, + messageKey: _keyAuthUnauthorized, + fallbackMessage: _fallbackAuthUnauthorized, + detail: detail, + retryable: false, + context: context, + source: error, + ); + } + + return null; + } +} + +class _TimeoutExceptionHandler extends SdkErrorHandler { + const _TimeoutExceptionHandler(); + + @override + bool canHandle(Object error) => error is TimeoutException; + + @override + SdkError handle(Object error, {SdkErrorContext? context}) { + final detail = _detailFromSimpleMessage(error.toString()); + return _build( + code: SdkErrorCode.timeout, + category: SdkErrorCategory.network, + messageKey: _keyTimeout, + fallbackMessage: _fallbackTimeout, + detail: detail, + retryable: true, + context: context, + source: error, + ); + } +} + +class _UnsupportedErrorHandler extends SdkErrorHandler { + const _UnsupportedErrorHandler(); + + @override + bool canHandle(Object error) => error is UnsupportedError; + + @override + SdkError handle(Object error, {SdkErrorContext? context}) { + final detail = _detailFromSimpleMessage(error.toString()); + return _build( + code: SdkErrorCode.notSupported, + category: SdkErrorCategory.unsupported, + messageKey: _keyNotSupported, + fallbackMessage: _fallbackNotSupported, + detail: detail, + retryable: false, + context: context, + source: error, + ); + } +} + +class _StringErrorHandler extends SdkErrorHandler { + const _StringErrorHandler(); + + @override + bool canHandle(Object error) => error is String; + + @override + SdkError handle(Object error, {SdkErrorContext? context}) { + final message = error as String; + final detail = _detailFromSimpleMessage(message); + final lower = message.toLowerCase(); + if (lower.contains('timeout')) { + return _build( + code: SdkErrorCode.timeout, + category: SdkErrorCategory.network, + messageKey: _keyTimeout, + fallbackMessage: _fallbackTimeout, + detail: detail, + retryable: true, + context: context, + source: error, + ); + } + + if (_containsAny(lower, ['transport', 'unreachable', 'connection'])) { + return _build( + code: SdkErrorCode.networkUnavailable, + category: SdkErrorCategory.network, + messageKey: _keyNetworkUnavailable, + fallbackMessage: _fallbackNetworkUnavailable, + detail: detail, + retryable: true, + context: context, + source: error, + ); + } + + if (_containsAny(lower, ['insufficient', 'not enough funds'])) { + return _build( + code: SdkErrorCode.insufficientFunds, + category: SdkErrorCategory.funds, + messageKey: _keyInsufficientFunds, + fallbackMessage: _fallbackInsufficientFunds, + detail: detail, + retryable: false, + context: context, + source: error, + ); + } + + if (lower.contains('invalid address')) { + return _build( + code: SdkErrorCode.invalidAddress, + category: SdkErrorCategory.validation, + messageKey: _keyInvalidAddress, + fallbackMessage: _fallbackInvalidAddress, + detail: detail, + retryable: false, + context: context, + source: error, + ); + } + + if (lower.contains('fee')) { + return _build( + code: SdkErrorCode.invalidFee, + category: SdkErrorCategory.validation, + messageKey: _keyInvalidFee, + fallbackMessage: _fallbackInvalidFee, + detail: detail, + retryable: false, + context: context, + source: error, + ); + } + + if (_containsAny(lower, ['user cancelled', 'user canceled'])) { + return _build( + code: SdkErrorCode.userCancelled, + category: SdkErrorCategory.validation, + messageKey: _keyUserCancelled, + fallbackMessage: _fallbackUserCancelled, + detail: detail, + retryable: false, + context: context, + source: error, + ); + } + + return const _FallbackHandler().handle(error, context: context); + } +} + +class _FallbackHandler extends SdkErrorHandler { + const _FallbackHandler(); + + @override + bool canHandle(Object error) => true; + + @override + SdkError handle(Object error, {SdkErrorContext? context}) { + final detail = _detailFromSimpleMessage(error.toString()); + return _build( + code: SdkErrorCode.general, + category: SdkErrorCategory.unknown, + messageKey: _keyGeneral, + fallbackMessage: _fallbackGeneral(error), + detail: detail, + retryable: false, + context: context, + source: error, + ); + } +} + +SdkError _build({ + required SdkErrorCode code, + required SdkErrorCategory category, + required String messageKey, + required String fallbackMessage, + String? detail, + List messageArgs = const [], + required bool retryable, + SdkErrorContext? context, + Object? source, +}) { + final detailSuffix = _detailSuffix(detail); + final resolvedArgs = messageArgs.isNotEmpty + ? messageArgs + : [detailSuffix ?? '']; + final detailValue = detail?.trim(); + final shouldAppendDetail = + detailSuffix != null && + detailValue != null && + detailValue.isNotEmpty && + !fallbackMessage.contains(detailValue); + final resolvedFallback = shouldAppendDetail + ? '$fallbackMessage$detailSuffix' + : fallbackMessage; + return SdkError( + code: code, + category: category, + messageKey: messageKey, + fallbackMessage: resolvedFallback, + messageArgs: resolvedArgs, + retryable: retryable, + context: context, + source: source, + ); +} + +String? _detailFromMmRpcException( + MmRpcException error, + SdkErrorContext? context, +) { + switch (error) { + case WithdrawErrorNotSufficientBalanceException(): + final coin = error.coin; + return 'available ${_formatAmount(error.available.value, coin)}, ' + 'required ${_formatAmount(error.required.value, coin)}'; + case WithdrawErrorCoinDoesntSupportInitWithdrawException(): + return _detailFromSimpleMessage(error.coin); + case WithdrawErrorCoinDoesntSupportNftWithdrawException(): + return _detailFromSimpleMessage(error.coin); + case WithdrawErrorNotSufficientPlatformBalanceForFeeException(): + final coin = error.coin; + return 'available ${_formatAmount(error.available.value, coin)}, ' + 'required ${_formatAmount(error.required.value, coin)}'; + case WithdrawErrorAmountTooLowException(): + final coin = context?.assetId; + return 'amount ${_formatAmount(error.amount.value, coin)}, ' + 'min ${_formatAmount(error.threshold.value, coin)}'; + case WithdrawErrorUnexpectedFromAddressException(): + return _detailFromSimpleMessage(error.value); + case WithdrawErrorUnknownAccountException(): + return _detailFromSimpleMessage('account ${error.accountId}'); + case WithdrawErrorUnexpectedUserActionException(): + return _detailFromSimpleMessage('expected ${error.expected}'); + case WithdrawErrorInvalidAddressException(): + return _detailFromSimpleMessage(error.value); + case WithdrawErrorInvalidFeePolicyException(): + return _detailFromSimpleMessage(error.value); + case WithdrawErrorInvalidFeeException(): + final detail = [ + error.reason, + if (error.details?.value != null) error.details!.value.toString(), + ].where((item) => item.trim().isNotEmpty).join(': '); + return _detailFromSimpleMessage(detail); + case WithdrawErrorInvalidMemoException(): + return _detailFromSimpleMessage(error.value); + case WithdrawErrorNoSuchCoinException(): + return _detailFromSimpleMessage(error.coin); + case WithdrawErrorBroadcastExpectedException(): + return _detailFromSimpleMessage(error.value); + case WithdrawErrorTransportException(): + return _detailFromSimpleMessage(error.value); + case WithdrawErrorInternalErrorException(): + return _detailFromSimpleMessage(error.value); + case WithdrawErrorUnsupportedErrorException(): + return _detailFromSimpleMessage(error.value); + case WithdrawErrorContractTypeDoesntSupportNftWithdrawingException(): + return _detailFromSimpleMessage(error.value); + case WithdrawErrorActionNotAllowedException(): + return _detailFromSimpleMessage(error.value); + case WithdrawErrorGetNftInfoErrorException(): + return _detailFromSimpleMessage(error.value.toString()); + case WithdrawErrorNotEnoughNftsAmountException(): + return _detailFromSimpleMessage( + 'token ${error.tokenAddress} #${error.tokenId}, ' + 'available ${error.available.value}, required ${error.required.value}', + ); + case WithdrawErrorDbErrorException(): + return _detailFromSimpleMessage(error.value); + case WithdrawErrorMyAddressNotNftOwnerException(): + return _detailFromSimpleMessage( + 'owner ${error.tokenOwner}, yours ${error.myAddress}', + ); + case WithdrawErrorNoChainIdSetException(): + return _detailFromSimpleMessage(error.coin); + case WithdrawErrorSigningErrorException(): + return _detailFromSimpleMessage(error.value); + case WithdrawErrorIBCErrorException(): + return _detailFromSimpleMessage(error.value.toString()); + case WithdrawErrorZeroBalanceToWithdrawMaxException(): + return _detailFromSimpleMessage(context?.assetId); + case WithdrawErrorTimeoutException(): + return _detailFromSimpleMessage(_formatDuration(error.value.value)); + case Web3RpcErrorTimeoutException(): + return _detailFromSimpleMessage(error.value); + case Web3RpcErrorTransportException(): + return _detailFromSimpleMessage(error.value); + case Web3RpcErrorInvalidResponseException(): + return _detailFromSimpleMessage(error.value); + case Web3RpcErrorInvalidGasApiConfigException(): + return _detailFromSimpleMessage(error.value); + case Web3RpcErrorNumConversErrorException(): + return _detailFromSimpleMessage(error.value); + case EthActivationV2ErrorActivationFailedException(): + return _detailFromSimpleMessage('${error.ticker}: ${error.error}'); + case EthActivationV2ErrorUnsupportedChainException(): + return _detailFromSimpleMessage('${error.chain} (${error.feature})'); + case EthActivationV2ErrorTaskTimedOutException(): + return _detailFromSimpleMessage(_formatDuration(error.duration.value)); + case EthActivationV2ErrorInvalidPayloadException(): + case EthActivationV2ErrorInvalidSwapContractAddrException(): + case EthActivationV2ErrorInvalidFallbackSwapContractException(): + case EthActivationV2ErrorInvalidPathToAddressException(): + case EthActivationV2ErrorCouldNotFetchBalanceException(): + case EthActivationV2ErrorUnreachableNodesException(): + case EthActivationV2ErrorErrorDeserializingDerivationPathException(): + case EthActivationV2ErrorPrivKeyPolicyNotAllowedException(): + case EthActivationV2ErrorFailedSpawningBalanceEventsException(): + case EthActivationV2ErrorHDWalletStorageErrorException(): + case EthActivationV2ErrorInternalErrorException(): + case EthActivationV2ErrorTransportException(): + case EthActivationV2ErrorUnexpectedDerivationMethodException(): + case EthActivationV2ErrorWalletConnectErrorException(): + // All of these error types carry a "value" string in the generated class. + // ignore: avoid_dynamic_calls + return _detailFromSimpleMessage((error as dynamic).value?.toString()); + case EthActivationV2ErrorMetamaskErrorException(): + case EthActivationV2ErrorHwErrorException(): + case EthActivationV2ErrorCustomTokenErrorException(): + // These errors carry structured values; stringify for display. + // ignore: avoid_dynamic_calls + return _detailFromSimpleMessage((error as dynamic).value?.toString()); + case EthTokenActivationErrorInternalErrorException(): + case EthTokenActivationErrorClientConnectionFailedException(): + case EthTokenActivationErrorCouldNotFetchBalanceException(): + case EthTokenActivationErrorInvalidPayloadException(): + case EthTokenActivationErrorTransportException(): + // ignore: avoid_dynamic_calls + return _detailFromSimpleMessage((error as dynamic).value?.toString()); + case EthTokenActivationErrorUnexpectedDerivationMethodException(): + case EthTokenActivationErrorPrivKeyPolicyNotAllowedException(): + case EthTokenActivationErrorCustomTokenErrorException(): + // ignore: avoid_dynamic_calls + return _detailFromSimpleMessage((error as dynamic).value?.toString()); + case AddressDerivingErrorInvalidBip44ChainException(): + return _detailFromSimpleMessage(error.chain.toJson()); + case AddressDerivingErrorBip32ErrorException(): + case AddressDerivingErrorInternalException(): + // ignore: avoid_dynamic_calls + return _detailFromSimpleMessage((error as dynamic).value?.toString()); + default: + final message = _detailFromSimpleMessage(error.message); + if (message != null) return message; + try { + // Some generated error types expose a `value` field instead of `message`. + // ignore: avoid_dynamic_calls + return _detailFromSimpleMessage((error as dynamic).value?.toString()); + } catch (_) { + return null; + } + } +} + +String? _detailFromSimpleMessage(String? message) { + if (message == null) return null; + final trimmed = message.trim(); + return trimmed.isEmpty ? null : trimmed; +} + +String? _detailSuffix(String? detail) { + if (detail == null) return null; + final trimmed = detail.trim(); + if (trimmed.isEmpty) return null; + return ' ($trimmed)'; +} + +String _formatAmount(String value, String? coin) { + if (coin == null || coin.trim().isEmpty) { + return value; + } + return '$value $coin'; +} + +String _formatDuration(Duration duration) { + if (duration.inSeconds > 0) { + return '${duration.inSeconds}s'; + } + return '${duration.inMilliseconds}ms'; +} + +bool _containsAny(String source, List needles) => + needles.any(source.contains); + +const String _keyNetworkUnavailable = 'sdk_errors.network_unavailable'; +const String _keyTimeout = 'sdk_errors.timeout'; +const String _keyInvalidResponse = 'sdk_errors.invalid_response'; +const String _keyInsufficientFunds = 'sdk_errors.insufficient_funds'; +const String _keyInsufficientGas = 'sdk_errors.insufficient_gas'; +const String _keyZeroBalance = 'sdk_errors.zero_balance'; +const String _keyAmountTooLow = 'sdk_errors.amount_too_low'; +const String _keyInvalidAddress = 'sdk_errors.invalid_address'; +const String _keyInvalidAddressApp = 'invalidAddress'; +const String _keyWithdrawNotSufficientBalance = + 'withdrawNotSufficientBalanceError'; +const String _keyWithdrawNotEnoughBalanceForGas = + 'withdrawNotEnoughBalanceForGasError'; +const String _keyWithdrawZeroBalance = 'withdrawZeroBalanceError'; +const String _keyWithdrawAmountTooLow = 'withdrawAmountTooLowError'; +const String _keyWithdrawNoSuchCoin = 'withdrawNoSuchCoinError'; +const String _keyInvalidFee = 'sdk_errors.invalid_fee'; +const String _keyInvalidMemo = 'sdk_errors.invalid_memo'; +const String _keyAssetNotActivated = 'sdk_errors.asset_not_activated'; +const String _keyActivationFailed = 'sdk_errors.activation_failed'; +const String _keyUserCancelled = 'sdk_errors.user_cancelled'; +const String _keyHardwareFailure = 'sdk_errors.hardware_failure'; +const String _keyNotSupported = 'sdk_errors.not_supported'; +const String _keyAuthInvalidCredentials = 'sdk_errors.auth_invalid_credentials'; +const String _keyAuthUnauthorized = 'sdk_errors.auth_unauthorized'; +const String _keyAuthWalletNotFound = 'sdk_errors.auth_wallet_not_found'; +const String _keyGeneral = 'sdk_errors.general'; + +const String _fallbackNetworkUnavailable = + 'Network error. Please check your connection and try again.'; +const String _fallbackTimeout = 'The request timed out. Please try again.'; +const String _fallbackInvalidResponse = + 'Unexpected response from the network. Please try again.'; +const String _fallbackInsufficientFunds = + 'Insufficient balance to complete this action.'; +const String _fallbackInsufficientGas = + 'Insufficient balance to pay network fees.'; +const String _fallbackZeroBalance = + 'Your balance is zero. Please deposit funds and try again.'; +const String _fallbackAmountTooLow = + 'The amount is too low. Please increase the amount and try again.'; +const String _fallbackInvalidAddress = + 'The address is invalid. Please check and try again.'; +const String _fallbackInvalidFee = + 'The fee value is invalid. Please review and try again.'; +const String _fallbackInvalidMemo = + 'The memo is invalid. Please review and try again.'; +const String _fallbackAssetNotActivated = + 'The asset is not activated or is unavailable. Please enable it and try again.'; +const String _fallbackActivationFailed = 'Activation failed. Please try again.'; +const String _fallbackUserCancelled = 'Action cancelled by user.'; +const String _fallbackHardwareFailure = + 'Hardware wallet operation failed. Please try again.'; +const String _fallbackNotSupported = + 'This action is not supported for the selected asset.'; +const String _fallbackAuthInvalidCredentials = + 'Invalid credentials. Please check your password.'; +const String _fallbackAuthUnauthorized = + 'Authorization failed. Please sign in again.'; +const String _fallbackAuthWalletNotFound = + 'Wallet not found. Please verify the wallet name.'; + +String _fallbackGeneral(Object error) => + 'Something went wrong. Please try again. ($error)'; diff --git a/packages/komodo_defi_sdk/lib/src/fees/fee_manager.dart b/packages/komodo_defi_sdk/lib/src/fees/fee_manager.dart index b42faf8a9..155dbd24a 100644 --- a/packages/komodo_defi_sdk/lib/src/fees/fee_manager.dart +++ b/packages/komodo_defi_sdk/lib/src/fees/fee_manager.dart @@ -1,3 +1,4 @@ +import 'package:komodo_defi_sdk/src/errors/sdk_error_mapper.dart'; import 'package:komodo_defi_types/komodo_defi_types.dart'; /// Manages cryptocurrency transaction fee operations and policies. @@ -53,6 +54,7 @@ class FeeManager { static const bool _feeEstimationEnabled = false; final ApiClient _client; + static const SdkErrorMapper _errorMapper = SdkErrorMapper(); /// Enable fee estimator for a specific coin. /// @@ -71,11 +73,15 @@ class FeeManager { /// print('Fee estimator enabled: $result'); /// ``` Future enableFeeEstimator(String coin, String estimatorType) async { - final response = await _client.rpc.feeManagement.feeEstimatorEnable( - coin: coin, - estimatorType: estimatorType, - ); - return response.result; + try { + final response = await _client.rpc.feeManagement.feeEstimatorEnable( + coin: coin, + estimatorType: estimatorType, + ); + return response.result; + } catch (e) { + throw _mapError(e, operation: 'fee_estimator.enable', assetId: coin); + } } /// Retrieves estimated fee per gas for Ethereum-based transactions. @@ -117,18 +123,17 @@ class FeeManager { String coin, { FeeEstimatorType estimatorType = FeeEstimatorType.simple, }) async { - if (!_feeEstimationEnabled) { - throw UnsupportedError( - 'Fee estimation is currently disabled. The API endpoints are not yet available. ' - 'Set `_feeEstimationEnabled` to `true` when the endpoints become available.', + _throwIfFeeEstimationDisabled(); + + try { + final response = await _client.rpc.feeManagement.getEthEstimatedFeePerGas( + coin: coin, + estimatorType: estimatorType, ); + return response.result; + } catch (e) { + throw _mapError(e, operation: 'fee.estimate.eth', assetId: coin); } - - final response = await _client.rpc.feeManagement.getEthEstimatedFeePerGas( - coin: coin, - estimatorType: estimatorType, - ); - return response.result; } /// Retrieves estimated fees for UTXO-based transactions (Bitcoin, Litecoin, etc.). @@ -170,18 +175,17 @@ class FeeManager { String coin, { FeeEstimatorType estimatorType = FeeEstimatorType.simple, }) async { - if (!_feeEstimationEnabled) { - throw UnsupportedError( - 'Fee estimation is currently disabled. The API endpoints are not yet available. ' - 'Set `_feeEstimationEnabled` to `true` when the endpoints become available.', + _throwIfFeeEstimationDisabled(); + + try { + final response = await _client.rpc.feeManagement.getUtxoEstimatedFee( + coin: coin, + estimatorType: estimatorType, ); + return response.result; + } catch (e) { + throw _mapError(e, operation: 'fee.estimate.utxo', assetId: coin); } - - final response = await _client.rpc.feeManagement.getUtxoEstimatedFee( - coin: coin, - estimatorType: estimatorType, - ); - return response.result; } /// Retrieves estimated fees for Tendermint/Cosmos-based transactions. @@ -227,18 +231,15 @@ class FeeManager { String coin, { FeeEstimatorType estimatorType = FeeEstimatorType.simple, }) async { - if (!_feeEstimationEnabled) { - throw UnsupportedError( - 'Fee estimation is currently disabled. The API endpoints are not yet available. ' - 'Set `_feeEstimationEnabled` to `true` when the endpoints become available.', - ); - } + _throwIfFeeEstimationDisabled(); - final response = await _client.rpc.feeManagement.getTendermintEstimatedFee( - coin: coin, - estimatorType: estimatorType, - ); - return response.result; + try { + final response = await _client.rpc.feeManagement + .getTendermintEstimatedFee(coin: coin, estimatorType: estimatorType); + return response.result; + } catch (e) { + throw _mapError(e, operation: 'fee.estimate.tendermint', assetId: coin); + } } /// Retrieves the current fee policy for swap transactions of a specific coin. @@ -261,9 +262,13 @@ class FeeManager { /// } /// ``` Future getSwapTransactionFeePolicy(String coin) async { - final response = await _client.rpc.feeManagement - .getSwapTransactionFeePolicy(coin: coin); - return response.result; + try { + final response = await _client.rpc.feeManagement + .getSwapTransactionFeePolicy(coin: coin); + return response.result; + } catch (e) { + throw _mapError(e, operation: 'fee.policy.get', assetId: coin); + } } /// Sets a new fee policy for swap transactions of a specific coin. @@ -291,9 +296,33 @@ class FeeManager { String coin, FeePolicy policy, ) async { - final response = await _client.rpc.feeManagement - .setSwapTransactionFeePolicy(coin: coin, swapTxFeePolicy: policy); - return response.result; + try { + final response = await _client.rpc.feeManagement + .setSwapTransactionFeePolicy(coin: coin, swapTxFeePolicy: policy); + return response.result; + } catch (e) { + throw _mapError(e, operation: 'fee.policy.set', assetId: coin); + } + } + + SdkError _mapError( + Object error, { + required String operation, + String? assetId, + }) { + return _errorMapper.map( + error, + context: SdkErrorContext(operation: operation, assetId: assetId), + ); + } + + void _throwIfFeeEstimationDisabled() { + if (_feeEstimationEnabled) return; + + throw UnsupportedError( + 'Fee estimation is currently disabled. The API endpoints are not yet available. ' + 'Set `_feeEstimationEnabled` to `true` when the endpoints become available.', + ); } /// Disposes of resources used by the FeeManager. diff --git a/packages/komodo_defi_sdk/lib/src/komodo_defi_sdk.dart b/packages/komodo_defi_sdk/lib/src/komodo_defi_sdk.dart index e3273e232..bf2b9efad 100644 --- a/packages/komodo_defi_sdk/lib/src/komodo_defi_sdk.dart +++ b/packages/komodo_defi_sdk/lib/src/komodo_defi_sdk.dart @@ -285,6 +285,10 @@ class KomodoDefiSdk with SecureRpcPasswordMixin { /// Provides access to fee management utilities. FeeManager get fees => _assertSdkInitialized(_container()); + /// Provides high-level trading helpers and stream-first watchers. + TradingManager get trading => + _assertSdkInitialized(_container()); + /// Gets a reference to the balance manager for checking asset balances. /// /// Provides functionality for checking and monitoring asset balances. @@ -306,6 +310,35 @@ class KomodoDefiSdk with SecureRpcPasswordMixin { KdfEventStreamingService get streaming => _assertSdkInitialized(_container().streaming); + /// Subscribes to a managed orderbook stream for a trading pair. + /// + /// This uses the SDK's internal stream lifecycle manager with reference + /// counting and automatic `stream::disable` cleanup when the last + /// subscription is cancelled. + Future> subscribeToOrderbook({ + required String base, + required String rel, + }) { + final manager = _assertSdkInitialized(_container()); + return manager.subscribeToOrderbook(base: base, rel: rel); + } + + /// Subscribes to managed swap status updates. + /// + /// The subscription is reference-counted across all callers. + Future> subscribeToSwapStatus() { + final manager = _assertSdkInitialized(_container()); + return manager.subscribeToSwapStatus(); + } + + /// Subscribes to managed order status updates. + /// + /// The subscription is reference-counted across all callers. + Future> subscribeToOrderStatus() { + final manager = _assertSdkInitialized(_container()); + return manager.subscribeToOrderStatus(); + } + /// Public stream of framework logs. /// /// Subscribe to receive human-readable log messages from the underlying @@ -510,9 +543,7 @@ class KomodoDefiSdk with SecureRpcPasswordMixin { _disposeIfRegistered((m) => m.dispose()), _disposeIfRegistered((m) => m.dispose()), _disposeIfRegistered((m) => m.dispose()), - _disposeIfRegistered( - (m) async => m.dispose(), - ), + _disposeIfRegistered((m) async => m.dispose()), _disposeIfRegistered((m) => m.dispose()), _disposeIfRegistered((m) => m.dispose()), _disposeIfRegistered((m) => m.dispose()), diff --git a/packages/komodo_defi_sdk/lib/src/pubkeys/pubkey_manager.dart b/packages/komodo_defi_sdk/lib/src/pubkeys/pubkey_manager.dart index c25b6cbf6..ca848177a 100644 --- a/packages/komodo_defi_sdk/lib/src/pubkeys/pubkey_manager.dart +++ b/packages/komodo_defi_sdk/lib/src/pubkeys/pubkey_manager.dart @@ -2,11 +2,10 @@ import 'dart:async'; import 'package:komodo_defi_local_auth/komodo_defi_local_auth.dart'; import 'package:komodo_defi_sdk/src/_internal_exports.dart'; -import 'package:komodo_defi_sdk/src/activation/activation_exceptions.dart'; +import 'package:komodo_defi_sdk/src/pubkeys/pubkeys_storage.dart'; import 'package:komodo_defi_types/komodo_defi_type_utils.dart'; import 'package:komodo_defi_types/komodo_defi_types.dart'; import 'package:logging/logging.dart'; -import 'package:komodo_defi_sdk/src/pubkeys/pubkeys_storage.dart'; /// Interface defining the contract for pubkey management operations abstract class IPubkeyManager { @@ -67,6 +66,8 @@ class PubkeyManager implements IPubkeyManager { final Map _watchedAssets = {}; // Deduplicate concurrent getPubkeys requests per asset final Map> _inFlightPubkeyRequests = {}; + final Map _hdAddressScanRetryAfter = {}; + static const Duration _hdAddressScanRetryCooldown = Duration(minutes: 2); StreamSubscription? _authSubscription; WalletId? _currentWalletId; @@ -173,6 +174,11 @@ class PubkeyManager implements IPubkeyManager { final future = () async { await retry(() => _activationCoordinator.activateAsset(asset)); final strategy = await _resolvePubkeyStrategy(asset); + await _scanForNewHdAddressesIfNeeded( + walletId: walletId, + asset: asset, + strategy: strategy, + ); final pubkeys = await strategy.getPubkeys(asset.id, _client); _pubkeysCache[asset.id] = pubkeys; _persistPubkeysForWallet(walletId, asset, pubkeys).ignore(); @@ -447,6 +453,37 @@ class PubkeyManager implements IPubkeyManager { } } + Future _scanForNewHdAddressesIfNeeded({ + required WalletId walletId, + required Asset asset, + required PubkeyStrategy strategy, + }) async { + if (!strategy.supportsMultipleAddresses) { + return; + } + + final scanKey = '${walletId.name}:${asset.id.id}'; + final retryAfter = _hdAddressScanRetryAfter[scanKey]; + if (retryAfter != null && DateTime.now().isBefore(retryAfter)) { + return; + } + + try { + await strategy.scanForNewAddresses(asset.id, _client); + _hdAddressScanRetryAfter.remove(scanKey); + } catch (error, stackTrace) { + _hdAddressScanRetryAfter[scanKey] = DateTime.now().add( + _hdAddressScanRetryCooldown, + ); + _logger.warning( + 'HD address scan failed for ${asset.id.name}; continuing with ' + 'existing pubkeys', + error, + stackTrace, + ); + } + } + /// Called when authentication state changes to do the following: /// - clear active watchers by canceling all subscriptions /// - close all controllers after indicating disconnection with state error @@ -507,6 +544,7 @@ class PubkeyManager implements IPubkeyManager { // Clear caches _pubkeysCache.clear(); _inFlightPubkeyRequests.clear(); + _hdAddressScanRetryAfter.clear(); stopwatch.stop(); _logger.fine( @@ -563,6 +601,7 @@ class PubkeyManager implements IPubkeyManager { } _pubkeysCache.clear(); + _hdAddressScanRetryAfter.clear(); _watchedAssets.clear(); _currentWalletId = null; _logger.fine('Disposed'); diff --git a/packages/komodo_defi_sdk/lib/src/trading/trading_manager.dart b/packages/komodo_defi_sdk/lib/src/trading/trading_manager.dart new file mode 100644 index 000000000..de9a03330 --- /dev/null +++ b/packages/komodo_defi_sdk/lib/src/trading/trading_manager.dart @@ -0,0 +1,422 @@ +import 'dart:async'; + +import 'package:komodo_defi_framework/komodo_defi_framework.dart' + show OrderbookEvent, SwapStatusEvent; +import 'package:komodo_defi_rpc_methods/komodo_defi_rpc_methods.dart'; +import 'package:komodo_defi_sdk/src/streaming/event_streaming_manager.dart'; +import 'package:komodo_defi_types/komodo_defi_types.dart'; + +/// High-level trading helpers for orderbook/swap monitoring and +/// frequently-requested trading RPCs. +/// +/// This manager provides: +/// - stream-first orderbook and swap status watchers with polling fallback +/// - in-flight and short-TTL dedupe for expensive trade requests +class TradingManager { + /// Creates a [TradingManager] backed by shared SDK services. + TradingManager({ + required ApiClient client, + required EventStreamingManager eventStreamingManager, + }) : _client = client, + _eventStreamingManager = eventStreamingManager; + + final ApiClient _client; + final EventStreamingManager _eventStreamingManager; + final _requestCache = _TimedRequestCache(); + + static const Duration _orderbookCacheTtl = Duration(milliseconds: 800); + static const Duration _swapStatusCacheTtl = Duration(milliseconds: 800); + static const Duration _recentSwapsCacheTtl = Duration(seconds: 2); + static const Duration _tradePreimageCacheTtl = Duration(seconds: 2); + static const Duration _maxTakerCacheTtl = Duration(seconds: 5); + static const Duration _minTradingCacheTtl = Duration(seconds: 10); + + /// Fetches a single orderbook snapshot. + Future getOrderbook({ + required String base, + required String rel, + }) { + return _requestCache.getOrCreate( + 'orderbook:$base:$rel', + ttl: _orderbookCacheTtl, + request: () => _client.rpc.orderbook.orderbook(base: base, rel: rel), + ); + } + + /// Watches orderbook updates using stream events and a polling fallback. + /// + /// Emits one immediate snapshot first, then emits stream updates. If stream + /// updates are stale for [streamStaleTimeout], polling snapshots are emitted. + Stream watchOrderbook({ + required String base, + required String rel, + Duration fallbackPollingInterval = const Duration(seconds: 15), + Duration streamStaleTimeout = const Duration(seconds: 20), + }) { + late StreamController controller; + StreamSubscription? streamSubscription; + Timer? pollingTimer; + DateTime? lastStreamUpdateAt; + var fetchInProgress = false; + var isCancelled = false; + + bool canEmit() => !isCancelled && !controller.isClosed; + + Future tearDownResources() async { + pollingTimer?.cancel(); + pollingTimer = null; + await streamSubscription?.cancel(); + streamSubscription = null; + } + + Future emitSnapshot() async { + if (fetchInProgress || !canEmit()) return; + fetchInProgress = true; + try { + final snapshot = await getOrderbook(base: base, rel: rel); + if (canEmit()) { + controller.add(snapshot); + } + } on Object catch (e, s) { + if (canEmit()) { + controller.addError(e, s); + } + } finally { + fetchInProgress = false; + } + } + + Future start() async { + await emitSnapshot(); + if (!canEmit()) return; + + try { + final subscription = await _eventStreamingManager.subscribeToOrderbook( + base: base, + rel: rel, + ); + if (!canEmit()) { + await subscription.cancel(); + return; + } + + streamSubscription = subscription + ..onData((event) { + if (!canEmit()) return; + lastStreamUpdateAt = DateTime.now(); + controller.add(_mapOrderbookEventToResponse(event)); + }) + ..onError((Object error, StackTrace trace) { + if (canEmit()) { + controller.addError(error, trace); + } + }); + } on Object catch (e, s) { + if (canEmit()) { + controller.addError(e, s); + } + } + if (!canEmit()) { + await tearDownResources(); + return; + } + + pollingTimer = Timer.periodic(fallbackPollingInterval, (_) { + if (!canEmit()) return; + final lastUpdateAt = lastStreamUpdateAt; + final streamIsFresh = + lastUpdateAt != null && + DateTime.now().difference(lastUpdateAt) < streamStaleTimeout; + if (!streamIsFresh) { + emitSnapshot().ignore(); + } + }); + } + + controller = StreamController( + onListen: () => start().ignore(), + onCancel: () async { + isCancelled = true; + await tearDownResources(); + }, + ); + + return controller.stream; + } + + /// Fetches status for a single swap UUID. + Future getSwapStatus({required String uuid}) { + return _requestCache.getOrCreate( + 'swap_status:$uuid', + ttl: _swapStatusCacheTtl, + request: () => _client.rpc.trading.swapStatus(uuid: uuid), + ); + } + + /// Watches updates for a single swap UUID using stream events and a polling + /// fallback. + /// + /// Emits one immediate snapshot first, then emits matching stream updates. + Stream watchSwapStatus({ + required String uuid, + Duration fallbackPollingInterval = const Duration(seconds: 10), + Duration streamStaleTimeout = const Duration(seconds: 20), + }) { + late StreamController controller; + StreamSubscription? streamSubscription; + Timer? pollingTimer; + DateTime? lastStreamUpdateAt; + var fetchInProgress = false; + var isCancelled = false; + + bool canEmit() => !isCancelled && !controller.isClosed; + + Future tearDownResources() async { + pollingTimer?.cancel(); + pollingTimer = null; + await streamSubscription?.cancel(); + streamSubscription = null; + } + + Future emitSnapshot() async { + if (fetchInProgress || !canEmit()) return; + fetchInProgress = true; + try { + final snapshot = await getSwapStatus(uuid: uuid); + if (canEmit()) { + controller.add(snapshot.swapInfo); + } + } on Object catch (e, s) { + if (canEmit()) { + controller.addError(e, s); + } + } finally { + fetchInProgress = false; + } + } + + Future start() async { + await emitSnapshot(); + if (!canEmit()) return; + + try { + final subscription = await _eventStreamingManager + .subscribeToSwapStatus(); + if (!canEmit()) { + await subscription.cancel(); + return; + } + + streamSubscription = subscription + ..onData((event) { + if (!canEmit() || event.uuid != uuid) return; + lastStreamUpdateAt = DateTime.now(); + controller.add(event.swapInfo); + }) + ..onError((Object error, StackTrace trace) { + if (canEmit()) { + controller.addError(error, trace); + } + }); + } on Object catch (e, s) { + if (canEmit()) { + controller.addError(e, s); + } + } + if (!canEmit()) { + await tearDownResources(); + return; + } + + pollingTimer = Timer.periodic(fallbackPollingInterval, (_) { + if (!canEmit()) return; + final lastUpdateAt = lastStreamUpdateAt; + final streamIsFresh = + lastUpdateAt != null && + DateTime.now().difference(lastUpdateAt) < streamStaleTimeout; + if (!streamIsFresh) { + emitSnapshot().ignore(); + } + }); + } + + controller = StreamController( + onListen: () => start().ignore(), + onCancel: () async { + isCancelled = true; + await tearDownResources(); + }, + ); + + return controller.stream; + } + + /// Cached wrapper for `trade_preimage`. + Future tradePreimage({ + required String base, + required String rel, + required SwapMethod swapMethod, + String? volume, + bool? max, + String? price, + }) { + final key = + 'trade_preimage:$base:$rel:${swapMethod.name}:' + '${volume?.toString() ?? 'null'}:' + '${max?.toString() ?? 'null'}:' + '${price?.toString() ?? 'null'}'; + + return _requestCache.getOrCreate( + key, + ttl: _tradePreimageCacheTtl, + request: () => _client.rpc.trading.tradePreimage( + base: base, + rel: rel, + swapMethod: swapMethod, + volume: volume, + max: max, + price: price, + ), + ); + } + + /// Cached wrapper for `max_taker_vol`. + Future maxTakerVolume({ + required String coin, + String? tradeWith, + }) async { + final response = await _requestCache.getOrCreate( + 'max_taker_vol:$coin:${tradeWith ?? ''}', + ttl: _maxTakerCacheTtl, + request: () => + _client.rpc.trading.maxTakerVolume(coin: coin, tradeWith: tradeWith), + ); + return response; + } + + /// Cached wrapper for `min_trading_vol`. + Future minTradingVolume({ + required String coin, + }) async { + final response = await _requestCache.getOrCreate( + 'min_trading_vol:$coin', + ttl: _minTradingCacheTtl, + request: () => _client.rpc.trading.minTradingVolume(coin: coin), + ); + return response; + } + + /// Thin wrapper for `my_recent_swaps`. + Future recentSwaps({ + int? limit, + int? pageNumber, + String? fromUuid, + String? coin, + String? otherCoin, + int? fromTimestamp, + int? toTimestamp, + }) { + final key = + 'my_recent_swaps:' + '${limit?.toString() ?? 'null'}:' + '${pageNumber?.toString() ?? 'null'}:' + '${fromUuid ?? 'null'}:' + '${coin ?? 'null'}:' + '${otherCoin ?? 'null'}:' + '${fromTimestamp?.toString() ?? 'null'}:' + '${toTimestamp?.toString() ?? 'null'}'; + return _requestCache.getOrCreate( + key, + ttl: _recentSwapsCacheTtl, + request: () => _client.rpc.trading.recentSwaps( + limit: limit, + pageNumber: pageNumber, + fromUuid: fromUuid, + coin: coin, + otherCoin: otherCoin, + fromTimestamp: fromTimestamp, + toTimestamp: toTimestamp, + ), + ); + } + + OrderbookResponse _mapOrderbookEventToResponse(OrderbookEvent event) { + final asks = event.asks.map(_mapOrderbookEntry).toList(); + final bids = event.bids.map(_mapOrderbookEntry).toList(); + + return OrderbookResponse( + mmrpc: '2.0', + base: event.base, + rel: event.rel, + bids: bids, + asks: asks, + numBids: bids.length, + numAsks: asks.length, + timestamp: DateTime.now().millisecondsSinceEpoch ~/ 1000, + ); + } + + OrderInfo _mapOrderbookEntry(Map entry) { + final price = entry['price']?.toString(); + final maxVolume = entry['max_volume']?.toString(); + + if (price == null || maxVolume == null) { + throw ArgumentError('Orderbook entry is missing price/max_volume'); + } + + final minVolume = entry['min_volume']?.toString(); + + return OrderInfo( + uuid: entry['uuid']?.toString(), + pubkey: entry['pubkey']?.toString(), + price: NumericValue(decimal: price), + baseMaxVolume: NumericValue(decimal: maxVolume), + baseMaxVolumeAggregated: NumericValue(decimal: maxVolume), + baseMinVolume: minVolume == null + ? null + : NumericValue(decimal: minVolume), + ); + } +} + +class _TimedRequestCache { + final Map> _resolved = {}; + final Map> _inFlight = {}; + + Future getOrCreate( + String key, { + required Duration ttl, + required Future Function() request, + }) async { + final now = DateTime.now(); + final cached = _resolved[key]; + if (cached != null && now.difference(cached.cachedAt) < ttl) { + return cached.value as T; + } + + final inFlight = _inFlight[key]; + if (inFlight != null) { + return await inFlight as T; + } + + final future = request(); + _inFlight[key] = future; + try { + final value = await future; + _resolved[key] = _TimedCacheEntry(value: value, cachedAt: DateTime.now()); + return value; + } finally { + final removed = _inFlight.remove(key); + if (removed != null) { + unawaited(removed); + } + } + } +} + +class _TimedCacheEntry { + _TimedCacheEntry({required this.value, required this.cachedAt}); + + final T value; + final DateTime cachedAt; +} diff --git a/packages/komodo_defi_sdk/lib/src/withdrawals/legacy_withdrawal_manager.dart b/packages/komodo_defi_sdk/lib/src/withdrawals/legacy_withdrawal_manager.dart index a517d3eb7..e10c00a66 100644 --- a/packages/komodo_defi_sdk/lib/src/withdrawals/legacy_withdrawal_manager.dart +++ b/packages/komodo_defi_sdk/lib/src/withdrawals/legacy_withdrawal_manager.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'package:decimal/decimal.dart'; +import 'package:komodo_defi_sdk/src/errors/sdk_error_mapper.dart'; import 'package:komodo_defi_sdk/src/withdrawals/withdrawal_manager.dart'; import 'package:komodo_defi_types/komodo_defi_types.dart'; @@ -9,6 +10,7 @@ class LegacyWithdrawalManager implements WithdrawalManager { LegacyWithdrawalManager(this._client); final ApiClient _client; + static const SdkErrorMapper _errorMapper = SdkErrorMapper(); /// Creates a preview and immediately executes the withdrawal. /// @@ -31,9 +33,10 @@ class LegacyWithdrawalManager implements WithdrawalManager { if (response.status == 'Error') { yield* Stream.error( - WithdrawalException( + _mapError( response.details as String, - WithdrawalException.mapErrorToCode(response.details as String), + operation: 'withdrawal.legacy', + assetId: parameters.asset, ), ); return; @@ -82,18 +85,16 @@ class LegacyWithdrawalManager implements WithdrawalManager { ); } catch (e) { yield* Stream.error( - WithdrawalException( - 'Failed to broadcast transaction: $e', - WithdrawalErrorCode.networkError, + _mapError( + e, + operation: 'withdrawal.broadcast', + assetId: parameters.asset, ), ); } } catch (e) { yield* Stream.error( - WithdrawalException( - 'Withdrawal failed: $e', - WithdrawalErrorCode.unknownError, - ), + _mapError(e, operation: 'withdrawal.legacy', assetId: parameters.asset), ); } } @@ -107,27 +108,27 @@ class LegacyWithdrawalManager implements WithdrawalManager { final response = await _client.rpc.withdraw.withdraw(parameters); if (response.status == 'Error') { - throw WithdrawalException( + throw _mapError( response.details as String, - WithdrawalException.mapErrorToCode(response.details as String), + operation: 'withdrawal.preview', + assetId: parameters.asset, ); } if (response.details is! WithdrawResult) { - throw WithdrawalException( + throw _mapError( 'Invalid preview response format', - WithdrawalErrorCode.unknownError, + operation: 'withdrawal.preview', + assetId: parameters.asset, ); } return response.details as WithdrawResult; } catch (e) { - if (e is WithdrawalException) { - rethrow; - } - throw WithdrawalException( - 'Preview failed: $e', - WithdrawalErrorCode.unknownError, + throw _mapError( + e, + operation: 'withdrawal.preview', + assetId: parameters.asset, ); } } @@ -189,10 +190,7 @@ class LegacyWithdrawalManager implements WithdrawalManager { ); } catch (e) { yield* Stream.error( - WithdrawalException( - 'Failed to broadcast transaction: $e', - WithdrawalErrorCode.networkError, - ), + _mapError(e, operation: 'withdrawal.execute', assetId: assetId), ); } } @@ -213,4 +211,15 @@ class LegacyWithdrawalManager implements WithdrawalManager { // Legacy implementation doesn't support priority-based fees return null; } + + SdkError _mapError( + Object error, { + required String operation, + String? assetId, + }) { + return _errorMapper.map( + error, + context: SdkErrorContext(operation: operation, assetId: assetId), + ); + } } diff --git a/packages/komodo_defi_sdk/lib/src/withdrawals/withdrawal_manager.dart b/packages/komodo_defi_sdk/lib/src/withdrawals/withdrawal_manager.dart index d0689ad42..954c2ed72 100644 --- a/packages/komodo_defi_sdk/lib/src/withdrawals/withdrawal_manager.dart +++ b/packages/komodo_defi_sdk/lib/src/withdrawals/withdrawal_manager.dart @@ -4,6 +4,7 @@ import 'dart:developer' show log; import 'package:decimal/decimal.dart'; import 'package:komodo_defi_rpc_methods/komodo_defi_rpc_methods.dart'; import 'package:komodo_defi_sdk/src/_internal_exports.dart'; +import 'package:komodo_defi_sdk/src/errors/sdk_error_mapper.dart'; import 'package:komodo_defi_sdk/src/fees/fee_manager.dart'; import 'package:komodo_defi_sdk/src/withdrawals/legacy_withdrawal_manager.dart'; import 'package:komodo_defi_types/komodo_defi_types.dart'; @@ -102,6 +103,7 @@ class WithdrawalManager { final FeeManager _feeManager; final LegacyWithdrawalManager _legacyManager; final _activeWithdrawals = >{}; + static const SdkErrorMapper _errorMapper = SdkErrorMapper(); /// Cancels an active withdrawal task. /// @@ -515,7 +517,7 @@ class WithdrawalManager { if (lastStatus.status.toLowerCase() == 'error') { throw WithdrawalException( lastStatus.details as String, - _mapErrorToCode(lastStatus.details as String), + WithdrawalException.mapErrorToCode(lastStatus.details as String), ); } @@ -528,12 +530,10 @@ class WithdrawalManager { return lastStatus.details as WithdrawalPreview; } catch (e) { - if (e is WithdrawalException) { - rethrow; - } - throw WithdrawalException( - 'Preview failed: $e', - WithdrawalErrorCode.unknownError, + throw _mapError( + e, + operation: 'withdrawal.preview', + assetId: parameters.asset, ); } } @@ -594,11 +594,14 @@ class WithdrawalManager { } // Ensure asset is activated before broadcasting - final activationResult = await _activationCoordinator.activateAsset(asset); + final activationResult = await _activationCoordinator.activateAsset( + asset, + ); if (activationResult.isFailure) { - throw WithdrawalException( - 'Failed to activate asset $assetId: ${activationResult.errorMessage ?? activationResult.toString()}', - WithdrawalErrorCode.unknownError, + throw _mapError( + activationResult.errorMessage ?? activationResult.toString(), + operation: 'withdrawal.activate', + assetId: assetId, ); } @@ -642,10 +645,7 @@ class WithdrawalManager { ); } catch (e) { yield* Stream.error( - WithdrawalException( - 'Failed to broadcast transaction: $e', - WithdrawalErrorCode.networkError, - ), + _mapError(e, operation: 'withdrawal.execute', assetId: assetId), ); } } @@ -707,9 +707,10 @@ class WithdrawalManager { ); if (activationResult.isFailure) { - throw WithdrawalException( - 'Failed to activate asset ${parameters.asset}', - WithdrawalErrorCode.unknownError, + throw _mapError( + activationResult.errorMessage ?? activationResult.toString(), + operation: 'withdrawal.activate', + assetId: parameters.asset, ); } @@ -730,9 +731,10 @@ class WithdrawalManager { )) { if (status.status == 'Error') { yield* Stream.error( - WithdrawalException( + _mapError( status.details as String, - _mapErrorToCode(status.details as String), + operation: 'withdrawal.progress', + assetId: parameters.asset, ), ); return; @@ -773,9 +775,10 @@ class WithdrawalManager { log('Error while broadcasting transaction: $e'); log('Stack trace: $stackTrace'); yield* Stream.error( - WithdrawalException( - 'Failed to broadcast transaction: $e', - WithdrawalErrorCode.networkError, + _mapError( + e, + operation: 'withdrawal.broadcast', + assetId: parameters.asset, ), ); } @@ -785,9 +788,10 @@ class WithdrawalManager { log('Error during withdrawal: $e'); log('Stack trace: $stackTrace'); yield* Stream.error( - WithdrawalException( - 'Withdrawal failed: $e', - WithdrawalErrorCode.unknownError, + _mapError( + e, + operation: 'withdrawal.execute', + assetId: parameters.asset, ), ); } finally { @@ -796,33 +800,15 @@ class WithdrawalManager { } } - /// Maps error messages to withdrawal error codes. - /// - /// This helper method analyzes error messages from the API and maps them - /// to appropriate [WithdrawalErrorCode] values for consistent error - /// handling. - /// - /// Parameters: - /// - [error] - The error message to analyze - /// - /// Returns the appropriate [WithdrawalErrorCode] based on the error content. - WithdrawalErrorCode _mapErrorToCode(String error) { - final errorLower = error.toLowerCase(); - - if (errorLower.contains('insufficient funds') || - errorLower.contains('not enough funds')) { - return WithdrawalErrorCode.insufficientFunds; - } - - if (errorLower.contains('invalid address')) { - return WithdrawalErrorCode.invalidAddress; - } - - if (errorLower.contains('fee')) { - return WithdrawalErrorCode.networkError; - } - - return WithdrawalErrorCode.unknownError; + SdkError _mapError( + Object error, { + required String operation, + String? assetId, + }) { + return _errorMapper.map( + error, + context: SdkErrorContext(operation: operation, assetId: assetId), + ); } /// Provides estimated confirmation times for Ethereum-based transactions. diff --git a/packages/komodo_defi_sdk/test/backward_compatibility_test.dart b/packages/komodo_defi_sdk/test/backward_compatibility_test.dart index b0bc808ab..c5fd8c99e 100644 --- a/packages/komodo_defi_sdk/test/backward_compatibility_test.dart +++ b/packages/komodo_defi_sdk/test/backward_compatibility_test.dart @@ -5,6 +5,7 @@ import 'package:komodo_defi_sdk/src/activation/shared_activation_coordinator.dar import 'package:komodo_defi_sdk/src/assets/asset_lookup.dart'; import 'package:komodo_defi_sdk/src/balances/balance_manager.dart'; import 'package:komodo_defi_sdk/src/pubkeys/pubkey_manager.dart'; +import 'package:komodo_defi_sdk/src/streaming/event_streaming_manager.dart'; import 'package:komodo_defi_types/komodo_defi_types.dart'; import 'package:mocktail/mocktail.dart'; import 'package:test/test.dart'; @@ -18,6 +19,9 @@ class _MockActivationCoordinator extends Mock class _MockAssetLookup extends Mock implements IAssetLookup {} +class _MockEventStreamingManager extends Mock + implements EventStreamingManager {} + /// Tests to verify backward compatibility of public APIs /// These tests ensure that existing public method signatures remain unchanged /// and that external consumers are not affected by cleanup improvements @@ -135,6 +139,7 @@ void main() { late _MockActivationCoordinator activation; late _MockPubkeyManager pubkeyManager; late _MockAssetLookup assetLookup; + late _MockEventStreamingManager eventStreamingManager; late BalanceManager manager; setUp(() { @@ -142,6 +147,7 @@ void main() { activation = _MockActivationCoordinator(); pubkeyManager = _MockPubkeyManager(); assetLookup = _MockAssetLookup(); + eventStreamingManager = _MockEventStreamingManager(); when( () => auth.authStateChanges, @@ -152,6 +158,7 @@ void main() { auth: auth, pubkeyManager: pubkeyManager, activationCoordinator: activation, + eventStreamingManager: eventStreamingManager, ); }); @@ -167,6 +174,7 @@ void main() { auth: auth, pubkeyManager: pubkeyManager, activationCoordinator: activation, + eventStreamingManager: eventStreamingManager, ), returnsNormally, ); @@ -211,6 +219,7 @@ void main() { late _MockActivationCoordinator activation; late _MockAssetLookup assetLookup; late _MockPubkeyManager pubkeyManager; + late _MockEventStreamingManager eventStreamingManager; late PubkeyManager pubkeyManagerInstance; late BalanceManager balanceManagerInstance; @@ -220,6 +229,7 @@ void main() { activation = _MockActivationCoordinator(); assetLookup = _MockAssetLookup(); pubkeyManager = _MockPubkeyManager(); + eventStreamingManager = _MockEventStreamingManager(); when( () => auth.authStateChanges, @@ -231,6 +241,7 @@ void main() { auth: auth, pubkeyManager: pubkeyManager, activationCoordinator: activation, + eventStreamingManager: eventStreamingManager, ); }); @@ -270,6 +281,7 @@ void main() { auth: auth, pubkeyManager: pubkeyManager, activationCoordinator: activation, + eventStreamingManager: eventStreamingManager, ); // Simulate auth state changes diff --git a/packages/komodo_defi_sdk/test/balances/balance_manager_test.dart b/packages/komodo_defi_sdk/test/balances/balance_manager_test.dart index e5d3f6289..52283e43a 100644 --- a/packages/komodo_defi_sdk/test/balances/balance_manager_test.dart +++ b/packages/komodo_defi_sdk/test/balances/balance_manager_test.dart @@ -6,6 +6,7 @@ import 'package:komodo_defi_sdk/src/activation/shared_activation_coordinator.dar import 'package:komodo_defi_sdk/src/assets/asset_lookup.dart'; import 'package:komodo_defi_sdk/src/balances/balance_manager.dart'; import 'package:komodo_defi_sdk/src/pubkeys/pubkey_manager.dart'; +import 'package:komodo_defi_sdk/src/streaming/event_streaming_manager.dart'; import 'package:komodo_defi_types/komodo_defi_types.dart'; import 'package:mocktail/mocktail.dart'; import 'package:test/test.dart'; @@ -19,6 +20,9 @@ class _MockPubkeyManager extends Mock implements PubkeyManager {} class _MockAssetLookup extends Mock implements IAssetLookup {} +class _MockEventStreamingManager extends Mock + implements EventStreamingManager {} + void main() { setUpAll(() { registerFallbackValue( @@ -58,6 +62,7 @@ void main() { late _MockActivationCoordinator activation; late _MockPubkeyManager pubkeyManager; late _MockAssetLookup assetLookup; + late _MockEventStreamingManager eventStreamingManager; setUp(() { registerFallbackValue( @@ -74,6 +79,7 @@ void main() { activation = _MockActivationCoordinator(); pubkeyManager = _MockPubkeyManager(); assetLookup = _MockAssetLookup(); + eventStreamingManager = _MockEventStreamingManager(); }); test('dispose swallows cancel/close errors and is idempotent', () async { @@ -87,6 +93,7 @@ void main() { auth: auth, pubkeyManager: pubkeyManager, activationCoordinator: activation, + eventStreamingManager: eventStreamingManager, ); await manager.dispose(); @@ -161,6 +168,7 @@ void main() { auth: auth, pubkeyManager: pubkeyManager, activationCoordinator: activation, + eventStreamingManager: eventStreamingManager, ); addTearDown(() async { @@ -214,6 +222,7 @@ void main() { late _MockActivationCoordinator activation; late _MockPubkeyManager pubkeyManager; late _MockAssetLookup assetLookup; + late _MockEventStreamingManager eventStreamingManager; late StreamController authChanges; late BalanceManager manager; @@ -222,6 +231,7 @@ void main() { activation = _MockActivationCoordinator(); pubkeyManager = _MockPubkeyManager(); assetLookup = _MockAssetLookup(); + eventStreamingManager = _MockEventStreamingManager(); authChanges = StreamController.broadcast(); when(() => auth.authStateChanges).thenAnswer((_) => authChanges.stream); @@ -231,6 +241,7 @@ void main() { auth: auth, pubkeyManager: pubkeyManager, activationCoordinator: activation, + eventStreamingManager: eventStreamingManager, ); // Setup common mocks @@ -767,6 +778,7 @@ void main() { late _MockActivationCoordinator activation; late _MockPubkeyManager pubkeyManager; late _MockAssetLookup assetLookup; + late _MockEventStreamingManager eventStreamingManager; late StreamController authChanges; late BalanceManager manager; @@ -775,6 +787,7 @@ void main() { activation = _MockActivationCoordinator(); pubkeyManager = _MockPubkeyManager(); assetLookup = _MockAssetLookup(); + eventStreamingManager = _MockEventStreamingManager(); authChanges = StreamController.broadcast(); when(() => auth.authStateChanges).thenAnswer((_) => authChanges.stream); @@ -784,6 +797,7 @@ void main() { auth: auth, pubkeyManager: pubkeyManager, activationCoordinator: activation, + eventStreamingManager: eventStreamingManager, ); when(() => activation.isAssetActive(any())).thenAnswer((_) async => true); @@ -1118,6 +1132,7 @@ void main() { auth: disposalAuth, pubkeyManager: disposalPubkeyManager, activationCoordinator: disposalActivation, + eventStreamingManager: eventStreamingManager, ); // Create resources @@ -1347,6 +1362,7 @@ void main() { late _MockActivationCoordinator activation; late _MockPubkeyManager pubkeyManager; late _MockAssetLookup assetLookup; + late _MockEventStreamingManager eventStreamingManager; late StreamController authChanges; late BalanceManager manager; @@ -1355,6 +1371,7 @@ void main() { activation = _MockActivationCoordinator(); pubkeyManager = _MockPubkeyManager(); assetLookup = _MockAssetLookup(); + eventStreamingManager = _MockEventStreamingManager(); authChanges = StreamController.broadcast(); when(() => auth.authStateChanges).thenAnswer((_) => authChanges.stream); @@ -1364,6 +1381,7 @@ void main() { auth: auth, pubkeyManager: pubkeyManager, activationCoordinator: activation, + eventStreamingManager: eventStreamingManager, ); when(() => auth.currentUser).thenAnswer( diff --git a/packages/komodo_defi_sdk/test/errors/sdk_error_mapper_test.dart b/packages/komodo_defi_sdk/test/errors/sdk_error_mapper_test.dart new file mode 100644 index 000000000..722bd9b06 --- /dev/null +++ b/packages/komodo_defi_sdk/test/errors/sdk_error_mapper_test.dart @@ -0,0 +1,64 @@ +import 'package:komodo_defi_rpc_methods/komodo_defi_rpc_methods.dart'; +import 'package:komodo_defi_sdk/src/errors/sdk_error_mapper.dart'; +import 'package:komodo_defi_types/komodo_defi_types.dart'; +import 'package:test/test.dart'; + +void main() { + group('SdkErrorMapper', () { + const mapper = SdkErrorMapper(); + + test('maps withdraw insufficient balance to insufficient funds', () { + const error = WithdrawErrorNotSufficientBalanceException( + coin: 'KMD', + available: BigDecimal('1'), + required: BigDecimal('2'), + ); + final sdkError = mapper.map(error); + + expect(sdkError.code, SdkErrorCode.insufficientFunds); + expect(sdkError.category, SdkErrorCategory.funds); + expect(sdkError.messageKey, 'withdrawNotSufficientBalanceError'); + expect(sdkError.messageArgs, ['KMD', '1', '2']); + }); + + test('maps web3 timeout to network timeout', () { + const error = Web3RpcErrorTimeoutException('timeout'); + final sdkError = mapper.map(error); + + expect(sdkError.code, SdkErrorCode.timeout); + expect(sdkError.category, SdkErrorCategory.network); + expect(sdkError.messageKey, 'sdk_errors.timeout'); + }); + + test('maps withdraw no such coin with coin arg', () { + const error = WithdrawErrorNoSuchCoinException(coin: 'KMD'); + final sdkError = mapper.map(error); + + expect(sdkError.code, SdkErrorCode.assetNotActivated); + expect(sdkError.category, SdkErrorCategory.activation); + expect(sdkError.messageKey, 'withdrawNoSuchCoinError'); + expect(sdkError.messageArgs, ['KMD']); + }); + + test('maps unsupported errors by exception class name heuristics', () { + const error = GetFeeEstimationRequestErrorCoinNotSupportedException(); + final sdkError = mapper.map(error); + + expect(sdkError.code, SdkErrorCode.notSupported); + expect(sdkError.category, SdkErrorCategory.unsupported); + expect(sdkError.messageKey, 'sdk_errors.not_supported'); + }); + + test('maps auth incorrect password to invalid credentials', () { + final error = AuthException( + 'Incorrect wallet password', + type: AuthExceptionType.incorrectPassword, + ); + final sdkError = mapper.map(error); + + expect(sdkError.code, SdkErrorCode.authInvalidCredentials); + expect(sdkError.category, SdkErrorCategory.auth); + expect(sdkError.messageKey, 'sdk_errors.auth_invalid_credentials'); + }); + }); +} diff --git a/packages/komodo_defi_sdk/test/transaction_history/transaction_history_strategies_test.dart b/packages/komodo_defi_sdk/test/transaction_history/transaction_history_strategies_test.dart index fc6d33587..80a68d924 100644 --- a/packages/komodo_defi_sdk/test/transaction_history/transaction_history_strategies_test.dart +++ b/packages/komodo_defi_sdk/test/transaction_history/transaction_history_strategies_test.dart @@ -11,6 +11,26 @@ class _MockPubkeyManager extends Mock implements PubkeyManager {} class _MockLocalAuth extends Mock implements KomodoDefiLocalAuth {} +Asset _createEvmAsset({ + required String coin, + required int chainId, + String type = 'ETH', + bool isTestnet = false, +}) { + return Asset.fromJson({ + 'coin': coin, + 'type': type, + 'fname': coin, + 'chain_id': chainId, + 'is_testnet': isTestnet, + 'nodes': const [ + {'url': 'https://rpc.example.com'}, + ], + 'swap_contract_address': '0x0000000000000000000000000000000000000001', + 'fallback_swap_contract': '0x0000000000000000000000000000000000000001', + }); +} + Asset _createZhtlcAsset() { final protocol = ZhtlcProtocol.fromJson({ 'type': 'ZHTLC', @@ -43,6 +63,37 @@ void main() { auth = _MockLocalAuth(); }); + group('EtherscanProtocolHelper', () { + const helper = EtherscanProtocolHelper(); + + test('supports ETH endpoint and keeps KDF tx history disabled', () { + final eth = _createEvmAsset(coin: 'ETH', chainId: 1); + + expect(helper.supportsProtocol(eth), isTrue); + expect( + helper.getApiUrlForAsset(eth)?.toString(), + endsWith('/v2/eth_tx_history'), + ); + expect(helper.shouldEnableTransactionHistory(eth), isFalse); + }); + + test('supports GRC20 endpoint and keeps KDF tx history disabled', () { + final gleect = _createEvmAsset( + coin: 'GLEECT', + chainId: 11169, + type: 'GRC20', + isTestnet: true, + ); + + expect(helper.supportsProtocol(gleect), isTrue); + expect( + helper.getApiUrlForAsset(gleect)?.toString(), + endsWith('/v2/grc_tx_history'), + ); + expect(helper.shouldEnableTransactionHistory(gleect), isFalse); + }); + }); + group('TransactionHistoryStrategyFactory', () { test('selects ZHTLC strategy for ZHTLC asset', () { final factory = TransactionHistoryStrategyFactory(pubkeyManager, auth); @@ -70,5 +121,19 @@ void main() { expect(strategy, isA()); }); + + test('uses Etherscan strategy for GRC20 chain', () { + final factory = TransactionHistoryStrategyFactory(pubkeyManager, auth); + final gleect = _createEvmAsset( + coin: 'GLEECT', + chainId: 11169, + type: 'GRC20', + isTestnet: true, + ); + + final strategy = factory.forAsset(gleect); + + expect(strategy, isA()); + }); }); } diff --git a/packages/komodo_defi_types/lib/src/activation/activation_progress.dart b/packages/komodo_defi_types/lib/src/activation/activation_progress.dart index 2234967bd..67af259ee 100644 --- a/packages/komodo_defi_types/lib/src/activation/activation_progress.dart +++ b/packages/komodo_defi_types/lib/src/activation/activation_progress.dart @@ -72,6 +72,7 @@ class ActivationProgress extends Equatable { this.isComplete = false, this.errorMessage, this.progressDetails, + this.sdkError, }); /// Factory for successful completion @@ -114,11 +115,13 @@ class ActivationProgress extends Equatable { String? errorCode, String? details, StackTrace? stackTrace, + SdkError? sdkError, }) { return ActivationProgress( status: 'Activation failed', errorMessage: message, isComplete: true, + sdkError: sdkError, progressDetails: ActivationProgressDetails( currentStep: ActivationStep.error, stepCount: 1, @@ -134,6 +137,7 @@ class ActivationProgress extends Equatable { final bool isComplete; final String? errorMessage; final ActivationProgressDetails? progressDetails; + final SdkError? sdkError; bool get isSuccess => isComplete && errorMessage == null; bool get isError => errorMessage != null; @@ -141,6 +145,9 @@ class ActivationProgress extends Equatable { /// Returns a formatted message suitable for user display String get userMessage { if (isError) { + if (sdkError != null) { + return sdkError!.fallbackMessage; + } return 'Error: $errorMessage'; } final progress = progressPercentage != null @@ -156,6 +163,7 @@ class ActivationProgress extends Equatable { bool? isComplete, String? errorMessage, ActivationProgressDetails? progressDetails, + SdkError? sdkError, }) { return ActivationProgress( status: status ?? this.status, @@ -163,6 +171,7 @@ class ActivationProgress extends Equatable { isComplete: isComplete ?? this.isComplete, errorMessage: errorMessage ?? this.errorMessage, progressDetails: progressDetails ?? this.progressDetails, + sdkError: sdkError ?? this.sdkError, ); } @@ -173,6 +182,7 @@ class ActivationProgress extends Equatable { isComplete, errorMessage, progressDetails, + sdkError, ]; JsonMap toJson() => { @@ -181,6 +191,7 @@ class ActivationProgress extends Equatable { 'isComplete': isComplete, if (errorMessage != null) 'errorMessage': errorMessage, if (progressDetails != null) 'details': progressDetails!.toJson(), + if (sdkError != null) 'sdkError': sdkError!.toJson(), }; } diff --git a/packages/komodo_defi_types/lib/src/coin_classes/coin_subclasses.dart b/packages/komodo_defi_types/lib/src/coin_classes/coin_subclasses.dart index 349c9257c..660984a4c 100644 --- a/packages/komodo_defi_types/lib/src/coin_classes/coin_subclasses.dart +++ b/packages/komodo_defi_types/lib/src/coin_classes/coin_subclasses.dart @@ -21,6 +21,7 @@ enum CoinSubClass { utxo, smartBch, erc20, + grc20, tendermint, tendermintToken, krc20, @@ -28,7 +29,6 @@ enum CoinSubClass { hrc20, hecoChain, rskSmartBitcoin, - grc20, zhtlc, unknown; @@ -387,6 +387,8 @@ extension CoinSubClassTokenStandard on CoinSubClass { switch (this) { case CoinSubClass.erc20: return 'ERC20'; + case CoinSubClass.grc20: + return 'GRC20'; case CoinSubClass.bep20: return 'BEP20'; case CoinSubClass.qrc20: @@ -409,8 +411,6 @@ extension CoinSubClassTokenStandard on CoinSubClass { return 'HRC20'; case CoinSubClass.hecoChain: return 'HCO20'; - case CoinSubClass.grc20: - return 'GRC20'; // Subclasses without a canonical short token/network standard suffix case CoinSubClass.moonbeam: case CoinSubClass.slp: // ignore: deprecated_member_use_from_same_package diff --git a/packages/komodo_defi_types/lib/src/errors/sdk_error.dart b/packages/komodo_defi_types/lib/src/errors/sdk_error.dart new file mode 100644 index 000000000..fc98ad454 --- /dev/null +++ b/packages/komodo_defi_types/lib/src/errors/sdk_error.dart @@ -0,0 +1,136 @@ +import 'package:equatable/equatable.dart'; +import 'package:komodo_defi_types/komodo_defi_type_utils.dart'; + +/// High-level error categories for SDK consumers. +enum SdkErrorCategory { + network, + validation, + funds, + auth, + activation, + hardware, + unsupported, + unknown, +} + +/// Stable error codes for SDK consumers. +enum SdkErrorCode { + networkUnavailable, + timeout, + transport, + invalidResponse, + insufficientFunds, + insufficientGas, + insufficientFeeBalance, + zeroBalance, + amountTooLow, + invalidAddress, + invalidFee, + invalidMemo, + assetNotActivated, + assetNotFound, + activationFailed, + userCancelled, + hardwareFailure, + notSupported, + authInvalidCredentials, + authUnauthorized, + authWalletNotFound, + general, +} + +/// Optional context describing where the error occurred. +class SdkErrorContext extends Equatable { + const SdkErrorContext({ + this.operation, + this.assetId, + this.rpcMethod, + this.extra = const {}, + }); + + final String? operation; + final String? assetId; + final String? rpcMethod; + final JsonMap extra; + + JsonMap toJson() => { + if (operation != null) 'operation': operation, + if (assetId != null) 'assetId': assetId, + if (rpcMethod != null) 'rpcMethod': rpcMethod, + if (extra.isNotEmpty) 'extra': extra, + }; + + @override + List get props => [operation, assetId, rpcMethod, extra]; +} + +/// Structured SDK error wrapper for user-facing messaging. +class SdkError extends Equatable implements Exception { + const SdkError({ + required this.code, + required this.category, + required this.messageKey, + required this.fallbackMessage, + this.messageArgs = const [], + this.retryable = false, + this.context, + this.source, + }); + + final SdkErrorCode code; + final SdkErrorCategory category; + final String messageKey; + final String fallbackMessage; + final List messageArgs; + final bool retryable; + final SdkErrorContext? context; + + /// Original error for debugging/telemetry. + final Object? source; + + SdkError copyWith({ + SdkErrorCode? code, + SdkErrorCategory? category, + String? messageKey, + String? fallbackMessage, + List? messageArgs, + bool? retryable, + SdkErrorContext? context, + Object? source, + }) { + return SdkError( + code: code ?? this.code, + category: category ?? this.category, + messageKey: messageKey ?? this.messageKey, + fallbackMessage: fallbackMessage ?? this.fallbackMessage, + messageArgs: messageArgs ?? this.messageArgs, + retryable: retryable ?? this.retryable, + context: context ?? this.context, + source: source ?? this.source, + ); + } + + JsonMap toJson() => { + 'code': code.name, + 'category': category.name, + 'messageKey': messageKey, + 'fallbackMessage': fallbackMessage, + if (messageArgs.isNotEmpty) 'messageArgs': messageArgs, + 'retryable': retryable, + if (context != null) 'context': context!.toJson(), + }; + + @override + String toString() => fallbackMessage; + + @override + List get props => [ + code, + category, + messageKey, + fallbackMessage, + messageArgs, + retryable, + context, + ]; +} diff --git a/packages/komodo_defi_types/lib/src/types.dart b/packages/komodo_defi_types/lib/src/types.dart index 8e8dbbbf7..a606ee917 100644 --- a/packages/komodo_defi_types/lib/src/types.dart +++ b/packages/komodo_defi_types/lib/src/types.dart @@ -23,6 +23,7 @@ export 'coin_classes/coin_subclasses.dart'; export 'coin_classes/protocol_class.dart'; export 'constants.dart'; export 'cryptography/mnemonic.dart'; +export 'errors/sdk_error.dart'; export 'exceptions/http_exceptions.dart'; export 'exported_rpc_types.dart'; export 'fees/fee_management.dart'; diff --git a/packages/komodo_defi_types/lib/src/withdrawal/withdrawal_types.dart b/packages/komodo_defi_types/lib/src/withdrawal/withdrawal_types.dart index 6e48c5772..c80299ee3 100644 --- a/packages/komodo_defi_types/lib/src/withdrawal/withdrawal_types.dart +++ b/packages/komodo_defi_types/lib/src/withdrawal/withdrawal_types.dart @@ -21,9 +21,9 @@ class WithdrawResult { this.kmdRewards, this.memo, }) : assert( - txHex != null || txJson != null, - 'Either txHex or txJson must be provided', - ); + txHex != null || txJson != null, + 'Either txHex or txJson must be provided', + ); factory WithdrawResult.fromJson(JsonMap json) { return WithdrawResult( @@ -60,20 +60,20 @@ class WithdrawResult { final String? memo; JsonMap toJson() => { - if (txHex != null) 'tx_hex': txHex, - if (txJson != null) 'tx_json': txJson, - 'tx_hash': txHash, - 'from': from, - 'to': to, - ...balanceChanges.toJson(), - 'block_height': blockHeight, - 'timestamp': timestamp, - 'fee_details': fee.toJson(), - 'coin': coin, - if (internalId != null) 'internal_id': internalId, - if (kmdRewards != null) 'kmd_rewards': kmdRewards!.toJson(), - if (memo != null) 'memo': memo, - }; + if (txHex != null) 'tx_hex': txHex, + if (txJson != null) 'tx_json': txJson, + 'tx_hash': txHash, + 'from': from, + 'to': to, + ...balanceChanges.toJson(), + 'block_height': blockHeight, + 'timestamp': timestamp, + 'fee_details': fee.toJson(), + 'coin': coin, + if (internalId != null) 'internal_id': internalId, + if (kmdRewards != null) 'kmd_rewards': kmdRewards!.toJson(), + if (memo != null) 'memo': memo, + }; } /// Domain model for a successful withdrawal operation @@ -95,7 +95,8 @@ class WithdrawalResult extends Equatable { coin: result.coin, toAddress: result.to.first, fee: result.fee, - kmdRewardsEligible: result.kmdRewards != null && + kmdRewardsEligible: + result.kmdRewards != null && Decimal.parse(result.kmdRewards!.amount) > Decimal.zero, ); } @@ -112,13 +113,13 @@ class WithdrawalResult extends Equatable { @override List get props => [ - txHash, - balanceChanges, - coin, - toAddress, - fee, - kmdRewardsEligible, - ]; + txHash, + balanceChanges, + coin, + toAddress, + fee, + kmdRewardsEligible, + ]; } /// Progress tracking for withdrawal operations @@ -130,6 +131,7 @@ class WithdrawalProgress extends Equatable { this.errorCode, this.errorMessage, this.taskId, + this.sdkError, }); final WithdrawalStatus status; @@ -138,16 +140,18 @@ class WithdrawalProgress extends Equatable { final WithdrawalErrorCode? errorCode; final String? errorMessage; final String? taskId; + final SdkError? sdkError; @override List get props => [ - status, - message, - withdrawalResult, - errorCode, - errorMessage, - taskId, - ]; + status, + message, + withdrawalResult, + errorCode, + errorMessage, + taskId, + sdkError, + ]; } /// Parameters for initiating a withdrawal @@ -164,9 +168,9 @@ class WithdrawParameters extends Equatable { this.ibcSourceChannel, this.isMax, }) : assert( - amount != null || (isMax ?? false), - 'Amount must be non-null when not using max', - ); + amount != null || (isMax ?? false), + 'Amount must be non-null when not using max', + ); final String asset; final String toAddress; @@ -180,30 +184,30 @@ class WithdrawParameters extends Equatable { final bool? isMax; JsonMap toJson() => { - 'coin': asset, - 'to': toAddress, - if (fee != null) 'fee': fee!.toJson(), - if (amount != null) 'amount': amount.toString(), - if (isMax != null) 'max': isMax, - if (from != null) 'from': from!.toRpcParams(), - if (memo != null) 'memo': memo, - if (ibcTransfer != null) 'ibc_transfer': ibcTransfer, - if (ibcSourceChannel != null) 'ibc_source_channel': ibcSourceChannel, - }; + 'coin': asset, + 'to': toAddress, + if (fee != null) 'fee': fee!.toJson(), + if (amount != null) 'amount': amount.toString(), + if (isMax != null) 'max': isMax, + if (from != null) 'from': from!.toRpcParams(), + if (memo != null) 'memo': memo, + if (ibcTransfer != null) 'ibc_transfer': ibcTransfer, + if (ibcSourceChannel != null) 'ibc_source_channel': ibcSourceChannel, + }; @override List get props => [ - asset, - toAddress, - amount, - fee, - feePriority, - from, - memo, - ibcTransfer, - ibcSourceChannel, - isMax, - ]; + asset, + toAddress, + amount, + fee, + feePriority, + from, + memo, + ibcTransfer, + ibcSourceChannel, + isMax, + ]; } /// Preview of a withdrawal operation, using same structure as API response @@ -224,24 +228,20 @@ enum Bip44Chain { /// Specifies the source of funds for a withdrawal // TODO: Implement Trezor sourcew class WithdrawalSource extends Equatable implements RpcRequestParams { - const WithdrawalSource._({ - required this.type, - required this.params, - }); + const WithdrawalSource._({required this.type, required this.params}); factory WithdrawalSource.hdWalletId({ required int accountId, required int addressId, Bip44Chain chain = Bip44Chain.external, - }) => - WithdrawalSource._( - type: WithdrawalSourceType.hdWallet, - params: { - 'account_id': accountId, - 'chain': chain.value, - 'address_id': addressId, - }, - ); + }) => WithdrawalSource._( + type: WithdrawalSourceType.hdWallet, + params: { + 'account_id': accountId, + 'chain': chain.value, + 'address_id': addressId, + }, + ); factory WithdrawalSource.hdDerivationPath(String derivationPath) => WithdrawalSource._( @@ -255,13 +255,10 @@ class WithdrawalSource extends Equatable implements RpcRequestParams { required int accountId, required String chain, required int addressId, - }) => - WithdrawalSource._( - type: WithdrawalSourceType.hdWallet, - params: { - 'derivation_path': "m/44'/$coinId'/$accountId'/$chain/$addressId", - }, - ); + }) => WithdrawalSource._( + type: WithdrawalSourceType.hdWallet, + params: {'derivation_path': "m/44'/$coinId'/$accountId'/$chain/$addressId"}, + ); // TODO: // factory WithdrawalSource.trezor @@ -272,23 +269,17 @@ class WithdrawalSource extends Equatable implements RpcRequestParams { @override JsonMap toRpcParams() => params; - JsonMap toJson() => { - 'type': type.toString(), - ...params, - }; + JsonMap toJson() => {'type': type.toString(), ...params}; @override List get props => [ - type, - [...params.values, params.keys], - ]; + type, + [...params.values, params.keys], + ]; } class KmdRewards { - KmdRewards({ - required this.amount, - this.claimedByMe, - }); + KmdRewards({required this.amount, this.claimedByMe}); factory KmdRewards.fromJson(JsonMap json) { return KmdRewards( @@ -301,7 +292,7 @@ class KmdRewards { final bool? claimedByMe; JsonMap toJson() => { - 'amount': amount, - if (claimedByMe != null) 'claimed_by_me': claimedByMe, - }; + 'amount': amount, + if (claimedByMe != null) 'claimed_by_me': claimedByMe, + }; } diff --git a/packages/komodo_ui/lib/src/core/inputs/fee_info_input.dart b/packages/komodo_ui/lib/src/core/inputs/fee_info_input.dart index fee6124c1..4193a597b 100644 --- a/packages/komodo_ui/lib/src/core/inputs/fee_info_input.dart +++ b/packages/komodo_ui/lib/src/core/inputs/fee_info_input.dart @@ -7,6 +7,7 @@ typedef FeeInfoChanged = void Function(FeeInfo? fee); /// Constants for fee calculations const _gweiInEth = 1000000000; // 10^9 +const _defaultUtxoDecimals = 8; /// A widget for inputting custom fee information. /// @@ -194,12 +195,10 @@ class FeeInfoInput extends StatelessWidget { Expanded( child: TextFormField( enabled: isCustomFee, - initialValue: - currentFee?.maxFeePerGas != null - ? (currentFee!.maxFeePerGas * - Decimal.fromInt(_gweiInEth)) - .toString() - : null, + initialValue: currentFee?.maxFeePerGas != null + ? (currentFee!.maxFeePerGas * Decimal.fromInt(_gweiInEth)) + .toString() + : null, decoration: const InputDecoration( labelText: 'Max Fee Per Gas (Gwei)', ), @@ -241,12 +240,11 @@ class FeeInfoInput extends StatelessWidget { Expanded( child: TextFormField( enabled: isCustomFee, - initialValue: - currentFee?.maxPriorityFeePerGas != null - ? (currentFee!.maxPriorityFeePerGas * - Decimal.fromInt(_gweiInEth)) - .toString() - : null, + initialValue: currentFee?.maxPriorityFeePerGas != null + ? (currentFee!.maxPriorityFeePerGas * + Decimal.fromInt(_gweiInEth)) + .toString() + : null, decoration: const InputDecoration( labelText: 'Max Priority Fee (Gwei)', ), @@ -587,16 +585,18 @@ class FeeInfoInput extends StatelessWidget { /// Builds manual inputs for UTXO fee entry with a toggle button Widget _buildUtxoFeeInputs(BuildContext context, UtxoProtocol protocol) { - final defaultFee = protocol.txFee ?? 10000; + final defaultFeeAtomic = protocol.txFee ?? 10000; + final decimals = asset.id.chainId.decimals ?? _defaultUtxoDecimals; + final defaultFeeAmount = _atomicToDecimal(defaultFeeAtomic, decimals); // Extract current fee amount or use default final currentFeeAmount = selectedFee?.maybeMap( utxoFixed: (f) => f.amount, utxoPerKbyte: (p) => p.amount, - orElse: () => Decimal.parse(defaultFee.toString()), + orElse: () => defaultFeeAmount, ) ?? - Decimal.parse(defaultFee.toString()); + defaultFeeAmount; // Determine if we're using fixed fee or per KB fee final isPerKbyteFee = @@ -608,43 +608,40 @@ class FeeInfoInput extends StatelessWidget { initialValue: currentFeeAmount.toString(), decoration: InputDecoration( labelText: 'Fee Amount', - hintText: defaultFee.toString(), + hintText: _formatDecimal(defaultFeeAmount), // Toggle button in prefix prefixIconConstraints: const BoxConstraints( maxWidth: 36, maxHeight: 36, ), - prefixIcon: - isCustomFee - ? IconButton.filledTonal( - iconSize: 20, - tooltip: - isPerKbyteFee - ? 'Switch to fixed fee' - : 'Switch to fee per kilobyte', - icon: Icon(isPerKbyteFee ? Icons.scale : Icons.attach_money), - onPressed: () { - final amount = currentFeeAmount; - if (!isPerKbyteFee) { - // Switch to per KB fee - onFeeSelected( - FeeInfo.utxoPerKbyte(coin: asset.id.id, amount: amount), - ); - } else { - // Switch to fixed fee - onFeeSelected( - FeeInfo.utxoFixed(coin: asset.id.id, amount: amount), - ); - } - }, - ) - : null, + prefixIcon: isCustomFee + ? IconButton.filledTonal( + iconSize: 20, + tooltip: isPerKbyteFee + ? 'Switch to fixed fee' + : 'Switch to fee per kilobyte', + icon: Icon(isPerKbyteFee ? Icons.scale : Icons.tune), + onPressed: () { + final amount = currentFeeAmount; + if (!isPerKbyteFee) { + // Switch to per KB fee + onFeeSelected( + FeeInfo.utxoPerKbyte(coin: asset.id.id, amount: amount), + ); + } else { + // Switch to fixed fee + onFeeSelected( + FeeInfo.utxoFixed(coin: asset.id.id, amount: amount), + ); + } + }, + ) + : null, // Show appropriate suffix based on fee type suffixText: isPerKbyteFee ? '${asset.id.id}/KB' : asset.id.id, - helperText: - isPerKbyteFee - ? 'Fee per kilobyte of transaction size' - : 'Total transaction fee', + helperText: isPerKbyteFee + ? 'Fee per kilobyte of transaction size' + : 'Total transaction fee', ), keyboardType: const TextInputType.numberWithOptions(decimal: true), onChanged: (value) { @@ -663,3 +660,13 @@ class FeeInfoInput extends StatelessWidget { ); } } + +Decimal _atomicToDecimal(int amount, int decimals) { + if (decimals <= 0) return Decimal.fromInt(amount); + final scale = Decimal.parse('1${'0' * decimals}'); + return (Decimal.fromInt(amount) / scale).toDecimal(); +} + +String _formatDecimal(Decimal value, {int precision = 8}) { + return value.toStringAsFixed(precision).replaceAll(RegExp(r'\.?0+$'), ''); +} diff --git a/packages/komodo_ui/lib/src/core/inputs/searchable_select.dart b/packages/komodo_ui/lib/src/core/inputs/searchable_select.dart index 02282e683..92ca04be0 100644 --- a/packages/komodo_ui/lib/src/core/inputs/searchable_select.dart +++ b/packages/komodo_ui/lib/src/core/inputs/searchable_select.dart @@ -469,6 +469,9 @@ Future showDropdownSearch( final renderBox = context.findRenderObject()! as RenderBox; final offset = renderBox.localToGlobal(Offset.zero); final screenHeight = MediaQuery.of(context).size.height; + final scrollable = Scrollable.maybeOf(context); + ScrollPosition? scrollPosition; + VoidCallback? scrollListener; // Check if there's enough space below final spaceBelow = screenHeight - offset.dy - renderBox.size.height; @@ -479,6 +482,9 @@ Future showDropdownSearch( final showAbove = spaceBelow < requiredSpace && offset.dy > spaceBelow; void clearOverlay() { + if (scrollPosition != null && scrollListener != null) { + scrollPosition!.removeListener(scrollListener!); + } _overlayEntry?.remove(); _overlayEntry = null; _completer = null; @@ -492,6 +498,11 @@ Future showDropdownSearch( clearOverlay(); _completer = Completer(); + if (scrollable != null) { + scrollPosition = scrollable.position; + scrollListener = () => onItemSelected(null); + scrollPosition!.addListener(scrollListener!); + } _overlayEntry = OverlayEntry( builder: (context) { // Calculate width based on minWidth parameter if provided diff --git a/packages/komodo_wallet_build_transformer/lib/src/steps/fetch_coin_assets_build_step.dart b/packages/komodo_wallet_build_transformer/lib/src/steps/fetch_coin_assets_build_step.dart index 521fcdfb9..88e152204 100644 --- a/packages/komodo_wallet_build_transformer/lib/src/steps/fetch_coin_assets_build_step.dart +++ b/packages/komodo_wallet_build_transformer/lib/src/steps/fetch_coin_assets_build_step.dart @@ -309,6 +309,9 @@ class FetchCoinAssetsBuildStep extends BuildStep { List _getFilesInFolder(String folderPath) { final localFolder = Directory(folderPath); + if (!localFolder.existsSync()) { + return []; + } final localFolderContents = localFolder.listSync(recursive: true); return localFolderContents .map((FileSystemEntity file) => file.path) diff --git a/packages/komodo_wallet_build_transformer/lib/src/steps/github/github_file_downloader.dart b/packages/komodo_wallet_build_transformer/lib/src/steps/github/github_file_downloader.dart index 473bf3cad..931c492b9 100644 --- a/packages/komodo_wallet_build_transformer/lib/src/steps/github/github_file_downloader.dart +++ b/packages/komodo_wallet_build_transformer/lib/src/steps/github/github_file_downloader.dart @@ -134,14 +134,10 @@ class GitHubFileDownloader { String relativeFilePath, String repoCommit, ) async { - final isRawContentUrl = relativeFilePath.startsWith( - 'https://raw.githubusercontent.com', - ); - - final fileContentUrl = Uri.parse( - isRawContentUrl - ? '$repoContentUrl/$repoCommit/$relativeFilePath' - : '$repoContentUrl/$relativeFilePath', + final fileContentUrl = resolveFileContentUri( + repoContentUrl: repoContentUrl, + filePath: relativeFilePath, + repoCommit: repoCommit, ); _log.fine('Fetching file content from: $fileContentUrl'); @@ -157,13 +153,41 @@ class GitHubFileDownloader { return response.body; } - String _buildFileDownloadUrl(String filePath, String repoCommit) { - final isRawContentUrl = repoContentUrl.contains( + static Uri resolveFileContentUri({ + required String repoContentUrl, + required String filePath, + required String repoCommit, + }) { + final pathUri = Uri.tryParse(filePath); + if (pathUri != null && + (pathUri.scheme.toLowerCase() == 'http' || + pathUri.scheme.toLowerCase() == 'https')) { + return pathUri; + } + + final normalizedBase = repoContentUrl.endsWith('/') + ? repoContentUrl.substring(0, repoContentUrl.length - 1) + : repoContentUrl; + final normalizedPath = filePath.startsWith('/') + ? filePath.substring(1) + : filePath; + + final isRawContentUrl = normalizedBase.contains( 'raw.githubusercontent.com', ); - return isRawContentUrl - ? '$repoContentUrl/$repoCommit/$filePath' - : '$repoContentUrl/$filePath'; + final resolvedUrl = isRawContentUrl + ? '$normalizedBase/$repoCommit/$normalizedPath' + : '$normalizedBase/$normalizedPath'; + + return Uri.parse(resolvedUrl); + } + + String _buildFileDownloadUrl(String filePath, String repoCommit) { + return resolveFileContentUri( + repoContentUrl: repoContentUrl, + filePath: filePath, + repoCommit: repoCommit, + ).toString(); } /// Downloads the mapped folders from a GitHub repository at a specific commit @@ -191,13 +215,12 @@ class GitHubFileDownloader { repoCommit, ); - final List> downloadFutures = - folderContents.entries.map((entry) async { - _log.fine( - 'Downloading ${entry.value.length} files from ${entry.key}', - ); - await _downloadFolderContents(entry.key, entry.value, repoCommit); - }).toList(); + final List> downloadFutures = folderContents.entries.map(( + entry, + ) async { + _log.fine('Downloading ${entry.value.length} files from ${entry.key}'); + await _downloadFolderContents(entry.key, entry.value, repoCommit); + }).toList(); await Future.wait(downloadFutures); @@ -240,14 +263,13 @@ class GitHubFileDownloader { List value, String repoCommit, ) async { - final filesWithCdn = - value - .map( - (file) => file.copyWith( - downloadUrl: _buildFileDownloadUrl(file.path, repoCommit), - ), - ) - .toList(); + final filesWithCdn = value + .map( + (file) => file.copyWith( + downloadUrl: _buildFileDownloadUrl(file.path, repoCommit), + ), + ) + .toList(); await for (final GitHubFileDownloadEvent event in downloadFiles( filesWithCdn, @@ -277,34 +299,31 @@ class GitHubFileDownloader { List value, String repoCommit, ) async { - final List> downloadFutures = - value.map((file) async { - final fileWithCdn = file.copyWith( - downloadUrl: _buildFileDownloadUrl(file.path, repoCommit), - ); - await for (final GitHubFileDownloadEvent event in downloadFiles([ - fileWithCdn, - ], key)) { - switch (event.event) { - case GitHubDownloadEvent.downloaded: - _downloadedFiles++; - sendProgressMessage( - 'Downloading file: ${event.localPath}', - success: true, - ); - case GitHubDownloadEvent.skipped: - _skippedFiles++; - sendProgressMessage( - 'Skipped file: ${event.localPath}', - success: true, - ); - case GitHubDownloadEvent.failed: - sendProgressMessage( - 'Failed to download file: ${event.localPath}', - ); - } - } - }).toList(); + final List> downloadFutures = value.map((file) async { + final fileWithCdn = file.copyWith( + downloadUrl: _buildFileDownloadUrl(file.path, repoCommit), + ); + await for (final GitHubFileDownloadEvent event in downloadFiles([ + fileWithCdn, + ], key)) { + switch (event.event) { + case GitHubDownloadEvent.downloaded: + _downloadedFiles++; + sendProgressMessage( + 'Downloading file: ${event.localPath}', + success: true, + ); + case GitHubDownloadEvent.skipped: + _skippedFiles++; + sendProgressMessage( + 'Skipped file: ${event.localPath}', + success: true, + ); + case GitHubDownloadEvent.failed: + sendProgressMessage('Failed to download file: ${event.localPath}'); + } + } + }).toList(); await Future.wait(downloadFutures); } diff --git a/packages/komodo_wallet_build_transformer/test/steps/github/github_file_downloader_test.dart b/packages/komodo_wallet_build_transformer/test/steps/github/github_file_downloader_test.dart index 9eadc76c8..523459199 100644 --- a/packages/komodo_wallet_build_transformer/test/steps/github/github_file_downloader_test.dart +++ b/packages/komodo_wallet_build_transformer/test/steps/github/github_file_downloader_test.dart @@ -157,6 +157,52 @@ void main() { }, ); + test('should include commit segment for raw GitHub content URLs', () { + final uri = GitHubFileDownloader.resolveFileContentUri( + repoContentUrl: 'https://raw.githubusercontent.com/GLEECBTC/coins', + filePath: 'utils/coins_config_unfiltered.json', + repoCommit: 'json-config-update', + ); + + expect( + uri.toString(), + equals( + 'https://raw.githubusercontent.com/GLEECBTC/coins/json-config-update/utils/coins_config_unfiltered.json', + ), + ); + }); + + test('should not include commit segment for CDN content URLs', () { + final uri = GitHubFileDownloader.resolveFileContentUri( + repoContentUrl: 'https://gleecbtc.github.io/coins', + filePath: 'utils/coins_config_unfiltered.json', + repoCommit: 'json-config-update', + ); + + expect( + uri.toString(), + equals( + 'https://gleecbtc.github.io/coins/utils/coins_config_unfiltered.json', + ), + ); + }); + + test('should preserve absolute file URLs as-is', () { + final uri = GitHubFileDownloader.resolveFileContentUri( + repoContentUrl: 'https://raw.githubusercontent.com/GLEECBTC/coins', + filePath: + 'https://raw.githubusercontent.com/GLEECBTC/coins/master/seed-nodes.json', + repoCommit: 'json-config-update', + ); + + expect( + uri.toString(), + equals( + 'https://raw.githubusercontent.com/GLEECBTC/coins/master/seed-nodes.json', + ), + ); + }); + test( 'should handle commit hash vs branch name correctly for CDN URLs', () { diff --git a/playground/lib/kdf_operations/kdf_operations_server_native.dart b/playground/lib/kdf_operations/kdf_operations_server_native.dart index 295db53b3..f8aaa21d3 100644 --- a/playground/lib/kdf_operations/kdf_operations_server_native.dart +++ b/playground/lib/kdf_operations/kdf_operations_server_native.dart @@ -48,7 +48,10 @@ class KdfHttpServerOperations implements IKdfOperations { Future isAvailable(IKdfHostConfig hostConfig) async { throw UnsupportedError('Unknown platforms are not supported'); } - + + @override + void resetHttpClient() {} + @override - void dispose() { } + void dispose() {} } diff --git a/playground/lib/kdf_operations/kdf_operations_server_stub.dart b/playground/lib/kdf_operations/kdf_operations_server_stub.dart index 295db53b3..f8aaa21d3 100644 --- a/playground/lib/kdf_operations/kdf_operations_server_stub.dart +++ b/playground/lib/kdf_operations/kdf_operations_server_stub.dart @@ -48,7 +48,10 @@ class KdfHttpServerOperations implements IKdfOperations { Future isAvailable(IKdfHostConfig hostConfig) async { throw UnsupportedError('Unknown platforms are not supported'); } - + + @override + void resetHttpClient() {} + @override - void dispose() { } + void dispose() {} } diff --git a/playground/lib/kdf_operations/kdf_operations_server_web.dart b/playground/lib/kdf_operations/kdf_operations_server_web.dart index 581d1fe95..d3f0e69d8 100644 --- a/playground/lib/kdf_operations/kdf_operations_server_web.dart +++ b/playground/lib/kdf_operations/kdf_operations_server_web.dart @@ -42,7 +42,8 @@ class KdfHttpServerOperations implements IKdfOperations { // The mm2_main function now returns a Promise that resolves to a result code // or rejects with a structured error. We need to handle both cases. final resultStr = await controller.evaluateJavascript( - source: ''' + source: + ''' (async function() { try { const result = await kdf.mm2_main(${jsonEncode(jsConfig)}); @@ -165,7 +166,8 @@ class KdfHttpServerOperations implements IKdfOperations { try { final responseStr = await controller.evaluateJavascript( - source: ''' + source: + ''' (async function() { try { const result = await kdf.mm2_rpc(${jsonEncode(request)}); @@ -211,6 +213,9 @@ class KdfHttpServerOperations implements IKdfOperations { } } + @override + void resetHttpClient() {} + Future _ensureWebViewInitialized() async { if (_isInitialized) return; @@ -249,6 +254,7 @@ class KdfHttpServerOperations implements IKdfOperations { _logger?.call('HTTP server running at http://localhost:8080'); } + @override void dispose() { _webView.dispose(); _server.close(); From 6cb17110f3859bed2d838ab577d6244b96b6c6dd Mon Sep 17 00:00:00 2001 From: Nitride <77973576+CharlVS@users.noreply.github.com> Date: Thu, 12 Mar 2026 12:58:09 +0100 Subject: [PATCH 02/40] fix(web): complete wasm-safe sdk interop cleanup (#313) --- .../operations/kdf_operations_factory.dart | 2 +- .../event_streaming_platform_web.dart | 44 +++++++------------ .../streaming/event_streaming_service.dart | 34 +++++++------- packages/komodo_defi_framework/pubspec.yaml | 1 - .../kdf_operations/kdf_operations_server.dart | 2 +- 5 files changed, 32 insertions(+), 51 deletions(-) diff --git a/packages/komodo_defi_framework/lib/src/operations/kdf_operations_factory.dart b/packages/komodo_defi_framework/lib/src/operations/kdf_operations_factory.dart index ba30f9f8c..670c346cf 100644 --- a/packages/komodo_defi_framework/lib/src/operations/kdf_operations_factory.dart +++ b/packages/komodo_defi_framework/lib/src/operations/kdf_operations_factory.dart @@ -3,7 +3,7 @@ import 'package:komodo_defi_framework/src/operations/kdf_operations_interface.da import 'package:komodo_defi_framework/src/operations/kdf_operations_remote.dart'; import 'package:komodo_defi_framework/src/operations/kdf_operations_wasm.dart' if (dart.library.io) 'package:komodo_defi_framework/src/operations/kdf_operations_native.dart' - if (dart.library.html) 'package:komodo_defi_framework/src/operations/kdf_operations_wasm.dart' + if (dart.library.js_interop) 'package:komodo_defi_framework/src/operations/kdf_operations_wasm.dart' as local; IKdfOperations createKdfOperations({ diff --git a/packages/komodo_defi_framework/lib/src/streaming/event_streaming_platform_web.dart b/packages/komodo_defi_framework/lib/src/streaming/event_streaming_platform_web.dart index 700df7f3e..4aa78e0a8 100644 --- a/packages/komodo_defi_framework/lib/src/streaming/event_streaming_platform_web.dart +++ b/packages/komodo_defi_framework/lib/src/streaming/event_streaming_platform_web.dart @@ -1,29 +1,20 @@ // Web implementation: connect to SharedWorker('event_streaming_worker.js') // and forward messages to Dart via the provided callback. -// ignore: avoid_web_libraries_in_flutter -import 'dart:html' as html show Event; +import 'dart:js_interop'; +import 'dart:js_interop_unsafe'; + import 'package:flutter/foundation.dart'; -import 'package:js/js_util.dart' as jsu; +import 'package:web/web.dart' as web; import 'package:komodo_defi_framework/src/config/kdf_config.dart'; typedef EventStreamUnsubscribe = void Function(); -Object _getGlobalProperty(String name) => - jsu.getProperty(jsu.globalThis, name); - -Object? _getProperty(Object o, String name) => - jsu.getProperty(o, name); - -void _setProperty(Object o, String name, Object? value) => - jsu.setProperty(o, name, value); - -T _callConstructor(Object ctor, List args) => - jsu.callConstructor(ctor, args) as T; +const _eventStreamingWorkerPath = + 'assets/packages/komodo_defi_framework/assets/web/event_streaming_worker.js'; -T _callMethod(Object o, String name, List args) => - jsu.callMethod(o, name, args) as T; +final web.EventHandler _noopHandler = ((web.Event _) {}).toJS; EventStreamUnsubscribe connectEventStream({ IKdfHostConfig? hostConfig, @@ -31,19 +22,14 @@ EventStreamUnsubscribe connectEventStream({ required void Function() onFirstByte, }) { try { - final Object sharedWorkerCtor = _getGlobalProperty('SharedWorker'); - final Object worker = _callConstructor(sharedWorkerCtor, [ - 'assets/packages/komodo_defi_framework/assets/web/event_streaming_worker.js', - ]); - final Object? portMaybe = _getProperty(worker, 'port'); - if (portMaybe == null) return () {}; - final Object port = portMaybe; - _callMethod(port, 'start', const []); + final worker = web.SharedWorker(_eventStreamingWorkerPath.toJS); + final port = worker.port; + port.start(); bool firstMessageReceived = false; - void handler(html.Event e) { - final Object? data = _getProperty(e, 'data'); + void handler(web.Event event) { + final data = event is web.MessageEvent ? event.data.dartify() : null; // Signal first byte received on first message if (!firstMessageReceived) { @@ -57,12 +43,12 @@ EventStreamUnsubscribe connectEventStream({ onMessage(data); } - _setProperty(port, 'onmessage', jsu.allowInterop(handler)); + port.onmessage = handler.toJS; return () { try { - _setProperty(port, 'onmessage', null); - _callMethod(port, 'close', const []); + port.onmessage = _noopHandler; + port.close(); } catch (_) {} }; } catch (_) { diff --git a/packages/komodo_defi_framework/lib/src/streaming/event_streaming_service.dart b/packages/komodo_defi_framework/lib/src/streaming/event_streaming_service.dart index e52e4075e..b7ca910e7 100644 --- a/packages/komodo_defi_framework/lib/src/streaming/event_streaming_service.dart +++ b/packages/komodo_defi_framework/lib/src/streaming/event_streaming_service.dart @@ -8,17 +8,13 @@ import 'package:flutter/foundation.dart'; import 'package:komodo_defi_framework/src/config/kdf_config.dart'; import 'package:komodo_defi_framework/src/streaming/event_streaming_platform_stub.dart' if (dart.library.io) 'package:komodo_defi_framework/src/streaming/event_streaming_platform_io.dart' - if (dart.library.html) 'package:komodo_defi_framework/src/streaming/event_streaming_platform_web.dart'; + if (dart.library.js_interop) 'package:komodo_defi_framework/src/streaming/event_streaming_platform_web.dart'; import 'package:komodo_defi_framework/src/streaming/events/kdf_event.dart'; import 'package:komodo_defi_types/komodo_defi_type_utils.dart'; typedef EventPredicate = bool Function(KdfEvent event); -enum SseConnectionState { - disconnected, - connecting, - connected, -} +enum SseConnectionState { disconnected, connecting, connected } class KdfEventStreamingService { KdfEventStreamingService({IKdfHostConfig? hostConfig}) @@ -31,31 +27,31 @@ class KdfEventStreamingService { SseConnectionState _connectionState = SseConnectionState.disconnected; Stream get events => _events.stream; - + /// Future that completes when the first byte is received from the SSE stream. /// This indicates the server's event loop is fully flowing and the client is registered. Future get firstByteReceived => _firstByteCompleter.future; - + /// Current connection state SseConnectionState get connectionState => _connectionState; - + /// Whether the SSE connection is currently connected bool get isConnected => _connectionState == SseConnectionState.connected; /// Start listening to stream events. /// - Web: Connects to SharedWorker forwarded messages. /// - Native (IO): Connects to SSE endpoint exposed by KDF RPC server. - /// + /// /// DEPRECATED: Use connectIfNeeded() instead. This method is kept for backward compatibility /// but should not be called at app startup. SSE connection should be tied to authentication state. @Deprecated('Use connectIfNeeded() instead') void initialize() { connectIfNeeded(); } - + /// Ensures SSE connection is established if not already connected. /// This method is idempotent and can be called multiple times safely. - /// + /// /// Should be called: /// - After user authentication completes /// - Before attempting enable_* RPC calls @@ -65,35 +61,35 @@ class KdfEventStreamingService { // Already connecting or connected return; } - + _connectionState = SseConnectionState.connecting; _log('SSE Connect: Initiating connection...'); - + _unsubscribe ??= connectEventStream( hostConfig: _hostConfig, onMessage: _onIncomingData, onFirstByte: _onFirstByte, ); } - + /// Disconnect the SSE connection. /// Should be called when user signs out. void disconnect() { if (_connectionState == SseConnectionState.disconnected) { return; } - + _log('SSE Disconnect: Closing connection...'); _unsubscribe?.call(); _unsubscribe = null; _connectionState = SseConnectionState.disconnected; - + // Reset first byte completer for next connection if (_firstByteCompleter.isCompleted) { _firstByteCompleter = Completer(); } } - + void _onFirstByte() { if (!_firstByteCompleter.isCompleted) { _firstByteCompleter.complete(); @@ -101,7 +97,7 @@ class KdfEventStreamingService { _log('SSE Connect: First byte received, connection established'); } } - + void _log(String message) { if (kDebugMode) { print('[EventStreamingService] $message'); diff --git a/packages/komodo_defi_framework/pubspec.yaml b/packages/komodo_defi_framework/pubspec.yaml index f85d79c29..e506ae75f 100644 --- a/packages/komodo_defi_framework/pubspec.yaml +++ b/packages/komodo_defi_framework/pubspec.yaml @@ -41,7 +41,6 @@ dev_dependencies: flutter_lints: ^6.0.0 flutter_test: sdk: flutter - js: any very_good_analysis: ^9.0.0 yaml: ^3.1.2 diff --git a/playground/lib/kdf_operations/kdf_operations_server.dart b/playground/lib/kdf_operations/kdf_operations_server.dart index 306dbaf74..2da7fb63f 100644 --- a/playground/lib/kdf_operations/kdf_operations_server.dart +++ b/playground/lib/kdf_operations/kdf_operations_server.dart @@ -1,3 +1,3 @@ export './kdf_operations_server_stub.dart' - if (dart.library.html) './kdf_operations_server_web.dart' + if (dart.library.js_interop) './kdf_operations_server_web.dart' if (dart.library.io) './kdf_operations_server_native.dart'; From 54e1420f8f657414168555a849d9041ca3eea5b9 Mon Sep 17 00:00:00 2001 From: Nitride <77973576+CharlVS@users.noreply.github.com> Date: Thu, 12 Mar 2026 17:05:34 +0100 Subject: [PATCH 03/40] feat(sdk): add high-level balance/transaction manager interfaces (#314) * feat(sdk): add high-level balance and tx manager interfaces * fix(sdk): address pr review cancellation and ordering bugs --- .../komodo_defi_sdk/lib/komodo_defi_sdk.dart | 2 + .../src/activation/activation_exceptions.dart | 15 ++ .../src/activation/activation_manager.dart | 75 ++++++- .../lib/src/assets/asset_manager.dart | 18 ++ .../lib/src/balances/balance_manager.dart | 33 ++- .../_transaction_history_index.dart | 1 + .../transaction_history_manager.dart | 38 ++++ .../transaction_merge_utils.dart | 191 ++++++++++++++++++ 8 files changed, 361 insertions(+), 12 deletions(-) create mode 100644 packages/komodo_defi_sdk/lib/src/transaction_history/transaction_merge_utils.dart diff --git a/packages/komodo_defi_sdk/lib/komodo_defi_sdk.dart b/packages/komodo_defi_sdk/lib/komodo_defi_sdk.dart index 22ea5671b..b612cae13 100644 --- a/packages/komodo_defi_sdk/lib/komodo_defi_sdk.dart +++ b/packages/komodo_defi_sdk/lib/komodo_defi_sdk.dart @@ -59,6 +59,8 @@ export 'src/assets/asset_extensions.dart' export 'src/assets/asset_pubkey_extensions.dart'; export 'src/assets/legacy_asset_extensions.dart'; export 'src/komodo_defi_sdk.dart' show KomodoDefiSdk; +export 'src/transaction_history/transaction_merge_utils.dart' + show TransactionListReconciler, TransactionMergeUtils; export 'src/widgets/asset_balance_text.dart'; export 'src/zcash_params/models/download_progress.dart'; export 'src/zcash_params/models/download_result.dart'; diff --git a/packages/komodo_defi_sdk/lib/src/activation/activation_exceptions.dart b/packages/komodo_defi_sdk/lib/src/activation/activation_exceptions.dart index fb7420111..6a0237d0e 100644 --- a/packages/komodo_defi_sdk/lib/src/activation/activation_exceptions.dart +++ b/packages/komodo_defi_sdk/lib/src/activation/activation_exceptions.dart @@ -70,3 +70,18 @@ class ActivationNetworkException extends ActivationFailedException { return 'ActivationNetworkException: Asset ${assetId.name} activation failed due to network issues: $message'; } } + +/// Exception thrown when an activation is cancelled by caller or lifecycle. +class ActivationCancelledException extends ActivationFailedException { + const ActivationCancelledException({ + required super.assetId, + required super.message, + super.errorCode = 'ACTIVATION_CANCELLED', + super.originalError, + }); + + @override + String toString() { + return 'ActivationCancelledException: Asset ${assetId.name} activation cancelled: $message'; + } +} diff --git a/packages/komodo_defi_sdk/lib/src/activation/activation_manager.dart b/packages/komodo_defi_sdk/lib/src/activation/activation_manager.dart index 09091da0b..fd061e571 100644 --- a/packages/komodo_defi_sdk/lib/src/activation/activation_manager.dart +++ b/packages/komodo_defi_sdk/lib/src/activation/activation_manager.dart @@ -39,6 +39,7 @@ class ActivationManager { static const SdkErrorMapper _errorMapper = SdkErrorMapper(); final Map> _activationCompleters = {}; + final Map _cancelledActivations = {}; bool _isDisposed = false; /// Helper for mutex-protected operations with timeout @@ -56,6 +57,35 @@ class ActivationManager { Stream activateAsset(Asset asset) => activateAssets([asset]); + /// Request cancellation of an in-flight activation for [assetId]. + /// + /// Cancellation is best-effort. The current activation stream is terminated + /// at the next progress boundary and emits an error completion state. + void cancelActivation( + AssetId assetId, { + String reason = 'Activation cancelled by caller', + }) { + if (_isDisposed) return; + // Only record cancellation for activations that are currently in-flight. + // This avoids stale cancellation markers cancelling future fresh attempts. + if (!_activationCompleters.containsKey(assetId)) { + _cancelledActivations.remove(assetId); + return; + } + _cancelledActivations[assetId] = reason; + } + + /// Request cancellation for all in-flight activations. + void cancelAllActivations({ + String reason = 'Activation cancelled by caller', + }) { + if (_isDisposed) return; + final pendingIds = _activationCompleters.keys.toList(); + for (final assetId in pendingIds) { + _cancelledActivations[assetId] = reason; + } + } + /// Activate multiple assets Stream activateAssets(List assets) async* { if (_isDisposed) { @@ -65,6 +95,18 @@ class ActivationManager { final groups = _AssetGroup._groupByPrimary(assets); for (final group in groups) { + if (_cancelledActivations.containsKey(group.primary.id)) { + final reason = + _cancelledActivations[group.primary.id] ?? + 'Activation cancelled by caller'; + yield ActivationProgress.error( + message: reason, + errorCode: 'ACTIVATION_CANCELLED', + ); + _cancelledActivations.remove(group.primary.id); + continue; + } + // Check activation status atomically final activationStatus = await _checkActivationStatus(group); if (activationStatus.isComplete) { @@ -117,6 +159,24 @@ class ActivationManager { parentAsset ?? group.primary, group.children?.toList(), )) { + if (_cancelledActivations.containsKey(group.primary.id)) { + final reason = + _cancelledActivations[group.primary.id] ?? + 'Activation cancelled by caller'; + final cancellationError = ActivationCancelledException( + assetId: group.primary.id, + message: reason, + ); + if (!primaryCompleter.isCompleted) { + primaryCompleter.completeError(cancellationError); + } + yield ActivationProgress.error( + message: reason, + errorCode: 'ACTIVATION_CANCELLED', + ); + break; + } + yield _attachSdkError(progress, group.primary.id); if (progress.isComplete) { @@ -149,10 +209,7 @@ class ActivationManager { } final errorMessage = progress.errorMessage ?? 'Activation failed'; - final sdkError = _mapError( - errorMessage, - assetId, - ); + final sdkError = _mapError(errorMessage, assetId); return progress.copyWith( errorMessage: sdkError.fallbackMessage, @@ -163,10 +220,7 @@ class ActivationManager { SdkError _mapError(Object error, AssetId assetId) { return _errorMapper.map( error, - context: SdkErrorContext( - operation: 'activation', - assetId: assetId.id, - ), + context: SdkErrorContext(operation: 'activation', assetId: assetId.id), ); } @@ -174,7 +228,8 @@ class ActivationManager { Future _checkActivationStatus(_AssetGroup group) async { try { // Use cache instead of direct RPC call to avoid excessive requests - final enabledAssetIds = await _activatedAssetsCache.getActivatedAssetIds(); + final enabledAssetIds = await _activatedAssetsCache + .getActivatedAssetIds(); final isActive = enabledAssetIds.contains(group.primary.id); final childrenActive = @@ -264,6 +319,7 @@ class ActivationManager { Future _cleanupActivation(AssetId assetId) async { await _protectedOperation(() async { _activationCompleters.remove(assetId); + _cancelledActivations.remove(assetId); }); } @@ -319,6 +375,7 @@ class ActivationManager { } _activationCompleters.clear(); + _cancelledActivations.clear(); }); } } diff --git a/packages/komodo_defi_sdk/lib/src/assets/asset_manager.dart b/packages/komodo_defi_sdk/lib/src/assets/asset_manager.dart index ea19febfc..0015fd552 100644 --- a/packages/komodo_defi_sdk/lib/src/assets/asset_manager.dart +++ b/packages/komodo_defi_sdk/lib/src/assets/asset_manager.dart @@ -214,6 +214,24 @@ class AssetManager implements IAssetProvider { Stream activateAssets(List assets) => _activationManager().activateAssets(assets); + /// Requests cancellation of an in-flight activation for [assetId]. + /// + /// Cancellation is best-effort and will be observed on the next activation + /// progress boundary. + void cancelActivation( + AssetId assetId, { + String reason = 'Activation cancelled by caller', + }) { + _activationManager().cancelActivation(assetId, reason: reason); + } + + /// Requests cancellation for all in-flight activation tasks. + void cancelAllActivations({ + String reason = 'Activation cancelled by caller', + }) { + _activationManager().cancelAllActivations(reason: reason); + } + /// Disposes of the asset manager, cleaning up resources. /// /// This is called automatically by the SDK when disposing. diff --git a/packages/komodo_defi_sdk/lib/src/balances/balance_manager.dart b/packages/komodo_defi_sdk/lib/src/balances/balance_manager.dart index db4738878..fe2d2b04b 100644 --- a/packages/komodo_defi_sdk/lib/src/balances/balance_manager.dart +++ b/packages/komodo_defi_sdk/lib/src/balances/balance_manager.dart @@ -37,6 +37,15 @@ abstract class IBalanceManager { bool activateIfNeeded = true, }); + /// Returns whether [assetId] currently has an active watcher subscription. + bool hasActiveWatcher(AssetId assetId); + + /// Counts how many [assetIds] do not currently have active watcher coverage. + int countMissingWatchersForAssets(Iterable assetIds); + + /// Returns true when any asset in [assetIds] lacks active watcher coverage. + bool hasMissingWatchersForAssets(Iterable assetIds); + /// Gets the last known balance for an asset without triggering a refresh. /// Returns null if no balance has been fetched yet. BalanceInfo? lastKnown(AssetId assetId); @@ -117,6 +126,26 @@ class BalanceManager implements IBalanceManager { /// Getter for pubkeyManager to make it accessible PubkeyManager? get pubkeyManager => _pubkeyManager; + @override + bool hasActiveWatcher(AssetId assetId) { + if (_isDisposed) return false; + return _activeWatchers.containsKey(assetId); + } + + @override + int countMissingWatchersForAssets(Iterable assetIds) { + if (_isDisposed) return assetIds.toSet().length; + final uniqueIds = assetIds.toSet(); + return uniqueIds + .where((assetId) => !_activeWatchers.containsKey(assetId)) + .length; + } + + @override + bool hasMissingWatchersForAssets(Iterable assetIds) { + return countMissingWatchersForAssets(assetIds) > 0; + } + /// Setter for activationCoordinator to resolve circular dependencies void setActivationCoordinator(SharedActivationCoordinator coordinator) { _activationCoordinator = coordinator; @@ -837,9 +866,7 @@ class BalanceManager implements IBalanceManager { // Snapshot controllers and close all concurrently; swallow errors final List> pubkeyHintSubs = - List>.from( - _pubkeyHintWatchers.values, - ); + List>.from(_pubkeyHintWatchers.values); _pubkeyHintWatchers.clear(); for (final StreamSubscription sub in pubkeyHintSubs) { cancelFutures.add( diff --git a/packages/komodo_defi_sdk/lib/src/transaction_history/_transaction_history_index.dart b/packages/komodo_defi_sdk/lib/src/transaction_history/_transaction_history_index.dart index e0210c00d..21d427799 100644 --- a/packages/komodo_defi_sdk/lib/src/transaction_history/_transaction_history_index.dart +++ b/packages/komodo_defi_sdk/lib/src/transaction_history/_transaction_history_index.dart @@ -6,5 +6,6 @@ library _transaction_history; export 'strategies/etherscan_transaction_history_strategy.dart'; export 'strategies/zhtlc_transaction_strategy.dart'; export 'transaction_history_manager.dart'; +export 'transaction_merge_utils.dart'; export 'transaction_history_strategies.dart'; export 'transaction_storage.dart'; diff --git a/packages/komodo_defi_sdk/lib/src/transaction_history/transaction_history_manager.dart b/packages/komodo_defi_sdk/lib/src/transaction_history/transaction_history_manager.dart index 8383ce631..a3f09b494 100644 --- a/packages/komodo_defi_sdk/lib/src/transaction_history/transaction_history_manager.dart +++ b/packages/komodo_defi_sdk/lib/src/transaction_history/transaction_history_manager.dart @@ -32,6 +32,15 @@ abstract interface class _TransactionHistoryManager { /// with the initial batch from storage and the latest transactions from /// the API. The stream will close when the latest transaction is reached. Stream> getTransactionsStreamed(Asset asset); + + /// High-level merged history stream for UI list consumption. + /// + /// This stream handles update reconciliation, pending->confirmed bridging, + /// and stable ordering internally. + Stream> watchTransactionHistoryMerged( + Asset asset, { + Transaction Function(Transaction transaction)? transform, + }); } class TransactionHistoryManager implements _TransactionHistoryManager { @@ -336,6 +345,35 @@ class TransactionHistoryManager implements _TransactionHistoryManager { } } + @override + Stream> watchTransactionHistoryMerged( + Asset asset, { + Transaction Function(Transaction transaction)? transform, + }) async* { + final reconciler = TransactionListReconciler(); + var merged = []; + var emittedInitial = false; + + await for (final batch in getTransactionsStreamed(asset)) { + final incoming = transform == null + ? batch + : batch.map(transform).toList(growable: false); + merged = reconciler.merge(existing: merged, incoming: incoming); + emittedInitial = true; + yield List.unmodifiable(merged); + } + + if (!emittedInitial) { + yield const []; + } + + await for (final transaction in watchTransactions(asset)) { + final normalized = transform?.call(transaction) ?? transaction; + merged = reconciler.merge(existing: merged, incoming: [normalized]); + yield List.unmodifiable(merged); + } + } + @override Stream watchTransactions(Asset asset) { if (_isDisposed) { diff --git a/packages/komodo_defi_sdk/lib/src/transaction_history/transaction_merge_utils.dart b/packages/komodo_defi_sdk/lib/src/transaction_history/transaction_merge_utils.dart new file mode 100644 index 000000000..693c7a99b --- /dev/null +++ b/packages/komodo_defi_sdk/lib/src/transaction_history/transaction_merge_utils.dart @@ -0,0 +1,191 @@ +import 'package:collection/collection.dart'; +import 'package:komodo_defi_types/komodo_defi_types.dart'; + +/// Shared helpers for reconciling transaction lifecycle updates in clients. +/// +/// This utility keeps identity stable by internal transaction ID while still +/// supporting a Tendermint-specific pending->confirmed bridge when a matching +/// event is re-emitted with a different internal ID. +class TransactionMergeUtils { + TransactionMergeUtils._(); + + static const ListEquality _listEquality = ListEquality(); + + /// Canonical lifecycle key for transaction list reconciliation. + static String transactionKey(Transaction transaction) { + return transaction.internalId; + } + + /// Merge commonly-updated fields from [incoming] into [existing]. + static Transaction mergeTransactionFields( + Transaction existing, + Transaction incoming, + ) { + return existing.copyWith( + confirmations: incoming.confirmations, + blockHeight: incoming.blockHeight, + fee: incoming.fee ?? existing.fee, + memo: incoming.memo ?? existing.memo, + timestamp: incoming.timestamp, + ); + } + + static bool isTendermintAsset(AssetId assetId) { + return assetId.subClass == CoinSubClass.tendermint || + assetId.subClass == CoinSubClass.tendermintToken; + } + + static bool isConfirmed(Transaction transaction) { + return transaction.confirmations > 0 || transaction.blockHeight > 0; + } + + static bool isPending(Transaction transaction) { + return transaction.confirmations <= 0 && transaction.blockHeight == 0; + } + + static bool matchesTransferFingerprint( + Transaction first, + Transaction second, + ) { + return _listEquality.equals(first.from, second.from) && + _listEquality.equals(first.to, second.to) && + first.balanceChanges.netChange == second.balanceChanges.netChange && + first.balanceChanges.totalAmount == second.balanceChanges.totalAmount; + } + + /// Finds a pending transaction key that should be replaced by [incoming]. + /// + /// This bridge is intentionally limited to Tendermint/TendermintToken assets + /// with exactly one fingerprint match to avoid collapsing distinct transfers + /// that share the same hash. + static String? findPendingReplacementKey({ + required Map byKey, + required Transaction incoming, + }) { + if (!isTendermintAsset(incoming.assetId) || !isConfirmed(incoming)) { + return null; + } + + final txHash = incoming.txHash; + if (txHash == null || txHash.isEmpty) { + return null; + } + + final matchingEntries = byKey.entries.where((entry) { + final existing = entry.value; + return existing.assetId.isSameAsset(incoming.assetId) && + isPending(existing) && + existing.txHash == txHash && + matchesTransferFingerprint(existing, incoming); + }).toList(); + + if (matchingEntries.length != 1) { + return null; + } + + return matchingEntries.first.key; + } +} + +/// Stateful reconciler for merging transaction update batches into a list view. +class TransactionListReconciler { + final Map _firstSeenAtByInternalId = {}; + + /// Clears internal ordering state. + void reset() => _firstSeenAtByInternalId.clear(); + + /// Merges [incoming] updates into [existing] and returns sorted results. + List merge({ + required List existing, + required Iterable incoming, + }) { + final byKey = { + for (final tx in existing) TransactionMergeUtils.transactionKey(tx): tx, + }; + + for (final tx in incoming) { + _mergeInPlace(byKey, tx); + _firstSeenAtByInternalId.putIfAbsent( + tx.internalId, + () => tx.timestamp.millisecondsSinceEpoch != 0 + ? tx.timestamp + : DateTime.now(), + ); + } + + final merged = byKey.values.toList()..sort(_compareTransactions); + return merged; + } + + void _mergeInPlace(Map byKey, Transaction incoming) { + final incomingKey = TransactionMergeUtils.transactionKey(incoming); + final existing = byKey[incomingKey]; + + if (existing != null) { + byKey[incomingKey] = TransactionMergeUtils.mergeTransactionFields( + existing, + incoming, + ); + return; + } + + final pendingReplacementKey = + TransactionMergeUtils.findPendingReplacementKey( + byKey: byKey, + incoming: incoming, + ); + + if (pendingReplacementKey != null) { + final pending = byKey.remove(pendingReplacementKey); + if (pending != null) { + final mergedPending = TransactionMergeUtils.mergeTransactionFields( + pending, + incoming, + ).copyWith(internalId: incoming.internalId); + + final pendingFirstSeen = _firstSeenAtByInternalId.remove( + pendingReplacementKey, + ); + if (pendingFirstSeen != null) { + _firstSeenAtByInternalId.putIfAbsent( + incoming.internalId, + () => pendingFirstSeen, + ); + } + + byKey[incomingKey] = mergedPending; + return; + } + } + + byKey[incomingKey] = incoming; + } + + int _compareTransactions(Transaction left, Transaction right) { + final unconfirmedTimestamp = DateTime.fromMillisecondsSinceEpoch(0); + final leftIsUnconfirmed = left.timestamp == unconfirmedTimestamp; + final rightIsUnconfirmed = right.timestamp == unconfirmedTimestamp; + + if (leftIsUnconfirmed && rightIsUnconfirmed) { + final leftFirstSeen = + _firstSeenAtByInternalId[left.internalId] ?? unconfirmedTimestamp; + final rightFirstSeen = + _firstSeenAtByInternalId[right.internalId] ?? unconfirmedTimestamp; + final compareByFirstSeen = rightFirstSeen.compareTo(leftFirstSeen); + if (compareByFirstSeen != 0) { + return compareByFirstSeen; + } + return right.internalId.compareTo(left.internalId); + } + + if (leftIsUnconfirmed) { + return -1; + } + + if (rightIsUnconfirmed) { + return 1; + } + + return right.timestamp.compareTo(left.timestamp); + } +} From 9388a405c5b5e6e9ee42088e7a536e0e89f10909 Mon Sep 17 00:00:00 2001 From: Nitride <77973576+CharlVS@users.noreply.github.com> Date: Mon, 16 Mar 2026 18:35:39 +0100 Subject: [PATCH 04/40] fix(web): improve wasm JS interop bindings (#315) --- .../lib/src/storage/opfs_interop.dart | 62 +++--- .../app_build/build_config.json | 2 +- .../lib/src/js/js_interop_utils.dart | 28 +-- .../src/operations/kdf_operations_wasm.dart | 176 ++++++++---------- .../event_streaming_platform_web.dart | 20 +- .../web/res/kdf_wrapper.dart | 23 ++- 6 files changed, 151 insertions(+), 160 deletions(-) diff --git a/packages/dragon_logs/lib/src/storage/opfs_interop.dart b/packages/dragon_logs/lib/src/storage/opfs_interop.dart index 78715d52a..fa291972f 100644 --- a/packages/dragon_logs/lib/src/storage/opfs_interop.dart +++ b/packages/dragon_logs/lib/src/storage/opfs_interop.dart @@ -1,20 +1,21 @@ import 'dart:js_interop'; -import 'dart:js_interop_unsafe'; import 'package:web/web.dart'; /// JavaScript async iterator result type @JS() @anonymous -extension type JSIteratorResult._(JSObject _) implements JSObject { +extension type JSIteratorResult._(JSObject _) + implements JSObject { external bool get done; - external JSAny? get value; + external T? get value; } /// JavaScript async iterator type @JS() @anonymous -extension type JSAsyncIterator._(JSObject _) implements JSObject { - external JSPromise next(); +extension type JSAsyncIterator._(JSObject _) + implements JSObject { + external JSPromise> next(); } /// Extensions for FileSystemDirectoryHandle to provide missing async iterator methods @@ -23,21 +24,21 @@ extension type JSAsyncIterator._(JSObject _) implements JSObject { extension FileSystemDirectoryHandleExtension on FileSystemDirectoryHandle { /// Returns an async iterator for the values (handles) in this directory. /// Equivalent to calling `directoryHandle.values()` in JavaScript. - external JSAsyncIterator values(); + external JSAsyncIterator values(); /// Returns an async iterator for the keys (names) in this directory. /// Equivalent to calling `directoryHandle.keys()` in JavaScript. - external JSAsyncIterator keys(); + external JSAsyncIterator keys(); /// Returns an async iterator for the entries (name-handle pairs) in this directory. /// Equivalent to calling `directoryHandle.entries()` in JavaScript. - external JSAsyncIterator entries(); + external JSAsyncIterator> entries(); } /// Helper extensions to convert JavaScript async iterators to Dart async iterables -extension JSAsyncIteratorExtension on JSAsyncIterator { +extension JSAsyncIteratorExtension on JSAsyncIterator { /// Converts a JavaScript async iterator to a Dart Stream - Stream asStream() async* { + Stream asStream() async* { while (true) { final result = await next().toDart; if (result.done) break; @@ -49,26 +50,37 @@ extension JSAsyncIteratorExtension on JSAsyncIterator { /// Extension to provide async iteration capabilities for FileSystemDirectoryHandle values extension FileSystemDirectoryHandleValuesIterable on FileSystemDirectoryHandle { /// Returns a Stream of FileSystemHandle objects for async iteration over directory contents - Stream valuesStream() { - return values().asStream().map((jsValue) => jsValue as FileSystemHandle); + Stream valuesStream() async* { + await for (final handle in values().asStream()) { + if (handle != null) { + yield handle; + } + } } /// Returns a Stream of file/directory names for async iteration over directory contents - Stream keysStream() { - return keys().asStream().map((jsValue) => (jsValue as JSString).toDart); + Stream keysStream() async* { + await for (final key in keys().asStream()) { + if (key != null) { + yield key.toDart; + } + } } /// Returns a Stream of [name, handle] pairs for async iteration over directory contents - static const int nameIndex = 0; - static const int handleIndex = 1; - Stream<(String, FileSystemHandle)> entriesStream() { - return entries().asStream().map((jsValue) { - // The entries() iterator returns [name, handle] arrays - // Use js_interop_unsafe to access array elements by numeric index - final jsObject = jsValue as JSObject; - final name = jsObject.getProperty(nameIndex.toJS).toDart; - final handle = jsObject.getProperty(handleIndex.toJS); - return (name, handle); - }); + Stream<(String, FileSystemHandle)> entriesStream() async* { + await for (final entry in entries().asStream()) { + if (entry == null || entry.length < 2) { + continue; + } + + final nameValue = entry[0]; + final handleValue = entry[1]; + if (!nameValue.isA() || !handleValue.isA()) { + continue; + } + + yield (nameValue.dartify()! as String, handleValue as FileSystemHandle); + } } } diff --git a/packages/komodo_defi_framework/app_build/build_config.json b/packages/komodo_defi_framework/app_build/build_config.json index 476793f1d..5e8016c17 100644 --- a/packages/komodo_defi_framework/app_build/build_config.json +++ b/packages/komodo_defi_framework/app_build/build_config.json @@ -68,7 +68,7 @@ "coins": { "fetch_at_build_enabled": true, "update_commit_on_build": true, - "bundled_coins_repo_commit": "c394bc48ba010966af83500eaae9da45a40d03d3", + "bundled_coins_repo_commit": "e027082339558cc79d653d0e871f0d211562fe2f", "coins_repo_api_url": "https://api.github.com/repos/GLEECBTC/coins", "coins_repo_content_url": "https://raw.githubusercontent.com/GLEECBTC/coins", "coins_repo_branch": "master", diff --git a/packages/komodo_defi_framework/lib/src/js/js_interop_utils.dart b/packages/komodo_defi_framework/lib/src/js/js_interop_utils.dart index aaf63f3e5..4a82d0cd2 100644 --- a/packages/komodo_defi_framework/lib/src/js/js_interop_utils.dart +++ b/packages/komodo_defi_framework/lib/src/js/js_interop_utils.dart @@ -8,6 +8,11 @@ import 'package:logging/logging.dart'; final Logger _jsInteropLogger = Logger('JsInteropUtils'); +@js_interop.JS('Promise.resolve') +external js_interop.JSPromise _resolveJsPromise( + js_interop.JSAny? value, +); + /// Parses a JS interop response into a JsonMap. /// /// Accepts: @@ -16,14 +21,9 @@ final Logger _jsInteropLogger = Logger('JsInteropUtils'); /// - String (JSON encoded) /// /// Throws a [FormatException] if the response cannot be parsed into a JSON map. -JsonMap parseJsInteropJson(dynamic jsResponse) { +JsonMap parseJsInteropJson(js_interop.JSAny? jsResponse) { try { - dynamic value = jsResponse; - - // If we received a JS value, convert to Dart first - if (value is js_interop.JSAny?) { - value = value?.dartify(); - } + final dynamic value = jsResponse?.dartify(); if (value is String) { final decoded = jsonDecode(value); @@ -45,7 +45,10 @@ JsonMap parseJsInteropJson(dynamic jsResponse) { } /// Generic helper that parses a JS response and maps it to a Dart model. -T parseJsInteropCall(dynamic jsResponse, T Function(JsonMap) fromJson) { +T parseJsInteropCall( + js_interop.JSAny? jsResponse, + T Function(JsonMap) fromJson, +) { final map = parseJsInteropJson(jsResponse); return fromJson(map); } @@ -76,11 +79,12 @@ List _deepConvertList(List list) { /// - If [jsValue] is not a JSPromise, it is dartified directly /// - Returns the dartified dynamic value Future resolveJsAnyMaybePromise(js_interop.JSAny? jsValue) async { - if (jsValue is js_interop.JSPromise) { - final resolved = await jsValue.toDart; - return resolved?.dartify(); + if (jsValue == null || jsValue.isUndefinedOrNull) { + return null; } - return jsValue?.dartify(); + + final resolved = await _resolveJsPromise(jsValue).toDart; + return resolved?.dartify(); } /// Generic helper to resolve a JS interop value (maybe a Promise) and map it. diff --git a/packages/komodo_defi_framework/lib/src/operations/kdf_operations_wasm.dart b/packages/komodo_defi_framework/lib/src/operations/kdf_operations_wasm.dart index 464722889..cc4075217 100644 --- a/packages/komodo_defi_framework/lib/src/operations/kdf_operations_wasm.dart +++ b/packages/komodo_defi_framework/lib/src/operations/kdf_operations_wasm.dart @@ -1,12 +1,10 @@ import 'dart:async'; import 'dart:js_interop' as js_interop; -import 'dart:js_interop_unsafe'; import 'package:flutter/services.dart'; import 'package:flutter_web_plugins/flutter_web_plugins.dart'; import 'package:http/http.dart'; import 'package:komodo_defi_framework/komodo_defi_framework.dart'; -import 'package:komodo_defi_framework/src/config/kdf_logging_config.dart'; import 'package:komodo_defi_framework/src/js/js_error_utils.dart'; import 'package:komodo_defi_framework/src/js/js_interop_utils.dart'; import 'package:komodo_defi_framework/src/js/js_result_mappers.dart' as js_maps; @@ -16,6 +14,47 @@ import 'package:mutex/mutex.dart'; const _kdfAsstsPath = 'kdf'; const _kdfJsBootstrapperPath = '$_kdfAsstsPath/res/kdflib_bootstrapper.js'; +@js_interop.JS() +extension type _KdfBootstrapperModule._(js_interop.JSObject _) + implements js_interop.JSObject { + @js_interop.JS('default') + external _KdfWasmBindings? get defaultBinding; + + external _KdfWasmBindings? get kdf; +} + +@js_interop.JS() +extension type _KdfWasmBindings._(js_interop.JSObject _) + implements js_interop.JSObject { + external js_interop.JSBoolean get isInitialized; + + @js_interop.JS('init_wasm') + external js_interop.JSPromise? initWasm(); + + @js_interop.JS('mm2_main') + external js_interop.JSAny? mm2Main( + js_interop.JSAny? config, + js_interop.JSFunction logHandler, + ); + + @js_interop.JS('mm2_main_status') + external js_interop.JSNumber? mm2MainStatus(); + + @js_interop.JS('mm2_stop') + external js_interop.JSAny? mm2Stop(); + + @js_interop.JS('mm2_rpc') + external js_interop.JSPromise? mm2Rpc( + js_interop.JSAny? request, + ); +} + +@js_interop.JS() +extension type _KdfErrorWithCode._(js_interop.JSAny _) + implements js_interop.JSAny { + external js_interop.JSAny? get code; +} + IKdfOperations createLocalKdfOperations({ required void Function(String)? logCallback, required LocalConfig config, @@ -40,7 +79,7 @@ class KdfOperationsWasm implements IKdfOperations { final LocalConfig _config; bool _libraryLoaded = false; - js_interop.JSObject? _kdfModule; + _KdfWasmBindings? _kdfModule; void Function(String)? _logger; void _log(String message) => (_logger ?? print).call(message); @@ -62,10 +101,7 @@ class KdfOperationsWasm implements IKdfOperations { } bool get _isWasmInitialized { - return _kdfModule - ?.getProperty('isInitialized'.toJS) - .toDart ?? - false; + return _kdfModule?.isInitialized.toDart ?? false; } @override @@ -80,9 +116,7 @@ class KdfOperationsWasm implements IKdfOperations { return _startupLock.protect(() async { await _ensureLoaded(); - final jsConfig = - {'conf': config, 'log_level': logLevel ?? 3}.jsify() - as js_interop.JSObject?; + final jsConfig = {'conf': config, 'log_level': logLevel ?? 3}.jsify(); try { return await _executeKdfMain(jsConfig); @@ -101,11 +135,8 @@ class KdfOperationsWasm implements IKdfOperations { }); } - Future _executeKdfMain( - js_interop.JSObject? jsConfig, - ) async { - final jsMethod = _kdfModule!.callMethod( - 'mm2_main'.toJS, + Future _executeKdfMain(js_interop.JSAny? jsConfig) async { + final jsMethod = _kdfModule!.mm2Main( jsConfig, (int level, String message) { _log('[$level] KDF: $message'); @@ -122,39 +153,6 @@ class KdfOperationsWasm implements IKdfOperations { try { _debugLog('Handling JSAny error: [${jsError.runtimeType}] $jsError'); - // Direct JSNumber error - if (isInstance(jsError, 'JSNumber')) { - final dynamic dartNumber = (jsError as js_interop.JSNumber).dartify(); - final code = extractNumericCodeFromDartError(dartNumber); - if (code != null) { - _debugLog('KdfOperationsWasm: Resolved as JSNumber code: $code'); - return KdfStartupResult.fromDefaultInt(code); - } - } - - // JSObject with useful fields - if (isInstance(jsError, 'JSObject')) { - final jsObj = jsError as js_interop.JSObject; - - // Prefer robust dartify and then inspect - final dynamic dartified = jsObj.dartify(); - final code = extractNumericCodeFromDartError(dartified); - if (code != null) return KdfStartupResult.fromDefaultInt(code); - - final msg = extractMessageFromDartError(dartified); - if (msg != null && messageIndicatesAlreadyRunning(msg)) { - return KdfStartupResult.alreadyRunning; - } - - // Fallback for 'code' property directly on JS object if not covered above - if (jsObj.hasProperty('code'.toJS).toDart) { - final jsAnyCode = jsObj.getProperty('code'.toJS); - final code2 = extractNumericCodeFromDartError(jsAnyCode?.dartify()); - if (code2 != null) return KdfStartupResult.fromDefaultInt(code2); - } - } - - // Try dartify as last resort final dynamic error = jsError.dartify(); _debugLog('Dartified error type: ${error.runtimeType}, value: $error'); @@ -166,6 +164,12 @@ class KdfOperationsWasm implements IKdfOperations { return KdfStartupResult.alreadyRunning; } + final codeValue = _KdfErrorWithCode._(jsError).code?.dartify(); + final codeFromProperty = extractNumericCodeFromDartError(codeValue); + if (codeFromProperty != null) { + return KdfStartupResult.fromDefaultInt(codeFromProperty); + } + _log('Could not extract error code from JSAny: $error'); } catch (conversionError) { _log('Error during JSAny conversion: $conversionError'); @@ -174,19 +178,10 @@ class KdfOperationsWasm implements IKdfOperations { return KdfStartupResult.unknownError; } - bool isInstance( - js_interop.JSAny? obj, [ - String? typeString, - ]) { - return obj.instanceOfString(typeString ?? T.runtimeType.toString()); - } - @override Future kdfMainStatus() async { await _ensureLoaded(); - final status = _kdfModule! - .callMethod('mm2_main_status'.toJS) - ?.toDartInt; + final status = _kdfModule!.mm2MainStatus()?.toDartInt; return MainStatus.fromDefaultInt(status!); } @@ -196,13 +191,14 @@ class KdfOperationsWasm implements IKdfOperations { try { // Call mm2_stop which may return a Promise or a direct value - final jsAny = _kdfModule!.callMethod('mm2_stop'.toJS); + final jsAny = _kdfModule!.mm2Stop(); final status = await parseJsInteropMaybePromise( jsAny, js_maps.mapJsStopResult, ); - // Ensure the node actually stops when we expect success or already stopped + // Ensure the node actually stops when we expect success or an + // already-stopped result. if (status == StopStatus.ok || status == StopStatus.stoppingAlready) { await Future.doWhile(() async { final isStopped = (await kdfMainStatus()) == MainStatus.notRunning; @@ -237,14 +233,12 @@ class KdfOperationsWasm implements IKdfOperations { } /// Makes the JavaScript RPC call and returns the raw JS response - Future _makeJsCall(JsonMap request) async { + Future _makeJsCall(JsonMap request) async { _debugLog('mm2Rpc request: ${request.censored()}'); request['userpass'] = _config.rpcPassword; - final jsRequest = request.jsify() as js_interop.JSObject?; - final jsPromise = - _kdfModule!.callMethod('mm2_rpc'.toJS, jsRequest) - as js_interop.JSPromise?; + final jsRequest = request.jsify(); + final jsPromise = _kdfModule!.mm2Rpc(jsRequest); if (jsPromise == null || jsPromise.isUndefinedOrNull) { throw Exception( @@ -281,14 +275,14 @@ class KdfOperationsWasm implements IKdfOperations { } catch (e) { _debugLog('Raw JS response: $jsResponse (stringify failed: $e)'); } - return jsResponse as js_interop.JSObject; + return jsResponse; } /// Validates the response structure void _validateResponse( JsonMap dartResponse, JsonMap request, - js_interop.JSObject jsResponse, + js_interop.JSAny? jsResponse, ) { // Legacy RPCs have no standard response format to validate if (request.valueOrNull('mmrpc') != '2.0') return; @@ -327,7 +321,7 @@ class KdfOperationsWasm implements IKdfOperations { } bool _areFunctionsLoaded() { - return _kdfModule?.hasProperty('mm2_main'.toJS).toDart ?? false; + return _kdfModule != null; } Future _ensureLoaded() async { @@ -349,8 +343,7 @@ class KdfOperationsWasm implements IKdfOperations { } Future _initWasm() async { - final initWasmPromise = - _kdfModule?.callMethod('init_wasm'.toJS) as js_interop.JSPromise?; + final initWasmPromise = _kdfModule?.initWasm(); if (initWasmPromise != null) { await initWasmPromise.toDart; } @@ -358,11 +351,14 @@ class KdfOperationsWasm implements IKdfOperations { Future _injectLibrary() async { try { - _kdfModule = - (await js_interop - .importModule('./$_kdfJsBootstrapperPath'.toJS) - .toDart) - .getProperty('kdf'.toJS); + final module = _KdfBootstrapperModule._( + await js_interop.importModule('./$_kdfJsBootstrapperPath'.toJS).toDart, + ); + _kdfModule = module.kdf ?? module.defaultBinding; + + if (_kdfModule == null) { + throw StateError('Imported KDF module did not expose a kdf binding.'); + } _log('KDF library loaded successfully'); } catch (e) { @@ -370,29 +366,6 @@ class KdfOperationsWasm implements IKdfOperations { 'Failed to load and import script $_kdfJsBootstrapperPath\n$e'; _log(message); - final debugProperties = Map.fromIterable( - [ - 'isInitialized', - 'kdf', - 'initSync', - 'initWasm', - 'init', - 'mm2_main', - 'mm2_main_status', - 'mm2_stop', - 'mm2_init', - 'init_wasm', - '__wbg_init', - ], - value: (key) { - final jsKey = (key as String).toJS; - return 'Has property: ${_kdfModule!.hasProperty(jsKey).toDart} with type: ' - '${_kdfModule!.getProperty(jsKey).runtimeType}'; - }, - ); - - _log('KDF Has properties: $debugProperties'); - throw Exception(message); } } @@ -413,13 +386,12 @@ class KdfOperationsWasm implements IKdfOperations { class KdfPluginWeb { static void registerWith(Registrar registrar) { - final channel = MethodChannel( + MethodChannel( 'komodo_defi_framework', const StandardMethodCodec(), registrar, - ); - channel.setMethodCallHandler((call) async { - // Handle method calls here if needed + ).setMethodCallHandler((call) async { + // Handle method calls here if needed. }); registrar.registerMessageHandler(); diff --git a/packages/komodo_defi_framework/lib/src/streaming/event_streaming_platform_web.dart b/packages/komodo_defi_framework/lib/src/streaming/event_streaming_platform_web.dart index 4aa78e0a8..3bef535f4 100644 --- a/packages/komodo_defi_framework/lib/src/streaming/event_streaming_platform_web.dart +++ b/packages/komodo_defi_framework/lib/src/streaming/event_streaming_platform_web.dart @@ -2,34 +2,31 @@ // and forward messages to Dart via the provided callback. import 'dart:js_interop'; -import 'dart:js_interop_unsafe'; import 'package:flutter/foundation.dart'; -import 'package:web/web.dart' as web; - import 'package:komodo_defi_framework/src/config/kdf_config.dart'; +import 'package:web/web.dart' as web; typedef EventStreamUnsubscribe = void Function(); const _eventStreamingWorkerPath = 'assets/packages/komodo_defi_framework/assets/web/event_streaming_worker.js'; -final web.EventHandler _noopHandler = ((web.Event _) {}).toJS; +final web.EventHandlerNonNull _noopHandler = ((web.Event _) {}).toJS; EventStreamUnsubscribe connectEventStream({ - IKdfHostConfig? hostConfig, required void Function(Object? data) onMessage, required void Function() onFirstByte, + IKdfHostConfig? hostConfig, }) { try { final worker = web.SharedWorker(_eventStreamingWorkerPath.toJS); - final port = worker.port; - port.start(); + final port = worker.port..start(); bool firstMessageReceived = false; - void handler(web.Event event) { - final data = event is web.MessageEvent ? event.data.dartify() : null; + void handler(web.MessageEvent event) { + final data = event.data.dartify(); // Signal first byte received on first message if (!firstMessageReceived) { @@ -47,8 +44,9 @@ EventStreamUnsubscribe connectEventStream({ return () { try { - port.onmessage = _noopHandler; - port.close(); + port + ..onmessage = _noopHandler + ..close(); } catch (_) {} }; } catch (_) { diff --git a/packages/komodo_defi_framework/web/res/kdf_wrapper.dart b/packages/komodo_defi_framework/web/res/kdf_wrapper.dart index 46dc6b661..c39c9fe47 100644 --- a/packages/komodo_defi_framework/web/res/kdf_wrapper.dart +++ b/packages/komodo_defi_framework/web/res/kdf_wrapper.dart @@ -8,12 +8,20 @@ import 'dart:async'; // This is a web-specific file, so it's safe to ignore this warning // ignore: avoid_web_libraries_in_flutter import 'dart:js_interop'; -import 'dart:js_interop_unsafe'; import 'package:flutter/services.dart'; import 'package:flutter_web_plugins/flutter_web_plugins.dart'; import 'package:web/web.dart'; +@JS('mm2_main') +external JSAny? _mm2MainJs(String conf, JSFunction logCallback); + +@JS('mm2_main_status') +external JSAny? _mm2MainStatusJs(); + +@JS('mm2_stop') +external JSAny? _mm2StopJs(); + class KdfPlugin { static void registerWith(Registrar registrar) { final plugin = KdfPlugin(); @@ -62,7 +70,7 @@ class KdfPlugin { final completer = Completer(); - final script = (document.createElement('script') as HTMLScriptElement) + final script = HTMLScriptElement() ..src = 'kdf/kdflib.js' ..onload = () { _libraryLoaded = true; @@ -82,15 +90,12 @@ class KdfPlugin { try { final jsCallback = logCallback.toJS; - final jsResponse = globalContext.callMethod( - 'mm2_main'.toJS, - [conf.toJS, jsCallback].toJS, - ); + final jsResponse = _mm2MainJs(conf, jsCallback); if (jsResponse == null) { throw Exception('mm2_main call returned null'); } - final dynamic dartResponse = (jsResponse as JSAny?).dartify(); + final dynamic dartResponse = jsResponse.dartify(); if (dartResponse == null) { throw Exception('Failed to convert mm2_main response to Dart'); } @@ -106,13 +111,13 @@ class KdfPlugin { throw StateError('KDF library not loaded. Call ensureLoaded() first.'); } - final jsResult = globalContext.callMethod('mm2_main_status'.toJS); + final jsResult = _mm2MainStatusJs(); return jsResult.dartify()! as int; } Future _mm2Stop() async { await _ensureLoaded(); - final jsResult = globalContext.callMethod('mm2_stop'.toJS); + final jsResult = _mm2StopJs(); return jsResult.dartify()! as int; } } From ab4e7f113d177b8ff1f4dafe1a5d94e9186ad724 Mon Sep 17 00:00:00 2001 From: Nitride <77973576+CharlVS@users.noreply.github.com> Date: Wed, 18 Mar 2026 14:46:03 +0100 Subject: [PATCH 05/40] chore: upgrade KDF to 3.0.0-beta preview (#317) --- .../app_build/build_config.json | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/packages/komodo_defi_framework/app_build/build_config.json b/packages/komodo_defi_framework/app_build/build_config.json index 5e8016c17..0228d580d 100644 --- a/packages/komodo_defi_framework/app_build/build_config.json +++ b/packages/komodo_defi_framework/app_build/build_config.json @@ -1,7 +1,7 @@ { "api": { - "api_commit_hash": "475cdb49bc343a8fefdc2caaa1635d5ec426990b", - "branch": "v2.6.0-beta", + "api_commit_hash": "52ba4f9ff12fd9e31768abc924a99ceadd64b2d6", + "branch": "staging", "fetch_at_build_enabled": true, "concurrent_downloads_enabled": true, "source_urls": [ @@ -13,14 +13,14 @@ "web": { "matching_pattern": "^(?:kdf_[a-f0-9]{7,40}-wasm|mm2_[a-f0-9]{7,40}-wasm|mm2-[a-f0-9]{7,40}-wasm)\\.zip$", "valid_zip_sha256_checksums": [ - "8f16eaf4cc401bad35f8a5c47c512b232d20f2501b38ec6c2b4bcd015c76c32f" + "e0b43651c773f222a4b8aa0c0a314bb3748b4f6edaefb3db70c76b34aaf4030b" ], "path": "web/kdf/bin" }, "ios": { "matching_pattern": "^(?:kdf_[a-f0-9]{7,40}-ios-aarch64|mm2_[a-f0-9]{7,40}-ios-aarch64|mm2-[a-f0-9]{7,40}-ios-aarch64-CI)\\.zip$", "valid_zip_sha256_checksums": [ - "aa791e8f269dc56c0fc80c5ca224839074ea2ae554993ee237b98599fe0e659f" + "b81d7c7a8f9a7c2c5c15d700ca2ff2683f0ad307ed7d9cd7b7541638940831d4" ], "path": "ios" }, @@ -31,35 +31,35 @@ "mac-arm64" ], "valid_zip_sha256_checksums": [ - "90e7ad6c9cc084cb7a9ae2da0b3f375081a03f3ae265b316a342b63525cb6a8d" + "6c09130fa7e4977dff617df5d5be385c1f2a57e6764a63e12e79797df52568f4" ], "path": "macos/bin" }, "windows": { "matching_pattern": "^(?:kdf_[a-f0-9]{7,40}-win-x86-64|mm2_[a-f0-9]{7,40}-win-x86-64|mm2-[a-f0-9]{7,40}-Win64)\\.zip$", "valid_zip_sha256_checksums": [ - "1b845b4b36aa27a122edfd291dfe01cc8581083da4bd99941c8a66b1de069c27" + "398e5adc95706613c71c672424aadf719899581fbc48702a5bb800c80c3e27bd" ], "path": "windows/bin" }, "android-armv7": { "matching_pattern": "^(?:kdf_[a-f0-9]{7,40}-android-armv7|mm2_[a-f0-9]{7,40}-android-armv7|mm2-[a-f0-9]{7,40}-android-armv7-CI)\\.zip$", "valid_zip_sha256_checksums": [ - "b21e17f04df13d9b600405f880663f0fdf99c85b2b1e53db4bd85bc933345831" + "eb41a69db8b002f51a4d04045492c0f6f9fcce8dd3aa655c877e41e5482d1b36" ], "path": "android/app/src/main/cpp/libs/armeabi-v7a" }, "android-aarch64": { "matching_pattern": "^(?:kdf_[a-f0-9]{7,40}-android-aarch64|mm2_[a-f0-9]{7,40}-android-aarch64|mm2-[a-f0-9]{7,40}-android-aarch64-CI)\\.zip$", "valid_zip_sha256_checksums": [ - "56b79ab33f4b86236c120ce982b04ccc23d8cdd43b34f66398ea1f8b011becd3" + "dc5bdd90196477eba9c4debe1dc93504d24980b45a669d777374bfe09dda73aa" ], "path": "android/app/src/main/cpp/libs/arm64-v8a" }, "linux": { "matching_pattern": "^(?:kdf_[a-f0-9]{7,40}-linux-x86-64|mm2_[a-f0-9]{7,40}-linux-x86-64|mm2-[a-f0-9]{7,40}-Linux-Release)\\.zip$", "valid_zip_sha256_checksums": [ - "4bfabfcfe44b33b32799a90dcd8d373ba5847d8b56e820a1ce5d702dfb76e2cc" + "f282176b99d080315df9f47c11713084cc37de0056c144950aa9f033a8cc6302" ], "path": "linux/bin" } From cd5b45d46ada1948195bb0cb37d8a0737f7d0c66 Mon Sep 17 00:00:00 2001 From: Nitride <77973576+CharlVS@users.noreply.github.com> Date: Wed, 18 Mar 2026 16:02:55 +0100 Subject: [PATCH 06/40] feat(coins): Add TRON and TRC20 support (#316) * feat(sdk): add TRON and TRC20 support * chore(sdk): remove generated whitespace * fix(sdk): align config parsing and KDF compatibility --- .../coins_config/custom_token_storage.dart | 26 +- .../activation_params_index.dart | 2 + .../erc20_activation_params.dart | 15 ++ .../trc20_activation_params.dart | 30 +++ .../trx_activation_params.dart | 72 ++++++ .../common_structures/common_structures.dart | 2 + .../general/address_format.dart | 6 + .../rpc_methods/eth/enable_custom_erc20.dart | 6 +- .../lib/src/rpc_methods/eth/enable_erc20.dart | 2 +- .../eth/enable_eth_with_tokens.dart | 156 ++++++++++-- .../rpc_methods/eth/eth_rpc_extensions.dart | 10 +- .../rpc_methods/eth/task_enable_eth_init.dart | 2 +- .../withdrawal/withdraw_request.dart | 29 +-- ..._eth_with_tokens_legacy_response_test.dart | 99 ++++++++ .../test/src/tron_rpc_methods_test.dart | 161 ++++++++++++ .../test/src/withdraw_request_test.dart | 36 +++ .../activation_strategy_base.dart | 6 +- .../custom_erc20_activation_strategy.dart | 23 +- .../erc20_activation_strategy.dart | 18 +- .../eth_task_activation_strategy.dart | 36 ++- .../eth_with_tokens_activation_strategy.dart | 32 ++- .../src/withdrawals/withdrawal_manager.dart | 1 + .../tron_activation_strategy_test.dart | 93 +++++++ .../lib/src/assets/asset_id.dart | 3 +- .../lib/src/coin_classes/coin_subclasses.dart | 24 ++ .../lib/src/coin_classes/protocol_class.dart | 52 ++-- .../src/protocols/erc20/erc20_protocol.dart | 14 +- .../lib/src/protocols/qtum/qtum_protocol.dart | 15 +- .../lib/src/protocols/sia/sia_protocol.dart | 8 +- .../lib/src/protocols/slp/slp_protocol.dart | 9 +- .../tendermint/tendermint_protocol.dart | 9 +- .../src/protocols/trc20/trc20_protocol.dart | 112 +++++++++ .../lib/src/protocols/trx/trx_protocol.dart | 87 +++++++ .../lib/src/protocols/utxo/utxo_protocol.dart | 14 +- .../src/protocols/zhtlc/zhtlc_protocol.dart | 8 +- .../lib/src/transactions/fee_info.dart | 236 +++++++++++------- .../src/transactions/fee_info.freezed.dart | 106 +++++++- packages/komodo_defi_types/lib/src/types.dart | 2 + .../lib/src/utils/protocol_type_utils.dart | 9 + .../lib/src/withdrawal/withdrawal_types.dart | 4 + .../komodo_defi_types/test/fee_info_test.dart | 110 +++++++- .../test/tron_protocol_test.dart | 153 ++++++++++++ .../src/core/displays/fee_info_display.dart | 42 +++- 43 files changed, 1617 insertions(+), 263 deletions(-) create mode 100644 packages/komodo_defi_rpc_methods/lib/src/common_structures/activation/activation_params/trc20_activation_params.dart create mode 100644 packages/komodo_defi_rpc_methods/lib/src/common_structures/activation/activation_params/trx_activation_params.dart create mode 100644 packages/komodo_defi_rpc_methods/test/src/enable_eth_with_tokens_legacy_response_test.dart create mode 100644 packages/komodo_defi_rpc_methods/test/src/tron_rpc_methods_test.dart create mode 100644 packages/komodo_defi_rpc_methods/test/src/withdraw_request_test.dart create mode 100644 packages/komodo_defi_sdk/test/activation/tron_activation_strategy_test.dart create mode 100644 packages/komodo_defi_types/lib/src/protocols/trc20/trc20_protocol.dart create mode 100644 packages/komodo_defi_types/lib/src/protocols/trx/trx_protocol.dart create mode 100644 packages/komodo_defi_types/lib/src/utils/protocol_type_utils.dart create mode 100644 packages/komodo_defi_types/test/tron_protocol_test.dart diff --git a/packages/komodo_coin_updates/lib/src/coins_config/custom_token_storage.dart b/packages/komodo_coin_updates/lib/src/coins_config/custom_token_storage.dart index ae5d4b87f..3a85f8aa3 100644 --- a/packages/komodo_coin_updates/lib/src/coins_config/custom_token_storage.dart +++ b/packages/komodo_coin_updates/lib/src/coins_config/custom_token_storage.dart @@ -63,14 +63,7 @@ class CustomTokenStorage implements CustomTokenStore { logContext: 'for custom tokens', ) .map( - (asset) => asset.copyWith( - // IMPORTANT: This cast to Erc20Protocol is by design for now, - // as custom tokens are currently only supported for ERC20. - // This may change in future versions to support other protocols. - protocol: (asset.protocol as Erc20Protocol).copyWith( - isCustomToken: true, - ), - ), + (asset) => asset.copyWith(protocol: _markCustomToken(asset.protocol)), ) .toList(); } @@ -80,12 +73,7 @@ class CustomTokenStorage implements CustomTokenStore { _log.fine('Retrieving custom token ${assetId.id}'); final box = await _openCustomTokensBox(); final asset = await box.get(assetId.id); - return asset?.copyWith( - // IMPORTANT: This cast to Erc20Protocol is by design for now, - // as custom tokens are currently only supported for ERC20. - // This may change in future versions to support other protocols. - protocol: (asset.protocol as Erc20Protocol).copyWith(isCustomToken: true), - ); + return asset?.copyWith(protocol: _markCustomToken(asset.protocol)); } @override @@ -196,4 +184,14 @@ class CustomTokenStorage implements CustomTokenStore { return _customTokensBox!; } + + ProtocolClass _markCustomToken(ProtocolClass protocol) { + return switch (protocol) { + final Erc20Protocol p => p.copyWith(isCustomToken: true), + final Trc20Protocol p => p.copyWith(isCustomToken: true), + _ => throw UnsupportedError( + 'Unsupported custom token protocol: ${protocol.runtimeType}', + ), + }; + } } diff --git a/packages/komodo_defi_rpc_methods/lib/src/common_structures/activation/activation_params/activation_params_index.dart b/packages/komodo_defi_rpc_methods/lib/src/common_structures/activation/activation_params/activation_params_index.dart index 37411d396..afe811164 100644 --- a/packages/komodo_defi_rpc_methods/lib/src/common_structures/activation/activation_params/activation_params_index.dart +++ b/packages/komodo_defi_rpc_methods/lib/src/common_structures/activation/activation_params/activation_params_index.dart @@ -14,5 +14,7 @@ export 'qtum_activation_params.dart'; export 'sia_activation_params.dart'; export 'slp_activation_params.dart'; export 'tendermint_activation_params.dart'; +export 'trc20_activation_params.dart'; +export 'trx_activation_params.dart'; export 'utxo_activation_params.dart'; export 'zhtlc_activation_params.dart'; diff --git a/packages/komodo_defi_rpc_methods/lib/src/common_structures/activation/activation_params/erc20_activation_params.dart b/packages/komodo_defi_rpc_methods/lib/src/common_structures/activation/activation_params/erc20_activation_params.dart index 27ae65ecf..8936fa100 100644 --- a/packages/komodo_defi_rpc_methods/lib/src/common_structures/activation/activation_params/erc20_activation_params.dart +++ b/packages/komodo_defi_rpc_methods/lib/src/common_structures/activation/activation_params/erc20_activation_params.dart @@ -6,6 +6,7 @@ class Erc20ActivationParams extends ActivationParams { required this.nodes, required this.swapContractAddress, required this.fallbackSwapContract, + super.privKeyPolicy, }); factory Erc20ActivationParams.fromJsonConfig(JsonMap json) { @@ -19,6 +20,20 @@ class Erc20ActivationParams extends ActivationParams { final String swapContractAddress; final String fallbackSwapContract; + Erc20ActivationParams copyWith({ + List? nodes, + String? swapContractAddress, + String? fallbackSwapContract, + PrivateKeyPolicy? privKeyPolicy, + }) { + return Erc20ActivationParams( + nodes: nodes ?? this.nodes, + swapContractAddress: swapContractAddress ?? this.swapContractAddress, + fallbackSwapContract: fallbackSwapContract ?? this.fallbackSwapContract, + privKeyPolicy: privKeyPolicy ?? this.privKeyPolicy, + ); + } + @override JsonMap toRpcParams() => super.toRpcParams().deepMerge({ // Align with KDF API which expects node objects (url/gui_auth), not plain strings diff --git a/packages/komodo_defi_rpc_methods/lib/src/common_structures/activation/activation_params/trc20_activation_params.dart b/packages/komodo_defi_rpc_methods/lib/src/common_structures/activation/activation_params/trc20_activation_params.dart new file mode 100644 index 000000000..306789cf5 --- /dev/null +++ b/packages/komodo_defi_rpc_methods/lib/src/common_structures/activation/activation_params/trc20_activation_params.dart @@ -0,0 +1,30 @@ +import 'package:komodo_defi_rpc_methods/src/internal_exports.dart'; +import 'package:komodo_defi_types/komodo_defi_type_utils.dart'; + +class Trc20ActivationParams extends ActivationParams { + Trc20ActivationParams({required this.nodes, super.privKeyPolicy}); + + factory Trc20ActivationParams.fromJsonConfig(JsonMap json) { + return Trc20ActivationParams( + nodes: json.value('nodes').map(EvmNode.fromJson).toList(), + ); + } + + final List nodes; + + Trc20ActivationParams copyWith({ + List? nodes, + PrivateKeyPolicy? privKeyPolicy, + }) { + return Trc20ActivationParams( + nodes: nodes ?? this.nodes, + privKeyPolicy: privKeyPolicy ?? this.privKeyPolicy, + ); + } + + @override + JsonMap toRpcParams() => super.toRpcParams().deepMerge({ + 'nodes': nodes.map((e) => e.toJson()).toList(), + 'priv_key_policy': privKeyPolicy?.toJson(), + }); +} diff --git a/packages/komodo_defi_rpc_methods/lib/src/common_structures/activation/activation_params/trx_activation_params.dart b/packages/komodo_defi_rpc_methods/lib/src/common_structures/activation/activation_params/trx_activation_params.dart new file mode 100644 index 000000000..432918493 --- /dev/null +++ b/packages/komodo_defi_rpc_methods/lib/src/common_structures/activation/activation_params/trx_activation_params.dart @@ -0,0 +1,72 @@ +import 'package:komodo_defi_rpc_methods/komodo_defi_rpc_methods.dart'; +import 'package:komodo_defi_types/komodo_defi_type_utils.dart'; + +class TrxWithTokensActivationParams extends ActivationParams { + TrxWithTokensActivationParams({ + required this.nodes, + required this.tokenRequests, + required this.txHistory, + this.mm2, + required super.privKeyPolicy, + super.requiredConfirmations, + super.requiresNotarization = false, + }); + + factory TrxWithTokensActivationParams.fromJson(JsonMap json) { + final base = ActivationParams.fromConfigJson(json); + + return TrxWithTokensActivationParams( + nodes: json.value>('nodes').map(EvmNode.fromJson).toList(), + tokenRequests: + json + .valueOrNull>('erc20_tokens_requests') + ?.map(TokensRequest.fromJson) + .toList() ?? + [], + requiredConfirmations: base.requiredConfirmations, + requiresNotarization: base.requiresNotarization, + privKeyPolicy: base.privKeyPolicy, + txHistory: json.valueOrNull('tx_history'), + mm2: json.valueOrNull('mm2'), + ); + } + + final List nodes; + final List tokenRequests; + final bool? txHistory; + final int? mm2; + + TrxWithTokensActivationParams copyWith({ + List? nodes, + List? tokenRequests, + int? requiredConfirmations, + bool? requiresNotarization, + PrivateKeyPolicy? privKeyPolicy, + bool? txHistory, + int? mm2, + }) { + return TrxWithTokensActivationParams( + nodes: nodes ?? this.nodes, + tokenRequests: tokenRequests ?? this.tokenRequests, + requiredConfirmations: + requiredConfirmations ?? this.requiredConfirmations, + requiresNotarization: requiresNotarization ?? this.requiresNotarization, + privKeyPolicy: privKeyPolicy ?? this.privKeyPolicy, + txHistory: txHistory ?? this.txHistory, + mm2: mm2 ?? this.mm2, + ); + } + + @override + JsonMap toRpcParams() { + return { + ...super.toRpcParams(), + 'nodes': nodes.map((e) => e.toJson()).toList(), + 'erc20_tokens_requests': tokenRequests.map((e) => e.toJson()).toList(), + if (txHistory != null) 'tx_history': txHistory, + if (mm2 != null) 'mm2': mm2, + 'priv_key_policy': + (privKeyPolicy ?? const PrivateKeyPolicy.contextPrivKey()).toJson(), + }; + } +} diff --git a/packages/komodo_defi_rpc_methods/lib/src/common_structures/common_structures.dart b/packages/komodo_defi_rpc_methods/lib/src/common_structures/common_structures.dart index db5bc1c83..36ba4b164 100644 --- a/packages/komodo_defi_rpc_methods/lib/src/common_structures/common_structures.dart +++ b/packages/komodo_defi_rpc_methods/lib/src/common_structures/common_structures.dart @@ -15,6 +15,8 @@ export 'activation/activation_params/qtum_activation_params.dart'; export 'activation/activation_params/sia_activation_params.dart'; export 'activation/activation_params/slp_activation_params.dart'; export 'activation/activation_params/tendermint_activation_params.dart'; +export 'activation/activation_params/trc20_activation_params.dart'; +export 'activation/activation_params/trx_activation_params.dart'; export 'activation/activation_params/utxo_activation_params.dart'; export 'activation/activation_params/zhtlc_activation_params.dart'; export 'activation/coin_protocol.dart'; diff --git a/packages/komodo_defi_rpc_methods/lib/src/common_structures/general/address_format.dart b/packages/komodo_defi_rpc_methods/lib/src/common_structures/general/address_format.dart index 80c815b0a..cca0dc42a 100644 --- a/packages/komodo_defi_rpc_methods/lib/src/common_structures/general/address_format.dart +++ b/packages/komodo_defi_rpc_methods/lib/src/common_structures/general/address_format.dart @@ -8,6 +8,12 @@ class AddressFormat { bool isBchNetwork = false, }) { switch (subClass) { + case CoinSubClass.trx: + case CoinSubClass.trc20: + return AddressFormat( + format: AddressFormatType.wallet.toString(), + network: '', + ); case CoinSubClass.erc20: case CoinSubClass.grc20: case CoinSubClass.ethereumClassic: diff --git a/packages/komodo_defi_rpc_methods/lib/src/rpc_methods/eth/enable_custom_erc20.dart b/packages/komodo_defi_rpc_methods/lib/src/rpc_methods/eth/enable_custom_erc20.dart index 283d1bd7a..e48fb05bc 100644 --- a/packages/komodo_defi_rpc_methods/lib/src/rpc_methods/eth/enable_custom_erc20.dart +++ b/packages/komodo_defi_rpc_methods/lib/src/rpc_methods/eth/enable_custom_erc20.dart @@ -9,12 +9,14 @@ class EnableCustomErc20TokenRequest required this.activationParams, required this.platform, required this.contractAddress, + this.protocolType = 'ERC20', }) : super(method: 'enable_erc20', rpcPass: rpcPass, mmrpc: RpcVersion.v2_0); final String ticker; - final Erc20ActivationParams activationParams; + final ActivationParams activationParams; final String platform; final String contractAddress; + final String protocolType; @override Map toJson() { @@ -32,7 +34,7 @@ class EnableCustomErc20TokenRequest 'ticker': ticker, 'activation_params': activationParams.toRpcParams(), 'protocol': { - 'type': 'ERC20', + 'type': protocolType, 'protocol_data': { 'platform': platform, 'contract_address': contractAddress, diff --git a/packages/komodo_defi_rpc_methods/lib/src/rpc_methods/eth/enable_erc20.dart b/packages/komodo_defi_rpc_methods/lib/src/rpc_methods/eth/enable_erc20.dart index 0ed648a29..c02b46955 100644 --- a/packages/komodo_defi_rpc_methods/lib/src/rpc_methods/eth/enable_erc20.dart +++ b/packages/komodo_defi_rpc_methods/lib/src/rpc_methods/eth/enable_erc20.dart @@ -10,7 +10,7 @@ class EnableErc20Request }) : super(method: 'enable_erc20', rpcPass: rpcPass, mmrpc: RpcVersion.v2_0); final String ticker; - final Erc20ActivationParams activationParams; + final ActivationParams activationParams; @override Map toJson() { diff --git a/packages/komodo_defi_rpc_methods/lib/src/rpc_methods/eth/enable_eth_with_tokens.dart b/packages/komodo_defi_rpc_methods/lib/src/rpc_methods/eth/enable_eth_with_tokens.dart index c7f730d7c..a3b2b154a 100644 --- a/packages/komodo_defi_rpc_methods/lib/src/rpc_methods/eth/enable_eth_with_tokens.dart +++ b/packages/komodo_defi_rpc_methods/lib/src/rpc_methods/eth/enable_eth_with_tokens.dart @@ -18,7 +18,7 @@ class EnableEthWithTokensRequest ); final String ticker; - final EthWithTokensActivationParams activationParams; + final ActivationParams activationParams; final bool getBalances; @override @@ -34,7 +34,7 @@ class EnableEthWithTokensRequest @override EnableEthWithTokensResponse parse(Map json) => - EnableEthWithTokensResponse.parse(json); + EnableEthWithTokensResponse.parse(json, platformTicker: ticker); } /// Response from enabling ETH with tokens request @@ -46,16 +46,23 @@ class EnableEthWithTokensResponse extends BaseResponse { required this.nftsInfos, }); - factory EnableEthWithTokensResponse.parse(JsonMap json) { + factory EnableEthWithTokensResponse.parse( + JsonMap json, { + String? platformTicker, + }) { final result = json.value('result'); + final walletBalanceJson = result.valueOrNull('wallet_balance'); return EnableEthWithTokensResponse( mmrpc: json.value('mmrpc'), currentBlock: result.value('current_block'), - walletBalance: WalletBalance.fromJson( - result.value('wallet_balance'), - ), - nftsInfos: result.value('nfts_infos'), + walletBalance: walletBalanceJson != null + ? WalletBalance.fromJson(walletBalanceJson) + : WalletBalance.fromLegacyAddressInfos( + result, + platformTicker: platformTicker, + ), + nftsInfos: result.valueOrNull('nfts_infos') ?? const {}, ); } @@ -80,11 +87,55 @@ class WalletBalance { factory WalletBalance.fromJson(JsonMap json) { return WalletBalance( walletType: json.value('wallet_type'), - accounts: - json - .value>('accounts') - .map((e) => WalletAccount.fromJson(e as JsonMap)) - .toList(), + accounts: json + .value>('accounts') + .map((e) => WalletAccount.fromJson(e as JsonMap)) + .toList(), + ); + } + + factory WalletBalance.fromLegacyAddressInfos( + JsonMap json, { + String? platformTicker, + }) { + final platformAddresses = + json.valueOrNull('eth_addresses_infos') ?? const {}; + final tokenAddresses = + json.valueOrNull('erc20_addresses_infos') ?? const {}; + final addressesByValue = {}; + void addLegacyAddress(String address, JsonMap json) { + final next = WalletAddress.fromLegacyJson( + address: address, + json: json, + platformTicker: platformTicker, + ); + final previous = addressesByValue[address]; + addressesByValue[address] = previous == null + ? next + : previous.merge(next); + } + + for (final entry in platformAddresses.entries) { + addLegacyAddress(entry.key, entry.value as JsonMap); + } + for (final entry in tokenAddresses.entries) { + addLegacyAddress(entry.key, entry.value as JsonMap); + } + final addresses = addressesByValue.values.toList(); + final totalBalance = _aggregateTokenBalances( + addresses.map((address) => address.balance), + ); + + return WalletBalance( + walletType: 'iguana', + accounts: [ + WalletAccount( + accountIndex: 0, + derivationPath: '', + totalBalance: totalBalance, + addresses: addresses, + ), + ], ); } @@ -112,11 +163,10 @@ class WalletAccount { totalBalance: TokenBalanceMap.fromJson( json.value('total_balance'), ), - addresses: - json - .value>('addresses') - .map((e) => WalletAddress.fromJson(e as JsonMap)) - .toList(), + addresses: json + .value>('addresses') + .map((e) => WalletAddress.fromJson(e as JsonMap)) + .toList(), ); } @@ -150,6 +200,27 @@ class WalletAddress { ); } + factory WalletAddress.fromLegacyJson({ + required String address, + required JsonMap json, + String? platformTicker, + }) { + final balancesJson = json.valueOrNull('balances'); + final tickers = + json.valueOrNull>('tickers')?.whereType() ?? + const []; + return WalletAddress( + address: address, + derivationPath: json.valueOrNull('derivation_path') ?? '', + chain: json.valueOrNull('chain') ?? 'external', + balance: _legacyBalancesToTokenBalanceMap( + balancesJson, + platformTicker: platformTicker, + tickers: tickers, + ), + ); + } + final String address; final String derivationPath; final String chain; @@ -161,4 +232,55 @@ class WalletAddress { 'chain': chain, 'balance': balance.toJson(), }; + + WalletAddress merge(WalletAddress other) { + return WalletAddress( + address: address, + derivationPath: derivationPath.isNotEmpty + ? derivationPath + : other.derivationPath, + chain: chain.isNotEmpty ? chain : other.chain, + balance: _aggregateTokenBalances([balance, other.balance]), + ); + } +} + +TokenBalanceMap _legacyBalancesToTokenBalanceMap( + JsonMap? balancesJson, { + String? platformTicker, + Iterable tickers = const [], +}) { + final zeroBalancesJson = _zeroBalancesJsonFromTickers(tickers); + if (balancesJson == null) { + return TokenBalanceMap.fromJson(zeroBalancesJson); + } + if (balancesJson.values.every((value) => value is JsonMap)) { + return TokenBalanceMap.fromJson({...zeroBalancesJson, ...balancesJson}); + } + final ticker = platformTicker ?? 'ETH'; + return TokenBalanceMap.fromJson({...zeroBalancesJson, ticker: balancesJson}); +} + +JsonMap _zeroBalancesJsonFromTickers(Iterable tickers) => + Map.fromEntries( + tickers.toSet().map( + (ticker) => + MapEntry(ticker, BalanceInfo.zero().toJson()), + ), + ); + +TokenBalanceMap _aggregateTokenBalances(Iterable balances) { + final aggregated = {}; + for (final tokenBalanceMap in balances) { + final json = tokenBalanceMap.toJson(); + for (final entry in json.entries) { + final balance = BalanceInfo.fromJson(entry.value as JsonMap); + aggregated.update( + entry.key, + (current) => current + balance, + ifAbsent: () => balance, + ); + } + } + return TokenBalanceMap(balances: aggregated); } diff --git a/packages/komodo_defi_rpc_methods/lib/src/rpc_methods/eth/eth_rpc_extensions.dart b/packages/komodo_defi_rpc_methods/lib/src/rpc_methods/eth/eth_rpc_extensions.dart index 689a77302..994f2619c 100644 --- a/packages/komodo_defi_rpc_methods/lib/src/rpc_methods/eth/eth_rpc_extensions.dart +++ b/packages/komodo_defi_rpc_methods/lib/src/rpc_methods/eth/eth_rpc_extensions.dart @@ -7,7 +7,7 @@ class Erc20MethodsNamespace extends BaseRpcMethodNamespace { Future enableEthWithTokens({ required String ticker, - required EthWithTokensActivationParams params, + required ActivationParams params, bool getBalances = true, }) { return execute( @@ -22,7 +22,7 @@ class Erc20MethodsNamespace extends BaseRpcMethodNamespace { Future enableErc20({ required String ticker, - required Erc20ActivationParams activationParams, + required ActivationParams activationParams, }) { return execute( EnableErc20Request( @@ -35,9 +35,10 @@ class Erc20MethodsNamespace extends BaseRpcMethodNamespace { Future enableCustomErc20Token({ required String ticker, - required Erc20ActivationParams activationParams, + required ActivationParams activationParams, required String platform, required String contractAddress, + String protocolType = 'ERC20', }) { return execute( EnableCustomErc20TokenRequest( @@ -46,6 +47,7 @@ class Erc20MethodsNamespace extends BaseRpcMethodNamespace { activationParams: activationParams, platform: platform, contractAddress: contractAddress, + protocolType: protocolType, ), ); } @@ -53,7 +55,7 @@ class Erc20MethodsNamespace extends BaseRpcMethodNamespace { // ETH Task Methods Future enableEthInit({ required String ticker, - required EthWithTokensActivationParams params, + required ActivationParams params, }) { return execute( TaskEnableEthInit(rpcPass: rpcPass ?? '', ticker: ticker, params: params), diff --git a/packages/komodo_defi_rpc_methods/lib/src/rpc_methods/eth/task_enable_eth_init.dart b/packages/komodo_defi_rpc_methods/lib/src/rpc_methods/eth/task_enable_eth_init.dart index b0abc57f3..e08d4c24c 100644 --- a/packages/komodo_defi_rpc_methods/lib/src/rpc_methods/eth/task_enable_eth_init.dart +++ b/packages/komodo_defi_rpc_methods/lib/src/rpc_methods/eth/task_enable_eth_init.dart @@ -9,7 +9,7 @@ class TaskEnableEthInit @override // ignore: overridden_fields - final EthWithTokensActivationParams params; + final ActivationParams params; @override Map toJson() => { diff --git a/packages/komodo_defi_rpc_methods/lib/src/rpc_methods/withdrawal/withdraw_request.dart b/packages/komodo_defi_rpc_methods/lib/src/rpc_methods/withdrawal/withdraw_request.dart index ef3a00703..a4a3976c8 100644 --- a/packages/komodo_defi_rpc_methods/lib/src/rpc_methods/withdrawal/withdraw_request.dart +++ b/packages/komodo_defi_rpc_methods/lib/src/rpc_methods/withdrawal/withdraw_request.dart @@ -7,14 +7,11 @@ import 'package:komodo_defi_types/komodo_defi_types.dart'; const String _kDefaultKmdRewardsAmount = '0'; /// Returns KMD-specific parameters for withdrawal requests -/// +/// /// KDF requires kmd_rewards object with claimed_by_me flag for KMD withdrawals Map _kmdRewardsParams() => { - 'kmd_rewards': { - 'amount': _kDefaultKmdRewardsAmount, - 'claimed_by_me': true, - }, - }; + 'kmd_rewards': {'amount': _kDefaultKmdRewardsAmount, 'claimed_by_me': true}, +}; /// Request for standard withdrawal (non-task API) /// @@ -34,6 +31,7 @@ class WithdrawRequest this.memo, this.max = false, this.ibcSourceChannel, + this.expirationSeconds, }) : assert( amount != null || max, 'Amount cannot be specified if sending the maximum amount', @@ -52,6 +50,7 @@ class WithdrawRequest final String? memo; final bool max; final int? ibcSourceChannel; + final int? expirationSeconds; @override Map toJson() => { @@ -66,6 +65,7 @@ class WithdrawRequest if (memo != null) 'memo': memo, if (coin.toUpperCase() == 'KMD') ..._kmdRewardsParams(), if (ibcSourceChannel != null) 'ibc_source_channel': ibcSourceChannel, + if (expirationSeconds != null) 'expiration_seconds': expirationSeconds, }, }; @@ -100,6 +100,7 @@ class WithdrawInitRequest from = params.from, memo = params.memo, max = params.isMax ?? false, + expirationSeconds = params.expirationSeconds, assert( params.amount != null || (params.isMax ?? false), 'Amount must be non-null if isMax is false and ' @@ -114,6 +115,7 @@ class WithdrawInitRequest final WithdrawalSource? from; final String? memo; final bool max; + final int? expirationSeconds; @override Map toJson() => { @@ -126,6 +128,7 @@ class WithdrawInitRequest if (from != null) 'from': from!.toRpcParams(), if (memo != null) 'memo': memo, if (max) 'max': max, + if (expirationSeconds != null) 'expiration_seconds': expirationSeconds, if (coin.toUpperCase() == 'KMD') ..._kmdRewardsParams(), }, }; @@ -174,10 +177,9 @@ class WithdrawStatusResponse extends BaseResponse { return WithdrawStatusResponse( mmrpc: json.value('mmrpc'), status: status, - details: - status == 'Ok' - ? WithdrawResult.fromJson(result.value('details')) - : result.value('details'), + details: status == 'Ok' + ? WithdrawResult.fromJson(result.value('details')) + : result.value('details'), ); } @@ -192,10 +194,9 @@ class WithdrawStatusResponse extends BaseResponse { 'mmrpc': mmrpc, 'result': { 'status': status, - 'details': - (details is WithdrawResult) - ? (details as WithdrawResult).toJson() - : details, + 'details': (details is WithdrawResult) + ? (details as WithdrawResult).toJson() + : details, }, }; diff --git a/packages/komodo_defi_rpc_methods/test/src/enable_eth_with_tokens_legacy_response_test.dart b/packages/komodo_defi_rpc_methods/test/src/enable_eth_with_tokens_legacy_response_test.dart new file mode 100644 index 000000000..8fbc060db --- /dev/null +++ b/packages/komodo_defi_rpc_methods/test/src/enable_eth_with_tokens_legacy_response_test.dart @@ -0,0 +1,99 @@ +import 'package:decimal/decimal.dart'; +import 'package:komodo_defi_rpc_methods/komodo_defi_rpc_methods.dart'; +import 'package:test/test.dart'; + +void main() { + group('enable_eth_with_tokens legacy response parsing', () { + test('preserves zero-balance tickers when get_balances is false', () { + final response = EnableEthWithTokensResponse.parse({ + 'mmrpc': '2.0', + 'result': { + 'current_block': 64265247, + 'eth_addresses_infos': { + '0x083C32B38e8050473f6999e22f670d1404235592': { + 'derivation_method': {'type': 'Iguana'}, + 'pubkey': '04abc', + }, + }, + 'erc20_addresses_infos': { + '0x083C32B38e8050473f6999e22f670d1404235592': { + 'derivation_method': {'type': 'Iguana'}, + 'pubkey': '04abc', + 'tickers': ['PGX-PLG20', 'AAVE-PLG20'], + }, + }, + 'nfts_infos': {}, + }, + }, platformTicker: 'MATIC'); + + final addressBalance = response + .walletBalance + .accounts + .first + .addresses + .first + .balance + .toJson(); + final totalBalance = response.walletBalance.accounts.first.totalBalance + .toJson(); + + expect(addressBalance.keys, containsAll(['PGX-PLG20', 'AAVE-PLG20'])); + expect(totalBalance.keys, containsAll(['PGX-PLG20', 'AAVE-PLG20'])); + expect(addressBalance.keys, isNot(contains('MATIC'))); + expect( + response.walletBalance.accounts.first.addresses.first.balance + .balanceOf('PGX-PLG20') + .spendable, + Decimal.zero, + ); + expect( + response.walletBalance.accounts.first.addresses.first.balance + .balanceOf('AAVE-PLG20') + .unspendable, + Decimal.zero, + ); + }); + + test('merges tickers with real balances without overwriting them', () { + final response = EnableEthWithTokensResponse.parse({ + 'mmrpc': '2.0', + 'result': { + 'current_block': 64265343, + 'eth_addresses_infos': { + '0x083C32B38e8050473f6999e22f670d1404235592': { + 'derivation_method': {'type': 'Iguana'}, + 'pubkey': '04abc', + }, + }, + 'erc20_addresses_infos': { + '0x083C32B38e8050473f6999e22f670d1404235592': { + 'derivation_method': {'type': 'Iguana'}, + 'pubkey': '04abc', + 'tickers': ['PGX-PLG20', 'AAVE-PLG20'], + 'balances': { + 'PGX-PLG20': { + 'spendable': '237.729414631067', + 'unspendable': '0', + }, + }, + }, + }, + 'nfts_infos': {}, + }, + }, platformTicker: 'MATIC'); + + final addressBalance = + response.walletBalance.accounts.first.addresses.first.balance; + + expect( + addressBalance.toJson().keys, + containsAll(['PGX-PLG20', 'AAVE-PLG20']), + ); + expect( + addressBalance.balanceOf('PGX-PLG20').spendable, + Decimal.parse('237.729414631067'), + ); + expect(addressBalance.balanceOf('AAVE-PLG20').spendable, Decimal.zero); + }); + }); +} diff --git a/packages/komodo_defi_rpc_methods/test/src/tron_rpc_methods_test.dart b/packages/komodo_defi_rpc_methods/test/src/tron_rpc_methods_test.dart new file mode 100644 index 000000000..688b4419f --- /dev/null +++ b/packages/komodo_defi_rpc_methods/test/src/tron_rpc_methods_test.dart @@ -0,0 +1,161 @@ +import 'package:decimal/decimal.dart'; +import 'package:komodo_defi_rpc_methods/komodo_defi_rpc_methods.dart'; +import 'package:komodo_defi_types/komodo_defi_types.dart'; +import 'package:test/test.dart'; + +void main() { + group('TRON RPC request serialization', () { + test('enable_eth_with_tokens serializes TRX without swap contracts', () { + final request = EnableEthWithTokensRequest( + rpcPass: 'rpc-pass', + ticker: 'TRX', + activationParams: TrxWithTokensActivationParams( + nodes: [EvmNode(url: 'https://api.trongrid.io')], + tokenRequests: [TokensRequest(ticker: 'USDT-TRC20')], + txHistory: true, + mm2: 1, + privKeyPolicy: const PrivateKeyPolicy.contextPrivKey(), + ), + ); + + final json = request.toJson(); + final params = json['params'] as Map; + + expect(params['ticker'], 'TRX'); + expect(params['mm2'], 1); + expect(params['nodes'], [ + {'url': 'https://api.trongrid.io', 'gui_auth': false}, + ]); + expect(params['erc20_tokens_requests'], [ + {'ticker': 'USDT-TRC20', 'required_confirmations': 3}, + ]); + expect(params['tx_history'], isTrue); + expect(params.containsKey('swap_contract_address'), isFalse); + expect(params.containsKey('fallback_swap_contract'), isFalse); + }); + + test('task::enable_eth::init serializes TRX params', () { + final request = TaskEnableEthInit( + rpcPass: 'rpc-pass', + ticker: 'TRX', + params: TrxWithTokensActivationParams( + nodes: [EvmNode(url: 'https://api.trongrid.io')], + tokenRequests: const [], + txHistory: false, + privKeyPolicy: const PrivateKeyPolicy.trezor(), + ), + ); + + final json = request.toJson(); + final params = json['params'] as Map; + + expect(json['method'], 'task::enable_eth::init'); + expect(params['ticker'], 'TRX'); + expect(params['nodes'], [ + {'url': 'https://api.trongrid.io', 'gui_auth': false}, + ]); + expect(params.containsKey('swap_contract_address'), isFalse); + expect( + params['priv_key_policy'], + equals(const PrivateKeyPolicy.trezor().toJson()), + ); + }); + + test('enable_erc20 custom token request supports TRC20 protocol types', () { + final request = EnableCustomErc20TokenRequest( + rpcPass: 'rpc-pass', + ticker: 'USDT-TRC20', + activationParams: Trc20ActivationParams( + nodes: [EvmNode(url: 'https://api.trongrid.io')], + privKeyPolicy: const PrivateKeyPolicy.contextPrivKey(), + ), + platform: 'TRX', + contractAddress: 'TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t', + protocolType: 'TRC20', + ); + + final json = request.toJson(); + final params = json['params'] as Map; + + expect(params['protocol'], { + 'type': 'TRC20', + 'protocol_data': { + 'platform': 'TRX', + 'contract_address': 'TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t', + }, + }); + expect( + params['activation_params'], + containsPair('nodes', [ + {'url': 'https://api.trongrid.io', 'gui_auth': false}, + ]), + ); + }); + + test('withdraw init serializes expiration_seconds for TRON', () { + final request = WithdrawInitRequest( + rpcPass: 'rpc-pass', + params: WithdrawParameters( + asset: 'TRX', + toAddress: 'TW9RqU6bTJnM4quyRbvTwm3xfSHgk718qU', + amount: Decimal.parse('10'), + expirationSeconds: 90, + ), + ); + + final json = request.toJson(); + final params = json['params'] as Map; + + expect(params['coin'], 'TRX'); + expect(params['expiration_seconds'], 90); + }); + }); + + group('TRON RPC response parsing', () { + test('enable_eth_with_tokens parses legacy TRON address maps', () { + final response = EnableEthWithTokensResponse.parse({ + 'mmrpc': '2.0', + 'result': { + 'current_block': 68000000, + 'eth_addresses_infos': { + 'TDcxD6E5wTzvqCJd4RfkGfw9NkCBdvYcV9': { + 'derivation_method': {'type': 'Iguana'}, + 'pubkey': '04abc', + 'balances': {'spendable': '50.000000', 'unspendable': '0'}, + }, + }, + 'erc20_addresses_infos': { + 'TDcxD6E5wTzvqCJd4RfkGfw9NkCBdvYcV9': { + 'derivation_method': {'type': 'Iguana'}, + 'pubkey': '04abc', + 'balances': { + 'USDT-TRC20': {'spendable': '10.000000', 'unspendable': '0'}, + }, + }, + }, + 'nfts_infos': {}, + }, + }, platformTicker: 'TRX'); + + expect(response.currentBlock, 68000000); + expect(response.walletBalance.accounts, hasLength(1)); + expect(response.walletBalance.accounts.first.addresses, hasLength(1)); + expect( + response.walletBalance.accounts.first.addresses.first.address, + 'TDcxD6E5wTzvqCJd4RfkGfw9NkCBdvYcV9', + ); + expect( + response.walletBalance.accounts.first.addresses.first.balance + .balanceOf('TRX') + .spendable, + Decimal.parse('50.000000'), + ); + expect( + response.walletBalance.accounts.first.addresses.first.balance + .balanceOf('USDT-TRC20') + .spendable, + Decimal.parse('10.000000'), + ); + }); + }); +} diff --git a/packages/komodo_defi_rpc_methods/test/src/withdraw_request_test.dart b/packages/komodo_defi_rpc_methods/test/src/withdraw_request_test.dart new file mode 100644 index 000000000..f35d2efba --- /dev/null +++ b/packages/komodo_defi_rpc_methods/test/src/withdraw_request_test.dart @@ -0,0 +1,36 @@ +import 'package:decimal/decimal.dart'; +import 'package:komodo_defi_rpc_methods/komodo_defi_rpc_methods.dart'; +import 'package:komodo_defi_types/komodo_defi_types.dart'; +import 'package:test/test.dart'; + +void main() { + group('withdraw request serialization', () { + test('serializes Tendermint fee requests as CosmosGas', () { + final request = WithdrawInitRequest( + rpcPass: 'rpc-pass', + params: WithdrawParameters( + asset: 'ATOM', + toAddress: 'cosmos1destination', + amount: Decimal.parse('0.1'), + fee: FeeInfo.tendermint( + coin: 'ATOM', + amount: Decimal.parse('0.038553'), + gasLimit: 100000, + ), + ), + ); + + final json = request.toJson(); + final params = json['params'] as Map; + final fee = params['fee'] as Map; + + expect(fee['type'], equals('CosmosGas')); + expect(fee['coin'], equals('ATOM')); + expect(fee['gas_limit'], equals(100000)); + expect( + (fee['gas_price'] as num).toDouble(), + closeTo(0.00000038553, 1e-18), + ); + }); + }); +} diff --git a/packages/komodo_defi_sdk/lib/src/activation/base_strategies/activation_strategy_base.dart b/packages/komodo_defi_sdk/lib/src/activation/base_strategies/activation_strategy_base.dart index 9af28a4ab..c671d135f 100644 --- a/packages/komodo_defi_sdk/lib/src/activation/base_strategies/activation_strategy_base.dart +++ b/packages/komodo_defi_sdk/lib/src/activation/base_strategies/activation_strategy_base.dart @@ -89,7 +89,7 @@ class SmartAssetActivator extends BatchCapableActivator { } bool _supportsBatchActivation(Asset asset) { - return asset.protocol.supportedProtocols.isNotEmpty; + return _activator.supportsBatchActivationFor(asset); } } @@ -115,6 +115,10 @@ class CompositeAssetActivator extends BatchCapableActivator { return strategy; } + bool supportsBatchActivationFor(Asset asset) { + return _findStrategy(asset).supportsBatchActivation; + } + @override bool canHandle(Asset asset) => _strategies.any((s) => s.canHandle(asset)); diff --git a/packages/komodo_defi_sdk/lib/src/activation/protocol_strategies/custom_erc20_activation_strategy.dart b/packages/komodo_defi_sdk/lib/src/activation/protocol_strategies/custom_erc20_activation_strategy.dart index 1eb19f170..2320d68d3 100644 --- a/packages/komodo_defi_sdk/lib/src/activation/protocol_strategies/custom_erc20_activation_strategy.dart +++ b/packages/komodo_defi_sdk/lib/src/activation/protocol_strategies/custom_erc20_activation_strategy.dart @@ -14,6 +14,7 @@ class CustomErc20ActivationStrategy extends ProtocolActivationStrategy { @override Set get supportedProtocols => { + CoinSubClass.trc20, CoinSubClass.erc20, CoinSubClass.grc20, CoinSubClass.bep20, @@ -31,7 +32,6 @@ class CustomErc20ActivationStrategy extends ProtocolActivationStrategy { CoinSubClass.rskSmartBitcoin, CoinSubClass.arbitrum, CoinSubClass.base, - CoinSubClass.grc20, }; @override @@ -66,11 +66,23 @@ class CustomErc20ActivationStrategy extends ProtocolActivationStrategy { throw StateError('Protocol data is missing from custom token config'); } - final activationParams = Erc20ActivationParams.fromJsonConfig( - asset.protocol.config, - ); + final activationParams = switch (asset.protocol) { + final Erc20Protocol _ => Erc20ActivationParams.fromJsonConfig( + asset.protocol.config, + ), + final Trc20Protocol _ => Trc20ActivationParams.fromJsonConfig( + asset.protocol.config, + ), + _ => throw UnsupportedError( + 'Unsupported custom token protocol: ${asset.protocol.runtimeType}', + ), + }; final platform = protocolData.value('platform'); final contractAddress = protocolData.value('contract_address'); + final protocolType = switch (asset.protocol.subClass) { + CoinSubClass.trc20 => 'TRC20', + _ => 'ERC20', + }; // Debug logging for custom ERC20 token activation if (KdfLoggingConfig.verboseLogging) { @@ -89,6 +101,7 @@ class CustomErc20ActivationStrategy extends ProtocolActivationStrategy { activationParams: activationParams, platform: platform, contractAddress: contractAddress, + protocolType: protocolType, ); if (KdfLoggingConfig.verboseLogging) { @@ -114,7 +127,7 @@ class CustomErc20ActivationStrategy extends ProtocolActivationStrategy { asset: asset, error: e, stackTrace: stack, - errorCode: 'ERC20_ACTIVATION_ERROR', + errorCode: 'CUSTOM_TOKEN_ACTIVATION_ERROR', stepCount: 2, ); } diff --git a/packages/komodo_defi_sdk/lib/src/activation/protocol_strategies/erc20_activation_strategy.dart b/packages/komodo_defi_sdk/lib/src/activation/protocol_strategies/erc20_activation_strategy.dart index e35bab0d1..81becf33e 100644 --- a/packages/komodo_defi_sdk/lib/src/activation/protocol_strategies/erc20_activation_strategy.dart +++ b/packages/komodo_defi_sdk/lib/src/activation/protocol_strategies/erc20_activation_strategy.dart @@ -15,6 +15,7 @@ class Erc20ActivationStrategy extends ProtocolActivationStrategy { @override Set get supportedProtocols => { + CoinSubClass.trc20, CoinSubClass.erc20, CoinSubClass.grc20, CoinSubClass.bep20, @@ -32,7 +33,6 @@ class Erc20ActivationStrategy extends ProtocolActivationStrategy { CoinSubClass.rskSmartBitcoin, CoinSubClass.arbitrum, CoinSubClass.base, - CoinSubClass.grc20, }; @override @@ -69,9 +69,17 @@ class Erc20ActivationStrategy extends ProtocolActivationStrategy { ); try { - final activationParams = Erc20ActivationParams.fromJsonConfig( - asset.protocol.config, - ); + final activationParams = switch (asset.protocol) { + final Erc20Protocol _ => Erc20ActivationParams.fromJsonConfig( + asset.protocol.config, + ).copyWith(privKeyPolicy: privKeyPolicy), + final Trc20Protocol _ => Trc20ActivationParams.fromJsonConfig( + asset.protocol.config, + ).copyWith(privKeyPolicy: privKeyPolicy), + _ => throw UnsupportedError( + 'Unsupported token protocol: ${asset.protocol.runtimeType}', + ), + }; // Debug logging for ERC20 token activation if (KdfLoggingConfig.verboseLogging) { @@ -113,7 +121,7 @@ class Erc20ActivationStrategy extends ProtocolActivationStrategy { asset: asset, error: e, stackTrace: stack, - errorCode: 'ERC20_ACTIVATION_ERROR', + errorCode: 'TOKEN_ACTIVATION_ERROR', stepCount: 2, ); } diff --git a/packages/komodo_defi_sdk/lib/src/activation/protocol_strategies/eth_task_activation_strategy.dart b/packages/komodo_defi_sdk/lib/src/activation/protocol_strategies/eth_task_activation_strategy.dart index d323b07a9..946cba7eb 100644 --- a/packages/komodo_defi_sdk/lib/src/activation/protocol_strategies/eth_task_activation_strategy.dart +++ b/packages/komodo_defi_sdk/lib/src/activation/protocol_strategies/eth_task_activation_strategy.dart @@ -17,6 +17,7 @@ class EthTaskActivationStrategy extends ProtocolActivationStrategy { @override Set get supportedProtocols => { + CoinSubClass.trx, CoinSubClass.erc20, CoinSubClass.grc20, CoinSubClass.bep20, @@ -34,7 +35,6 @@ class EthTaskActivationStrategy extends ProtocolActivationStrategy { CoinSubClass.rskSmartBitcoin, CoinSubClass.arbitrum, CoinSubClass.base, - CoinSubClass.grc20, }; @override @@ -52,7 +52,7 @@ class EthTaskActivationStrategy extends ProtocolActivationStrategy { Asset asset, [ List? children, ]) async* { - final protocol = asset.protocol as Erc20Protocol; + final protocol = asset.protocol; yield ActivationProgress( status: 'Starting ${asset.id.name} activation...', @@ -62,7 +62,7 @@ class EthTaskActivationStrategy extends ProtocolActivationStrategy { additionalInfo: { 'chainType': protocol.subClass.formatted, 'contractAddress': protocol.contractAddress, - 'nodes': protocol.nodes.length, + 'nodes': protocol.requiredServers.electrum?.length ?? 0, }, ), ); @@ -84,21 +84,35 @@ class EthTaskActivationStrategy extends ProtocolActivationStrategy { asset, ); - final activationParams = + final tokenRequests = + children?.map((e) => TokensRequest(ticker: e.id.id)).toList() ?? []; + final activationParams = switch (asset.protocol) { + final Erc20Protocol _ => EthWithTokensActivationParams.fromJson( asset.protocol.config, ).copyWith( - erc20Tokens: - children?.map((e) => TokensRequest(ticker: e.id.id)).toList() ?? - [], + erc20Tokens: tokenRequests, txHistory: txHistoryFlag, privKeyPolicy: privKeyPolicy, - ); + ), + final TrxProtocol _ => + TrxWithTokensActivationParams.fromJson( + asset.protocol.config, + ).copyWith( + tokenRequests: tokenRequests, + txHistory: txHistoryFlag, + privKeyPolicy: privKeyPolicy, + ), + _ => throw UnsupportedError( + 'Unsupported platform protocol for task activation: ' + '${asset.protocol.runtimeType}', + ), + }; // Debug logging for ETH task-based activation if (KdfLoggingConfig.verboseLogging) { log( - '[RPC] Activating ETH platform (task-based): ${asset.id.id}', + '[RPC] Activating platform asset (task-based): ${asset.id.id}', name: 'EthTaskActivationStrategy', ); log( @@ -180,7 +194,7 @@ class EthTaskActivationStrategy extends ProtocolActivationStrategy { asset: asset, error: e, stackTrace: stack, - errorCode: 'ETH_TASK_ACTIVATION_ERROR', + errorCode: 'PLATFORM_TASK_ACTIVATION_ERROR', stepCount: 5, ); } @@ -210,7 +224,7 @@ class EthTaskActivationStrategy extends ProtocolActivationStrategy { ); case 'ActivatingTokens': return ( - status: 'Activating ERC20 tokens...', + status: 'Activating token assets...', percentage: 80, step: ActivationStep.tokenActivation, info: {'activationType': 'tokens'}, diff --git a/packages/komodo_defi_sdk/lib/src/activation/protocol_strategies/eth_with_tokens_activation_strategy.dart b/packages/komodo_defi_sdk/lib/src/activation/protocol_strategies/eth_with_tokens_activation_strategy.dart index da4d7600e..2320e9206 100644 --- a/packages/komodo_defi_sdk/lib/src/activation/protocol_strategies/eth_with_tokens_activation_strategy.dart +++ b/packages/komodo_defi_sdk/lib/src/activation/protocol_strategies/eth_with_tokens_activation_strategy.dart @@ -17,6 +17,7 @@ class EthWithTokensActivationStrategy extends ProtocolActivationStrategy { @override Set get supportedProtocols => { + CoinSubClass.trx, CoinSubClass.erc20, CoinSubClass.grc20, CoinSubClass.bep20, @@ -34,7 +35,6 @@ class EthWithTokensActivationStrategy extends ProtocolActivationStrategy { CoinSubClass.rskSmartBitcoin, CoinSubClass.arbitrum, CoinSubClass.base, - CoinSubClass.grc20, }; @override @@ -105,21 +105,35 @@ class EthWithTokensActivationStrategy extends ProtocolActivationStrategy { asset, ); - final activationParams = + final tokenRequests = + children?.map((e) => TokensRequest(ticker: e.id.id)).toList() ?? []; + final activationParams = switch (asset.protocol) { + final Erc20Protocol _ => EthWithTokensActivationParams.fromJson( asset.protocol.config, ).copyWith( - erc20Tokens: - children?.map((e) => TokensRequest(ticker: e.id.id)).toList() ?? - [], + erc20Tokens: tokenRequests, txHistory: txHistoryFlag, privKeyPolicy: privKeyPolicy, - ); + ), + final TrxProtocol _ => + TrxWithTokensActivationParams.fromJson( + asset.protocol.config, + ).copyWith( + tokenRequests: tokenRequests, + txHistory: txHistoryFlag, + privKeyPolicy: privKeyPolicy, + ), + _ => throw UnsupportedError( + 'Unsupported platform protocol for batch activation: ' + '${asset.protocol.runtimeType}', + ), + }; // Debug logging for ETH platform activation if (KdfLoggingConfig.verboseLogging) { log( - '[RPC] Activating ETH platform: ${asset.id.id}', + '[RPC] Activating platform asset: ${asset.id.id}', name: 'EthWithTokensActivationStrategy', ); } @@ -137,7 +151,7 @@ class EthWithTokensActivationStrategy extends ProtocolActivationStrategy { if (KdfLoggingConfig.verboseLogging) { log( - '[RPC] Successfully activated ETH platform: ${asset.id.id} with ${children?.length ?? 0} tokens', + '[RPC] Successfully activated platform asset: ${asset.id.id} with ${children?.length ?? 0} tokens', name: 'EthWithTokensActivationStrategy', ); } @@ -168,7 +182,7 @@ class EthWithTokensActivationStrategy extends ProtocolActivationStrategy { asset: asset, error: e, stackTrace: stack, - errorCode: 'ETH_WITH_TOKENS_ACTIVATION_ERROR', + errorCode: 'PLATFORM_WITH_TOKENS_ACTIVATION_ERROR', stepCount: 3, ); } diff --git a/packages/komodo_defi_sdk/lib/src/withdrawals/withdrawal_manager.dart b/packages/komodo_defi_sdk/lib/src/withdrawals/withdrawal_manager.dart index 954c2ed72..febe40f0f 100644 --- a/packages/komodo_defi_sdk/lib/src/withdrawals/withdrawal_manager.dart +++ b/packages/komodo_defi_sdk/lib/src/withdrawals/withdrawal_manager.dart @@ -1031,6 +1031,7 @@ class WithdrawalManager { memo: params.memo, ibcTransfer: params.ibcTransfer, ibcSourceChannel: params.ibcSourceChannel, + expirationSeconds: params.expirationSeconds, isMax: params.isMax, ); } catch (e, stackTrace) { diff --git a/packages/komodo_defi_sdk/test/activation/tron_activation_strategy_test.dart b/packages/komodo_defi_sdk/test/activation/tron_activation_strategy_test.dart new file mode 100644 index 000000000..f8d15410f --- /dev/null +++ b/packages/komodo_defi_sdk/test/activation/tron_activation_strategy_test.dart @@ -0,0 +1,93 @@ +import 'package:komodo_defi_sdk/src/activation/protocol_strategies/custom_erc20_activation_strategy.dart'; +import 'package:komodo_defi_sdk/src/activation/protocol_strategies/erc20_activation_strategy.dart'; +import 'package:komodo_defi_sdk/src/activation/protocol_strategies/eth_task_activation_strategy.dart'; +import 'package:komodo_defi_sdk/src/activation/protocol_strategies/eth_with_tokens_activation_strategy.dart'; +import 'package:komodo_defi_rpc_methods/komodo_defi_rpc_methods.dart'; +import 'package:komodo_defi_types/komodo_defi_types.dart'; +import 'package:test/test.dart'; + +Map _trxConfig() => { + 'coin': 'TRX', + 'type': 'TRX', + 'name': 'TRON', + 'fname': 'TRON', + 'wallet_only': true, + 'mm2': 1, + 'decimals': 6, + 'required_confirmations': 1, + 'derivation_path': "m/44'/195'", + 'protocol': { + 'type': 'TRX', + 'protocol_data': {'network': 'Mainnet'}, + }, + 'nodes': >[], +}; + +Map _trc20Config() => { + 'coin': 'USDT-TRC20', + 'type': 'TRC-20', + 'name': 'Tether', + 'fname': 'Tether', + 'wallet_only': true, + 'mm2': 1, + 'decimals': 6, + 'derivation_path': "m/44'/195'", + 'protocol': { + 'type': 'TRC20', + 'protocol_data': { + 'platform': 'TRX', + 'contract_address': 'TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t', + }, + }, + 'contract_address': 'TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t', + 'parent_coin': 'TRX', + 'nodes': >[], +}; + +void main() { + group('TRON activation strategy support', () { + final client = ApiClientMock(); + final parent = Asset.fromJson(_trxConfig(), knownIds: const {}); + final child = Asset.fromJson(_trc20Config(), knownIds: {parent.id}); + + test('non-Trezor platform strategy accepts TRX parent assets', () { + final strategy = EthWithTokensActivationStrategy( + client, + const PrivateKeyPolicy.contextPrivKey(), + ); + + expect(strategy.canHandle(parent), isTrue); + expect(strategy.canHandle(child), isFalse); + }); + + test('Trezor platform strategy accepts TRX parent assets', () { + final strategy = EthTaskActivationStrategy( + client, + const PrivateKeyPolicy.trezor(), + ); + + expect(strategy.canHandle(parent), isTrue); + expect(strategy.canHandle(child), isFalse); + }); + + test('token strategy accepts configured TRC20 child assets', () { + final strategy = Erc20ActivationStrategy( + client, + const PrivateKeyPolicy.contextPrivKey(), + ); + + expect(strategy.canHandle(child), isTrue); + }); + + test('custom token strategy accepts custom TRC20 child assets', () { + final customChild = child.copyWith( + protocol: (child.protocol as Trc20Protocol).copyWith( + isCustomToken: true, + ), + ); + final strategy = CustomErc20ActivationStrategy(client); + + expect(strategy.canHandle(customChild), isTrue); + }); + }); +} diff --git a/packages/komodo_defi_types/lib/src/assets/asset_id.dart b/packages/komodo_defi_types/lib/src/assets/asset_id.dart index c59319fc7..a73970ead 100644 --- a/packages/komodo_defi_types/lib/src/assets/asset_id.dart +++ b/packages/komodo_defi_types/lib/src/assets/asset_id.dart @@ -2,6 +2,7 @@ import 'package:equatable/equatable.dart'; import 'package:komodo_defi_types/komodo_defi_type_utils.dart'; import 'package:komodo_defi_types/komodo_defi_types.dart'; import 'package:komodo_defi_types/src/utils/json_type_utils.dart'; +import 'package:komodo_defi_types/src/utils/protocol_type_utils.dart'; class AssetId extends Equatable { const AssetId({ @@ -15,7 +16,7 @@ class AssetId extends Equatable { }); factory AssetId.parse(JsonMap json, {required Set? knownIds}) { - final subClass = CoinSubClass.parse(json.value('type')); + final subClass = resolveProtocolSubClassFromConfig(json); final parentCoinTicker = json.valueOrNull('parent_coin'); final maybeParent = parentCoinTicker == null diff --git a/packages/komodo_defi_types/lib/src/coin_classes/coin_subclasses.dart b/packages/komodo_defi_types/lib/src/coin_classes/coin_subclasses.dart index 660984a4c..e56734951 100644 --- a/packages/komodo_defi_types/lib/src/coin_classes/coin_subclasses.dart +++ b/packages/komodo_defi_types/lib/src/coin_classes/coin_subclasses.dart @@ -3,6 +3,8 @@ import 'dart:ui'; // TODO? Add a getter for the ticker of the coin subclass if needed. But this // may overlap with the protocol class, in which case it's not needed. enum CoinSubClass { + trx, + trc20, moonbeam, ftm20, arbitrum, @@ -40,6 +42,10 @@ enum CoinSubClass { // TODO: verify all the tickers. String get ticker { switch (this) { + case CoinSubClass.trx: + return 'TRX'; + case CoinSubClass.trc20: + return 'TRX'; case CoinSubClass.moonbeam: return 'MOON'; case CoinSubClass.ftm20: @@ -100,6 +106,10 @@ enum CoinSubClass { String get iconTicker { switch (this) { + case CoinSubClass.trx: + return 'TRX'; + case CoinSubClass.trc20: + return 'TRC'; case CoinSubClass.moonbeam: return 'GLMR'; case CoinSubClass.ftm20: @@ -248,6 +258,10 @@ enum CoinSubClass { return true; } + if (this == CoinSubClass.trx && child == CoinSubClass.trc20) { + return true; + } + // For most cases, parent and child should have the same subclass return this == child; } @@ -261,6 +275,10 @@ enum CoinSubClass { // subclasses where they don't have a symbol used in coin IDs. String get formatted { switch (this) { + case CoinSubClass.trx: + return 'TRON'; + case CoinSubClass.trc20: + return 'TRC20'; case CoinSubClass.moonbeam: return 'Moonbeam'; case CoinSubClass.ftm20: @@ -320,6 +338,9 @@ enum CoinSubClass { Color? get color { switch (this) { + case CoinSubClass.trx: + case CoinSubClass.trc20: + return const Color(0xFFFF060A); // trx: "#ff060a" case CoinSubClass.moonbeam: return const Color(0xFFE4147C); // glmr: "#e4147c" case CoinSubClass.ftm20: @@ -385,6 +406,8 @@ extension CoinSubClassTokenStandard on CoinSubClass { /// be appended for the given subclass. String? get tokenStandardSuffix { switch (this) { + case CoinSubClass.trc20: + return 'TRC20'; case CoinSubClass.erc20: return 'ERC20'; case CoinSubClass.grc20: @@ -413,6 +436,7 @@ extension CoinSubClassTokenStandard on CoinSubClass { return 'HCO20'; // Subclasses without a canonical short token/network standard suffix case CoinSubClass.moonbeam: + case CoinSubClass.trx: case CoinSubClass.slp: // ignore: deprecated_member_use_from_same_package case CoinSubClass.sia: case CoinSubClass.smartChain: diff --git a/packages/komodo_defi_types/lib/src/coin_classes/protocol_class.dart b/packages/komodo_defi_types/lib/src/coin_classes/protocol_class.dart index 8ca8eb917..d58c1e86a 100644 --- a/packages/komodo_defi_types/lib/src/coin_classes/protocol_class.dart +++ b/packages/komodo_defi_types/lib/src/coin_classes/protocol_class.dart @@ -4,6 +4,7 @@ import 'package:komodo_defi_rpc_methods/komodo_defi_rpc_methods.dart'; import 'package:komodo_defi_types/komodo_defi_type_utils.dart'; import 'package:komodo_defi_types/komodo_defi_types.dart'; import 'package:komodo_defi_types/src/utils/json_type_utils.dart'; +import 'package:komodo_defi_types/src/utils/protocol_type_utils.dart'; /// Base class for all protocol definitions abstract class ProtocolClass with ExplorerUrlMixin implements Equatable { @@ -16,8 +17,8 @@ abstract class ProtocolClass with ExplorerUrlMixin implements Equatable { /// Creates the appropriate protocol class from JSON config factory ProtocolClass.fromJson(JsonMap json, {CoinSubClass? requestedType}) { - final primaryType = - requestedType ?? CoinSubClass.parse(json.value('type')); + final resolvedType = resolveProtocolSubClassFromConfig(json); + final primaryType = requestedType ?? resolvedType; final otherTypes = json .valueOrNull>('other_types') @@ -25,15 +26,16 @@ abstract class ProtocolClass with ExplorerUrlMixin implements Equatable { .toList() ?? []; - // If a specific type is requested, update the config - final configToUse = requestedType != null && requestedType != primaryType - ? (JsonMap.of(json) - ..['type'] = requestedType.toString().split('.').last) - : json; try { return switch (primaryType) { + CoinSubClass.trx => TrxProtocol.fromJson(json, subClass: primaryType), + CoinSubClass.trc20 => Trc20Protocol.fromJson( + json, + subClass: primaryType, + ), CoinSubClass.utxo || CoinSubClass.smartChain => UtxoProtocol.fromJson( - configToUse, + json, + subClass: primaryType, supportedProtocols: otherTypes, ), // SLP is no longer supported by its own protocol (BCH) @@ -57,16 +59,27 @@ abstract class ProtocolClass with ExplorerUrlMixin implements Equatable { CoinSubClass.hecoChain || CoinSubClass.rskSmartBitcoin || CoinSubClass.grc20 || - CoinSubClass.erc20 => Erc20Protocol.fromJson(json), - CoinSubClass.qrc20 => QtumProtocol.fromJson(json), - CoinSubClass.zhtlc => ZhtlcProtocol.fromJson(json), + CoinSubClass.erc20 => Erc20Protocol.fromJson( + json, + subClass: primaryType, + ), + CoinSubClass.qrc20 => QtumProtocol.fromJson( + json, + subClass: primaryType, + ), + CoinSubClass.zhtlc => ZhtlcProtocol.fromJson( + json, + subClass: primaryType, + ), CoinSubClass.tendermintToken || CoinSubClass.tendermint => TendermintProtocol.fromJson( - configToUse, + json, + subClass: primaryType, supportedProtocols: otherTypes, ), CoinSubClass.sia => SiaProtocol.fromJson( - configToUse, + json, + subClass: primaryType, supportedProtocols: otherTypes, ), // ignore: deprecated_member_use_from_same_package @@ -133,10 +146,7 @@ abstract class ProtocolClass with ExplorerUrlMixin implements Equatable { ProtocolClass? createProtocolVariant(CoinSubClass type) { if (!supportsProtocolType(type) || type == subClass) return null; - final variantConfig = JsonMap.from(config) - ..['type'] = type.toString().split('.').last; - - return ProtocolClass.fromJson(variantConfig); + return ProtocolClass.fromJson(config, requestedType: type); } /// Declarative streaming capabilities based on protocol subclass and @@ -178,7 +188,13 @@ abstract class ProtocolClass with ExplorerUrlMixin implements Equatable { config, ).genericCopyWith(privKeyPolicy: privKeyPolicy); - String? get contractAddress => config.valueOrNull('contract_address'); + String? get contractAddress => + config.valueOrNull('contract_address') ?? + config.valueOrNull( + 'protocol', + 'protocol_data', + 'contract_address', + ); @override List get props => [ diff --git a/packages/komodo_defi_types/lib/src/protocols/erc20/erc20_protocol.dart b/packages/komodo_defi_types/lib/src/protocols/erc20/erc20_protocol.dart index 88cf9e418..cd98f3305 100644 --- a/packages/komodo_defi_types/lib/src/protocols/erc20/erc20_protocol.dart +++ b/packages/komodo_defi_types/lib/src/protocols/erc20/erc20_protocol.dart @@ -1,6 +1,7 @@ import 'package:komodo_defi_rpc_methods/komodo_defi_rpc_methods.dart'; import 'package:komodo_defi_types/komodo_defi_type_utils.dart'; import 'package:komodo_defi_types/komodo_defi_types.dart'; +import 'package:komodo_defi_types/src/utils/protocol_type_utils.dart'; class Erc20Protocol extends ProtocolClass { Erc20Protocol._({ @@ -9,10 +10,10 @@ class Erc20Protocol extends ProtocolClass { super.isCustomToken = false, }); - factory Erc20Protocol.fromJson(JsonMap json) { + factory Erc20Protocol.fromJson(JsonMap json, {CoinSubClass? subClass}) { _validateErc20Config(json); return Erc20Protocol._( - subClass: CoinSubClass.parse(json.value('type')), + subClass: subClass ?? resolveProtocolSubClassFromConfig(json), isCustomToken: json.valueOrNull('is_custom_token') ?? false, config: json, ); @@ -31,7 +32,9 @@ class Erc20Protocol extends ProtocolClass { ActivationParams defaultActivationParams({PrivateKeyPolicy? privKeyPolicy}) { // For ERC20, we typically don't need child tokens in the default case // If you need to support child tokens, you can add an overloaded method - return Erc20ActivationParams.fromJsonConfig(super.config); + return Erc20ActivationParams.fromJsonConfig( + super.config, + ).copyWith(privKeyPolicy: privKeyPolicy); } ActivationParams activationParamsWithTokens([List? childTokens]) { @@ -54,10 +57,7 @@ class Erc20Protocol extends ProtocolClass { for (final field in requiredFields.entries) { if (!json.containsKey(field.key)) { - throw MissingProtocolFieldException( - field.value, - field.key, - ); + throw MissingProtocolFieldException(field.value, field.key); } } } diff --git a/packages/komodo_defi_types/lib/src/protocols/qtum/qtum_protocol.dart b/packages/komodo_defi_types/lib/src/protocols/qtum/qtum_protocol.dart index e3a3dc2e2..4092100fa 100644 --- a/packages/komodo_defi_types/lib/src/protocols/qtum/qtum_protocol.dart +++ b/packages/komodo_defi_types/lib/src/protocols/qtum/qtum_protocol.dart @@ -1,17 +1,15 @@ import 'package:komodo_defi_rpc_methods/komodo_defi_rpc_methods.dart'; import 'package:komodo_defi_types/komodo_defi_type_utils.dart'; import 'package:komodo_defi_types/komodo_defi_types.dart'; +import 'package:komodo_defi_types/src/utils/protocol_type_utils.dart'; class QtumProtocol extends ProtocolClass { - QtumProtocol._({ - required super.subClass, - required super.config, - }); + QtumProtocol._({required super.subClass, required super.config}); - factory QtumProtocol.fromJson(JsonMap json) { + factory QtumProtocol.fromJson(JsonMap json, {CoinSubClass? subClass}) { _validateQtumConfig(json); return QtumProtocol._( - subClass: CoinSubClass.parse(json.value('type')), + subClass: subClass ?? resolveProtocolSubClassFromConfig(json), config: json, ); } @@ -36,10 +34,7 @@ class QtumProtocol extends ProtocolClass { for (final field in requiredFields.entries) { if (!json.containsKey(field.key)) { - throw MissingProtocolFieldException( - field.value, - field.key, - ); + throw MissingProtocolFieldException(field.value, field.key); } } } diff --git a/packages/komodo_defi_types/lib/src/protocols/sia/sia_protocol.dart b/packages/komodo_defi_types/lib/src/protocols/sia/sia_protocol.dart index 98264854b..51280f40a 100644 --- a/packages/komodo_defi_types/lib/src/protocols/sia/sia_protocol.dart +++ b/packages/komodo_defi_types/lib/src/protocols/sia/sia_protocol.dart @@ -1,5 +1,6 @@ import 'package:komodo_defi_types/komodo_defi_type_utils.dart'; import 'package:komodo_defi_types/komodo_defi_types.dart'; +import 'package:komodo_defi_types/src/utils/protocol_type_utils.dart'; class SiaProtocol extends ProtocolClass { SiaProtocol._({ @@ -10,11 +11,12 @@ class SiaProtocol extends ProtocolClass { factory SiaProtocol.fromJson( JsonMap json, { + CoinSubClass? subClass, List supportedProtocols = const [], }) { _validateSiaConfig(json); return SiaProtocol._( - subClass: CoinSubClass.parse(json.value('type')), + subClass: subClass ?? resolveProtocolSubClassFromConfig(json), config: json, supportedProtocols: supportedProtocols, ); @@ -22,9 +24,7 @@ class SiaProtocol extends ProtocolClass { static void _validateSiaConfig(JsonMap json) { // Minimal required fields for SIA protocol configuration - final requiredFields = { - 'nodes': 'Seed nodes list', - }; + final requiredFields = {'nodes': 'Seed nodes list'}; for (final field in requiredFields.entries) { if (!json.containsKey(field.key)) { throw MissingProtocolFieldException(field.value, field.key); diff --git a/packages/komodo_defi_types/lib/src/protocols/slp/slp_protocol.dart b/packages/komodo_defi_types/lib/src/protocols/slp/slp_protocol.dart index 031ee34d9..9626ddaf1 100644 --- a/packages/komodo_defi_types/lib/src/protocols/slp/slp_protocol.dart +++ b/packages/komodo_defi_types/lib/src/protocols/slp/slp_protocol.dart @@ -1,5 +1,6 @@ import 'package:komodo_defi_types/komodo_defi_type_utils.dart'; import 'package:komodo_defi_types/komodo_defi_types.dart'; +import 'package:komodo_defi_types/src/utils/protocol_type_utils.dart'; class SlpProtocol extends ProtocolClass { SlpProtocol._({ @@ -10,11 +11,12 @@ class SlpProtocol extends ProtocolClass { factory SlpProtocol.fromJson( JsonMap json, { + CoinSubClass? subClass, List supportedProtocols = const [], }) { _validateSlpConfig(json); return SlpProtocol._( - subClass: CoinSubClass.parse(json.value('type')), + subClass: subClass ?? resolveProtocolSubClassFromConfig(json), config: json, supportedProtocols: supportedProtocols, ); @@ -42,10 +44,7 @@ class SlpProtocol extends ProtocolClass { for (final field in requiredFields.entries) { if (!json.containsKey(field.key)) { - throw MissingProtocolFieldException( - field.value, - field.key, - ); + throw MissingProtocolFieldException(field.value, field.key); } } } diff --git a/packages/komodo_defi_types/lib/src/protocols/tendermint/tendermint_protocol.dart b/packages/komodo_defi_types/lib/src/protocols/tendermint/tendermint_protocol.dart index d9450ee32..f4482bede 100644 --- a/packages/komodo_defi_types/lib/src/protocols/tendermint/tendermint_protocol.dart +++ b/packages/komodo_defi_types/lib/src/protocols/tendermint/tendermint_protocol.dart @@ -1,6 +1,7 @@ import 'package:komodo_defi_rpc_methods/komodo_defi_rpc_methods.dart'; import 'package:komodo_defi_types/komodo_defi_type_utils.dart'; import 'package:komodo_defi_types/komodo_defi_types.dart'; +import 'package:komodo_defi_types/src/utils/protocol_type_utils.dart'; class TendermintProtocol extends ProtocolClass { TendermintProtocol._({ @@ -11,11 +12,12 @@ class TendermintProtocol extends ProtocolClass { factory TendermintProtocol.fromJson( JsonMap json, { + CoinSubClass? subClass, List supportedProtocols = const [], }) { _validateTendermintConfig(json); return TendermintProtocol._( - subClass: CoinSubClass.parse(json.value('type')), + subClass: subClass ?? resolveProtocolSubClassFromConfig(json), config: json, supportedProtocols: supportedProtocols, ); @@ -30,10 +32,7 @@ class TendermintProtocol extends ProtocolClass { for (final field in requiredFields.entries) { if (!json.containsKey(field.key)) { - throw MissingProtocolFieldException( - field.value, - field.key, - ); + throw MissingProtocolFieldException(field.value, field.key); } } } diff --git a/packages/komodo_defi_types/lib/src/protocols/trc20/trc20_protocol.dart b/packages/komodo_defi_types/lib/src/protocols/trc20/trc20_protocol.dart new file mode 100644 index 000000000..8fc7ea54a --- /dev/null +++ b/packages/komodo_defi_types/lib/src/protocols/trc20/trc20_protocol.dart @@ -0,0 +1,112 @@ +import 'package:komodo_defi_rpc_methods/komodo_defi_rpc_methods.dart'; +import 'package:komodo_defi_types/komodo_defi_type_utils.dart'; +import 'package:komodo_defi_types/komodo_defi_types.dart'; +import 'package:komodo_defi_types/src/utils/protocol_type_utils.dart'; + +class Trc20Protocol extends ProtocolClass { + Trc20Protocol._({ + required super.subClass, + required super.config, + super.isCustomToken = false, + }); + + factory Trc20Protocol.fromJson(JsonMap json, {CoinSubClass? subClass}) { + _validateTrc20Config(json); + return Trc20Protocol._( + subClass: subClass ?? resolveProtocolSubClassFromConfig(json), + isCustomToken: json.valueOrNull('is_custom_token') ?? false, + config: json, + ); + } + + static void _validateTrc20Config(JsonMap json) { + final hasNodes = json.containsKey('nodes'); + final hasPlatform = + json.valueOrNull('protocol', 'protocol_data', 'platform') != + null; + final hasContractAddress = + json.valueOrNull('contract_address') != null || + json.valueOrNull( + 'protocol', + 'protocol_data', + 'contract_address', + ) != + null; + + if (!hasNodes) { + throw MissingProtocolFieldException('RPC nodes', 'nodes'); + } + if (!hasPlatform) { + throw MissingProtocolFieldException( + 'Platform coin', + 'protocol.protocol_data.platform', + ); + } + if (!hasContractAddress) { + throw MissingProtocolFieldException( + 'Contract address', + 'protocol.protocol_data.contract_address', + ); + } + } + + List get nodes => + config.value('nodes').map(EvmNode.fromJson).toList(); + + String get platform => + config.value('protocol', 'protocol_data', 'platform'); + + @override + bool get supportsMultipleAddresses => true; + + @override + bool get requiresHdWallet => false; + + @override + bool get isMemoSupported => false; + + @override + Trc20ActivationParams defaultActivationParams({ + PrivateKeyPolicy privKeyPolicy = const PrivateKeyPolicy.contextPrivKey(), + }) { + return Trc20ActivationParams.fromJsonConfig( + config, + ).copyWith(privKeyPolicy: privKeyPolicy); + } + + Trc20Protocol copyWith({ + List? nodes, + String? contractAddress, + String? platform, + bool? isCustomToken, + }) { + final protocol = JsonMap.of( + config.valueOrNull('protocol') ?? const {}, + ); + final protocolData = JsonMap.of( + protocol.valueOrNull('protocol_data') ?? + const {}, + ); + if (contractAddress != null) { + protocolData['contract_address'] = contractAddress; + } + if (platform != null) { + protocolData['platform'] = platform; + } + if (contractAddress != null || platform != null) { + protocol['protocol_data'] = protocolData; + } + + return Trc20Protocol._( + subClass: subClass, + isCustomToken: isCustomToken ?? this.isCustomToken, + config: JsonMap.from(config) + ..addAll({ + if (nodes != null) + 'nodes': nodes.map((node) => node.toJson()).toList(), + if (contractAddress != null) 'contract_address': contractAddress, + if (contractAddress != null || platform != null) 'protocol': protocol, + }), + ); + } +} diff --git a/packages/komodo_defi_types/lib/src/protocols/trx/trx_protocol.dart b/packages/komodo_defi_types/lib/src/protocols/trx/trx_protocol.dart new file mode 100644 index 000000000..142269955 --- /dev/null +++ b/packages/komodo_defi_types/lib/src/protocols/trx/trx_protocol.dart @@ -0,0 +1,87 @@ +import 'package:komodo_defi_rpc_methods/komodo_defi_rpc_methods.dart'; +import 'package:komodo_defi_types/komodo_defi_type_utils.dart'; +import 'package:komodo_defi_types/komodo_defi_types.dart'; +import 'package:komodo_defi_types/src/utils/protocol_type_utils.dart'; + +class TrxProtocol extends ProtocolClass { + TrxProtocol._({ + required super.subClass, + required super.config, + super.isCustomToken = false, + }); + + factory TrxProtocol.fromJson(JsonMap json, {CoinSubClass? subClass}) { + _validateTrxConfig(json); + return TrxProtocol._( + subClass: subClass ?? resolveProtocolSubClassFromConfig(json), + isCustomToken: json.valueOrNull('is_custom_token') ?? false, + config: json, + ); + } + + static void _validateTrxConfig(JsonMap json) { + final hasNodes = json.containsKey('nodes'); + final hasProtocolType = + json.valueOrNull('protocol', 'type') != null; + + if (!hasNodes) { + throw MissingProtocolFieldException('RPC nodes', 'nodes'); + } + if (!hasProtocolType) { + throw MissingProtocolFieldException('Protocol type', 'protocol.type'); + } + } + + List get nodes => + config.value('nodes').map(EvmNode.fromJson).toList(); + + String? get network => + config.valueOrNull('protocol', 'protocol_data', 'network'); + + @override + bool get supportsMultipleAddresses => true; + + @override + bool get requiresHdWallet => false; + + @override + bool get isMemoSupported => false; + + @override + TrxWithTokensActivationParams defaultActivationParams({ + PrivateKeyPolicy privKeyPolicy = const PrivateKeyPolicy.contextPrivKey(), + }) { + return TrxWithTokensActivationParams.fromJson( + config, + ).copyWith(privKeyPolicy: privKeyPolicy); + } + + TrxProtocol copyWith({ + List? nodes, + String? network, + bool? isCustomToken, + }) { + final protocol = JsonMap.of( + config.valueOrNull('protocol') ?? const {}, + ); + final protocolData = JsonMap.of( + protocol.valueOrNull('protocol_data') ?? + const {}, + ); + if (network != null) { + protocolData['network'] = network; + protocol['protocol_data'] = protocolData; + } + + return TrxProtocol._( + subClass: subClass, + isCustomToken: isCustomToken ?? this.isCustomToken, + config: JsonMap.from(config) + ..addAll({ + if (nodes != null) + 'nodes': nodes.map((node) => node.toJson()).toList(), + if (network != null) 'protocol': protocol, + }), + ); + } +} diff --git a/packages/komodo_defi_types/lib/src/protocols/utxo/utxo_protocol.dart b/packages/komodo_defi_types/lib/src/protocols/utxo/utxo_protocol.dart index c2041842e..63a5b017e 100644 --- a/packages/komodo_defi_types/lib/src/protocols/utxo/utxo_protocol.dart +++ b/packages/komodo_defi_types/lib/src/protocols/utxo/utxo_protocol.dart @@ -1,6 +1,7 @@ import 'package:komodo_defi_rpc_methods/komodo_defi_rpc_methods.dart'; import 'package:komodo_defi_types/komodo_defi_type_utils.dart'; import 'package:komodo_defi_types/komodo_defi_types.dart'; +import 'package:komodo_defi_types/src/utils/protocol_type_utils.dart'; class UtxoProtocol extends ProtocolClass { UtxoProtocol._({ @@ -11,11 +12,12 @@ class UtxoProtocol extends ProtocolClass { factory UtxoProtocol.fromJson( JsonMap json, { + CoinSubClass? subClass, List supportedProtocols = const [], }) { _validateUtxoConfig(json); return UtxoProtocol._( - subClass: CoinSubClass.parse(json.value('type')), + subClass: subClass ?? resolveProtocolSubClassFromConfig(json), config: json, supportedProtocols: supportedProtocols, ); @@ -37,10 +39,7 @@ class UtxoProtocol extends ProtocolClass { } return UtxoActivationParams.fromJson(config) - .copyWith( - txHistory: true, - privKeyPolicy: privKeyPolicy, - ) + .copyWith(txHistory: true, privKeyPolicy: privKeyPolicy) .copyWithHd( minAddressesNumber: 1, scanPolicy: scanPolicy, @@ -74,10 +73,7 @@ class UtxoProtocol extends ProtocolClass { for (final field in requiredFields.entries) { if (!json.containsKey(field.key)) { - throw MissingProtocolFieldException( - field.value, - field.key, - ); + throw MissingProtocolFieldException(field.value, field.key); } } } diff --git a/packages/komodo_defi_types/lib/src/protocols/zhtlc/zhtlc_protocol.dart b/packages/komodo_defi_types/lib/src/protocols/zhtlc/zhtlc_protocol.dart index 65ceac9f7..460192d00 100644 --- a/packages/komodo_defi_types/lib/src/protocols/zhtlc/zhtlc_protocol.dart +++ b/packages/komodo_defi_types/lib/src/protocols/zhtlc/zhtlc_protocol.dart @@ -1,13 +1,14 @@ import 'package:komodo_defi_types/komodo_defi_type_utils.dart'; import 'package:komodo_defi_types/komodo_defi_types.dart'; +import 'package:komodo_defi_types/src/utils/protocol_type_utils.dart'; class ZhtlcProtocol extends ProtocolClass { ZhtlcProtocol._({required super.subClass, required super.config}); - factory ZhtlcProtocol.fromJson(JsonMap json) { + factory ZhtlcProtocol.fromJson(JsonMap json, {CoinSubClass? subClass}) { _validateZhtlcConfig(json); return ZhtlcProtocol._( - subClass: CoinSubClass.parse(json.value('type')), + subClass: subClass ?? resolveProtocolSubClassFromConfig(json), config: json, ); } @@ -26,7 +27,8 @@ class ZhtlcProtocol extends ProtocolClass { // We require at least one of electrum servers or light wallet d servers to be present. // Backward compatibility: some configs provided 'electrum' under config used by Electrum mode - final hasElectrum = json.containsKey('electrum') || json.containsKey('electrum_servers'); + final hasElectrum = + json.containsKey('electrum') || json.containsKey('electrum_servers'); final hasLightWalletD = json.containsKey('light_wallet_d_servers'); if (!hasElectrum && !hasLightWalletD) { diff --git a/packages/komodo_defi_types/lib/src/transactions/fee_info.dart b/packages/komodo_defi_types/lib/src/transactions/fee_info.dart index df8cc556d..967106b84 100644 --- a/packages/komodo_defi_types/lib/src/transactions/fee_info.dart +++ b/packages/komodo_defi_types/lib/src/transactions/fee_info.dart @@ -5,7 +5,7 @@ import 'package:komodo_defi_types/komodo_defi_type_utils.dart'; part 'fee_info.freezed.dart'; // We are doing manual fromJson/toJson, so no need for part 'fee_info.g.dart'; -/// A union representing eight possible fee types: +/// A union representing nine possible fee types: /// - UtxoFixed /// - UtxoPerKbyte /// - EthGas (legacy) @@ -13,6 +13,7 @@ part 'fee_info.freezed.dart'; /// - Qrc20Gas /// - CosmosGas /// - Tendermint +/// - Tron /// - Sia @Freezed() sealed class FeeInfo with _$FeeInfo { @@ -55,8 +56,9 @@ sealed class FeeInfo with _$FeeInfo { return FeeInfo.ethGasEip1559( coin: json['coin'] as String? ?? '', maxFeePerGas: Decimal.parse(json['max_fee_per_gas'].toString()), - maxPriorityFeePerGas: - Decimal.parse(json['max_priority_fee_per_gas'].toString()), + maxPriorityFeePerGas: Decimal.parse( + json['max_priority_fee_per_gas'].toString(), + ), gas: json['gas'] as int, totalGasFee: totalGasFee, ); @@ -76,6 +78,17 @@ sealed class FeeInfo with _$FeeInfo { amount: Decimal.parse(json['amount'].toString()), gasLimit: json['gas_limit'] as int, ); + case 'Tron': + return FeeInfo.tron( + coin: json['coin'] as String? ?? '', + bandwidthUsed: json['bandwidth_used'] as int? ?? 0, + energyUsed: json['energy_used'] as int? ?? 0, + bandwidthFee: Decimal.parse(json['bandwidth_fee'].toString()), + energyFee: Decimal.parse(json['energy_fee'].toString()), + totalFeeAmount: json['total_fee'] != null + ? Decimal.parse(json['total_fee'].toString()) + : null, + ); case 'CosmosGas': return FeeInfo.cosmosGas( coin: json['coin'] as String? ?? '', @@ -210,7 +223,7 @@ sealed class FeeInfo with _$FeeInfo { /// 7) Tendermint fee, with fixed `amount` and `gasLimit`. /// - /// Example JSON: + /// Example response JSON: /// ```json /// { /// "type": "Tendermint", @@ -219,7 +232,12 @@ sealed class FeeInfo with _$FeeInfo { /// "gas_limit": 100000 /// } /// ``` - /// Total fee is just the amount (not calculated from gas * price) + /// + /// Parsed Tendermint responses use the `Tendermint` shape above, but + /// outgoing withdraw requests are still encoded as `CosmosGas` for + /// compatibility with the current KDF API. + /// + /// Total fee is just the amount (not calculated from gas * price). const factory FeeInfo.tendermint({ required String coin, @@ -230,7 +248,17 @@ sealed class FeeInfo with _$FeeInfo { required int gasLimit, }) = FeeInfoTendermint; - /// 8) SIA fee, with fixed `amount` and `policy`. + /// 8) TRON fee, with explicit bandwidth and energy usage/fees. + const factory FeeInfo.tron({ + required String coin, + required int bandwidthUsed, + required int energyUsed, + required Decimal bandwidthFee, + required Decimal energyFee, + Decimal? totalFeeAmount, + }) = FeeInfoTron; + + /// 9) SIA fee, with fixed `amount` and `policy`. /// /// Example JSON: /// ```json @@ -254,97 +282,113 @@ sealed class FeeInfo with _$FeeInfo { /// A convenience getter returning the *total fee* in the coin's main units. Decimal get totalFee => switch (this) { - FeeInfoUtxoFixed(:final amount) => amount, - FeeInfoUtxoPerKbyte(:final amount) => amount, - FeeInfoEthGas(:final gasPrice, :final gas, :final totalGasFee) => - totalGasFee ?? (gasPrice * Decimal.fromInt(gas)), - FeeInfoEthGasEip1559( - :final maxFeePerGas, - :final gas, - :final totalGasFee - ) => - totalGasFee ?? (maxFeePerGas * Decimal.fromInt(gas)), - FeeInfoQrc20Gas(:final gasPrice, :final gasLimit, :final totalGasFee) => - totalGasFee ?? (gasPrice * Decimal.fromInt(gasLimit)), - FeeInfoCosmosGas(:final gasPrice, :final gasLimit) => - gasPrice * Decimal.fromInt(gasLimit), - FeeInfoTendermint(:final amount) => amount, - FeeInfoSia(:final amount) => amount, - }; + FeeInfoUtxoFixed(:final amount) => amount, + FeeInfoUtxoPerKbyte(:final amount) => amount, + FeeInfoEthGas(:final gasPrice, :final gas, :final totalGasFee) => + totalGasFee ?? (gasPrice * Decimal.fromInt(gas)), + FeeInfoEthGasEip1559(:final maxFeePerGas, :final gas, :final totalGasFee) => + totalGasFee ?? (maxFeePerGas * Decimal.fromInt(gas)), + FeeInfoQrc20Gas(:final gasPrice, :final gasLimit, :final totalGasFee) => + totalGasFee ?? (gasPrice * Decimal.fromInt(gasLimit)), + FeeInfoCosmosGas(:final gasPrice, :final gasLimit) => + gasPrice * Decimal.fromInt(gasLimit), + FeeInfoTendermint(:final amount) => amount, + FeeInfoTron(:final bandwidthFee, :final energyFee, :final totalFeeAmount) => + totalFeeAmount ?? (bandwidthFee + energyFee), + FeeInfoSia(:final amount) => amount, + }; /// Convert this [FeeInfo] to a JSON object matching the mmRPC 2.0 docs. JsonMap toJson() => switch (this) { - FeeInfoUtxoFixed(:final coin, :final amount) => { - 'type': 'UtxoFixed', - 'coin': coin, - 'amount': amount.toString(), - }, - FeeInfoUtxoPerKbyte(:final coin, :final amount) => { - 'type': 'UtxoPerKbyte', - 'coin': coin, - 'amount': amount.toString(), - }, - FeeInfoEthGas( - :final coin, - :final gasPrice, - :final gas, - :final totalGasFee - ) => - { - 'type': 'EthGas', - 'coin': coin, - 'gas_price': gasPrice.toString(), - 'gas': gas, - if (totalGasFee != null) 'total_fee': totalGasFee.toString(), - }, - FeeInfoEthGasEip1559( - :final coin, - :final maxFeePerGas, - :final maxPriorityFeePerGas, - :final gas, - :final totalGasFee - ) => - { - 'type': 'EthGasEip1559', - 'coin': coin, - 'max_fee_per_gas': maxFeePerGas.toString(), - 'max_priority_fee_per_gas': maxPriorityFeePerGas.toString(), - 'gas': gas, - if (totalGasFee != null) 'total_fee': totalGasFee.toString(), - }, - FeeInfoQrc20Gas( - :final coin, - :final gasPrice, - :final gasLimit, - :final totalGasFee - ) => - { - 'type': 'Qrc20Gas', - 'coin': coin, - 'gas_price': gasPrice.toDouble(), - 'gas_limit': gasLimit, - if (totalGasFee != null) 'total_gas_fee': totalGasFee.toString(), - }, - FeeInfoCosmosGas(:final coin, :final gasPrice, :final gasLimit) => { - 'type': 'CosmosGas', - 'coin': coin, - 'gas_price': gasPrice.toDouble(), - 'gas_limit': gasLimit, - }, - // TODO: update to Tendermint for KDF v2.5.0-beta - FeeInfoTendermint(:final coin, :final amount, :final gasLimit) => { - 'type': 'CosmosGas', - 'coin': coin, - 'gas_price': gasLimit > 0 - ? (amount / Decimal.fromInt(gasLimit)).toDouble() - : 0.0, - 'gas_limit': gasLimit, - }, - FeeInfoSia(:final coin, :final amount, :final policy) => { - 'type': 'Sia', - 'coin': coin, - 'total_amount': amount.toString(), - 'policy': policy, - }, - }; + FeeInfoUtxoFixed(:final coin, :final amount) => { + 'type': 'UtxoFixed', + 'coin': coin, + 'amount': amount.toString(), + }, + FeeInfoUtxoPerKbyte(:final coin, :final amount) => { + 'type': 'UtxoPerKbyte', + 'coin': coin, + 'amount': amount.toString(), + }, + FeeInfoEthGas( + :final coin, + :final gasPrice, + :final gas, + :final totalGasFee, + ) => + { + 'type': 'EthGas', + 'coin': coin, + 'gas_price': gasPrice.toString(), + 'gas': gas, + if (totalGasFee != null) 'total_fee': totalGasFee.toString(), + }, + FeeInfoEthGasEip1559( + :final coin, + :final maxFeePerGas, + :final maxPriorityFeePerGas, + :final gas, + :final totalGasFee, + ) => + { + 'type': 'EthGasEip1559', + 'coin': coin, + 'max_fee_per_gas': maxFeePerGas.toString(), + 'max_priority_fee_per_gas': maxPriorityFeePerGas.toString(), + 'gas': gas, + if (totalGasFee != null) 'total_fee': totalGasFee.toString(), + }, + FeeInfoQrc20Gas( + :final coin, + :final gasPrice, + :final gasLimit, + :final totalGasFee, + ) => + { + 'type': 'Qrc20Gas', + 'coin': coin, + 'gas_price': gasPrice.toDouble(), + 'gas_limit': gasLimit, + if (totalGasFee != null) 'total_gas_fee': totalGasFee.toString(), + }, + FeeInfoCosmosGas(:final coin, :final gasPrice, :final gasLimit) => { + 'type': 'CosmosGas', + 'coin': coin, + 'gas_price': gasPrice.toDouble(), + 'gas_limit': gasLimit, + }, + // Tendermint fee responses use the `Tendermint` shape, but withdraw + // requests must still be sent as `CosmosGas`. + FeeInfoTendermint(:final coin, :final amount, :final gasLimit) => { + 'type': 'CosmosGas', + 'coin': coin, + 'gas_price': gasLimit > 0 + ? (amount / Decimal.fromInt(gasLimit)).toDouble() + : 0.0, + 'gas_limit': gasLimit, + }, + FeeInfoTron( + :final coin, + :final bandwidthUsed, + :final energyUsed, + :final bandwidthFee, + :final energyFee, + :final totalFeeAmount, + ) => + { + 'type': 'Tron', + 'coin': coin, + 'bandwidth_used': bandwidthUsed, + 'energy_used': energyUsed, + 'bandwidth_fee': bandwidthFee.toString(), + 'energy_fee': energyFee.toString(), + if (totalFeeAmount != null) 'total_fee': totalFeeAmount.toString(), + }, + FeeInfoSia(:final coin, :final amount, :final policy) => { + 'type': 'Sia', + 'coin': coin, + 'total_amount': amount.toString(), + 'policy': policy, + }, + }; } diff --git a/packages/komodo_defi_types/lib/src/transactions/fee_info.freezed.dart b/packages/komodo_defi_types/lib/src/transactions/fee_info.freezed.dart index 3a099e866..579dde7e4 100644 --- a/packages/komodo_defi_types/lib/src/transactions/fee_info.freezed.dart +++ b/packages/komodo_defi_types/lib/src/transactions/fee_info.freezed.dart @@ -87,7 +87,7 @@ extension FeeInfoPatterns on FeeInfo { /// } /// ``` -@optionalTypeArgs TResult maybeMap({TResult Function( FeeInfoUtxoFixed value)? utxoFixed,TResult Function( FeeInfoUtxoPerKbyte value)? utxoPerKbyte,TResult Function( FeeInfoEthGas value)? ethGas,TResult Function( FeeInfoEthGasEip1559 value)? ethGasEip1559,TResult Function( FeeInfoQrc20Gas value)? qrc20Gas,TResult Function( FeeInfoCosmosGas value)? cosmosGas,TResult Function( FeeInfoTendermint value)? tendermint,TResult Function( FeeInfoSia value)? sia,required TResult orElse(),}){ +@optionalTypeArgs TResult maybeMap({TResult Function( FeeInfoUtxoFixed value)? utxoFixed,TResult Function( FeeInfoUtxoPerKbyte value)? utxoPerKbyte,TResult Function( FeeInfoEthGas value)? ethGas,TResult Function( FeeInfoEthGasEip1559 value)? ethGasEip1559,TResult Function( FeeInfoQrc20Gas value)? qrc20Gas,TResult Function( FeeInfoCosmosGas value)? cosmosGas,TResult Function( FeeInfoTendermint value)? tendermint,TResult Function( FeeInfoTron value)? tron,TResult Function( FeeInfoSia value)? sia,required TResult orElse(),}){ final _that = this; switch (_that) { case FeeInfoUtxoFixed() when utxoFixed != null: @@ -97,7 +97,8 @@ return ethGas(_that);case FeeInfoEthGasEip1559() when ethGasEip1559 != null: return ethGasEip1559(_that);case FeeInfoQrc20Gas() when qrc20Gas != null: return qrc20Gas(_that);case FeeInfoCosmosGas() when cosmosGas != null: return cosmosGas(_that);case FeeInfoTendermint() when tendermint != null: -return tendermint(_that);case FeeInfoSia() when sia != null: +return tendermint(_that);case FeeInfoTron() when tron != null: +return tron(_that);case FeeInfoSia() when sia != null: return sia(_that);case _: return orElse(); @@ -116,7 +117,7 @@ return sia(_that);case _: /// } /// ``` -@optionalTypeArgs TResult map({required TResult Function( FeeInfoUtxoFixed value) utxoFixed,required TResult Function( FeeInfoUtxoPerKbyte value) utxoPerKbyte,required TResult Function( FeeInfoEthGas value) ethGas,required TResult Function( FeeInfoEthGasEip1559 value) ethGasEip1559,required TResult Function( FeeInfoQrc20Gas value) qrc20Gas,required TResult Function( FeeInfoCosmosGas value) cosmosGas,required TResult Function( FeeInfoTendermint value) tendermint,required TResult Function( FeeInfoSia value) sia,}){ +@optionalTypeArgs TResult map({required TResult Function( FeeInfoUtxoFixed value) utxoFixed,required TResult Function( FeeInfoUtxoPerKbyte value) utxoPerKbyte,required TResult Function( FeeInfoEthGas value) ethGas,required TResult Function( FeeInfoEthGasEip1559 value) ethGasEip1559,required TResult Function( FeeInfoQrc20Gas value) qrc20Gas,required TResult Function( FeeInfoCosmosGas value) cosmosGas,required TResult Function( FeeInfoTendermint value) tendermint,required TResult Function( FeeInfoTron value) tron,required TResult Function( FeeInfoSia value) sia,}){ final _that = this; switch (_that) { case FeeInfoUtxoFixed(): @@ -126,7 +127,8 @@ return ethGas(_that);case FeeInfoEthGasEip1559(): return ethGasEip1559(_that);case FeeInfoQrc20Gas(): return qrc20Gas(_that);case FeeInfoCosmosGas(): return cosmosGas(_that);case FeeInfoTendermint(): -return tendermint(_that);case FeeInfoSia(): +return tendermint(_that);case FeeInfoTron(): +return tron(_that);case FeeInfoSia(): return sia(_that);} } /// A variant of `map` that fallback to returning `null`. @@ -141,7 +143,7 @@ return sia(_that);} /// } /// ``` -@optionalTypeArgs TResult? mapOrNull({TResult? Function( FeeInfoUtxoFixed value)? utxoFixed,TResult? Function( FeeInfoUtxoPerKbyte value)? utxoPerKbyte,TResult? Function( FeeInfoEthGas value)? ethGas,TResult? Function( FeeInfoEthGasEip1559 value)? ethGasEip1559,TResult? Function( FeeInfoQrc20Gas value)? qrc20Gas,TResult? Function( FeeInfoCosmosGas value)? cosmosGas,TResult? Function( FeeInfoTendermint value)? tendermint,TResult? Function( FeeInfoSia value)? sia,}){ +@optionalTypeArgs TResult? mapOrNull({TResult? Function( FeeInfoUtxoFixed value)? utxoFixed,TResult? Function( FeeInfoUtxoPerKbyte value)? utxoPerKbyte,TResult? Function( FeeInfoEthGas value)? ethGas,TResult? Function( FeeInfoEthGasEip1559 value)? ethGasEip1559,TResult? Function( FeeInfoQrc20Gas value)? qrc20Gas,TResult? Function( FeeInfoCosmosGas value)? cosmosGas,TResult? Function( FeeInfoTendermint value)? tendermint,TResult? Function( FeeInfoTron value)? tron,TResult? Function( FeeInfoSia value)? sia,}){ final _that = this; switch (_that) { case FeeInfoUtxoFixed() when utxoFixed != null: @@ -151,7 +153,8 @@ return ethGas(_that);case FeeInfoEthGasEip1559() when ethGasEip1559 != null: return ethGasEip1559(_that);case FeeInfoQrc20Gas() when qrc20Gas != null: return qrc20Gas(_that);case FeeInfoCosmosGas() when cosmosGas != null: return cosmosGas(_that);case FeeInfoTendermint() when tendermint != null: -return tendermint(_that);case FeeInfoSia() when sia != null: +return tendermint(_that);case FeeInfoTron() when tron != null: +return tron(_that);case FeeInfoSia() when sia != null: return sia(_that);case _: return null; @@ -169,7 +172,7 @@ return sia(_that);case _: /// } /// ``` -@optionalTypeArgs TResult maybeWhen({TResult Function( String coin, Decimal amount)? utxoFixed,TResult Function( String coin, Decimal amount)? utxoPerKbyte,TResult Function( String coin, Decimal gasPrice, int gas, Decimal? totalGasFee)? ethGas,TResult Function( String coin, Decimal maxFeePerGas, Decimal maxPriorityFeePerGas, int gas, Decimal? totalGasFee)? ethGasEip1559,TResult Function( String coin, Decimal gasPrice, int gasLimit, Decimal? totalGasFee)? qrc20Gas,TResult Function( String coin, Decimal gasPrice, int gasLimit)? cosmosGas,TResult Function( String coin, Decimal amount, int gasLimit)? tendermint,TResult Function( String coin, Decimal amount, String policy)? sia,required TResult orElse(),}) {final _that = this; +@optionalTypeArgs TResult maybeWhen({TResult Function( String coin, Decimal amount)? utxoFixed,TResult Function( String coin, Decimal amount)? utxoPerKbyte,TResult Function( String coin, Decimal gasPrice, int gas, Decimal? totalGasFee)? ethGas,TResult Function( String coin, Decimal maxFeePerGas, Decimal maxPriorityFeePerGas, int gas, Decimal? totalGasFee)? ethGasEip1559,TResult Function( String coin, Decimal gasPrice, int gasLimit, Decimal? totalGasFee)? qrc20Gas,TResult Function( String coin, Decimal gasPrice, int gasLimit)? cosmosGas,TResult Function( String coin, Decimal amount, int gasLimit)? tendermint,TResult Function( String coin, int bandwidthUsed, int energyUsed, Decimal bandwidthFee, Decimal energyFee, Decimal? totalFeeAmount)? tron,TResult Function( String coin, Decimal amount, String policy)? sia,required TResult orElse(),}) {final _that = this; switch (_that) { case FeeInfoUtxoFixed() when utxoFixed != null: return utxoFixed(_that.coin,_that.amount);case FeeInfoUtxoPerKbyte() when utxoPerKbyte != null: @@ -178,7 +181,8 @@ return ethGas(_that.coin,_that.gasPrice,_that.gas,_that.totalGasFee);case FeeInf return ethGasEip1559(_that.coin,_that.maxFeePerGas,_that.maxPriorityFeePerGas,_that.gas,_that.totalGasFee);case FeeInfoQrc20Gas() when qrc20Gas != null: return qrc20Gas(_that.coin,_that.gasPrice,_that.gasLimit,_that.totalGasFee);case FeeInfoCosmosGas() when cosmosGas != null: return cosmosGas(_that.coin,_that.gasPrice,_that.gasLimit);case FeeInfoTendermint() when tendermint != null: -return tendermint(_that.coin,_that.amount,_that.gasLimit);case FeeInfoSia() when sia != null: +return tendermint(_that.coin,_that.amount,_that.gasLimit);case FeeInfoTron() when tron != null: +return tron(_that.coin,_that.bandwidthUsed,_that.energyUsed,_that.bandwidthFee,_that.energyFee,_that.totalFeeAmount);case FeeInfoSia() when sia != null: return sia(_that.coin,_that.amount,_that.policy);case _: return orElse(); @@ -197,7 +201,7 @@ return sia(_that.coin,_that.amount,_that.policy);case _: /// } /// ``` -@optionalTypeArgs TResult when({required TResult Function( String coin, Decimal amount) utxoFixed,required TResult Function( String coin, Decimal amount) utxoPerKbyte,required TResult Function( String coin, Decimal gasPrice, int gas, Decimal? totalGasFee) ethGas,required TResult Function( String coin, Decimal maxFeePerGas, Decimal maxPriorityFeePerGas, int gas, Decimal? totalGasFee) ethGasEip1559,required TResult Function( String coin, Decimal gasPrice, int gasLimit, Decimal? totalGasFee) qrc20Gas,required TResult Function( String coin, Decimal gasPrice, int gasLimit) cosmosGas,required TResult Function( String coin, Decimal amount, int gasLimit) tendermint,required TResult Function( String coin, Decimal amount, String policy) sia,}) {final _that = this; +@optionalTypeArgs TResult when({required TResult Function( String coin, Decimal amount) utxoFixed,required TResult Function( String coin, Decimal amount) utxoPerKbyte,required TResult Function( String coin, Decimal gasPrice, int gas, Decimal? totalGasFee) ethGas,required TResult Function( String coin, Decimal maxFeePerGas, Decimal maxPriorityFeePerGas, int gas, Decimal? totalGasFee) ethGasEip1559,required TResult Function( String coin, Decimal gasPrice, int gasLimit, Decimal? totalGasFee) qrc20Gas,required TResult Function( String coin, Decimal gasPrice, int gasLimit) cosmosGas,required TResult Function( String coin, Decimal amount, int gasLimit) tendermint,required TResult Function( String coin, int bandwidthUsed, int energyUsed, Decimal bandwidthFee, Decimal energyFee, Decimal? totalFeeAmount) tron,required TResult Function( String coin, Decimal amount, String policy) sia,}) {final _that = this; switch (_that) { case FeeInfoUtxoFixed(): return utxoFixed(_that.coin,_that.amount);case FeeInfoUtxoPerKbyte(): @@ -206,7 +210,8 @@ return ethGas(_that.coin,_that.gasPrice,_that.gas,_that.totalGasFee);case FeeInf return ethGasEip1559(_that.coin,_that.maxFeePerGas,_that.maxPriorityFeePerGas,_that.gas,_that.totalGasFee);case FeeInfoQrc20Gas(): return qrc20Gas(_that.coin,_that.gasPrice,_that.gasLimit,_that.totalGasFee);case FeeInfoCosmosGas(): return cosmosGas(_that.coin,_that.gasPrice,_that.gasLimit);case FeeInfoTendermint(): -return tendermint(_that.coin,_that.amount,_that.gasLimit);case FeeInfoSia(): +return tendermint(_that.coin,_that.amount,_that.gasLimit);case FeeInfoTron(): +return tron(_that.coin,_that.bandwidthUsed,_that.energyUsed,_that.bandwidthFee,_that.energyFee,_that.totalFeeAmount);case FeeInfoSia(): return sia(_that.coin,_that.amount,_that.policy);} } /// A variant of `when` that fallback to returning `null` @@ -221,7 +226,7 @@ return sia(_that.coin,_that.amount,_that.policy);} /// } /// ``` -@optionalTypeArgs TResult? whenOrNull({TResult? Function( String coin, Decimal amount)? utxoFixed,TResult? Function( String coin, Decimal amount)? utxoPerKbyte,TResult? Function( String coin, Decimal gasPrice, int gas, Decimal? totalGasFee)? ethGas,TResult? Function( String coin, Decimal maxFeePerGas, Decimal maxPriorityFeePerGas, int gas, Decimal? totalGasFee)? ethGasEip1559,TResult? Function( String coin, Decimal gasPrice, int gasLimit, Decimal? totalGasFee)? qrc20Gas,TResult? Function( String coin, Decimal gasPrice, int gasLimit)? cosmosGas,TResult? Function( String coin, Decimal amount, int gasLimit)? tendermint,TResult? Function( String coin, Decimal amount, String policy)? sia,}) {final _that = this; +@optionalTypeArgs TResult? whenOrNull({TResult? Function( String coin, Decimal amount)? utxoFixed,TResult? Function( String coin, Decimal amount)? utxoPerKbyte,TResult? Function( String coin, Decimal gasPrice, int gas, Decimal? totalGasFee)? ethGas,TResult? Function( String coin, Decimal maxFeePerGas, Decimal maxPriorityFeePerGas, int gas, Decimal? totalGasFee)? ethGasEip1559,TResult? Function( String coin, Decimal gasPrice, int gasLimit, Decimal? totalGasFee)? qrc20Gas,TResult? Function( String coin, Decimal gasPrice, int gasLimit)? cosmosGas,TResult? Function( String coin, Decimal amount, int gasLimit)? tendermint,TResult? Function( String coin, int bandwidthUsed, int energyUsed, Decimal bandwidthFee, Decimal energyFee, Decimal? totalFeeAmount)? tron,TResult? Function( String coin, Decimal amount, String policy)? sia,}) {final _that = this; switch (_that) { case FeeInfoUtxoFixed() when utxoFixed != null: return utxoFixed(_that.coin,_that.amount);case FeeInfoUtxoPerKbyte() when utxoPerKbyte != null: @@ -230,7 +235,8 @@ return ethGas(_that.coin,_that.gasPrice,_that.gas,_that.totalGasFee);case FeeInf return ethGasEip1559(_that.coin,_that.maxFeePerGas,_that.maxPriorityFeePerGas,_that.gas,_that.totalGasFee);case FeeInfoQrc20Gas() when qrc20Gas != null: return qrc20Gas(_that.coin,_that.gasPrice,_that.gasLimit,_that.totalGasFee);case FeeInfoCosmosGas() when cosmosGas != null: return cosmosGas(_that.coin,_that.gasPrice,_that.gasLimit);case FeeInfoTendermint() when tendermint != null: -return tendermint(_that.coin,_that.amount,_that.gasLimit);case FeeInfoSia() when sia != null: +return tendermint(_that.coin,_that.amount,_that.gasLimit);case FeeInfoTron() when tron != null: +return tron(_that.coin,_that.bandwidthUsed,_that.energyUsed,_that.bandwidthFee,_that.energyFee,_that.totalFeeAmount);case FeeInfoSia() when sia != null: return sia(_that.coin,_that.amount,_that.policy);case _: return null; @@ -755,6 +761,82 @@ as int, /// @nodoc +class FeeInfoTron extends FeeInfo { + const FeeInfoTron({required this.coin, required this.bandwidthUsed, required this.energyUsed, required this.bandwidthFee, required this.energyFee, this.totalFeeAmount}): super._(); + + +@override final String coin; + final int bandwidthUsed; + final int energyUsed; + final Decimal bandwidthFee; + final Decimal energyFee; + final Decimal? totalFeeAmount; + +/// Create a copy of FeeInfo +/// with the given fields replaced by the non-null parameter values. +@override @JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +$FeeInfoTronCopyWith get copyWith => _$FeeInfoTronCopyWithImpl(this, _$identity); + + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is FeeInfoTron&&(identical(other.coin, coin) || other.coin == coin)&&(identical(other.bandwidthUsed, bandwidthUsed) || other.bandwidthUsed == bandwidthUsed)&&(identical(other.energyUsed, energyUsed) || other.energyUsed == energyUsed)&&(identical(other.bandwidthFee, bandwidthFee) || other.bandwidthFee == bandwidthFee)&&(identical(other.energyFee, energyFee) || other.energyFee == energyFee)&&(identical(other.totalFeeAmount, totalFeeAmount) || other.totalFeeAmount == totalFeeAmount)); +} + + +@override +int get hashCode => Object.hash(runtimeType,coin,bandwidthUsed,energyUsed,bandwidthFee,energyFee,totalFeeAmount); + +@override +String toString() { + return 'FeeInfo.tron(coin: $coin, bandwidthUsed: $bandwidthUsed, energyUsed: $energyUsed, bandwidthFee: $bandwidthFee, energyFee: $energyFee, totalFeeAmount: $totalFeeAmount)'; +} + + +} + +/// @nodoc +abstract mixin class $FeeInfoTronCopyWith<$Res> implements $FeeInfoCopyWith<$Res> { + factory $FeeInfoTronCopyWith(FeeInfoTron value, $Res Function(FeeInfoTron) _then) = _$FeeInfoTronCopyWithImpl; +@override @useResult +$Res call({ + String coin, int bandwidthUsed, int energyUsed, Decimal bandwidthFee, Decimal energyFee, Decimal? totalFeeAmount +}); + + + + +} +/// @nodoc +class _$FeeInfoTronCopyWithImpl<$Res> + implements $FeeInfoTronCopyWith<$Res> { + _$FeeInfoTronCopyWithImpl(this._self, this._then); + + final FeeInfoTron _self; + final $Res Function(FeeInfoTron) _then; + +/// Create a copy of FeeInfo +/// with the given fields replaced by the non-null parameter values. +@override @pragma('vm:prefer-inline') $Res call({Object? coin = null,Object? bandwidthUsed = null,Object? energyUsed = null,Object? bandwidthFee = null,Object? energyFee = null,Object? totalFeeAmount = freezed,}) { + return _then(FeeInfoTron( +coin: null == coin ? _self.coin : coin // ignore: cast_nullable_to_non_nullable +as String,bandwidthUsed: null == bandwidthUsed ? _self.bandwidthUsed : bandwidthUsed // ignore: cast_nullable_to_non_nullable +as int,energyUsed: null == energyUsed ? _self.energyUsed : energyUsed // ignore: cast_nullable_to_non_nullable +as int,bandwidthFee: null == bandwidthFee ? _self.bandwidthFee : bandwidthFee // ignore: cast_nullable_to_non_nullable +as Decimal,energyFee: null == energyFee ? _self.energyFee : energyFee // ignore: cast_nullable_to_non_nullable +as Decimal,totalFeeAmount: freezed == totalFeeAmount ? _self.totalFeeAmount : totalFeeAmount // ignore: cast_nullable_to_non_nullable +as Decimal?, + )); +} + + +} + +/// @nodoc + + class FeeInfoSia extends FeeInfo { const FeeInfoSia({required this.coin, required this.amount, required this.policy}): super._(); diff --git a/packages/komodo_defi_types/lib/src/types.dart b/packages/komodo_defi_types/lib/src/types.dart index a606ee917..f1376720c 100644 --- a/packages/komodo_defi_types/lib/src/types.dart +++ b/packages/komodo_defi_types/lib/src/types.dart @@ -40,6 +40,8 @@ export 'protocols/qtum/qtum_protocol.dart'; export 'protocols/sia/sia_protocol.dart'; export 'protocols/slp/slp_protocol.dart'; export 'protocols/tendermint/tendermint_protocol.dart'; +export 'protocols/trc20/trc20_protocol.dart'; +export 'protocols/trx/trx_protocol.dart'; export 'protocols/utxo/utxo_protocol.dart'; export 'protocols/zhtlc/zhtlc_protocol.dart'; export 'public_key/address_operations.dart'; diff --git a/packages/komodo_defi_types/lib/src/utils/protocol_type_utils.dart b/packages/komodo_defi_types/lib/src/utils/protocol_type_utils.dart new file mode 100644 index 000000000..9de7fa8d3 --- /dev/null +++ b/packages/komodo_defi_types/lib/src/utils/protocol_type_utils.dart @@ -0,0 +1,9 @@ +import 'package:komodo_defi_types/komodo_defi_type_utils.dart'; +import 'package:komodo_defi_types/komodo_defi_types.dart'; + +/// Resolves the canonical protocol type from a coin config. +/// +/// Generated coin configs encode the concrete asset subtype in the top-level +/// `type` field (`TRX`, `TRC-20`, `AVX-20`, `BEP-20`, etc.). +CoinSubClass resolveProtocolSubClassFromConfig(JsonMap json) => + CoinSubClass.parse(json.value('type')); diff --git a/packages/komodo_defi_types/lib/src/withdrawal/withdrawal_types.dart b/packages/komodo_defi_types/lib/src/withdrawal/withdrawal_types.dart index c80299ee3..c646d5ae2 100644 --- a/packages/komodo_defi_types/lib/src/withdrawal/withdrawal_types.dart +++ b/packages/komodo_defi_types/lib/src/withdrawal/withdrawal_types.dart @@ -166,6 +166,7 @@ class WithdrawParameters extends Equatable { this.memo, this.ibcTransfer, this.ibcSourceChannel, + this.expirationSeconds, this.isMax, }) : assert( amount != null || (isMax ?? false), @@ -181,6 +182,7 @@ class WithdrawParameters extends Equatable { final String? memo; final bool? ibcTransfer; final int? ibcSourceChannel; + final int? expirationSeconds; final bool? isMax; JsonMap toJson() => { @@ -193,6 +195,7 @@ class WithdrawParameters extends Equatable { if (memo != null) 'memo': memo, if (ibcTransfer != null) 'ibc_transfer': ibcTransfer, if (ibcSourceChannel != null) 'ibc_source_channel': ibcSourceChannel, + if (expirationSeconds != null) 'expiration_seconds': expirationSeconds, }; @override @@ -206,6 +209,7 @@ class WithdrawParameters extends Equatable { memo, ibcTransfer, ibcSourceChannel, + expirationSeconds, isMax, ]; } diff --git a/packages/komodo_defi_types/test/fee_info_test.dart b/packages/komodo_defi_types/test/fee_info_test.dart index 5f488fc65..feceb80f7 100644 --- a/packages/komodo_defi_types/test/fee_info_test.dart +++ b/packages/komodo_defi_types/test/fee_info_test.dart @@ -12,7 +12,7 @@ void main() { ); final json = feeInfo.toJson(); - + expect(json['type'], equals('EthGas')); expect(json['coin'], equals('ETH')); expect(json['gas_price'], equals('0.000000003')); @@ -28,7 +28,7 @@ void main() { }; final feeInfo = FeeInfo.fromJson(json); - + expect(feeInfo, isA()); final ethGas = feeInfo as FeeInfoEthGas; expect(ethGas.coin, equals('ETH')); @@ -45,7 +45,7 @@ void main() { }; final feeInfo = FeeInfo.fromJson(json); - + expect(feeInfo, isA()); final ethGas = feeInfo as FeeInfoEthGas; expect(ethGas.coin, equals('ETH')); @@ -53,4 +53,106 @@ void main() { expect(ethGas.gas, equals(21000)); }); }); -} \ No newline at end of file + + group('FeeInfo Tron serialization', () { + test('should serialize Tron fee details with correct type', () { + final feeInfo = FeeInfo.tron( + coin: 'TRX', + bandwidthUsed: 345, + energyUsed: 29650, + bandwidthFee: Decimal.parse('0.345'), + energyFee: Decimal.parse('12.453'), + totalFeeAmount: Decimal.parse('12.798'), + ); + + final json = feeInfo.toJson(); + + expect(json['type'], equals('Tron')); + expect(json['coin'], equals('TRX')); + expect(json['bandwidth_used'], equals(345)); + expect(json['energy_used'], equals(29650)); + expect(json['bandwidth_fee'], equals('0.345')); + expect(json['energy_fee'], equals('12.453')); + expect(json['total_fee'], equals('12.798')); + }); + + test('should deserialize Tron fee details from JSON', () { + final json = { + 'type': 'Tron', + 'coin': 'TRX', + 'bandwidth_used': 267, + 'energy_used': 0, + 'bandwidth_fee': '0.267', + 'energy_fee': '0', + 'total_fee': '0.267', + }; + + final feeInfo = FeeInfo.fromJson(json); + + expect(feeInfo, isA()); + final tronFee = feeInfo as FeeInfoTron; + expect(tronFee.coin, equals('TRX')); + expect(tronFee.bandwidthUsed, equals(267)); + expect(tronFee.energyUsed, equals(0)); + expect(tronFee.bandwidthFee, equals(Decimal.parse('0.267'))); + expect(tronFee.energyFee, equals(Decimal.zero)); + expect(tronFee.totalFeeAmount, equals(Decimal.parse('0.267'))); + expect(tronFee.totalFee, equals(Decimal.parse('0.267'))); + }); + }); + + group('FeeInfo Tendermint compatibility', () { + test('should serialize Tendermint fees as CosmosGas for requests', () { + final feeInfo = FeeInfo.tendermint( + coin: 'ATOM', + amount: Decimal.parse('0.038553'), + gasLimit: 100000, + ); + + final json = feeInfo.toJson(); + + expect(json['type'], equals('CosmosGas')); + expect(json['coin'], equals('ATOM')); + expect(json['gas_limit'], equals(100000)); + expect( + (json['gas_price'] as num).toDouble(), + closeTo(0.00000038553, 1e-18), + ); + }); + + test( + 'should serialize Tendermint fees with zero gas limit as zero gas price', + () { + final feeInfo = FeeInfo.tendermint( + coin: 'ATOM', + amount: Decimal.parse('0.038553'), + gasLimit: 0, + ); + + final json = feeInfo.toJson(); + + expect(json['type'], equals('CosmosGas')); + expect(json['gas_limit'], equals(0)); + expect(json['gas_price'], equals(0.0)); + }, + ); + + test('should deserialize Tendermint fee details from response JSON', () { + final json = { + 'type': 'Tendermint', + 'coin': 'ATOM', + 'amount': '0.038553', + 'gas_limit': 100000, + }; + + final feeInfo = FeeInfo.fromJson(json); + + expect(feeInfo, isA()); + final tendermintFee = feeInfo as FeeInfoTendermint; + expect(tendermintFee.coin, equals('ATOM')); + expect(tendermintFee.amount, equals(Decimal.parse('0.038553'))); + expect(tendermintFee.gasLimit, equals(100000)); + expect(tendermintFee.totalFee, equals(Decimal.parse('0.038553'))); + }); + }); +} diff --git a/packages/komodo_defi_types/test/tron_protocol_test.dart b/packages/komodo_defi_types/test/tron_protocol_test.dart new file mode 100644 index 000000000..43efa55a6 --- /dev/null +++ b/packages/komodo_defi_types/test/tron_protocol_test.dart @@ -0,0 +1,153 @@ +import 'package:komodo_defi_types/komodo_defi_types.dart'; +import 'package:test/test.dart'; + +Map _trxConfig() => { + 'coin': 'TRX', + 'type': 'TRX', + 'name': 'TRON', + 'fname': 'TRON', + 'wallet_only': true, + 'mm2': 1, + 'decimals': 6, + 'required_confirmations': 1, + 'derivation_path': "m/44'/195'", + 'protocol': { + 'type': 'TRX', + 'protocol_data': {'network': 'Mainnet'}, + }, + 'nodes': >[], +}; + +Map _trc20Config() => { + 'coin': 'USDT-TRC20', + 'type': 'TRC-20', + 'name': 'Tether', + 'fname': 'Tether', + 'wallet_only': true, + 'mm2': 1, + 'decimals': 6, + 'derivation_path': "m/44'/195'", + 'protocol': { + 'type': 'TRC20', + 'protocol_data': { + 'platform': 'TRX', + 'contract_address': 'TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t', + }, + }, + 'contract_address': 'TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t', + 'parent_coin': 'TRX', + 'nodes': >[], +}; + +Map _avaxConfig() => { + 'coin': 'AVAX', + 'type': 'AVX-20', + 'name': 'Avalanche', + 'fname': 'Avalanche', + 'wallet_only': false, + 'mm2': 1, + 'chain_id': 43114, + 'required_confirmations': 3, + 'protocol': { + 'type': 'ETH', + 'protocol_data': {'chain_id': 43114}, + }, + 'swap_contract_address': '0x9130b257D37A52E52F21054c4DA3450c72f595CE', + 'fallback_swap_contract': '0x9130b257D37A52E52F21054c4DA3450c72f595CE', + 'nodes': >[], +}; + +Map _bnbConfig() => { + 'coin': 'BNB', + 'type': 'BEP-20', + 'name': 'BNB Smart Chain', + 'fname': 'BNB Smart Chain', + 'wallet_only': false, + 'mm2': 1, + 'chain_id': 56, + 'required_confirmations': 3, + 'protocol': { + 'type': 'ETH', + 'protocol_data': {'chain_id': 56}, + }, + 'swap_contract_address': '0xeDc5b89Fe1f0382F9E4316069971D90a0951DB31', + 'fallback_swap_contract': '0xeDc5b89Fe1f0382F9E4316069971D90a0951DB31', + 'nodes': >[], +}; + +Map _arrrBep20Config() => { + 'coin': 'ARRR-BEP20', + 'type': 'BEP-20', + 'name': 'Pirate', + 'fname': 'Pirate', + 'wallet_only': true, + 'mm2': 1, + 'chain_id': 56, + 'decimals': 8, + 'required_confirmations': 3, + 'protocol': { + 'type': 'ERC20', + 'protocol_data': { + 'platform': 'BNB', + 'contract_address': '0xCDAF240C90F989847c56aC9Dee754F76F41c5833', + }, + }, + 'contract_address': '0xCDAF240C90F989847c56aC9Dee754F76F41c5833', + 'parent_coin': 'BNB', + 'swap_contract_address': '0xeDc5b89Fe1f0382F9E4316069971D90a0951DB31', + 'fallback_swap_contract': '0xeDc5b89Fe1f0382F9E4316069971D90a0951DB31', + 'nodes': >[], +}; + +void main() { + group('TRON protocol parsing', () { + test('AssetId.parse uses top-level type for TRX platform assets', () { + final assetId = AssetId.parse(_trxConfig(), knownIds: const {}); + + expect(assetId.id, 'TRX'); + expect(assetId.subClass, CoinSubClass.trx); + expect(assetId.displayName, 'TRON'); + }); + + test('ProtocolClass.fromJson parses TRX without EVM swap contracts', () { + final protocol = ProtocolClass.fromJson(_trxConfig()); + + expect(protocol, isA()); + expect(protocol.subClass, CoinSubClass.trx); + expect((protocol as TrxProtocol).nodes, isEmpty); + expect(protocol.network, 'Mainnet'); + expect(protocol.supportsTxHistoryStreaming(isChildAsset: false), isTrue); + }); + + test('Asset.fromJson links TRC20 child asset to TRX parent', () { + final parent = Asset.fromJson(_trxConfig(), knownIds: const {}); + final child = Asset.fromJson(_trc20Config(), knownIds: {parent.id}); + + expect(child.id.subClass, CoinSubClass.trc20); + expect(child.protocol, isA()); + expect(child.id.parentId, parent.id); + expect(parent.id.subClass.canBeParentOf(child.id.subClass), isTrue); + expect(child.supportsBalanceStreaming, isTrue); + expect(child.supportsTxHistoryStreaming, isTrue); + }); + + test('non-TRON platform assets keep top-level subtype precedence', () { + final assetId = AssetId.parse(_avaxConfig(), knownIds: const {}); + final protocol = ProtocolClass.fromJson(_avaxConfig()); + + expect(assetId.subClass, CoinSubClass.avx20); + expect(protocol, isA()); + expect(protocol.subClass, CoinSubClass.avx20); + }); + + test('non-TRON token assets keep top-level subtype precedence', () { + final parent = Asset.fromJson(_bnbConfig(), knownIds: const {}); + final child = Asset.fromJson(_arrrBep20Config(), knownIds: {parent.id}); + + expect(child.id.subClass, CoinSubClass.bep20); + expect(child.protocol, isA()); + expect(child.protocol.subClass, CoinSubClass.bep20); + expect(child.id.parentId, parent.id); + }); + }); +} diff --git a/packages/komodo_ui/lib/src/core/displays/fee_info_display.dart b/packages/komodo_ui/lib/src/core/displays/fee_info_display.dart index 73d450ec7..77d9bf915 100644 --- a/packages/komodo_ui/lib/src/core/displays/fee_info_display.dart +++ b/packages/komodo_ui/lib/src/core/displays/fee_info_display.dart @@ -5,8 +5,8 @@ import 'package:komodo_ui/src/utils/formatters/fee_info_formatters.dart'; /// A widget for displaying FeeInfo details in a consistent format. /// -/// This widget handles all fee types (ETH gas, QRC20 gas, Cosmos gas, UTXO, -/// Tendermint, and SIA) and displays their relevant details in a clear, +/// This widget handles all fee types (ETH gas, QRC20 gas, Cosmos gas, UTXO, +/// Tendermint, and SIA) and displays their relevant details in a clear, /// formatted way. /// /// **Note:** Fee estimation features are currently disabled as the API endpoints @@ -154,12 +154,44 @@ class FeeInfoDisplay extends StatelessWidget { ), ], - final FeeInfoSia fee => [ - Text('Policy:', style: Theme.of(context).textTheme.bodyMedium), + final FeeInfoTron fee => [ + Text( + 'Bandwidth Used:', + style: Theme.of(context).textTheme.bodyMedium, + ), + Text( + '${fee.bandwidthUsed}', + style: Theme.of(context).textTheme.labelLarge, + ), + Text( + 'Energy Used:', + style: Theme.of(context).textTheme.bodyMedium, + ), Text( - fee.policy, + '${fee.energyUsed}', style: Theme.of(context).textTheme.labelLarge, ), + Text( + 'Bandwidth Fee:', + style: Theme.of(context).textTheme.bodyMedium, + ), + Text( + '${fee.bandwidthFee} ${fee.coin}', + style: Theme.of(context).textTheme.labelLarge, + ), + Text( + 'Energy Fee:', + style: Theme.of(context).textTheme.bodyMedium, + ), + Text( + '${fee.energyFee} ${fee.coin}', + style: Theme.of(context).textTheme.labelLarge, + ), + ], + + final FeeInfoSia fee => [ + Text('Policy:', style: Theme.of(context).textTheme.bodyMedium), + Text(fee.policy, style: Theme.of(context).textTheme.labelLarge), Text('Amount:', style: Theme.of(context).textTheme.bodyMedium), Text( fee.formatTotal(precision: 8), From ce60e667c51dbc3baa3baeb03af2f7d96c301938 Mon Sep 17 00:00:00 2001 From: Nitride <77973576+CharlVS@users.noreply.github.com> Date: Wed, 18 Mar 2026 18:14:23 +0100 Subject: [PATCH 07/40] fix(startup): handle 6133 seed fallback and invalid configs (#318) --- .../src/coins_config/config_transform.dart | 49 +++++++++++ .../lib/src/seed_node_updater.dart | 4 + .../test/config_transform_test.dart | 70 ++++++++++++++++ ...local_asset_coin_config_provider_test.dart | 38 +++++++++ .../test/seed_node_updater_mock_test.dart | 43 +++++++++- .../test/seed_node_updater_test.dart | 20 ++--- .../lib/src/config/seed_node_validator.dart | 27 ++++--- .../lib/src/services/seed_node_service.dart | 81 ++++++++++++++----- .../test/seed_node_service_test.dart | 54 +++++++++++++ .../test/seed_node_validator_test.dart | 25 ++++++ .../komodo_defi_types/lib/src/constants.dart | 2 +- .../test/seed_node_test.dart | 32 ++++---- 12 files changed, 386 insertions(+), 59 deletions(-) create mode 100644 packages/komodo_defi_framework/test/seed_node_service_test.dart create mode 100644 packages/komodo_defi_framework/test/seed_node_validator_test.dart diff --git a/packages/komodo_coin_updates/lib/src/coins_config/config_transform.dart b/packages/komodo_coin_updates/lib/src/coins_config/config_transform.dart index c23800bd5..03c18a172 100644 --- a/packages/komodo_coin_updates/lib/src/coins_config/config_transform.dart +++ b/packages/komodo_coin_updates/lib/src/coins_config/config_transform.dart @@ -1,5 +1,6 @@ import 'package:flutter/foundation.dart' show kIsWeb, kIsWasm; import 'package:komodo_defi_types/komodo_defi_type_utils.dart'; +import 'package:komodo_defi_types/komodo_defi_types.dart'; import 'package:logging/logging.dart'; /// Defines a transform that can be applied to a single coin configuration. @@ -133,6 +134,19 @@ class CoinFilter { /// Protocol types to exclude from the runtime list. static const _filteredProtocolTypes = {}; + /// Protocol subclasses that the runtime parser explicitly does not support. + static const _unsupportedProtocolSubClasses = { + CoinSubClass.smartBch, + CoinSubClass.unknown, + }; + + /// EVM configs must carry activation data locally because they do not + /// inherit the required RPC/swap fields from a parent during parsing. + static const _requiredEvmStringFields = { + 'swap_contract_address', + 'fallback_swap_contract', + }; + /// Returns true if the given coin should be filtered out. bool shouldFilter(JsonMap config) { // Honor an explicit exclusion marker @@ -143,13 +157,48 @@ class CoinFilter { final coin = config.value('coin'); final protocolSubClass = config.valueOrNull('type'); final protocolClass = config.valueOrNull('protocol', 'type'); + final resolvedSubClass = _resolveProtocolSubClass(config); final isTestnet = config.valueOrNull('is_testnet') ?? false; return _filteredCoins.containsKey(coin) || _filteredProtocolTypes.containsKey(protocolClass) || _filteredProtocolSubTypes.containsKey(protocolSubClass) || + _unsupportedProtocolSubClasses.contains(resolvedSubClass) || + _isInvalidEvmConfig(config) || (_isTestCoinsOnly && !isTestnet); } + + bool _isInvalidEvmConfig(JsonMap config) { + final subClass = _resolveProtocolSubClass(config); + if (subClass == null || !evmCoinSubClasses.contains(subClass)) { + return false; + } + + final nodes = config.valueOrNull('nodes'); + if (nodes == null || nodes.isEmpty) { + return true; + } + + for (final field in _requiredEvmStringFields) { + final value = config.valueOrNull(field)?.trim(); + if (value == null || value.isEmpty) { + return true; + } + } + + return false; + } + + CoinSubClass? _resolveProtocolSubClass(JsonMap config) { + final rawType = + config.valueOrNull('type') ?? + config.valueOrNull('protocol', 'type'); + if (rawType == null) { + return null; + } + + return CoinSubClass.tryParse(rawType); + } } /// Filters out non-wss electrum/server URLs from the given coin config for diff --git a/packages/komodo_coin_updates/lib/src/seed_node_updater.dart b/packages/komodo_coin_updates/lib/src/seed_node_updater.dart index ca36d6408..6fc7dd3c9 100644 --- a/packages/komodo_coin_updates/lib/src/seed_node_updater.dart +++ b/packages/komodo_coin_updates/lib/src/seed_node_updater.dart @@ -70,6 +70,10 @@ class SeedNodeUpdater { seedNodes = seedNodes.where((e) => e.wss).toList(); } + if (seedNodes.isEmpty) { + throw Exception('No seed nodes found for netid $kDefaultNetId'); + } + return (seedNodes: seedNodes, netId: kDefaultNetId); } catch (e) { debugPrint('Error fetching seed nodes: $e'); diff --git a/packages/komodo_coin_updates/test/config_transform_test.dart b/packages/komodo_coin_updates/test/config_transform_test.dart index b5ac347de..89eec192d 100644 --- a/packages/komodo_coin_updates/test/config_transform_test.dart +++ b/packages/komodo_coin_updates/test/config_transform_test.dart @@ -97,4 +97,74 @@ void main() { expect(out['parent_coin'], 'XYZ'); }); }); + + group('CoinFilter', () { + test('filters invalid EVM configs with missing activation fields', () { + const filter = CoinFilter(); + final config = JsonMap.of({ + 'coin': 'BROKENETH', + 'type': 'ETH', + 'protocol': { + 'type': 'ETH', + 'protocol_data': {'chain_id': 1}, + }, + 'nodes': [ + {'url': 'https://eth.example.com'}, + ], + 'swap_contract_address': '0x61EEC68Cf64d1b31e41EA713356De2563fB6D3F1', + }); + + expect(filter.shouldFilter(config), isTrue); + }); + + test('filters invalid EVM configs with empty node lists', () { + const filter = CoinFilter(); + final config = JsonMap.of({ + 'coin': 'BROKENMATIC', + 'type': 'Matic', + 'protocol': { + 'type': 'ETH', + 'protocol_data': {'chain_id': 137}, + }, + 'nodes': [], + 'swap_contract_address': '0x9130b257D37A52E52F21054c4DA3450c72f595CE', + 'fallback_swap_contract': '0x9130b257D37A52E52F21054c4DA3450c72f595CE', + }); + + expect(filter.shouldFilter(config), isTrue); + }); + + test('filters unsupported protocol subclasses before parsing', () { + const filter = CoinFilter(); + final config = JsonMap.of({ + 'coin': 'SBCH', + 'type': 'SmartBCH', + 'protocol': { + 'type': 'ETH', + 'protocol_data': {'chain_id': 10000}, + }, + }); + + expect(filter.shouldFilter(config), isTrue); + }); + + test('keeps complete EVM configs', () { + const filter = CoinFilter(); + final config = JsonMap.of({ + 'coin': 'ETH', + 'type': 'ETH', + 'protocol': { + 'type': 'ETH', + 'protocol_data': {'chain_id': 1}, + }, + 'nodes': [ + {'url': 'https://eth.example.com'}, + ], + 'swap_contract_address': '0x61EEC68Cf64d1b31e41EA713356De2563fB6D3F1', + 'fallback_swap_contract': '0x24ABE4c71FC658C91313b6552cd40cD808b3Ea80', + }); + + expect(filter.shouldFilter(config), isFalse); + }); + }); } diff --git a/packages/komodo_coin_updates/test/local_asset_coin_config_provider_test.dart b/packages/komodo_coin_updates/test/local_asset_coin_config_provider_test.dart index e5043bbe7..e82497605 100644 --- a/packages/komodo_coin_updates/test/local_asset_coin_config_provider_test.dart +++ b/packages/komodo_coin_updates/test/local_asset_coin_config_provider_test.dart @@ -117,5 +117,43 @@ void main() { final kmd = assets.firstWhere((a) => a.id.id == 'KMD'); expect(kmd.isWalletOnly, isTrue); }); + + test('filters invalid EVM configs before parsing', () async { + const jsonMap = { + 'KMD': { + 'coin': 'KMD', + 'decimals': 8, + 'type': 'UTXO', + 'protocol': {'type': 'UTXO'}, + 'fname': 'Komodo', + 'chain_id': 0, + 'is_testnet': false, + }, + 'BROKENETH': { + 'coin': 'BROKENETH', + 'type': 'ETH', + 'protocol': { + 'type': 'ETH', + 'protocol_data': {'chain_id': 1}, + }, + 'chain_id': 1, + 'nodes': [], + 'swap_contract_address': '0x61EEC68Cf64d1b31e41EA713356De2563fB6D3F1', + }, + }; + final bundle = _FakeBundle({ + 'packages/komodo_defi_framework/assets/config/coins_config.json': + jsonEncode(jsonMap), + }); + + final provider = LocalAssetCoinConfigProvider.fromConfig( + const AssetRuntimeUpdateConfig(), + bundle: bundle, + ); + + final assets = await provider.getAssets(); + expect(assets.any((a) => a.id.id == 'KMD'), isTrue); + expect(assets.any((a) => a.id.id == 'BROKENETH'), isFalse); + }); }); } diff --git a/packages/komodo_coin_updates/test/seed_node_updater_mock_test.dart b/packages/komodo_coin_updates/test/seed_node_updater_mock_test.dart index b69ef35bd..be84ac803 100644 --- a/packages/komodo_coin_updates/test/seed_node_updater_mock_test.dart +++ b/packages/komodo_coin_updates/test/seed_node_updater_mock_test.dart @@ -36,7 +36,7 @@ void main() { "host": "test1.example.com", "type": "domain", "wss": true, - "netid": 8762, + "netid": 6133, "contact": [{"email": "test1@example.com"}] }, { @@ -44,7 +44,7 @@ void main() { "host": "test2.example.com", "type": "domain", "wss": true, - "netid": 8762, + "netid": 6133, "contact": [{"email": "test2@example.com"}] } ]'''; @@ -61,7 +61,7 @@ void main() { // Assert expect(result.seedNodes.length, equals(2)); - expect(result.netId, equals(8762)); + expect(result.netId, equals(6133)); expect(result.seedNodes[0].name, equals('test-seed-1')); expect(result.seedNodes[0].host, equals('test1.example.com')); expect(result.seedNodes[1].name, equals('test-seed-2')); @@ -95,6 +95,43 @@ void main() { verifyNever(() => mockClient.close()); }); + test('should throw when no seed nodes match the default netid', () async { + // Arrange + final mockResponse = MockResponse(); + const responseBody = '''[ + { + "name": "stale-seed-1", + "host": "stale1.example.com", + "type": "domain", + "wss": true, + "netid": 8762, + "contact": [{"email": "stale1@example.com"}] + } + ]'''; + + when(() => mockResponse.statusCode).thenReturn(200); + when(() => mockResponse.body).thenReturn(responseBody); + when(() => mockClient.get(any())).thenAnswer((_) async => mockResponse); + + // Act & Assert + await expectLater( + () => SeedNodeUpdater.fetchSeedNodes( + config: config, + httpClient: mockClient, + ), + throwsA( + isA().having( + (e) => e.toString(), + 'message', + contains('No seed nodes found for netid 6133'), + ), + ), + ); + + verify(() => mockClient.get(any())).called(1); + verifyNever(() => mockClient.close()); + }); + test('should handle HTTP errors properly', () async { // Arrange final mockResponse = MockResponse(); diff --git a/packages/komodo_coin_updates/test/seed_node_updater_test.dart b/packages/komodo_coin_updates/test/seed_node_updater_test.dart index dc34b9a23..c70e7d9ff 100644 --- a/packages/komodo_coin_updates/test/seed_node_updater_test.dart +++ b/packages/komodo_coin_updates/test/seed_node_updater_test.dart @@ -39,7 +39,7 @@ void main() { host: 'seed01.kmdefi.net', type: 'domain', wss: true, - netId: 8762, + netId: 6133, contact: [SeedNodeContact(email: 'test1@example.com')], ), const SeedNode( @@ -47,7 +47,7 @@ void main() { host: 'seed02.kmdefi.net', type: 'domain', wss: true, - netId: 8762, + netId: 6133, contact: [SeedNodeContact(email: 'test1@example.com')], ), ]; @@ -71,7 +71,7 @@ void main() { host: 'seed03.kmdefi.net', type: 'domain', wss: true, - netId: 8762, + netId: 6133, contact: [], // Empty contact list ), ]; @@ -83,7 +83,7 @@ void main() { host: 'seed04.kmdefi.net', type: 'domain', wss: false, - netId: 8762, + netId: 6133, contact: [SeedNodeContact(email: 'basic@example.com')], ), ...seedNodes, @@ -105,7 +105,7 @@ void main() { host: 'mainnet.kmdefi.net', type: 'domain', wss: true, - netId: 8762, // Mainnet + netId: 6133, // Mainnet contact: [SeedNodeContact(email: 'mainnet@example.com')], ), const SeedNode( @@ -132,7 +132,7 @@ void main() { host: 'wss.kmdefi.net', type: 'domain', wss: true, // WebSocket Secure - netId: 8762, + netId: 6133, contact: [SeedNodeContact(email: 'wss@example.com')], ), const SeedNode( @@ -140,7 +140,7 @@ void main() { host: 'ws.kmdefi.net', type: 'domain', wss: false, // Regular WebSocket - netId: 8762, + netId: 6133, contact: [SeedNodeContact(email: 'ws@example.com')], ), ]; @@ -159,7 +159,7 @@ void main() { host: '192.168.1.100', type: 'ip', // IP address type wss: true, - netId: 8762, + netId: 6133, contact: [SeedNodeContact(email: 'ip@example.com')], ), const SeedNode( @@ -167,7 +167,7 @@ void main() { host: '10.0.0.50', type: 'ip', wss: false, - netId: 8762, + netId: 6133, contact: [SeedNodeContact(email: 'ip2@example.com')], ), ]; @@ -186,7 +186,7 @@ void main() { host: 'complex.kmdefi.net', type: 'domain', wss: true, - netId: 8762, + netId: 6133, contact: [ SeedNodeContact(email: 'admin@example.com'), SeedNodeContact(email: 'support@example.com'), diff --git a/packages/komodo_defi_framework/lib/src/config/seed_node_validator.dart b/packages/komodo_defi_framework/lib/src/config/seed_node_validator.dart index ca48c85c1..4a28aac99 100644 --- a/packages/komodo_defi_framework/lib/src/config/seed_node_validator.dart +++ b/packages/komodo_defi_framework/lib/src/config/seed_node_validator.dart @@ -1,8 +1,11 @@ +import 'package:flutter/foundation.dart' show debugPrint; import 'package:komodo_defi_framework/src/config/kdf_logging_config.dart'; import 'package:komodo_defi_framework/src/exceptions/kdf_exception.dart'; /// Helper class to validate seed node configurations class SeedNodeValidator { + static const List _defaultSeedNodes = ['seed01.kmdefi.net']; + /// Validates the seed node configuration /// /// Throws [KdfException] if the configuration is invalid @@ -23,8 +26,10 @@ class SeedNodeValidator { // If P2P is disabled, no need for further validation if (disableP2p ?? false) { if (KdfLoggingConfig.verboseLogging) { - print('WARN P2P is disabled. Features that require a P2P network ' - '(like swaps, peer health checks, etc.) will not work.'); + debugPrint( + 'WARN P2P is disabled. Features that require a P2P network ' + '(like swaps, peer health checks, etc.) will not work.', + ); } return; } @@ -58,17 +63,19 @@ class SeedNodeValidator { // Warning about future requirements - updated to be more explicit if (seedNodes == null || seedNodes.isEmpty) { if (KdfLoggingConfig.verboseLogging) { - print('WARN From v2.5.0-beta, there will be no default seed nodes, ' - 'and the seednodes parameter will be required unless disable_p2p is set to true.'); + debugPrint( + 'WARN From v2.5.0-beta, there will be no default seed nodes, ' + 'and the seednodes parameter will be required unless ' + 'disable_p2p is set to true.', + ); } } } - /// Gets the default seed nodes if none are provided + /// Gets the emergency fallback seed nodes. /// - /// Note: From v2.5.0-beta, there will be no default seed nodes, - /// and the seednodes parameter will be required unless disable_p2p is set to true. - static List getDefaultSeedNodes() { - return ['seed01.kmdefi.net', 'seed02.kmdefi.net']; - } + /// The primary fallback path loads the bundled `seed_nodes.json` asset. + /// This list is kept as a last-resort fallback when the asset cannot be + /// loaded or does not contain any valid nodes for the current net ID. + static List getDefaultSeedNodes() => _defaultSeedNodes; } diff --git a/packages/komodo_defi_framework/lib/src/services/seed_node_service.dart b/packages/komodo_defi_framework/lib/src/services/seed_node_service.dart index 3f393e0f2..e9551920a 100644 --- a/packages/komodo_defi_framework/lib/src/services/seed_node_service.dart +++ b/packages/komodo_defi_framework/lib/src/services/seed_node_service.dart @@ -1,7 +1,9 @@ import 'package:flutter/foundation.dart'; +import 'package:flutter/services.dart' show AssetBundle, rootBundle; import 'package:komodo_coin_updates/komodo_coin_updates.dart'; import 'package:komodo_defi_framework/src/config/kdf_logging_config.dart'; import 'package:komodo_defi_framework/src/config/seed_node_validator.dart'; +import 'package:komodo_defi_types/komodo_defi_type_utils.dart'; import 'package:komodo_defi_types/komodo_defi_types.dart'; /// Service class responsible for fetching and managing seed nodes. @@ -9,6 +11,9 @@ import 'package:komodo_defi_types/komodo_defi_types.dart'; /// This class follows the Single Responsibility Principle by focusing /// solely on seed node acquisition and management. class SeedNodeService { + static const String _packageName = 'komodo_defi_framework'; + static const String _seedNodesAssetPath = 'assets/config/seed_nodes.json'; + /// Gets the runtime configuration for seed node updates. /// /// This method loads the appropriate configuration for fetching seed nodes, @@ -18,14 +23,15 @@ class SeedNodeService { return await configRepository.tryLoad() ?? const AssetRuntimeUpdateConfig(); } - /// Fetches seed nodes from the remote configuration with fallback to defaults. + /// Fetches seed nodes from the remote configuration with fallback to + /// bundled defaults. /// - /// This method attempts to fetch the latest seed nodes from the Komodo Platform - /// repository and converts them to the string format expected by the KDF startup - /// configuration. + /// This method attempts to fetch the latest seed nodes from the Komodo + /// Platform repository and converts them to the string format expected by + /// the KDF startup configuration. /// - /// Returns a list of seed node host addresses. If fetching fails, returns - /// the hardcoded default seed nodes as a fallback. + /// Returns a list of seed node host addresses. If fetching fails, falls back + /// to the bundled seed node asset, then to the hardcoded emergency seed list. static Future<({List seedNodes, int netId})> fetchSeedNodes({ bool filterForWeb = kIsWeb, }) async { @@ -45,22 +51,53 @@ class SeedNodeService { ); } catch (e) { if (KdfLoggingConfig.verboseLogging) { - print('WARN Failed to fetch seed nodes from remote: $e'); - print('WARN Falling back to default seed nodes'); + debugPrint('WARN Failed to fetch seed nodes from remote: $e'); + debugPrint('WARN Falling back to bundled seed nodes'); + } + + try { + final fallbackNodes = await loadBundledSeedNodes( + filterForWeb: filterForWeb, + ); + return (seedNodes: fallbackNodes, netId: kDefaultNetId); + } catch (fallbackError) { + if (KdfLoggingConfig.verboseLogging) { + debugPrint('WARN Failed to load bundled seed nodes: $fallbackError'); + debugPrint('WARN Falling back to emergency seed nodes'); + } + + return ( + seedNodes: SeedNodeValidator.getDefaultSeedNodes(), + netId: kDefaultNetId, + ); } - return ( - seedNodes: getDefaultSeedNodes(), - netId: kDefaultNetId, - ); } } - /// Gets the default seed nodes if remote fetching fails. + /// Loads bundled seed nodes from the framework asset package. /// - /// Note: From v2.5.0-beta, there will be no default seed nodes, - /// and the seednodes parameter will be required unless disable_p2p is set to true. - static List getDefaultSeedNodes() { - return SeedNodeValidator.getDefaultSeedNodes(); + /// The bundled asset is filtered the same way as the remote source: + /// only the current [kDefaultNetId] is accepted, and on web only WSS nodes + /// are kept. + static Future> loadBundledSeedNodes({ + bool filterForWeb = kIsWeb, + AssetBundle? bundle, + }) async { + const assetKey = 'packages/$_packageName/$_seedNodesAssetPath'; + final content = await (bundle ?? rootBundle).loadString(assetKey); + var seedNodes = SeedNode.fromJsonList( + jsonListFromString(content), + ).where((node) => node.netId == kDefaultNetId).toList(); + + if (filterForWeb && kIsWeb) { + seedNodes = seedNodes.where((node) => node.wss).toList(); + } + + if (seedNodes.isEmpty) { + throw Exception('No bundled seed nodes found for netid $kDefaultNetId'); + } + + return SeedNodeUpdater.seedNodesToStringList(seedNodes); } /// Gets seed nodes based on configuration preferences. @@ -79,7 +116,7 @@ class SeedNodeService { bool fetchRemote = true, }) async { // If P2P is disabled, no seed nodes are needed - if (disableP2p == true) { + if (disableP2p ?? false) { return null; } @@ -92,8 +129,12 @@ class SeedNodeService { if (fetchRemote) { final result = await fetchSeedNodes(); return result.seedNodes; - } else { - return getDefaultSeedNodes(); + } + + try { + return await loadBundledSeedNodes(); + } catch (_) { + return SeedNodeValidator.getDefaultSeedNodes(); } } } diff --git a/packages/komodo_defi_framework/test/seed_node_service_test.dart b/packages/komodo_defi_framework/test/seed_node_service_test.dart new file mode 100644 index 000000000..fe33aac92 --- /dev/null +++ b/packages/komodo_defi_framework/test/seed_node_service_test.dart @@ -0,0 +1,54 @@ +import 'package:flutter/services.dart' show AssetBundle, ByteData; +import 'package:flutter_test/flutter_test.dart'; +import 'package:komodo_defi_framework/src/services/seed_node_service.dart'; + +class _FakeBundle extends AssetBundle { + _FakeBundle(this.map); + + final Map map; + + @override + Future load(String key) => throw UnimplementedError(); + + @override + Future loadString(String key, {bool cache = true}) async => + map[key] ?? (throw StateError('Asset not found: $key')); + + @override + void evict(String key) {} +} + +void main() { + group('SeedNodeService.loadBundledSeedNodes', () { + test('filters bundled seed nodes by the current net id', () async { + final bundle = _FakeBundle({ + 'packages/komodo_defi_framework/assets/config/seed_nodes.json': ''' +[ + { + "name": "seed-node-1", + "host": "seed01.kmdefi.net", + "type": "domain", + "wss": true, + "netid": 6133, + "contact": [{"email": ""}] + }, + { + "name": "seed-node-2", + "host": "seed02.kmdefi.net", + "type": "domain", + "wss": true, + "netid": 8762, + "contact": [{"email": ""}] + } +] +''', + }); + + final seedNodes = await SeedNodeService.loadBundledSeedNodes( + bundle: bundle, + ); + + expect(seedNodes, equals(const ['seed01.kmdefi.net'])); + }); + }); +} diff --git a/packages/komodo_defi_framework/test/seed_node_validator_test.dart b/packages/komodo_defi_framework/test/seed_node_validator_test.dart new file mode 100644 index 000000000..a0e1987c4 --- /dev/null +++ b/packages/komodo_defi_framework/test/seed_node_validator_test.dart @@ -0,0 +1,25 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:komodo_defi_framework/src/config/seed_node_validator.dart'; + +void main() { + group('SeedNodeValidator', () { + test('accepts a single seed node for non-bootstrap peers', () { + expect( + () => SeedNodeValidator.validate( + seedNodes: const ['seed01.kmdefi.net'], + disableP2p: false, + iAmSeed: false, + isBootstrapNode: false, + ), + returnsNormally, + ); + }); + + test('default seed nodes contain the single supported host', () { + expect( + SeedNodeValidator.getDefaultSeedNodes(), + equals(const ['seed01.kmdefi.net']), + ); + }); + }); +} diff --git a/packages/komodo_defi_types/lib/src/constants.dart b/packages/komodo_defi_types/lib/src/constants.dart index f9f959d57..2f70b06f1 100644 --- a/packages/komodo_defi_types/lib/src/constants.dart +++ b/packages/komodo_defi_types/lib/src/constants.dart @@ -2,4 +2,4 @@ library komodo_defi_types.constants; /// Default network identifier used by seed nodes and framework configuration. -const int kDefaultNetId = 8762; +const int kDefaultNetId = 6133; diff --git a/packages/komodo_defi_types/test/seed_node_test.dart b/packages/komodo_defi_types/test/seed_node_test.dart index 08f8fe050..ab4f9e8d8 100644 --- a/packages/komodo_defi_types/test/seed_node_test.dart +++ b/packages/komodo_defi_types/test/seed_node_test.dart @@ -9,7 +9,7 @@ void main() { 'host': 'seed01.kmdefi.net', 'type': 'domain', 'wss': true, - 'netid': 8762, + 'netid': 6133, 'contact': [ {'email': 'admin@example.com'}, ], @@ -29,20 +29,22 @@ void main() { host: 'seed02.kmdefi.net', type: 'domain', wss: true, - netId: 8762, - contact: [ - SeedNodeContact(email: 'test@example.com'), - ], + netId: 6133, + contact: [SeedNodeContact(email: 'test@example.com')], ); final json = seedNode.toJson(); + final contacts = json['contact'] as List?; expect(json['name'], equals('seed-node-2')); expect(json['host'], equals('seed02.kmdefi.net')); - expect(json['contact'], isA>()); - expect((json['contact'] as List).length, equals(1)); - expect( - (json['contact'] as List).first['email'], equals('test@example.com'),); + expect(contacts, isNotNull); + expect(contacts, isA>()); + final nonNullContacts = contacts!; + expect(nonNullContacts.length, equals(1)); + + final contact = nonNullContacts.first! as Map; + expect(contact['email'], equals('test@example.com')); }); test('should create list of SeedNodes from JSON list', () { @@ -52,7 +54,7 @@ void main() { 'host': 'seed01.kmdefi.net', 'type': 'domain', 'wss': true, - 'netid': 8762, + 'netid': 6133, 'contact': [ {'email': ''}, ], @@ -62,11 +64,11 @@ void main() { 'host': 'seed02.kmdefi.net', 'type': 'domain', 'wss': true, - 'netid': 8762, + 'netid': 6133, 'contact': [ {'email': ''}, ], - } + }, ]; final seedNodes = SeedNode.fromJsonList(jsonList); @@ -84,7 +86,7 @@ void main() { host: 'example.com', type: 'domain', wss: true, - netId: 8762, + netId: 6133, contact: [SeedNodeContact(email: 'test@example.com')], ); @@ -93,7 +95,7 @@ void main() { host: 'example.com', type: 'domain', wss: true, - netId: 8762, + netId: 6133, contact: [SeedNodeContact(email: 'test@example.com')], ); @@ -102,7 +104,7 @@ void main() { host: 'example.com', type: 'domain', wss: true, - netId: 8762, + netId: 6133, contact: [SeedNodeContact(email: 'test@example.com')], ); From b563bdf9f642cf0d9992324ff7c91ec3abdbbd5a Mon Sep 17 00:00:00 2001 From: Nitride <77973576+CharlVS@users.noreply.github.com> Date: Wed, 18 Mar 2026 18:17:09 +0100 Subject: [PATCH 08/40] feat(sdk): add token safety and fee support helpers (#319) --- .../coins_config/custom_token_storage.dart | 79 +++++++- .../src/coins_config/custom_token_store.dart | 15 +- .../test/custom_token_storage_test.dart | 180 ++++++++++++++++++ .../app_build/build_config.json | 2 +- .../src/withdrawals/withdrawal_manager.dart | 92 +++++---- .../withdrawal_manager_fee_support_test.dart | 88 +++++++++ .../lib/komodo_defi_types.dart | 1 + .../src/assets/custom_token_exceptions.dart | 35 ++++ packages/komodo_defi_types/lib/src/types.dart | 1 + 9 files changed, 449 insertions(+), 44 deletions(-) create mode 100644 packages/komodo_coin_updates/test/custom_token_storage_test.dart create mode 100644 packages/komodo_defi_sdk/test/withdrawals/withdrawal_manager_fee_support_test.dart create mode 100644 packages/komodo_defi_types/lib/src/assets/custom_token_exceptions.dart diff --git a/packages/komodo_coin_updates/lib/src/coins_config/custom_token_storage.dart b/packages/komodo_coin_updates/lib/src/coins_config/custom_token_storage.dart index 3a85f8aa3..586225327 100644 --- a/packages/komodo_coin_updates/lib/src/coins_config/custom_token_storage.dart +++ b/packages/komodo_coin_updates/lib/src/coins_config/custom_token_storage.dart @@ -38,6 +38,7 @@ class CustomTokenStorage implements CustomTokenStore { Future storeCustomToken(Asset asset) async { _log.fine('Storing custom token ${asset.id.id}'); final box = await _openCustomTokensBox(); + await _validateCanStoreAsset(box, asset); await box.put(asset.id.id, asset); } @@ -45,6 +46,10 @@ class CustomTokenStorage implements CustomTokenStore { Future storeCustomTokens(List assets) async { _log.fine('Storing ${assets.length} custom tokens'); final box = await _openCustomTokensBox(); + _validateBatchCollisions(assets); + for (final asset in assets) { + await _validateCanStoreAsset(box, asset); + } final putMap = {for (final a in assets) a.id.id: a}; await box.putAll(putMap); } @@ -127,7 +132,9 @@ class CustomTokenStorage implements CustomTokenStore { @override Future upsertCustomToken(Asset asset) async { final box = await _openCustomTokensBox(); - final existed = box.containsKey(asset.id.id); + final existingAsset = await box.get(asset.id.id); + final existed = existingAsset != null; + _assertNoConflict(existingAsset, asset); await box.put(asset.id.id, asset); if (existed) { @@ -142,7 +149,9 @@ class CustomTokenStorage implements CustomTokenStore { @override Future addCustomTokenIfNotExists(Asset asset) async { final box = await _openCustomTokensBox(); - if (box.containsKey(asset.id.id)) { + final existingAsset = await box.get(asset.id.id); + if (existingAsset != null) { + _assertNoConflict(existingAsset, asset); _log.fine('Custom token ${asset.id.id} already exists, skipping'); return false; } @@ -185,6 +194,72 @@ class CustomTokenStorage implements CustomTokenStore { return _customTokensBox!; } + Future _validateCanStoreAsset(LazyBox box, Asset asset) async { + final existingAsset = await box.get(asset.id.id); + _assertNoConflict(existingAsset, asset); + } + + void _validateBatchCollisions(List assets) { + final assetsById = {}; + for (final asset in assets) { + final existingAsset = assetsById[asset.id.id]; + _assertNoConflict(existingAsset, asset); + assetsById[asset.id.id] = asset; + } + } + + void _assertNoConflict(Asset? existingAsset, Asset requestedAsset) { + if (existingAsset == null) { + return; + } + + if (_hasMatchingContract(existingAsset, requestedAsset)) { + return; + } + + throw CustomTokenConflictException( + assetId: requestedAsset.id.id, + network: requestedAsset.id.subClass, + existingContractAddress: existingAsset.protocol.contractAddress ?? '', + requestedContractAddress: requestedAsset.protocol.contractAddress ?? '', + ); + } + + bool _hasMatchingContract(Asset existingAsset, Asset requestedAsset) { + final hasMatchingIdentity = + existingAsset.id.subClass == requestedAsset.id.subClass && + existingAsset.id.chainId.formattedChainId == + requestedAsset.id.chainId.formattedChainId && + existingAsset.id.parentId == requestedAsset.id.parentId; + if (!hasMatchingIdentity) { + return false; + } + + final existingContractAddress = existingAsset.protocol.contractAddress; + final requestedContractAddress = requestedAsset.protocol.contractAddress; + if (existingContractAddress == null || requestedContractAddress == null) { + return false; + } + + return _normalizeContractAddress( + existingAsset.id.subClass, + existingContractAddress, + ) == + _normalizeContractAddress( + requestedAsset.id.subClass, + requestedContractAddress, + ); + } + + String _normalizeContractAddress( + CoinSubClass network, + String contractAddress, + ) { + return network == CoinSubClass.trc20 + ? contractAddress + : contractAddress.toLowerCase(); + } + ProtocolClass _markCustomToken(ProtocolClass protocol) { return switch (protocol) { final Erc20Protocol p => p.copyWith(isCustomToken: true), diff --git a/packages/komodo_coin_updates/lib/src/coins_config/custom_token_store.dart b/packages/komodo_coin_updates/lib/src/coins_config/custom_token_store.dart index 2fc2dbb91..40149489a 100644 --- a/packages/komodo_coin_updates/lib/src/coins_config/custom_token_store.dart +++ b/packages/komodo_coin_updates/lib/src/coins_config/custom_token_store.dart @@ -6,11 +6,15 @@ abstract class CustomTokenStore { Future init(); /// Stores a single custom token. - /// If a token with the same AssetId already exists, it will be overwritten. + /// If a token with the same storage key already exists, it is overwritten + /// only when it represents the same contract on the same network. + /// Otherwise, [CustomTokenConflictException] is thrown. Future storeCustomToken(Asset asset); /// Stores multiple custom tokens atomically (all-or-nothing). - /// Existing tokens with the same AssetIds will be overwritten. + /// Existing tokens with the same storage key are overwritten only when they + /// represent the same contract on the same network. + /// Otherwise, [CustomTokenConflictException] is thrown. /// Implementations should throw on partial failure. Future storeCustomTokens(List assets); @@ -39,11 +43,14 @@ abstract class CustomTokenStore { Future hasCustomTokens(); /// Upserts a custom token: updates if it exists, inserts otherwise. - /// Returns true if updated, false if inserted. + /// Returns true if updated, false if inserted. Throws + /// [CustomTokenConflictException] for same-key different-contract writes. Future upsertCustomToken(Asset asset); /// Adds a custom token to storage if it doesn't already exist. - /// Returns true if the token was added, false if it already existed. + /// Returns true if the token was added, false if the same contract already + /// existed. Throws [CustomTokenConflictException] for same-key + /// different-contract writes. Future addCustomTokenIfNotExists(Asset asset); /// Returns the number of custom tokens in storage. diff --git a/packages/komodo_coin_updates/test/custom_token_storage_test.dart b/packages/komodo_coin_updates/test/custom_token_storage_test.dart new file mode 100644 index 000000000..f10a6f660 --- /dev/null +++ b/packages/komodo_coin_updates/test/custom_token_storage_test.dart @@ -0,0 +1,180 @@ +import 'package:komodo_coin_updates/komodo_coin_updates.dart'; +import 'package:komodo_defi_types/komodo_defi_types.dart'; +import 'package:test/test.dart'; + +import 'hive/test_harness.dart'; + +Map _trxConfig() => { + 'coin': 'TRX', + 'type': 'TRX', + 'name': 'TRON', + 'fname': 'TRON', + 'wallet_only': true, + 'mm2': 1, + 'decimals': 6, + 'required_confirmations': 1, + 'derivation_path': "m/44'/195'", + 'protocol': { + 'type': 'TRX', + 'protocol_data': {'network': 'Mainnet'}, + }, + 'nodes': >[], +}; + +Map _trc20Config({ + required String coin, + required String name, + required String contractAddress, +}) => { + 'coin': coin, + 'type': 'TRC-20', + 'name': name, + 'fname': name, + 'wallet_only': true, + 'mm2': 1, + 'decimals': 6, + 'derivation_path': "m/44'/195'", + 'protocol': { + 'type': 'TRC20', + 'protocol_data': {'platform': 'TRX', 'contract_address': contractAddress}, + }, + 'contract_address': contractAddress, + 'parent_coin': 'TRX', + 'nodes': >[], +}; + +Asset _buildTrc20Asset({ + required Asset platformAsset, + required String coin, + required String name, + required String contractAddress, +}) { + return Asset.fromJson( + _trc20Config(coin: coin, name: name, contractAddress: contractAddress), + knownIds: {platformAsset.id}, + ); +} + +void main() { + group('CustomTokenStorage', () { + late HiveTestEnv hiveEnv; + late CustomTokenStorage storage; + late Asset platformAsset; + + setUp(() async { + hiveEnv = HiveTestEnv(); + await hiveEnv.setup(); + storage = CustomTokenStorage(customTokensBoxName: 'custom_tokens_test'); + await storage.init(); + platformAsset = Asset.fromJson(_trxConfig(), knownIds: const {}); + }); + + tearDown(() async { + await storage.dispose(); + await hiveEnv.dispose(); + }); + + test( + 'upsert allows replacing an existing token with the same contract', + () async { + final original = _buildTrc20Asset( + platformAsset: platformAsset, + coin: 'USDT-TRC20', + name: 'Tether USD', + contractAddress: 'TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t', + ); + final updated = _buildTrc20Asset( + platformAsset: platformAsset, + coin: 'USDT-TRC20', + name: 'Tether USD Updated', + contractAddress: 'TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t', + ); + + await storage.storeCustomToken(original); + final didUpdate = await storage.upsertCustomToken(updated); + final stored = await storage.getCustomToken(updated.id); + + expect(didUpdate, isTrue); + expect(stored?.id.name, updated.id.name); + expect( + stored?.protocol.contractAddress, + updated.protocol.contractAddress, + ); + }, + ); + + test('store rejects same-id different-contract collisions', () async { + final original = _buildTrc20Asset( + platformAsset: platformAsset, + coin: 'USDT-TRC20', + name: 'Tether USD', + contractAddress: 'TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t', + ); + final conflicting = _buildTrc20Asset( + platformAsset: platformAsset, + coin: 'USDT-TRC20', + name: 'Another USDT', + contractAddress: 'TXLAQ63Xg1NAzckPwKHvzw7CSEmLMEqcdj', + ); + + await storage.storeCustomToken(original); + + await expectLater( + storage.storeCustomToken(conflicting), + throwsA(isA()), + ); + + final stored = await storage.getCustomToken(original.id); + expect(await storage.getCustomTokenCount(), 1); + expect( + stored?.protocol.contractAddress, + original.protocol.contractAddress, + ); + }); + + test( + 'addCustomTokenIfNotExists is idempotent for the same contract', + () async { + final asset = _buildTrc20Asset( + platformAsset: platformAsset, + coin: 'USDT-TRC20', + name: 'Tether USD', + contractAddress: 'TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t', + ); + + final firstInsert = await storage.addCustomTokenIfNotExists(asset); + final secondInsert = await storage.addCustomTokenIfNotExists(asset); + + expect(firstInsert, isTrue); + expect(secondInsert, isFalse); + expect(await storage.getCustomTokenCount(), 1); + }, + ); + + test( + 'storeCustomTokens fails atomically for conflicting batch entries', + () async { + final first = _buildTrc20Asset( + platformAsset: platformAsset, + coin: 'USDT-TRC20', + name: 'Tether USD', + contractAddress: 'TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t', + ); + final conflicting = _buildTrc20Asset( + platformAsset: platformAsset, + coin: 'USDT-TRC20', + name: 'Another USDT', + contractAddress: 'TXLAQ63Xg1NAzckPwKHvzw7CSEmLMEqcdj', + ); + + await expectLater( + storage.storeCustomTokens([first, conflicting]), + throwsA(isA()), + ); + + expect(await storage.getCustomTokenCount(), 0); + expect(await storage.getCustomToken(first.id), isNull); + }, + ); + }); +} diff --git a/packages/komodo_defi_framework/app_build/build_config.json b/packages/komodo_defi_framework/app_build/build_config.json index 0228d580d..420ae0b39 100644 --- a/packages/komodo_defi_framework/app_build/build_config.json +++ b/packages/komodo_defi_framework/app_build/build_config.json @@ -68,7 +68,7 @@ "coins": { "fetch_at_build_enabled": true, "update_commit_on_build": true, - "bundled_coins_repo_commit": "e027082339558cc79d653d0e871f0d211562fe2f", + "bundled_coins_repo_commit": "f74c985339ff45de8768ab6c8be02ed8e2d0f49a", "coins_repo_api_url": "https://api.github.com/repos/GLEECBTC/coins", "coins_repo_content_url": "https://raw.githubusercontent.com/GLEECBTC/coins", "coins_repo_branch": "master", diff --git a/packages/komodo_defi_sdk/lib/src/withdrawals/withdrawal_manager.dart b/packages/komodo_defi_sdk/lib/src/withdrawals/withdrawal_manager.dart index febe40f0f..89e5615ce 100644 --- a/packages/komodo_defi_sdk/lib/src/withdrawals/withdrawal_manager.dart +++ b/packages/komodo_defi_sdk/lib/src/withdrawals/withdrawal_manager.dart @@ -9,6 +9,40 @@ import 'package:komodo_defi_sdk/src/fees/fee_manager.dart'; import 'package:komodo_defi_sdk/src/withdrawals/legacy_withdrawal_manager.dart'; import 'package:komodo_defi_types/komodo_defi_types.dart'; +/// Shared fee-estimation strategy classification used by withdrawal flows. +enum FeeEstimationSupport { + /// Ethereum-style gas estimation. + evmGas, + + /// UTXO fee-per-kilobyte estimation. + utxoPerKbyte, + + /// Tendermint gas estimation. + tendermint, + + /// QTUM gas estimation with UTXO fallback. + qtum, + + /// ZHTLC fixed-fee estimation. + zhtlc, + + /// No fee estimation support is available. + unsupported, +} + +/// Returns the fee-estimation support level for [protocol]. +FeeEstimationSupport feeEstimationSupportForProtocol(ProtocolClass protocol) { + return switch (protocol) { + Erc20Protocol() => FeeEstimationSupport.evmGas, + UtxoProtocol() => FeeEstimationSupport.utxoPerKbyte, + TendermintProtocol() => FeeEstimationSupport.tendermint, + QtumProtocol() => FeeEstimationSupport.qtum, + ZhtlcProtocol() => FeeEstimationSupport.zhtlc, + TrxProtocol() || Trc20Protocol() => FeeEstimationSupport.unsupported, + _ => FeeEstimationSupport.unsupported, + }; +} + /// Manages cryptocurrency asset withdrawals to external addresses. /// /// The [WithdrawalManager] provides functionality for: @@ -27,9 +61,9 @@ import 'package:komodo_defi_types/komodo_defi_types.dart'; /// 3. Broadcasting to the network /// 4. Status tracking /// -/// **Note:** Fee estimation features are currently disabled as the API endpoints -/// are not yet available. Set `_feeEstimationEnabled` to `true` when the API -/// endpoints become available. +/// **Note:** Fee estimation features are currently disabled as the API +/// endpoints are not yet available. Set `_feeEstimationEnabled` to `true` +/// when the API endpoints become available. /// /// Usage example: /// ```dart @@ -208,8 +242,8 @@ class WithdrawalManager { final protocol = asset.protocol; // Handle different protocol types - switch (protocol.runtimeType) { - case Erc20Protocol: + switch (feeEstimationSupportForProtocol(protocol)) { + case FeeEstimationSupport.evmGas: // Ethereum-based protocols use gas estimation final estimation = await _feeManager.getEthEstimatedFeePerGas( assetId, @@ -248,7 +282,7 @@ class WithdrawalManager { ), ); - case UtxoProtocol: + case FeeEstimationSupport.utxoPerKbyte: // UTXO-based protocols use per-kbyte fee estimation final estimation = await _feeManager.getUtxoEstimatedFee(assetId); return WithdrawalFeeOptions( @@ -279,7 +313,7 @@ class WithdrawalManager { ), ); - case TendermintProtocol: + case FeeEstimationSupport.tendermint: // Tendermint/Cosmos protocols use gas price and gas limit final estimation = await _feeManager.getTendermintEstimatedFee( assetId, @@ -315,7 +349,7 @@ class WithdrawalManager { ), ); - case QtumProtocol: + case FeeEstimationSupport.qtum: // QTUM uses similar gas model to Ethereum but with different fee structure try { final estimation = await _feeManager.getEthEstimatedFeePerGas( @@ -383,7 +417,7 @@ class WithdrawalManager { ); } - case ZhtlcProtocol: + case FeeEstimationSupport.zhtlc: // ZHTLC (Zcash) uses UTXO-style fees final estimation = await _feeManager.getUtxoEstimatedFee(assetId); return WithdrawalFeeOptions( @@ -414,8 +448,7 @@ class WithdrawalManager { ), ); - default: - // For unknown protocols, return null to indicate unsupported + case FeeEstimationSupport.unsupported: log('Fee options not supported for protocol ${protocol.runtimeType}'); return null; } @@ -929,8 +962,8 @@ class WithdrawalManager { final priority = params.feePriority ?? WithdrawalFeeLevel.medium; FeeInfo? fee; - switch (protocol.runtimeType) { - case Erc20Protocol: + switch (feeEstimationSupportForProtocol(protocol)) { + case FeeEstimationSupport.evmGas: // Ethereum-based protocols (ETH, ERC20 tokens) use gas estimation final estimation = await _feeManager.getEthEstimatedFeePerGas( asset.id.id, @@ -943,7 +976,7 @@ class WithdrawalManager { gas: _defaultEthGasLimit, ); - case UtxoProtocol: + case FeeEstimationSupport.utxoPerKbyte: // UTXO-based protocols use per-kbyte fee estimation final estimation = await _feeManager.getUtxoEstimatedFee(asset.id.id); final selectedLevel = _getUtxoFeeLevel(estimation, priority); @@ -952,7 +985,7 @@ class WithdrawalManager { amount: selectedLevel.feePerKbyte, ); - case TendermintProtocol: + case FeeEstimationSupport.tendermint: // Tendermint/Cosmos protocols use gas price and gas limit final estimation = await _feeManager.getTendermintEstimatedFee( asset.id.id, @@ -964,7 +997,7 @@ class WithdrawalManager { gasLimit: selectedLevel.gasLimit, ); - case QtumProtocol: + case FeeEstimationSupport.qtum: // QTUM uses similar gas model to Ethereum but different fee structure try { final estimation = await _feeManager.getEthEstimatedFeePerGas( @@ -988,7 +1021,7 @@ class WithdrawalManager { ); } - case ZhtlcProtocol: + case FeeEstimationSupport.zhtlc: // ZHTLC (Zcash) uses UTXO-style fees final estimation = await _feeManager.getUtxoEstimatedFee(asset.id.id); final selectedLevel = _getUtxoFeeLevel(estimation, priority); @@ -999,26 +1032,11 @@ class WithdrawalManager { Decimal.fromInt(250), // Assume ~250 bytes ); - default: - // For unknown protocols, attempt ETH estimation as fallback - try { - final estimation = await _feeManager.getEthEstimatedFeePerGas( - asset.id.id, - ); - final selectedLevel = _getEthFeeLevel(estimation, priority); - fee = FeeInfo.ethGasEip1559( - coin: asset.id.id, - maxFeePerGas: selectedLevel.maxFeePerGas, - maxPriorityFeePerGas: selectedLevel.maxPriorityFeePerGas, - gas: _defaultEthGasLimit, - ); - } catch (e) { - log( - 'No fee estimation available for protocol ${protocol.runtimeType}', - ); - // Return original parameters without fee - return params; - } + case FeeEstimationSupport.unsupported: + log( + 'No fee estimation available for protocol ${protocol.runtimeType}', + ); + return params; } return WithdrawParameters( diff --git a/packages/komodo_defi_sdk/test/withdrawals/withdrawal_manager_fee_support_test.dart b/packages/komodo_defi_sdk/test/withdrawals/withdrawal_manager_fee_support_test.dart new file mode 100644 index 000000000..c042d223e --- /dev/null +++ b/packages/komodo_defi_sdk/test/withdrawals/withdrawal_manager_fee_support_test.dart @@ -0,0 +1,88 @@ +import 'package:komodo_defi_sdk/src/withdrawals/withdrawal_manager.dart'; +import 'package:komodo_defi_types/komodo_defi_types.dart'; +import 'package:test/test.dart'; + +Map _trxConfig() => { + 'coin': 'TRX', + 'type': 'TRX', + 'name': 'TRON', + 'fname': 'TRON', + 'wallet_only': true, + 'mm2': 1, + 'decimals': 6, + 'required_confirmations': 1, + 'derivation_path': "m/44'/195'", + 'protocol': { + 'type': 'TRX', + 'protocol_data': {'network': 'Mainnet'}, + }, + 'nodes': >[], +}; + +Map _trc20Config() => { + 'coin': 'USDT-TRC20', + 'type': 'TRC-20', + 'name': 'Tether', + 'fname': 'Tether', + 'wallet_only': true, + 'mm2': 1, + 'decimals': 6, + 'derivation_path': "m/44'/195'", + 'protocol': { + 'type': 'TRC20', + 'protocol_data': { + 'platform': 'TRX', + 'contract_address': 'TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t', + }, + }, + 'contract_address': 'TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t', + 'parent_coin': 'TRX', + 'nodes': >[], +}; + +Map _erc20Config() => { + 'coin': 'ETH', + 'type': 'ETH', + 'name': 'Ethereum', + 'fname': 'Ethereum', + 'wallet_only': false, + 'mm2': 1, + 'chain_id': 1, + 'required_confirmations': 3, + 'protocol': { + 'type': 'ETH', + 'protocol_data': {'chain_id': 1}, + }, + 'nodes': >[], +}; + +void main() { + group('feeEstimationSupportForProtocol', () { + test('TRX platform assets are explicitly unsupported', () { + final protocol = ProtocolClass.fromJson(_trxConfig()); + + expect( + feeEstimationSupportForProtocol(protocol), + FeeEstimationSupport.unsupported, + ); + }); + + test('TRC20 tokens are explicitly unsupported', () { + final protocol = ProtocolClass.fromJson(_trc20Config()); + + expect( + feeEstimationSupportForProtocol(protocol), + FeeEstimationSupport.unsupported, + ); + }); + + test('supported EVM protocols still classify as gas-estimated', () { + final protocol = ProtocolClass.fromJson(_erc20Config()); + + expect( + feeEstimationSupportForProtocol(protocol), + FeeEstimationSupport.evmGas, + ); + }); + }); +} diff --git a/packages/komodo_defi_types/lib/komodo_defi_types.dart b/packages/komodo_defi_types/lib/komodo_defi_types.dart index 279499c95..873190192 100644 --- a/packages/komodo_defi_types/lib/komodo_defi_types.dart +++ b/packages/komodo_defi_types/lib/komodo_defi_types.dart @@ -9,6 +9,7 @@ export 'src/api/api_client.dart'; export 'src/assets/asset.dart'; export 'src/assets/asset_cache_key.dart'; export 'src/assets/asset_id.dart'; +export 'src/assets/custom_token_exceptions.dart'; export 'src/auth/auth_result.dart'; // export 'src/auth/exceptions/incorrect_password_exception.dart'; export 'src/auth/exceptions/auth_exception.dart'; diff --git a/packages/komodo_defi_types/lib/src/assets/custom_token_exceptions.dart b/packages/komodo_defi_types/lib/src/assets/custom_token_exceptions.dart new file mode 100644 index 000000000..04646748e --- /dev/null +++ b/packages/komodo_defi_types/lib/src/assets/custom_token_exceptions.dart @@ -0,0 +1,35 @@ +import 'package:komodo_defi_types/src/coin_classes/coin_subclasses.dart'; + +class CustomTokenConflictException implements Exception { + const CustomTokenConflictException({ + required this.assetId, + required this.network, + required this.existingContractAddress, + required this.requestedContractAddress, + }); + + final String assetId; + final CoinSubClass network; + final String existingContractAddress; + final String requestedContractAddress; + + String get message => + 'A different ${network.formatted} token with id "$assetId" is already ' + 'stored. Existing contract: $existingContractAddress. Requested ' + 'contract: $requestedContractAddress.'; + + @override + String toString() => message; +} + +class UnsupportedCustomTokenNetworkException implements Exception { + const UnsupportedCustomTokenNetworkException(this.network); + + final CoinSubClass network; + + String get message => + 'Custom token import is not supported for ${network.formatted}.'; + + @override + String toString() => message; +} diff --git a/packages/komodo_defi_types/lib/src/types.dart b/packages/komodo_defi_types/lib/src/types.dart index f1376720c..2ce8aea6e 100644 --- a/packages/komodo_defi_types/lib/src/types.dart +++ b/packages/komodo_defi_types/lib/src/types.dart @@ -12,6 +12,7 @@ export 'assets/asset.dart'; export 'assets/asset_cache_key.dart'; export 'assets/asset_id.dart'; export 'assets/asset_symbol.dart'; +export 'assets/custom_token_exceptions.dart'; export 'auth/auth_options.dart'; export 'auth/auth_result.dart'; export 'auth/exceptions/auth_exception.dart'; From 3d1b231757c34acf65ed06262e073f28c8e0eda7 Mon Sep 17 00:00:00 2001 From: Nitride <77973576+CharlVS@users.noreply.github.com> Date: Wed, 18 Mar 2026 20:43:28 +0100 Subject: [PATCH 09/40] fix(sdk): expose custom token cleanup (#321) --- .../komodo_defi_sdk/lib/src/komodo_defi_sdk.dart | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/packages/komodo_defi_sdk/lib/src/komodo_defi_sdk.dart b/packages/komodo_defi_sdk/lib/src/komodo_defi_sdk.dart index bf2b9efad..493d868d0 100644 --- a/packages/komodo_defi_sdk/lib/src/komodo_defi_sdk.dart +++ b/packages/komodo_defi_sdk/lib/src/komodo_defi_sdk.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'dart:developer'; import 'package:get_it/get_it.dart'; +import 'package:komodo_coins/komodo_coins.dart' show KomodoAssetsUpdateManager; import 'package:komodo_defi_framework/komodo_defi_framework.dart'; import 'package:komodo_defi_local_auth/komodo_defi_local_auth.dart'; import 'package:komodo_defi_sdk/komodo_defi_sdk.dart'; @@ -206,6 +207,19 @@ class KomodoDefiSdk with SecureRpcPasswordMixin { /// Throws [StateError] if accessed before initialization. AssetManager get assets => _assertSdkInitialized(_container()); + /// Deletes a persisted custom token from SDK-managed storage. + /// + /// This removes the token from the custom-token store and the in-memory + /// asset registry, then invalidates the activated-assets cache so follow-up + /// activation checks do not continue resolving the deleted asset. + Future deleteCustomToken(AssetId assetId) async { + _assertSdkInitialized(assets); + await _container().assets.deleteCustomToken( + assetId, + ); + activatedAssetsCache.invalidate(); + } + /// Cache of activated assets with per-instance TTL. /// /// Useful for avoiding repeated activation RPC calls across features. From 7a3f409ac3a31da934edec4d77d79a5bf21fe0a3 Mon Sep 17 00:00:00 2001 From: Nitride <77973576+CharlVS@users.noreply.github.com> Date: Wed, 18 Mar 2026 21:23:10 +0100 Subject: [PATCH 10/40] feat(sdk): add SIA activation and withdrawal support (#320) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor(withdrawals): enforce preview-then-execute workflow to prevent duplicate signing - Add executeWithdrawal() method to broadcast pre-signed transactions - Deprecate withdraw() method in favor of two-step preview-execute flow - Update example app to use new executeWithdrawal() method - Prevents transactions from being signed multiple times - Ensures users always review transaction details before broadcast Breaking Changes: - withdraw() is now deprecated (still functional but not recommended) - Recommended flow: previewWithdrawal() → executeWithdrawal() * Update packages/komodo_defi_sdk/lib/src/withdrawals/withdrawal_manager.dart Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * use latest KDF commit in `sia-rc-cleanup` * migrate SIA updates from old branch * fix wallet build errors * fix sia activation param * chore: update generated code chore: update generated code by running `melos run prepare` (requires Dart `melos` tool to be installed) * fix: fix broken sia RPC method test fix broken sia RPC method test. NB! Only fixing static analysis errors - the functionality of the test itself remains unchanged * clean task loop exit * implement SIA withdrawals * exclude SIA coins from pk export * fix(sia): explicitly remove support for tx streaming for sia * test(sia): add test case to confirm that tx history streaming is not supported for sia * chore(deps): bump kdf to 85d354d * update kdf commit * updates kdf commit --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: smk762 Co-authored-by: Francois --- .../legacy_withdrawal_manager.dart | 65 +++++++++++++++++++ 1 file changed, 65 insertions(+) diff --git a/packages/komodo_defi_sdk/lib/src/withdrawals/legacy_withdrawal_manager.dart b/packages/komodo_defi_sdk/lib/src/withdrawals/legacy_withdrawal_manager.dart index e10c00a66..0341216ae 100644 --- a/packages/komodo_defi_sdk/lib/src/withdrawals/legacy_withdrawal_manager.dart +++ b/packages/komodo_defi_sdk/lib/src/withdrawals/legacy_withdrawal_manager.dart @@ -195,6 +195,71 @@ class LegacyWithdrawalManager implements WithdrawalManager { } } + /// Execute a withdrawal from a previously generated preview. + /// + /// This method broadcasts the pre-signed transaction from the preview, + /// avoiding the need to sign the transaction again. This is the ONLY + /// recommended way to execute withdrawals for Tendermint assets. + /// + /// Parameters: + /// - [preview] - The preview result from [previewWithdrawal] + /// - [assetId] - The asset identifier (coin symbol) + /// + /// Returns a [Stream] that emits progress updates. + @override + Stream executeWithdrawal( + WithdrawalPreview preview, + String assetId, + ) async* { + try { + // Initial progress update + yield WithdrawalProgress( + status: WithdrawalStatus.inProgress, + message: 'Broadcasting signed transaction...', + withdrawalResult: WithdrawalResult( + txHash: preview.txHash, + balanceChanges: preview.balanceChanges, + coin: assetId, + toAddress: preview.to.first, + fee: preview.fee, + kmdRewardsEligible: + preview.kmdRewards != null && + Decimal.parse(preview.kmdRewards!.amount) > Decimal.zero, + ), + ); + + // Broadcast the pre-signed transaction + final broadcastResponse = await _client.rpc.withdraw.sendRawTransaction( + coin: assetId, + txHex: preview.txHex, + txJson: preview.txJson, + ); + + // Final success update with actual broadcast transaction hash + yield WithdrawalProgress( + status: WithdrawalStatus.complete, + message: 'Withdrawal completed successfully', + withdrawalResult: WithdrawalResult( + txHash: broadcastResponse.txHash, + balanceChanges: preview.balanceChanges, + coin: assetId, + toAddress: preview.to.first, + fee: preview.fee, + kmdRewardsEligible: + preview.kmdRewards != null && + Decimal.parse(preview.kmdRewards!.amount) > Decimal.zero, + ), + ); + } catch (e) { + yield* Stream.error( + WithdrawalException( + 'Failed to broadcast transaction: $e', + WithdrawalErrorCode.networkError, + ), + ); + } + } + /// No-op for legacy implementation since there's no task to cancel @override Future cancelWithdrawal(int taskId) async => false; From 482ca5feb53960ded40788873f41d291bb317906 Mon Sep 17 00:00:00 2001 From: Nitride <77973576+CharlVS@users.noreply.github.com> Date: Wed, 18 Mar 2026 21:54:49 +0100 Subject: [PATCH 11/40] fix(withdrawals): remove duplicate executeWithdrawal method (#322) The squash merge of the SIA branch into dev left a duplicate executeWithdrawal method in LegacyWithdrawalManager. Remove the second copy which also inconsistently used WithdrawalException directly instead of the _mapError helper. --- .../legacy_withdrawal_manager.dart | 65 ------------------- 1 file changed, 65 deletions(-) diff --git a/packages/komodo_defi_sdk/lib/src/withdrawals/legacy_withdrawal_manager.dart b/packages/komodo_defi_sdk/lib/src/withdrawals/legacy_withdrawal_manager.dart index 0341216ae..e10c00a66 100644 --- a/packages/komodo_defi_sdk/lib/src/withdrawals/legacy_withdrawal_manager.dart +++ b/packages/komodo_defi_sdk/lib/src/withdrawals/legacy_withdrawal_manager.dart @@ -195,71 +195,6 @@ class LegacyWithdrawalManager implements WithdrawalManager { } } - /// Execute a withdrawal from a previously generated preview. - /// - /// This method broadcasts the pre-signed transaction from the preview, - /// avoiding the need to sign the transaction again. This is the ONLY - /// recommended way to execute withdrawals for Tendermint assets. - /// - /// Parameters: - /// - [preview] - The preview result from [previewWithdrawal] - /// - [assetId] - The asset identifier (coin symbol) - /// - /// Returns a [Stream] that emits progress updates. - @override - Stream executeWithdrawal( - WithdrawalPreview preview, - String assetId, - ) async* { - try { - // Initial progress update - yield WithdrawalProgress( - status: WithdrawalStatus.inProgress, - message: 'Broadcasting signed transaction...', - withdrawalResult: WithdrawalResult( - txHash: preview.txHash, - balanceChanges: preview.balanceChanges, - coin: assetId, - toAddress: preview.to.first, - fee: preview.fee, - kmdRewardsEligible: - preview.kmdRewards != null && - Decimal.parse(preview.kmdRewards!.amount) > Decimal.zero, - ), - ); - - // Broadcast the pre-signed transaction - final broadcastResponse = await _client.rpc.withdraw.sendRawTransaction( - coin: assetId, - txHex: preview.txHex, - txJson: preview.txJson, - ); - - // Final success update with actual broadcast transaction hash - yield WithdrawalProgress( - status: WithdrawalStatus.complete, - message: 'Withdrawal completed successfully', - withdrawalResult: WithdrawalResult( - txHash: broadcastResponse.txHash, - balanceChanges: preview.balanceChanges, - coin: assetId, - toAddress: preview.to.first, - fee: preview.fee, - kmdRewardsEligible: - preview.kmdRewards != null && - Decimal.parse(preview.kmdRewards!.amount) > Decimal.zero, - ), - ); - } catch (e) { - yield* Stream.error( - WithdrawalException( - 'Failed to broadcast transaction: $e', - WithdrawalErrorCode.networkError, - ), - ); - } - } - /// No-op for legacy implementation since there's no task to cancel @override Future cancelWithdrawal(int taskId) async => false; From 3b4f492c6cb117d5dd0ab6f1805a5831b354966f Mon Sep 17 00:00:00 2001 From: Nitride <77973576+CharlVS@users.noreply.github.com> Date: Sat, 21 Mar 2026 14:32:48 +0100 Subject: [PATCH 12/40] fix(ui): detect asset icon precache failures (#326) --- .../lib/src/defi/asset/asset_icon.dart | 71 +++++++++++-------- 1 file changed, 40 insertions(+), 31 deletions(-) diff --git a/packages/komodo_ui/lib/src/defi/asset/asset_icon.dart b/packages/komodo_ui/lib/src/defi/asset/asset_icon.dart index 179083f49..48a8d8543 100644 --- a/packages/komodo_ui/lib/src/defi/asset/asset_icon.dart +++ b/packages/komodo_ui/lib/src/defi/asset/asset_icon.dart @@ -129,6 +129,19 @@ class AssetIcon extends StatelessWidget { } } +/// [precacheImage] with [ImageStreamListener.onError] still completes its future +/// successfully; this type records whether loading actually succeeded. +final class _PrecacheOutcome { + _PrecacheOutcome() : _succeeded = true; + + bool _succeeded; + bool get succeeded => _succeeded; + + void recordFailure(Object error, StackTrace? stackTrace) { + _succeeded = false; + } +} + class _AssetIconResolver extends StatelessWidget { const _AssetIconResolver({ required this.assetId, @@ -163,6 +176,19 @@ class _AssetIconResolver extends StatelessWidget { String get _imagePath => '$_coinImagesFolder$_sanitizedId.png'; String get _cdnUrl => '$_mediaCdnUrl$_sanitizedId.png'; + static Future _didImagePrecacheSucceed( + ImageProvider image, + BuildContext context, + ) async { + final outcome = _PrecacheOutcome(); + await precacheImage( + image, + context, + onError: outcome.recordFailure, + ); + return outcome.succeeded; + } + static Future precacheAssetIcon( BuildContext context, AssetId asset, { @@ -189,44 +215,27 @@ class _AssetIconResolver extends StatelessWidget { return; } - bool? assetExists; - bool? cdnExists; - final assetImage = AssetImage(resolver._imagePath); final cdnImage = NetworkImage(resolver._cdnUrl); - assetExists = true; - await precacheImage( - assetImage, - context, - onError: (e, stackTrace) { - assetExists = false; - if (throwExceptions) { - throw Exception( - 'Failed to pre-cache image for asset ${asset.id}: $e', - ); - } - }, - ); + final assetSucceeded = await _didImagePrecacheSucceed(assetImage, context); + _assetExistenceCache[resolver._imagePath] = assetSucceeded; + + if (assetSucceeded) { + return; + } + final cdnSucceeded = + context.mounted && await _didImagePrecacheSucceed(cdnImage, context); if (context.mounted) { - cdnExists = true; - await precacheImage( - cdnImage, - context, - onError: (e, stackTrace) { - cdnExists = false; - if (throwExceptions) { - throw Exception( - 'Failed to pre-cache CDN image for asset ${asset.id}: $e', - ); - } - }, - ); + _cdnExistenceCache[sanitizedId] = cdnSucceeded; } - _assetExistenceCache[resolver._imagePath] = assetExists ?? false; - if (cdnExists != null) _cdnExistenceCache[sanitizedId] = cdnExists!; + if (throwExceptions && !cdnSucceeded) { + throw Exception( + 'Failed to pre-cache bundled and CDN images for asset ${asset.id}', + ); + } } catch (e) { debugPrint('Error in precacheAssetIcon for ${asset.id}: $e'); if (throwExceptions) rethrow; From 67193af1524732ef9a072642ef26c0563472ecc9 Mon Sep 17 00:00:00 2001 From: Nitride <77973576+CharlVS@users.noreply.github.com> Date: Sat, 21 Mar 2026 14:34:48 +0100 Subject: [PATCH 13/40] chore(build): format config updates consistently (#325) --- .../app_build/build_config.json | 169 ++++++++--------- .../lib/komodo_wallet_build_transformer.dart | 1 + .../models/coin_assets/coin_build_config.dart | 13 +- .../lib/src/util/ide_json_formatter.dart | 177 ++++++++++++++++++ .../test/util/ide_json_formatter_test.dart | 40 ++++ .../bin/update_api_config.dart | 4 +- packages/komodo_wallet_cli/pubspec.yaml | 3 +- 7 files changed, 311 insertions(+), 96 deletions(-) create mode 100644 packages/komodo_wallet_build_transformer/lib/komodo_wallet_build_transformer.dart create mode 100644 packages/komodo_wallet_build_transformer/lib/src/util/ide_json_formatter.dart create mode 100644 packages/komodo_wallet_build_transformer/test/util/ide_json_formatter_test.dart diff --git a/packages/komodo_defi_framework/app_build/build_config.json b/packages/komodo_defi_framework/app_build/build_config.json index 420ae0b39..9ce619231 100644 --- a/packages/komodo_defi_framework/app_build/build_config.json +++ b/packages/komodo_defi_framework/app_build/build_config.json @@ -1,90 +1,87 @@ { - "api": { - "api_commit_hash": "52ba4f9ff12fd9e31768abc924a99ceadd64b2d6", - "branch": "staging", - "fetch_at_build_enabled": true, - "concurrent_downloads_enabled": true, - "source_urls": [ - "https://api.github.com/repos/GLEECBTC/komodo-defi-framework", - "https://devbuilds.gleec.com", - "https://nebula.decker.im" + "api": { + "api_commit_hash": "52ba4f9ff12fd9e31768abc924a99ceadd64b2d6", + "branch": "staging", + "fetch_at_build_enabled": true, + "concurrent_downloads_enabled": true, + "source_urls": [ + "https://api.github.com/repos/GLEECBTC/komodo-defi-framework", + "https://devbuilds.gleec.com", + "https://nebula.decker.im" + ], + "platforms": { + "web": { + "matching_pattern": "^(?:kdf_[a-f0-9]{7,40}-wasm|mm2_[a-f0-9]{7,40}-wasm|mm2-[a-f0-9]{7,40}-wasm)\\.zip$", + "valid_zip_sha256_checksums": [ + "e0b43651c773f222a4b8aa0c0a314bb3748b4f6edaefb3db70c76b34aaf4030b" ], - "platforms": { - "web": { - "matching_pattern": "^(?:kdf_[a-f0-9]{7,40}-wasm|mm2_[a-f0-9]{7,40}-wasm|mm2-[a-f0-9]{7,40}-wasm)\\.zip$", - "valid_zip_sha256_checksums": [ - "e0b43651c773f222a4b8aa0c0a314bb3748b4f6edaefb3db70c76b34aaf4030b" - ], - "path": "web/kdf/bin" - }, - "ios": { - "matching_pattern": "^(?:kdf_[a-f0-9]{7,40}-ios-aarch64|mm2_[a-f0-9]{7,40}-ios-aarch64|mm2-[a-f0-9]{7,40}-ios-aarch64-CI)\\.zip$", - "valid_zip_sha256_checksums": [ - "b81d7c7a8f9a7c2c5c15d700ca2ff2683f0ad307ed7d9cd7b7541638940831d4" - ], - "path": "ios" - }, - "macos": { - "matching_pattern": "^(?:kdf-macos-universal2-[a-f0-9]{7,40}|kdf_[a-f0-9]{7,40}-mac-universal)\\.zip$", - "matching_preference": [ - "universal2", - "mac-arm64" - ], - "valid_zip_sha256_checksums": [ - "6c09130fa7e4977dff617df5d5be385c1f2a57e6764a63e12e79797df52568f4" - ], - "path": "macos/bin" - }, - "windows": { - "matching_pattern": "^(?:kdf_[a-f0-9]{7,40}-win-x86-64|mm2_[a-f0-9]{7,40}-win-x86-64|mm2-[a-f0-9]{7,40}-Win64)\\.zip$", - "valid_zip_sha256_checksums": [ - "398e5adc95706613c71c672424aadf719899581fbc48702a5bb800c80c3e27bd" - ], - "path": "windows/bin" - }, - "android-armv7": { - "matching_pattern": "^(?:kdf_[a-f0-9]{7,40}-android-armv7|mm2_[a-f0-9]{7,40}-android-armv7|mm2-[a-f0-9]{7,40}-android-armv7-CI)\\.zip$", - "valid_zip_sha256_checksums": [ - "eb41a69db8b002f51a4d04045492c0f6f9fcce8dd3aa655c877e41e5482d1b36" - ], - "path": "android/app/src/main/cpp/libs/armeabi-v7a" - }, - "android-aarch64": { - "matching_pattern": "^(?:kdf_[a-f0-9]{7,40}-android-aarch64|mm2_[a-f0-9]{7,40}-android-aarch64|mm2-[a-f0-9]{7,40}-android-aarch64-CI)\\.zip$", - "valid_zip_sha256_checksums": [ - "dc5bdd90196477eba9c4debe1dc93504d24980b45a669d777374bfe09dda73aa" - ], - "path": "android/app/src/main/cpp/libs/arm64-v8a" - }, - "linux": { - "matching_pattern": "^(?:kdf_[a-f0-9]{7,40}-linux-x86-64|mm2_[a-f0-9]{7,40}-linux-x86-64|mm2-[a-f0-9]{7,40}-Linux-Release)\\.zip$", - "valid_zip_sha256_checksums": [ - "f282176b99d080315df9f47c11713084cc37de0056c144950aa9f033a8cc6302" - ], - "path": "linux/bin" - } - } + "path": "web/kdf/bin" + }, + "ios": { + "matching_pattern": "^(?:kdf_[a-f0-9]{7,40}-ios-aarch64|mm2_[a-f0-9]{7,40}-ios-aarch64|mm2-[a-f0-9]{7,40}-ios-aarch64-CI)\\.zip$", + "valid_zip_sha256_checksums": [ + "b81d7c7a8f9a7c2c5c15d700ca2ff2683f0ad307ed7d9cd7b7541638940831d4" + ], + "path": "ios" + }, + "macos": { + "matching_pattern": "^(?:kdf-macos-universal2-[a-f0-9]{7,40}|kdf_[a-f0-9]{7,40}-mac-universal)\\.zip$", + "matching_preference": ["universal2", "mac-arm64"], + "valid_zip_sha256_checksums": [ + "6c09130fa7e4977dff617df5d5be385c1f2a57e6764a63e12e79797df52568f4" + ], + "path": "macos/bin" + }, + "windows": { + "matching_pattern": "^(?:kdf_[a-f0-9]{7,40}-win-x86-64|mm2_[a-f0-9]{7,40}-win-x86-64|mm2-[a-f0-9]{7,40}-Win64)\\.zip$", + "valid_zip_sha256_checksums": [ + "398e5adc95706613c71c672424aadf719899581fbc48702a5bb800c80c3e27bd" + ], + "path": "windows/bin" + }, + "android-armv7": { + "matching_pattern": "^(?:kdf_[a-f0-9]{7,40}-android-armv7|mm2_[a-f0-9]{7,40}-android-armv7|mm2-[a-f0-9]{7,40}-android-armv7-CI)\\.zip$", + "valid_zip_sha256_checksums": [ + "eb41a69db8b002f51a4d04045492c0f6f9fcce8dd3aa655c877e41e5482d1b36" + ], + "path": "android/app/src/main/cpp/libs/armeabi-v7a" + }, + "android-aarch64": { + "matching_pattern": "^(?:kdf_[a-f0-9]{7,40}-android-aarch64|mm2_[a-f0-9]{7,40}-android-aarch64|mm2-[a-f0-9]{7,40}-android-aarch64-CI)\\.zip$", + "valid_zip_sha256_checksums": [ + "dc5bdd90196477eba9c4debe1dc93504d24980b45a669d777374bfe09dda73aa" + ], + "path": "android/app/src/main/cpp/libs/arm64-v8a" + }, + "linux": { + "matching_pattern": "^(?:kdf_[a-f0-9]{7,40}-linux-x86-64|mm2_[a-f0-9]{7,40}-linux-x86-64|mm2-[a-f0-9]{7,40}-Linux-Release)\\.zip$", + "valid_zip_sha256_checksums": [ + "f282176b99d080315df9f47c11713084cc37de0056c144950aa9f033a8cc6302" + ], + "path": "linux/bin" + } + } + }, + "coins": { + "fetch_at_build_enabled": true, + "update_commit_on_build": true, + "bundled_coins_repo_commit": "8fab732731df59a2c1ccc694d565e8535cd51c93", + "coins_repo_api_url": "https://api.github.com/repos/GLEECBTC/coins", + "coins_repo_content_url": "https://raw.githubusercontent.com/GLEECBTC/coins", + "coins_repo_branch": "feat/add-tron-coins", + "runtime_updates_enabled": true, + "mapped_files": { + "assets/config/coins_config.json": "utils/coins_config_unfiltered.json", + "assets/config/coins.json": "coins", + "assets/config/seed_nodes.json": "seed-nodes.json" + }, + "mapped_folders": { + "assets/coin_icons/png/": "icons" }, - "coins": { - "fetch_at_build_enabled": true, - "update_commit_on_build": true, - "bundled_coins_repo_commit": "f74c985339ff45de8768ab6c8be02ed8e2d0f49a", - "coins_repo_api_url": "https://api.github.com/repos/GLEECBTC/coins", - "coins_repo_content_url": "https://raw.githubusercontent.com/GLEECBTC/coins", - "coins_repo_branch": "master", - "runtime_updates_enabled": true, - "mapped_files": { - "assets/config/coins_config.json": "utils/coins_config_unfiltered.json", - "assets/config/coins.json": "coins", - "assets/config/seed_nodes.json": "seed-nodes.json" - }, - "mapped_folders": { - "assets/coin_icons/png/": "icons" - }, - "concurrent_downloads_enabled": false, - "cdn_branch_mirrors": { - "master": "https://gleecbtc.github.io/coins", - "main": "https://gleecbtc.github.io/coins" - } + "concurrent_downloads_enabled": false, + "cdn_branch_mirrors": { + "master": "https://gleecbtc.github.io/coins", + "main": "https://gleecbtc.github.io/coins" } -} \ No newline at end of file + } +} diff --git a/packages/komodo_wallet_build_transformer/lib/komodo_wallet_build_transformer.dart b/packages/komodo_wallet_build_transformer/lib/komodo_wallet_build_transformer.dart new file mode 100644 index 000000000..f089b68de --- /dev/null +++ b/packages/komodo_wallet_build_transformer/lib/komodo_wallet_build_transformer.dart @@ -0,0 +1 @@ +export 'src/util/ide_json_formatter.dart' show formatJsonForIde; diff --git a/packages/komodo_wallet_build_transformer/lib/src/steps/models/coin_assets/coin_build_config.dart b/packages/komodo_wallet_build_transformer/lib/src/steps/models/coin_assets/coin_build_config.dart index 6390344ec..6843df2f5 100644 --- a/packages/komodo_wallet_build_transformer/lib/src/steps/models/coin_assets/coin_build_config.dart +++ b/packages/komodo_wallet_build_transformer/lib/src/steps/models/coin_assets/coin_build_config.dart @@ -6,6 +6,7 @@ import 'dart:io'; import 'package:komodo_wallet_build_transformer/src/steps/github/github_file_downloader.dart'; import 'package:komodo_wallet_build_transformer/src/steps/models/build_config.dart'; +import 'package:komodo_wallet_build_transformer/src/util/ide_json_formatter.dart'; import 'package:path/path.dart' as path; /// Represents the build configuration for fetching coin assets. @@ -99,8 +100,8 @@ class CoinBuildConfig { final Map mappedFolders; /// A map of branch names to CDN mirror URLs. - /// When downloading assets, if the current branch has a CDN mirror configured, - /// it will be used instead of the default content URL. + /// When downloading assets, if the current branch has a CDN mirror + /// configured, it will be used instead of the default content URL. /// This helps avoid rate limiting for commonly used branches. final Map cdnBranchMirrors; @@ -192,13 +193,11 @@ class CoinBuildConfig { final foldersToCreate = [path.dirname(assetPath)]; createFolders(foldersToCreate); - final mergedConfig = - (originalBuildConfig?.toJson() ?? {})..addAll({'coins': toJson()}); + final mergedConfig = (originalBuildConfig?.toJson() ?? {}) + ..addAll({'coins': toJson()}); print('Saving coin assets config to $assetPath'); - const encoder = JsonEncoder.withIndent(' '); - - final data = encoder.convert(mergedConfig); + final data = formatJsonForIde(mergedConfig); await File(assetPath).writeAsString(data, flush: true); } } diff --git a/packages/komodo_wallet_build_transformer/lib/src/util/ide_json_formatter.dart b/packages/komodo_wallet_build_transformer/lib/src/util/ide_json_formatter.dart new file mode 100644 index 000000000..718845350 --- /dev/null +++ b/packages/komodo_wallet_build_transformer/lib/src/util/ide_json_formatter.dart @@ -0,0 +1,177 @@ +import 'dart:convert'; + +const String _defaultIndent = ' '; +const int _defaultMaxLineLength = 80; + +/// Formats JSON using the same whitespace conventions as the IDE formatter: +/// two-space indentation with short primitive arrays kept on one line. +String formatJsonForIde( + Object? json, { + String indent = _defaultIndent, + int maxLineLength = _defaultMaxLineLength, +}) { + final formatter = _IdeJsonFormatter( + indent: indent, + maxLineLength: maxLineLength, + ); + final buffer = StringBuffer(); + + formatter.writeValue(buffer, json, depth: 0, currentLinePrefixLength: 0); + buffer.writeln(); + + return buffer.toString(); +} + +final class _IdeJsonFormatter { + const _IdeJsonFormatter({required this.indent, required this.maxLineLength}); + + final String indent; + final int maxLineLength; + + void writeValue( + StringBuffer buffer, + Object? value, { + required int depth, + required int currentLinePrefixLength, + }) { + final inlineValue = _tryInlineValue( + value, + currentLinePrefixLength: currentLinePrefixLength, + ); + + if (inlineValue != null) { + buffer.write(inlineValue); + return; + } + + if (value is Map) { + _writeObject(buffer, value, depth: depth); + return; + } + + if (value is List) { + _writeList(buffer, value, depth: depth); + return; + } + + buffer.write(jsonEncode(value)); + } + + String? _tryInlineValue( + Object? value, { + required int currentLinePrefixLength, + }) { + if (value is Map) { + return value.isEmpty ? '{}' : null; + } + + if (value is! List) { + return jsonEncode(value); + } + + if (value.isEmpty) { + return '[]'; + } + + final encodedItems = []; + for (final item in value) { + if (item is Map || item is List) { + return null; + } + + encodedItems.add(jsonEncode(item)); + } + + final inlineValue = '[${encodedItems.join(', ')}]'; + if (currentLinePrefixLength + inlineValue.length > maxLineLength) { + return null; + } + + return inlineValue; + } + + void _writeObject( + StringBuffer buffer, + Map value, { + required int depth, + }) { + if (value.isEmpty) { + buffer.write('{}'); + return; + } + + buffer.writeln('{'); + final entries = value.entries.toList(growable: false); + + for (var i = 0; i < entries.length; i++) { + final entry = entries[i]; + final key = entry.key; + if (key is! String) { + throw ArgumentError.value( + key, + 'key', + 'JSON object keys must be strings', + ); + } + + final entryIndent = _indentFor(depth + 1); + final encodedKey = jsonEncode(key); + + buffer + ..write(entryIndent) + ..write(encodedKey) + ..write(': '); + + writeValue( + buffer, + entry.value, + depth: depth + 1, + currentLinePrefixLength: entryIndent.length + encodedKey.length + 2, + ); + + if (i < entries.length - 1) { + buffer.write(','); + } + buffer.writeln(); + } + + buffer + ..write(_indentFor(depth)) + ..write('}'); + } + + void _writeList( + StringBuffer buffer, + List value, { + required int depth, + }) { + if (value.isEmpty) { + buffer.write('[]'); + return; + } + + buffer.writeln('['); + + for (var i = 0; i < value.length; i++) { + final itemIndent = _indentFor(depth + 1); + buffer.write(itemIndent); + writeValue( + buffer, + value[i], + depth: depth + 1, + currentLinePrefixLength: itemIndent.length, + ); + + if (i < value.length - 1) { + buffer.write(','); + } + buffer.writeln(); + } + + buffer + ..write(_indentFor(depth)) + ..write(']'); + } + + String _indentFor(int depth) => List.filled(depth, indent).join(); +} diff --git a/packages/komodo_wallet_build_transformer/test/util/ide_json_formatter_test.dart b/packages/komodo_wallet_build_transformer/test/util/ide_json_formatter_test.dart new file mode 100644 index 000000000..53c668456 --- /dev/null +++ b/packages/komodo_wallet_build_transformer/test/util/ide_json_formatter_test.dart @@ -0,0 +1,40 @@ +import 'package:komodo_wallet_build_transformer/src/util/ide_json_formatter.dart'; +import 'package:test/test.dart'; + +void main() { + group('formatJsonForIde', () { + test('keeps short primitive arrays inline and wraps long arrays', () { + const checksum = + '6c09130fa7e4977dff617df5d5be385c1f2a57e6764a63e12e79797df52568f4'; + + final formatted = formatJsonForIde({ + 'api': { + 'platforms': { + 'macos': { + 'matching_preference': ['universal2', 'mac-arm64'], + 'valid_zip_sha256_checksums': [checksum], + }, + }, + }, + }); + + expect( + formatted, + equals(''' +{ + "api": { + "platforms": { + "macos": { + "matching_preference": ["universal2", "mac-arm64"], + "valid_zip_sha256_checksums": [ + "$checksum" + ] + } + } + } +} +'''), + ); + }); + }); +} diff --git a/packages/komodo_wallet_cli/bin/update_api_config.dart b/packages/komodo_wallet_cli/bin/update_api_config.dart index d8ba86ea1..693141556 100644 --- a/packages/komodo_wallet_cli/bin/update_api_config.dart +++ b/packages/komodo_wallet_cli/bin/update_api_config.dart @@ -7,6 +7,7 @@ import 'package:args/args.dart'; import 'package:crypto/crypto.dart'; import 'package:html/parser.dart' as parser; import 'package:http/http.dart' as http; +import 'package:komodo_wallet_build_transformer/komodo_wallet_build_transformer.dart'; import 'package:logging/logging.dart'; import 'package:path/path.dart' as path; @@ -794,8 +795,7 @@ class KdfFetcher { // Write config back to disk final configFile = File(configPath); - const encoder = JsonEncoder.withIndent(' '); - await configFile.writeAsString(encoder.convert(config)); + await configFile.writeAsString(formatJsonForIde(config)); log.info( 'Updated build config with commit hash: $commitHash${currentBranch != branch ? ' and branch: $branch' : ''}', diff --git a/packages/komodo_wallet_cli/pubspec.yaml b/packages/komodo_wallet_cli/pubspec.yaml index c991594e4..b4b075d64 100644 --- a/packages/komodo_wallet_cli/pubspec.yaml +++ b/packages/komodo_wallet_cli/pubspec.yaml @@ -25,6 +25,7 @@ dependencies: crypto: ^3.0.3 html: ^0.15.4 http: ^1.4.0 + komodo_wallet_build_transformer: ^0.4.0 logging: ^1.3.0 path: ^1.9.1 yaml: ^3.1.3 @@ -60,4 +61,4 @@ assets: executables: flutter_upgrade_nested: - update_api_config: \ No newline at end of file + update_api_config: From 95328700e383ca7d47e1ea0a47b478c64e58eb41 Mon Sep 17 00:00:00 2001 From: Nitride <77973576+CharlVS@users.noreply.github.com> Date: Sat, 21 Mar 2026 18:33:30 +0100 Subject: [PATCH 14/40] fix(auth): add mutex-protected atomic metadata updates (#328) Add a dedicated metadata mutex to serialise read-modify-write cycles on user metadata, preventing concurrent coin activations from overwriting each other's state. Introduces updateActiveUserMetadataKey for atomic single-key transforms and migrates setOrRemoveActiveUserKeyValue to use it. Also updates bundled coins config to master branch (aceacfb). --- .../app_build/build_config.json | 4 +- .../lib/src/auth/auth_service.dart | 61 +++++++++++++++---- .../lib/src/komodo_defi_local_auth.dart | 26 +++++--- .../lib/src/trezor/trezor_auth_service.dart | 7 +++ 4 files changed, 74 insertions(+), 24 deletions(-) diff --git a/packages/komodo_defi_framework/app_build/build_config.json b/packages/komodo_defi_framework/app_build/build_config.json index 9ce619231..d8184253d 100644 --- a/packages/komodo_defi_framework/app_build/build_config.json +++ b/packages/komodo_defi_framework/app_build/build_config.json @@ -65,10 +65,10 @@ "coins": { "fetch_at_build_enabled": true, "update_commit_on_build": true, - "bundled_coins_repo_commit": "8fab732731df59a2c1ccc694d565e8535cd51c93", + "bundled_coins_repo_commit": "aceacfb4996a9bda784b0dfda588e24bba3fa9f8", "coins_repo_api_url": "https://api.github.com/repos/GLEECBTC/coins", "coins_repo_content_url": "https://raw.githubusercontent.com/GLEECBTC/coins", - "coins_repo_branch": "feat/add-tron-coins", + "coins_repo_branch": "master", "runtime_updates_enabled": true, "mapped_files": { "assets/config/coins_config.json": "utils/coins_config_unfiltered.json", diff --git a/packages/komodo_defi_local_auth/lib/src/auth/auth_service.dart b/packages/komodo_defi_local_auth/lib/src/auth/auth_service.dart index 07b64bb1e..bef499912 100644 --- a/packages/komodo_defi_local_auth/lib/src/auth/auth_service.dart +++ b/packages/komodo_defi_local_auth/lib/src/auth/auth_service.dart @@ -94,6 +94,16 @@ abstract interface class IAuthService { /// is fully integrated with KW. This may be deprecated in the future. Future setActiveUserMetadata(JsonMap metadata); + /// Atomically reads the current value of [key] from the active user's + /// metadata, applies [transform] to it, and writes the result back. + /// + /// This is safe to call concurrently — a dedicated metadata mutex + /// serialises all read-modify-write cycles. + Future updateActiveUserMetadataKey( + String key, + dynamic Function(dynamic currentValue) transform, + ); + /// Attempts to restore a user session without requiring password authentication /// Only works if the KDF API is running and the wallet exists Future restoreSession(KdfUser user); @@ -123,6 +133,7 @@ class KdfAuthService implements IAuthService { StreamController.broadcast(); final SecureLocalStorage _secureStorage = SecureLocalStorage(); final ReadWriteMutex _authMutex = ReadWriteMutex(); + final Mutex _metadataMutex = Mutex(); final Logger _logger = Logger('KdfAuthService'); final String _sessionId; @@ -697,21 +708,45 @@ class KdfAuthService implements IAuthService { @override Future setActiveUserMetadata(Map metadata) async { - final activeUser = await _activeUserOrThrow(); - // TODO: Implement locks for this to avoid this method interfering with - // more sensitive operations. - final user = await _secureStorage.getUser(activeUser.walletId.name); - if (user == null) throw AuthException.notFound(); + await _metadataMutex.protect(() async { + final activeUser = await _activeUserOrThrow(); + final user = await _secureStorage.getUser(activeUser.walletId.name); + if (user == null) throw AuthException.notFound(); + + final updatedUser = user.copyWith(metadata: metadata); + await _secureStorage.saveUser(updatedUser); + + // Update cache silently without triggering auth state change. Updating + // the storage and cache at the same time emulates the same behaviour as + // before. Update user metadata for any subsequent access without emitting + // auth state changes, as the metadata field is currently used for events + // like coin activation, wallet type (derivation), and seed backup status + _lastEmittedUser = updatedUser; + }); + } - final updatedUser = user.copyWith(metadata: metadata); - await _secureStorage.saveUser(updatedUser); + @override + Future updateActiveUserMetadataKey( + String key, + dynamic Function(dynamic currentValue) transform, + ) async { + await _metadataMutex.protect(() async { + final activeUser = await _activeUserOrThrow(); + final user = await _secureStorage.getUser(activeUser.walletId.name); + if (user == null) throw AuthException.notFound(); + + final metadata = JsonMap.from(user.metadata); + final transformed = transform(metadata[key]); + if (transformed == null) { + metadata.remove(key); + } else { + metadata[key] = transformed; + } - // Update cache silently without triggering auth state change. Updating the - // storage and cache at the same time emulates the same behaviour as before. - // Update user metadata for any subsequent access without emitting auth - // state changes, as the metadata field is currently used for events like - // coin activation, wallet type (derivation), and seed backup status - _lastEmittedUser = updatedUser; + final updatedUser = user.copyWith(metadata: metadata); + await _secureStorage.saveUser(updatedUser); + _lastEmittedUser = updatedUser; + }); } @override diff --git a/packages/komodo_defi_local_auth/lib/src/komodo_defi_local_auth.dart b/packages/komodo_defi_local_auth/lib/src/komodo_defi_local_auth.dart index a637be0aa..df9392c98 100644 --- a/packages/komodo_defi_local_auth/lib/src/komodo_defi_local_auth.dart +++ b/packages/komodo_defi_local_auth/lib/src/komodo_defi_local_auth.dart @@ -6,7 +6,6 @@ import 'package:komodo_defi_local_auth/src/auth/auth_state.dart'; import 'package:komodo_defi_local_auth/src/auth/storage/secure_storage.dart'; import 'package:komodo_defi_local_auth/src/trezor/_trezor_index.dart'; import 'package:komodo_defi_rpc_methods/komodo_defi_rpc_methods.dart'; -import 'package:komodo_defi_types/komodo_defi_type_utils.dart'; import 'package:komodo_defi_types/komodo_defi_types.dart'; /// The [KomodoDefiAuth] class provides a simplified local authentication @@ -193,6 +192,15 @@ abstract interface class KomodoDefiAuth { Future setOrRemoveActiveUserKeyValue(String key, dynamic value); + /// Atomically reads the current value of [key] from the active user's + /// metadata, applies [transform], and writes the result back. + /// + /// Safe to call concurrently — uses a dedicated metadata mutex internally. + Future updateActiveUserKeyValue( + String key, + dynamic Function(dynamic currentValue) transform, + ); + /// Provides PIN to a Trezor hardware device during authentication. /// /// The [taskId] should be obtained from the authentication state when the @@ -611,15 +619,15 @@ class KomodoDefiLocalAuth implements KomodoDefiAuth { @override Future setOrRemoveActiveUserKeyValue(String key, dynamic value) async { - final activeUser = await _authService.getActiveUser(); - - if (activeUser == null) throw AuthException.notFound(); - - final updatedMetadata = JsonMap.from(activeUser.metadata)..[key] = value; - - if (value == null) updatedMetadata.remove(key); + await _authService.updateActiveUserMetadataKey(key, (_) => value); + } - await _authService.setActiveUserMetadata(updatedMetadata); + @override + Future updateActiveUserKeyValue( + String key, + dynamic Function(dynamic currentValue) transform, + ) async { + await _authService.updateActiveUserMetadataKey(key, transform); } @override diff --git a/packages/komodo_defi_local_auth/lib/src/trezor/trezor_auth_service.dart b/packages/komodo_defi_local_auth/lib/src/trezor/trezor_auth_service.dart index 2fef1f740..eeca492e4 100644 --- a/packages/komodo_defi_local_auth/lib/src/trezor/trezor_auth_service.dart +++ b/packages/komodo_defi_local_auth/lib/src/trezor/trezor_auth_service.dart @@ -104,6 +104,13 @@ class TrezorAuthService implements IAuthService { Future setActiveUserMetadata(JsonMap metadata) => _authService.setActiveUserMetadata(metadata); + @override + Future updateActiveUserMetadataKey( + String key, + dynamic Function(dynamic currentValue) transform, + ) => + _authService.updateActiveUserMetadataKey(key, transform); + @override Future restoreSession(KdfUser user) => _authService.restoreSession(user); From 317719d9ebe790d6f28e7a369ef857bd3d95b530 Mon Sep 17 00:00:00 2001 From: Nitride <77973576+CharlVS@users.noreply.github.com> Date: Mon, 23 Mar 2026 12:32:21 +0100 Subject: [PATCH 15/40] fix(types): use reified generics in JSON traversal for wasm/minified builds (#329) Replace T.toString() parsing for List/Map handling with compile-time type checks so dart2js and dart2wasm (including minification) behave correctly. Add unit tests for value/valueOrNull list and map paths. --- .../lib/src/utils/json_type_utils.dart | 38 ++-- .../test/utils/json_type_utils_test.dart | 186 ++++++++++++++++++ 2 files changed, 203 insertions(+), 21 deletions(-) create mode 100644 packages/komodo_defi_types/test/utils/json_type_utils_test.dart diff --git a/packages/komodo_defi_types/lib/src/utils/json_type_utils.dart b/packages/komodo_defi_types/lib/src/utils/json_type_utils.dart index a19875c15..0bcc5bade 100644 --- a/packages/komodo_defi_types/lib/src/utils/json_type_utils.dart +++ b/packages/komodo_defi_types/lib/src/utils/json_type_utils.dart @@ -5,6 +5,10 @@ import 'package:decimal/decimal.dart'; typedef JsonMap = Map; typedef JsonList = List; +/// Compares reified generic types without relying on [Type.toString()]. +/// Works on VM, dart2js, and dart2wasm (including minified builds). +bool _isSameType() => [] is List && [] is List; + /// Converts a map-like structure to a JSON-compatible [Map]. /// /// This function recursively converts all keys to strings and all nested maps/lists @@ -99,24 +103,16 @@ T? _traverseJson( // Handle various type conversions try { if (value != null) { - // Handle List and other list types - if (T.toString().startsWith('List<') && value is List) { - final genericType = T.toString().substring(5, T.toString().length - 1); - switch (genericType) { - case 'String': - return value.cast() as T; - case 'int': - return value.cast() as T; - case 'double': - return value.cast() as T; - case 'num': - return value.cast() as T; - case 'bool': - return value.cast() as T; - default: - if (genericType == 'JsonMap' || genericType == 'JsonMap') { - return value.cast() as T; - } + // Handle List types using reified generics (no T.toString() parsing) + if (value is List) { + if (_isSameType>()) return value.cast() as T; + if (_isSameType>()) return value.cast() as T; + if (_isSameType>()) return value.cast() as T; + if (_isSameType>()) return value.cast() as T; + if (_isSameType>()) return value.cast() as T; + if (_isSameType>()) return value.cast() as T; + if (_isSameType>()) { + return List.from(value) as T; } } @@ -172,7 +168,7 @@ T? _traverseJson( } // Handle general Map type conversion - if (T != dynamic && value is Map && T.toString().startsWith('Map<')) { + if (T != dynamic && value is Map && _isSameType()) { return _convertMap(value); } @@ -240,9 +236,9 @@ T _convertMap(Map sourceMap) { } }); - if (T is JsonMap || T is JsonMap || T is JsonMap?) { + if (_isSameType()) { return sanitizedMap as T; - } else if ((T is Map) || (T is Map?)) { + } else if (_isSameType>()) { return Map.from(sanitizedMap) as T; } diff --git a/packages/komodo_defi_types/test/utils/json_type_utils_test.dart b/packages/komodo_defi_types/test/utils/json_type_utils_test.dart new file mode 100644 index 000000000..7f343dd84 --- /dev/null +++ b/packages/komodo_defi_types/test/utils/json_type_utils_test.dart @@ -0,0 +1,186 @@ +import 'package:komodo_defi_types/src/utils/json_type_utils.dart'; +import 'package:test/test.dart'; + +void main() { + group('value and valueOrNull with List types', () { + test('value> returns typed list', () { + final json = { + 'tags': ['alpha', 'beta', 'gamma'], + }; + final result = json.value>('tags'); + expect(result, isA>()); + expect(result, ['alpha', 'beta', 'gamma']); + }); + + test('value> returns typed list', () { + final json = { + 'counts': [1, 2, 3], + }; + final result = json.value>('counts'); + expect(result, isA>()); + expect(result, [1, 2, 3]); + }); + + test('value> returns typed list', () { + final json = { + 'prices': [1.5, 2.5, 3.5], + }; + final result = json.value>('prices'); + expect(result, isA>()); + expect(result, [1.5, 2.5, 3.5]); + }); + + test('value> returns typed list', () { + final json = { + 'values': [1, 2.5, 3], + }; + final result = json.value>('values'); + expect(result, isA>()); + expect(result, [1, 2.5, 3]); + }); + + test('value> returns typed list', () { + final json = { + 'flags': [true, false, true], + }; + final result = json.value>('flags'); + expect(result, isA>()); + expect(result, [true, false, true]); + }); + + test('value> returns typed list', () { + final json = { + 'items': [ + {'name': 'a'}, + {'name': 'b'}, + ], + }; + final result = json.value>>('items'); + expect(result, isA>>()); + expect(result.length, 2); + expect(result[0]['name'], 'a'); + }); + + test('value> returns typed list', () { + final json = { + 'mixed': [1, 'two', true], + }; + final result = json.value>('mixed'); + expect(result, isA>()); + expect(result, [1, 'two', true]); + }); + + test('valueOrNull> returns null for missing key', () { + final json = {'other': 'value'}; + final result = json.valueOrNull>('tags'); + expect(result, isNull); + }); + + test('valueOrNull> returns list for existing key', () { + final json = { + 'tags': ['a', 'b'], + }; + final result = json.valueOrNull>('tags'); + expect(result, isA>()); + expect(result, ['a', 'b']); + }); + + test('valueOrNull> returns null for null value', () { + final json = {'tags': null}; + final result = json.valueOrNull>('tags'); + expect(result, isNull); + }); + }); + + group('value and valueOrNull with Map types', () { + test('value returns nested map', () { + final json = { + 'config': {'key': 'val'}, + }; + final result = json.value>('config'); + expect(result, isA>()); + expect(result['key'], 'val'); + }); + + test('valueOrNull returns null for missing key', () { + final json = {'other': 1}; + final result = json.valueOrNull>('config'); + expect(result, isNull); + }); + }); + + group('value with primitive type conversions', () { + test('int to String conversion', () { + final json = {'count': 42}; + final result = json.value('count'); + expect(result, '42'); + }); + + test('bool from int (0 = false, 1 = true)', () { + final json = {'flag': 1}; + final result = json.value('flag'); + expect(result, true); + + final json2 = {'flag': 0}; + final result2 = json2.value('flag'); + expect(result2, false); + }); + + test('int from num normalization', () { + final json = {'count': 42.0}; + final result = json.value('count'); + expect(result, 42); + }); + + test('double from num normalization', () { + final json = {'price': 42}; + final result = json.value('price'); + expect(result, 42.0); + }); + + test('String value passthrough', () { + final json = {'name': 'hello'}; + final result = json.value('name'); + expect(result, 'hello'); + }); + }); + + group('nested key traversal', () { + test('value with nested keys', () { + final json = { + 'outer': { + 'inner': 'deep', + }, + }; + final result = json.value('outer', 'inner'); + expect(result, 'deep'); + }); + + test('valueOrNull with missing nested key returns null', () { + final json = { + 'outer': {'other': 'value'}, + }; + final result = json.valueOrNull('outer', 'inner'); + expect(result, isNull); + }); + }); + + group('tryGetStringList', () { + test('returns list for valid string list', () { + final json = { + 'items': ['a', 'b'], + }; + expect(json.tryGetStringList('items'), ['a', 'b']); + }); + + test('returns null for missing key', () { + final json = {'other': 1}; + expect(json.tryGetStringList('items'), isNull); + }); + + test('returns null for non-list value', () { + final json = {'items': 'not a list'}; + expect(json.tryGetStringList('items'), isNull); + }); + }); +} From c70beedf2c00dbc19d7e6b214a50b2f3bbc017e0 Mon Sep 17 00:00:00 2001 From: Nitride <77973576+CharlVS@users.noreply.github.com> Date: Mon, 23 Mar 2026 13:17:14 +0100 Subject: [PATCH 16/40] fix(test): add missing updateActiveUserMetadataKey to fake auth service (#330) The IAuthService interface gained updateActiveUserMetadataKey in #328 but the _FakeAuthService in the Trezor auth tests was not updated, causing a compilation error. --- .../test/src/trezor/trezor_auth_service_test.dart | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/packages/komodo_defi_local_auth/test/src/trezor/trezor_auth_service_test.dart b/packages/komodo_defi_local_auth/test/src/trezor/trezor_auth_service_test.dart index 4cd461aa5..a760d000e 100644 --- a/packages/komodo_defi_local_auth/test/src/trezor/trezor_auth_service_test.dart +++ b/packages/komodo_defi_local_auth/test/src/trezor/trezor_auth_service_test.dart @@ -195,6 +195,13 @@ class _FakeAuthService implements IAuthService { Future setActiveUserMetadata(JsonMap metadata) async => throw UnimplementedError(); + @override + Future updateActiveUserMetadataKey( + String key, + dynamic Function(dynamic currentValue) transform, + ) async => + throw UnimplementedError(); + @override Future updatePassword({ required String currentPassword, From 869dd00fee6fb3c073041810a602c97ea786966e Mon Sep 17 00:00:00 2001 From: Nitride <77973576+CharlVS@users.noreply.github.com> Date: Mon, 23 Mar 2026 13:43:36 +0100 Subject: [PATCH 17/40] chore(framework): bump bundled coins repo commit (#331) Update bundled_coins_repo_commit to 32a0cdc2e9ab5927a571cb1c85fc4be080567e70. --- packages/komodo_defi_framework/app_build/build_config.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/komodo_defi_framework/app_build/build_config.json b/packages/komodo_defi_framework/app_build/build_config.json index d8184253d..7aadd36d1 100644 --- a/packages/komodo_defi_framework/app_build/build_config.json +++ b/packages/komodo_defi_framework/app_build/build_config.json @@ -65,7 +65,7 @@ "coins": { "fetch_at_build_enabled": true, "update_commit_on_build": true, - "bundled_coins_repo_commit": "aceacfb4996a9bda784b0dfda588e24bba3fa9f8", + "bundled_coins_repo_commit": "32a0cdc2e9ab5927a571cb1c85fc4be080567e70", "coins_repo_api_url": "https://api.github.com/repos/GLEECBTC/coins", "coins_repo_content_url": "https://raw.githubusercontent.com/GLEECBTC/coins", "coins_repo_branch": "master", From b4ad5c0434cf32823591eb146818e7c2cbaa3cb5 Mon Sep 17 00:00:00 2001 From: Nitride <77973576+CharlVS@users.noreply.github.com> Date: Mon, 23 Mar 2026 14:16:43 +0100 Subject: [PATCH 18/40] fix(streaming): gate enable_* calls on real SSE first-byte event (#332) The streaming manager previously fell through to enable_* calls after a short 2-second timeout even when no first byte had been received from the SSE / SharedWorker connection. KDF responds with UnknownClient when the client is not yet registered, causing all three retry attempts to fail and leaving orderbook (and other) streams permanently broken for the session. Changes: - Increase the first-byte wait to 30s per attempt (3 attempts with reconnect between each) so we never call enable_* before KDF has actually registered the SSE client. - Abort with an exception instead of proceeding without readiness; the fallback RPC + polling path in OrderbookBloc handles this gracefully. - On UnknownClient retries, re-run the full first-byte readiness gate instead of a fixed 3s sleep. - Always reset the first-byte Completer on disconnect(), even if it was never completed, so subsequent sessions don't block on a stale future. --- .../streaming/event_streaming_service.dart | 8 +- .../src/streaming/events/orderbook_event.dart | 13 +- .../streaming/event_streaming_manager.dart | 130 ++++++++++-------- 3 files changed, 85 insertions(+), 66 deletions(-) diff --git a/packages/komodo_defi_framework/lib/src/streaming/event_streaming_service.dart b/packages/komodo_defi_framework/lib/src/streaming/event_streaming_service.dart index b7ca910e7..5b84d3f80 100644 --- a/packages/komodo_defi_framework/lib/src/streaming/event_streaming_service.dart +++ b/packages/komodo_defi_framework/lib/src/streaming/event_streaming_service.dart @@ -84,10 +84,10 @@ class KdfEventStreamingService { _unsubscribe = null; _connectionState = SseConnectionState.disconnected; - // Reset first byte completer for next connection - if (_firstByteCompleter.isCompleted) { - _firstByteCompleter = Completer(); - } + // Always use a fresh completer so a new session can await first byte. + // If the previous connection never delivered a byte, the old completer + // would otherwise never complete and block readiness forever. + _firstByteCompleter = Completer(); } void _onFirstByte() { diff --git a/packages/komodo_defi_framework/lib/src/streaming/events/orderbook_event.dart b/packages/komodo_defi_framework/lib/src/streaming/events/orderbook_event.dart index 1ca5beee6..631e14d3b 100644 --- a/packages/komodo_defi_framework/lib/src/streaming/events/orderbook_event.dart +++ b/packages/komodo_defi_framework/lib/src/streaming/events/orderbook_event.dart @@ -13,12 +13,12 @@ class OrderbookEvent extends KdfEvent { EventTypeString get typeEnum => EventTypeString.orderbook; factory OrderbookEvent.fromJson(JsonMap json) { - final asks = (json.value>('asks')) - .map((e) => _parseOrderbookEntry(e as JsonMap)) - .toList(); - final bids = (json.value>('bids')) - .map((e) => _parseOrderbookEntry(e as JsonMap)) - .toList(); + final asks = (json.value>( + 'asks', + )).map((e) => _parseOrderbookEntry(e as JsonMap)).toList(); + final bids = (json.value>( + 'bids', + )).map((e) => _parseOrderbookEntry(e as JsonMap)).toList(); return OrderbookEvent( base: json.value('base'), @@ -56,4 +56,3 @@ class OrderbookEvent extends KdfEvent { String toString() => 'OrderbookEvent(base: $base, rel: $rel, asks: ${asks.length}, bids: ${bids.length})'; } - diff --git a/packages/komodo_defi_sdk/lib/src/streaming/event_streaming_manager.dart b/packages/komodo_defi_sdk/lib/src/streaming/event_streaming_manager.dart index 16349047e..a856db7cf 100644 --- a/packages/komodo_defi_sdk/lib/src/streaming/event_streaming_manager.dart +++ b/packages/komodo_defi_sdk/lib/src/streaming/event_streaming_manager.dart @@ -54,9 +54,14 @@ class EventStreamingManager { // Post-connect delay before first enable_* call (1500ms for robust readiness) static const Duration _postConnectDelay = Duration(milliseconds: 1500); - - // Maximum time to wait for first byte before falling back to time-based delay - static const Duration _firstByteTimeout = Duration(seconds: 2); + + /// Per-attempt wait for the first real event from SSE / SharedWorker (KDF + /// client registration). Short timeouts caused enable_* before registration. + static const Duration _firstByteTimeout = Duration(seconds: 30); + + static const int _firstByteMaxAttempts = 3; + + static const Duration _firstByteReconnectDelay = Duration(seconds: 1); // Per-key in-flight guards to prevent duplicate enable_* calls final Map>> _inFlightEnables = {}; @@ -64,41 +69,50 @@ class EventStreamingManager { /// Wait for SSE readiness: first byte received + grace period elapsed Future _waitForSseReadiness() async { if (_sseReadinessComplete) return; - - _log('Waiting for SSE readiness (first byte + ${_postConnectDelay.inMilliseconds}ms grace period)...'); - - // Ensure SSE connection is initiated - _eventService.connectIfNeeded(); - - // Wait for first byte with timeout - bool firstByteReceived = false; - try { - await _eventService.firstByteReceived.timeout( - _firstByteTimeout, - onTimeout: () { - _log('First byte timeout after ${_firstByteTimeout.inSeconds}s - SSE may not be connected'); - throw TimeoutException('First byte not received'); - }, - ); - firstByteReceived = true; - _log('First byte received from SSE stream'); - } on TimeoutException { - _log('WARNING: Proceeding without first byte confirmation - enable_* calls may fail'); - } catch (e) { - _log('Error waiting for first byte: $e'); - } - - // Additional grace period after first byte (or timeout) - if (firstByteReceived) { - _log('Waiting ${_postConnectDelay.inMilliseconds}ms grace period...'); - await Future.delayed(_postConnectDelay); - } else { - // If first byte wasn't received, wait longer to give SSE more time - final fallbackDelay = Duration(milliseconds: _postConnectDelay.inMilliseconds * 2); - _log('Waiting ${fallbackDelay.inMilliseconds}ms fallback delay (no first byte)...'); - await Future.delayed(fallbackDelay); + + _log( + 'Waiting for SSE readiness (first real event + ' + '${_postConnectDelay.inMilliseconds}ms grace period)...', + ); + + for (var attempt = 1; attempt <= _firstByteMaxAttempts; attempt++) { + _eventService.connectIfNeeded(); + + try { + await _eventService.firstByteReceived.timeout( + _firstByteTimeout, + onTimeout: () { + _log( + 'First byte timeout after ${_firstByteTimeout.inSeconds}s ' + '(attempt $attempt/$_firstByteMaxAttempts)', + ); + throw TimeoutException('First byte not received'); + }, + ); + _log('First byte received from SSE stream'); + break; + } on TimeoutException { + if (attempt >= _firstByteMaxAttempts) { + _log( + 'SSE first byte not received after $_firstByteMaxAttempts attempts; ' + 'aborting enable_* (avoid UnknownClient)', + ); + rethrow; + } + _log('Reconnecting SSE before next first-byte wait...'); + _eventService.disconnect(); + await Future.delayed(_firstByteReconnectDelay); + } catch (e) { + _log('Error waiting for first byte: $e'); + if (attempt >= _firstByteMaxAttempts) rethrow; + _eventService.disconnect(); + await Future.delayed(_firstByteReconnectDelay); + } } - + + _log('Waiting ${_postConnectDelay.inMilliseconds}ms grace period...'); + await Future.delayed(_postConnectDelay); + _sseReadinessComplete = true; _log('SSE readiness complete - ready for enable_* calls'); } @@ -161,7 +175,9 @@ class EventStreamingManager { // Log enable_* attempt with details final coinInfo = coin != null ? ', coin=$coin' : ''; - _log('Enable stream attempt: type=$streamType, key=$key, client_id=$_defaultClientId$coinInfo'); + _log( + 'Enable stream attempt: type=$streamType, key=$key, client_id=$_defaultClientId$coinInfo', + ); int attemptCount = 0; const maxAttempts = 3; @@ -170,7 +186,7 @@ class EventStreamingManager { while (attemptCount < maxAttempts) { try { attemptCount++; - + // Enable new stream final response = await enableStream(); @@ -181,17 +197,22 @@ class EventStreamingManager { ); _incrementRefCount(key); - _log('Enable stream success: type=$streamType, key=$key, streamer_id=$streamerId'); + _log( + 'Enable stream success: type=$streamType, key=$key, streamer_id=$streamerId', + ); return _createTypedSubscription(key, eventStream); - } catch (e) { - _log('Enable stream failed (attempt $attemptCount/$maxAttempts): type=$streamType, error=$e'); - + _log( + 'Enable stream failed (attempt $attemptCount/$maxAttempts): type=$streamType, error=$e', + ); + // Check if it's an UnknownClient error final errorStr = e.toString(); if (errorStr.contains('UnknownClient')) { - _log('UnknownClient error detected - server forgot client registration'); - + _log( + 'UnknownClient error detected - server forgot client registration', + ); + if (attemptCount < maxAttempts) { // Force full SSE reconnect on UnknownClient regardless of connection state // The client may think it's connected, but KDF has dropped the registration @@ -199,25 +220,24 @@ class EventStreamingManager { _eventService.disconnect(); _sseReadinessComplete = false; // Reset readiness flag _activeStreams.remove(key); // Clear stale stream entry - - // Reconnect SSE - _eventService.connectIfNeeded(); - - // Wait for full connection cycle (preflight + handshake + first byte + grace period) - await Future.delayed(const Duration(seconds: 3)); - + + // Wait for first real event + grace (same as initial connect) + await _waitForSseReadiness(); + _log('Retrying enable_* after SSE reconnection...'); - await Future.delayed(retryDelay); + await Future.delayed(retryDelay); continue; } } - + // Rethrow if max attempts reached or non-UnknownClient error rethrow; } } - throw Exception('Failed to enable stream after $maxAttempts attempts: $key'); + throw Exception( + 'Failed to enable stream after $maxAttempts attempts: $key', + ); } /// Enable balance stream for a specific coin. From 0f5b76d7d1a88a7776970710a336ed1d9e6f69cb Mon Sep 17 00:00:00 2001 From: CharlVS <77973576+CharlVS@users.noreply.github.com> Date: Mon, 23 Mar 2026 14:21:04 +0100 Subject: [PATCH 19/40] chore(release): publish packages - dragon_charts_flutter@0.1.1-dev.4 - dragon_logs@2.0.1 - komodo_cex_market_data@0.1.0 - komodo_coin_updates@2.0.0 - komodo_coins@0.3.2 - komodo_defi_framework@0.4.0 - komodo_defi_local_auth@0.4.0 - komodo_defi_rpc_methods@0.4.0 - komodo_defi_sdk@0.5.0 - komodo_defi_types@0.4.0 - komodo_ui@0.3.1 - komodo_wallet_build_transformer@0.4.1 - komodo_wallet_cli@0.5.0 --- CHANGELOG.md | 156 ++++++++++++++++++ packages/dragon_charts_flutter/CHANGELOG.md | 5 + packages/dragon_charts_flutter/pubspec.yaml | 2 +- packages/dragon_logs/CHANGELOG.md | 5 + packages/dragon_logs/pubspec.yaml | 2 +- packages/komodo_cex_market_data/CHANGELOG.md | 10 ++ packages/komodo_cex_market_data/pubspec.yaml | 4 +- packages/komodo_coin_updates/CHANGELOG.md | 16 ++ packages/komodo_coin_updates/pubspec.yaml | 4 +- packages/komodo_coins/CHANGELOG.md | 6 + packages/komodo_coins/pubspec.yaml | 6 +- packages/komodo_defi_framework/CHANGELOG.md | 20 +++ packages/komodo_defi_framework/pubspec.yaml | 10 +- packages/komodo_defi_local_auth/CHANGELOG.md | 10 ++ packages/komodo_defi_local_auth/pubspec.yaml | 8 +- packages/komodo_defi_rpc_methods/CHANGELOG.md | 11 ++ packages/komodo_defi_rpc_methods/pubspec.yaml | 4 +- packages/komodo_defi_sdk/CHANGELOG.md | 23 +++ packages/komodo_defi_sdk/pubspec.yaml | 16 +- packages/komodo_defi_types/CHANGELOG.md | 18 ++ packages/komodo_defi_types/pubspec.yaml | 4 +- packages/komodo_ui/CHANGELOG.md | 8 + packages/komodo_ui/pubspec.yaml | 4 +- .../CHANGELOG.md | 6 + .../pubspec.yaml | 2 +- packages/komodo_wallet_cli/CHANGELOG.md | 6 + packages/komodo_wallet_cli/pubspec.yaml | 4 +- 27 files changed, 335 insertions(+), 35 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5912d881c..719da16aa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,162 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## 2026-03-23 + +### Changes + +--- + +Packages with breaking changes: + + - [`komodo_cex_market_data` - `v0.1.0`](#komodo_cex_market_data---v010) + - [`komodo_coin_updates` - `v2.0.0`](#komodo_coin_updates---v200) + - [`komodo_defi_framework` - `v0.4.0`](#komodo_defi_framework---v040) + - [`komodo_defi_local_auth` - `v0.4.0`](#komodo_defi_local_auth---v040) + - [`komodo_defi_rpc_methods` - `v0.4.0`](#komodo_defi_rpc_methods---v040) + - [`komodo_defi_sdk` - `v0.5.0`](#komodo_defi_sdk---v050) + - [`komodo_defi_types` - `v0.4.0`](#komodo_defi_types---v040) + - [`komodo_wallet_cli` - `v0.5.0`](#komodo_wallet_cli---v050) + +Packages with other changes: + + - [`dragon_charts_flutter` - `v0.1.1-dev.4`](#dragon_charts_flutter---v011-dev4) + - [`dragon_logs` - `v2.0.1`](#dragon_logs---v201) + - [`komodo_coins` - `v0.3.2`](#komodo_coins---v032) + - [`komodo_ui` - `v0.3.1`](#komodo_ui---v031) + - [`komodo_wallet_build_transformer` - `v0.4.1`](#komodo_wallet_build_transformer---v041) + +--- + +#### `komodo_cex_market_data` - `v0.1.0` + + - **PERF**(logs): reduce market metrics log verbosity and duplication (#223). + - **FIX**(sdk): close balance and pubkeysubscriptions on auth state changes (#232). + - **FIX**(binance): use the per-coin supported quote currency list instead of the global cache (#224). + - **FEAT**(cex-market-data): add CoinPaprika API provider as a fallback option (#215). + - **BREAKING** **FIX**(rpc): minimise RPC usage with comprehensive caching and streaming support (#262). + +#### `komodo_coin_updates` - `v2.0.0` + + - **PERF**(logs): reduce market metrics log verbosity and duplication (#223). + - **FIX**(startup): handle 6133 seed fallback and invalid configs (#318). + - **FIX**(config): loosen types for needs transform check and fix lightwalletservers type. + - **FIX**(config): add ssl-only transform for native platforms. + - **FIX**(sdk): close balance and pubkeysubscriptions on auth state changes (#232). + - **FIX**(zhltc): zhltc activation fixes (#227). + - **FEAT**(sdk): add token safety and fee support helpers (#319). + - **FEAT**(coins): Add TRON and TRC20 support (#316). + - **FEAT**(message-signing): Add AddressPath type and refactor to use Asset/PubkeyInfo (#231). + - **FEAT**(coin-config): add custom token support to coin config manager (#225). + - **BREAKING** **FIX**(rpc): minimise RPC usage with comprehensive caching and streaming support (#262). + +#### `komodo_defi_framework` - `v0.4.0` + + - **REFACTOR**(macos): streamline KDF binary placement; update signing flow (#247). + - **FIX**(streaming): gate enable_* calls on real SSE first-byte event (#332). + - **FIX**(auth): add mutex-protected atomic metadata updates (#328). + - **FIX**(startup): handle 6133 seed fallback and invalid configs (#318). + - **FIX**(web): improve wasm JS interop bindings (#315). + - **FIX**(web): complete wasm-safe sdk interop cleanup (#313). + - **FIX**: re-format build config. + - **FIX**: swap zcash params primary/backup URLs to use official z.cash as primary (#301). + - **FIX**(zhltc): zhltc activation fixes (#227). + - **FIX**(auth): store bip39 compatibility regardless of wallet type (#216). + - **FIX**(komodo_defi_framework): rename transformer marker and update references\n\n- Use assets/transformer_invoker.txt instead of dotfile\n- Update pubspec and READMEs\n- Remove special .gitignore unignore. + - **FEAT**(sdk): add token safety and fee support helpers (#319). + - **FEAT**(sdk): typed error handling, trading streams, and activation refactoring (#312). + - **FEAT**: add support for ETH-BASE and derived assets (#254). + - **BREAKING** **FIX**(rpc): minimise RPC usage with comprehensive caching and streaming support (#262). + +#### `komodo_defi_local_auth` - `v0.4.0` + + - **FIX**(test): add missing updateActiveUserMetadataKey to fake auth service (#330). + - **FIX**(auth): add mutex-protected atomic metadata updates (#328). + - **FIX**(auth): store bip39 compatibility regardless of wallet type (#216). + - **FEAT**(sdk): typed error handling, trading streams, and activation refactoring (#312). + - **BREAKING** **FIX**(rpc): minimise RPC usage with comprehensive caching and streaming support (#262). + +#### `komodo_defi_rpc_methods` - `v0.4.0` + + - **FIX**(sdk): close balance and pubkeysubscriptions on auth state changes (#232). + - **FIX**(zhltc): zhltc activation fixes (#227). + - **FEAT**(coins): Add TRON and TRC20 support (#316). + - **FEAT**(sdk): typed error handling, trading streams, and activation refactoring (#312). + - **FEAT**(message-signing): Add AddressPath type and refactor to use Asset/PubkeyInfo (#231). + - **BREAKING** **FIX**(rpc): minimise RPC usage with comprehensive caching and streaming support (#262). + +#### `komodo_defi_sdk` - `v0.5.0` + + - **FIX**(streaming): gate enable_* calls on real SSE first-byte event (#332). + - **FIX**(withdrawals): remove duplicate executeWithdrawal method (#322). + - **FIX**(sdk): expose custom token cleanup (#321). + - **FIX**: swap zcash params primary/backup URLs to use official z.cash as primary (#301). + - **FIX**(sdk): close balance and pubkeysubscriptions on auth state changes (#232). + - **FIX**(zhltc): zhltc activation fixes (#227). + - **FIX**(custom-token-import): refresh asset list on import and use lowercase for custom token import (#220). + - **FEAT**(sdk): add SIA activation and withdrawal support (#320). + - **FEAT**(sdk): add token safety and fee support helpers (#319). + - **FEAT**(coins): Add TRON and TRC20 support (#316). + - **FEAT**(sdk): add high-level balance/transaction manager interfaces (#314). + - **FEAT**(sdk): typed error handling, trading streams, and activation refactoring (#312). + - **FEAT**(activation): integrate ActivatedAssetsCache to optimize asset activation checks. + - **FEAT**: add support for ETH-BASE and derived assets (#254). + - **FEAT**(message-signing): Add AddressPath type and refactor to use Asset/PubkeyInfo (#231). + - **FEAT**(coin-config): add custom token support to coin config manager (#225). + - **FEAT**(cex-market-data): add CoinPaprika API provider as a fallback option (#215). + - **BREAKING** **FIX**(rpc): minimise RPC usage with comprehensive caching and streaming support (#262). + +#### `komodo_defi_types` - `v0.4.0` + + - **FIX**(types): use reified generics in JSON traversal for wasm/minified builds (#329). + - **FIX**(startup): handle 6133 seed fallback and invalid configs (#318). + - **FIX**(asset-tagging): correct UTXO coins incorrectly tagged as Smart Chain (#244). + - **FIX**(sdk): close balance and pubkeysubscriptions on auth state changes (#232). + - **FIX**(zhltc): zhltc activation fixes (#227). + - **FIX**(custom-token-import): refresh asset list on import and use lowercase for custom token import (#220). + - **FEAT**(sdk): add token safety and fee support helpers (#319). + - **FEAT**(coins): Add TRON and TRC20 support (#316). + - **FEAT**(sdk): typed error handling, trading streams, and activation refactoring (#312). + - **FEAT**: add support for ETH-BASE and derived assets (#254). + - **FEAT**(coin-config): add custom token support to coin config manager (#225). + - **FEAT**(types): parent display name suffix via subclass (#213). + - **BREAKING** **FIX**(rpc): minimise RPC usage with comprehensive caching and streaming support (#262). + +#### `komodo_wallet_cli` - `v0.5.0` + + - **BREAKING** **FIX**(rpc): minimise RPC usage with comprehensive caching and streaming support (#262). + +#### `dragon_charts_flutter` - `v0.1.1-dev.4` + + - **FIX**(zhltc): zhltc activation fixes (#227). + - **FEAT**: add configurable sparkline baseline (#248). + +#### `dragon_logs` - `v2.0.1` + + - **FIX**(web): improve wasm JS interop bindings (#315). + - **FIX**(zhltc): zhltc activation fixes (#227). + +#### `komodo_coins` - `v0.3.2` + + - **PERF**(logs): reduce market metrics log verbosity and duplication (#223). + - **FIX**(zhltc): zhltc activation fixes (#227). + - **FEAT**(coin-config): add custom token support to coin config manager (#225). + +#### `komodo_ui` - `v0.3.1` + + - **FIX**(ui): detect asset icon precache failures (#326). + - **FIX**(zhltc): zhltc activation fixes (#227). + - **FIX**(custom-token-import): refresh asset list on import and use lowercase for custom token import (#220). + - **FEAT**(coins): Add TRON and TRC20 support (#316). + - **FEAT**(sdk): typed error handling, trading streams, and activation refactoring (#312). + +#### `komodo_wallet_build_transformer` - `v0.4.1` + + - **FIX**: swap zcash params primary/backup URLs to use official z.cash as primary (#301). + - **FIX**(komodo_defi_framework): rename transformer marker and update references\n\n- Use assets/transformer_invoker.txt instead of dotfile\n- Update pubspec and READMEs\n- Remove special .gitignore unignore. + - **FEAT**(sdk): typed error handling, trading streams, and activation refactoring (#312). + + ## 2025-08-25 ### Changes diff --git a/packages/dragon_charts_flutter/CHANGELOG.md b/packages/dragon_charts_flutter/CHANGELOG.md index b6928b015..608f38782 100644 --- a/packages/dragon_charts_flutter/CHANGELOG.md +++ b/packages/dragon_charts_flutter/CHANGELOG.md @@ -1,3 +1,8 @@ +## 0.1.1-dev.4 + + - **FIX**(zhltc): zhltc activation fixes (#227). + - **FEAT**: add configurable sparkline baseline (#248). + ## 0.1.1-dev.3 > Note: This release has breaking changes. diff --git a/packages/dragon_charts_flutter/pubspec.yaml b/packages/dragon_charts_flutter/pubspec.yaml index 5fbda8499..a837f6e54 100644 --- a/packages/dragon_charts_flutter/pubspec.yaml +++ b/packages/dragon_charts_flutter/pubspec.yaml @@ -1,6 +1,6 @@ name: dragon_charts_flutter description: A lightweight and highly customizable charting library for Flutter. -version: 0.1.1-dev.3 +version: 0.1.1-dev.4 homepage: https://komodoplatform.com repository: https://github.com/GLEECBTC/komodo-defi-sdk-flutter diff --git a/packages/dragon_logs/CHANGELOG.md b/packages/dragon_logs/CHANGELOG.md index 2e5a963b2..94c43eea4 100644 --- a/packages/dragon_logs/CHANGELOG.md +++ b/packages/dragon_logs/CHANGELOG.md @@ -1,3 +1,8 @@ +## 2.0.1 + + - **FIX**(web): improve wasm JS interop bindings (#315). + - **FIX**(zhltc): zhltc activation fixes (#227). + ## 2.0.0 > Note: This release has breaking changes. diff --git a/packages/dragon_logs/pubspec.yaml b/packages/dragon_logs/pubspec.yaml index 33cb59ff9..5216ee43d 100644 --- a/packages/dragon_logs/pubspec.yaml +++ b/packages/dragon_logs/pubspec.yaml @@ -1,6 +1,6 @@ name: dragon_logs description: An efficient cross-platform Flutter log storage framework with minimal dependencies. -version: 2.0.0 +version: 2.0.1 repository: https://github.com/GLEECBTC/komodo-defi-sdk-flutter/tree/main/packages/dragon_logs homepage: https://komodoplatform.com diff --git a/packages/komodo_cex_market_data/CHANGELOG.md b/packages/komodo_cex_market_data/CHANGELOG.md index ff208c864..6f1b9ca12 100644 --- a/packages/komodo_cex_market_data/CHANGELOG.md +++ b/packages/komodo_cex_market_data/CHANGELOG.md @@ -1,3 +1,13 @@ +## 0.1.0 + +> Note: This release has breaking changes. + + - **PERF**(logs): reduce market metrics log verbosity and duplication (#223). + - **FIX**(sdk): close balance and pubkeysubscriptions on auth state changes (#232). + - **FIX**(binance): use the per-coin supported quote currency list instead of the global cache (#224). + - **FEAT**(cex-market-data): add CoinPaprika API provider as a fallback option (#215). + - **BREAKING** **FIX**(rpc): minimise RPC usage with comprehensive caching and streaming support (#262). + ## 0.0.3+1 - Update a dependency to the latest release. diff --git a/packages/komodo_cex_market_data/pubspec.yaml b/packages/komodo_cex_market_data/pubspec.yaml index da0a55223..ecc411c92 100644 --- a/packages/komodo_cex_market_data/pubspec.yaml +++ b/packages/komodo_cex_market_data/pubspec.yaml @@ -1,6 +1,6 @@ name: komodo_cex_market_data description: CEX market data repositories and strategies with fallbacks for Komodo SDK apps. -version: 0.0.3+1 +version: 0.1.0 repository: https://github.com/GLEECBTC/komodo-defi-sdk-flutter environment: @@ -17,7 +17,7 @@ dependencies: freezed_annotation: ^3.0.0 json_annotation: ^4.9.0 - komodo_defi_types: ^0.3.2+1 + komodo_defi_types: ^0.4.0 # Approved via https://github.com/GLEECBTC/gleec-wallet/pull/1106 hive_ce: ^2.2.3+ce # Changed from hive to hive_ce for Hive CE compatibility diff --git a/packages/komodo_coin_updates/CHANGELOG.md b/packages/komodo_coin_updates/CHANGELOG.md index 02fac4283..3df80de26 100644 --- a/packages/komodo_coin_updates/CHANGELOG.md +++ b/packages/komodo_coin_updates/CHANGELOG.md @@ -1,3 +1,19 @@ +## 2.0.0 + +> Note: This release has breaking changes. + + - **PERF**(logs): reduce market metrics log verbosity and duplication (#223). + - **FIX**(startup): handle 6133 seed fallback and invalid configs (#318). + - **FIX**(config): loosen types for needs transform check and fix lightwalletservers type. + - **FIX**(config): add ssl-only transform for native platforms. + - **FIX**(sdk): close balance and pubkeysubscriptions on auth state changes (#232). + - **FIX**(zhltc): zhltc activation fixes (#227). + - **FEAT**(sdk): add token safety and fee support helpers (#319). + - **FEAT**(coins): Add TRON and TRC20 support (#316). + - **FEAT**(message-signing): Add AddressPath type and refactor to use Asset/PubkeyInfo (#231). + - **FEAT**(coin-config): add custom token support to coin config manager (#225). + - **BREAKING** **FIX**(rpc): minimise RPC usage with comprehensive caching and streaming support (#262). + ## 1.1.1 - Update a dependency to the latest release. diff --git a/packages/komodo_coin_updates/pubspec.yaml b/packages/komodo_coin_updates/pubspec.yaml index a24fc776f..0c7f89558 100644 --- a/packages/komodo_coin_updates/pubspec.yaml +++ b/packages/komodo_coin_updates/pubspec.yaml @@ -1,6 +1,6 @@ name: komodo_coin_updates description: Runtime coin config update coin updates. -version: 1.1.1 +version: 2.0.0 repository: https://github.com/GLEECBTC/komodo-defi-sdk-flutter environment: @@ -21,7 +21,7 @@ dependencies: hive_ce_flutter: ^2.2.3+ce http: ^1.4.0 json_annotation: ^4.9.0 - komodo_defi_types: ^0.3.2+1 + komodo_defi_types: ^0.4.0 logging: ^1.3.0 dev_dependencies: diff --git a/packages/komodo_coins/CHANGELOG.md b/packages/komodo_coins/CHANGELOG.md index 41054ee2f..241a53a1b 100644 --- a/packages/komodo_coins/CHANGELOG.md +++ b/packages/komodo_coins/CHANGELOG.md @@ -1,3 +1,9 @@ +## 0.3.2 + + - **PERF**(logs): reduce market metrics log verbosity and duplication (#223). + - **FIX**(zhltc): zhltc activation fixes (#227). + - **FEAT**(coin-config): add custom token support to coin config manager (#225). + ## 0.3.1+2 - Update a dependency to the latest release. diff --git a/packages/komodo_coins/pubspec.yaml b/packages/komodo_coins/pubspec.yaml index c03085f63..e0f06b4cb 100644 --- a/packages/komodo_coins/pubspec.yaml +++ b/packages/komodo_coins/pubspec.yaml @@ -1,6 +1,6 @@ name: komodo_coins description: "A package for fetching managing Komodo Platform coin configuration data storage, runtime updates, and queries." -version: 0.3.1+2 +version: 0.3.2 homepage: "https://komodoplatform.com" repository: "https://github.com/GLEECBTC/komodo-defi-sdk-flutter" @@ -16,8 +16,8 @@ dependencies: sdk: flutter hive_ce: ^2.11.3 http: ^1.4.0 - komodo_coin_updates: ^1.1.1 - komodo_defi_types: ^0.3.2+1 + komodo_coin_updates: ^2.0.0 + komodo_defi_types: ^0.4.0 logging: ^1.3.0 path: ^1.9.1 path_provider: ^2.1.5 diff --git a/packages/komodo_defi_framework/CHANGELOG.md b/packages/komodo_defi_framework/CHANGELOG.md index 0907020ce..2ad2ee499 100644 --- a/packages/komodo_defi_framework/CHANGELOG.md +++ b/packages/komodo_defi_framework/CHANGELOG.md @@ -1,3 +1,23 @@ +## 0.4.0 + +> Note: This release has breaking changes. + + - **REFACTOR**(macos): streamline KDF binary placement; update signing flow (#247). + - **FIX**(streaming): gate enable_* calls on real SSE first-byte event (#332). + - **FIX**(auth): add mutex-protected atomic metadata updates (#328). + - **FIX**(startup): handle 6133 seed fallback and invalid configs (#318). + - **FIX**(web): improve wasm JS interop bindings (#315). + - **FIX**(web): complete wasm-safe sdk interop cleanup (#313). + - **FIX**: re-format build config. + - **FIX**: swap zcash params primary/backup URLs to use official z.cash as primary (#301). + - **FIX**(zhltc): zhltc activation fixes (#227). + - **FIX**(auth): store bip39 compatibility regardless of wallet type (#216). + - **FIX**(komodo_defi_framework): rename transformer marker and update references\n\n- Use assets/transformer_invoker.txt instead of dotfile\n- Update pubspec and READMEs\n- Remove special .gitignore unignore. + - **FEAT**(sdk): add token safety and fee support helpers (#319). + - **FEAT**(sdk): typed error handling, trading streams, and activation refactoring (#312). + - **FEAT**: add support for ETH-BASE and derived assets (#254). + - **BREAKING** **FIX**(rpc): minimise RPC usage with comprehensive caching and streaming support (#262). + ## 0.3.1+2 - Update a dependency to the latest release. diff --git a/packages/komodo_defi_framework/pubspec.yaml b/packages/komodo_defi_framework/pubspec.yaml index e506ae75f..4a74984b2 100644 --- a/packages/komodo_defi_framework/pubspec.yaml +++ b/packages/komodo_defi_framework/pubspec.yaml @@ -1,7 +1,7 @@ name: komodo_defi_framework description: "A Flutter plugin for the Komodo DeFi Framework, supporting both native (FFI) and web (WASM) platforms." -version: 0.3.1+2 +version: 0.4.0 homepage: https://komodoplatform.com repository: https://github.com/GLEECBTC/komodo-defi-sdk-flutter @@ -23,10 +23,10 @@ dependencies: flutter_web_plugins: sdk: flutter http: ^1.4.0 - komodo_coin_updates: ^1.1.1 - komodo_coins: ^0.3.1+2 - komodo_defi_types: ^0.3.2+1 - komodo_wallet_build_transformer: ^0.4.0 + komodo_coin_updates: ^2.0.0 + komodo_coins: ^0.3.2 + komodo_defi_types: ^0.4.0 + komodo_wallet_build_transformer: ^0.4.1 logging: ^1.3.0 mutex: ^3.1.0 path: ^1.9.1 diff --git a/packages/komodo_defi_local_auth/CHANGELOG.md b/packages/komodo_defi_local_auth/CHANGELOG.md index be156d75d..d7026567b 100644 --- a/packages/komodo_defi_local_auth/CHANGELOG.md +++ b/packages/komodo_defi_local_auth/CHANGELOG.md @@ -1,3 +1,13 @@ +## 0.4.0 + +> Note: This release has breaking changes. + + - **FIX**(test): add missing updateActiveUserMetadataKey to fake auth service (#330). + - **FIX**(auth): add mutex-protected atomic metadata updates (#328). + - **FIX**(auth): store bip39 compatibility regardless of wallet type (#216). + - **FEAT**(sdk): typed error handling, trading streams, and activation refactoring (#312). + - **BREAKING** **FIX**(rpc): minimise RPC usage with comprehensive caching and streaming support (#262). + ## 0.3.1+2 - Update a dependency to the latest release. diff --git a/packages/komodo_defi_local_auth/pubspec.yaml b/packages/komodo_defi_local_auth/pubspec.yaml index 6f569918f..8f4f7216b 100644 --- a/packages/komodo_defi_local_auth/pubspec.yaml +++ b/packages/komodo_defi_local_auth/pubspec.yaml @@ -1,7 +1,7 @@ name: komodo_defi_local_auth description: A package responsible for managing and abstracting out an authentication service on top of the API's methods -version: 0.3.1+2 +version: 0.4.0 repository: https://github.com/GLEECBTC/komodo-defi-sdk-flutter environment: @@ -17,9 +17,9 @@ dependencies: flutter_secure_storage: ^10.0.0-beta.4 freezed_annotation: ^3.0.0 - komodo_defi_framework: ^0.3.1+2 - komodo_defi_rpc_methods: ^0.3.1+1 - komodo_defi_types: ^0.3.2+1 + komodo_defi_framework: ^0.4.0 + komodo_defi_rpc_methods: ^0.4.0 + komodo_defi_types: ^0.4.0 local_auth: ^2.3.0 logging: ^1.3.0 diff --git a/packages/komodo_defi_rpc_methods/CHANGELOG.md b/packages/komodo_defi_rpc_methods/CHANGELOG.md index ae2424c65..d0a05cd70 100644 --- a/packages/komodo_defi_rpc_methods/CHANGELOG.md +++ b/packages/komodo_defi_rpc_methods/CHANGELOG.md @@ -1,3 +1,14 @@ +## 0.4.0 + +> Note: This release has breaking changes. + + - **FIX**(sdk): close balance and pubkeysubscriptions on auth state changes (#232). + - **FIX**(zhltc): zhltc activation fixes (#227). + - **FEAT**(coins): Add TRON and TRC20 support (#316). + - **FEAT**(sdk): typed error handling, trading streams, and activation refactoring (#312). + - **FEAT**(message-signing): Add AddressPath type and refactor to use Asset/PubkeyInfo (#231). + - **BREAKING** **FIX**(rpc): minimise RPC usage with comprehensive caching and streaming support (#262). + ## 0.3.1+1 - Update a dependency to the latest release. diff --git a/packages/komodo_defi_rpc_methods/pubspec.yaml b/packages/komodo_defi_rpc_methods/pubspec.yaml index c3bf2eaaa..1d0c3b34f 100644 --- a/packages/komodo_defi_rpc_methods/pubspec.yaml +++ b/packages/komodo_defi_rpc_methods/pubspec.yaml @@ -1,7 +1,7 @@ name: komodo_defi_rpc_methods description: A package containing the RPC methods and responses for the Komodo DeFi Framework API repository: https://github.com/GLEECBTC/komodo-defi-sdk-flutter -version: 0.3.1+1 +version: 0.4.0 environment: sdk: ">=3.9.0 <4.0.0" @@ -15,7 +15,7 @@ dependencies: equatable: ^2.0.7 freezed_annotation: ^3.0.0 json_annotation: ^4.9.0 - komodo_defi_types: ^0.3.2+1 + komodo_defi_types: ^0.4.0 meta: ^1.15.0 path: ^1.9.1 diff --git a/packages/komodo_defi_sdk/CHANGELOG.md b/packages/komodo_defi_sdk/CHANGELOG.md index d97f7c403..063339d47 100644 --- a/packages/komodo_defi_sdk/CHANGELOG.md +++ b/packages/komodo_defi_sdk/CHANGELOG.md @@ -1,3 +1,26 @@ +## 0.5.0 + +> Note: This release has breaking changes. + + - **FIX**(streaming): gate enable_* calls on real SSE first-byte event (#332). + - **FIX**(withdrawals): remove duplicate executeWithdrawal method (#322). + - **FIX**(sdk): expose custom token cleanup (#321). + - **FIX**: swap zcash params primary/backup URLs to use official z.cash as primary (#301). + - **FIX**(sdk): close balance and pubkeysubscriptions on auth state changes (#232). + - **FIX**(zhltc): zhltc activation fixes (#227). + - **FIX**(custom-token-import): refresh asset list on import and use lowercase for custom token import (#220). + - **FEAT**(sdk): add SIA activation and withdrawal support (#320). + - **FEAT**(sdk): add token safety and fee support helpers (#319). + - **FEAT**(coins): Add TRON and TRC20 support (#316). + - **FEAT**(sdk): add high-level balance/transaction manager interfaces (#314). + - **FEAT**(sdk): typed error handling, trading streams, and activation refactoring (#312). + - **FEAT**(activation): integrate ActivatedAssetsCache to optimize asset activation checks. + - **FEAT**: add support for ETH-BASE and derived assets (#254). + - **FEAT**(message-signing): Add AddressPath type and refactor to use Asset/PubkeyInfo (#231). + - **FEAT**(coin-config): add custom token support to coin config manager (#225). + - **FEAT**(cex-market-data): add CoinPaprika API provider as a fallback option (#215). + - **BREAKING** **FIX**(rpc): minimise RPC usage with comprehensive caching and streaming support (#262). + ## 0.4.0+3 - Update a dependency to the latest release. diff --git a/packages/komodo_defi_sdk/pubspec.yaml b/packages/komodo_defi_sdk/pubspec.yaml index 3221dda85..54efceb9f 100644 --- a/packages/komodo_defi_sdk/pubspec.yaml +++ b/packages/komodo_defi_sdk/pubspec.yaml @@ -3,7 +3,7 @@ description: A high-level opinionated library that provides a simple way to build cross-platform Komodo Defi Framework applications (primarily focused on wallets). This package seves as the entry point for the packages in this repository. -version: 0.4.0+3 +version: 0.5.0 repository: https://github.com/GLEECBTC/komodo-defi-sdk-flutter environment: @@ -26,13 +26,13 @@ dependencies: http: ^1.4.0 json_annotation: ^4.9.0 - komodo_cex_market_data: ^0.0.3+1 - komodo_coins: ^0.3.1+2 - komodo_defi_framework: ^0.3.1+2 - komodo_defi_local_auth: ^0.3.1+2 - komodo_defi_rpc_methods: ^0.3.1+1 - komodo_defi_types: ^0.3.2+1 - komodo_ui: ^0.3.0+3 + komodo_cex_market_data: ^0.1.0 + komodo_coins: ^0.3.2 + komodo_defi_framework: ^0.4.0 + komodo_defi_local_auth: ^0.4.0 + komodo_defi_rpc_methods: ^0.4.0 + komodo_defi_types: ^0.4.0 + komodo_ui: ^0.3.1 logging: ^1.3.0 mutex: ^3.1.0 diff --git a/packages/komodo_defi_types/CHANGELOG.md b/packages/komodo_defi_types/CHANGELOG.md index 897cd6ffe..8cfecbab0 100644 --- a/packages/komodo_defi_types/CHANGELOG.md +++ b/packages/komodo_defi_types/CHANGELOG.md @@ -1,3 +1,21 @@ +## 0.4.0 + +> Note: This release has breaking changes. + + - **FIX**(types): use reified generics in JSON traversal for wasm/minified builds (#329). + - **FIX**(startup): handle 6133 seed fallback and invalid configs (#318). + - **FIX**(asset-tagging): correct UTXO coins incorrectly tagged as Smart Chain (#244). + - **FIX**(sdk): close balance and pubkeysubscriptions on auth state changes (#232). + - **FIX**(zhltc): zhltc activation fixes (#227). + - **FIX**(custom-token-import): refresh asset list on import and use lowercase for custom token import (#220). + - **FEAT**(sdk): add token safety and fee support helpers (#319). + - **FEAT**(coins): Add TRON and TRC20 support (#316). + - **FEAT**(sdk): typed error handling, trading streams, and activation refactoring (#312). + - **FEAT**: add support for ETH-BASE and derived assets (#254). + - **FEAT**(coin-config): add custom token support to coin config manager (#225). + - **FEAT**(types): parent display name suffix via subclass (#213). + - **BREAKING** **FIX**(rpc): minimise RPC usage with comprehensive caching and streaming support (#262). + ## 0.3.2+1 - **DOCS**(komodo_defi_types): update CHANGELOG for 0.3.2 with pub submission fix. diff --git a/packages/komodo_defi_types/pubspec.yaml b/packages/komodo_defi_types/pubspec.yaml index 15b4a566b..760d820db 100644 --- a/packages/komodo_defi_types/pubspec.yaml +++ b/packages/komodo_defi_types/pubspec.yaml @@ -1,6 +1,6 @@ name: komodo_defi_types description: Type definitions for Komodo DeFi Framework. -version: 0.3.2+1 +version: 0.4.0 repository: https://github.com/GLEECBTC/komodo-defi-sdk-flutter environment: @@ -19,7 +19,7 @@ dependencies: sdk: flutter freezed_annotation: ^3.0.0 json_annotation: ^4.9.0 - komodo_defi_rpc_methods: ^0.3.1+1 + komodo_defi_rpc_methods: ^0.4.0 logging: ^1.3.0 meta: ^1.15.0 diff --git a/packages/komodo_ui/CHANGELOG.md b/packages/komodo_ui/CHANGELOG.md index e3ac48d4a..7450eb247 100644 --- a/packages/komodo_ui/CHANGELOG.md +++ b/packages/komodo_ui/CHANGELOG.md @@ -1,3 +1,11 @@ +## 0.3.1 + + - **FIX**(ui): detect asset icon precache failures (#326). + - **FIX**(zhltc): zhltc activation fixes (#227). + - **FIX**(custom-token-import): refresh asset list on import and use lowercase for custom token import (#220). + - **FEAT**(coins): Add TRON and TRC20 support (#316). + - **FEAT**(sdk): typed error handling, trading streams, and activation refactoring (#312). + ## 0.3.0+3 - Update a dependency to the latest release. diff --git a/packages/komodo_ui/pubspec.yaml b/packages/komodo_ui/pubspec.yaml index e2cfbf0df..82d29e935 100644 --- a/packages/komodo_ui/pubspec.yaml +++ b/packages/komodo_ui/pubspec.yaml @@ -1,7 +1,7 @@ name: komodo_ui description: A high-level widget catalog relevant to building Flutter UI apps which consume Komodo DeFi Framework -version: 0.3.0+3 +version: 0.3.1 repository: https://github.com/GLEECBTC/komodo-defi-sdk-flutter environment: @@ -17,7 +17,7 @@ dependencies: sdk: flutter intl: ^0.20.2 - komodo_defi_types: ^0.3.2+1 + komodo_defi_types: ^0.4.0 mobile_scanner: ^7.0.0 dev_dependencies: diff --git a/packages/komodo_wallet_build_transformer/CHANGELOG.md b/packages/komodo_wallet_build_transformer/CHANGELOG.md index 1e98bede2..9c30ef9fd 100644 --- a/packages/komodo_wallet_build_transformer/CHANGELOG.md +++ b/packages/komodo_wallet_build_transformer/CHANGELOG.md @@ -1,3 +1,9 @@ +## 0.4.1 + + - **FIX**: swap zcash params primary/backup URLs to use official z.cash as primary (#301). + - **FIX**(komodo_defi_framework): rename transformer marker and update references\n\n- Use assets/transformer_invoker.txt instead of dotfile\n- Update pubspec and READMEs\n- Remove special .gitignore unignore. + - **FEAT**(sdk): typed error handling, trading streams, and activation refactoring (#312). + ## 0.4.0 > Note: This release has breaking changes. diff --git a/packages/komodo_wallet_build_transformer/pubspec.yaml b/packages/komodo_wallet_build_transformer/pubspec.yaml index c553b24ac..163c41388 100644 --- a/packages/komodo_wallet_build_transformer/pubspec.yaml +++ b/packages/komodo_wallet_build_transformer/pubspec.yaml @@ -1,6 +1,6 @@ name: komodo_wallet_build_transformer description: A build transformer for Gleec Wallet used for managing all build-time dependencies. -version: 0.4.0 +version: 0.4.1 repository: https://github.com/GLEECBTC/komodo-defi-sdk-flutter environment: diff --git a/packages/komodo_wallet_cli/CHANGELOG.md b/packages/komodo_wallet_cli/CHANGELOG.md index 0d1f5d297..e3d4e2b15 100644 --- a/packages/komodo_wallet_cli/CHANGELOG.md +++ b/packages/komodo_wallet_cli/CHANGELOG.md @@ -1,3 +1,9 @@ +## 0.5.0 + +> Note: This release has breaking changes. + + - **BREAKING** **FIX**(rpc): minimise RPC usage with comprehensive caching and streaming support (#262). + ## 0.4.0+1 - **REFACTOR**(komodo_wallet_cli): replace print() with stdout/stderr and improve logging. diff --git a/packages/komodo_wallet_cli/pubspec.yaml b/packages/komodo_wallet_cli/pubspec.yaml index b4b075d64..553640585 100644 --- a/packages/komodo_wallet_cli/pubspec.yaml +++ b/packages/komodo_wallet_cli/pubspec.yaml @@ -11,7 +11,7 @@ name: komodo_wallet_cli description: Developer CLI package for Gleec Wallet tooling. -version: 0.4.0+1 +version: 0.5.0 repository: https://github.com/GLEECBTC/komodo-defi-sdk-flutter/ environment: @@ -25,7 +25,7 @@ dependencies: crypto: ^3.0.3 html: ^0.15.4 http: ^1.4.0 - komodo_wallet_build_transformer: ^0.4.0 + komodo_wallet_build_transformer: ^0.4.1 logging: ^1.3.0 path: ^1.9.1 yaml: ^3.1.3 From 4edb1c68cea9d3186536ddea04533509d842cc27 Mon Sep 17 00:00:00 2001 From: Nitride <77973576+CharlVS@users.noreply.github.com> Date: Mon, 23 Mar 2026 15:48:20 +0100 Subject: [PATCH 20/40] chore: update v0.9.4 changelog (#333) --- CHANGELOG.md | 100 +++++++++++++++++++++++++++------------------------ 1 file changed, 53 insertions(+), 47 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 719da16aa..0b664a5ea 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -33,7 +33,7 @@ Packages with other changes: #### `komodo_cex_market_data` - `v0.1.0` - **PERF**(logs): reduce market metrics log verbosity and duplication (#223). - - **FIX**(sdk): close balance and pubkeysubscriptions on auth state changes (#232). + - **FIX**(sdk): close balance and pubkey subscriptions on auth state changes (#232). - **FIX**(binance): use the per-coin supported quote currency list instead of the global cache (#224). - **FEAT**(cex-market-data): add CoinPaprika API provider as a fallback option (#215). - **BREAKING** **FIX**(rpc): minimise RPC usage with comprehensive caching and streaming support (#262). @@ -42,86 +42,92 @@ Packages with other changes: - **PERF**(logs): reduce market metrics log verbosity and duplication (#223). - **FIX**(startup): handle 6133 seed fallback and invalid configs (#318). - - **FIX**(config): loosen types for needs transform check and fix lightwalletservers type. - - **FIX**(config): add ssl-only transform for native platforms. - - **FIX**(sdk): close balance and pubkeysubscriptions on auth state changes (#232). + - **FIX**(config): loosen types for `needsTransform` checks and fix `lightwalletservers` typing. + - **FIX**(config): add SSL-only transforms for native platforms. + - **FIX**(sdk): close balance and pubkey subscriptions on auth state changes (#232). - **FIX**(zhltc): zhltc activation fixes (#227). - - **FEAT**(sdk): add token safety and fee support helpers (#319). - - **FEAT**(coins): Add TRON and TRC20 support (#316). - - **FEAT**(message-signing): Add AddressPath type and refactor to use Asset/PubkeyInfo (#231). + - **FEAT**(coins): add TRON/TRC20-aware config and storage handling (#316). + - **FEAT**(sdk): add token safety and fee support helpers for custom-token flows (#319). + - **FEAT**(message-signing): add `AddressPath` support and refactor Asset/PubkeyInfo usage (#231). - **FEAT**(coin-config): add custom token support to coin config manager (#225). - **BREAKING** **FIX**(rpc): minimise RPC usage with comprehensive caching and streaming support (#262). #### `komodo_defi_framework` - `v0.4.0` - - **REFACTOR**(macos): streamline KDF binary placement; update signing flow (#247). - - **FIX**(streaming): gate enable_* calls on real SSE first-byte event (#332). + - **REFACTOR**(macos): streamline KDF binary placement and update the signing flow (#247). + - **CHORE**(framework): upgrade bundled KDF and coins references for the 3.0.0-beta preview and latest coins roll (#317, #331). + - **FIX**(streaming): gate enable_* calls on a real SSE first-byte event (#332). - **FIX**(auth): add mutex-protected atomic metadata updates (#328). - **FIX**(startup): handle 6133 seed fallback and invalid configs (#318). - - **FIX**(web): improve wasm JS interop bindings (#315). - - **FIX**(web): complete wasm-safe sdk interop cleanup (#313). - - **FIX**: re-format build config. - - **FIX**: swap zcash params primary/backup URLs to use official z.cash as primary (#301). + - **FIX**(web): improve WASM JS interop bindings (#315). + - **FIX**(web): complete WASM-safe SDK interop cleanup (#313). + - **FIX**(build): reformat build config and normalize API source URLs (#301). - **FIX**(zhltc): zhltc activation fixes (#227). - **FIX**(auth): store bip39 compatibility regardless of wallet type (#216). - - **FIX**(komodo_defi_framework): rename transformer marker and update references\n\n- Use assets/transformer_invoker.txt instead of dotfile\n- Update pubspec and READMEs\n- Remove special .gitignore unignore. + - **FIX**(build): rename the transformer marker to `assets/transformer_invoker.txt`, update pubspec/README references, and remove the old dotfile exception. - **FEAT**(sdk): add token safety and fee support helpers (#319). - - **FEAT**(sdk): typed error handling, trading streams, and activation refactoring (#312). + - **FEAT**(sdk): add typed error plumbing, trading-stream foundations, and activation refactoring (#312). - **FEAT**: add support for ETH-BASE and derived assets (#254). - **BREAKING** **FIX**(rpc): minimise RPC usage with comprehensive caching and streaming support (#262). #### `komodo_defi_local_auth` - `v0.4.0` - - **FIX**(test): add missing updateActiveUserMetadataKey to fake auth service (#330). + - **FIX**(test): add missing `updateActiveUserMetadataKey` coverage to the fake auth service (#330). - **FIX**(auth): add mutex-protected atomic metadata updates (#328). - **FIX**(auth): store bip39 compatibility regardless of wallet type (#216). - - **FEAT**(sdk): typed error handling, trading streams, and activation refactoring (#312). + - **FEAT**(sdk): add typed error handling, trading streams, and activation refactoring foundations (#312). - **BREAKING** **FIX**(rpc): minimise RPC usage with comprehensive caching and streaming support (#262). #### `komodo_defi_rpc_methods` - `v0.4.0` - - **FIX**(sdk): close balance and pubkeysubscriptions on auth state changes (#232). + - **FIX**(sdk): close balance and pubkey subscriptions on auth state changes (#232). - **FIX**(zhltc): zhltc activation fixes (#227). - - **FEAT**(coins): Add TRON and TRC20 support (#316). - - **FEAT**(sdk): typed error handling, trading streams, and activation refactoring (#312). - - **FEAT**(message-signing): Add AddressPath type and refactor to use Asset/PubkeyInfo (#231). + - **FEAT**(errors): add generated MM2 exception models and richer task error details (#312). + - **FEAT**(wallet): extend HD wallet, password change, delete-wallet, and Trezor RPC shapes (#312). + - **FEAT**(coins): add TRON/TRC20 activation parameters and withdrawal request support (#316). + - **FEAT**(message-signing): add `AddressPath` type and refactor to use Asset/PubkeyInfo (#231). - **BREAKING** **FIX**(rpc): minimise RPC usage with comprehensive caching and streaming support (#262). #### `komodo_defi_sdk` - `v0.5.0` - - **FIX**(streaming): gate enable_* calls on real SSE first-byte event (#332). - - **FIX**(withdrawals): remove duplicate executeWithdrawal method (#322). - - **FIX**(sdk): expose custom token cleanup (#321). - - **FIX**: swap zcash params primary/backup URLs to use official z.cash as primary (#301). - - **FIX**(sdk): close balance and pubkeysubscriptions on auth state changes (#232). + - **FIX**(streaming): gate enable_* calls on a real SSE first-byte event (#332). + - **FIX**(withdrawals): remove the duplicate `executeWithdrawal` path (#322). + - **FIX**(sdk): expose custom token cleanup hooks (#321). + - **FIX**(build): keep zcash params pointed at official z.cash and align API source inputs (#301). + - **FIX**(sdk): close balance and pubkey subscriptions on auth state changes (#232). - **FIX**(zhltc): zhltc activation fixes (#227). - - **FIX**(custom-token-import): refresh asset list on import and use lowercase for custom token import (#220). + - **FIX**(custom-token-import): refresh asset lists on import and use lowercase identifiers for custom token import (#220). + - **PERF**(streaming): expose managed `orderbook`, `swap_status`, and `order_status` subscriptions for stream-first trading refresh flows (#312, #262). + - **PERF**(rpc): add in-flight and short-lived result caches for `trade_preimage`, `max_taker_vol`, `max_maker_vol`, and `min_trading_vol` (#262). + - **PERF**(bridge): dedupe bridge orderbook depth requests, add taker preimage cache parity, and reduce validation retry fan-out (#262). + - **PERF**(polling): reduce recurring `my_recent_swaps` payload size, slow swaps and orders polling away from active DEX routes, and keep minute-level balance sweeping as fallback only when live watchers are unavailable (#262). - **FEAT**(sdk): add SIA activation and withdrawal support (#320). - - **FEAT**(sdk): add token safety and fee support helpers (#319). - - **FEAT**(coins): Add TRON and TRC20 support (#316). - - **FEAT**(sdk): add high-level balance/transaction manager interfaces (#314). - - **FEAT**(sdk): typed error handling, trading streams, and activation refactoring (#312). - - **FEAT**(activation): integrate ActivatedAssetsCache to optimize asset activation checks. + - **FEAT**(sdk): add token safety checks, fee helpers, and custom-token cleanup hooks (#319, #321). + - **FEAT**(coins): add TRON/TRC20 activation support, protocol wiring, and withdrawal coverage (#316). + - **FEAT**(sdk): add asset, balance, and transaction-history manager interfaces (#314). + - **FEAT**(sdk): add typed error mapping, trading-manager support, and activation/withdrawal refactoring foundations (#312). + - **FEAT**(activation): integrate `ActivatedAssetsCache` to optimize asset activation checks. + - **FEAT**(activation): add subclass token activation strategies and GRC routing for derived assets. - **FEAT**: add support for ETH-BASE and derived assets (#254). - - **FEAT**(message-signing): Add AddressPath type and refactor to use Asset/PubkeyInfo (#231). + - **FEAT**(message-signing): add `AddressPath` type and refactor to use Asset/PubkeyInfo (#231). - **FEAT**(coin-config): add custom token support to coin config manager (#225). - **FEAT**(cex-market-data): add CoinPaprika API provider as a fallback option (#215). - **BREAKING** **FIX**(rpc): minimise RPC usage with comprehensive caching and streaming support (#262). #### `komodo_defi_types` - `v0.4.0` - - **FIX**(types): use reified generics in JSON traversal for wasm/minified builds (#329). + - **FIX**(types): use reified generics in JSON traversal for WASM and minified builds (#329). - **FIX**(startup): handle 6133 seed fallback and invalid configs (#318). - **FIX**(asset-tagging): correct UTXO coins incorrectly tagged as Smart Chain (#244). - - **FIX**(sdk): close balance and pubkeysubscriptions on auth state changes (#232). + - **FIX**(sdk): close balance and pubkey subscriptions on auth state changes (#232). - **FIX**(zhltc): zhltc activation fixes (#227). - - **FIX**(custom-token-import): refresh asset list on import and use lowercase for custom token import (#220). - - **FEAT**(sdk): add token safety and fee support helpers (#319). - - **FEAT**(coins): Add TRON and TRC20 support (#316). - - **FEAT**(sdk): typed error handling, trading streams, and activation refactoring (#312). + - **FIX**(custom-token-import): refresh asset lists on import and use lowercase identifiers for custom token import (#220). + - **FEAT**(sdk): add token safety helpers and custom token exceptions (#319). + - **FEAT**(coins): add TRON/TRC20 protocols, protocol-type utilities, and derived-asset routing (#316). + - **FEAT**(sdk): add typed error support, updated activation progress, and refreshed withdrawal/fee types (#312). - **FEAT**: add support for ETH-BASE and derived assets (#254). - **FEAT**(coin-config): add custom token support to coin config manager (#225). - - **FEAT**(types): parent display name suffix via subclass (#213). + - **FEAT**(types): add parent display-name suffixes via subclasses (#213). - **BREAKING** **FIX**(rpc): minimise RPC usage with comprehensive caching and streaming support (#262). #### `komodo_wallet_cli` - `v0.5.0` @@ -135,7 +141,7 @@ Packages with other changes: #### `dragon_logs` - `v2.0.1` - - **FIX**(web): improve wasm JS interop bindings (#315). + - **FIX**(web): improve WASM JS interop bindings (#315). - **FIX**(zhltc): zhltc activation fixes (#227). #### `komodo_coins` - `v0.3.2` @@ -148,15 +154,15 @@ Packages with other changes: - **FIX**(ui): detect asset icon precache failures (#326). - **FIX**(zhltc): zhltc activation fixes (#227). - - **FIX**(custom-token-import): refresh asset list on import and use lowercase for custom token import (#220). - - **FEAT**(coins): Add TRON and TRC20 support (#316). - - **FEAT**(sdk): typed error handling, trading streams, and activation refactoring (#312). + - **FIX**(custom-token-import): refresh asset lists on import and use lowercase identifiers for custom token import (#220). + - **FEAT**(coins): add TRON and TRC20 support (#316). + - **FEAT**(sdk): add typed error handling, trading streams, and activation refactoring foundations (#312). #### `komodo_wallet_build_transformer` - `v0.4.1` - - **FIX**: swap zcash params primary/backup URLs to use official z.cash as primary (#301). - - **FIX**(komodo_defi_framework): rename transformer marker and update references\n\n- Use assets/transformer_invoker.txt instead of dotfile\n- Update pubspec and READMEs\n- Remove special .gitignore unignore. - - **FEAT**(sdk): typed error handling, trading streams, and activation refactoring (#312). + - **FIX**(build): keep zcash params pointed at official z.cash and align API source inputs (#301). + - **FIX**(build): rename the transformer marker to `assets/transformer_invoker.txt`, update pubspec/README references, and remove the old dotfile exception. + - **FEAT**(build): refresh GitHub asset downloading and typed build-transform plumbing for the new SDK generation flow (#312). ## 2025-08-25 From b5626fdc1d507172a8b37e79d6120423343ff6af Mon Sep 17 00:00:00 2001 From: Nitride <77973576+CharlVS@users.noreply.github.com> Date: Mon, 23 Mar 2026 15:50:54 +0100 Subject: [PATCH 21/40] chore(build): update coins build config (#334) --- packages/komodo_defi_framework/app_build/build_config.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/komodo_defi_framework/app_build/build_config.json b/packages/komodo_defi_framework/app_build/build_config.json index 7aadd36d1..0a7b02172 100644 --- a/packages/komodo_defi_framework/app_build/build_config.json +++ b/packages/komodo_defi_framework/app_build/build_config.json @@ -65,10 +65,10 @@ "coins": { "fetch_at_build_enabled": true, "update_commit_on_build": true, - "bundled_coins_repo_commit": "32a0cdc2e9ab5927a571cb1c85fc4be080567e70", + "bundled_coins_repo_commit": "4d9b6b1da8f2e9da4dc35a4f0311c7e8c0746d53", "coins_repo_api_url": "https://api.github.com/repos/GLEECBTC/coins", "coins_repo_content_url": "https://raw.githubusercontent.com/GLEECBTC/coins", - "coins_repo_branch": "master", + "coins_repo_branch": "feat/add-tron-coins", "runtime_updates_enabled": true, "mapped_files": { "assets/config/coins_config.json": "utils/coins_config_unfiltered.json", From bee3cc67d3d5633e9aede11141ad21bb4c3d70a7 Mon Sep 17 00:00:00 2001 From: Nitride <77973576+CharlVS@users.noreply.github.com> Date: Mon, 23 Mar 2026 16:40:50 +0100 Subject: [PATCH 22/40] fix(komodo_defi_sdk): keep last-known spot prices across cache rotation (#335) Add a secondary cache for current (non-historical) prices so priceIfKnown does not return null during periodic live-cache clears. Document streaming tech debt and cover behaviour with unit tests. --- .../Market_Data_Price_Streaming_Tech_Debt.md | 60 +++++++++++++++++++ .../src/market_data/market_data_manager.dart | 53 ++++++++++++---- .../test/market_data_manager_test.dart | 60 +++++++++++++++++++ 3 files changed, 161 insertions(+), 12 deletions(-) create mode 100644 docs/tech_debt/Market_Data_Price_Streaming_Tech_Debt.md diff --git a/docs/tech_debt/Market_Data_Price_Streaming_Tech_Debt.md b/docs/tech_debt/Market_Data_Price_Streaming_Tech_Debt.md new file mode 100644 index 000000000..54e458819 --- /dev/null +++ b/docs/tech_debt/Market_Data_Price_Streaming_Tech_Debt.md @@ -0,0 +1,60 @@ +## Tech Debt Report: Market Data Price Streaming + +### Context + +- Current `MarketDataManager` APIs are request/response based. +- `priceIfKnown` is used as a read-only cache accessor by wallet totals. +- A recent stability fix keeps a last-known current-price cache to prevent + transient null windows when live cache rotates. +- Native streaming is still missing in `MarketDataManager` (explicit TODO in + `market_data_manager.dart`). + +### Problem + +- The app has no first-class SDK stream for fiat price updates. +- Existing app-level polling helpers are not SDK-owned contracts and are not + consistently used across features. +- Consumers must combine periodic fetches with cache reads, which duplicates + refresh logic in the app layer. + +### Target Design + +- Add native streaming support to `MarketDataManager`: + - `Stream watchFiatPrice(AssetId assetId, {QuoteCurrency quoteCurrency = Stablecoin.usdt, Duration refreshInterval = const Duration(minutes: 1)})` + - Optional multi-asset API for aggregate screens: + - `Stream> watchFiatPrices(Iterable assetIds, {QuoteCurrency quoteCurrency = Stablecoin.usdt, Duration refreshInterval = const Duration(minutes: 1)})` +- Stream contract: + - Emit immediately with last-known value (or null if none). + - Refresh on interval using existing repository fallback logic. + - Keep last successful value on transient provider failures. + - Avoid duplicate requests for identical active watch keys. + +### Migration Plan + +1. SDK phase: + - Implement stream APIs in `MarketDataManager` and `CexMarketDataManager`. + - Reuse existing cache keys and fallback selection strategy. + - Add cancellation-safe watcher bookkeeping and deduplication. +2. App phase: + - Migrate wallet total displays to stream-driven updates. + - Remove app-side price polling helpers once no longer used. +3. Cleanup phase: + - Re-evaluate cache timer responsibilities when stream lifecycle is in place. + - Keep `priceIfKnown` as synchronous access for non-reactive callers. + +### Testing Requirements + +- SDK unit tests: + - initial stream emission from cache, + - periodic refresh emission, + - fallback behavior on repository failures, + - watcher deduplication and resource cleanup. +- App integration tests: + - wallet total remains stable during provider failures, + - totals update when stream delivers new prices. + +### Suggested Conventional Commits + +- `feat(market-data): add native fiat price watch streams to MarketDataManager` +- `refactor(wallet): consume sdk price streams for wallet totals` +- `chore(wallet): remove legacy app-side fiat polling helper` diff --git a/packages/komodo_defi_sdk/lib/src/market_data/market_data_manager.dart b/packages/komodo_defi_sdk/lib/src/market_data/market_data_manager.dart index c10a15cdf..428c95446 100644 --- a/packages/komodo_defi_sdk/lib/src/market_data/market_data_manager.dart +++ b/packages/komodo_defi_sdk/lib/src/market_data/market_data_manager.dart @@ -7,7 +7,8 @@ import 'package:logging/logging.dart'; // TODO: Add streaming support for price updates. The challenges share a lot // of similarities with the balance manager. Investigate if we can create a -// generic manager class for such cases. +// generic manager class for such cases. See: +// docs/tech_debt/Market_Data_Price_Streaming_Tech_Debt.md /// Interface defining the contract for price management operations abstract class MarketDataManager { @@ -73,12 +74,19 @@ class CexMarketDataManager CexMarketDataManager({ required List priceRepositories, RepositorySelectionStrategy? selectionStrategy, - }) : _priceRepositories = priceRepositories, + Duration cacheClearInterval = _defaultCacheClearInterval, + }) : assert( + cacheClearInterval > Duration.zero, + 'cacheClearInterval must be greater than zero', + ), + _cacheClearInterval = cacheClearInterval, + _priceRepositories = priceRepositories, _selectionStrategy = selectionStrategy ?? DefaultRepositorySelectionStrategy(); static final _logger = Logger('CexMarketDataManager'); - static const _cacheClearInterval = Duration(minutes: 5); + static const _defaultCacheClearInterval = Duration(minutes: 5); + final Duration _cacheClearInterval; Timer? _cacheTimer; @override @@ -121,6 +129,11 @@ class CexMarketDataManager // Cache to store asset prices final Map _priceCache = {}; + // Cache to keep last-known current prices (without historical date). + // This intentionally survives periodic live cache rotations to avoid + // transient null windows for read-only cache consumers. + final Map _lastKnownCurrentPriceCache = {}; + // Cache to store 24h price changes final Map _priceChangeCache = {}; @@ -140,10 +153,9 @@ class CexMarketDataManager }) { final basePrefix = assetId.baseCacheKeyPrefix; // Normalize input dates to UTC midnight before lookups to avoid timezone issues - final normalizedDate = - priceDate != null - ? DateTime.utc(priceDate.year, priceDate.month, priceDate.day) - : null; + final normalizedDate = priceDate != null + ? DateTime.utc(priceDate.year, priceDate.month, priceDate.day) + : null; return canonicalCacheKeyFromBasePrefix(basePrefix, { 'quote': quoteCurrency.symbol, 'kind': 'price', @@ -202,6 +214,9 @@ class CexMarketDataManager fiatCurrency: quoteCurrency, ); _priceCache[cacheKey] = price; + if (priceDate == null) { + _lastKnownCurrentPriceCache[cacheKey] = price; + } _logger.finer( 'Fetched price from ${repo.runtimeType} for ' '${assetId.symbol.assetConfigId}: $price', @@ -223,7 +238,21 @@ class CexMarketDataManager quoteCurrency: quoteCurrency, ); - return _getCachedPrice(cacheKey); + final cachedPrice = _getCachedPrice(cacheKey); + if (cachedPrice != null) { + return cachedPrice; + } + + // Historical prices are expected to expire from the live cache. + if (priceDate != null) { + return null; + } + + final lastKnownCurrentPrice = _lastKnownCurrentPriceCache[cacheKey]; + if (lastKnownCurrentPrice != null) { + _logger.finer('Last-known current price cache hit for $cacheKey'); + } + return lastKnownCurrentPrice; } @override @@ -345,10 +374,9 @@ class CexMarketDataManager _assertInitialized(); // Normalize input dates to UTC midnight to avoid timezone issues - final normalizedDates = - dates - .map((date) => DateTime.utc(date.year, date.month, date.day)) - .toList(); + final normalizedDates = dates + .map((date) => DateTime.utc(date.year, date.month, date.day)) + .toList(); final cached = {}; final missingDates = []; @@ -409,6 +437,7 @@ class CexMarketDataManager _cacheTimer?.cancel(); _cacheTimer = null; _priceCache.clear(); + _lastKnownCurrentPriceCache.clear(); _priceChangeCache.clear(); clearRepositoryHealthData(); // Clear mixin data _logger.fine('Disposed CexMarketDataManager'); diff --git a/packages/komodo_defi_sdk/test/market_data_manager_test.dart b/packages/komodo_defi_sdk/test/market_data_manager_test.dart index 3c704a076..b5430dbf0 100644 --- a/packages/komodo_defi_sdk/test/market_data_manager_test.dart +++ b/packages/komodo_defi_sdk/test/market_data_manager_test.dart @@ -62,6 +62,66 @@ void main() { verify(() => fallback.getCoinFiatPrice(asset('BTC'))).called(1); }); + test( + 'priceIfKnown keeps last-known current price after live cache clear', + () async { + final repo = MockPrimaryRepository(); + final manager = CexMarketDataManager( + priceRepositories: [repo], + selectionStrategy: DefaultRepositorySelectionStrategy(), + cacheClearInterval: const Duration(milliseconds: 20), + ); + + when(repo.getCoinList).thenAnswer((_) async => []); + when( + () => repo.supports(any(), any(), any()), + ).thenAnswer((_) async => true); + when( + () => repo.getCoinFiatPrice( + any(), + fiatCurrency: any(named: 'fiatCurrency'), + ), + ).thenAnswer((_) async => Decimal.parse('100.0')); + + await manager.init(); + + // Seed current-price caches. + final seededPrice = await manager.maybeFiatPrice(asset('BTC')); + expect(seededPrice, equals(Decimal.parse('100.0'))); + expect( + manager.priceIfKnown(asset('BTC')), + equals(Decimal.parse('100.0')), + ); + + // Wait long enough for the periodic live cache clear to run. + await Future.delayed(const Duration(milliseconds: 60)); + + // Live cache is rotated, but last-known current price should still + // be returned. + expect( + manager.priceIfKnown(asset('BTC')), + equals(Decimal.parse('100.0')), + ); + + // Refresh from repository and ensure last-known cache updates. + when( + () => repo.getCoinFiatPrice( + any(), + fiatCurrency: any(named: 'fiatCurrency'), + ), + ).thenAnswer((_) async => Decimal.parse('120.0')); + + final refreshedPrice = await manager.maybeFiatPrice(asset('BTC')); + expect(refreshedPrice, equals(Decimal.parse('120.0'))); + expect( + manager.priceIfKnown(asset('BTC')), + equals(Decimal.parse('120.0')), + ); + + await manager.dispose(); + }, + ); + test('fiatPrice uses fallback when primary repository fails', () async { final primaryRepo = MockPrimaryRepository(); final fallbackRepo = MockFallbackRepository(); From ead35b07d541467efc9867609f2ffb65cf5c4bd8 Mon Sep 17 00:00:00 2001 From: Nitride <77973576+CharlVS@users.noreply.github.com> Date: Mon, 23 Mar 2026 18:05:12 +0100 Subject: [PATCH 23/40] fix: harden numeric JSON parsing for num/int compatibility (#336) --- .../binance/models/binance_exchange_info.dart | 2 +- .../models/binance_exchange_info_reduced.dart | 2 +- .../lib/src/binance/models/filter.dart | 8 +- .../lib/src/binance/models/rate_limit.dart | 4 +- .../lib/src/binance/models/symbol.dart | 11 +- .../src/binance/models/symbol_reduced.dart | 6 +- .../coin_historical_data/community_data.dart | 5 +- .../web/res/kdf_wrapper.dart | 6 +- .../pagination/history_target.dart | 4 +- .../primitive/mm2_rational.dart | 6 +- .../rpc_methods/trading/trade_preimage.dart | 48 +++-- .../lib/src/fees/fee_management.dart | 121 ++++++------ .../lib/src/transactions/fee_info.dart | 14 +- .../src/steps/models/github/github_file.dart | 9 +- .../lib/src/steps/models/github/release.dart | 184 +++++++++--------- 15 files changed, 215 insertions(+), 215 deletions(-) diff --git a/packages/komodo_cex_market_data/lib/src/binance/models/binance_exchange_info.dart b/packages/komodo_cex_market_data/lib/src/binance/models/binance_exchange_info.dart index e92cc8e1a..958f43d59 100644 --- a/packages/komodo_cex_market_data/lib/src/binance/models/binance_exchange_info.dart +++ b/packages/komodo_cex_market_data/lib/src/binance/models/binance_exchange_info.dart @@ -14,7 +14,7 @@ class BinanceExchangeInfoResponse { factory BinanceExchangeInfoResponse.fromJson(Map json) { return BinanceExchangeInfoResponse( timezone: json['timezone'] as String, - serverTime: json['serverTime'] as int, + serverTime: (json['serverTime'] as num).toInt(), rateLimits: (json['rateLimits'] as List) .map((dynamic v) => RateLimit.fromJson(v as Map)) .toList(), diff --git a/packages/komodo_cex_market_data/lib/src/binance/models/binance_exchange_info_reduced.dart b/packages/komodo_cex_market_data/lib/src/binance/models/binance_exchange_info_reduced.dart index cd25ca385..606f8fa0e 100644 --- a/packages/komodo_cex_market_data/lib/src/binance/models/binance_exchange_info_reduced.dart +++ b/packages/komodo_cex_market_data/lib/src/binance/models/binance_exchange_info_reduced.dart @@ -15,7 +15,7 @@ class BinanceExchangeInfoResponseReduced { ) { return BinanceExchangeInfoResponseReduced( timezone: json['timezone'] as String, - serverTime: json['serverTime'] as int, + serverTime: (json['serverTime'] as num).toInt(), symbols: (json['symbols'] as List) .map((dynamic v) => SymbolReduced.fromJson(v as Map)) .toList(), diff --git a/packages/komodo_cex_market_data/lib/src/binance/models/filter.dart b/packages/komodo_cex_market_data/lib/src/binance/models/filter.dart index 1eb4786e5..a7472f2d0 100644 --- a/packages/komodo_cex_market_data/lib/src/binance/models/filter.dart +++ b/packages/komodo_cex_market_data/lib/src/binance/models/filter.dart @@ -29,14 +29,14 @@ class Filter { minQty: json['minQty'] as String?, maxQty: json['maxQty'] as String?, stepSize: json['stepSize'] as String?, - limit: json['limit'] as int?, + limit: (json['limit'] as num?)?.toInt(), minNotional: json['minNotional'] as String?, applyMinToMarket: json['applyMinToMarket'] as bool?, maxNotional: json['maxNotional'] as String?, applyMaxToMarket: json['applyMaxToMarket'] as bool?, - avgPriceMins: json['avgPriceMins'] as int?, - maxNumOrders: json['maxNumOrders'] as int?, - maxNumAlgoOrders: json['maxNumAlgoOrders'] as int?, + avgPriceMins: (json['avgPriceMins'] as num?)?.toInt(), + maxNumOrders: (json['maxNumOrders'] as num?)?.toInt(), + maxNumAlgoOrders: (json['maxNumAlgoOrders'] as num?)?.toInt(), ); } diff --git a/packages/komodo_cex_market_data/lib/src/binance/models/rate_limit.dart b/packages/komodo_cex_market_data/lib/src/binance/models/rate_limit.dart index 0cab9afed..f97507655 100644 --- a/packages/komodo_cex_market_data/lib/src/binance/models/rate_limit.dart +++ b/packages/komodo_cex_market_data/lib/src/binance/models/rate_limit.dart @@ -13,8 +13,8 @@ class RateLimit { return RateLimit( rateLimitType: json['rateLimitType'] as String, interval: json['interval'] as String, - intervalNum: json['intervalNum'] as int, - limit: json['limit'] as int, + intervalNum: (json['intervalNum'] as num).toInt(), + limit: (json['limit'] as num).toInt(), ); } diff --git a/packages/komodo_cex_market_data/lib/src/binance/models/symbol.dart b/packages/komodo_cex_market_data/lib/src/binance/models/symbol.dart index 62b34e645..7966e8463 100644 --- a/packages/komodo_cex_market_data/lib/src/binance/models/symbol.dart +++ b/packages/komodo_cex_market_data/lib/src/binance/models/symbol.dart @@ -33,12 +33,13 @@ class Symbol { symbol: json['symbol'] as String, status: json['status'] as String, baseAsset: json['baseAsset'] as String, - baseAssetPrecision: json['baseAssetPrecision'] as int, + baseAssetPrecision: (json['baseAssetPrecision'] as num).toInt(), quoteAsset: json['quoteAsset'] as String, - quotePrecision: json['quotePrecision'] as int, - quoteAssetPrecision: json['quoteAssetPrecision'] as int, - baseCommissionPrecision: json['baseCommissionPrecision'] as int, - quoteCommissionPrecision: json['quoteCommissionPrecision'] as int, + quotePrecision: (json['quotePrecision'] as num).toInt(), + quoteAssetPrecision: (json['quoteAssetPrecision'] as num).toInt(), + baseCommissionPrecision: (json['baseCommissionPrecision'] as num).toInt(), + quoteCommissionPrecision: (json['quoteCommissionPrecision'] as num) + .toInt(), orderTypes: (json['orderTypes'] as List) .map((dynamic v) => v as String) .toList(), diff --git a/packages/komodo_cex_market_data/lib/src/binance/models/symbol_reduced.dart b/packages/komodo_cex_market_data/lib/src/binance/models/symbol_reduced.dart index 663c3a363..e3045650c 100644 --- a/packages/komodo_cex_market_data/lib/src/binance/models/symbol_reduced.dart +++ b/packages/komodo_cex_market_data/lib/src/binance/models/symbol_reduced.dart @@ -20,10 +20,10 @@ class SymbolReduced { symbol: json['symbol'] as String, status: json['status'] as String, baseAsset: json['baseAsset'] as String, - baseAssetPrecision: json['baseAssetPrecision'] as int, + baseAssetPrecision: (json['baseAssetPrecision'] as num).toInt(), quoteAsset: json['quoteAsset'] as String, - quotePrecision: json['quotePrecision'] as int, - quoteAssetPrecision: json['quoteAssetPrecision'] as int, + quotePrecision: (json['quotePrecision'] as num).toInt(), + quoteAssetPrecision: (json['quoteAssetPrecision'] as num).toInt(), isSpotTradingAllowed: json['isSpotTradingAllowed'] as bool, ); } diff --git a/packages/komodo_cex_market_data/lib/src/coingecko/models/coin_historical_data/community_data.dart b/packages/komodo_cex_market_data/lib/src/coingecko/models/coin_historical_data/community_data.dart index 516b25b45..75731d67c 100644 --- a/packages/komodo_cex_market_data/lib/src/coingecko/models/coin_historical_data/community_data.dart +++ b/packages/komodo_cex_market_data/lib/src/coingecko/models/coin_historical_data/community_data.dart @@ -13,8 +13,9 @@ class CommunityData extends Equatable { factory CommunityData.fromJson(Map json) => CommunityData( facebookLikes: json['facebook_likes'] as dynamic, twitterFollowers: json['twitter_followers'] as dynamic, - redditAveragePosts48h: json['reddit_average_posts_48h'] as int?, - redditAverageComments48h: json['reddit_average_comments_48h'] as int?, + redditAveragePosts48h: (json['reddit_average_posts_48h'] as num?)?.toInt(), + redditAverageComments48h: (json['reddit_average_comments_48h'] as num?) + ?.toInt(), redditSubscribers: json['reddit_subscribers'] as dynamic, redditAccountsActive48h: json['reddit_accounts_active_48h'] as dynamic, ); diff --git a/packages/komodo_defi_framework/web/res/kdf_wrapper.dart b/packages/komodo_defi_framework/web/res/kdf_wrapper.dart index c39c9fe47..b30d5bde7 100644 --- a/packages/komodo_defi_framework/web/res/kdf_wrapper.dart +++ b/packages/komodo_defi_framework/web/res/kdf_wrapper.dart @@ -100,7 +100,7 @@ class KdfPlugin { throw Exception('Failed to convert mm2_main response to Dart'); } - return dartResponse as int; + return (dartResponse as num).toInt(); } catch (e) { throw Exception('Error in mm2_main: $e\nConfig: $conf'); } @@ -112,12 +112,12 @@ class KdfPlugin { } final jsResult = _mm2MainStatusJs(); - return jsResult.dartify()! as int; + return (jsResult.dartify()! as num).toInt(); } Future _mm2Stop() async { await _ensureLoaded(); final jsResult = _mm2StopJs(); - return jsResult.dartify()! as int; + return (jsResult.dartify()! as num).toInt(); } } diff --git a/packages/komodo_defi_rpc_methods/lib/src/common_structures/pagination/history_target.dart b/packages/komodo_defi_rpc_methods/lib/src/common_structures/pagination/history_target.dart index 67fca0571..b0300a748 100644 --- a/packages/komodo_defi_rpc_methods/lib/src/common_structures/pagination/history_target.dart +++ b/packages/komodo_defi_rpc_methods/lib/src/common_structures/pagination/history_target.dart @@ -83,8 +83,8 @@ class HdHistoryTarget implements HistoryTarget { factory HdHistoryTarget.fromJson(Map json) { return HdHistoryTarget( type: HistoryTargetType.parse(json['type'] as String), - accountId: json['account_id'] as int, - addressId: json['address_id'] as int?, + accountId: (json['account_id'] as num).toInt(), + addressId: (json['address_id'] as num?)?.toInt(), chain: json['chain'] as String?, ); } diff --git a/packages/komodo_defi_rpc_methods/lib/src/common_structures/primitive/mm2_rational.dart b/packages/komodo_defi_rpc_methods/lib/src/common_structures/primitive/mm2_rational.dart index 6f44aa49f..614982359 100644 --- a/packages/komodo_defi_rpc_methods/lib/src/common_structures/primitive/mm2_rational.dart +++ b/packages/komodo_defi_rpc_methods/lib/src/common_structures/primitive/mm2_rational.dart @@ -4,8 +4,8 @@ import 'package:rational/rational.dart'; const int mm2LimbBase = 4294967296; // 2^32 BigInt bigIntFromMm2Json(List json) { - final sign = json[0] as int; - final limbs = (json[1] as List).cast(); + final sign = (json[0] as num).toInt(); + final limbs = (json[1] as List).map((limb) => (limb as num).toInt()); if (sign == 0) return BigInt.zero; var value = BigInt.zero; var multiplier = BigInt.one; @@ -50,4 +50,4 @@ Rational rationalFromMm2(List json) { List rationalToMm2(Rational r) { return [bigIntToMm2Json(r.numerator), bigIntToMm2Json(r.denominator)]; -} \ No newline at end of file +} diff --git a/packages/komodo_defi_rpc_methods/lib/src/rpc_methods/trading/trade_preimage.dart b/packages/komodo_defi_rpc_methods/lib/src/rpc_methods/trading/trade_preimage.dart index 653417386..69074930a 100644 --- a/packages/komodo_defi_rpc_methods/lib/src/rpc_methods/trading/trade_preimage.dart +++ b/packages/komodo_defi_rpc_methods/lib/src/rpc_methods/trading/trade_preimage.dart @@ -43,8 +43,9 @@ class TradePreimageRequest 'params': { 'base': base, 'rel': rel, - 'swap_method': - swapMethod == SwapMethod.setPrice ? 'setprice' : swapMethod.name, + 'swap_method': swapMethod == SwapMethod.setPrice + ? 'setprice' + : swapMethod.name, if (volume != null) 'volume': volume, if (max != null) 'max': max, if (price != null) 'price': price, @@ -72,28 +73,23 @@ class TradePreimageResponse extends BaseResponse { return TradePreimageResponse( mmrpc: json.value('mmrpc'), - baseCoinFee: - result.containsKey('base_coin_fee') - ? PreimageCoinFee.fromJson(result.value('base_coin_fee')) - : null, - relCoinFee: - result.containsKey('rel_coin_fee') - ? PreimageCoinFee.fromJson(result.value('rel_coin_fee')) - : null, - takerFee: - result.containsKey('taker_fee') - ? PreimageCoinFee.fromJson(result.value('taker_fee')) - : null, - feeToSendTakerFee: - result.containsKey('fee_to_send_taker_fee') - ? PreimageCoinFee.fromJson( - result.value('fee_to_send_taker_fee'), - ) - : null, - totalFees: - (result.valueOrNull('total_fees') ?? []) - .map(PreimageTotalFee.fromJson) - .toList(), + baseCoinFee: result.containsKey('base_coin_fee') + ? PreimageCoinFee.fromJson(result.value('base_coin_fee')) + : null, + relCoinFee: result.containsKey('rel_coin_fee') + ? PreimageCoinFee.fromJson(result.value('rel_coin_fee')) + : null, + takerFee: result.containsKey('taker_fee') + ? PreimageCoinFee.fromJson(result.value('taker_fee')) + : null, + feeToSendTakerFee: result.containsKey('fee_to_send_taker_fee') + ? PreimageCoinFee.fromJson( + result.value('fee_to_send_taker_fee'), + ) + : null, + totalFees: (result.valueOrNull('total_fees') ?? []) + .map(PreimageTotalFee.fromJson) + .toList(), ); } @@ -130,8 +126,8 @@ class TradePreimageResponse extends BaseResponse { const _mm2LimbBase = 1 << 32; // 2^32 BigInt _bigIntFromMm2Json(List json) { - final sign = json[0] as int; - final limbs = (json[1] as List).cast(); + final sign = (json[0] as num).toInt(); + final limbs = (json[1] as List).map((limb) => (limb as num).toInt()); if (sign == 0) return BigInt.zero; var value = BigInt.zero; var multiplier = BigInt.one; diff --git a/packages/komodo_defi_types/lib/src/fees/fee_management.dart b/packages/komodo_defi_types/lib/src/fees/fee_management.dart index d00824bbf..6525c20fe 100644 --- a/packages/komodo_defi_types/lib/src/fees/fee_management.dart +++ b/packages/komodo_defi_types/lib/src/fees/fee_management.dart @@ -8,9 +8,9 @@ enum FeeEstimatorType { @override String toString() => switch (this) { - FeeEstimatorType.simple => 'Simple', - FeeEstimatorType.provider => 'Provider', - }; + FeeEstimatorType.simple => 'Simple', + FeeEstimatorType.provider => 'Provider', + }; static FeeEstimatorType fromString(String value) { switch (value.toLowerCase()) { @@ -32,11 +32,11 @@ enum FeePolicy { @override String toString() => switch (this) { - FeePolicy.low => 'Low', - FeePolicy.medium => 'Medium', - FeePolicy.high => 'High', - FeePolicy.internal => 'Internal', - }; + FeePolicy.low => 'Low', + FeePolicy.medium => 'Medium', + FeePolicy.high => 'High', + FeePolicy.internal => 'Internal', + }; static FeePolicy fromString(String value) { switch (value.toLowerCase()) { @@ -65,11 +65,12 @@ class EthFeeLevel extends Equatable { factory EthFeeLevel.fromJson(Map json) { return EthFeeLevel( - maxPriorityFeePerGas: - Decimal.parse(json['max_priority_fee_per_gas'].toString()), + maxPriorityFeePerGas: Decimal.parse( + json['max_priority_fee_per_gas'].toString(), + ), maxFeePerGas: Decimal.parse(json['max_fee_per_gas'].toString()), - minWaitTime: json['min_wait_time'] as int?, - maxWaitTime: json['max_wait_time'] as int?, + minWaitTime: (json['min_wait_time'] as num?)?.toInt(), + maxWaitTime: (json['max_wait_time'] as num?)?.toInt(), ); } @@ -79,15 +80,19 @@ class EthFeeLevel extends Equatable { final int? maxWaitTime; Map toJson() => { - 'max_priority_fee_per_gas': maxPriorityFeePerGas.toString(), - 'max_fee_per_gas': maxFeePerGas.toString(), - if (minWaitTime != null) 'min_wait_time': minWaitTime, - if (maxWaitTime != null) 'max_wait_time': maxWaitTime, - }; + 'max_priority_fee_per_gas': maxPriorityFeePerGas.toString(), + 'max_fee_per_gas': maxFeePerGas.toString(), + if (minWaitTime != null) 'min_wait_time': minWaitTime, + if (maxWaitTime != null) 'max_wait_time': maxWaitTime, + }; @override - List get props => - [maxPriorityFeePerGas, maxFeePerGas, minWaitTime, maxWaitTime]; + List get props => [ + maxPriorityFeePerGas, + maxFeePerGas, + minWaitTime, + maxWaitTime, + ]; } /// Response object for [get_eth_estimated_fee_per_gas]. @@ -126,27 +131,27 @@ class EthEstimatedFeePerGas extends Equatable { final String? priorityFeeTrend; Map toJson() => { - 'base_fee': baseFee.toString(), - 'low': low.toJson(), - 'medium': medium.toJson(), - 'high': high.toJson(), - 'source': source, - if (baseFeeTrend != null) 'base_fee_trend': baseFeeTrend, - if (priorityFeeTrend != null) 'priority_fee_trend': priorityFeeTrend, - 'units': units, - }; + 'base_fee': baseFee.toString(), + 'low': low.toJson(), + 'medium': medium.toJson(), + 'high': high.toJson(), + 'source': source, + if (baseFeeTrend != null) 'base_fee_trend': baseFeeTrend, + if (priorityFeeTrend != null) 'priority_fee_trend': priorityFeeTrend, + 'units': units, + }; @override List get props => [ - baseFee, - low, - medium, - high, - source, - units, - baseFeeTrend, - priorityFeeTrend, - ]; + baseFee, + low, + medium, + high, + source, + units, + baseFeeTrend, + priorityFeeTrend, + ]; } /// Response object for [get_utxo_estimated_fee]. @@ -170,10 +175,10 @@ class UtxoEstimatedFee extends Equatable { final UtxoFeeLevel high; Map toJson() => { - 'low': low.toJson(), - 'medium': medium.toJson(), - 'high': high.toJson(), - }; + 'low': low.toJson(), + 'medium': medium.toJson(), + 'high': high.toJson(), + }; @override List get props => [low, medium, high]; @@ -181,10 +186,7 @@ class UtxoEstimatedFee extends Equatable { /// UTXO fee level with per-kbyte fee rate class UtxoFeeLevel extends Equatable { - const UtxoFeeLevel({ - required this.feePerKbyte, - this.estimatedTime, - }); + const UtxoFeeLevel({required this.feePerKbyte, this.estimatedTime}); factory UtxoFeeLevel.fromJson(Map json) { return UtxoFeeLevel( @@ -200,9 +202,9 @@ class UtxoFeeLevel extends Equatable { final String? estimatedTime; Map toJson() => { - 'fee_per_kbyte': feePerKbyte.toString(), - if (estimatedTime != null) 'estimated_time': estimatedTime, - }; + 'fee_per_kbyte': feePerKbyte.toString(), + if (estimatedTime != null) 'estimated_time': estimatedTime, + }; @override List get props => [feePerKbyte, estimatedTime]; @@ -219,8 +221,9 @@ class TendermintEstimatedFee extends Equatable { factory TendermintEstimatedFee.fromJson(Map json) { return TendermintEstimatedFee( low: TendermintFeeLevel.fromJson(json['low'] as Map), - medium: - TendermintFeeLevel.fromJson(json['medium'] as Map), + medium: TendermintFeeLevel.fromJson( + json['medium'] as Map, + ), high: TendermintFeeLevel.fromJson(json['high'] as Map), ); } @@ -230,10 +233,10 @@ class TendermintEstimatedFee extends Equatable { final TendermintFeeLevel high; Map toJson() => { - 'low': low.toJson(), - 'medium': medium.toJson(), - 'high': high.toJson(), - }; + 'low': low.toJson(), + 'medium': medium.toJson(), + 'high': high.toJson(), + }; @override List get props => [low, medium, high]; @@ -250,7 +253,7 @@ class TendermintFeeLevel extends Equatable { factory TendermintFeeLevel.fromJson(Map json) { return TendermintFeeLevel( gasPrice: Decimal.parse(json['gas_price'].toString()), - gasLimit: json['gas_limit'] as int, + gasLimit: (json['gas_limit'] as num).toInt(), estimatedTime: json['estimated_time'] as String?, ); } @@ -268,10 +271,10 @@ class TendermintFeeLevel extends Equatable { Decimal get totalFee => gasPrice * Decimal.fromInt(gasLimit); Map toJson() => { - 'gas_price': gasPrice.toString(), - 'gas_limit': gasLimit, - if (estimatedTime != null) 'estimated_time': estimatedTime, - }; + 'gas_price': gasPrice.toString(), + 'gas_limit': gasLimit, + if (estimatedTime != null) 'estimated_time': estimatedTime, + }; @override List get props => [gasPrice, gasLimit, estimatedTime]; diff --git a/packages/komodo_defi_types/lib/src/transactions/fee_info.dart b/packages/komodo_defi_types/lib/src/transactions/fee_info.dart index 967106b84..69bd5c38f 100644 --- a/packages/komodo_defi_types/lib/src/transactions/fee_info.dart +++ b/packages/komodo_defi_types/lib/src/transactions/fee_info.dart @@ -46,7 +46,7 @@ sealed class FeeInfo with _$FeeInfo { coin: json['coin'] as String? ?? '', // If JSON provides e.g. "0.000000003", parse to Decimal => 3e-9 gasPrice: Decimal.parse(json['gas_price'].toString()), - gas: json['gas'] as int, + gas: (json['gas'] as num).toInt(), totalGasFee: totalGasFee, ); case 'EthGasEip1559': @@ -59,7 +59,7 @@ sealed class FeeInfo with _$FeeInfo { maxPriorityFeePerGas: Decimal.parse( json['max_priority_fee_per_gas'].toString(), ), - gas: json['gas'] as int, + gas: (json['gas'] as num).toInt(), totalGasFee: totalGasFee, ); case 'Qrc20Gas': @@ -69,20 +69,20 @@ sealed class FeeInfo with _$FeeInfo { return FeeInfo.qrc20Gas( coin: json['coin'] as String? ?? '', gasPrice: Decimal.parse(json['gas_price'].toString()), - gasLimit: json['gas_limit'] as int, + gasLimit: (json['gas_limit'] as num).toInt(), totalGasFee: totalGasFee, ); case 'Tendermint': return FeeInfo.tendermint( coin: json['coin'] as String? ?? '', amount: Decimal.parse(json['amount'].toString()), - gasLimit: json['gas_limit'] as int, + gasLimit: (json['gas_limit'] as num).toInt(), ); case 'Tron': return FeeInfo.tron( coin: json['coin'] as String? ?? '', - bandwidthUsed: json['bandwidth_used'] as int? ?? 0, - energyUsed: json['energy_used'] as int? ?? 0, + bandwidthUsed: (json['bandwidth_used'] as num?)?.toInt() ?? 0, + energyUsed: (json['energy_used'] as num?)?.toInt() ?? 0, bandwidthFee: Decimal.parse(json['bandwidth_fee'].toString()), energyFee: Decimal.parse(json['energy_fee'].toString()), totalFeeAmount: json['total_fee'] != null @@ -95,7 +95,7 @@ sealed class FeeInfo with _$FeeInfo { // The doc sometimes shows 0.05 as a number (double), // so we convert it to string, then parse: gasPrice: Decimal.parse(json['gas_price'].toString()), - gasLimit: json['gas_limit'] as int, + gasLimit: (json['gas_limit'] as num).toInt(), ); case 'Sia': return FeeInfo.sia( diff --git a/packages/komodo_wallet_build_transformer/lib/src/steps/models/github/github_file.dart b/packages/komodo_wallet_build_transformer/lib/src/steps/models/github/github_file.dart index 3bbb99eee..e2158ba25 100644 --- a/packages/komodo_wallet_build_transformer/lib/src/steps/models/github/github_file.dart +++ b/packages/komodo_wallet_build_transformer/lib/src/steps/models/github/github_file.dart @@ -21,16 +21,15 @@ class GitHubFile { name: data['name'] as String, path: data['path'] as String, sha: data['sha'] as String, - size: data['size'] as int, + size: (data['size'] as num).toInt(), url: data['url'] as String?, htmlUrl: data['html_url'] as String?, gitUrl: data['git_url'] as String?, downloadUrl: data['download_url'] as String, type: data['type'] as String, - links: - data['_links'] == null - ? null - : Links.fromJson(data['_links'] as Map), + links: data['_links'] == null + ? null + : Links.fromJson(data['_links'] as Map), ); /// Converts the [GitHubFile] instance to a JSON map. diff --git a/packages/komodo_wallet_build_transformer/lib/src/steps/models/github/release.dart b/packages/komodo_wallet_build_transformer/lib/src/steps/models/github/release.dart index ce17732c5..33cebf253 100644 --- a/packages/komodo_wallet_build_transformer/lib/src/steps/models/github/release.dart +++ b/packages/komodo_wallet_build_transformer/lib/src/steps/models/github/release.dart @@ -10,16 +10,16 @@ class Release { }); factory Release.fromJson(Map json) => Release( - url: json['url'] as String, - htmlUrl: json['html_url'] as String, - assetsUrl: json['assets_url'] as String, - tagName: json['tag_name'] as String, - targetCommitish: json['target_commitish'] as String, - author: SimpleUser.fromJson(json['author'] as Map), - assets: (json['assets'] as List) - .map((e) => ReleaseAsset.fromJson(e as Map)) - .toList(), - ); + url: json['url'] as String, + htmlUrl: json['html_url'] as String, + assetsUrl: json['assets_url'] as String, + tagName: json['tag_name'] as String, + targetCommitish: json['target_commitish'] as String, + author: SimpleUser.fromJson(json['author'] as Map), + assets: (json['assets'] as List) + .map((e) => ReleaseAsset.fromJson(e as Map)) + .toList(), + ); final String url; final String htmlUrl; final String assetsUrl; @@ -29,14 +29,14 @@ class Release { final List assets; Map toJson() => { - 'url': url, - 'html_url': htmlUrl, - 'assets_url': assetsUrl, - 'tag_name': tagName, - 'target_commitish': targetCommitish, - 'author': author.toJson(), - 'assets': assets.map((e) => e.toJson()).toList(), - }; + 'url': url, + 'html_url': htmlUrl, + 'assets_url': assetsUrl, + 'tag_name': tagName, + 'target_commitish': targetCommitish, + 'author': author.toJson(), + 'assets': assets.map((e) => e.toJson()).toList(), + }; Release copyWith({ String? url, @@ -77,22 +77,22 @@ class ReleaseAsset { }); factory ReleaseAsset.fromJson(Map json) => ReleaseAsset( - url: json['url'] as String, - browserDownloadUrl: json['browser_download_url'] as String, - id: json['id'] as int, - nodeId: json['node_id'] as String, - name: json['name'] as String, - label: json['label'] as String?, - state: json['state'] as String, - contentType: json['content_type'] as String, - size: json['size'] as int, - downloadCount: json['download_count'] as int, - createdAt: DateTime.parse(json['created_at'] as String), - updatedAt: DateTime.parse(json['updated_at'] as String), - uploader: json['uploader'] != null - ? SimpleUser.fromJson(json['uploader'] as Map) - : null, - ); + url: json['url'] as String, + browserDownloadUrl: json['browser_download_url'] as String, + id: (json['id'] as num).toInt(), + nodeId: json['node_id'] as String, + name: json['name'] as String, + label: json['label'] as String?, + state: json['state'] as String, + contentType: json['content_type'] as String, + size: (json['size'] as num).toInt(), + downloadCount: (json['download_count'] as num).toInt(), + createdAt: DateTime.parse(json['created_at'] as String), + updatedAt: DateTime.parse(json['updated_at'] as String), + uploader: json['uploader'] != null + ? SimpleUser.fromJson(json['uploader'] as Map) + : null, + ); final String url; final String browserDownloadUrl; final int id; @@ -108,20 +108,20 @@ class ReleaseAsset { final SimpleUser? uploader; Map toJson() => { - 'url': url, - 'browser_download_url': browserDownloadUrl, - 'id': id, - 'node_id': nodeId, - 'name': name, - 'label': label, - 'state': state, - 'content_type': contentType, - 'size': size, - 'download_count': downloadCount, - 'created_at': createdAt.toIso8601String(), - 'updated_at': updatedAt.toIso8601String(), - 'uploader': uploader?.toJson(), - }; + 'url': url, + 'browser_download_url': browserDownloadUrl, + 'id': id, + 'node_id': nodeId, + 'name': name, + 'label': label, + 'state': state, + 'content_type': contentType, + 'size': size, + 'download_count': downloadCount, + 'created_at': createdAt.toIso8601String(), + 'updated_at': updatedAt.toIso8601String(), + 'uploader': uploader?.toJson(), + }; ReleaseAsset copyWith({ String? url, @@ -182,28 +182,28 @@ class SimpleUser { }); factory SimpleUser.fromJson(Map json) => SimpleUser( - name: json['name'] as String?, - email: json['email'] as String?, - login: json['login'] as String, - id: json['id'] as int, - nodeId: json['node_id'] as String, - avatarUrl: json['avatar_url'] as String, - gravatarId: json['gravatar_id'] as String?, - url: json['url'] as String, - htmlUrl: json['html_url'] as String, - followersUrl: json['followers_url'] as String, - followingUrl: json['following_url'] as String, - gistsUrl: json['gists_url'] as String, - starredUrl: json['starred_url'] as String, - subscriptionsUrl: json['subscriptions_url'] as String, - organizationsUrl: json['organizations_url'] as String, - reposUrl: json['repos_url'] as String, - eventsUrl: json['events_url'] as String, - receivedEventsUrl: json['received_events_url'] as String, - type: json['type'] as String, - siteAdmin: json['site_admin'] as bool, - starredAt: json['starred_at'] as String?, - ); + name: json['name'] as String?, + email: json['email'] as String?, + login: json['login'] as String, + id: (json['id'] as num).toInt(), + nodeId: json['node_id'] as String, + avatarUrl: json['avatar_url'] as String, + gravatarId: json['gravatar_id'] as String?, + url: json['url'] as String, + htmlUrl: json['html_url'] as String, + followersUrl: json['followers_url'] as String, + followingUrl: json['following_url'] as String, + gistsUrl: json['gists_url'] as String, + starredUrl: json['starred_url'] as String, + subscriptionsUrl: json['subscriptions_url'] as String, + organizationsUrl: json['organizations_url'] as String, + reposUrl: json['repos_url'] as String, + eventsUrl: json['events_url'] as String, + receivedEventsUrl: json['received_events_url'] as String, + type: json['type'] as String, + siteAdmin: json['site_admin'] as bool, + starredAt: json['starred_at'] as String?, + ); final String? name; final String? email; @@ -228,28 +228,28 @@ class SimpleUser { final String? starredAt; Map toJson() => { - 'name': name, - 'email': email, - 'login': login, - 'id': id, - 'node_id': nodeId, - 'avatar_url': avatarUrl, - 'gravatar_id': gravatarId, - 'url': url, - 'html_url': htmlUrl, - 'followers_url': followersUrl, - 'following_url': followingUrl, - 'gists_url': gistsUrl, - 'starred_url': starredUrl, - 'subscriptions_url': subscriptionsUrl, - 'organizations_url': organizationsUrl, - 'repos_url': reposUrl, - 'events_url': eventsUrl, - 'received_events_url': receivedEventsUrl, - 'type': type, - 'site_admin': siteAdmin, - 'starred_at': starredAt, - }; + 'name': name, + 'email': email, + 'login': login, + 'id': id, + 'node_id': nodeId, + 'avatar_url': avatarUrl, + 'gravatar_id': gravatarId, + 'url': url, + 'html_url': htmlUrl, + 'followers_url': followersUrl, + 'following_url': followingUrl, + 'gists_url': gistsUrl, + 'starred_url': starredUrl, + 'subscriptions_url': subscriptionsUrl, + 'organizations_url': organizationsUrl, + 'repos_url': reposUrl, + 'events_url': eventsUrl, + 'received_events_url': receivedEventsUrl, + 'type': type, + 'site_admin': siteAdmin, + 'starred_at': starredAt, + }; SimpleUser copyWith({ String? name, From ec8e7149eaeda1e13dbf9b363f9fda5a318d7035 Mon Sep 17 00:00:00 2001 From: Nitride <77973576+CharlVS@users.noreply.github.com> Date: Tue, 24 Mar 2026 18:57:03 +0100 Subject: [PATCH 24/40] fix(komodo_defi_types): support TRON explorer URLs (#338) * fix(komodo_defi_types): support TRON explorer URLs * chore(sdk): commit pending SDK updates --- .../app_build/build_config.json | 2 +- .../tron_activation_strategy_test.dart | 2 +- .../transaction_history_strategies_test.dart | 2 +- .../lib/src/coin_classes/protocol_class.dart | 4 ++ .../protocols/base/explorer_url_pattern.dart | 69 +++++++++++++------ .../test/tron_protocol_test.dart | 26 ++++++- 6 files changed, 80 insertions(+), 25 deletions(-) diff --git a/packages/komodo_defi_framework/app_build/build_config.json b/packages/komodo_defi_framework/app_build/build_config.json index 0a7b02172..76a424612 100644 --- a/packages/komodo_defi_framework/app_build/build_config.json +++ b/packages/komodo_defi_framework/app_build/build_config.json @@ -65,7 +65,7 @@ "coins": { "fetch_at_build_enabled": true, "update_commit_on_build": true, - "bundled_coins_repo_commit": "4d9b6b1da8f2e9da4dc35a4f0311c7e8c0746d53", + "bundled_coins_repo_commit": "16496a71d9473bfab1357ab0b71511c7ba106ce4", "coins_repo_api_url": "https://api.github.com/repos/GLEECBTC/coins", "coins_repo_content_url": "https://raw.githubusercontent.com/GLEECBTC/coins", "coins_repo_branch": "feat/add-tron-coins", diff --git a/packages/komodo_defi_sdk/test/activation/tron_activation_strategy_test.dart b/packages/komodo_defi_sdk/test/activation/tron_activation_strategy_test.dart index f8d15410f..181a08124 100644 --- a/packages/komodo_defi_sdk/test/activation/tron_activation_strategy_test.dart +++ b/packages/komodo_defi_sdk/test/activation/tron_activation_strategy_test.dart @@ -1,8 +1,8 @@ +import 'package:komodo_defi_rpc_methods/komodo_defi_rpc_methods.dart'; import 'package:komodo_defi_sdk/src/activation/protocol_strategies/custom_erc20_activation_strategy.dart'; import 'package:komodo_defi_sdk/src/activation/protocol_strategies/erc20_activation_strategy.dart'; import 'package:komodo_defi_sdk/src/activation/protocol_strategies/eth_task_activation_strategy.dart'; import 'package:komodo_defi_sdk/src/activation/protocol_strategies/eth_with_tokens_activation_strategy.dart'; -import 'package:komodo_defi_rpc_methods/komodo_defi_rpc_methods.dart'; import 'package:komodo_defi_types/komodo_defi_types.dart'; import 'package:test/test.dart'; diff --git a/packages/komodo_defi_sdk/test/transaction_history/transaction_history_strategies_test.dart b/packages/komodo_defi_sdk/test/transaction_history/transaction_history_strategies_test.dart index 80a68d924..12c2b070c 100644 --- a/packages/komodo_defi_sdk/test/transaction_history/transaction_history_strategies_test.dart +++ b/packages/komodo_defi_sdk/test/transaction_history/transaction_history_strategies_test.dart @@ -32,7 +32,7 @@ Asset _createEvmAsset({ } Asset _createZhtlcAsset() { - final protocol = ZhtlcProtocol.fromJson({ + final protocol = ZhtlcProtocol.fromJson(const { 'type': 'ZHTLC', 'electrum_servers': [ {'url': 'lightwalletd.pirate.black', 'port': 9067, 'protocol': 'SSL'}, diff --git a/packages/komodo_defi_types/lib/src/coin_classes/protocol_class.dart b/packages/komodo_defi_types/lib/src/coin_classes/protocol_class.dart index d58c1e86a..f693369f4 100644 --- a/packages/komodo_defi_types/lib/src/coin_classes/protocol_class.dart +++ b/packages/komodo_defi_types/lib/src/coin_classes/protocol_class.dart @@ -166,6 +166,10 @@ abstract class ProtocolClass with ExplorerUrlMixin implements Equatable { } bool supportsTxHistoryStreaming({required bool isChildAsset}) { + // TRON does not currently expose KDF-backed tx history. + if (subClass == CoinSubClass.trx || subClass == CoinSubClass.trc20) { + return false; + } // EVM does not support tx history streaming in KDF if (evmCoinSubClasses.contains(subClass)) { return false; diff --git a/packages/komodo_defi_types/lib/src/protocols/base/explorer_url_pattern.dart b/packages/komodo_defi_types/lib/src/protocols/base/explorer_url_pattern.dart index 7c0d77f02..fa12b94ca 100644 --- a/packages/komodo_defi_types/lib/src/protocols/base/explorer_url_pattern.dart +++ b/packages/komodo_defi_types/lib/src/protocols/base/explorer_url_pattern.dart @@ -10,16 +10,24 @@ class ExplorerUrlPattern { }); factory ExplorerUrlPattern.fromJson(JsonMap config) { - final baseUrl = config.valueOrNull('explorer_url'); + final baseUrl = _normalizePatternValue( + config.valueOrNull('explorer_url'), + ); if (baseUrl == null) return const ExplorerUrlPattern(); // Add scheme if missing final urlString = baseUrl.startsWith('http') ? baseUrl : 'https://$baseUrl'; return ExplorerUrlPattern( baseUrl: Uri.tryParse(urlString), - txPattern: config.valueOrNull('explorer_tx_url'), - addressPattern: config.valueOrNull('explorer_address_url'), - blockPattern: config.valueOrNull('explorer_block_url'), + txPattern: _normalizePatternValue( + config.valueOrNull('explorer_tx_url'), + ), + addressPattern: _normalizePatternValue( + config.valueOrNull('explorer_address_url'), + ), + blockPattern: _normalizePatternValue( + config.valueOrNull('explorer_block_url'), + ), ); } @@ -45,14 +53,36 @@ class ExplorerUrlPattern { // If no placeholders were found, append the first param value if (params.isNotEmpty && url == pattern) { - url = '$url/${Uri.encodeComponent(params.values.first)}'; + url = '$url${Uri.encodeComponent(params.values.first)}'; + } + + final resolvedBaseUrl = baseUrl; + if (resolvedBaseUrl == null) return null; + + if (resolvedBaseUrl.fragment.isNotEmpty && !url.startsWith('#')) { + final baseFragment = resolvedBaseUrl.fragment; + final normalizedBaseFragment = baseFragment.endsWith('/') + ? baseFragment + : '$baseFragment/'; + final normalizedUrl = url.startsWith('/') ? url.substring(1) : url; + + return resolvedBaseUrl.replace( + fragment: '$normalizedBaseFragment$normalizedUrl', + ); } - return baseUrl?.resolve(url); + return resolvedBaseUrl.resolve(url); } catch (e) { return null; } } + + static String? _normalizePatternValue(String? value) { + if (value == null) return null; + + final normalized = value.trim(); + return normalized.isEmpty ? null : normalized; + } } /// Mixin to provide explorer URL functionality to Protocol classes @@ -63,30 +93,29 @@ mixin ExplorerUrlMixin { Uri? explorerTxUrl(String txHash) { if (txHash.isEmpty) return null; - final hash = - needs0xPrefix && !txHash.startsWith('0x') ? '0x$txHash' : txHash; + final hash = needs0xPrefix && !txHash.startsWith('0x') + ? '0x$txHash' + : txHash; - return explorerPattern.buildUrl( - explorerPattern.txPattern, - {'HASH': hash, 'TX': hash}, - ); + return explorerPattern.buildUrl(explorerPattern.txPattern, { + 'HASH': hash, + 'TX': hash, + }); } Uri? explorerAddressUrl(String address) { if (address.isEmpty) return null; - return explorerPattern.buildUrl( - explorerPattern.addressPattern, - {'ADDRESS': address}, - ); + return explorerPattern.buildUrl(explorerPattern.addressPattern, { + 'ADDRESS': address, + }); } Uri? explorerBlockUrl(String blockId) { if (blockId.isEmpty) return null; - return explorerPattern.buildUrl( - explorerPattern.blockPattern, - {'BLOCK': blockId}, - ); + return explorerPattern.buildUrl(explorerPattern.blockPattern, { + 'BLOCK': blockId, + }); } } diff --git a/packages/komodo_defi_types/test/tron_protocol_test.dart b/packages/komodo_defi_types/test/tron_protocol_test.dart index 43efa55a6..572d71f09 100644 --- a/packages/komodo_defi_types/test/tron_protocol_test.dart +++ b/packages/komodo_defi_types/test/tron_protocol_test.dart @@ -11,6 +11,9 @@ Map _trxConfig() => { 'decimals': 6, 'required_confirmations': 1, 'derivation_path': "m/44'/195'", + 'explorer_url': 'https://tronscan.org/', + 'explorer_tx_url': '#/transaction/', + 'explorer_address_url': '#/address/', 'protocol': { 'type': 'TRX', 'protocol_data': {'network': 'Mainnet'}, @@ -27,6 +30,9 @@ Map _trc20Config() => { 'mm2': 1, 'decimals': 6, 'derivation_path': "m/44'/195'", + 'explorer_url': 'https://tronscan.org/', + 'explorer_tx_url': '#/transaction/', + 'explorer_address_url': '#/address/', 'protocol': { 'type': 'TRC20', 'protocol_data': { @@ -116,7 +122,15 @@ void main() { expect(protocol.subClass, CoinSubClass.trx); expect((protocol as TrxProtocol).nodes, isEmpty); expect(protocol.network, 'Mainnet'); - expect(protocol.supportsTxHistoryStreaming(isChildAsset: false), isTrue); + expect(protocol.supportsTxHistoryStreaming(isChildAsset: false), isFalse); + expect( + protocol.explorerTxUrl('abc123')?.toString(), + 'https://tronscan.org/#/transaction/abc123', + ); + expect( + protocol.explorerAddressUrl('TAddress123')?.toString(), + 'https://tronscan.org/#/address/TAddress123', + ); }); test('Asset.fromJson links TRC20 child asset to TRX parent', () { @@ -128,7 +142,15 @@ void main() { expect(child.id.parentId, parent.id); expect(parent.id.subClass.canBeParentOf(child.id.subClass), isTrue); expect(child.supportsBalanceStreaming, isTrue); - expect(child.supportsTxHistoryStreaming, isTrue); + expect(child.supportsTxHistoryStreaming, isFalse); + expect( + child.protocol.explorerTxUrl('def456')?.toString(), + 'https://tronscan.org/#/transaction/def456', + ); + expect( + child.protocol.explorerAddressUrl('TTokenAddress456')?.toString(), + 'https://tronscan.org/#/address/TTokenAddress456', + ); }); test('non-TRON platform assets keep top-level subtype precedence', () { From 39dd1fc605ddfa009ab1c15ebb631a3be6bde36d Mon Sep 17 00:00:00 2001 From: Nitride <77973576+CharlVS@users.noreply.github.com> Date: Wed, 25 Mar 2026 00:43:39 +0100 Subject: [PATCH 25/40] feat(komodo_defi_sdk): add Tronscan transaction history strategy (#339) * feat(komodo_defi_sdk): add Tronscan transaction history strategy Implement TronscanTransactionStrategy for fetching transaction history from the Tronscan HTTP API for TRX native and TRC-20 token assets. - Support mainnet (apilist.tronscanapi.com) and Nile testnet endpoints - Fetch TRX transfers via /api/transfer, TRC-20 via /api/token_trc20/transfers - Two-layer pagination: server-side batching with client-side aggregation - Retry on 429/503 with Retry-After header and exponential backoff with jitter - Optional TRON-PRO-API-KEY header for improved rate limits - Merge duplicate TransactionInfo across multiple pubkey addresses - Wire through TransactionHistoryStrategyFactory, Manager, and bootstrap - Add KomodoDefiSdkConfig.tronProApiKey configuration field - Update tests for TRX/TRC-20 asset selection, ordering, and retry behavior * feat(transaction-history): migrate TRON history to TRONGrid --- .../lib/src/sdk/komodo_defi_sdk_config.dart | 7 + .../_transaction_history_index.dart | 1 + .../fixed_scale_decimal_string.dart | 30 + .../strategies/tron_grid_address_codec.dart | 119 ++++ .../strategies/tron_grid_cursor_codec.dart | 28 + ...tronscan_transaction_history_strategy.dart | 652 ++++++++++++++++++ .../transaction_history_manager.dart | 151 +++- .../transaction_history_strategies.dart | 1 + .../transaction_history_strategies_test.dart | 541 ++++++++++++++- .../transaction_history_strategy.dart | 11 +- 10 files changed, 1508 insertions(+), 33 deletions(-) create mode 100644 packages/komodo_defi_sdk/lib/src/transaction_history/strategies/fixed_scale_decimal_string.dart create mode 100644 packages/komodo_defi_sdk/lib/src/transaction_history/strategies/tron_grid_address_codec.dart create mode 100644 packages/komodo_defi_sdk/lib/src/transaction_history/strategies/tron_grid_cursor_codec.dart create mode 100644 packages/komodo_defi_sdk/lib/src/transaction_history/strategies/tronscan_transaction_history_strategy.dart diff --git a/packages/komodo_defi_sdk/lib/src/sdk/komodo_defi_sdk_config.dart b/packages/komodo_defi_sdk/lib/src/sdk/komodo_defi_sdk_config.dart index 14c00170d..5618c1fcb 100644 --- a/packages/komodo_defi_sdk/lib/src/sdk/komodo_defi_sdk_config.dart +++ b/packages/komodo_defi_sdk/lib/src/sdk/komodo_defi_sdk_config.dart @@ -11,6 +11,7 @@ class KomodoDefiSdkConfig { this.activationRetryDelay = const Duration(seconds: 2), this.activatedAssetsCacheTtl = const Duration(seconds: 10), this.marketDataConfig = const MarketDataConfig(), + this.tronProApiKey, }); /// Set of asset IDs that should be enabled by default @@ -38,6 +39,10 @@ class KomodoDefiSdkConfig { /// Configuration for market data repositories final MarketDataConfig marketDataConfig; + /// No longer used. Transaction history now uses TRONGrid which requires no + /// API key. Retained for backward compatibility. + final String? tronProApiKey; + KomodoDefiSdkConfig copyWith({ Set? defaultAssets, bool? preActivateDefaultAssets, @@ -47,6 +52,7 @@ class KomodoDefiSdkConfig { Duration? activationRetryDelay, Duration? activatedAssetsCacheTtl, MarketDataConfig? marketDataConfig, + String? tronProApiKey, }) { return KomodoDefiSdkConfig( defaultAssets: defaultAssets ?? this.defaultAssets, @@ -62,6 +68,7 @@ class KomodoDefiSdkConfig { activatedAssetsCacheTtl: activatedAssetsCacheTtl ?? this.activatedAssetsCacheTtl, marketDataConfig: marketDataConfig ?? this.marketDataConfig, + tronProApiKey: tronProApiKey ?? this.tronProApiKey, ); } } diff --git a/packages/komodo_defi_sdk/lib/src/transaction_history/_transaction_history_index.dart b/packages/komodo_defi_sdk/lib/src/transaction_history/_transaction_history_index.dart index 21d427799..9a7578f9f 100644 --- a/packages/komodo_defi_sdk/lib/src/transaction_history/_transaction_history_index.dart +++ b/packages/komodo_defi_sdk/lib/src/transaction_history/_transaction_history_index.dart @@ -4,6 +4,7 @@ library _transaction_history; export 'strategies/etherscan_transaction_history_strategy.dart'; +export 'strategies/tronscan_transaction_history_strategy.dart'; export 'strategies/zhtlc_transaction_strategy.dart'; export 'transaction_history_manager.dart'; export 'transaction_merge_utils.dart'; diff --git a/packages/komodo_defi_sdk/lib/src/transaction_history/strategies/fixed_scale_decimal_string.dart b/packages/komodo_defi_sdk/lib/src/transaction_history/strategies/fixed_scale_decimal_string.dart new file mode 100644 index 000000000..a247878f4 --- /dev/null +++ b/packages/komodo_defi_sdk/lib/src/transaction_history/strategies/fixed_scale_decimal_string.dart @@ -0,0 +1,30 @@ +/// Converts an integer or digit string [raw] into a fixed-scale decimal string +/// by inserting the decimal point [decimals] places from the right. +/// +/// Avoids `decimal` package division, which can stringify as rational +/// fractions (`n/d`) that do not round-trip through `Decimal.parse`. +String fixedScaleIntToDecimalString(int raw, int decimals) { + if (decimals <= 0) return raw.toString(); + return fixedScaleDigitStringToDecimalString(raw.toString(), decimals); +} + +/// Same as [fixedScaleIntToDecimalString] but for arbitrary-length integer +/// strings (e.g. TRC-20 `value` fields). +String fixedScaleBigIntStringToDecimalString(String raw, int decimals) { + if (decimals <= 0) return raw; + return fixedScaleDigitStringToDecimalString(raw, decimals); +} + +/// Core implementation shared by [fixedScaleIntToDecimalString] and +/// [fixedScaleBigIntStringToDecimalString]. +String fixedScaleDigitStringToDecimalString(String raw, int decimals) { + final negative = raw.startsWith('-'); + var digits = negative ? raw.substring(1) : raw; + digits = digits.padLeft(decimals + 1, '0'); + final intPart = digits.substring(0, digits.length - decimals); + final fracPart = digits.substring(digits.length - decimals); + final trimmedFrac = fracPart.replaceFirst(RegExp(r'0+$'), ''); + final sign = negative ? '-' : ''; + if (trimmedFrac.isEmpty) return '$sign$intPart'; + return '$sign$intPart.$trimmedFrac'; +} diff --git a/packages/komodo_defi_sdk/lib/src/transaction_history/strategies/tron_grid_address_codec.dart b/packages/komodo_defi_sdk/lib/src/transaction_history/strategies/tron_grid_address_codec.dart new file mode 100644 index 000000000..588134f0e --- /dev/null +++ b/packages/komodo_defi_sdk/lib/src/transaction_history/strategies/tron_grid_address_codec.dart @@ -0,0 +1,119 @@ +import 'dart:typed_data' show Uint8List; + +import 'package:crypto/crypto.dart' show sha256; + +/// True if [s] is non-empty and contains only ASCII hex digits. +bool _isAsciiHex(String s) => s.isNotEmpty && _asciiHexOnly.hasMatch(s); + +final RegExp _asciiHexOnly = RegExp(r'^[0-9a-fA-F]+$'); + +const String _base58Alphabet = + '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz'; + +/// Compares two TRON addresses after normalising to lowercase hex (`41` + 20 +/// bytes) so Base58Check pubkeys match TRONGrid hex in `raw_data`. +bool tronAddressesEqual(String a, String b) => + normalizeTronAddressToHex(a) == normalizeTronAddressToHex(b); + +/// Lowercase hex `41…` (42 chars) for a TRON address in hex or Base58Check. +String normalizeTronAddressToHex(String address) { + if (address.isEmpty) return address; + + if (address.length == 42 && + address.startsWith('41') && + _isAsciiHex(address)) { + return address.toLowerCase(); + } + + if (address.startsWith('T') || address.startsWith('4')) { + final hex = _base58CheckPayloadToHex(address); + if (hex != null) return hex.toLowerCase(); + } + + return address.toLowerCase(); +} + +/// Returns [address] in Base58Check when it is 42-char hex; else unchanged. +String tronAddressForDisplay(String address) { + if (address.isEmpty) return address; + if (address.length == 42 && + address.toLowerCase().startsWith('41') && + _isAsciiHex(address)) { + return _hex41ToBase58Check(address); + } + return address; +} + +String? _base58CheckPayloadToHex(String base58) { + try { + var value = BigInt.zero; + for (var i = 0; i < base58.length; i++) { + final digit = _base58Alphabet.indexOf(base58[i]); + if (digit < 0) return null; + value = value * BigInt.from(58) + BigInt.from(digit); + } + + var hex = value.toRadixString(16); + if (hex.length.isOdd) hex = '0$hex'; + + var leadingOnes = 0; + for (var i = 0; i < base58.length && base58[i] == '1'; i++) { + leadingOnes++; + } + hex = '00' * leadingOnes + hex; + + if (hex.length < 50) return null; + return hex.substring(0, 42); + } on Object { + return null; + } +} + +String _hex41ToBase58Check(String hex) { + try { + final cleanHex = hex.replaceFirst(RegExp('^0x'), '').toLowerCase(); + if (cleanHex.length != 42 || !cleanHex.startsWith('41')) return hex; + + final addressBytes = _hexToBytes(cleanHex); + final hash1 = sha256.convert(addressBytes).bytes; + final hash2 = sha256.convert(hash1).bytes; + final checksum = hash2.sublist(0, 4); + + final full = Uint8List(addressBytes.length + 4) + ..setAll(0, addressBytes) + ..setAll(addressBytes.length, checksum); + + return _bytesToBase58(full); + } on Object { + return hex; + } +} + +Uint8List _hexToBytes(String hex) { + final result = Uint8List(hex.length ~/ 2); + for (var i = 0; i < hex.length; i += 2) { + result[i ~/ 2] = int.parse(hex.substring(i, i + 2), radix: 16); + } + return result; +} + +String _bytesToBase58(Uint8List bytes) { + var value = BigInt.zero; + for (final b in bytes) { + value = (value << 8) + BigInt.from(b); + } + + final sb = StringBuffer(); + while (value > BigInt.zero) { + final remainder = (value % BigInt.from(58)).toInt(); + value = value ~/ BigInt.from(58); + sb.write(_base58Alphabet[remainder]); + } + + for (final b in bytes) { + if (b != 0) break; + sb.write('1'); + } + + return sb.toString().split('').reversed.join(); +} diff --git a/packages/komodo_defi_sdk/lib/src/transaction_history/strategies/tron_grid_cursor_codec.dart b/packages/komodo_defi_sdk/lib/src/transaction_history/strategies/tron_grid_cursor_codec.dart new file mode 100644 index 000000000..ae9f4e85a --- /dev/null +++ b/packages/komodo_defi_sdk/lib/src/transaction_history/strategies/tron_grid_cursor_codec.dart @@ -0,0 +1,28 @@ +import 'dart:convert' show jsonDecode, jsonEncode; + +/// Encodes a `{address: fingerprint}` map for transaction history `fromId`. +/// +/// Always JSON-encodes so address keys are preserved (see strategy docs). +String encodeTronGridCursorMap(Map cursors) => + jsonEncode(cursors); + +/// Decodes a cursor string from transaction history `fromId`. +/// +/// JSON objects become per-address maps. A bare non-JSON string is stored under +/// `''` as a wildcard fingerprint for single-address continuation. +Map decodeTronGridCursorMap(String? fromId) { + if (fromId == null || fromId.isEmpty) return {}; + + if (fromId.startsWith('{')) { + try { + final decoded = jsonDecode(fromId); + if (decoded is Map) { + return decoded.map((k, v) => MapEntry(k.toString(), v.toString())); + } + } on FormatException catch (_) { + // Not valid JSON; treat as a bare fingerprint. + } + } + + return {'': fromId}; +} diff --git a/packages/komodo_defi_sdk/lib/src/transaction_history/strategies/tronscan_transaction_history_strategy.dart b/packages/komodo_defi_sdk/lib/src/transaction_history/strategies/tronscan_transaction_history_strategy.dart new file mode 100644 index 000000000..b053c215e --- /dev/null +++ b/packages/komodo_defi_sdk/lib/src/transaction_history/strategies/tronscan_transaction_history_strategy.dart @@ -0,0 +1,652 @@ +import 'dart:math' as math; + +import 'package:decimal/decimal.dart'; +import 'package:http/http.dart' as http; +import 'package:komodo_defi_rpc_methods/komodo_defi_rpc_methods.dart'; +import 'package:komodo_defi_sdk/src/pubkeys/pubkey_manager.dart'; +import 'package:komodo_defi_sdk/src/transaction_history/strategies/fixed_scale_decimal_string.dart'; +import 'package:komodo_defi_sdk/src/transaction_history/strategies/tron_grid_address_codec.dart'; +import 'package:komodo_defi_sdk/src/transaction_history/strategies/tron_grid_cursor_codec.dart'; +import 'package:komodo_defi_types/komodo_defi_type_utils.dart'; +import 'package:komodo_defi_types/komodo_defi_types.dart'; + +/// Fetches TRX and TRC-20 (on TRX) history from the TRONGrid HTTP API, +/// one page at a time so the manager can stream results to the UI. +/// +/// Uses the public TRONGrid API which does **not** require an API key. +/// Rate-limited to 3 RPS with a 3-second suspension on violation. +/// +/// **Pagination contract:** TRONGrid uses cursor-based pagination via an opaque +/// `fingerprint` token. This strategy stores the fingerprint in +/// [MyTxHistoryResponse.fromId] so the manager can pass it back via +/// [TransactionBasedPagination.fromId] on the next call. When `fromId` is +/// `null`, there are no more pages. +/// +/// See [TRX transactions](https://developers.tron.network/reference/get-transaction-info-by-account-address) +/// and [TRC-20 transfers](https://developers.tron.network/reference/trc20-transaction-information-by-account-address). +class TronGridTransactionStrategy extends TransactionHistoryStrategy { + /// Creates a strategy that fetches TRON history from TRONGrid page-by-page. + TronGridTransactionStrategy({ + required this.pubkeyManager, + http.Client? httpClient, + this.tronProApiKey, + String? apiHostOverride, + }) : _client = httpClient ?? http.Client(), + _ownsClient = httpClient == null, + _apiHostOverride = apiHostOverride; + + /// Rows per TRONGrid API request (their maximum). + static const int _gridPageSize = 200; + + /// Cursor marker for addresses that haven't been fetched yet. Used to enable + /// per-address incremental streaming: the strategy returns results as soon as + /// the first address yields data, encoding remaining addresses with this + /// marker so the manager's streaming loop fetches them on subsequent calls. + static const String _kPending = '__pending__'; + + /// For TRX (where rows are filtered client-side for TransferContract), fetch + /// up to this many TRONGrid pages per [fetchTransactionHistory] call so a + /// single invocation still returns a meaningful batch. + static const int _maxInternalPages = 3; + + static const int _maxHttpAttempts = 6; + + /// Minimum gap between HTTP requests to stay within 3 RPS. + static const Duration _minRequestInterval = Duration(milliseconds: 350); + + final http.Client _client; + final bool _ownsClient; + final String? _apiHostOverride; + + /// Provides public-key / address data for the asset being queried. + final PubkeyManager pubkeyManager; + + /// Retained for backward compatibility; TRONGrid does not require a key. + final String? tronProApiKey; + + DateTime _lastRequestTime = DateTime.fromMillisecondsSinceEpoch(0); + + @override + bool get usesOpaquePaginationCursor => true; + + @override + /// Both page-based (initial fetch) and cursor-based (streaming) pagination. + Set get supportedPaginationModes => { + PagePagination, + TransactionBasedPagination, + }; + + @override + bool supportsAsset(Asset asset) => switch (asset.protocol) { + TrxProtocol() => true, + Trc20Protocol(:final platform) => platform == 'TRX', + _ => false, + }; + + @override + bool requiresKdfTransactionHistory(Asset asset) => false; + + @override + Future fetchTransactionHistory( + ApiClient client, + Asset asset, + TransactionPagination pagination, + ) async { + if (!supportsAsset(asset)) { + throw UnsupportedError( + 'Asset ${asset.id.name} is not supported by ' + 'TronGridTransactionStrategy', + ); + } + + validatePagination(pagination); + + final host = _apiHostOverride ?? _defaultApiHost(asset.protocol); + final addresses = await _getAssetPubkeys(asset); + final limit = pagination.limit ?? 50; + + // Decode per-address cursors. On the first call (PagePagination) the map + // is empty so every address starts from the beginning. On subsequent calls + // the map contains only addresses that still have remaining pages. + final cursors = + pagination is TransactionBasedPagination && + !_looksLikeTransactionHash(pagination.fromId) + ? decodeTronGridCursorMap(pagination.fromId) + : {}; + + final byHash = {}; + final nextCursors = {}; + + try { + for (var i = 0; i < addresses.length; i++) { + final addr = addresses[i].address; + + // On a continuation call, skip addresses already exhausted (absent + // from the cursor map). The empty-key wildcard ('') from the single- + // address fast path matches any address. + if (cursors.isNotEmpty && + !cursors.containsKey(addr) && + !cursors.containsKey('')) { + continue; + } + + // Resolve fingerprint: exact address key first, then wildcard (''). + // The _kPending marker means "not yet started" — treat as null. + var fp = cursors[addr] ?? cursors['']; + if (fp == _kPending) fp = null; + + final result = switch (asset.protocol) { + TrxProtocol() => await _fetchTrxPage( + host: host, + address: addr, + asset: asset, + fingerprint: fp, + limit: limit, + ), + Trc20Protocol() => await _fetchTrc20Page( + host: host, + address: addr, + asset: asset, + fingerprint: fp, + ), + _ => const _PageResult(transactions: []), + }; + + for (final tx in result.transactions) { + final h = tx.txHash; + final existing = byHash[h]; + byHash[h] = existing == null + ? tx + : _mergeTransactionInfo(existing, tx); + } + + if (result.nextFingerprint != null) { + nextCursors[addr] = result.nextFingerprint!; + } + + // Yield results early: if we already have transactions and there are + // remaining addresses, encode them as pending in the cursor so the + // manager's streaming loop fetches them on the next call. + if (byHash.isNotEmpty && i < addresses.length - 1) { + for (var j = i + 1; j < addresses.length; j++) { + final pending = addresses[j].address; + if (!nextCursors.containsKey(pending)) { + nextCursors[pending] = cursors[pending] ?? _kPending; + } + } + break; + } + } + + final transactions = byHash.values.toList() + ..sort((a, b) => b.timestamp.compareTo(a.timestamp)); + + final currentBlock = transactions.isNotEmpty + ? transactions.first.blockHeight + : 0; + + final cursorString = nextCursors.isEmpty + ? null + : encodeTronGridCursorMap(nextCursors); + + return MyTxHistoryResponse( + mmrpc: RpcVersion.v2_0, + currentBlock: currentBlock, + fromId: cursorString, + limit: limit, + skipped: 0, + syncStatus: SyncStatusResponse( + state: TransactionSyncStatusEnum.finished, + ), + total: transactions.length, + totalPages: 1, + pageNumber: 1, + pagingOptions: cursorString != null + ? Pagination(fromId: cursorString) + : null, + transactions: transactions, + ); + } catch (e) { + throw HttpException('Error fetching TRONGrid transaction history: $e'); + } + } + + // --------------------------------------------------------------------------- + // Single-page fetch methods + // --------------------------------------------------------------------------- + + /// Fetches up to [_maxInternalPages] TRONGrid pages of general transactions, + /// filtering for `TransferContract` rows, until at least [limit] results are + /// collected or there are no more pages. + Future<_PageResult> _fetchTrxPage({ + required String host, + required String address, + required Asset asset, + required int limit, + String? fingerprint, + }) async { + final decimals = _decimals(asset); + final out = []; + var cursor = fingerprint; + + for (var page = 0; page < _maxInternalPages; page++) { + await _throttle(); + + final params = { + 'only_confirmed': 'true', + 'limit': '$_gridPageSize', + 'visible': 'true', + }; + if (cursor != null) params['fingerprint'] = cursor; + + final uri = Uri.https(host, '/v1/accounts/$address/transactions', params); + + final json = await _getJson(uri); + final data = json.valueOrNull('data') ?? const []; + + if (data.isEmpty) { + cursor = null; + break; + } + + for (final row in data) { + final tx = _gridTrxRowToTransactionInfo( + row: row, + viewerAddress: address, + coinId: asset.id.id, + decimals: decimals, + ); + if (tx != null) out.add(tx); + } + + final fp = _nextTronGridPageFingerprint(json); + if (fp == null) { + cursor = null; + break; + } + cursor = fp; + + if (out.length >= limit) break; + } + + return _PageResult(transactions: out, nextFingerprint: cursor); + } + + /// Fetches a single TRONGrid page of TRC-20 transfers. Every row is relevant + /// (no client-side type filtering), so one page is sufficient per call. + Future<_PageResult> _fetchTrc20Page({ + required String host, + required String address, + required Asset asset, + String? fingerprint, + }) async { + final contract = _trc20ContractAddress(asset); + if (contract == null || contract.isEmpty) { + return const _PageResult(transactions: []); + } + + final decimals = _tokenDecimalsFromRowOrAsset(asset); + + await _throttle(); + + final params = { + 'only_confirmed': 'true', + 'limit': '$_gridPageSize', + 'contract_address': contract, + }; + if (fingerprint != null) params['fingerprint'] = fingerprint; + + final uri = Uri.https( + host, + '/v1/accounts/$address/transactions/trc20', + params, + ); + + final json = await _getJson(uri); + final data = json.valueOrNull('data') ?? const []; + if (data.isEmpty) { + return const _PageResult(transactions: []); + } + + final out = []; + for (final row in data) { + final tx = _gridTrc20RowToTransactionInfo( + row: row, + viewerAddress: address, + coinId: asset.id.id, + decimals: decimals, + ); + if (tx != null) out.add(tx); + } + + final fp = _nextTronGridPageFingerprint(json); + + return _PageResult(transactions: out, nextFingerprint: fp); + } + + // --------------------------------------------------------------------------- + // Helpers + // --------------------------------------------------------------------------- + + /// Opaque TRONGrid `meta.fingerprint` token, or `null` when absent / empty. + String? _nextTronGridPageFingerprint(JsonMap json) { + final fp = json + .valueOrNull('meta') + ?.valueOrNull('fingerprint'); + if (fp == null || fp.isEmpty) return null; + return fp; + } + + Future> _getAssetPubkeys(Asset asset) async { + return (await pubkeyManager.getPubkeys(asset)).keys; + } + + String _defaultApiHost(ProtocolClass protocol) { + return protocol.isTestnet ? 'nile.trongrid.io' : 'api.trongrid.io'; + } + + bool _looksLikeTransactionHash(String value) => + RegExp(r'^[0-9A-Fa-f]{64}$').hasMatch(value); + + int _decimals(Asset asset) => + asset.protocol.config.valueOrNull('decimals') ?? 6; + + String? _trc20ContractAddress(Asset asset) { + final config = asset.protocol.config; + return config.valueOrNull('contract_address') ?? + config.valueOrNull( + 'protocol', + 'protocol_data', + 'contract_address', + ); + } + + int _tokenDecimalsFromRowOrAsset(Asset asset) { + return asset.protocol.config.valueOrNull('decimals') ?? 18; + } + + // --------------------------------------------------------------------------- + // Row → TransactionInfo mappers + // --------------------------------------------------------------------------- + + /// Maps a TRONGrid `/v1/accounts/.../transactions` row (fetched with + /// `visible=true`) to [TransactionInfo]. Addresses are Base58 thanks to + /// the visibility flag, consistent with the TRC-20 mapper. + TransactionInfo? _gridTrxRowToTransactionInfo({ + required JsonMap row, + required String viewerAddress, + required String coinId, + required int decimals, + }) { + final hash = row.valueOrNull('txID'); + if (hash == null || hash.isEmpty) return null; + + final rawData = row.valueOrNull('raw_data'); + final contracts = rawData?.valueOrNull('contract'); + if (contracts == null || contracts.isEmpty) return null; + + final first = contracts.first; + final contractType = first.valueOrNull('type'); + if (contractType != 'TransferContract') return null; + + final retList = row.valueOrNull('ret'); + final retMap = (retList != null && retList.isNotEmpty) + ? retList.first as JsonMap? + : null; + final contractRet = retMap?.valueOrNull('contractRet'); + if (contractRet != 'SUCCESS') return null; + + final value = first + .valueOrNull('parameter') + ?.valueOrNull('value'); + if (value == null) return null; + + final from = value.valueOrNull('owner_address') ?? ''; + final to = value.valueOrNull('to_address') ?? ''; + final amountRaw = value.valueOrNull('amount'); + if (amountRaw == null) return null; + + final block = row.valueOrNull('blockNumber') ?? 0; + final tsMs = row.valueOrNull('block_timestamp') ?? 0; + final tsSec = tsMs ~/ 1000; + + final absHuman = fixedScaleIntToDecimalString(amountRaw.toInt(), decimals); + + final isOut = tronAddressesEqual(from, viewerAddress); + final isIn = tronAddressesEqual(to, viewerAddress); + if (!isOut && !isIn) return null; + + final (signedBalance, spentByMe, receivedByMe) = _classifyDirection( + isOut: isOut, + isIn: isIn, + absHuman: absHuman, + ); + + return TransactionInfo( + txHash: hash, + from: [tronAddressForDisplay(from)], + to: [tronAddressForDisplay(to)], + myBalanceChange: signedBalance, + blockHeight: block, + confirmations: 1, + timestamp: tsSec, + feeDetails: null, + coin: coinId, + internalId: hash, + spentByMe: spentByMe, + receivedByMe: receivedByMe, + memo: null, + ); + } + + TransactionInfo? _gridTrc20RowToTransactionInfo({ + required JsonMap row, + required String viewerAddress, + required String coinId, + required int decimals, + }) { + final hash = row.valueOrNull('transaction_id'); + if (hash == null || hash.isEmpty) return null; + + final from = row.valueOrNull('from') ?? ''; + final to = row.valueOrNull('to') ?? ''; + final rawValue = row.valueOrNull('value'); + if (rawValue == null || rawValue.isEmpty) return null; + + final tokenInfo = row.valueOrNull('token_info'); + final dec = tokenInfo?.valueOrNull('decimals') ?? decimals; + + final tsMs = row.valueOrNull('block_timestamp') ?? 0; + final tsSec = tsMs ~/ 1000; + + final absHuman = fixedScaleBigIntStringToDecimalString(rawValue, dec); + + final isOut = tronAddressesEqual(from, viewerAddress); + final isIn = tronAddressesEqual(to, viewerAddress); + if (!isOut && !isIn) return null; + + final (signedBalance, spentByMe, receivedByMe) = _classifyDirection( + isOut: isOut, + isIn: isIn, + absHuman: absHuman, + ); + + return TransactionInfo( + txHash: hash, + from: [from], + to: [to], + myBalanceChange: signedBalance, + blockHeight: 0, + confirmations: 1, + timestamp: tsSec, + feeDetails: null, + coin: coinId, + internalId: hash, + spentByMe: spentByMe, + receivedByMe: receivedByMe, + memo: null, + ); + } + + // --------------------------------------------------------------------------- + // Direction + // --------------------------------------------------------------------------- + + (String signedBalance, String spentByMe, String receivedByMe) + _classifyDirection({ + required bool isOut, + required bool isIn, + required String absHuman, + }) { + if (isOut && !isIn) return ('-$absHuman', absHuman, '0'); + if (isIn && !isOut) return (absHuman, '0', absHuman); + return ('0', absHuman, absHuman); + } + + // --------------------------------------------------------------------------- + // Merge duplicates (same tx seen from multiple pubkeys) + // --------------------------------------------------------------------------- + + TransactionInfo _mergeTransactionInfo(TransactionInfo a, TransactionInfo b) { + final net = + (Decimal.parse(a.myBalanceChange) + Decimal.parse(b.myBalanceChange)) + .toString(); + final spentA = a.spentByMe != null + ? Decimal.parse(a.spentByMe!) + : Decimal.zero; + final spentB = b.spentByMe != null + ? Decimal.parse(b.spentByMe!) + : Decimal.zero; + final recvA = a.receivedByMe != null + ? Decimal.parse(a.receivedByMe!) + : Decimal.zero; + final recvB = b.receivedByMe != null + ? Decimal.parse(b.receivedByMe!) + : Decimal.zero; + + return TransactionInfo( + txHash: a.txHash, + from: {...a.from, ...b.from}.toList(), + to: {...a.to, ...b.to}.toList(), + myBalanceChange: net, + blockHeight: a.blockHeight, + confirmations: a.confirmations > b.confirmations + ? a.confirmations + : b.confirmations, + timestamp: a.timestamp > b.timestamp ? a.timestamp : b.timestamp, + feeDetails: a.feeDetails ?? b.feeDetails, + coin: a.coin, + internalId: a.internalId, + spentByMe: (spentA + spentB).toString(), + receivedByMe: (recvA + recvB).toString(), + memo: a.memo ?? b.memo, + ); + } + + // --------------------------------------------------------------------------- + // Rate limiter — keeps requests within TRONGrid's 3 RPS budget + // --------------------------------------------------------------------------- + + Future _throttle() async { + final elapsed = DateTime.now().difference(_lastRequestTime); + if (elapsed < _minRequestInterval) { + await Future.delayed(_minRequestInterval - elapsed); + } + _lastRequestTime = DateTime.now(); + } + + // --------------------------------------------------------------------------- + // HTTP with retry & TRONGrid rate-limit parsing + // --------------------------------------------------------------------------- + + Future _getJson(Uri uri) async { + final random = math.Random(); + var attempt = 0; + var backoff = const Duration(milliseconds: 500); + const maxBackoff = Duration(seconds: 10); + + while (true) { + try { + final response = await _client.get(uri); + if (response.statusCode == 200) { + return jsonFromString(response.body); + } + + final retriable = + response.statusCode == 429 || response.statusCode == 503; + if (retriable && attempt < _maxHttpAttempts - 1) { + final baseWait = _parseRetryWait(response) ?? backoff; + final jitter = Duration(milliseconds: random.nextInt(250)); + await Future.delayed(baseWait + jitter); + _lastRequestTime = DateTime.now(); + attempt++; + final doubled = backoff.inMilliseconds * 2; + backoff = doubled > maxBackoff.inMilliseconds + ? maxBackoff + : Duration(milliseconds: doubled); + continue; + } + + throw HttpException( + 'TRONGrid request failed: ${response.statusCode}', + uri: uri, + ); + } on http.ClientException catch (e) { + if (attempt >= _maxHttpAttempts - 1) { + throw HttpException( + 'Network error while fetching TRONGrid history: ${e.message}', + uri: uri, + ); + } + final jitter = Duration(milliseconds: random.nextInt(250)); + await Future.delayed(backoff + jitter); + attempt++; + backoff = backoff.inMilliseconds * 2 > maxBackoff.inMilliseconds + ? maxBackoff + : Duration(milliseconds: backoff.inMilliseconds * 2); + } + } + } + + /// Extracts the wait duration from a TRONGrid rate-limit response. + /// + /// Checks the `Retry-After` header first, then falls back to parsing the + /// JSON body for the `"suspended for N s"` pattern that TRONGrid returns. + Duration? _parseRetryWait(http.Response response) { + final header = response.headers['retry-after']; + if (header != null) { + final seconds = int.tryParse(header.trim()); + if (seconds != null) { + return Duration(seconds: seconds.clamp(0, 60)); + } + } + + try { + final body = jsonFromString(response.body); + final error = body.valueOrNull('Error') ?? ''; + final match = RegExp(r'suspended for (\d+) s').firstMatch(error); + if (match != null) { + final seconds = int.parse(match.group(1)!); + return Duration(seconds: seconds.clamp(0, 60)); + } + } on FormatException catch (_) { + // Body is not valid JSON; fall through to return null. + } + + return null; + } + + /// Releases the HTTP client if it was internally created. + void dispose() { + if (_ownsClient) { + _client.close(); + } + } +} + +/// Result of a single page fetch from TRONGrid. +class _PageResult { + const _PageResult({required this.transactions, this.nextFingerprint}); + final List transactions; + final String? nextFingerprint; +} diff --git a/packages/komodo_defi_sdk/lib/src/transaction_history/transaction_history_manager.dart b/packages/komodo_defi_sdk/lib/src/transaction_history/transaction_history_manager.dart index a3f09b494..6598c7107 100644 --- a/packages/komodo_defi_sdk/lib/src/transaction_history/transaction_history_manager.dart +++ b/packages/komodo_defi_sdk/lib/src/transaction_history/transaction_history_manager.dart @@ -91,6 +91,11 @@ class TransactionHistoryManager implements _TransactionHistoryManager { static const _maxPollingRetries = 3; static const _maxBatchSize = 50; + /// Max consecutive strategy responses with no transactions but a non-null + /// cursor (e.g. TRX history skipping non-TransferContract pages). Prevents + /// unbounded loops on accounts with only contract traffic. + static const _maxEmptyPages = 10; + bool _isDisposed = false; StreamSubscription? _authSubscription; @@ -296,6 +301,7 @@ class TransactionHistoryManager implements _TransactionHistoryManager { var hasMore = true; var retryCount = 0; const maxRetries = 3; + var consecutiveEmptyPages = 0; while (hasMore && !_isDisposed) { try { @@ -314,10 +320,21 @@ class TransactionHistoryManager implements _TransactionHistoryManager { ); if (response.transactions.isEmpty) { - hasMore = false; + if (response.fromId != null) { + consecutiveEmptyPages++; + if (consecutiveEmptyPages > _maxEmptyPages) { + hasMore = false; + } else { + fromId = response.fromId; + } + } else { + hasMore = false; + } continue; } + consecutiveEmptyPages = 0; + final transactions = response.transactions .map((tx) => tx.asTransaction(asset.id)) .toList(); @@ -327,12 +344,10 @@ class TransactionHistoryManager implements _TransactionHistoryManager { fromId = response.fromId; - if (response.transactions.length < _maxBatchSize || fromId == null) { + if (fromId == null) { hasMore = false; - } else { - await Future.delayed(const Duration(milliseconds: 200)); } - } catch (e) { + } catch (_) { retryCount++; if (retryCount >= maxRetries) { hasMore = false; @@ -409,11 +424,23 @@ class TransactionHistoryManager implements _TransactionHistoryManager { try { final strategy = _strategyFactory.forAsset(asset); - var fromId = await _storage.getLatestTransactionId( + final latestStoredId = await _storage.getLatestTransactionId( asset.id, await _getCurrentWalletId(), ); + if (strategy.usesOpaquePaginationCursor && latestStoredId != null) { + final newTransactions = await _fetchOpaqueCursorTransactionsSince( + asset, + strategy: strategy, + latestStoredId: latestStoredId, + ); + await _batchStoreTransactions(newTransactions); + return; + } + + var fromId = latestStoredId; var hasMore = true; + var consecutiveEmptyPages = 0; while (hasMore && !_isDisposed) { await _rateLimiter.throttle(); @@ -433,10 +460,21 @@ class TransactionHistoryManager implements _TransactionHistoryManager { ); if (response.transactions.isEmpty) { - hasMore = false; + if (response.fromId != null) { + consecutiveEmptyPages++; + if (consecutiveEmptyPages > _maxEmptyPages) { + hasMore = false; + } else { + fromId = response.fromId; + } + } else { + hasMore = false; + } continue; } + consecutiveEmptyPages = 0; + final transactions = response.transactions .map((tx) => tx.asTransaction(asset.id)) .toList(); @@ -444,7 +482,7 @@ class TransactionHistoryManager implements _TransactionHistoryManager { await _batchStoreTransactions(transactions); fromId = response.fromId; - if (response.transactions.length < _maxBatchSize) { + if (fromId == null) { hasMore = false; } } @@ -453,6 +491,65 @@ class TransactionHistoryManager implements _TransactionHistoryManager { } } + Future> _fetchOpaqueCursorTransactionsSince( + Asset asset, { + required TransactionHistoryStrategy strategy, + required String latestStoredId, + }) async { + String? fromId; + var hasMore = true; + var consecutiveEmptyPages = 0; + final newTransactions = []; + + while (hasMore && !_isDisposed) { + final response = await strategy.fetchTransactionHistory( + _client, + asset, + fromId != null + ? TransactionBasedPagination( + fromId: fromId, + itemCount: _maxBatchSize, + ) + : const PagePagination(pageNumber: 1, itemsPerPage: _maxBatchSize), + ); + + if (response.transactions.isEmpty) { + if (response.fromId != null) { + consecutiveEmptyPages++; + if (consecutiveEmptyPages > _maxEmptyPages) { + hasMore = false; + } else { + fromId = response.fromId; + } + } else { + hasMore = false; + } + continue; + } + + consecutiveEmptyPages = 0; + + var reachedStoredHead = false; + for (final tx in response.transactions.map( + (tx) => tx.asTransaction(asset.id), + )) { + if (tx.internalId == latestStoredId) { + reachedStoredHead = true; + break; + } + newTransactions.add(tx); + } + + if (reachedStoredHead || response.fromId == null) { + hasMore = false; + } else { + fromId = response.fromId; + } + } + + return newTransactions; + } + @override Future clearTransactionHistory(Asset asset) async { if (_isDisposed) return; @@ -677,24 +774,30 @@ class TransactionHistoryManager implements _TransactionHistoryManager { await _getCurrentWalletId(), ); - final response = await strategy.fetchTransactionHistory( - _client, - asset, - latestId != null - ? TransactionBasedPagination( - fromId: latestId, - itemCount: _maxBatchSize, - ) - : const PagePagination(pageNumber: 1, itemsPerPage: _maxBatchSize), - ); - if (!_isPollingActive(asset.id)) return; - if (response.transactions.isNotEmpty) { - final newTransactions = response.transactions - .map((tx) => tx.asTransaction(asset.id)) - .toList(); - + final newTransactions = + strategy.usesOpaquePaginationCursor && latestId != null + ? await _fetchOpaqueCursorTransactionsSince( + asset, + strategy: strategy, + latestStoredId: latestId, + ) + : (await strategy.fetchTransactionHistory( + _client, + asset, + latestId != null + ? TransactionBasedPagination( + fromId: latestId, + itemCount: _maxBatchSize, + ) + : const PagePagination( + pageNumber: 1, + itemsPerPage: _maxBatchSize, + ), + )).transactions.map((tx) => tx.asTransaction(asset.id)).toList(); + + if (newTransactions.isNotEmpty) { await _batchStoreTransactions(newTransactions); final controller = _streamControllers[asset.id]; diff --git a/packages/komodo_defi_sdk/lib/src/transaction_history/transaction_history_strategies.dart b/packages/komodo_defi_sdk/lib/src/transaction_history/transaction_history_strategies.dart index 87620db93..eb7f1f532 100644 --- a/packages/komodo_defi_sdk/lib/src/transaction_history/transaction_history_strategies.dart +++ b/packages/komodo_defi_sdk/lib/src/transaction_history/transaction_history_strategies.dart @@ -14,6 +14,7 @@ class TransactionHistoryStrategyFactory { strategies ?? [ EtherscanTransactionStrategy(pubkeyManager: pubkeyManager), + TronGridTransactionStrategy(pubkeyManager: pubkeyManager), V2TransactionStrategy(auth), const LegacyTransactionStrategy(), const ZhtlcTransactionStrategy(), diff --git a/packages/komodo_defi_sdk/test/transaction_history/transaction_history_strategies_test.dart b/packages/komodo_defi_sdk/test/transaction_history/transaction_history_strategies_test.dart index 12c2b070c..a79e35454 100644 --- a/packages/komodo_defi_sdk/test/transaction_history/transaction_history_strategies_test.dart +++ b/packages/komodo_defi_sdk/test/transaction_history/transaction_history_strategies_test.dart @@ -1,6 +1,10 @@ +import 'dart:convert'; + +import 'package:http/http.dart' as http; import 'package:komodo_defi_local_auth/komodo_defi_local_auth.dart'; import 'package:komodo_defi_sdk/src/pubkeys/pubkey_manager.dart'; import 'package:komodo_defi_sdk/src/transaction_history/strategies/etherscan_transaction_history_strategy.dart'; +import 'package:komodo_defi_sdk/src/transaction_history/strategies/tronscan_transaction_history_strategy.dart'; import 'package:komodo_defi_sdk/src/transaction_history/strategies/zhtlc_transaction_strategy.dart'; import 'package:komodo_defi_sdk/src/transaction_history/transaction_history_strategies.dart'; import 'package:komodo_defi_types/komodo_defi_types.dart'; @@ -11,6 +15,10 @@ class _MockPubkeyManager extends Mock implements PubkeyManager {} class _MockLocalAuth extends Mock implements KomodoDefiLocalAuth {} +class _MockHttpClient extends Mock implements http.Client {} + +class _MockApiClient extends Mock implements ApiClient {} + Asset _createEvmAsset({ required String coin, required int chainId, @@ -31,6 +39,54 @@ Asset _createEvmAsset({ }); } +Asset _createTrxAsset() { + return Asset.fromJson({ + 'coin': 'TRX', + 'type': 'TRX', + 'name': 'TRON', + 'fname': 'TRON', + 'wallet_only': true, + 'mm2': 1, + 'decimals': 6, + 'required_confirmations': 1, + 'derivation_path': "m/44'/195'", + 'explorer_url': 'https://tronscan.org/', + 'explorer_tx_url': '#/transaction/', + 'explorer_address_url': '#/address/', + 'protocol': { + 'type': 'TRX', + 'protocol_data': {'network': 'Mainnet'}, + }, + 'nodes': >[], + }); +} + +Asset _createUsdtTrc20Asset() { + return Asset.fromJson({ + 'coin': 'USDT-TRC20', + 'type': 'TRC-20', + 'name': 'Tether', + 'fname': 'Tether', + 'wallet_only': true, + 'mm2': 1, + 'decimals': 6, + 'derivation_path': "m/44'/195'", + 'explorer_url': 'https://tronscan.org/', + 'explorer_tx_url': '#/transaction/', + 'explorer_address_url': '#/address/', + 'protocol': { + 'type': 'TRC20', + 'protocol_data': { + 'platform': 'TRX', + 'contract_address': 'TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t', + }, + }, + 'contract_address': 'TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t', + 'parent_coin': 'TRX', + 'nodes': >[], + }); +} + Asset _createZhtlcAsset() { final protocol = ZhtlcProtocol.fromJson(const { 'type': 'ZHTLC', @@ -54,10 +110,61 @@ Asset _createZhtlcAsset() { ); } +AssetPubkeys _makePubkeys(Asset asset) => AssetPubkeys( + assetId: asset.id, + keys: [ + PubkeyInfo( + address: 'TLa2f6VPqDgRE67v1736s7bJ8Ray5wYjU7', + derivationPath: null, + chain: null, + balance: BalanceInfo.zero(), + coinTicker: asset.id.id, + ), + ], + availableAddressesCount: 1, + syncStatus: SyncStatusEnum.success, +); + +Map _makeTrxTransferRow({ + required String txId, + required String ownerAddress, + required String toAddress, + required int amount, + required int timestamp, +}) => { + 'txID': txId, + 'blockNumber': 12345, + 'block_timestamp': timestamp, + 'ret': [ + {'contractRet': 'SUCCESS'}, + ], + 'raw_data': { + 'contract': [ + { + 'type': 'TransferContract', + 'parameter': { + 'value': { + 'owner_address': ownerAddress, + 'to_address': toAddress, + 'amount': amount, + }, + }, + }, + ], + }, +}; + void main() { late PubkeyManager pubkeyManager; late KomodoDefiLocalAuth auth; + setUpAll(() { + registerFallbackValue(_createTrxAsset()); + registerFallbackValue( + Uri.parse('https://api.trongrid.io/v1/accounts/T/transactions'), + ); + }); + setUp(() { pubkeyManager = _MockPubkeyManager(); auth = _MockLocalAuth(); @@ -77,7 +184,7 @@ void main() { expect(helper.shouldEnableTransactionHistory(eth), isFalse); }); - test('supports GRC20 endpoint and keeps KDF tx history disabled', () { + test('does not map GLEECT (GRC20) to Etherscan proxy endpoints', () { final gleect = _createEvmAsset( coin: 'GLEECT', chainId: 11169, @@ -85,11 +192,8 @@ void main() { isTestnet: true, ); - expect(helper.supportsProtocol(gleect), isTrue); - expect( - helper.getApiUrlForAsset(gleect)?.toString(), - endsWith('/v2/grc_tx_history'), - ); + expect(helper.supportsProtocol(gleect), isFalse); + expect(helper.getApiUrlForAsset(gleect), isNull); expect(helper.shouldEnableTransactionHistory(gleect), isFalse); }); }); @@ -122,7 +226,7 @@ void main() { expect(strategy, isA()); }); - test('uses Etherscan strategy for GRC20 chain', () { + test('uses Legacy strategy for GRC20 when Etherscan has no endpoint', () { final factory = TransactionHistoryStrategyFactory(pubkeyManager, auth); final gleect = _createEvmAsset( coin: 'GLEECT', @@ -133,7 +237,428 @@ void main() { final strategy = factory.forAsset(gleect); - expect(strategy, isA()); + expect(strategy, isA()); + }); + + test('selects Tronscan strategy for TRX asset', () { + final factory = TransactionHistoryStrategyFactory(pubkeyManager, auth); + final trx = _createTrxAsset(); + + final strategy = factory.forAsset(trx); + + expect(strategy, isA()); + }); + + test('selects Tronscan strategy for TRC20 on TRX', () { + final factory = TransactionHistoryStrategyFactory(pubkeyManager, auth); + final usdt = _createUsdtTrc20Asset(); + + final strategy = factory.forAsset(usdt); + + expect(strategy, isA()); + }); + + test('Legacy strategy wins over Tronscan when registered first', () { + final factory = TransactionHistoryStrategyFactory( + pubkeyManager, + auth, + strategies: [ + EtherscanTransactionStrategy(pubkeyManager: pubkeyManager), + const LegacyTransactionStrategy(), + TronGridTransactionStrategy(pubkeyManager: pubkeyManager), + V2TransactionStrategy(auth), + const ZhtlcTransactionStrategy(), + ], + ); + + final strategy = factory.forAsset(_createTrxAsset()); + + expect(strategy, isA()); + }); + }); + + group('TronscanTransactionStrategy', () { + test('retries on 429 with Retry-After header then succeeds', () async { + final httpClient = _MockHttpClient(); + final apiClient = _MockApiClient(); + var callCount = 0; + when(() => httpClient.get(any())).thenAnswer((_) async { + callCount++; + if (callCount == 1) { + return http.Response( + 'rate limited', + 429, + headers: {'retry-after': '0'}, + ); + } + return http.Response( + jsonEncode({'data': [], 'meta': {}}), + 200, + ); + }); + + final trx = _createTrxAsset(); + when( + () => pubkeyManager.getPubkeys(trx), + ).thenAnswer((_) async => _makePubkeys(trx)); + + final strategy = TronGridTransactionStrategy( + pubkeyManager: pubkeyManager, + httpClient: httpClient, + apiHostOverride: 'api.trongrid.io', + ); + + final response = await strategy.fetchTransactionHistory( + apiClient, + trx, + const PagePagination(pageNumber: 1, itemsPerPage: 20), + ); + + expect(callCount, 2); + expect(response.transactions, isEmpty); + verify(() => httpClient.get(any())).called(2); + }); + + test('retries on 429 with TRONGrid JSON body suspension', () async { + final httpClient = _MockHttpClient(); + final apiClient = _MockApiClient(); + var callCount = 0; + when(() => httpClient.get(any())).thenAnswer((_) async { + callCount++; + if (callCount == 1) { + return http.Response( + jsonEncode({ + 'Error': + 'request rate exceeded the allowed_rps(3), ' + 'and the query server is suspended for 3 s. ' + 'To obtain higher request quotas...', + }), + 429, + ); + } + return http.Response( + jsonEncode({'data': [], 'meta': {}}), + 200, + ); + }); + + final trx = _createTrxAsset(); + when( + () => pubkeyManager.getPubkeys(trx), + ).thenAnswer((_) async => _makePubkeys(trx)); + + final strategy = TronGridTransactionStrategy( + pubkeyManager: pubkeyManager, + httpClient: httpClient, + apiHostOverride: 'api.trongrid.io', + ); + + final response = await strategy.fetchTransactionHistory( + apiClient, + trx, + const PagePagination(pageNumber: 1, itemsPerPage: 20), + ); + + expect(callCount, 2); + expect(response.transactions, isEmpty); + }); + + test('uses TRONGrid API without custom auth headers', () async { + final httpClient = _MockHttpClient(); + final apiClient = _MockApiClient(); + Uri? capturedUri; + when(() => httpClient.get(any())).thenAnswer((invocation) async { + capturedUri = invocation.positionalArguments.first as Uri; + return http.Response( + jsonEncode({'data': [], 'meta': {}}), + 200, + ); + }); + + final trx = _createTrxAsset(); + when( + () => pubkeyManager.getPubkeys(trx), + ).thenAnswer((_) async => _makePubkeys(trx)); + + final strategy = TronGridTransactionStrategy( + pubkeyManager: pubkeyManager, + httpClient: httpClient, + apiHostOverride: 'api.trongrid.io', + ); + + await strategy.fetchTransactionHistory( + apiClient, + trx, + const PagePagination(pageNumber: 1, itemsPerPage: 20), + ); + + expect(capturedUri, isNotNull); + expect(capturedUri!.host, 'api.trongrid.io'); + expect( + capturedUri!.path, + contains('/v1/accounts/TLa2f6VPqDgRE67v1736s7bJ8Ray5wYjU7'), + ); + verify(() => httpClient.get(any())).called(1); + }); + + test('returns fingerprint as fromId for cursor-based streaming', () async { + final httpClient = _MockHttpClient(); + final apiClient = _MockApiClient(); + when(() => httpClient.get(any())).thenAnswer((_) async { + return http.Response( + jsonEncode({ + 'data': [ + _makeTrxTransferRow( + txId: 'abc123', + ownerAddress: 'TLa2f6VPqDgRE67v1736s7bJ8Ray5wYjU7', + toAddress: 'TKoCV62HPYYxghKQJV7bmW3g6KpWb1dGhQ', + amount: 1000000, + timestamp: 1700000000000, + ), + ], + 'meta': {'fingerprint': 'next-page-cursor-token'}, + }), + 200, + ); + }); + + final trx = _createTrxAsset(); + when( + () => pubkeyManager.getPubkeys(trx), + ).thenAnswer((_) async => _makePubkeys(trx)); + + final strategy = TronGridTransactionStrategy( + pubkeyManager: pubkeyManager, + httpClient: httpClient, + apiHostOverride: 'api.trongrid.io', + ); + + final response = await strategy.fetchTransactionHistory( + apiClient, + trx, + const PagePagination(pageNumber: 1, itemsPerPage: 20), + ); + + expect(jsonDecode(response.fromId!), { + 'TLa2f6VPqDgRE67v1736s7bJ8Ray5wYjU7': 'next-page-cursor-token', + }); + expect(response.transactions, hasLength(1)); + }); + + test( + 'multi-address: single __pending__ cursor stays JSON so address1 is not refetched', + () async { + final httpClient = _MockHttpClient(); + final apiClient = _MockApiClient(); + final addr1 = 'TLa2f6VPqDgRE67v1736s7bJ8Ray5wYjU7'; + final addr2 = 'TKoCV62HPYYxghKQJV7bmW3g6KpWb1dGhQ'; + final requestUris = []; + + final trx = _createTrxAsset(); + when(() => pubkeyManager.getPubkeys(trx)).thenAnswer( + (_) async => AssetPubkeys( + assetId: trx.id, + keys: [ + PubkeyInfo( + address: addr1, + derivationPath: null, + chain: null, + balance: BalanceInfo.zero(), + coinTicker: trx.id.id, + ), + PubkeyInfo( + address: addr2, + derivationPath: null, + chain: null, + balance: BalanceInfo.zero(), + coinTicker: trx.id.id, + ), + ], + availableAddressesCount: 2, + syncStatus: SyncStatusEnum.success, + ), + ); + + when(() => httpClient.get(any())).thenAnswer((invocation) async { + final uri = invocation.positionalArguments.first as Uri; + requestUris.add(uri); + if (uri.path.contains(addr1)) { + return http.Response( + jsonEncode({ + 'data': [ + _makeTrxTransferRow( + txId: 'tx1', + ownerAddress: addr1, + toAddress: addr2, + amount: 1000000, + timestamp: 1700000000000, + ), + ], + 'meta': {}, + }), + 200, + ); + } + if (uri.path.contains(addr2)) { + return http.Response( + jsonEncode({'data': [], 'meta': {}}), + 200, + ); + } + throw StateError('Unexpected TRONGrid URI: $uri'); + }); + + final strategy = TronGridTransactionStrategy( + pubkeyManager: pubkeyManager, + httpClient: httpClient, + apiHostOverride: 'api.trongrid.io', + ); + + final first = await strategy.fetchTransactionHistory( + apiClient, + trx, + const PagePagination(pageNumber: 1, itemsPerPage: 20), + ); + + expect(first.transactions, hasLength(1)); + final decoded = jsonDecode(first.fromId!) as Map; + expect(decoded, {addr2: '__pending__'}); + + final cursor = first.fromId!; + await strategy.fetchTransactionHistory( + apiClient, + trx, + TransactionBasedPagination(fromId: cursor, itemCount: 20), + ); + + expect(requestUris, hasLength(2)); + expect(requestUris[0].path, contains(addr1)); + expect(requestUris[1].path, contains(addr2)); + expect(requestUris[1].path, isNot(contains(addr1))); + }, + ); + + test('passes fingerprint via TransactionBasedPagination', () async { + final httpClient = _MockHttpClient(); + final apiClient = _MockApiClient(); + Uri? capturedUri; + when(() => httpClient.get(any())).thenAnswer((invocation) async { + capturedUri = invocation.positionalArguments.first as Uri; + return http.Response( + jsonEncode({'data': [], 'meta': {}}), + 200, + ); + }); + + final trx = _createTrxAsset(); + when( + () => pubkeyManager.getPubkeys(trx), + ).thenAnswer((_) async => _makePubkeys(trx)); + + final strategy = TronGridTransactionStrategy( + pubkeyManager: pubkeyManager, + httpClient: httpClient, + apiHostOverride: 'api.trongrid.io', + ); + + await strategy.fetchTransactionHistory( + apiClient, + trx, + const TransactionBasedPagination( + fromId: 'previous-fingerprint-token', + itemCount: 50, + ), + ); + + expect(capturedUri, isNotNull); + expect( + capturedUri!.queryParameters['fingerprint'], + 'previous-fingerprint-token', + ); + }); + + test( + 'does not treat a transaction hash as a TRONGrid fingerprint', + () async { + final httpClient = _MockHttpClient(); + final apiClient = _MockApiClient(); + Uri? capturedUri; + const txHash = + '0123456789abcdef0123456789abcdef' + '0123456789abcdef0123456789abcdef'; + when(() => httpClient.get(any())).thenAnswer((invocation) async { + capturedUri = invocation.positionalArguments.first as Uri; + return http.Response( + jsonEncode({'data': [], 'meta': {}}), + 200, + ); + }); + + final trx = _createTrxAsset(); + when( + () => pubkeyManager.getPubkeys(trx), + ).thenAnswer((_) async => _makePubkeys(trx)); + + final strategy = TronGridTransactionStrategy( + pubkeyManager: pubkeyManager, + httpClient: httpClient, + apiHostOverride: 'api.trongrid.io', + ); + + await strategy.fetchTransactionHistory( + apiClient, + trx, + const TransactionBasedPagination(fromId: txHash, itemCount: 50), + ); + + expect(capturedUri, isNotNull); + expect( + capturedUri!.queryParameters.containsKey('fingerprint'), + isFalse, + ); + }, + ); + + test('returns null fromId when no more pages', () async { + final httpClient = _MockHttpClient(); + final apiClient = _MockApiClient(); + when(() => httpClient.get(any())).thenAnswer((_) async { + return http.Response( + jsonEncode({ + 'data': [ + _makeTrxTransferRow( + txId: 'lastTx', + ownerAddress: 'TLa2f6VPqDgRE67v1736s7bJ8Ray5wYjU7', + toAddress: 'TKoCV62HPYYxghKQJV7bmW3g6KpWb1dGhQ', + amount: 500000, + timestamp: 1700000000000, + ), + ], + 'meta': {}, + }), + 200, + ); + }); + + final trx = _createTrxAsset(); + when( + () => pubkeyManager.getPubkeys(trx), + ).thenAnswer((_) async => _makePubkeys(trx)); + + final strategy = TronGridTransactionStrategy( + pubkeyManager: pubkeyManager, + httpClient: httpClient, + apiHostOverride: 'api.trongrid.io', + ); + + final response = await strategy.fetchTransactionHistory( + apiClient, + trx, + const PagePagination(pageNumber: 1, itemsPerPage: 20), + ); + + expect(response.fromId, isNull); + expect(response.transactions, hasLength(1)); }); }); } diff --git a/packages/komodo_defi_types/lib/src/transactions/transaction_history_strategy.dart b/packages/komodo_defi_types/lib/src/transactions/transaction_history_strategy.dart index 2b8e6a0f1..f8db2b48a 100644 --- a/packages/komodo_defi_types/lib/src/transactions/transaction_history_strategy.dart +++ b/packages/komodo_defi_types/lib/src/transactions/transaction_history_strategy.dart @@ -22,11 +22,20 @@ abstract class TransactionHistoryStrategy { /// Whether this strategy requires KDF transaction history to be enabled /// during activation for real-time updates and pagination to work. /// - /// Default is true; strategies that source history externally (e.g. Etherscan) + /// Default is true; strategies that source history externally + /// (e.g. Etherscan) /// can override to false so activation can skip setting `tx_history` when /// streaming is also unsupported for the asset. bool requiresKdfTransactionHistory(Asset asset) => true; + /// Whether `TransactionBasedPagination.fromId` is an opaque cursor that must + /// come from a previous strategy response, rather than a stored transaction + /// ID from local history. + /// + /// Managers can use this to avoid seeding resume flows with transaction IDs + /// that the strategy cannot interpret. + bool get usesOpaquePaginationCursor => false; + /// Whether this strategy supports the given pagination mode bool supportsPaginationMode(Type paginationType) { return supportedPaginationModes.contains(paginationType); From 877bb57fe7ca3e528e60eab2688062dc1d631ade Mon Sep 17 00:00:00 2001 From: Nitride <77973576+CharlVS@users.noreply.github.com> Date: Wed, 25 Mar 2026 23:31:19 +0100 Subject: [PATCH 26/40] fix(sdk): restore TRX market data and coordinate activation (#340) Prevent CoinGecko support checks from poisoning repository selection and add TRX fallback IDs so price history can resolve. Deduplicate shared asset activation so already-active assets stop failing into polling fallbacks. --- .../coingecko/data/coingecko_repository.dart | 60 +++++++---- .../lib/src/id_resolution_strategy.dart | 29 ++++- .../src/repository_selection_strategy.dart | 5 +- .../coingecko/coingecko_repository_test.dart | 30 ++++++ .../coinpaprika_repository_test.dart | 41 ++++++- .../repository_selection_strategy_test.dart | 41 +++++++ .../src/activation/activation_manager.dart | 102 +++++++++++++++--- .../lib/src/komodo_defi_sdk.dart | 13 +++ 8 files changed, 277 insertions(+), 44 deletions(-) diff --git a/packages/komodo_cex_market_data/lib/src/coingecko/data/coingecko_repository.dart b/packages/komodo_cex_market_data/lib/src/coingecko/data/coingecko_repository.dart index 8694f8ce4..31f502b2c 100644 --- a/packages/komodo_cex_market_data/lib/src/coingecko/data/coingecko_repository.dart +++ b/packages/komodo_cex_market_data/lib/src/coingecko/data/coingecko_repository.dart @@ -1,4 +1,3 @@ -import 'package:async/async.dart'; import 'package:decimal/decimal.dart'; import 'package:komodo_cex_market_data/src/cex_repository.dart'; import 'package:komodo_cex_market_data/src/coingecko/_coingecko_index.dart'; @@ -29,7 +28,9 @@ class CoinGeckoRepository implements CexRepository { final IdResolutionStrategy _idResolutionStrategy; final bool _enableMemoization; - final AsyncMemoizer> _coinListMemoizer = AsyncMemoizer(); + /// Populated only after a successful fetch; failures do not poison retries. + List? _cachedCoinList; + Future>? _coinListInFlight; Set? _cachedFiatCurrencies; /// Fetches the CoinGecko market data. @@ -48,30 +49,49 @@ class CoinGeckoRepository implements CexRepository { @override Future> getCoinList() async { - if (_enableMemoization) { - return _coinListMemoizer.runOnce(_fetchCoinListInternal); - } else { + if (!_enableMemoization) { // Warning: Direct API calls without memoization can lead to API rate limiting // and unnecessary network requests. Use this mode sparingly. return _fetchCoinListInternal(); } + if (_cachedCoinList != null) { + return _cachedCoinList!; + } + if (_coinListInFlight != null) { + return _coinListInFlight!; + } + _coinListInFlight = _fetchCoinListInternal() + .then((list) { + _cachedCoinList = list; + return list; + }) + .whenComplete(() { + _coinListInFlight = null; + }); + return _coinListInFlight!; } /// Internal method to fetch coin list data from the API. Future> _fetchCoinListInternal() async { - final coins = await coinGeckoProvider.fetchCoinList(); - final supportedCurrencies = await coinGeckoProvider - .fetchSupportedVsCurrencies(); - - final result = coins - .map((CexCoin e) => e.copyWith(currencies: supportedCurrencies.toSet())) - .toSet(); - - _cachedFiatCurrencies = supportedCurrencies - .map((s) => s.toUpperCase()) - .toSet(); - - return result.toList(); + try { + final coins = await coinGeckoProvider.fetchCoinList(); + final supportedCurrencies = await coinGeckoProvider + .fetchSupportedVsCurrencies(); + + final result = coins + .map( + (CexCoin e) => e.copyWith(currencies: supportedCurrencies.toSet()), + ) + .toSet(); + + _cachedFiatCurrencies = supportedCurrencies + .map((s) => s.toUpperCase()) + .toSet(); + + return result.toList(); + } catch (e, st) { + Error.throwWithStackTrace(e, st); + } } @override @@ -305,6 +325,10 @@ class CoinGeckoRepository implements CexRepository { } on ArgumentError { // If we cannot resolve a trading symbol, treat as unsupported return false; + } catch (_) { + // Coin list / network failures: treat as unsupported so fallback repos run + // without throwing from [DefaultRepositorySelectionStrategy]. + return false; } } diff --git a/packages/komodo_cex_market_data/lib/src/id_resolution_strategy.dart b/packages/komodo_cex_market_data/lib/src/id_resolution_strategy.dart index 11b857958..4334d58e1 100644 --- a/packages/komodo_cex_market_data/lib/src/id_resolution_strategy.dart +++ b/packages/komodo_cex_market_data/lib/src/id_resolution_strategy.dart @@ -1,6 +1,23 @@ +import 'package:collection/collection.dart'; import 'package:komodo_defi_types/komodo_defi_types.dart'; import 'package:logging/logging.dart'; +const _coinGeckoFallbackIds = { + 'TRX': 'tron', + 'TRX-BEP20': 'tron', +}; + +const _coinPaprikaFallbackIds = { + 'TRX': 'trx-tron', + 'TRX-BEP20': 'trx-tron', +}; + +String? _fallbackIdForAsset(AssetId assetId, Map aliases) { + return {assetId.id.toUpperCase(), assetId.symbol.configSymbol.toUpperCase()} + .map((key) => aliases[key]) + .firstWhereOrNull((alias) => alias?.isNotEmpty ?? false); +} + /// Strategy for resolving platform-specific asset identifiers /// /// Exceptions: @@ -87,12 +104,14 @@ class CoinGeckoIdResolutionStrategy implements IdResolutionStrategy { /// be used and an error is thrown in [resolveTradingSymbol]. @override List getIdPriority(AssetId assetId) { - final coinGeckoId = assetId.symbol.coinGeckoId; + final coinGeckoId = + assetId.symbol.coinGeckoId ?? + _fallbackIdForAsset(assetId, _coinGeckoFallbackIds); if (coinGeckoId == null || coinGeckoId.isEmpty) { _logger.fine( - 'Missing coinGeckoId for asset ${assetId.symbol.configSymbol}, ' - 'falling back to configSymbol. This may cause API issues.', + 'Missing coinGeckoId for asset ${assetId.symbol.configSymbol}. ' + 'CoinGecko API cannot be used for this asset.', ); } @@ -140,7 +159,9 @@ class CoinPaprikaIdResolutionStrategy implements IdResolutionStrategy { /// error is thrown in [resolveTradingSymbol]. @override List getIdPriority(AssetId assetId) { - final coinPaprikaId = assetId.symbol.coinPaprikaId; + final coinPaprikaId = + assetId.symbol.coinPaprikaId ?? + _fallbackIdForAsset(assetId, _coinPaprikaFallbackIds); if (coinPaprikaId == null || coinPaprikaId.isEmpty) { _logger.fine( diff --git a/packages/komodo_cex_market_data/lib/src/repository_selection_strategy.dart b/packages/komodo_cex_market_data/lib/src/repository_selection_strategy.dart index 0cc3b660f..d70b6e232 100644 --- a/packages/komodo_cex_market_data/lib/src/repository_selection_strategy.dart +++ b/packages/komodo_cex_market_data/lib/src/repository_selection_strategy.dart @@ -48,6 +48,7 @@ abstract class RepositorySelectionStrategy { class DefaultRepositorySelectionStrategy implements RepositorySelectionStrategy { static final Logger _logger = Logger('DefaultRepositorySelectionStrategy'); + static const _supportsTimeout = Duration(seconds: 5); @override Future ensureCacheInitialized(List repositories) async { @@ -63,14 +64,12 @@ class DefaultRepositorySelectionStrategy required List availableRepositories, }) async { final candidates = []; - const timeout = Duration(seconds: 2); - await Future.wait( availableRepositories.map((repo) async { try { final isSupported = await repo .supports(assetId, fiatCurrency, requestType) - .timeout(timeout, onTimeout: () => false); + .timeout(_supportsTimeout, onTimeout: () => false); if (isSupported) { candidates.add(repo); } diff --git a/packages/komodo_cex_market_data/test/coingecko/coingecko_repository_test.dart b/packages/komodo_cex_market_data/test/coingecko/coingecko_repository_test.dart index 55f415403..6d01875dc 100644 --- a/packages/komodo_cex_market_data/test/coingecko/coingecko_repository_test.dart +++ b/packages/komodo_cex_market_data/test/coingecko/coingecko_repository_test.dart @@ -246,6 +246,36 @@ void main() { } }); + test('should resolve TRX via fallback CoinGecko id', () async { + when(() => mockProvider.fetchCoinList()).thenAnswer( + (_) async => [ + const CexCoin( + id: 'tron', + symbol: 'trx', + name: 'TRON', + currencies: {}, + ), + ], + ); + + final assetId = AssetId( + id: 'TRX', + name: 'TRON', + symbol: AssetSymbol(assetConfigId: 'TRX'), + chainId: AssetChainId(chainId: 0), + derivationPath: null, + subClass: CoinSubClass.utxo, + ); + + final supports = await repository.supports( + assetId, + Stablecoin.usdt, + PriceRequestType.priceHistory, + ); + + expect(supports, isTrue); + }); + test('should support EUR-pegged stablecoins via EUR mapping', () async { final assetId = AssetId( id: 'bitcoin', diff --git a/packages/komodo_cex_market_data/test/coinpaprika/coinpaprika_repository_test.dart b/packages/komodo_cex_market_data/test/coinpaprika/coinpaprika_repository_test.dart index 7adfe5e5d..8efd5a5d2 100644 --- a/packages/komodo_cex_market_data/test/coinpaprika/coinpaprika_repository_test.dart +++ b/packages/komodo_cex_market_data/test/coinpaprika/coinpaprika_repository_test.dart @@ -393,6 +393,44 @@ void main() { }, ); + test('returns true for TRX via fallback CoinPaprika id', () async { + when( + () => mockProvider.supportedQuoteCurrencies, + ).thenReturn([FiatCurrency.usd, FiatCurrency.eur]); + + MockHelpers.setupProviderCoinListResponse( + mockProvider, + coins: const [ + CoinPaprikaCoin( + id: 'trx-tron', + name: 'TRON', + symbol: 'TRX', + rank: 1, + isNew: false, + isActive: true, + type: 'coin', + ), + ], + ); + + final trxAsset = AssetId( + id: 'TRX', + name: 'TRON', + symbol: AssetSymbol(assetConfigId: 'TRX'), + chainId: AssetChainId(chainId: 0), + derivationPath: null, + subClass: CoinSubClass.utxo, + ); + + final result = await repository.supports( + trxAsset, + Stablecoin.usdt, + PriceRequestType.priceHistory, + ); + + expect(result, isTrue); + }); + test('returns false for unsupported quote currency', () async { // Arrange MockHelpers.setupProviderCoinListResponse( @@ -688,7 +726,6 @@ void main() { ), ]; - DateTime? capturedStartDate; DateTime? capturedEndDate; when( @@ -700,8 +737,6 @@ void main() { interval: any(named: 'interval'), ), ).thenAnswer((invocation) async { - capturedStartDate = - invocation.namedArguments[#startDate] as DateTime?; capturedEndDate = invocation.namedArguments[#endDate] as DateTime?; return mockOhlcData; }); diff --git a/packages/komodo_cex_market_data/test/repository_selection_strategy_test.dart b/packages/komodo_cex_market_data/test/repository_selection_strategy_test.dart index 0135406e9..13734fce8 100644 --- a/packages/komodo_cex_market_data/test/repository_selection_strategy_test.dart +++ b/packages/komodo_cex_market_data/test/repository_selection_strategy_test.dart @@ -184,6 +184,22 @@ class MockFailingRepository implements CexRepository { String toString() => 'MockFailingRepository'; } +class MockSlowSupportingRepository extends MockSupportingRepository { + MockSlowSupportingRepository(super.name, {required this.delay}); + + final Duration delay; + + @override + Future supports( + AssetId assetId, + QuoteCurrency fiatCurrency, + PriceRequestType requestType, + ) async { + await Future.delayed(delay); + return super.supports(assetId, fiatCurrency, requestType); + } +} + void main() { group('RepositorySelectionStrategy', () { late RepositorySelectionStrategy strategy; @@ -279,6 +295,31 @@ void main() { expect(repo, equals(supportingRepo)); }); + + test('allows slower support checks within timeout budget', () async { + final slowRepo = MockSlowSupportingRepository( + 'slow-supporting', + delay: const Duration(seconds: 3), + ); + + final asset = AssetId( + id: 'TRX', + name: 'TRON', + symbol: AssetSymbol(assetConfigId: 'TRX'), + chainId: AssetChainId(chainId: 0), + derivationPath: null, + subClass: CoinSubClass.utxo, + ); + + final repo = await strategy.selectRepository( + assetId: asset, + fiatCurrency: Stablecoin.usdt, + requestType: PriceRequestType.priceHistory, + availableRepositories: [slowRepo], + ); + + expect(repo, equals(slowRepo)); + }); }); group('mapped quote currency support', () { diff --git a/packages/komodo_defi_sdk/lib/src/activation/activation_manager.dart b/packages/komodo_defi_sdk/lib/src/activation/activation_manager.dart index fd061e571..21d81b5e8 100644 --- a/packages/komodo_defi_sdk/lib/src/activation/activation_manager.dart +++ b/packages/komodo_defi_sdk/lib/src/activation/activation_manager.dart @@ -114,12 +114,27 @@ class ActivationManager { continue; } - // Register activation attempt - final primaryCompleter = await _registerActivation(group.primary.id); - if (primaryCompleter == null) { + // Register activation attempt. + final registration = await _registerActivation(group.primary.id); + final primaryCompleter = registration.completer; + if (!registration.shouldStartActivation) { debugPrint( 'Activation already in progress for ${group.primary.id.name}', ); + try { + await primaryCompleter.future; + yield ActivationProgress.alreadyActiveSuccess( + assetName: group.primary.id.name, + childCount: group.children?.length ?? 0, + ); + } catch (e, st) { + final mappedError = _mapError(e, group.primary.id); + yield ActivationProgress.error( + message: mappedError.fallbackMessage, + sdkError: mappedError, + stackTrace: st, + ); + } continue; } @@ -183,13 +198,26 @@ class ActivationManager { await _handleActivationComplete(group, progress, primaryCompleter); } } - } catch (e) { + } catch (e, st) { + final recoveredProgress = await _tryRecoverAlreadyActivated(group, e); + if (recoveredProgress != null) { + if (!primaryCompleter.isCompleted) { + primaryCompleter.complete(); + } + yield recoveredProgress; + continue; + } + debugPrint('Activation failed: $e'); final mappedError = _mapError(e, group.primary.id); if (!primaryCompleter.isCompleted) { primaryCompleter.completeError(mappedError); } - throw mappedError; + yield ActivationProgress.error( + message: mappedError.fallbackMessage, + sdkError: mappedError, + stackTrace: st, + ); } finally { try { await _cleanupActivation(group.primary.id); @@ -224,12 +252,16 @@ class ActivationManager { ); } - /// Check if asset and its children are already activated - Future _checkActivationStatus(_AssetGroup group) async { + /// Check if asset and its children are already activated. + Future _checkActivationStatus( + _AssetGroup group, { + bool forceRefresh = false, + }) async { try { // Use cache instead of direct RPC call to avoid excessive requests - final enabledAssetIds = await _activatedAssetsCache - .getActivatedAssetIds(); + final enabledAssetIds = await _activatedAssetsCache.getActivatedAssetIds( + forceRefresh: forceRefresh, + ); final isActive = enabledAssetIds.contains(group.primary.id); final childrenActive = @@ -257,21 +289,49 @@ class ActivationManager { ); } - /// Register new activation attempt - Future?> _registerActivation(AssetId assetId) async { + /// Register a new activation attempt or join an existing one. + Future<_ActivationRegistration> _registerActivation(AssetId assetId) async { return _protectedOperation(() async { - // Return the existing completer if activation is already in progress - // This ensures subsequent callers properly wait for the activation to complete - if (_activationCompleters.containsKey(assetId)) { - return _activationCompleters[assetId]; + final existingCompleter = _activationCompleters[assetId]; + if (existingCompleter != null) { + return _ActivationRegistration( + completer: existingCompleter, + shouldStartActivation: false, + ); } final completer = Completer(); _activationCompleters[assetId] = completer; - return completer; + return _ActivationRegistration( + completer: completer, + shouldStartActivation: true, + ); }); } + Future _tryRecoverAlreadyActivated( + _AssetGroup group, + Object error, + ) async { + if (!_isAlreadyActivatedError(error)) { + return null; + } + + _activatedAssetsCache.invalidate(); + final refreshedStatus = await _checkActivationStatus( + group, + forceRefresh: true, + ); + return refreshedStatus.isComplete ? refreshedStatus : null; + } + + bool _isAlreadyActivatedError(Object error) { + final message = error.toString(); + return message.contains('PlatformIsAlreadyActivated') || + message.contains('CoinIsAlreadyActivated') || + message.contains('activated already'); + } + /// Handle completion of activation Future _handleActivationComplete( _AssetGroup group, @@ -415,3 +475,13 @@ class _AssetGroup { return groups.values.toList(); } } + +class _ActivationRegistration { + const _ActivationRegistration({ + required this.completer, + required this.shouldStartActivation, + }); + + final Completer completer; + final bool shouldStartActivation; +} diff --git a/packages/komodo_defi_sdk/lib/src/komodo_defi_sdk.dart b/packages/komodo_defi_sdk/lib/src/komodo_defi_sdk.dart index 493d868d0..949738faf 100644 --- a/packages/komodo_defi_sdk/lib/src/komodo_defi_sdk.dart +++ b/packages/komodo_defi_sdk/lib/src/komodo_defi_sdk.dart @@ -207,6 +207,19 @@ class KomodoDefiSdk with SecureRpcPasswordMixin { /// Throws [StateError] if accessed before initialization. AssetManager get assets => _assertSdkInitialized(_container()); + /// Activates an asset through the shared activation coordinator. + /// + /// This is the preferred path for app code that wants to ensure an asset is + /// enabled without racing other managers that may be activating the same + /// asset concurrently. + Future ensureAssetActivated(Asset asset) async { + final coordinator = _assertSdkInitialized( + _container(), + ); + final result = await coordinator.activateAsset(asset); + return result.isSuccess; + } + /// Deletes a persisted custom token from SDK-managed storage. /// /// This removes the token from the custom-token store and the in-memory From 19124899f0f819c2596084de6ccf7e9f3824f86f Mon Sep 17 00:00:00 2001 From: Nitride <77973576+CharlVS@users.noreply.github.com> Date: Fri, 27 Mar 2026 12:48:57 +0100 Subject: [PATCH 27/40] feat(sdk): balance recovery mode and fee info improvements (#341) - Add recovery watchers in BalanceManager when activation fails at startup so callers do not need to re-subscribe after races - Refactor balance polling into reusable fetchLatestBalance helper - Improve FeeInfo types and freezed codegen in komodo_defi_types - Add fee_info_display widget in komodo_ui - Update build_config, dev_builds_artefact_downloader, and update_api_config CLI - Add balance_manager_recovery_test and fee_info_test --- .../app_build/build_config.json | 18 +- .../lib/src/balances/balance_manager.dart | 120 +++++---- .../balance_manager_recovery_test.dart | 252 ++++++++++++++++++ .../lib/src/transactions/fee_info.dart | 17 +- .../src/transactions/fee_info.freezed.dart | 30 ++- .../komodo_defi_types/test/fee_info_test.dart | 36 +++ .../src/core/displays/fee_info_display.dart | 10 + .../dev_builds_artefact_downloader.dart | 8 +- .../bin/update_api_config.dart | 5 +- 9 files changed, 422 insertions(+), 74 deletions(-) create mode 100644 packages/komodo_defi_sdk/test/balances/balance_manager_recovery_test.dart diff --git a/packages/komodo_defi_framework/app_build/build_config.json b/packages/komodo_defi_framework/app_build/build_config.json index 76a424612..68def6f80 100644 --- a/packages/komodo_defi_framework/app_build/build_config.json +++ b/packages/komodo_defi_framework/app_build/build_config.json @@ -1,7 +1,7 @@ { "api": { - "api_commit_hash": "52ba4f9ff12fd9e31768abc924a99ceadd64b2d6", - "branch": "staging", + "api_commit_hash": "b3c9e669ac736dc4853064958ecc565a0284e16d", + "branch": "fix/tron-account-activation-fee", "fetch_at_build_enabled": true, "concurrent_downloads_enabled": true, "source_urls": [ @@ -13,14 +13,14 @@ "web": { "matching_pattern": "^(?:kdf_[a-f0-9]{7,40}-wasm|mm2_[a-f0-9]{7,40}-wasm|mm2-[a-f0-9]{7,40}-wasm)\\.zip$", "valid_zip_sha256_checksums": [ - "e0b43651c773f222a4b8aa0c0a314bb3748b4f6edaefb3db70c76b34aaf4030b" + "61be493c87eaaa5d5cc74fd9c2b2d98353c52d8dc1d3ff0ce55c9f3467cf4720" ], "path": "web/kdf/bin" }, "ios": { "matching_pattern": "^(?:kdf_[a-f0-9]{7,40}-ios-aarch64|mm2_[a-f0-9]{7,40}-ios-aarch64|mm2-[a-f0-9]{7,40}-ios-aarch64-CI)\\.zip$", "valid_zip_sha256_checksums": [ - "b81d7c7a8f9a7c2c5c15d700ca2ff2683f0ad307ed7d9cd7b7541638940831d4" + "cc8e545a1704d86cc20871fc1f14943db61d055fa8e3db466a7e40e253982ac7" ], "path": "ios" }, @@ -28,35 +28,35 @@ "matching_pattern": "^(?:kdf-macos-universal2-[a-f0-9]{7,40}|kdf_[a-f0-9]{7,40}-mac-universal)\\.zip$", "matching_preference": ["universal2", "mac-arm64"], "valid_zip_sha256_checksums": [ - "6c09130fa7e4977dff617df5d5be385c1f2a57e6764a63e12e79797df52568f4" + "6852d232afce798abd887381f0699e74e2ccfda94faf14dbf0af115b84de0584" ], "path": "macos/bin" }, "windows": { "matching_pattern": "^(?:kdf_[a-f0-9]{7,40}-win-x86-64|mm2_[a-f0-9]{7,40}-win-x86-64|mm2-[a-f0-9]{7,40}-Win64)\\.zip$", "valid_zip_sha256_checksums": [ - "398e5adc95706613c71c672424aadf719899581fbc48702a5bb800c80c3e27bd" + "0377cf6c539610cce2495731058f63d3165289a071ab4772a649374f4ea1658f" ], "path": "windows/bin" }, "android-armv7": { "matching_pattern": "^(?:kdf_[a-f0-9]{7,40}-android-armv7|mm2_[a-f0-9]{7,40}-android-armv7|mm2-[a-f0-9]{7,40}-android-armv7-CI)\\.zip$", "valid_zip_sha256_checksums": [ - "eb41a69db8b002f51a4d04045492c0f6f9fcce8dd3aa655c877e41e5482d1b36" + "1b3c064322b377fd66945c25e405a04246134b7564d56530ab1bd39ed3de30eb" ], "path": "android/app/src/main/cpp/libs/armeabi-v7a" }, "android-aarch64": { "matching_pattern": "^(?:kdf_[a-f0-9]{7,40}-android-aarch64|mm2_[a-f0-9]{7,40}-android-aarch64|mm2-[a-f0-9]{7,40}-android-aarch64-CI)\\.zip$", "valid_zip_sha256_checksums": [ - "dc5bdd90196477eba9c4debe1dc93504d24980b45a669d777374bfe09dda73aa" + "4ed7e4a3f1ddc7906d12b6bf2564e6e17a5a8cc772b3fd7f4ebf0e5d16fd58ee" ], "path": "android/app/src/main/cpp/libs/arm64-v8a" }, "linux": { "matching_pattern": "^(?:kdf_[a-f0-9]{7,40}-linux-x86-64|mm2_[a-f0-9]{7,40}-linux-x86-64|mm2-[a-f0-9]{7,40}-Linux-Release)\\.zip$", "valid_zip_sha256_checksums": [ - "f282176b99d080315df9f47c11713084cc37de0056c144950aa9f033a8cc6302" + "6dbff099e8ca002c6de828554bb6a81f5729ec48caab5ea5544cb1125a531255" ], "path": "linux/bin" } diff --git a/packages/komodo_defi_sdk/lib/src/balances/balance_manager.dart b/packages/komodo_defi_sdk/lib/src/balances/balance_manager.dart index fe2d2b04b..df0fa6543 100644 --- a/packages/komodo_defi_sdk/lib/src/balances/balance_manager.dart +++ b/packages/komodo_defi_sdk/lib/src/balances/balance_manager.dart @@ -408,15 +408,35 @@ class BalanceManager implements IBalanceManager { // If activation was requested but failed, emit error if (activateIfNeeded && !isActive) { + final activationError = ActivationFailedException( + assetId: assetId, + message: 'Asset activation failed', + errorCode: 'BALANCE_ACTIVATION_ERROR', + ); + if (!controller.isClosed) { - controller.addError( - ActivationFailedException( - assetId: assetId, - message: 'Asset activation failed', - errorCode: 'BALANCE_ACTIVATION_ERROR', - ), - ); + controller.addError(activationError); } + + // Recovery mode: keep this watcher alive and retry in the background + // so callers do not need to re-subscribe after startup races. + _logger.warning( + 'Activation unavailable for ${assetId.name}; ' + 'starting recovery watchers', + activationError, + ); + _startStaleBalanceGuard( + asset: asset, + assetId: assetId, + controller: controller, + activateIfNeeded: activateIfNeeded, + ); + await _startBalancePolling( + asset: asset, + assetId: assetId, + controller: controller, + activateIfNeeded: activateIfNeeded, + ); return; } @@ -580,55 +600,63 @@ class BalanceManager implements IBalanceManager { _logger.fine('Starting balance polling fallback for ${assetId.name}'); - final periodicStream = Stream.periodic(_defaultPollingInterval); - final subscription = periodicStream - .asyncMap((_) async { - if (_isDisposed) return null; + Future fetchLatestBalance() async { + if (_isDisposed) return null; - if (_activationCoordinator == null || _pubkeyManager == null) { - return null; - } + if (_activationCoordinator == null || _pubkeyManager == null) { + return null; + } - final currentUser = await _auth.currentUser; - if (currentUser == null || currentUser.walletId != _currentWalletId) { - return null; - } + final currentUser = await _auth.currentUser; + if (currentUser == null || currentUser.walletId != _currentWalletId) { + return null; + } + if (enableDebugLogging) { + _logger.info( + '[POLLING] Fetching balance for ${assetId.name} ' + '(every ${_defaultPollingInterval.inSeconds}s)', + ); + } + + try { + final isActive = await _ensureAssetActivated(asset, activateIfNeeded); + + if (isActive) { + final balance = await getBalance(assetId); if (enableDebugLogging) { _logger.info( - '[POLLING] Fetching balance for ${assetId.name} ' - '(every ${_defaultPollingInterval.inSeconds}s)', + '[POLLING] Balance fetched for ${assetId.name}: ' + '${balance.total}', ); } + return balance; + } + } on Object catch (error, stackTrace) { + if (enableDebugLogging) { + _logger.warning( + '[POLLING] Balance fetch failed for ${assetId.name}', + error, + stackTrace, + ); + } + } - try { - final isActive = await _ensureAssetActivated( - asset, - activateIfNeeded, - ); + return lastKnown(assetId); + } - if (isActive) { - final balance = await getBalance(assetId); - if (enableDebugLogging) { - _logger.info( - '[POLLING] Balance fetched for ${assetId.name}: ' - '${balance.total}', - ); - } - return balance; - } - } catch (error, stackTrace) { - if (enableDebugLogging) { - _logger.warning( - '[POLLING] Balance fetch failed for ${assetId.name}', - error, - stackTrace, - ); - } - } + // Kick off an immediate refresh so polling fallback can recover quickly + // after startup races without waiting for the first periodic tick. + unawaited(() async { + final balance = await fetchLatestBalance(); + if (balance != null && !controller.isClosed) { + controller.add(balance); + } + }()); - return lastKnown(assetId); - }) + final periodicStream = Stream.periodic(_defaultPollingInterval); + final subscription = periodicStream + .asyncMap((_) => fetchLatestBalance()) .listen( (balance) { if (balance != null && !controller.isClosed) { diff --git a/packages/komodo_defi_sdk/test/balances/balance_manager_recovery_test.dart b/packages/komodo_defi_sdk/test/balances/balance_manager_recovery_test.dart new file mode 100644 index 000000000..5da8711bf --- /dev/null +++ b/packages/komodo_defi_sdk/test/balances/balance_manager_recovery_test.dart @@ -0,0 +1,252 @@ +import 'dart:async'; + +import 'package:decimal/decimal.dart'; +import 'package:komodo_defi_local_auth/komodo_defi_local_auth.dart'; +import 'package:komodo_defi_sdk/src/activation/activation_exceptions.dart'; +import 'package:komodo_defi_sdk/src/activation/shared_activation_coordinator.dart'; +import 'package:komodo_defi_sdk/src/assets/asset_history_storage.dart'; +import 'package:komodo_defi_sdk/src/assets/asset_lookup.dart'; +import 'package:komodo_defi_sdk/src/balances/balance_manager.dart'; +import 'package:komodo_defi_sdk/src/pubkeys/pubkey_manager.dart'; +import 'package:komodo_defi_sdk/src/streaming/event_streaming_manager.dart'; +import 'package:komodo_defi_types/komodo_defi_types.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:test/test.dart'; + +class _MockAuth extends Mock implements KomodoDefiLocalAuth {} + +class _MockActivationCoordinator extends Mock + implements SharedActivationCoordinator {} + +class _MockPubkeyManager extends Mock implements PubkeyManager {} + +class _MockAssetLookup extends Mock implements IAssetLookup {} + +class _MockEventStreamingManager extends Mock + implements EventStreamingManager {} + +class _InMemoryAssetHistoryStorage extends AssetHistoryStorage { + final Map> _walletAssets = {}; + + String _key(WalletId walletId) => walletId.pubkeyHash ?? walletId.name; + + @override + Future storeWalletAssets( + WalletId walletId, + Set assetIds, + ) async { + _walletAssets[_key(walletId)] = Set.from(assetIds); + } + + @override + Future addAssetToWallet(WalletId walletId, String assetId) async { + final current = await getWalletAssets(walletId); + current.add(assetId); + await storeWalletAssets(walletId, current); + } + + @override + Future> getWalletAssets(WalletId walletId) async { + return Set.from(_walletAssets[_key(walletId)] ?? {}); + } + + @override + Future clearWalletAssets(WalletId walletId) async { + _walletAssets.remove(_key(walletId)); + } +} + +void main() { + setUpAll(() { + registerFallbackValue( + Asset( + id: AssetId( + id: 'FALLBACK', + name: 'Fallback', + symbol: AssetSymbol(assetConfigId: 'FALLBACK'), + chainId: AssetChainId(chainId: 1, decimalsValue: 8), + derivationPath: null, + subClass: CoinSubClass.tendermint, + ), + protocol: TendermintProtocol.fromJson({ + 'type': 'Tendermint', + 'rpc_urls': [ + {'url': 'http://localhost:26657'}, + ], + }), + isWalletOnly: false, + signMessagePrefix: null, + ), + ); + }); + + group('BalanceManager activation miss recovery', () { + late _MockAuth auth; + late _MockActivationCoordinator activation; + late _MockPubkeyManager pubkeyManager; + late _MockAssetLookup assetLookup; + late _MockEventStreamingManager eventStreamingManager; + late StreamController authChanges; + late BalanceManager manager; + late AssetId assetId; + late Asset asset; + late WalletId walletId; + late int activationAttempts; + + setUp(() { + auth = _MockAuth(); + activation = _MockActivationCoordinator(); + pubkeyManager = _MockPubkeyManager(); + assetLookup = _MockAssetLookup(); + eventStreamingManager = _MockEventStreamingManager(); + authChanges = StreamController.broadcast(); + + walletId = const WalletId( + name: 'recovery-wallet', + authOptions: AuthOptions(derivationMethod: DerivationMethod.iguana), + ); + + when(() => auth.authStateChanges).thenAnswer((_) => authChanges.stream); + when(() => auth.currentUser).thenAnswer( + (_) async => KdfUser(walletId: walletId, isBip39Seed: false), + ); + + assetId = AssetId( + id: 'ATOM', + name: 'Cosmos', + symbol: AssetSymbol(assetConfigId: 'ATOM'), + chainId: AssetChainId(chainId: 118, decimalsValue: 6), + derivationPath: null, + subClass: CoinSubClass.tendermint, + ); + asset = Asset( + id: assetId, + protocol: TendermintProtocol.fromJson({ + 'type': 'Tendermint', + 'rpc_urls': [ + {'url': 'http://localhost:26657'}, + ], + }), + isWalletOnly: false, + signMessagePrefix: null, + ); + + when(() => assetLookup.fromId(assetId)).thenReturn(asset); + when( + () => eventStreamingManager.subscribeToBalance(coin: assetId.id), + ).thenThrow(StateError('streaming should not be used in this test')); + + when( + () => pubkeyManager.watchPubkeys(asset), + ).thenAnswer((_) => const Stream.empty()); + when(() => pubkeyManager.getPubkeys(asset)).thenAnswer( + (_) async => AssetPubkeys( + assetId: assetId, + keys: [ + PubkeyInfo( + address: 'TTtRecoverExample', + derivationPath: null, + chain: null, + balance: BalanceInfo( + total: Decimal.fromInt(5), + spendable: Decimal.fromInt(5), + unspendable: Decimal.zero, + ), + coinTicker: assetId.id, + ), + ], + availableAddressesCount: 1, + syncStatus: SyncStatusEnum.success, + ), + ); + + activationAttempts = 0; + when( + () => activation.isAssetActive(assetId), + ).thenAnswer((_) async => false); + when(() => activation.activateAsset(any())).thenAnswer((_) async { + activationAttempts += 1; + if (activationAttempts == 1) { + return ActivationResult.failure(assetId, 'initial activation miss'); + } + return ActivationResult.success(assetId); + }); + + manager = BalanceManager( + assetLookup: assetLookup, + auth: auth, + pubkeyManager: pubkeyManager, + activationCoordinator: activation, + eventStreamingManager: eventStreamingManager, + assetHistoryStorage: _InMemoryAssetHistoryStorage(), + ); + }); + + tearDown(() async { + await manager.dispose(); + await authChanges.close(); + }); + + test('watchBalance emits activation error but recovers ' + 'and emits a real balance', () async { + final errors = []; + final values = []; + final recovered = Completer(); + + final sub = manager + .watchBalance(assetId) + .listen( + (balance) { + values.add(balance); + if (balance.spendable > Decimal.zero && !recovered.isCompleted) { + recovered.complete(); + } + }, + onError: (Object error, StackTrace _) { + errors.add(error); + }, + ); + + await recovered.future.timeout(const Duration(seconds: 2)); + + expect( + errors.whereType(), + isNotEmpty, + reason: 'Initial activation miss should still be observable', + ); + expect( + values.any((b) => b.spendable == Decimal.zero), + isTrue, + reason: 'First-time wallet optimization may emit zero before recovery', + ); + expect(values.last.spendable, Decimal.fromInt(5)); + + await sub.cancel(); + }); + + test( + 'single subscription receives recovered balance without re-subscribing', + () async { + final recovered = Completer(); + + final sub = manager.watchBalance(assetId).listen((balance) { + if (balance.spendable > Decimal.zero && !recovered.isCompleted) { + recovered.complete(balance); + } + }, onError: (_, __) {}); + + final recoveredBalance = await recovered.future.timeout( + const Duration(seconds: 2), + ); + expect(recoveredBalance.spendable, Decimal.fromInt(5)); + expect( + activationAttempts, + greaterThanOrEqualTo(2), + reason: 'Recovery should retry activation without new subscribers', + ); + + await sub.cancel(); + }, + ); + }); +} diff --git a/packages/komodo_defi_types/lib/src/transactions/fee_info.dart b/packages/komodo_defi_types/lib/src/transactions/fee_info.dart index 69bd5c38f..ca56da19c 100644 --- a/packages/komodo_defi_types/lib/src/transactions/fee_info.dart +++ b/packages/komodo_defi_types/lib/src/transactions/fee_info.dart @@ -85,6 +85,9 @@ sealed class FeeInfo with _$FeeInfo { energyUsed: (json['energy_used'] as num?)?.toInt() ?? 0, bandwidthFee: Decimal.parse(json['bandwidth_fee'].toString()), energyFee: Decimal.parse(json['energy_fee'].toString()), + accountCreationFee: json['account_creation_fee'] != null + ? Decimal.parse(json['account_creation_fee'].toString()) + : null, totalFeeAmount: json['total_fee'] != null ? Decimal.parse(json['total_fee'].toString()) : null, @@ -255,6 +258,7 @@ sealed class FeeInfo with _$FeeInfo { required int energyUsed, required Decimal bandwidthFee, required Decimal energyFee, + Decimal? accountCreationFee, Decimal? totalFeeAmount, }) = FeeInfoTron; @@ -293,8 +297,14 @@ sealed class FeeInfo with _$FeeInfo { FeeInfoCosmosGas(:final gasPrice, :final gasLimit) => gasPrice * Decimal.fromInt(gasLimit), FeeInfoTendermint(:final amount) => amount, - FeeInfoTron(:final bandwidthFee, :final energyFee, :final totalFeeAmount) => - totalFeeAmount ?? (bandwidthFee + energyFee), + FeeInfoTron( + :final bandwidthFee, + :final energyFee, + :final accountCreationFee, + :final totalFeeAmount, + ) => + totalFeeAmount ?? + (bandwidthFee + energyFee + (accountCreationFee ?? Decimal.zero)), FeeInfoSia(:final amount) => amount, }; @@ -373,6 +383,7 @@ sealed class FeeInfo with _$FeeInfo { :final energyUsed, :final bandwidthFee, :final energyFee, + :final accountCreationFee, :final totalFeeAmount, ) => { @@ -382,6 +393,8 @@ sealed class FeeInfo with _$FeeInfo { 'energy_used': energyUsed, 'bandwidth_fee': bandwidthFee.toString(), 'energy_fee': energyFee.toString(), + if (accountCreationFee != null) + 'account_creation_fee': accountCreationFee.toString(), if (totalFeeAmount != null) 'total_fee': totalFeeAmount.toString(), }, FeeInfoSia(:final coin, :final amount, :final policy) => { diff --git a/packages/komodo_defi_types/lib/src/transactions/fee_info.freezed.dart b/packages/komodo_defi_types/lib/src/transactions/fee_info.freezed.dart index 579dde7e4..2e03917d2 100644 --- a/packages/komodo_defi_types/lib/src/transactions/fee_info.freezed.dart +++ b/packages/komodo_defi_types/lib/src/transactions/fee_info.freezed.dart @@ -172,7 +172,7 @@ return sia(_that);case _: /// } /// ``` -@optionalTypeArgs TResult maybeWhen({TResult Function( String coin, Decimal amount)? utxoFixed,TResult Function( String coin, Decimal amount)? utxoPerKbyte,TResult Function( String coin, Decimal gasPrice, int gas, Decimal? totalGasFee)? ethGas,TResult Function( String coin, Decimal maxFeePerGas, Decimal maxPriorityFeePerGas, int gas, Decimal? totalGasFee)? ethGasEip1559,TResult Function( String coin, Decimal gasPrice, int gasLimit, Decimal? totalGasFee)? qrc20Gas,TResult Function( String coin, Decimal gasPrice, int gasLimit)? cosmosGas,TResult Function( String coin, Decimal amount, int gasLimit)? tendermint,TResult Function( String coin, int bandwidthUsed, int energyUsed, Decimal bandwidthFee, Decimal energyFee, Decimal? totalFeeAmount)? tron,TResult Function( String coin, Decimal amount, String policy)? sia,required TResult orElse(),}) {final _that = this; +@optionalTypeArgs TResult maybeWhen({TResult Function( String coin, Decimal amount)? utxoFixed,TResult Function( String coin, Decimal amount)? utxoPerKbyte,TResult Function( String coin, Decimal gasPrice, int gas, Decimal? totalGasFee)? ethGas,TResult Function( String coin, Decimal maxFeePerGas, Decimal maxPriorityFeePerGas, int gas, Decimal? totalGasFee)? ethGasEip1559,TResult Function( String coin, Decimal gasPrice, int gasLimit, Decimal? totalGasFee)? qrc20Gas,TResult Function( String coin, Decimal gasPrice, int gasLimit)? cosmosGas,TResult Function( String coin, Decimal amount, int gasLimit)? tendermint,TResult Function( String coin, int bandwidthUsed, int energyUsed, Decimal bandwidthFee, Decimal energyFee, Decimal? accountCreationFee, Decimal? totalFeeAmount)? tron,TResult Function( String coin, Decimal amount, String policy)? sia,required TResult orElse(),}) {final _that = this; switch (_that) { case FeeInfoUtxoFixed() when utxoFixed != null: return utxoFixed(_that.coin,_that.amount);case FeeInfoUtxoPerKbyte() when utxoPerKbyte != null: @@ -182,7 +182,7 @@ return ethGasEip1559(_that.coin,_that.maxFeePerGas,_that.maxPriorityFeePerGas,_t return qrc20Gas(_that.coin,_that.gasPrice,_that.gasLimit,_that.totalGasFee);case FeeInfoCosmosGas() when cosmosGas != null: return cosmosGas(_that.coin,_that.gasPrice,_that.gasLimit);case FeeInfoTendermint() when tendermint != null: return tendermint(_that.coin,_that.amount,_that.gasLimit);case FeeInfoTron() when tron != null: -return tron(_that.coin,_that.bandwidthUsed,_that.energyUsed,_that.bandwidthFee,_that.energyFee,_that.totalFeeAmount);case FeeInfoSia() when sia != null: +return tron(_that.coin,_that.bandwidthUsed,_that.energyUsed,_that.bandwidthFee,_that.energyFee,_that.accountCreationFee,_that.totalFeeAmount);case FeeInfoSia() when sia != null: return sia(_that.coin,_that.amount,_that.policy);case _: return orElse(); @@ -201,7 +201,7 @@ return sia(_that.coin,_that.amount,_that.policy);case _: /// } /// ``` -@optionalTypeArgs TResult when({required TResult Function( String coin, Decimal amount) utxoFixed,required TResult Function( String coin, Decimal amount) utxoPerKbyte,required TResult Function( String coin, Decimal gasPrice, int gas, Decimal? totalGasFee) ethGas,required TResult Function( String coin, Decimal maxFeePerGas, Decimal maxPriorityFeePerGas, int gas, Decimal? totalGasFee) ethGasEip1559,required TResult Function( String coin, Decimal gasPrice, int gasLimit, Decimal? totalGasFee) qrc20Gas,required TResult Function( String coin, Decimal gasPrice, int gasLimit) cosmosGas,required TResult Function( String coin, Decimal amount, int gasLimit) tendermint,required TResult Function( String coin, int bandwidthUsed, int energyUsed, Decimal bandwidthFee, Decimal energyFee, Decimal? totalFeeAmount) tron,required TResult Function( String coin, Decimal amount, String policy) sia,}) {final _that = this; +@optionalTypeArgs TResult when({required TResult Function( String coin, Decimal amount) utxoFixed,required TResult Function( String coin, Decimal amount) utxoPerKbyte,required TResult Function( String coin, Decimal gasPrice, int gas, Decimal? totalGasFee) ethGas,required TResult Function( String coin, Decimal maxFeePerGas, Decimal maxPriorityFeePerGas, int gas, Decimal? totalGasFee) ethGasEip1559,required TResult Function( String coin, Decimal gasPrice, int gasLimit, Decimal? totalGasFee) qrc20Gas,required TResult Function( String coin, Decimal gasPrice, int gasLimit) cosmosGas,required TResult Function( String coin, Decimal amount, int gasLimit) tendermint,required TResult Function( String coin, int bandwidthUsed, int energyUsed, Decimal bandwidthFee, Decimal energyFee, Decimal? accountCreationFee, Decimal? totalFeeAmount) tron,required TResult Function( String coin, Decimal amount, String policy) sia,}) {final _that = this; switch (_that) { case FeeInfoUtxoFixed(): return utxoFixed(_that.coin,_that.amount);case FeeInfoUtxoPerKbyte(): @@ -211,7 +211,7 @@ return ethGasEip1559(_that.coin,_that.maxFeePerGas,_that.maxPriorityFeePerGas,_t return qrc20Gas(_that.coin,_that.gasPrice,_that.gasLimit,_that.totalGasFee);case FeeInfoCosmosGas(): return cosmosGas(_that.coin,_that.gasPrice,_that.gasLimit);case FeeInfoTendermint(): return tendermint(_that.coin,_that.amount,_that.gasLimit);case FeeInfoTron(): -return tron(_that.coin,_that.bandwidthUsed,_that.energyUsed,_that.bandwidthFee,_that.energyFee,_that.totalFeeAmount);case FeeInfoSia(): +return tron(_that.coin,_that.bandwidthUsed,_that.energyUsed,_that.bandwidthFee,_that.energyFee,_that.accountCreationFee,_that.totalFeeAmount);case FeeInfoSia(): return sia(_that.coin,_that.amount,_that.policy);} } /// A variant of `when` that fallback to returning `null` @@ -226,7 +226,7 @@ return sia(_that.coin,_that.amount,_that.policy);} /// } /// ``` -@optionalTypeArgs TResult? whenOrNull({TResult? Function( String coin, Decimal amount)? utxoFixed,TResult? Function( String coin, Decimal amount)? utxoPerKbyte,TResult? Function( String coin, Decimal gasPrice, int gas, Decimal? totalGasFee)? ethGas,TResult? Function( String coin, Decimal maxFeePerGas, Decimal maxPriorityFeePerGas, int gas, Decimal? totalGasFee)? ethGasEip1559,TResult? Function( String coin, Decimal gasPrice, int gasLimit, Decimal? totalGasFee)? qrc20Gas,TResult? Function( String coin, Decimal gasPrice, int gasLimit)? cosmosGas,TResult? Function( String coin, Decimal amount, int gasLimit)? tendermint,TResult? Function( String coin, int bandwidthUsed, int energyUsed, Decimal bandwidthFee, Decimal energyFee, Decimal? totalFeeAmount)? tron,TResult? Function( String coin, Decimal amount, String policy)? sia,}) {final _that = this; +@optionalTypeArgs TResult? whenOrNull({TResult? Function( String coin, Decimal amount)? utxoFixed,TResult? Function( String coin, Decimal amount)? utxoPerKbyte,TResult? Function( String coin, Decimal gasPrice, int gas, Decimal? totalGasFee)? ethGas,TResult? Function( String coin, Decimal maxFeePerGas, Decimal maxPriorityFeePerGas, int gas, Decimal? totalGasFee)? ethGasEip1559,TResult? Function( String coin, Decimal gasPrice, int gasLimit, Decimal? totalGasFee)? qrc20Gas,TResult? Function( String coin, Decimal gasPrice, int gasLimit)? cosmosGas,TResult? Function( String coin, Decimal amount, int gasLimit)? tendermint,TResult? Function( String coin, int bandwidthUsed, int energyUsed, Decimal bandwidthFee, Decimal energyFee, Decimal? accountCreationFee, Decimal? totalFeeAmount)? tron,TResult? Function( String coin, Decimal amount, String policy)? sia,}) {final _that = this; switch (_that) { case FeeInfoUtxoFixed() when utxoFixed != null: return utxoFixed(_that.coin,_that.amount);case FeeInfoUtxoPerKbyte() when utxoPerKbyte != null: @@ -236,7 +236,7 @@ return ethGasEip1559(_that.coin,_that.maxFeePerGas,_that.maxPriorityFeePerGas,_t return qrc20Gas(_that.coin,_that.gasPrice,_that.gasLimit,_that.totalGasFee);case FeeInfoCosmosGas() when cosmosGas != null: return cosmosGas(_that.coin,_that.gasPrice,_that.gasLimit);case FeeInfoTendermint() when tendermint != null: return tendermint(_that.coin,_that.amount,_that.gasLimit);case FeeInfoTron() when tron != null: -return tron(_that.coin,_that.bandwidthUsed,_that.energyUsed,_that.bandwidthFee,_that.energyFee,_that.totalFeeAmount);case FeeInfoSia() when sia != null: +return tron(_that.coin,_that.bandwidthUsed,_that.energyUsed,_that.bandwidthFee,_that.energyFee,_that.accountCreationFee,_that.totalFeeAmount);case FeeInfoSia() when sia != null: return sia(_that.coin,_that.amount,_that.policy);case _: return null; @@ -762,14 +762,15 @@ as int, class FeeInfoTron extends FeeInfo { - const FeeInfoTron({required this.coin, required this.bandwidthUsed, required this.energyUsed, required this.bandwidthFee, required this.energyFee, this.totalFeeAmount}): super._(); - + const FeeInfoTron({required this.coin, required this.bandwidthUsed, required this.energyUsed, required this.bandwidthFee, required this.energyFee, this.accountCreationFee, this.totalFeeAmount}): super._(); + @override final String coin; final int bandwidthUsed; final int energyUsed; final Decimal bandwidthFee; final Decimal energyFee; + final Decimal? accountCreationFee; final Decimal? totalFeeAmount; /// Create a copy of FeeInfo @@ -782,16 +783,16 @@ $FeeInfoTronCopyWith get copyWith => _$FeeInfoTronCopyWithImpl Object.hash(runtimeType,coin,bandwidthUsed,energyUsed,bandwidthFee,energyFee,totalFeeAmount); +int get hashCode => Object.hash(runtimeType,coin,bandwidthUsed,energyUsed,bandwidthFee,energyFee,accountCreationFee,totalFeeAmount); @override String toString() { - return 'FeeInfo.tron(coin: $coin, bandwidthUsed: $bandwidthUsed, energyUsed: $energyUsed, bandwidthFee: $bandwidthFee, energyFee: $energyFee, totalFeeAmount: $totalFeeAmount)'; + return 'FeeInfo.tron(coin: $coin, bandwidthUsed: $bandwidthUsed, energyUsed: $energyUsed, bandwidthFee: $bandwidthFee, energyFee: $energyFee, accountCreationFee: $accountCreationFee, totalFeeAmount: $totalFeeAmount)'; } @@ -802,7 +803,7 @@ abstract mixin class $FeeInfoTronCopyWith<$Res> implements $FeeInfoCopyWith<$Res factory $FeeInfoTronCopyWith(FeeInfoTron value, $Res Function(FeeInfoTron) _then) = _$FeeInfoTronCopyWithImpl; @override @useResult $Res call({ - String coin, int bandwidthUsed, int energyUsed, Decimal bandwidthFee, Decimal energyFee, Decimal? totalFeeAmount + String coin, int bandwidthUsed, int energyUsed, Decimal bandwidthFee, Decimal energyFee, Decimal? accountCreationFee, Decimal? totalFeeAmount }); @@ -819,14 +820,15 @@ class _$FeeInfoTronCopyWithImpl<$Res> /// Create a copy of FeeInfo /// with the given fields replaced by the non-null parameter values. -@override @pragma('vm:prefer-inline') $Res call({Object? coin = null,Object? bandwidthUsed = null,Object? energyUsed = null,Object? bandwidthFee = null,Object? energyFee = null,Object? totalFeeAmount = freezed,}) { +@override @pragma('vm:prefer-inline') $Res call({Object? coin = null,Object? bandwidthUsed = null,Object? energyUsed = null,Object? bandwidthFee = null,Object? energyFee = null,Object? accountCreationFee = freezed,Object? totalFeeAmount = freezed,}) { return _then(FeeInfoTron( coin: null == coin ? _self.coin : coin // ignore: cast_nullable_to_non_nullable as String,bandwidthUsed: null == bandwidthUsed ? _self.bandwidthUsed : bandwidthUsed // ignore: cast_nullable_to_non_nullable as int,energyUsed: null == energyUsed ? _self.energyUsed : energyUsed // ignore: cast_nullable_to_non_nullable as int,bandwidthFee: null == bandwidthFee ? _self.bandwidthFee : bandwidthFee // ignore: cast_nullable_to_non_nullable as Decimal,energyFee: null == energyFee ? _self.energyFee : energyFee // ignore: cast_nullable_to_non_nullable -as Decimal,totalFeeAmount: freezed == totalFeeAmount ? _self.totalFeeAmount : totalFeeAmount // ignore: cast_nullable_to_non_nullable +as Decimal,accountCreationFee: freezed == accountCreationFee ? _self.accountCreationFee : accountCreationFee // ignore: cast_nullable_to_non_nullable +as Decimal?,totalFeeAmount: freezed == totalFeeAmount ? _self.totalFeeAmount : totalFeeAmount // ignore: cast_nullable_to_non_nullable as Decimal?, )); } diff --git a/packages/komodo_defi_types/test/fee_info_test.dart b/packages/komodo_defi_types/test/fee_info_test.dart index feceb80f7..9431a4990 100644 --- a/packages/komodo_defi_types/test/fee_info_test.dart +++ b/packages/komodo_defi_types/test/fee_info_test.dart @@ -62,6 +62,7 @@ void main() { energyUsed: 29650, bandwidthFee: Decimal.parse('0.345'), energyFee: Decimal.parse('12.453'), + accountCreationFee: Decimal.one, totalFeeAmount: Decimal.parse('12.798'), ); @@ -73,6 +74,7 @@ void main() { expect(json['energy_used'], equals(29650)); expect(json['bandwidth_fee'], equals('0.345')); expect(json['energy_fee'], equals('12.453')); + expect(json['account_creation_fee'], equals('1')); expect(json['total_fee'], equals('12.798')); }); @@ -84,6 +86,7 @@ void main() { 'energy_used': 0, 'bandwidth_fee': '0.267', 'energy_fee': '0', + 'account_creation_fee': '1', 'total_fee': '0.267', }; @@ -96,9 +99,42 @@ void main() { expect(tronFee.energyUsed, equals(0)); expect(tronFee.bandwidthFee, equals(Decimal.parse('0.267'))); expect(tronFee.energyFee, equals(Decimal.zero)); + expect(tronFee.accountCreationFee, equals(Decimal.one)); expect(tronFee.totalFeeAmount, equals(Decimal.parse('0.267'))); expect(tronFee.totalFee, equals(Decimal.parse('0.267'))); }); + + test('should include account creation fee in total fee fallback', () { + final json = { + 'type': 'Tron', + 'coin': 'TRX', + 'bandwidth_used': 267, + 'energy_used': 0, + 'bandwidth_fee': '0.1', + 'energy_fee': '0', + 'account_creation_fee': '1', + }; + + final feeInfo = FeeInfo.fromJson(json); + + expect(feeInfo, isA()); + expect(feeInfo.totalFee, equals(Decimal.parse('1.1'))); + }); + + test('should omit account creation fee when it is absent', () { + final feeInfo = FeeInfo.tron( + coin: 'TRX', + bandwidthUsed: 345, + energyUsed: 0, + bandwidthFee: Decimal.parse('0.345'), + energyFee: Decimal.zero, + ); + + final json = feeInfo.toJson(); + + expect(json.containsKey('account_creation_fee'), isFalse); + expect(feeInfo.totalFee, equals(Decimal.parse('0.345'))); + }); }); group('FeeInfo Tendermint compatibility', () { diff --git a/packages/komodo_ui/lib/src/core/displays/fee_info_display.dart b/packages/komodo_ui/lib/src/core/displays/fee_info_display.dart index 77d9bf915..13d38544d 100644 --- a/packages/komodo_ui/lib/src/core/displays/fee_info_display.dart +++ b/packages/komodo_ui/lib/src/core/displays/fee_info_display.dart @@ -187,6 +187,16 @@ class FeeInfoDisplay extends StatelessWidget { '${fee.energyFee} ${fee.coin}', style: Theme.of(context).textTheme.labelLarge, ), + if (fee.accountCreationFee != null) ...[ + Text( + 'Account Activation Fee:', + style: Theme.of(context).textTheme.bodyMedium, + ), + Text( + '${fee.accountCreationFee} ${fee.coin}', + style: Theme.of(context).textTheme.labelLarge, + ), + ], ], final FeeInfoSia fee => [ diff --git a/packages/komodo_wallet_build_transformer/lib/src/steps/defi_api_build_step/dev_builds_artefact_downloader.dart b/packages/komodo_wallet_build_transformer/lib/src/steps/defi_api_build_step/dev_builds_artefact_downloader.dart index eea044333..d7e1f58b5 100644 --- a/packages/komodo_wallet_build_transformer/lib/src/steps/defi_api_build_step/dev_builds_artefact_downloader.dart +++ b/packages/komodo_wallet_build_transformer/lib/src/steps/defi_api_build_step/dev_builds_artefact_downloader.dart @@ -30,13 +30,16 @@ class DevBuildsArtefactDownloader implements ArtefactDownloader { ApiFileMatchingConfig matchingConfig, String platform, ) async { - // Try both branch-scoped and base-scoped listings to support different mirrors + // Try raw and sanitized branch-scoped listings before the base index. final normalizedSource = sourceUrl.endsWith('/') ? sourceUrl : '$sourceUrl/'; final baseUri = Uri.parse(normalizedSource); + final sanitizedBranch = apiBranch.replaceAll('/', '-'); final candidateListingUrls = { if (apiBranch.isNotEmpty) baseUri.resolve('$apiBranch/'), + if (apiBranch.isNotEmpty && sanitizedBranch != apiBranch) + baseUri.resolve('$sanitizedBranch/'), baseUri, }; @@ -75,7 +78,8 @@ class DevBuildsArtefactDownloader implements ArtefactDownloader { final containsHash = hrefPath.contains(fullHash) || hrefPath.contains(shortHash); if (containsHash) { - // Build absolute URL respecting whether href is absolute or relative + // Build an absolute URL, regardless of whether href is + // already absolute or relative to the listing page. final resolvedUrl = href.startsWith('http') ? href : listingUrl.resolve(href).toString(); diff --git a/packages/komodo_wallet_cli/bin/update_api_config.dart b/packages/komodo_wallet_cli/bin/update_api_config.dart index 693141556..1bfc09a82 100644 --- a/packages/komodo_wallet_cli/bin/update_api_config.dart +++ b/packages/komodo_wallet_cli/bin/update_api_config.dart @@ -606,13 +606,16 @@ class KdfFetcher { String? matchingKeyword, List matchingPreference, ) async { - // Try both branch-scoped and base listings; mirrors now expose branch paths + // Try raw and sanitized branch-scoped listings before falling back to the base index. final normalizedMirror = mirrorUrl.endsWith('/') ? mirrorUrl : '$mirrorUrl/'; final mirrorUri = Uri.parse(normalizedMirror); + final sanitizedBranch = branch.replaceAll('/', '-'); final listingUrls = { if (branch.isNotEmpty) mirrorUri.resolve('$branch/'), + if (branch.isNotEmpty && sanitizedBranch != branch) + mirrorUri.resolve('$sanitizedBranch/'), mirrorUri, }; From b8555f1dd4dd19e7d04b2cdf7248ddfdbf36a1bc Mon Sep 17 00:00:00 2001 From: Nitride <77973576+CharlVS@users.noreply.github.com> Date: Fri, 27 Mar 2026 17:34:55 +0100 Subject: [PATCH 28/40] fix(rpc_methods): pass RPC method hint for ambiguous KDF error parsing (#342) NotSufficientBalance is shared across RPC namespaces; KdfErrorRegistry previously returned null for all ambiguous types, causing withdraw status errors to fall back to GenericErrorResponse. - Thread rpcMethodHint from ApiClient.post and BaseRequest.parseResponse - Extend GeneralErrorResponse.toTypedException with optional hint - When hint matches withdraw task methods, parse withdraw-specific balance errors --- .../lib/src/models/base_request.dart | 30 ++++++++-- .../lib/src/models/error_response.dart | 7 ++- .../lib/src/models/mm2_rpc_exceptions.dart | 56 ++++++++++++++++++- 3 files changed, 83 insertions(+), 10 deletions(-) diff --git a/packages/komodo_defi_rpc_methods/lib/src/models/base_request.dart b/packages/komodo_defi_rpc_methods/lib/src/models/base_request.dart index d20f39c3b..0e73de408 100644 --- a/packages/komodo_defi_rpc_methods/lib/src/models/base_request.dart +++ b/packages/komodo_defi_rpc_methods/lib/src/models/base_request.dart @@ -13,7 +13,10 @@ extension BaseRequestApiClientExtension on ApiClient { if (GeneralErrorResponse.isErrorResponse(response)) { // Try to parse into a typed KDF exception first - final typedException = _tryParseTypedException(response); + final typedException = _tryParseTypedException( + response, + rpcMethodHint: request.method, + ); if (typedException != null) { throw typedException; } @@ -24,14 +27,20 @@ extension BaseRequestApiClientExtension on ApiClient { } /// Attempts to parse the error response into a typed [MmRpcException]. - MmRpcException? _tryParseTypedException(JsonMap response) { + MmRpcException? _tryParseTypedException( + JsonMap response, { + required String rpcMethodHint, + }) { // Extract error details from the response structure final errorDetails = response.valueOrNull('result', 'details') ?? response.valueOrNull('message') ?? response; - return KdfErrorRegistry.tryParse(errorDetails); + return KdfErrorRegistry.tryParse( + errorDetails, + rpcMethodHint: rpcMethodHint, + ); } } @@ -95,7 +104,10 @@ abstract class BaseRequest { // First check if this is an error response if (GeneralErrorResponse.isErrorResponse(json)) { // Try to parse into a typed KDF exception first - final typedException = _tryParseTypedException(json); + final typedException = _tryParseTypedException( + json, + rpcMethodHint: method, + ); if (typedException != null) { throw typedException; } @@ -117,14 +129,20 @@ abstract class BaseRequest { } /// Attempts to parse the error response into a typed [MmRpcException]. - MmRpcException? _tryParseTypedException(JsonMap json) { + MmRpcException? _tryParseTypedException( + JsonMap json, { + required String rpcMethodHint, + }) { // Extract error details from the response structure final errorDetails = json.valueOrNull('result', 'details') ?? json.valueOrNull('message') ?? json; - return KdfErrorRegistry.tryParse(errorDetails); + return KdfErrorRegistry.tryParse( + errorDetails, + rpcMethodHint: rpcMethodHint, + ); } /// Override this method to provide custom error handling for specific error diff --git a/packages/komodo_defi_rpc_methods/lib/src/models/error_response.dart b/packages/komodo_defi_rpc_methods/lib/src/models/error_response.dart index 72d984f0b..c42796eb2 100644 --- a/packages/komodo_defi_rpc_methods/lib/src/models/error_response.dart +++ b/packages/komodo_defi_rpc_methods/lib/src/models/error_response.dart @@ -65,7 +65,7 @@ class GeneralErrorResponse extends BaseResponse implements Exception { /// } /// } /// ``` - MmRpcException? toTypedException() { + MmRpcException? toTypedException({String? rpcMethodHint}) { // Build a JSON map suitable for KdfErrorRegistry parsing final errorJson = { 'error_type': errorType, @@ -74,7 +74,10 @@ class GeneralErrorResponse extends BaseResponse implements Exception { 'error_path': errorPath, 'error_trace': errorTrace, }; - return KdfErrorRegistry.tryParse(errorJson); + return KdfErrorRegistry.tryParse( + errorJson, + rpcMethodHint: rpcMethodHint, + ); } @override diff --git a/packages/komodo_defi_rpc_methods/lib/src/models/mm2_rpc_exceptions.dart b/packages/komodo_defi_rpc_methods/lib/src/models/mm2_rpc_exceptions.dart index e56e7cfbe..2c43aa02d 100644 --- a/packages/komodo_defi_rpc_methods/lib/src/models/mm2_rpc_exceptions.dart +++ b/packages/komodo_defi_rpc_methods/lib/src/models/mm2_rpc_exceptions.dart @@ -25024,16 +25024,34 @@ abstract final class KdfErrorRegistry { /// /// Returns `null` if the error type is not recognized or if the JSON /// does not contain an `error_type` field. - static MmRpcException? tryParse(JsonMap json) { + /// + /// [rpcMethodHint] disambiguates types in [_ambiguousErrorTypes] that share + /// the same `error_type` string across RPC namespaces (e.g. withdraw vs + /// delegation `NotSufficientBalance`). + static MmRpcException? tryParse( + JsonMap json, { + String? rpcMethodHint, + }) { final errorType = json['error_type'] as String?; if (errorType == null) return null; - if (_ambiguousErrorTypes.contains(errorType)) return null; final errorData = json['error_data']; final message = json['error'] as String? ?? json['message'] as String?; final path = json['error_path'] as String?; final trace = json['error_trace'] as String?; + final withdrawScoped = _tryParseWithdrawScopedAmbiguousType( + errorType: errorType, + errorData: errorData, + rpcMethodHint: rpcMethodHint, + message: message, + path: path, + trace: trace, + ); + if (withdrawScoped != null) return withdrawScoped; + + if (_ambiguousErrorTypes.contains(errorType)) return null; + final parser = _errorParsers[errorType]; if (parser == null) return null; @@ -25046,6 +25064,40 @@ abstract final class KdfErrorRegistry { } } + static MmRpcException? _tryParseWithdrawScopedAmbiguousType({ + required String errorType, + required dynamic errorData, + String? rpcMethodHint, + String? message, + String? path, + String? trace, + }) { + if (rpcMethodHint == null || !_isWithdrawRelatedRpcMethod(rpcMethodHint)) { + return null; + } + if (errorType != 'NotSufficientBalance') return null; + try { + final w = WithdrawErrorNotSufficientBalance.fromJson(errorData); + return WithdrawErrorNotSufficientBalanceException( + coin: w.coin, + available: w.available, + required: w.required, + message: message, + path: path, + trace: trace, + ); + } catch (_) { + return null; + } + } + + static bool _isWithdrawRelatedRpcMethod(String method) { + return method == 'withdraw' || + method == 'task::withdraw::init' || + method == 'task::withdraw::status' || + method == 'task::withdraw::cancel'; + } + /// Checks if the given error type string is a known KDF error type. static bool isKnownErrorType(String errorType) { return _errorParsers.containsKey(errorType); From 3cb147b4a0d3c3b0941daa538c8ba10f7be34887 Mon Sep 17 00:00:00 2001 From: Nitride <77973576+CharlVS@users.noreply.github.com> Date: Mon, 30 Mar 2026 18:30:58 +0200 Subject: [PATCH 29/40] feat(sia)!: harden activation and withdrawal handling (#343) - enforce single-terminal SIA activation progress semantics\n- add first-terminal-wins guard in activation manager\n- validate and reject SIA withdraw source selection (from)\n- route SIA preview/execute/one-shot withdraw through legacy flow\n- remove SIA-specific withdraw RPC surface and tests; use generic withdraw parsing\n- add SIA activation, withdrawal, and tx-history strategy coverage BREAKING CHANGE: removed withdrawSia(), SiaWithdrawRequest, and SiaWithdrawResponse. Migrate to generic withdraw()/WithdrawStatusResponse + WithdrawResult. --- .../lib/src/rpc_methods/rpc_methods.dart | 1 - .../lib/src/rpc_methods/sia/enable_sia.dart | 115 +++++++---- .../rpc_methods/sia/sia_rpc_namespace.dart | 16 +- .../withdrawal/sia_withdraw_request.dart | 93 --------- .../withdrawal/withdrawal_rpc_namespace.dart | 23 --- .../test/sia_rpc_methods_test.dart | 30 +-- .../test/src/withdraw_request_test.dart | 57 ++++++ .../src/activation/activation_manager.dart | 9 + .../sia_activation_strategy.dart | 83 ++++---- .../src/withdrawals/withdrawal_manager.dart | 26 ++- .../sia_activation_strategy_test.dart | 141 +++++++++++++ .../transaction_history_strategies_test.dart | 26 +++ .../withdrawal_manager_sia_test.dart | 185 ++++++++++++++++++ 13 files changed, 580 insertions(+), 225 deletions(-) delete mode 100644 packages/komodo_defi_rpc_methods/lib/src/rpc_methods/withdrawal/sia_withdraw_request.dart create mode 100644 packages/komodo_defi_sdk/test/activation/sia_activation_strategy_test.dart create mode 100644 packages/komodo_defi_sdk/test/withdrawals/withdrawal_manager_sia_test.dart diff --git a/packages/komodo_defi_rpc_methods/lib/src/rpc_methods/rpc_methods.dart b/packages/komodo_defi_rpc_methods/lib/src/rpc_methods/rpc_methods.dart index 5802c7c3f..5035f3e39 100644 --- a/packages/komodo_defi_rpc_methods/lib/src/rpc_methods/rpc_methods.dart +++ b/packages/komodo_defi_rpc_methods/lib/src/rpc_methods/rpc_methods.dart @@ -104,7 +104,6 @@ export 'wallet/get_wallet_names_response.dart'; export 'wallet/my_balance.dart'; export 'wallet/unban_pubkeys.dart'; export 'withdrawal/send_raw_transaction_request.dart'; -export 'withdrawal/sia_withdraw_request.dart'; export 'withdrawal/withdraw_request.dart'; export 'withdrawal/withdrawal_rpc_namespace.dart'; export 'zhtlc/z_coin_tx_history.dart'; diff --git a/packages/komodo_defi_rpc_methods/lib/src/rpc_methods/sia/enable_sia.dart b/packages/komodo_defi_rpc_methods/lib/src/rpc_methods/sia/enable_sia.dart index 7095879a9..c41745cea 100644 --- a/packages/komodo_defi_rpc_methods/lib/src/rpc_methods/sia/enable_sia.dart +++ b/packages/komodo_defi_rpc_methods/lib/src/rpc_methods/sia/enable_sia.dart @@ -3,11 +3,8 @@ import 'package:komodo_defi_types/komodo_defi_type_utils.dart'; class TaskEnableSiaInit extends BaseRequest { - TaskEnableSiaInit({ - required this.ticker, - required this.params, - super.rpcPass, - }) : super(method: 'task::enable_sia::init', mmrpc: RpcVersion.v2_0); + TaskEnableSiaInit({required this.ticker, required this.params, super.rpcPass}) + : super(method: 'task::enable_sia::init', mmrpc: RpcVersion.v2_0); final String ticker; @@ -17,15 +14,12 @@ class TaskEnableSiaInit @override Map toJson() => { - ...super.toJson(), - 'userpass': rpcPass, - 'mmrpc': mmrpc, - 'method': method, - 'params': { - 'ticker': ticker, - 'activation_params': params.toRpcParams(), - }, - }; + ...super.toJson(), + 'userpass': rpcPass, + 'mmrpc': mmrpc, + 'method': method, + 'params': {'ticker': ticker, 'activation_params': params.toRpcParams()}, + }; @override NewTaskResponse parse(Map json) { @@ -34,7 +28,7 @@ class TaskEnableSiaInit } class TaskEnableSiaStatus - extends BaseRequest { + extends BaseRequest { TaskEnableSiaStatus({ required this.taskId, this.forgetIfFinished = true, @@ -46,39 +40,88 @@ class TaskEnableSiaStatus @override Map toJson() => { - ...super.toJson(), - 'userpass': rpcPass, - 'mmrpc': mmrpc, - 'method': method, - 'params': {'task_id': taskId, 'forget_if_finished': forgetIfFinished}, - }; + ...super.toJson(), + 'userpass': rpcPass, + 'mmrpc': mmrpc, + 'method': method, + 'params': {'task_id': taskId, 'forget_if_finished': forgetIfFinished}, + }; @override - TaskStatusResponse parse(Map json) { - return TaskStatusResponse.parse(json); + SiaTaskStatusResponse parse(Map json) { + return SiaTaskStatusResponse.parse(json); } } class TaskEnableSiaCancel - extends BaseRequest { - TaskEnableSiaCancel({ - required this.taskId, - super.rpcPass, - }) : super(method: 'task::enable_sia::cancel', mmrpc: RpcVersion.v2_0); + extends BaseRequest { + TaskEnableSiaCancel({required this.taskId, super.rpcPass}) + : super(method: 'task::enable_sia::cancel', mmrpc: RpcVersion.v2_0); final int taskId; @override Map toJson() => { - ...super.toJson(), - 'userpass': rpcPass, - 'mmrpc': mmrpc, - 'method': method, - 'params': {'task_id': taskId}, - }; + ...super.toJson(), + 'userpass': rpcPass, + 'mmrpc': mmrpc, + 'method': method, + 'params': {'task_id': taskId}, + }; + + @override + SiaTaskCancelResponse parse(Map json) => + SiaTaskCancelResponse.parse(json); +} + +class SiaTaskStatusResponse extends BaseResponse { + SiaTaskStatusResponse({ + required super.mmrpc, + required this.status, + required this.details, + }); + + factory SiaTaskStatusResponse.parse(Map json) { + final result = json.value('result'); + return SiaTaskStatusResponse( + mmrpc: json.value('mmrpc'), + status: result.value('status'), + details: result['details'], + ); + } + + final String status; + final Object? details; + + bool get isCompleted => status == 'Ok'; + + String? get detailsAsString { + final raw = details; + if (raw == null) { + return null; + } + return raw.toString(); + } @override - TaskStatusResponse parse(Map json) => - TaskStatusResponse.parse(json); + Map toJson() => { + 'mmrpc': mmrpc, + 'result': {'status': status, 'details': details}, + }; } +class SiaTaskCancelResponse extends BaseResponse { + SiaTaskCancelResponse({required super.mmrpc, required this.result}); + + factory SiaTaskCancelResponse.parse(Map json) { + return SiaTaskCancelResponse( + mmrpc: json.value('mmrpc'), + result: json.value('result'), + ); + } + + final String result; + + @override + Map toJson() => {'mmrpc': mmrpc, 'result': result}; +} diff --git a/packages/komodo_defi_rpc_methods/lib/src/rpc_methods/sia/sia_rpc_namespace.dart b/packages/komodo_defi_rpc_methods/lib/src/rpc_methods/sia/sia_rpc_namespace.dart index ef36b1f87..82b8b5805 100644 --- a/packages/komodo_defi_rpc_methods/lib/src/rpc_methods/sia/sia_rpc_namespace.dart +++ b/packages/komodo_defi_rpc_methods/lib/src/rpc_methods/sia/sia_rpc_namespace.dart @@ -1,5 +1,4 @@ import 'package:komodo_defi_rpc_methods/komodo_defi_rpc_methods.dart'; -import 'package:komodo_defi_types/komodo_defi_type_utils.dart'; class SiaMethodsNamespace extends BaseRpcMethodNamespace { SiaMethodsNamespace(super.client); @@ -9,15 +8,11 @@ class SiaMethodsNamespace extends BaseRpcMethodNamespace { required SiaActivationParams params, }) { return execute( - TaskEnableSiaInit( - rpcPass: rpcPass ?? '', - ticker: ticker, - params: params, - ), + TaskEnableSiaInit(rpcPass: rpcPass ?? '', ticker: ticker, params: params), ); } - Future enableSiaStatus( + Future enableSiaStatus( int taskId, { bool forgetIfFinished = true, }) { @@ -30,10 +25,7 @@ class SiaMethodsNamespace extends BaseRpcMethodNamespace { ); } - Future enableSiaCancel({required int taskId}) { - return execute( - TaskEnableSiaCancel(taskId: taskId, rpcPass: rpcPass), - ); + Future enableSiaCancel({required int taskId}) { + return execute(TaskEnableSiaCancel(taskId: taskId, rpcPass: rpcPass)); } } - diff --git a/packages/komodo_defi_rpc_methods/lib/src/rpc_methods/withdrawal/sia_withdraw_request.dart b/packages/komodo_defi_rpc_methods/lib/src/rpc_methods/withdrawal/sia_withdraw_request.dart deleted file mode 100644 index bb60d9a55..000000000 --- a/packages/komodo_defi_rpc_methods/lib/src/rpc_methods/withdrawal/sia_withdraw_request.dart +++ /dev/null @@ -1,93 +0,0 @@ -import 'package:decimal/decimal.dart'; -import 'package:komodo_defi_rpc_methods/komodo_defi_rpc_methods.dart'; -import 'package:komodo_defi_types/komodo_defi_type_utils.dart'; -import 'package:komodo_defi_types/komodo_defi_types.dart'; - -/// SIA-specific withdraw request -/// -/// Uses the same 'withdraw' RPC but parses SIA-flavored response payload -class SiaWithdrawRequest - extends BaseRequest { - SiaWithdrawRequest({ - required super.rpcPass, - required this.coin, - required this.to, - this.amount, - this.fee, - this.from, - this.max = false, - }) : assert(amount != null || max, 'Specify amount or set max=true'), - super(method: 'withdraw', mmrpc: RpcVersion.v2_0); - - final String coin; - final String to; - final Decimal? amount; - final FeeInfo? fee; - final WithdrawalSource? from; - final bool max; - - @override - Map toJson() => { - ...super.toJson(), - 'params': { - 'coin': coin, - 'to': to, - if (!max && amount != null) 'amount': amount!.toString(), - if (max) 'max': true, - if (fee != null) 'fee': fee!.toJson(), - if (from != null) 'from': from!.toRpcParams(), - }, - }; - - @override - SiaWithdrawResponse parse(Map json) => - SiaWithdrawResponse.parse(json); -} - -class SiaWithdrawResponse extends BaseResponse { - SiaWithdrawResponse({ - required super.mmrpc, - required this.status, - required this.spentByMe, - required this.receivedByMe, - required this.myBalanceChange, - this.feeDetails, - this.details, - }); - - factory SiaWithdrawResponse.parse(Map json) { - final result = json.value('result'); - return SiaWithdrawResponse( - mmrpc: json.value('mmrpc'), - status: result.value('status'), - spentByMe: result.value('spent_by_me'), - receivedByMe: result.value('received_by_me'), - myBalanceChange: result.value('my_balance_change'), - feeDetails: result.valueOrNull('fee_details') == null - ? null - : FeeInfo.fromJson(result.value('fee_details')), - details: result.valueOrNull('details'), - ); - } - - final String status; - final String spentByMe; - final String receivedByMe; - final String myBalanceChange; - final FeeInfo? feeDetails; - final dynamic details; - - @override - Map toJson() => { - 'mmrpc': mmrpc, - 'result': { - 'status': status, - 'spent_by_me': spentByMe, - 'received_by_me': receivedByMe, - 'my_balance_change': myBalanceChange, - if (feeDetails != null) 'fee_details': feeDetails!.toJson(), - if (details != null) 'details': details, - }, - }; -} - diff --git a/packages/komodo_defi_rpc_methods/lib/src/rpc_methods/withdrawal/withdrawal_rpc_namespace.dart b/packages/komodo_defi_rpc_methods/lib/src/rpc_methods/withdrawal/withdrawal_rpc_namespace.dart index 10f939a86..b4e7545d5 100644 --- a/packages/komodo_defi_rpc_methods/lib/src/rpc_methods/withdrawal/withdrawal_rpc_namespace.dart +++ b/packages/komodo_defi_rpc_methods/lib/src/rpc_methods/withdrawal/withdrawal_rpc_namespace.dart @@ -1,5 +1,4 @@ import 'package:komodo_defi_rpc_methods/src/internal_exports.dart'; -import 'package:decimal/decimal.dart'; import 'package:komodo_defi_types/komodo_defi_types.dart'; class WithdrawMethodsNamespace extends BaseRpcMethodNamespace { @@ -24,28 +23,6 @@ class WithdrawMethodsNamespace extends BaseRpcMethodNamespace { ); } - /// Convenience wrapper for SIA withdrawals with SIA-specific response parsing - Future withdrawSia({ - required String coin, - required String to, - Decimal? amount, - FeeInfo? fee, - WithdrawalSource? from, - bool max = false, - }) { - return execute( - SiaWithdrawRequest( - rpcPass: rpcPass ?? '', - coin: coin, - to: to, - amount: amount, - fee: fee, - from: from, - max: max, - ), - ); - } - /// Initialize a new withdrawal task // TODO: Consider refactoring to use individual parameters instead of a single // object for the request parameters for the sake of consistency with other diff --git a/packages/komodo_defi_rpc_methods/test/sia_rpc_methods_test.dart b/packages/komodo_defi_rpc_methods/test/sia_rpc_methods_test.dart index 0894efcd0..470c013bf 100644 --- a/packages/komodo_defi_rpc_methods/test/sia_rpc_methods_test.dart +++ b/packages/komodo_defi_rpc_methods/test/sia_rpc_methods_test.dart @@ -1,6 +1,5 @@ -import 'package:flutter_test/flutter_test.dart'; import 'package:komodo_defi_rpc_methods/komodo_defi_rpc_methods.dart'; -import 'package:komodo_defi_types/komodo_defi_type_utils.dart'; +import 'package:test/test.dart'; void main() { group('SIA RPC', () { @@ -23,20 +22,27 @@ void main() { expect(p['required_confirmations'], 1); }); - test('SiaWithdrawResponse parses nullable fee_details safely', () { - final response = { + test('TaskEnableSiaStatus parses object details', () { + final response = TaskEnableSiaStatus(taskId: 1).parse({ 'mmrpc': '2.0', 'result': { 'status': 'Ok', - 'spent_by_me': '0', - 'received_by_me': '100', - 'my_balance_change': '100', - // fee_details intentionally omitted + 'details': {'ticker': 'SC', 'current_block': 100}, }, - }; - final parsed = SiaWithdrawResponse.parse(JsonMap.of(response)); - expect(parsed.status, 'Ok'); - expect(parsed.feeDetails, isNull); + 'id': null, + }); + + expect(response.status, 'Ok'); + expect(response.isCompleted, isTrue); + expect(response.details, isA>()); + }); + + test('TaskEnableSiaCancel parses success result', () { + final response = TaskEnableSiaCancel( + taskId: 1, + ).parse({'mmrpc': '2.0', 'result': 'success', 'id': null}); + + expect(response.result, 'success'); }); }); } diff --git a/packages/komodo_defi_rpc_methods/test/src/withdraw_request_test.dart b/packages/komodo_defi_rpc_methods/test/src/withdraw_request_test.dart index f35d2efba..c9ce705d2 100644 --- a/packages/komodo_defi_rpc_methods/test/src/withdraw_request_test.dart +++ b/packages/komodo_defi_rpc_methods/test/src/withdraw_request_test.dart @@ -32,5 +32,62 @@ void main() { closeTo(0.00000038553, 1e-18), ); }); + + test('parses SIA-style non-task v2 withdraw response', () { + final request = WithdrawRequest( + rpcPass: 'rpc-pass', + coin: 'SC', + to: 'recipient', + amount: Decimal.parse('1'), + ); + + final parsed = request.parse({ + 'mmrpc': '2.0', + 'result': { + 'tx_json': { + 'siacoinInputs': >[], + 'siacoinOutputs': >[], + 'minerFee': '10000000000000000000', + }, + 'tx_hash': '0xabc', + 'from': ['sender'], + 'to': ['recipient'], + 'total_amount': '1.000000000000000000000000', + 'spent_by_me': '1.000000000000000000000000', + 'received_by_me': '0', + 'my_balance_change': '-1.000000000000000000000000', + 'block_height': 1, + 'timestamp': 123456, + 'fee_details': { + 'type': 'Sia', + 'coin': 'SC', + 'policy': 'Fixed', + 'total_amount': '0.000010000000000000000000', + }, + 'coin': 'SC', + 'internal_id': '', + 'transaction_type': 'SiaV2Transaction', + 'memo': null, + }, + 'id': null, + }); + + expect(parsed.status, 'Ok'); + expect(parsed.details, isA()); + + final details = parsed.details as WithdrawResult; + expect(details.coin, 'SC'); + expect(details.txHex, isNull); + expect(details.txJson, isNotNull); + expect(details.txHash, '0xabc'); + expect( + details.fee, + FeeInfo.sia( + coin: 'SC', + amount: Decimal.parse('0.000010000000000000000000'), + policy: 'Fixed', + ), + ); + }); }); } diff --git a/packages/komodo_defi_sdk/lib/src/activation/activation_manager.dart b/packages/komodo_defi_sdk/lib/src/activation/activation_manager.dart index 21d81b5e8..e600d2637 100644 --- a/packages/komodo_defi_sdk/lib/src/activation/activation_manager.dart +++ b/packages/komodo_defi_sdk/lib/src/activation/activation_manager.dart @@ -170,6 +170,7 @@ class ActivationManager { _activatedAssetsCache, ); + var completionHandled = false; await for (final progress in activator.activate( parentAsset ?? group.primary, group.children?.toList(), @@ -195,6 +196,14 @@ class ActivationManager { yield _attachSdkError(progress, group.primary.id); if (progress.isComplete) { + if (completionHandled) { + debugPrint( + 'Ignoring duplicate completion event for ' + '${group.primary.id.name}', + ); + continue; + } + completionHandled = true; await _handleActivationComplete(group, progress, primaryCompleter); } } diff --git a/packages/komodo_defi_sdk/lib/src/activation/protocol_strategies/sia_activation_strategy.dart b/packages/komodo_defi_sdk/lib/src/activation/protocol_strategies/sia_activation_strategy.dart index 52a954b42..66c336be4 100644 --- a/packages/komodo_defi_sdk/lib/src/activation/protocol_strategies/sia_activation_strategy.dart +++ b/packages/komodo_defi_sdk/lib/src/activation/protocol_strategies/sia_activation_strategy.dart @@ -60,60 +60,50 @@ class SiaActivationStrategy extends ProtocolActivationStrategy { final status = await KomodoDefiRpcMethods( client, ).sia.enableSiaStatus(taskId); - - yield ActivationProgress( - status: 'SIA activation in progress', - progressDetails: ActivationProgressDetails( - currentStep: ActivationStep.processing, - stepCount: 3, - additionalInfo: {'status': status.status}, - ), - ); - - // Stop polling on any terminal state - if (status.status != 'InProgress') { - if (status.status == 'Ok') { - yield ActivationProgress( - status: 'SIA activation complete', - isComplete: true, - progressDetails: ActivationProgressDetails( - currentStep: ActivationStep.complete, - stepCount: 3, - additionalInfo: {'taskId': taskId}, - ), - ); - } else { - final errorProgress = buildErrorProgress( - asset: asset, - error: status.details ?? 'SIA activation failed', - errorCode: 'SIA_ACTIVATION_ERROR', - stepCount: 3, - status: 'SIA activation failed', - ); - yield errorProgress.copyWith( - progressDetails: errorProgress.progressDetails?.copyWith( - additionalInfo: { - 'taskId': taskId, - 'status': status.status, - 'details': status.details, - }, - ), - ); - } + if (status.status == 'InProgress') { yield ActivationProgress( - status: 'SIA activation concluded', - isComplete: true, + status: 'SIA activation in progress', progressDetails: ActivationProgressDetails( - currentStep: status.status == 'Ok' - ? ActivationStep.complete - : ActivationStep.error, + currentStep: ActivationStep.processing, + stepCount: 3, + additionalInfo: {'status': status.status}, + ), + ); + await Future.delayed(kPollInterval); + continue; + } + + if (status.status == 'Ok') { + yield ActivationProgress.success( + details: ActivationProgressDetails( + currentStep: ActivationStep.complete, stepCount: 3, + additionalInfo: {'taskId': taskId, 'status': status.status}, ), ); break; } - await Future.delayed(kPollInterval); + final errorDetails = (status.detailsAsString ?? '').trim().isEmpty + ? 'SIA activation failed' + : status.detailsAsString!; + final errorProgress = buildErrorProgress( + asset: asset, + error: errorDetails, + errorCode: 'SIA_ACTIVATION_ERROR', + stepCount: 3, + status: 'SIA activation failed', + ); + yield errorProgress.copyWith( + progressDetails: errorProgress.progressDetails?.copyWith( + additionalInfo: { + 'taskId': taskId, + 'status': status.status, + 'details': status.details, + }, + ), + ); + break; } } on Exception catch (e, stack) { final errorProgress = buildErrorProgress( @@ -129,7 +119,6 @@ class SiaActivationStrategy extends ProtocolActivationStrategy { additionalInfo: {'error': e.toString()}, ), ); - rethrow; } } } diff --git a/packages/komodo_defi_sdk/lib/src/withdrawals/withdrawal_manager.dart b/packages/komodo_defi_sdk/lib/src/withdrawals/withdrawal_manager.dart index 89e5615ce..723e52943 100644 --- a/packages/komodo_defi_sdk/lib/src/withdrawals/withdrawal_manager.dart +++ b/packages/komodo_defi_sdk/lib/src/withdrawals/withdrawal_manager.dart @@ -525,6 +525,7 @@ class WithdrawalManager { final asset = _assetProvider .findAssetsByConfigId(parameters.asset) .single; + _validateSiaSourceSelection(parameters, asset); final isTendermintProtocol = asset.protocol is TendermintProtocol; final isSiaProtocol = asset.protocol is SiaProtocol; @@ -619,9 +620,10 @@ class WithdrawalManager { try { final asset = _assetProvider.findAssetsByConfigId(assetId).single; final isTendermintProtocol = asset.protocol is TendermintProtocol; + final isSiaProtocol = asset.protocol is SiaProtocol; // Tendermint assets are not yet supported by the task-based API - if (isTendermintProtocol) { + if (isTendermintProtocol || isSiaProtocol) { yield* _legacyManager.executeWithdrawal(preview, assetId); return; } @@ -725,6 +727,7 @@ class WithdrawalManager { final asset = _assetProvider .findAssetsByConfigId(parameters.asset) .single; + _validateSiaSourceSelection(parameters, asset); final isTendermintProtocol = asset.protocol is TendermintProtocol; final isSiaProtocol = asset.protocol is SiaProtocol; @@ -844,6 +847,27 @@ class WithdrawalManager { ); } + void _validateSiaSourceSelection(WithdrawParameters parameters, Asset asset) { + if (asset.protocol is! SiaProtocol || parameters.from == null) { + return; + } + + throw SdkError( + code: SdkErrorCode.notSupported, + category: SdkErrorCategory.unsupported, + messageKey: 'withdrawal.sia.source_not_supported', + fallbackMessage: + 'SIA withdrawals do not support "from" derivation/account/index ' + 'parameters.', + context: SdkErrorContext( + operation: 'withdrawal.validate', + assetId: parameters.asset, + extra: {'protocol': 'SIA', 'param': 'from'}, + ), + retryable: false, + ); + } + /// Provides estimated confirmation times for Ethereum-based transactions. /// /// Returns user-friendly estimated confirmation times based on the fee priority level. diff --git a/packages/komodo_defi_sdk/test/activation/sia_activation_strategy_test.dart b/packages/komodo_defi_sdk/test/activation/sia_activation_strategy_test.dart new file mode 100644 index 000000000..50a5fb12c --- /dev/null +++ b/packages/komodo_defi_sdk/test/activation/sia_activation_strategy_test.dart @@ -0,0 +1,141 @@ +import 'dart:collection'; + +import 'package:komodo_defi_sdk/src/activation/protocol_strategies/sia_activation_strategy.dart'; +import 'package:komodo_defi_types/komodo_defi_type_utils.dart'; +import 'package:komodo_defi_types/komodo_defi_types.dart'; +import 'package:test/test.dart'; + +class _QueueApiClient implements ApiClient { + _QueueApiClient({ + required Map> responsesByMethod, + this.errorsByMethod = const {}, + }) : _responsesByMethod = { + for (final entry in responsesByMethod.entries) + entry.key: Queue.from(entry.value), + }; + + final Map> _responsesByMethod; + final Map errorsByMethod; + + @override + Future executeRpc(JsonMap request) async { + final method = request.value('method'); + + final error = errorsByMethod[method]; + if (error != null) { + throw error; + } + + final queue = _responsesByMethod[method]; + if (queue == null || queue.isEmpty) { + throw StateError('No queued response for method $method'); + } + + return queue.removeFirst(); + } +} + +Asset _createSiaAsset() { + return Asset.fromJson(const { + 'coin': 'SC', + 'type': 'SIA', + 'name': 'Siacoin', + 'fname': 'Siacoin', + 'wallet_only': false, + 'mm2': 1, + 'chain_id': 2024, + 'decimals': 24, + 'required_confirmations': 1, + 'nodes': [ + {'url': 'https://api.siascan.com/wallet/api'}, + ], + }); +} + +void main() { + group('SiaActivationStrategy', () { + test('emits exactly one terminal success event', () async { + final strategy = SiaActivationStrategy( + _QueueApiClient( + responsesByMethod: { + 'task::enable_sia::init': [ + { + 'mmrpc': '2.0', + 'result': {'task_id': 7}, + }, + ], + 'task::enable_sia::status': [ + { + 'mmrpc': '2.0', + 'result': {'status': 'InProgress', 'details': 'syncing'}, + }, + { + 'mmrpc': '2.0', + 'result': {'status': 'Ok', 'details': 'done'}, + }, + ], + }, + ), + ); + + final events = await strategy.activate(_createSiaAsset()).toList(); + final terminalEvents = events.where((event) => event.isComplete).toList(); + + expect(terminalEvents, hasLength(1)); + expect(terminalEvents.single.isSuccess, isTrue); + }); + + test('emits exactly one terminal failure event for error status', () async { + final strategy = SiaActivationStrategy( + _QueueApiClient( + responsesByMethod: { + 'task::enable_sia::init': [ + { + 'mmrpc': '2.0', + 'result': {'task_id': 8}, + }, + ], + 'task::enable_sia::status': [ + { + 'mmrpc': '2.0', + 'result': {'status': 'Error', 'details': 'activation failed'}, + }, + ], + }, + ), + ); + + final events = await strategy.activate(_createSiaAsset()).toList(); + final terminalEvents = events.where((event) => event.isComplete).toList(); + + expect(terminalEvents, hasLength(1)); + expect(terminalEvents.single.isError, isTrue); + expect(terminalEvents.single.isSuccess, isFalse); + }); + + test('emits exactly one terminal failure event for exceptions', () async { + final strategy = SiaActivationStrategy( + _QueueApiClient( + responsesByMethod: { + 'task::enable_sia::init': [ + { + 'mmrpc': '2.0', + 'result': {'task_id': 9}, + }, + ], + }, + errorsByMethod: { + 'task::enable_sia::status': Exception('network failure'), + }, + ), + ); + + final events = await strategy.activate(_createSiaAsset()).toList(); + final terminalEvents = events.where((event) => event.isComplete).toList(); + + expect(terminalEvents, hasLength(1)); + expect(terminalEvents.single.isError, isTrue); + expect(terminalEvents.single.isSuccess, isFalse); + }); + }); +} diff --git a/packages/komodo_defi_sdk/test/transaction_history/transaction_history_strategies_test.dart b/packages/komodo_defi_sdk/test/transaction_history/transaction_history_strategies_test.dart index a79e35454..42ebd67a4 100644 --- a/packages/komodo_defi_sdk/test/transaction_history/transaction_history_strategies_test.dart +++ b/packages/komodo_defi_sdk/test/transaction_history/transaction_history_strategies_test.dart @@ -110,6 +110,23 @@ Asset _createZhtlcAsset() { ); } +Asset _createSiaAsset() { + return Asset.fromJson({ + 'coin': 'SC', + 'type': 'SIA', + 'name': 'Siacoin', + 'fname': 'Siacoin', + 'wallet_only': false, + 'mm2': 1, + 'chain_id': 2024, + 'decimals': 24, + 'required_confirmations': 1, + 'nodes': const [ + {'url': 'https://api.siascan.com/wallet/api'}, + ], + }); +} + AssetPubkeys _makePubkeys(Asset asset) => AssetPubkeys( assetId: asset.id, keys: [ @@ -240,6 +257,15 @@ void main() { expect(strategy, isA()); }); + test('uses Legacy strategy for SIA assets', () { + final factory = TransactionHistoryStrategyFactory(pubkeyManager, auth); + + final strategy = factory.forAsset(_createSiaAsset()); + + expect(strategy, isA()); + expect(strategy, isNot(isA())); + }); + test('selects Tronscan strategy for TRX asset', () { final factory = TransactionHistoryStrategyFactory(pubkeyManager, auth); final trx = _createTrxAsset(); diff --git a/packages/komodo_defi_sdk/test/withdrawals/withdrawal_manager_sia_test.dart b/packages/komodo_defi_sdk/test/withdrawals/withdrawal_manager_sia_test.dart new file mode 100644 index 000000000..8f16e11b3 --- /dev/null +++ b/packages/komodo_defi_sdk/test/withdrawals/withdrawal_manager_sia_test.dart @@ -0,0 +1,185 @@ +import 'package:decimal/decimal.dart'; +import 'package:komodo_defi_sdk/src/activation/shared_activation_coordinator.dart'; +import 'package:komodo_defi_sdk/src/assets/asset_lookup.dart'; +import 'package:komodo_defi_sdk/src/fees/fee_manager.dart'; +import 'package:komodo_defi_sdk/src/withdrawals/legacy_withdrawal_manager.dart'; +import 'package:komodo_defi_sdk/src/withdrawals/withdrawal_manager.dart'; +import 'package:komodo_defi_types/komodo_defi_types.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:test/test.dart'; + +class _MockApiClient extends Mock implements ApiClient {} + +class _MockAssetProvider extends Mock implements IAssetProvider {} + +class _MockFeeManager extends Mock implements FeeManager {} + +class _MockActivationCoordinator extends Mock + implements SharedActivationCoordinator {} + +class _MockLegacyWithdrawalManager extends Mock + implements LegacyWithdrawalManager {} + +Asset _createSiaAsset() { + return Asset.fromJson(const { + 'coin': 'SC', + 'type': 'SIA', + 'name': 'Siacoin', + 'fname': 'Siacoin', + 'wallet_only': false, + 'mm2': 1, + 'chain_id': 2024, + 'decimals': 24, + 'required_confirmations': 1, + 'nodes': [ + {'url': 'https://api.siascan.com/wallet/api'}, + ], + }); +} + +WithdrawalPreview _siaPreview() { + return WithdrawResult( + txJson: const { + 'siacoinInputs': >[], + 'siacoinOutputs': >[], + 'minerFee': '10000000000000000000', + }, + txHash: '0xabc', + from: const ['sender'], + to: const ['recipient'], + balanceChanges: BalanceChanges( + netChange: Decimal.parse('-1'), + receivedByMe: Decimal.zero, + spentByMe: Decimal.one, + totalAmount: Decimal.one, + ), + blockHeight: 1, + timestamp: 123456, + fee: FeeInfo.sia( + coin: 'SC', + amount: Decimal.parse('0.000010000000000000000000'), + policy: 'Fixed', + ), + coin: 'SC', + ); +} + +void main() { + group('WithdrawalManager SIA behavior', () { + late ApiClient client; + late IAssetProvider assetProvider; + late FeeManager feeManager; + late SharedActivationCoordinator activationCoordinator; + late LegacyWithdrawalManager legacyManager; + late WithdrawalManager manager; + late Asset siaAsset; + + setUp(() { + client = _MockApiClient(); + assetProvider = _MockAssetProvider(); + feeManager = _MockFeeManager(); + activationCoordinator = _MockActivationCoordinator(); + legacyManager = _MockLegacyWithdrawalManager(); + manager = WithdrawalManager( + client, + assetProvider, + feeManager, + activationCoordinator, + legacyManager, + ); + siaAsset = _createSiaAsset(); + + when( + () => assetProvider.findAssetsByConfigId('SC'), + ).thenReturn({siaAsset}); + }); + + test('preview rejects SIA source selection params', () async { + final params = WithdrawParameters( + asset: 'SC', + toAddress: 'recipient', + amount: Decimal.one, + from: WithdrawalSource.hdDerivationPath("m/44'/141'/0'/0/0"), + ); + + await expectLater( + manager.previewWithdrawal(params), + throwsA( + isA().having( + (error) => error.code, + 'code', + SdkErrorCode.notSupported, + ), + ), + ); + + verifyNever(() => legacyManager.previewWithdrawal(params)); + }); + + test('withdraw rejects SIA source selection params', () async { + final params = WithdrawParameters( + asset: 'SC', + toAddress: 'recipient', + amount: Decimal.one, + from: WithdrawalSource.hdDerivationPath("m/44'/141'/0'/0/0"), + ); + + await expectLater( + manager.withdraw(params).toList(), + throwsA( + isA().having( + (error) => error.code, + 'code', + SdkErrorCode.notSupported, + ), + ), + ); + + verifyNever(() => legacyManager.withdraw(params)); + }); + + test('preview delegates SIA flow to legacy manager', () async { + final params = WithdrawParameters( + asset: 'SC', + toAddress: 'recipient', + amount: Decimal.one, + ); + final preview = _siaPreview(); + when( + () => legacyManager.previewWithdrawal(params), + ).thenAnswer((_) async => preview); + + final result = await manager.previewWithdrawal(params); + + expect(result, same(preview)); + verify(() => legacyManager.previewWithdrawal(params)).called(1); + }); + + test('execute delegates SIA flow to legacy manager', () async { + final preview = _siaPreview(); + when( + () => legacyManager.executeWithdrawal(preview, 'SC'), + ).thenAnswer((_) => const Stream.empty()); + + await manager.executeWithdrawal(preview, 'SC').drain(); + + verify(() => legacyManager.executeWithdrawal(preview, 'SC')).called(1); + verifyNever(() => activationCoordinator.activateAsset(siaAsset)); + }); + + test('one-shot withdraw delegates SIA flow to legacy manager', () async { + final params = WithdrawParameters( + asset: 'SC', + toAddress: 'recipient', + amount: Decimal.one, + ); + when( + () => legacyManager.withdraw(params), + ).thenAnswer((_) => const Stream.empty()); + + await manager.withdraw(params).drain(); + + verify(() => legacyManager.withdraw(params)).called(1); + }); + }); +} From 6dddeee0b004024ba8746d451f2baad332df778d Mon Sep 17 00:00:00 2001 From: Nitride <77973576+CharlVS@users.noreply.github.com> Date: Mon, 30 Mar 2026 18:54:43 +0200 Subject: [PATCH 30/40] fix(ui): correct TRC20 badge icon mapping (#344) --- .../lib/src/coin_classes/coin_subclasses.dart | 2 +- .../test/tron_protocol_test.dart | 4 ++ .../komodo_ui/test/src/asset_logo_test.dart | 44 +++++++++++++++++++ 3 files changed, 49 insertions(+), 1 deletion(-) create mode 100644 packages/komodo_ui/test/src/asset_logo_test.dart diff --git a/packages/komodo_defi_types/lib/src/coin_classes/coin_subclasses.dart b/packages/komodo_defi_types/lib/src/coin_classes/coin_subclasses.dart index e56734951..6642db921 100644 --- a/packages/komodo_defi_types/lib/src/coin_classes/coin_subclasses.dart +++ b/packages/komodo_defi_types/lib/src/coin_classes/coin_subclasses.dart @@ -109,7 +109,7 @@ enum CoinSubClass { case CoinSubClass.trx: return 'TRX'; case CoinSubClass.trc20: - return 'TRC'; + return 'TRX'; case CoinSubClass.moonbeam: return 'GLMR'; case CoinSubClass.ftm20: diff --git a/packages/komodo_defi_types/test/tron_protocol_test.dart b/packages/komodo_defi_types/test/tron_protocol_test.dart index 572d71f09..32e5e0781 100644 --- a/packages/komodo_defi_types/test/tron_protocol_test.dart +++ b/packages/komodo_defi_types/test/tron_protocol_test.dart @@ -153,6 +153,10 @@ void main() { ); }); + test('TRC20 child assets use the TRX badge icon ticker', () { + expect(CoinSubClass.trc20.iconTicker, 'TRX'); + }); + test('non-TRON platform assets keep top-level subtype precedence', () { final assetId = AssetId.parse(_avaxConfig(), knownIds: const {}); final protocol = ProtocolClass.fromJson(_avaxConfig()); diff --git a/packages/komodo_ui/test/src/asset_logo_test.dart b/packages/komodo_ui/test/src/asset_logo_test.dart new file mode 100644 index 000000000..c1b40033e --- /dev/null +++ b/packages/komodo_ui/test/src/asset_logo_test.dart @@ -0,0 +1,44 @@ +// ignore_for_file: prefer_const_constructors + +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:komodo_defi_types/komodo_defi_types.dart'; +import 'package:komodo_ui/komodo_ui.dart'; + +void main() { + group('AssetLogo', () { + testWidgets('uses the TRX protocol badge for TRC20 child assets', ( + tester, + ) async { + final parent = AssetId( + id: 'TRX', + name: 'TRON', + symbol: AssetSymbol(assetConfigId: 'TRX'), + chainId: AssetChainId(chainId: 195, decimalsValue: 6), + derivationPath: "m/44'/195'", + subClass: CoinSubClass.trx, + ); + final child = AssetId( + id: 'USDT-TRC20', + name: 'Tether', + symbol: AssetSymbol(assetConfigId: 'USDT-TRC20'), + chainId: AssetChainId(chainId: 195, decimalsValue: 6), + derivationPath: "m/44'/195'", + subClass: CoinSubClass.trc20, + parentId: parent, + ); + + await tester.pumpWidget( + MaterialApp( + home: Scaffold(body: Center(child: AssetLogo.ofId(child))), + ), + ); + + final protocolIcon = tester.widget( + find.byType(AssetProtocolIcon), + ); + + expect(protocolIcon.protocolTicker, 'TRX'); + }); + }); +} From 682a0b9501ffbdc9c8a4da8fa588afe6ce856a61 Mon Sep 17 00:00:00 2001 From: Nitride <77973576+CharlVS@users.noreply.github.com> Date: Mon, 30 Mar 2026 22:39:35 +0200 Subject: [PATCH 31/40] fix(ui): avoid duplicate icon precache requests (#345) * fix(ui): avoid duplicate icon precache requests * fix(ui): retry CDN icon loads after transient failures * fix(ui): harden icon precache fallback and manifest caching --- .../lib/src/defi/asset/asset_icon.dart | 181 ++++++++++++++---- 1 file changed, 143 insertions(+), 38 deletions(-) diff --git a/packages/komodo_ui/lib/src/defi/asset/asset_icon.dart b/packages/komodo_ui/lib/src/defi/asset/asset_icon.dart index 48a8d8543..1e3c5df1f 100644 --- a/packages/komodo_ui/lib/src/defi/asset/asset_icon.dart +++ b/packages/komodo_ui/lib/src/defi/asset/asset_icon.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:flutter/services.dart' show AssetManifest, rootBundle; import 'package:komodo_defi_types/komodo_defi_types.dart'; /// A widget that displays an icon for a given [AssetId]. @@ -159,6 +160,10 @@ class _AssetIconResolver extends StatelessWidget { static final Map _assetExistenceCache = {}; static final Map _cdnExistenceCache = {}; static final Map _customIconsCache = {}; + static final Map _lastCdnFailureAt = {}; + static Set? _bundledAssetPaths; + static Future>? _bundledAssetPathsLoader; + static const _cdnRetryInterval = Duration(minutes: 1); static void registerCustomIcon(AssetId assetId, ImageProvider imageProvider) { final sanitizedId = assetId.symbol.configSymbol.toLowerCase(); @@ -169,6 +174,9 @@ class _AssetIconResolver extends StatelessWidget { _assetExistenceCache.clear(); _cdnExistenceCache.clear(); _customIconsCache.clear(); + _lastCdnFailureAt.clear(); + _bundledAssetPaths = null; + _bundledAssetPathsLoader = null; } String get _sanitizedId => @@ -176,19 +184,63 @@ class _AssetIconResolver extends StatelessWidget { String get _imagePath => '$_coinImagesFolder$_sanitizedId.png'; String get _cdnUrl => '$_mediaCdnUrl$_sanitizedId.png'; + static Future> _loadBundledAssetPaths() async { + if (_bundledAssetPaths != null) { + return _bundledAssetPaths!; + } + + if (_bundledAssetPathsLoader != null) { + return _bundledAssetPathsLoader!; + } + + _bundledAssetPathsLoader = () async { + final manifest = await AssetManifest.loadFromAssetBundle(rootBundle); + return manifest.listAssets().toSet(); + }(); + + try { + _bundledAssetPaths = await _bundledAssetPathsLoader; + return _bundledAssetPaths!; + } finally { + _bundledAssetPathsLoader = null; + } + } + + static Future _isBundledAssetDeclared(String assetPath) async { + try { + final bundledPaths = await _loadBundledAssetPaths(); + return bundledPaths.contains(assetPath); + } catch (e) { + debugPrint('Failed to load asset manifest for icon precache: $e'); + return null; + } + } + static Future _didImagePrecacheSucceed( ImageProvider image, BuildContext context, ) async { final outcome = _PrecacheOutcome(); - await precacheImage( - image, - context, - onError: outcome.recordFailure, - ); + await precacheImage(image, context, onError: outcome.recordFailure); return outcome.succeeded; } + static Future _precacheCdnImage( + BuildContext context, + NetworkImage cdnImage, + String sanitizedId, + ) async { + if (!context.mounted) return false; + final cdnSucceeded = await _didImagePrecacheSucceed(cdnImage, context); + _cdnExistenceCache[sanitizedId] = cdnSucceeded; + if (cdnSucceeded) { + _lastCdnFailureAt.remove(sanitizedId); + } else { + _lastCdnFailureAt[sanitizedId] = DateTime.now(); + } + return cdnSucceeded; + } + static Future precacheAssetIcon( BuildContext context, AssetId asset, { @@ -199,42 +251,61 @@ class _AssetIconResolver extends StatelessWidget { try { if (_customIconsCache.containsKey(sanitizedId)) { - if (context.mounted) { - await precacheImage( - _customIconsCache[sanitizedId]!, - context, - onError: (e, stackTrace) { - if (throwExceptions) { - throw Exception( - 'Failed to pre-cache custom image for coin $asset: $e', - ); - } - }, - ); + if (!context.mounted) return; + + final customSucceeded = await _didImagePrecacheSucceed( + _customIconsCache[sanitizedId]!, + context, + ); + if (throwExceptions && !customSucceeded) { + throw Exception('Failed to pre-cache custom image for coin $asset.'); } return; } final assetImage = AssetImage(resolver._imagePath); final cdnImage = NetworkImage(resolver._cdnUrl); + final bundledAssetExists = await _isBundledAssetDeclared( + resolver._imagePath, + ); - final assetSucceeded = await _didImagePrecacheSucceed(assetImage, context); - _assetExistenceCache[resolver._imagePath] = assetSucceeded; + if (bundledAssetExists == true || bundledAssetExists == null) { + if (!context.mounted) return; + final assetSucceeded = await _didImagePrecacheSucceed( + assetImage, + context, + ); + _assetExistenceCache[resolver._imagePath] = assetSucceeded; + if (assetSucceeded) { + _cdnExistenceCache.remove(sanitizedId); + _lastCdnFailureAt.remove(sanitizedId); + return; + } - if (assetSucceeded) { + _assetExistenceCache[resolver._imagePath] = false; + if (!context.mounted) return; + final cdnSucceeded = await _precacheCdnImage( + context, + cdnImage, + sanitizedId, + ); + if (throwExceptions && !cdnSucceeded) { + throw Exception( + 'Failed to pre-cache bundled and CDN images for asset ${asset.id}', + ); + } return; } - final cdnSucceeded = - context.mounted && await _didImagePrecacheSucceed(cdnImage, context); - if (context.mounted) { - _cdnExistenceCache[sanitizedId] = cdnSucceeded; - } - + _assetExistenceCache[resolver._imagePath] = false; + if (!context.mounted) return; + final cdnSucceeded = await _precacheCdnImage( + context, + cdnImage, + sanitizedId, + ); if (throwExceptions && !cdnSucceeded) { - throw Exception( - 'Failed to pre-cache bundled and CDN images for asset ${asset.id}', - ); + throw Exception('Failed to pre-cache CDN image for asset ${asset.id}'); } } catch (e) { debugPrint('Error in precacheAssetIcon for ${asset.id}: $e'); @@ -247,6 +318,28 @@ class _AssetIconResolver extends StatelessWidget { return _assetExistenceCache[resolver._imagePath] ?? false; } + Widget _buildFallbackIcon() { + return Icon(Icons.monetization_on_outlined, size: size); + } + + Widget _buildCdnImage() { + return Image.network( + _cdnUrl, + filterQuality: FilterQuality.high, + errorBuilder: (context, error, stackTrace) { + _cdnExistenceCache[_sanitizedId] = false; + _lastCdnFailureAt[_sanitizedId] = DateTime.now(); + return _buildFallbackIcon(); + }, + ); + } + + bool _shouldRetryCdnNow() { + final lastFailure = _lastCdnFailureAt[_sanitizedId]; + if (lastFailure == null) return true; + return DateTime.now().difference(lastFailure) >= _cdnRetryInterval; + } + @override Widget build(BuildContext context) { if (_customIconsCache.containsKey(_sanitizedId)) { @@ -260,22 +353,34 @@ class _AssetIconResolver extends StatelessWidget { ); } - _assetExistenceCache[_imagePath] = true; + final bundledState = _assetExistenceCache[_imagePath]; + final cdnState = _cdnExistenceCache[_sanitizedId]; + + if (bundledState == false && cdnState == true) { + return _buildCdnImage(); + } + + if (bundledState == false && cdnState == false) { + if (_shouldRetryCdnNow()) { + _cdnExistenceCache[_sanitizedId] = true; + return _buildCdnImage(); + } + return _buildFallbackIcon(); + } + + _assetExistenceCache[_imagePath] = bundledState ?? true; return Image.asset( _imagePath, filterQuality: FilterQuality.high, errorBuilder: (context, error, stackTrace) { _assetExistenceCache[_imagePath] = false; + if (_cdnExistenceCache[_sanitizedId] == false) { + return _buildFallbackIcon(); + } + _cdnExistenceCache[_sanitizedId] ??= true; - return Image.network( - _cdnUrl, - filterQuality: FilterQuality.high, - errorBuilder: (context, error, stackTrace) { - _cdnExistenceCache[_sanitizedId] = false; - return Icon(Icons.monetization_on_outlined, size: size); - }, - ); + return _buildCdnImage(); }, ); } From ed300b119e5e6a89727bc5180de81a30d88d37cc Mon Sep 17 00:00:00 2001 From: Nitride <77973576+CharlVS@users.noreply.github.com> Date: Mon, 30 Mar 2026 22:42:17 +0200 Subject: [PATCH 32/40] fix(market-data): add CoinGecko failure cooldown and harden icon precache (#346) * fix(ui): avoid duplicate icon precache requests * fix(ui): retry CDN icon loads after transient failures * fix(ui): harden icon precache fallback and manifest caching * fix(market-data): add failure cooldown to CoinGecko coin list fetching When getCoinList() failed, _cachedCoinList stayed null and every subsequent price request re-triggered the API call via supports(), creating a request storm that exhausted rate limits. Add a 5-minute cooldown after failures to prevent retry spam, matching the existing RepositoryFallbackMixin backoff duration. Co-Authored-By: Claude Opus 4.6 --------- Co-authored-by: Claude Opus 4.6 --- .../coingecko/data/coingecko_repository.dart | 27 +++++ .../coingecko/coingecko_repository_test.dart | 103 ++++++++++++++++++ 2 files changed, 130 insertions(+) diff --git a/packages/komodo_cex_market_data/lib/src/coingecko/data/coingecko_repository.dart b/packages/komodo_cex_market_data/lib/src/coingecko/data/coingecko_repository.dart index 31f502b2c..371a5d208 100644 --- a/packages/komodo_cex_market_data/lib/src/coingecko/data/coingecko_repository.dart +++ b/packages/komodo_cex_market_data/lib/src/coingecko/data/coingecko_repository.dart @@ -33,6 +33,12 @@ class CoinGeckoRepository implements CexRepository { Future>? _coinListInFlight; Set? _cachedFiatCurrencies; + /// Tracks when the last coin list fetch failed, to prevent API request spam. + /// When a fetch fails (e.g. due to rate limiting), we avoid retrying until + /// the cooldown period has elapsed. + DateTime? _lastCoinListFailure; + static const _coinListFailureCooldown = Duration(minutes: 5); + /// Fetches the CoinGecko market data. /// /// Returns a list of [CoinMarketData] objects containing the market data. @@ -57,14 +63,35 @@ class CoinGeckoRepository implements CexRepository { if (_cachedCoinList != null) { return _cachedCoinList!; } + + // Prevent API spam: don't retry if we recently failed. + // Without this guard, every price request triggers supports() which calls + // getCoinList(), and each failed call immediately retries the API — causing + // a request storm that exhausts rate limits. + if (_lastCoinListFailure != null) { + final elapsed = DateTime.now().difference(_lastCoinListFailure!); + if (elapsed < _coinListFailureCooldown) { + throw StateError( + 'CoinGecko coin list fetch is in cooldown after a recent failure ' + '(${(_coinListFailureCooldown - elapsed).inSeconds}s remaining)', + ); + } + _lastCoinListFailure = null; + } + if (_coinListInFlight != null) { return _coinListInFlight!; } _coinListInFlight = _fetchCoinListInternal() .then((list) { _cachedCoinList = list; + _lastCoinListFailure = null; return list; }) + .catchError((Object error, StackTrace stackTrace) { + _lastCoinListFailure = DateTime.now(); + Error.throwWithStackTrace(error, stackTrace); + }) .whenComplete(() { _coinListInFlight = null; }); diff --git a/packages/komodo_cex_market_data/test/coingecko/coingecko_repository_test.dart b/packages/komodo_cex_market_data/test/coingecko/coingecko_repository_test.dart index 6d01875dc..534f26b66 100644 --- a/packages/komodo_cex_market_data/test/coingecko/coingecko_repository_test.dart +++ b/packages/komodo_cex_market_data/test/coingecko/coingecko_repository_test.dart @@ -413,6 +413,109 @@ void main() { }); }); + group('getCoinList failure cooldown', () { + test( + 'should not retry API call during cooldown after failure', + () async { + var callCount = 0; + when(() => mockProvider.fetchCoinList()).thenAnswer((_) async { + callCount++; + throw Exception('API error'); + }); + when( + () => mockProvider.fetchSupportedVsCurrencies(), + ).thenAnswer((_) async => ['usd']); + + // Enable memoization for this test + final memoizedRepo = CoinGeckoRepository( + coinGeckoProvider: mockProvider, + ); + + // First call should hit the API and fail + expect(memoizedRepo.getCoinList(), throwsA(isA())); + await Future.delayed(Duration.zero); + expect(callCount, equals(1)); + + // Second call should throw StateError (cooldown) without hitting API + expect(memoizedRepo.getCoinList(), throwsA(isA())); + await Future.delayed(Duration.zero); + expect(callCount, equals(1)); // Still 1 - no new API call + }, + ); + + test( + 'should return false from supports() during cooldown without API call', + () async { + when(() => mockProvider.fetchCoinList()).thenThrow( + Exception('rate limit'), + ); + when( + () => mockProvider.fetchSupportedVsCurrencies(), + ).thenAnswer((_) async => ['usd']); + + final memoizedRepo = CoinGeckoRepository( + coinGeckoProvider: mockProvider, + ); + + final assetId = AssetId( + id: 'bitcoin', + name: 'Bitcoin', + symbol: AssetSymbol(assetConfigId: 'BTC', coinGeckoId: 'bitcoin'), + chainId: AssetChainId(chainId: 0), + derivationPath: null, + subClass: CoinSubClass.utxo, + ); + + // First supports() call triggers the API failure + final result1 = await memoizedRepo.supports( + assetId, + FiatCurrency.usd, + PriceRequestType.currentPrice, + ); + expect(result1, isFalse); + + // Second supports() call should return false without new API call + final result2 = await memoizedRepo.supports( + assetId, + FiatCurrency.usd, + PriceRequestType.currentPrice, + ); + expect(result2, isFalse); + + // Provider should only have been called once + verify(() => mockProvider.fetchCoinList()).called(1); + }, + ); + + test('should cache result on success and not call API again', () async { + when(() => mockProvider.fetchCoinList()).thenAnswer( + (_) async => [ + const CexCoin( + id: 'bitcoin', + symbol: 'btc', + name: 'Bitcoin', + currencies: {}, + ), + ], + ); + when( + () => mockProvider.fetchSupportedVsCurrencies(), + ).thenAnswer((_) async => ['usd']); + + final memoizedRepo = CoinGeckoRepository( + coinGeckoProvider: mockProvider, + ); + + final result1 = await memoizedRepo.getCoinList(); + final result2 = await memoizedRepo.getCoinList(); + + expect(result1.length, equals(1)); + expect(result2.length, equals(1)); + // API should only be called once + verify(() => mockProvider.fetchCoinList()).called(1); + }); + }); + group('_mapFiatCurrencyToCoingecko mapping verification', () { setUp(() { // Mock the coin list response From dbc1472feb7dbe224b07a03d507ac9fb5c2e6287 Mon Sep 17 00:00:00 2001 From: DeckerSU Date: Wed, 1 Apr 2026 18:55:55 +0200 Subject: [PATCH 33/40] chore(build): update KDF to v3.0.0-beta (d56a7bc) and coins to master - Bump api_commit_hash to d56a7bc5028f10b9c1bb8370bb2ee16444796a23 (branch: main) - Update all platform checksums from v3.0.0-beta release - Add libkdf-macos-universal2-* pattern to macOS matching_pattern - Switch coins_repo_branch from feat/add-tron-coins to master --- .../app_build/build_config.json | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/packages/komodo_defi_framework/app_build/build_config.json b/packages/komodo_defi_framework/app_build/build_config.json index 68def6f80..3612b9282 100644 --- a/packages/komodo_defi_framework/app_build/build_config.json +++ b/packages/komodo_defi_framework/app_build/build_config.json @@ -1,7 +1,7 @@ { "api": { - "api_commit_hash": "b3c9e669ac736dc4853064958ecc565a0284e16d", - "branch": "fix/tron-account-activation-fee", + "api_commit_hash": "d56a7bc5028f10b9c1bb8370bb2ee16444796a23", + "branch": "main", "fetch_at_build_enabled": true, "concurrent_downloads_enabled": true, "source_urls": [ @@ -13,50 +13,50 @@ "web": { "matching_pattern": "^(?:kdf_[a-f0-9]{7,40}-wasm|mm2_[a-f0-9]{7,40}-wasm|mm2-[a-f0-9]{7,40}-wasm)\\.zip$", "valid_zip_sha256_checksums": [ - "61be493c87eaaa5d5cc74fd9c2b2d98353c52d8dc1d3ff0ce55c9f3467cf4720" + "7653ef16352d4afec8ac11b2c54efd89618e7034d79c06bcc266948109e66ac9" ], "path": "web/kdf/bin" }, "ios": { "matching_pattern": "^(?:kdf_[a-f0-9]{7,40}-ios-aarch64|mm2_[a-f0-9]{7,40}-ios-aarch64|mm2-[a-f0-9]{7,40}-ios-aarch64-CI)\\.zip$", "valid_zip_sha256_checksums": [ - "cc8e545a1704d86cc20871fc1f14943db61d055fa8e3db466a7e40e253982ac7" + "68719828c7a2616955fd426c949a10bbef224591470d41821739974bf18e9efc" ], "path": "ios" }, "macos": { - "matching_pattern": "^(?:kdf-macos-universal2-[a-f0-9]{7,40}|kdf_[a-f0-9]{7,40}-mac-universal)\\.zip$", + "matching_pattern": "^(?:kdf-macos-universal2-[a-f0-9]{7,40}|kdf_[a-f0-9]{7,40}-mac-universal|libkdf-macos-universal2-[a-f0-9]{7,40})\\.zip$", "matching_preference": ["universal2", "mac-arm64"], "valid_zip_sha256_checksums": [ - "6852d232afce798abd887381f0699e74e2ccfda94faf14dbf0af115b84de0584" + "14a1473d46706fdfbd04d18939994b686016a126e4dc2cb8937d00f5645b8773" ], "path": "macos/bin" }, "windows": { "matching_pattern": "^(?:kdf_[a-f0-9]{7,40}-win-x86-64|mm2_[a-f0-9]{7,40}-win-x86-64|mm2-[a-f0-9]{7,40}-Win64)\\.zip$", "valid_zip_sha256_checksums": [ - "0377cf6c539610cce2495731058f63d3165289a071ab4772a649374f4ea1658f" + "4839a22f8722e5ab9b249c2e0d602659e74c45dfcac7b0345729aaf4738591b2" ], "path": "windows/bin" }, "android-armv7": { "matching_pattern": "^(?:kdf_[a-f0-9]{7,40}-android-armv7|mm2_[a-f0-9]{7,40}-android-armv7|mm2-[a-f0-9]{7,40}-android-armv7-CI)\\.zip$", "valid_zip_sha256_checksums": [ - "1b3c064322b377fd66945c25e405a04246134b7564d56530ab1bd39ed3de30eb" + "731af04ea8b95647433b232951c68b57eeab25c5948f2fbe5ee3896c272fa393" ], "path": "android/app/src/main/cpp/libs/armeabi-v7a" }, "android-aarch64": { "matching_pattern": "^(?:kdf_[a-f0-9]{7,40}-android-aarch64|mm2_[a-f0-9]{7,40}-android-aarch64|mm2-[a-f0-9]{7,40}-android-aarch64-CI)\\.zip$", "valid_zip_sha256_checksums": [ - "4ed7e4a3f1ddc7906d12b6bf2564e6e17a5a8cc772b3fd7f4ebf0e5d16fd58ee" + "1f2303240131ce11dec0afa67a014dd8753c1d3989faa9c701996f09c3e7b595" ], "path": "android/app/src/main/cpp/libs/arm64-v8a" }, "linux": { "matching_pattern": "^(?:kdf_[a-f0-9]{7,40}-linux-x86-64|mm2_[a-f0-9]{7,40}-linux-x86-64|mm2-[a-f0-9]{7,40}-Linux-Release)\\.zip$", "valid_zip_sha256_checksums": [ - "6dbff099e8ca002c6de828554bb6a81f5729ec48caab5ea5544cb1125a531255" + "20016e48158cc3926febb37bb2c51bce713eb46f53b0be6790ab132ceec2d501" ], "path": "linux/bin" } @@ -65,10 +65,10 @@ "coins": { "fetch_at_build_enabled": true, "update_commit_on_build": true, - "bundled_coins_repo_commit": "16496a71d9473bfab1357ab0b71511c7ba106ce4", + "bundled_coins_repo_commit": "1d2a5c9c4d23416df2fa1c5e2f263a244a09704d", "coins_repo_api_url": "https://api.github.com/repos/GLEECBTC/coins", "coins_repo_content_url": "https://raw.githubusercontent.com/GLEECBTC/coins", - "coins_repo_branch": "feat/add-tron-coins", + "coins_repo_branch": "master", "runtime_updates_enabled": true, "mapped_files": { "assets/config/coins_config.json": "utils/coins_config_unfiltered.json", From 0f61dfb024f11061e4714f1f736404cf9a3cff18 Mon Sep 17 00:00:00 2001 From: CharlVS <77973576+CharlVS@users.noreply.github.com> Date: Thu, 2 Apr 2026 14:50:24 -0400 Subject: [PATCH 34/40] feat(migration): scaffold legacy wallet migration package --- .../komodo_legacy_wallet_migration/.gitignore | 44 ++++++++++++ .../komodo_legacy_wallet_migration/LICENSE | 21 ++++++ .../komodo_legacy_wallet_migration/README.md | 67 +++++++++++++++++++ .../analysis_options.yaml | 1 + .../lib/komodo_legacy_wallet_migration.dart | 4 ++ .../src/komodo_legacy_wallet_migration.dart | 7 ++ .../pubspec.yaml | 32 +++++++++ .../komodo_legacy_wallet_migration_test.dart | 13 ++++ pubspec.yaml | 1 + 9 files changed, 190 insertions(+) create mode 100644 packages/komodo_legacy_wallet_migration/.gitignore create mode 100644 packages/komodo_legacy_wallet_migration/LICENSE create mode 100644 packages/komodo_legacy_wallet_migration/README.md create mode 100644 packages/komodo_legacy_wallet_migration/analysis_options.yaml create mode 100644 packages/komodo_legacy_wallet_migration/lib/komodo_legacy_wallet_migration.dart create mode 100644 packages/komodo_legacy_wallet_migration/lib/src/komodo_legacy_wallet_migration.dart create mode 100644 packages/komodo_legacy_wallet_migration/pubspec.yaml create mode 100644 packages/komodo_legacy_wallet_migration/test/src/komodo_legacy_wallet_migration_test.dart diff --git a/packages/komodo_legacy_wallet_migration/.gitignore b/packages/komodo_legacy_wallet_migration/.gitignore new file mode 100644 index 000000000..06ef8e610 --- /dev/null +++ b/packages/komodo_legacy_wallet_migration/.gitignore @@ -0,0 +1,44 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ +migrate_working_dir/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# VSCode related +.vscode/* + +# Flutter/Dart/Pub related +**/doc/api/ +**/ios/Flutter/.last_build_id +.dart_tool/ +.flutter-plugins +.flutter-plugins-dependencies +.packages +.pub-cache/ +.pub/ +/build/ +pubspec.lock + +# Web related +lib/generated_plugin_registrant.dart + +# Symbolication related +app.*.symbols + +# Obfuscation related +app.*.map.json + +# Test related +coverage \ No newline at end of file diff --git a/packages/komodo_legacy_wallet_migration/LICENSE b/packages/komodo_legacy_wallet_migration/LICENSE new file mode 100644 index 000000000..8d45e062a --- /dev/null +++ b/packages/komodo_legacy_wallet_migration/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/packages/komodo_legacy_wallet_migration/README.md b/packages/komodo_legacy_wallet_migration/README.md new file mode 100644 index 000000000..d965dfdce --- /dev/null +++ b/packages/komodo_legacy_wallet_migration/README.md @@ -0,0 +1,67 @@ +# Komodo Legacy Wallet Migration + +[![style: very good analysis][very_good_analysis_badge]][very_good_analysis_link] +[![Powered by Mason](https://img.shields.io/endpoint?url=https%3A%2F%2Ftinyurl.com%2Fmason-badge)](https://github.com/felangel/mason) +[![License: MIT][license_badge]][license_link] + +Legacy wallet migration utilities for Komodo/Gleec SDK apps. + +## Installation 💻 + +**❗ In order to start using Komodo Legacy Wallet Migration you must have the [Flutter SDK][flutter_install_link] installed on your machine.** + +Install via `flutter pub add`: + +```sh +dart pub add komodo_legacy_wallet_migration +``` + +--- + +## Continuous Integration 🤖 + +Komodo Legacy Wallet Migration comes with a built-in [GitHub Actions workflow][github_actions_link] powered by [Very Good Workflows][very_good_workflows_link] but you can also add your preferred CI/CD solution. + +Out of the box, on each pull request and push, the CI `formats`, `lints`, and `tests` the code. This ensures the code remains consistent and behaves correctly as you add functionality or make changes. The project uses [Very Good Analysis][very_good_analysis_link] for a strict set of analysis options used by our team. Code coverage is enforced using the [Very Good Workflows][very_good_coverage_link]. + +--- + +## Running Tests 🧪 + +For first time users, install the [very_good_cli][very_good_cli_link]: + +```sh +dart pub global activate very_good_cli +``` + +To run all unit tests: + +```sh +very_good test --coverage +``` + +To view the generated coverage report you can use [lcov](https://github.com/linux-test-project/lcov). + +```sh +# Generate Coverage Report +genhtml coverage/lcov.info -o coverage/ + +# Open Coverage Report +open coverage/index.html +``` + +[flutter_install_link]: https://docs.flutter.dev/get-started/install +[github_actions_link]: https://docs.github.com/en/actions/learn-github-actions +[license_badge]: https://img.shields.io/badge/license-MIT-blue.svg +[license_link]: https://opensource.org/licenses/MIT +[logo_black]: https://raw.githubusercontent.com/VGVentures/very_good_brand/main/styles/README/vgv_logo_black.png#gh-light-mode-only +[logo_white]: https://raw.githubusercontent.com/VGVentures/very_good_brand/main/styles/README/vgv_logo_white.png#gh-dark-mode-only +[mason_link]: https://github.com/felangel/mason +[very_good_analysis_badge]: https://img.shields.io/badge/style-very_good_analysis-B22C89.svg +[very_good_analysis_link]: https://pub.dev/packages/very_good_analysis +[very_good_cli_link]: https://pub.dev/packages/very_good_cli +[very_good_coverage_link]: https://github.com/marketplace/actions/very-good-coverage +[very_good_ventures_link]: https://verygood.ventures +[very_good_ventures_link_light]: https://verygood.ventures#gh-light-mode-only +[very_good_ventures_link_dark]: https://verygood.ventures#gh-dark-mode-only +[very_good_workflows_link]: https://github.com/VeryGoodOpenSource/very_good_workflows diff --git a/packages/komodo_legacy_wallet_migration/analysis_options.yaml b/packages/komodo_legacy_wallet_migration/analysis_options.yaml new file mode 100644 index 000000000..9df80aa49 --- /dev/null +++ b/packages/komodo_legacy_wallet_migration/analysis_options.yaml @@ -0,0 +1 @@ +include: package:very_good_analysis/analysis_options.yaml diff --git a/packages/komodo_legacy_wallet_migration/lib/komodo_legacy_wallet_migration.dart b/packages/komodo_legacy_wallet_migration/lib/komodo_legacy_wallet_migration.dart new file mode 100644 index 000000000..d42ad2e7a --- /dev/null +++ b/packages/komodo_legacy_wallet_migration/lib/komodo_legacy_wallet_migration.dart @@ -0,0 +1,4 @@ +/// Legacy wallet migration utilities for Komodo/Gleec SDK apps. +library; + +export 'src/komodo_legacy_wallet_migration.dart'; diff --git a/packages/komodo_legacy_wallet_migration/lib/src/komodo_legacy_wallet_migration.dart b/packages/komodo_legacy_wallet_migration/lib/src/komodo_legacy_wallet_migration.dart new file mode 100644 index 000000000..188619509 --- /dev/null +++ b/packages/komodo_legacy_wallet_migration/lib/src/komodo_legacy_wallet_migration.dart @@ -0,0 +1,7 @@ +/// {@template komodo_legacy_wallet_migration} +/// Legacy wallet migration utilities for Komodo/Gleec SDK apps. +/// {@endtemplate} +class KomodoLegacyWalletMigration { + /// {@macro komodo_legacy_wallet_migration} + const KomodoLegacyWalletMigration(); +} diff --git a/packages/komodo_legacy_wallet_migration/pubspec.yaml b/packages/komodo_legacy_wallet_migration/pubspec.yaml new file mode 100644 index 000000000..01d67f627 --- /dev/null +++ b/packages/komodo_legacy_wallet_migration/pubspec.yaml @@ -0,0 +1,32 @@ +name: komodo_legacy_wallet_migration +description: Legacy wallet migration utilities for Komodo/Gleec SDK apps. +version: 0.1.0 +publish_to: none +repository: https://github.com/GLEECBTC/komodo-defi-sdk-flutter + +environment: + sdk: ">=3.9.0 <4.0.0" + flutter: ">=3.35.0" + +resolution: workspace + +dependencies: + crypto: ^3.0.6 + dargon2_flutter: ^3.1.0 + encrypt: ^5.0.3 + flutter: + sdk: flutter + flutter_secure_storage: ^10.0.0-beta.4 + komodo_defi_types: ^0.4.0 + logging: ^1.3.0 + path: ^1.9.1 + path_provider: ^2.1.5 + shared_preferences: ^2.3.2 + sqflite: ^2.4.2 + +dev_dependencies: + flutter_test: + sdk: flutter + mocktail: ^1.0.4 + test: ^1.25.7 + very_good_analysis: ^9.0.0 diff --git a/packages/komodo_legacy_wallet_migration/test/src/komodo_legacy_wallet_migration_test.dart b/packages/komodo_legacy_wallet_migration/test/src/komodo_legacy_wallet_migration_test.dart new file mode 100644 index 000000000..cefa20cb8 --- /dev/null +++ b/packages/komodo_legacy_wallet_migration/test/src/komodo_legacy_wallet_migration_test.dart @@ -0,0 +1,13 @@ +// Not required for test files +// ignore_for_file: prefer_const_constructors + +import 'package:flutter_test/flutter_test.dart'; +import 'package:komodo_legacy_wallet_migration/komodo_legacy_wallet_migration.dart'; + +void main() { + group('KomodoLegacyWalletMigration', () { + test('can be instantiated', () { + expect(KomodoLegacyWalletMigration(), isNotNull); + }); + }); +} diff --git a/pubspec.yaml b/pubspec.yaml index bdf6c4000..db94032b8 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -22,6 +22,7 @@ workspace: - packages/dragon_charts_flutter - packages/dragon_charts_flutter/example - packages/komodo_wallet_cli + - packages/komodo_legacy_wallet_migration # TODO: Change products and playground to reference pub.dev hosted packages and remove from here. - products/dex_dungeon From ef0008c94751e006ef505de708c5f76d32e9a3cd Mon Sep 17 00:00:00 2001 From: CharlVS <77973576+CharlVS@users.noreply.github.com> Date: Mon, 13 Apr 2026 10:46:05 +0200 Subject: [PATCH 35/40] feat(sdk): add legacy wallet migration - persist recurring ZHTLC sync policy while keeping one-shot sync overrides transient during activation - harden KDF start/stop readiness checks and auth retries to recover from stale RPC sockets during wallet flows --- .../app_build/build_config.json | 2 +- .../lib/komodo_defi_framework.dart | 101 +++- .../lib/src/auth/auth_service.dart | 122 ++-- .../src/auth/auth_service_auth_extension.dart | 50 ++ .../src/auth/auth_service_kdf_extension.dart | 146 ++++- .../activation_params/activation_params.dart | 7 +- .../komodo_defi_sdk/lib/komodo_defi_sdk.dart | 2 + .../zhtlc_activation_strategy.dart | 31 +- .../activation_config_service.dart | 208 ++++++- .../lib/src/errors/sdk_error_mapper.dart | 1 + .../zhtlc_activation_strategy_test.dart | 155 +++++ .../activation_config_service_test.dart | 75 +++ .../src/auth/exceptions/auth_exception.dart | 87 +-- .../lib/komodo_legacy_wallet_migration.dart | 8 +- .../adapters/legacy_password_verifier.dart | 33 ++ .../src/adapters/legacy_secure_storage.dart | 42 ++ .../legacy_shared_preferences_store.dart | 29 + .../legacy_wallet_metadata_store.dart | 188 +++++++ .../src/adapters/legacy_wallet_platform.dart | 23 + .../src/komodo_legacy_wallet_migration.dart | 360 +++++++++++- .../models/legacy_wallet_cleanup_result.dart | 76 +++ .../legacy_wallet_migration_exception.dart | 68 +++ .../lib/src/models/legacy_wallet_record.dart | 65 +++ .../lib/src/models/legacy_wallet_secrets.dart | 103 ++++ .../komodo_legacy_wallet_migration_test.dart | 530 +++++++++++++++++- 25 files changed, 2347 insertions(+), 165 deletions(-) create mode 100644 packages/komodo_defi_sdk/test/activation/zhtlc_activation_strategy_test.dart create mode 100644 packages/komodo_defi_sdk/test/src/activation_config/activation_config_service_test.dart create mode 100644 packages/komodo_legacy_wallet_migration/lib/src/adapters/legacy_password_verifier.dart create mode 100644 packages/komodo_legacy_wallet_migration/lib/src/adapters/legacy_secure_storage.dart create mode 100644 packages/komodo_legacy_wallet_migration/lib/src/adapters/legacy_shared_preferences_store.dart create mode 100644 packages/komodo_legacy_wallet_migration/lib/src/adapters/legacy_wallet_metadata_store.dart create mode 100644 packages/komodo_legacy_wallet_migration/lib/src/adapters/legacy_wallet_platform.dart create mode 100644 packages/komodo_legacy_wallet_migration/lib/src/models/legacy_wallet_cleanup_result.dart create mode 100644 packages/komodo_legacy_wallet_migration/lib/src/models/legacy_wallet_migration_exception.dart create mode 100644 packages/komodo_legacy_wallet_migration/lib/src/models/legacy_wallet_record.dart create mode 100644 packages/komodo_legacy_wallet_migration/lib/src/models/legacy_wallet_secrets.dart diff --git a/packages/komodo_defi_framework/app_build/build_config.json b/packages/komodo_defi_framework/app_build/build_config.json index 3612b9282..ebd156fc8 100644 --- a/packages/komodo_defi_framework/app_build/build_config.json +++ b/packages/komodo_defi_framework/app_build/build_config.json @@ -65,7 +65,7 @@ "coins": { "fetch_at_build_enabled": true, "update_commit_on_build": true, - "bundled_coins_repo_commit": "1d2a5c9c4d23416df2fa1c5e2f263a244a09704d", + "bundled_coins_repo_commit": "46587568ac5ed542544dc9bd68bab35d7d818cf2", "coins_repo_api_url": "https://api.github.com/repos/GLEECBTC/coins", "coins_repo_content_url": "https://raw.githubusercontent.com/GLEECBTC/coins", "coins_repo_branch": "master", diff --git a/packages/komodo_defi_framework/lib/komodo_defi_framework.dart b/packages/komodo_defi_framework/lib/komodo_defi_framework.dart index 0946cae84..d75b3cf23 100644 --- a/packages/komodo_defi_framework/lib/komodo_defi_framework.dart +++ b/packages/komodo_defi_framework/lib/komodo_defi_framework.dart @@ -23,6 +23,10 @@ export 'package:komodo_defi_framework/src/streaming/events/kdf_event.dart'; export 'src/operations/kdf_operations_interface.dart'; class KomodoDefiFramework implements ApiClient { + static const Duration _versionProbeTimeout = Duration(seconds: 2); + static const Duration _stopPollInterval = Duration(milliseconds: 250); + static const Duration _stopSettleDelay = Duration(milliseconds: 250); + factory KomodoDefiFramework.create({ required IKdfHostConfig hostConfig, void Function(String)? externalLogger, @@ -162,24 +166,44 @@ class KomodoDefiFramework implements ApiClient { _log('Stopping KDF...'); final result = await _kdfOperations.kdfStop(); _log('KDF stop result: $result'); - // Await a max of 5 seconds for KDF to stop. Check every 500ms. - for (var i = 0; i < 10; i++) { - await Future.delayed(const Duration(milliseconds: 500)); - if (!await isRunning()) { - break; - } - if (i == 9) { - throw Exception('Error stopping KDF: KDF did not stop in time.'); + + // Drop any stale keep-alive socket before verifying shutdown. Otherwise, + // the post-stop version() fallback can hang on Android while the native + // thread is already tearing down. + resetHttpClient(); + + // Wait for native status to settle without probing RPC over HTTP. + for (var i = 0; i < 20; i++) { + await Future.delayed(_stopPollInterval); + final stillRunning = await isRunning(allowVersionFallback: false); + if (!stillRunning) { + await Future.delayed(_stopSettleDelay); + if (!await isRunning(allowVersionFallback: false)) { + return result; + } } } - return result; + throw Exception('Error stopping KDF: KDF did not stop in time.'); } - Future isRunning() async { + Future isRunning({bool allowVersionFallback = true}) async { + final nativeRunning = await _kdfOperations.isRunning(); + if (nativeRunning) { + return true; + } + + if (!allowVersionFallback) { + _log('KDF is not running.'); + return false; + } + final running = - await _kdfOperations.isRunning() || - await _kdfOperations.version() != null; + await _kdfOperations.version().timeout( + _versionProbeTimeout, + onTimeout: () => null, + ) != + null; if (!running) { _log('KDF is not running.'); } @@ -188,15 +212,23 @@ class KomodoDefiFramework implements ApiClient { Future version() async { final stopwatch = Stopwatch()..start(); - _log('version(): Starting version RPC call via ${_kdfOperations.operationsName}'); + _log( + 'version(): Starting version RPC call via ${_kdfOperations.operationsName}', + ); try { - final version = await _kdfOperations.version(); + final version = await _kdfOperations.version().timeout( + _versionProbeTimeout, + ); stopwatch.stop(); - _log('version(): Completed in ${stopwatch.elapsedMilliseconds}ms, result=$version'); + _log( + 'version(): Completed in ${stopwatch.elapsedMilliseconds}ms, result=$version', + ); return version; } catch (e) { stopwatch.stop(); - _log('version(): Failed after ${stopwatch.elapsedMilliseconds}ms with error: $e'); + _log( + 'version(): Failed after ${stopwatch.elapsedMilliseconds}ms with error: $e', + ); rethrow; } } @@ -205,7 +237,7 @@ class KomodoDefiFramework implements ApiClient { /// Returns true if KDF is running and responsive, false otherwise. /// This is useful for detecting when KDF has become unavailable, especially /// on mobile platforms after app backgrounding. - /// + /// /// IMPORTANT: This method ONLY relies on actual RPC verification (version() call) /// to avoid false positives where native status reports "running" but HTTP listener /// is not accepting connections (common after iOS backgrounding). @@ -217,7 +249,7 @@ class KomodoDefiFramework implements ApiClient { _log('KDF health check failed: version call returned null'); return false; } - + _log('KDF health check passed: version=$versionCheck'); return true; } catch (e) { @@ -279,7 +311,7 @@ class KomodoDefiFramework implements ApiClient { return response; } catch (e) { stopwatch.stop(); - + // Detect transport-fatal SocketExceptions that indicate KDF is down/dying // errno 32 (EPIPE): Broken pipe - writing to socket whose peer closed // errno 54 (ECONNRESET): Connection reset by peer @@ -287,18 +319,29 @@ class KomodoDefiFramework implements ApiClient { // errno 61 (ECONNREFUSED): Connection refused - no listener on port final errorString = e.toString().toLowerCase(); final isSocketException = errorString.contains('socketexception'); - final isFatalTransportError = isSocketException && ( - errorString.contains('broken pipe') || errorString.contains('errno = 32') || - errorString.contains('connection reset') || errorString.contains('errno = 54') || - errorString.contains('operation timed out') || errorString.contains('errno = 60') || - errorString.contains('connection refused') || errorString.contains('errno = 61') - ); + final isFatalTransportError = + isSocketException && + (errorString.contains('broken pipe') || + errorString.contains('errno = 32') || + errorString.contains('connection reset') || + errorString.contains('errno = 54') || + errorString.contains('operation timed out') || + errorString.contains('errno = 60') || + errorString.contains('connection refused') || + errorString.contains('errno = 61')); if (isFatalTransportError) { - final errorType = errorString.contains('errno = 32') || errorString.contains('broken pipe') ? 'EPIPE (32)' : - errorString.contains('errno = 54') || errorString.contains('connection reset') ? 'ECONNRESET (54)' : - errorString.contains('errno = 60') || errorString.contains('operation timed out') ? 'ETIMEDOUT (60)' : - 'ECONNREFUSED (61)'; + final errorType = + errorString.contains('errno = 32') || + errorString.contains('broken pipe') + ? 'EPIPE (32)' + : errorString.contains('errno = 54') || + errorString.contains('connection reset') + ? 'ECONNRESET (54)' + : errorString.contains('errno = 60') || + errorString.contains('operation timed out') + ? 'ETIMEDOUT (60)' + : 'ECONNREFUSED (61)'; _logger.severe( '[RPC] ${method ?? 'unknown'} failed: KDF transport error $errorType. ' 'Resetting HTTP client to drop stale connections.', diff --git a/packages/komodo_defi_local_auth/lib/src/auth/auth_service.dart b/packages/komodo_defi_local_auth/lib/src/auth/auth_service.dart index bef499912..5595fd179 100644 --- a/packages/komodo_defi_local_auth/lib/src/auth/auth_service.dart +++ b/packages/komodo_defi_local_auth/lib/src/auth/auth_service.dart @@ -151,6 +151,10 @@ class KdfAuthService implements IAuthService { List? _usersCache; DateTime? _usersCacheTimestamp; final Duration _usersCacheTtl = const Duration(minutes: 5); + static const Duration _kdfRpcReadyTimeout = Duration(seconds: 15); + static const Duration _kdfRpcProbeTimeout = Duration(seconds: 2); + static const Duration _kdfRpcPollInterval = Duration(milliseconds: 250); + static const Duration _startupSensitiveRpcTimeout = Duration(seconds: 10); ApiClient get _client => _kdfFramework.client; late final methods = KomodoDefiRpcMethods(_client); @@ -254,42 +258,85 @@ class KdfAuthService implements IAuthService { ), Mnemonic? mnemonic, }) async { - await _ensureKdfRunning(); + _logger.info( + '[$_sessionId] register: Starting registration for wallet: $walletName', + ); + final registerStopwatch = Stopwatch()..start(); - await _runReadOperation(() async { - final walletExists = await _walletExists(walletName); - if (walletExists) { - throw AuthException( - 'Wallet already exists', - type: AuthExceptionType.generalAuthError, + try { + final ensureStartStopwatch = Stopwatch()..start(); + await _ensureKdfRunning(); + ensureStartStopwatch.stop(); + _logger.info( + '[$_sessionId] register: ensure no-auth start completed in ' + '${ensureStartStopwatch.elapsedMilliseconds}ms', + ); + + final walletExistsStopwatch = Stopwatch()..start(); + await _runReadOperation(() async { + final walletExists = await _walletExists(walletName); + if (walletExists) { + throw AuthException( + 'Wallet already exists', + type: AuthExceptionType.generalAuthError, + ); + } + }); + walletExistsStopwatch.stop(); + _logger.info( + '[$_sessionId] register: wallet existence read completed in ' + '${walletExistsStopwatch.elapsedMilliseconds}ms', + ); + + // replaces the __assertWalletOrStop method - wait for read/write locks to + // be released here. + // can be used outside of a lock, since both functions are public-facing + // and manage their own read/write locks + final stopStopwatch = Stopwatch()..start(); + if (await isSignedIn()) { + await signOut(); + stopStopwatch.stop(); + _logger.info( + '[$_sessionId] register: stop phase completed in ' + '${stopStopwatch.elapsedMilliseconds}ms', + ); + } else { + stopStopwatch.stop(); + _logger.info( + '[$_sessionId] register: no active session to stop ' + '(${stopStopwatch.elapsedMilliseconds}ms)', ); } - }); - // replaces the __assertWalletOrStop method - wait for read/write locks to - // be released here. - // can be used outside of a lock, since both functions are public-facing - // and manage their own read/write locks - if (await isSignedIn()) { - await signOut(); - } - - final config = await _generateStartupConfig( - walletName: walletName, - walletPassword: password, - allowRegistrations: true, - plaintextMnemonic: mnemonic?.plaintextMnemonic, - hdEnabled: options.derivationMethod == DerivationMethod.hdWallet, - allowWeakPassword: options.allowWeakPassword, - ); + final config = await _generateStartupConfig( + walletName: walletName, + walletPassword: password, + allowRegistrations: true, + plaintextMnemonic: mnemonic?.plaintextMnemonic, + hdEnabled: options.derivationMethod == DerivationMethod.hdWallet, + allowWeakPassword: options.allowWeakPassword, + ); - return _lockWriteOperation(() async { - final isImported = mnemonic != null; - final currentUser = await _registerNewUser(config, options, isImported); - _emitAuthStateChange(currentUser); - _invalidateUsersCache(); - return currentUser; - }); + return _lockWriteOperation(() async { + final writePathStopwatch = Stopwatch()..start(); + final isImported = mnemonic != null; + final currentUser = await _registerNewUser(config, options, isImported); + writePathStopwatch.stop(); + _logger.info( + '[$_sessionId] register: registration write path completed in ' + '${writePathStopwatch.elapsedMilliseconds}ms', + ); + _emitAuthStateChange(currentUser); + _invalidateUsersCache(); + return currentUser; + }); + } finally { + registerStopwatch.stop(); + _logger.info( + '[$_sessionId] register: Finished in ' + '${registerStopwatch.elapsedMilliseconds}ms', + ); + } } @override @@ -304,7 +351,10 @@ class KdfAuthService implements IAuthService { return _usersCache!; } - final walletNames = await _client.rpc.wallet.getWalletNames(); + final walletNames = await _runStartupSensitiveRpc( + phase: 'get_wallet_names', + operation: () => _client.rpc.wallet.getWalletNames(), + ); final users = await Future.wait( walletNames.walletNames.map((name) async { @@ -1006,12 +1056,14 @@ class KdfAuthService implements IAuthService { throw KdfExtensions._mapStartupErrorToAuthException(result); } - _logger.info('[$_sessionId] _forceStartKdf: Waiting for RPC to be up'); + _kdfFramework.resetHttpClient(); + _logger.info('[$_sessionId] _forceStartKdf: Waiting for RPC to be ready'); final waitStopwatch = Stopwatch()..start(); - await _waitUntilKdfRpcIsUp(); + await _waitUntilKdfRpcReady(); waitStopwatch.stop(); _logger.info( - '[$_sessionId] _forceStartKdf: RPC is up after ${waitStopwatch.elapsedMilliseconds}ms', + '[$_sessionId] _forceStartKdf: RPC ready after ' + '${waitStopwatch.elapsedMilliseconds}ms', ); }); } diff --git a/packages/komodo_defi_local_auth/lib/src/auth/auth_service_auth_extension.dart b/packages/komodo_defi_local_auth/lib/src/auth/auth_service_auth_extension.dart index 91dbd0554..6a169df59 100644 --- a/packages/komodo_defi_local_auth/lib/src/auth/auth_service_auth_extension.dart +++ b/packages/komodo_defi_local_auth/lib/src/auth/auth_service_auth_extension.dart @@ -2,7 +2,17 @@ part of 'auth_service.dart'; extension KdfAuthServiceAuthExtension on KdfAuthService { Future _authenticateUser(KdfStartupConfig config) async { + _logger.info( + '[$_sessionId] _authenticateUser: Restarting KDF for ' + '${config.walletName}', + ); + final restartStopwatch = Stopwatch()..start(); await _restartKdf(config); + restartStopwatch.stop(); + _logger.info( + '[$_sessionId] _authenticateUser: auth start + readiness verify ' + 'completed in ${restartStopwatch.elapsedMilliseconds}ms', + ); final status = await _kdfFramework.kdfMainStatus(); if (status != MainStatus.rpcIsUp) { throw AuthException( @@ -13,7 +23,13 @@ extension KdfAuthServiceAuthExtension on KdfAuthService { // use the internal function here, which isn't read-protected, to avoid // deadlocks if used within a write-lock + final activeUserStopwatch = Stopwatch()..start(); var currentUser = await _getActiveUser(); + activeUserStopwatch.stop(); + _logger.info( + '[$_sessionId] _authenticateUser: first authenticated RPC completed in ' + '${activeUserStopwatch.elapsedMilliseconds}ms', + ); if (currentUser == null) { throw AuthException( 'No user signed in', @@ -46,7 +62,17 @@ extension KdfAuthServiceAuthExtension on KdfAuthService { AuthOptions authOptions, bool isImported, ) async { + _logger.info( + '[$_sessionId] _registerNewUser: Restarting KDF for ' + '${config.walletName}', + ); + final restartStopwatch = Stopwatch()..start(); await _restartKdf(config); + restartStopwatch.stop(); + _logger.info( + '[$_sessionId] _registerNewUser: auth start + readiness verify ' + 'completed in ${restartStopwatch.elapsedMilliseconds}ms', + ); final status = await _kdfFramework.kdfMainStatus(); if (status != MainStatus.rpcIsUp) { throw AuthException( @@ -56,13 +82,25 @@ extension KdfAuthServiceAuthExtension on KdfAuthService { } final walletId = WalletId.fromName(config.walletName!, authOptions); + final seedValidationStopwatch = Stopwatch()..start(); final isBip39Seed = await _isSeedBip39Compatible(config); + seedValidationStopwatch.stop(); + _logger.info( + '[$_sessionId] _registerNewUser: seed validation pipeline completed ' + 'in ${seedValidationStopwatch.elapsedMilliseconds}ms', + ); final currentUser = KdfUser( walletId: walletId, isBip39Seed: isBip39Seed, metadata: {'isImported': isImported}, ); + final secureStorageStopwatch = Stopwatch()..start(); await _secureStorage.saveUser(currentUser); + secureStorageStopwatch.stop(); + _logger.info( + '[$_sessionId] _registerNewUser: secure-storage save completed in ' + '${secureStorageStopwatch.elapsedMilliseconds}ms', + ); // Do not allow authentication to proceed for HD wallets if the seed is not // BIP39 compatible. @@ -79,10 +117,16 @@ extension KdfAuthServiceAuthExtension on KdfAuthService { /// Checks if the seed is a valid BIP39 seed phrase. /// Throws [AuthException] if the seed could not be obtained from KDF. Future _isSeedBip39Compatible(KdfStartupConfig config) async { + final mnemonicStopwatch = Stopwatch()..start(); final plaintext = await _getMnemonic( encrypted: false, walletPassword: config.walletPassword, ); + mnemonicStopwatch.stop(); + _logger.info( + '[$_sessionId] _registerNewUser: first authenticated RPC ' + '(get_mnemonic) completed in ${mnemonicStopwatch.elapsedMilliseconds}ms', + ); if (plaintext.plaintextMnemonic == null) { throw AuthException( @@ -91,9 +135,15 @@ extension KdfAuthServiceAuthExtension on KdfAuthService { ); } + final validationStopwatch = Stopwatch()..start(); final validator = MnemonicValidator(); await validator.init(); final isBip39 = validator.validateBip39(plaintext.plaintextMnemonic!); + validationStopwatch.stop(); + _logger.info( + '[$_sessionId] _registerNewUser: seed validation completed in ' + '${validationStopwatch.elapsedMilliseconds}ms', + ); return isBip39; } diff --git a/packages/komodo_defi_local_auth/lib/src/auth/auth_service_kdf_extension.dart b/packages/komodo_defi_local_auth/lib/src/auth/auth_service_kdf_extension.dart index a26d42229..26eb411cd 100644 --- a/packages/komodo_defi_local_auth/lib/src/auth/auth_service_kdf_extension.dart +++ b/packages/komodo_defi_local_auth/lib/src/auth/auth_service_kdf_extension.dart @@ -13,8 +13,10 @@ extension KdfExtensions on KdfAuthService { return null; } - final activeWallet = - (await _client.rpc.wallet.getWalletNames()).activatedWallet; + final activeWallet = (await _runStartupSensitiveRpc( + phase: 'active wallet read', + operation: () => _client.rpc.wallet.getWalletNames(), + )).activatedWallet; if (activeWallet == null) { return null; } @@ -39,14 +41,19 @@ extension KdfExtensions on KdfAuthService { ); } - final response = await _kdfFramework.client.executeRpc({ - 'mmrpc': '2.0', - 'method': 'get_mnemonic', - 'params': { - 'format': encrypted ? 'encrypted' : 'plaintext', - if (!encrypted) 'password': walletPassword, + final response = await _runStartupSensitiveRpc( + phase: 'get_mnemonic', + operation: () async { + return _kdfFramework.client.executeRpc({ + 'mmrpc': '2.0', + 'method': 'get_mnemonic', + 'params': { + 'format': encrypted ? 'encrypted' : 'plaintext', + if (!encrypted) 'password': walletPassword, + }, + }); }, - }); + ); if (response is JsonRpcErrorResponse) { throw AuthException( @@ -60,6 +67,7 @@ extension KdfExtensions on KdfAuthService { Future _stopKdf() async { await _kdfFramework.kdfStop(); + _kdfFramework.resetHttpClient(); _authStateController.add(null); } @@ -68,22 +76,54 @@ extension KdfExtensions on KdfAuthService { Future _ensureKdfRunning() async { if (!await _kdfFramework.isRunning()) { await _lockWriteOperation(() async { - await _kdfFramework.startKdf(await _noAuthConfig); - await _waitUntilKdfRpcIsUp(); + final startStopwatch = Stopwatch()..start(); + final kdfResult = await _kdfFramework.startKdf(await _noAuthConfig); + startStopwatch.stop(); + _logger.info( + '[$_sessionId] _ensureKdfRunning: startKdf(no-auth) returned ' + '${kdfResult.name} in ${startStopwatch.elapsedMilliseconds}ms', + ); + + if (!kdfResult.isStartingOrAlreadyRunning()) { + throw _mapStartupErrorToAuthException(kdfResult); + } + + _kdfFramework.resetHttpClient(); + await _waitUntilKdfRpcReady(); }); } } // consider moving to kdf api Future _restartKdf(KdfStartupConfig config) async { + final stopStopwatch = Stopwatch()..start(); await _stopKdf(); + stopStopwatch.stop(); + _logger.info( + '[$_sessionId] _restartKdf: stop phase completed in ' + '${stopStopwatch.elapsedMilliseconds}ms', + ); + + final startStopwatch = Stopwatch()..start(); final kdfResult = await _kdfFramework.startKdf(config); + startStopwatch.stop(); + _logger.info( + '[$_sessionId] _restartKdf: auth start returned ${kdfResult.name} in ' + '${startStopwatch.elapsedMilliseconds}ms', + ); if (!kdfResult.isStartingOrAlreadyRunning()) { throw _mapStartupErrorToAuthException(kdfResult); } - await _waitUntilKdfRpcIsUp(); + _kdfFramework.resetHttpClient(); + final readyStopwatch = Stopwatch()..start(); + await _waitUntilKdfRpcReady(); + readyStopwatch.stop(); + _logger.info( + '[$_sessionId] _restartKdf: readiness verify completed in ' + '${readyStopwatch.elapsedMilliseconds}ms', + ); } static AuthException _mapStartupErrorToAuthException( @@ -140,28 +180,90 @@ extension KdfExtensions on KdfAuthService { } } - Future _waitUntilKdfRpcIsUp({ - Duration timeout = const Duration(seconds: 5), - bool throwOnTimeout = false, + Future _waitUntilKdfRpcReady({ + Duration timeout = KdfAuthService._kdfRpcReadyTimeout, }) async { final stopwatch = Stopwatch()..start(); while (stopwatch.elapsed < timeout) { - final status = await _kdfFramework.kdfMainStatus(); + final status = await _kdfFramework.kdfMainStatus().timeout( + KdfAuthService._kdfRpcProbeTimeout, + onTimeout: () => MainStatus.notRunning, + ); if (status == MainStatus.rpcIsUp) { - return; + final version = await _kdfFramework.version().timeout( + KdfAuthService._kdfRpcProbeTimeout, + onTimeout: () => null, + ); + if (version != null) { + _logger.info( + '[$_sessionId] _waitUntilKdfRpcReady: RPC ready in ' + '${stopwatch.elapsedMilliseconds}ms', + ); + return; + } } - await Future.delayed(const Duration(milliseconds: 100)); + + await Future.delayed(KdfAuthService._kdfRpcPollInterval); } - if (throwOnTimeout) { - throw AuthException( - 'Timeout waiting for KDF RPC to start', - type: AuthExceptionType.generalAuthError, + throw AuthException( + 'KDF RPC did not become ready within ${timeout.inSeconds} seconds', + type: AuthExceptionType.apiConnectionError, + ); + } + + Future _runStartupSensitiveRpc({ + required String phase, + required Future Function() operation, + }) async { + Future runAttempt() => + operation().timeout(KdfAuthService._startupSensitiveRpcTimeout); + + try { + return await runAttempt(); + } catch (error, stackTrace) { + if (!_shouldRecoverStartupSensitiveRpc(error)) { + rethrow; + } + + _logger.warning( + '[$_sessionId] _runStartupSensitiveRpc: $phase failed on first ' + 'attempt, resetting HTTP client and retrying', + error, + stackTrace, ); + _kdfFramework.resetHttpClient(); + await _waitUntilKdfRpcReady(); + + try { + return await runAttempt(); + } catch (retryError, retryStackTrace) { + if (!_shouldRecoverStartupSensitiveRpc(retryError)) { + rethrow; + } + + _logger.severe( + '[$_sessionId] _runStartupSensitiveRpc: $phase failed after retry', + retryError, + retryStackTrace, + ); + throw AuthException( + 'KDF RPC unavailable during $phase', + type: AuthExceptionType.apiConnectionError, + details: {'phase': phase, 'cause': retryError.toString()}, + ); + } } } + bool _shouldRecoverStartupSensitiveRpc(Object error) { + return error is TimeoutException || + error is SocketException || + error is HttpException || + error is HandshakeException; + } + Future _generateStartupConfig({ required String walletName, required String walletPassword, diff --git a/packages/komodo_defi_rpc_methods/lib/src/common_structures/activation/activation_params/activation_params.dart b/packages/komodo_defi_rpc_methods/lib/src/common_structures/activation/activation_params/activation_params.dart index 1df318c44..0d4dd3dc4 100644 --- a/packages/komodo_defi_rpc_methods/lib/src/common_structures/activation/activation_params/activation_params.dart +++ b/packages/komodo_defi_rpc_methods/lib/src/common_structures/activation/activation_params/activation_params.dart @@ -411,8 +411,11 @@ class ActivationRpcData { /// Maximum number of electrum servers to keep connected. Defaults to 1. final int? maxConnected; - /// ZHTLC coins only. Optional, defaults to two days ago. Defines where to start - /// scanning blockchain data upon initial activation. + /// ZHTLC coins only. Optional. Defines where to start scanning blockchain + /// data on activation when provided by the client. + /// + /// If omitted, backend-side behavior applies (for example resuming from + /// persisted sync state or using backend defaults). /// /// Supported values: /// - Earliest: start from the coin's `sapling_activation_height` diff --git a/packages/komodo_defi_sdk/lib/komodo_defi_sdk.dart b/packages/komodo_defi_sdk/lib/komodo_defi_sdk.dart index b612cae13..b705bbfda 100644 --- a/packages/komodo_defi_sdk/lib/komodo_defi_sdk.dart +++ b/packages/komodo_defi_sdk/lib/komodo_defi_sdk.dart @@ -45,6 +45,8 @@ export 'src/activation_config/activation_config_service.dart' InMemoryKeyValueStore, JsonActivationConfigRepository, WalletIdResolver, + ZhtlcRecurringSyncMode, + ZhtlcRecurringSyncPolicy, ZhtlcUserConfig; export 'src/activation_config/hive_activation_config_repository.dart' show HiveActivationConfigRepository; diff --git a/packages/komodo_defi_sdk/lib/src/activation/protocol_strategies/zhtlc_activation_strategy.dart b/packages/komodo_defi_sdk/lib/src/activation/protocol_strategies/zhtlc_activation_strategy.dart index f6fb25bbc..a96427b17 100644 --- a/packages/komodo_defi_sdk/lib/src/activation/protocol_strategies/zhtlc_activation_strategy.dart +++ b/packages/komodo_defi_sdk/lib/src/activation/protocol_strategies/zhtlc_activation_strategy.dart @@ -4,8 +4,8 @@ import 'dart:convert'; import 'dart:developer' show log; -import 'package:komodo_defi_framework/komodo_defi_framework.dart'; +import 'package:komodo_defi_framework/komodo_defi_framework.dart'; import 'package:komodo_defi_rpc_methods/komodo_defi_rpc_methods.dart'; import 'package:komodo_defi_sdk/src/activation/_activation.dart'; import 'package:komodo_defi_sdk/src/activation/protocol_strategies/zhtlc_activation_progress.dart'; @@ -81,8 +81,9 @@ class ZhtlcActivationStrategy extends ProtocolActivationStrategy { privKeyPolicy: privKeyPolicy, ); - // Apply one-shot sync_params only when explicitly provided via config form - // right before activation. This avoids caching and unintended rewinds. + // Apply sync params only when explicitly requested as a one-shot + // override. If absent, omit sync_params so backend resume/default sync + // behavior is used. if (params.mode?.rpcData != null) { final oneShotSync = await configService.takeOneShotSyncParams(asset.id); if (oneShotSync != null) { @@ -106,15 +107,25 @@ class ZhtlcActivationStrategy extends ProtocolActivationStrategy { // Debug logging for ZHTLC activation if (KdfLoggingConfig.verboseLogging) { log( - '[RPC] Activating ZHTLC coin: ${asset.id.id}', - name: 'ZhtlcActivationStrategy', - ); + '[RPC] Activating ZHTLC coin: ${asset.id.id}', + name: 'ZhtlcActivationStrategy', + ); } if (KdfLoggingConfig.verboseLogging) { + final activationLogPayload = { + 'ticker': asset.id.id, + 'protocol': asset.protocol.subClass.formatted, + 'activation_params': params.toRpcParams(), + 'zcash_params_path': userConfig.zcashParamsPath, + 'scan_blocks_per_iteration': userConfig.scanBlocksPerIteration, + 'scan_interval_ms': userConfig.scanIntervalMs, + 'polling_interval_ms': effectivePollingInterval.inMilliseconds, + 'priv_key_policy': privKeyPolicy.toJson(), + }; log( - '[RPC] Activation parameters: ${jsonEncode({'ticker': asset.id.id, 'protocol': asset.protocol.subClass.formatted, 'activation_params': params.toRpcParams(), 'zcash_params_path': userConfig.zcashParamsPath, 'scan_blocks_per_iteration': userConfig.scanBlocksPerIteration, 'scan_interval_ms': userConfig.scanIntervalMs, 'polling_interval_ms': effectivePollingInterval.inMilliseconds, 'priv_key_policy': privKeyPolicy.toJson()})}', - name: 'ZhtlcActivationStrategy', - ); + '[RPC] Activation parameters: ${jsonEncode(activationLogPayload)}', + name: 'ZhtlcActivationStrategy', + ); } // Initialize task and watch via TaskShepherd @@ -167,7 +178,7 @@ class ZhtlcActivationStrategy extends ProtocolActivationStrategy { detail: detail, ); } - } catch (e, stack) { + } on Object catch (e, stack) { yield ActivationProgressZhtlc.failure(e, stack); } } diff --git a/packages/komodo_defi_sdk/lib/src/activation_config/activation_config_service.dart b/packages/komodo_defi_sdk/lib/src/activation_config/activation_config_service.dart index f783a1035..c00678996 100644 --- a/packages/komodo_defi_sdk/lib/src/activation_config/activation_config_service.dart +++ b/packages/komodo_defi_sdk/lib/src/activation_config/activation_config_service.dart @@ -1,14 +1,145 @@ import 'dart:async'; import 'dart:convert'; +import 'package:flutter/foundation.dart'; import 'package:hive_ce/hive.dart'; import 'package:komodo_defi_rpc_methods/komodo_defi_rpc_methods.dart'; import 'package:komodo_defi_types/komodo_defi_type_utils.dart'; import 'package:komodo_defi_types/komodo_defi_types.dart'; -import 'package:komodo_defi_local_auth/komodo_defi_local_auth.dart'; typedef JsonMap = Map; +enum ZhtlcRecurringSyncMode { + recentTransactions, + earliest, + height, + date; + + static ZhtlcRecurringSyncMode? tryParse(String? value) { + switch (value) { + case 'recent_transactions': + return ZhtlcRecurringSyncMode.recentTransactions; + case 'earliest': + return ZhtlcRecurringSyncMode.earliest; + case 'height': + return ZhtlcRecurringSyncMode.height; + case 'date': + return ZhtlcRecurringSyncMode.date; + default: + return null; + } + } + + String get jsonValue => switch (this) { + ZhtlcRecurringSyncMode.recentTransactions => 'recent_transactions', + ZhtlcRecurringSyncMode.earliest => 'earliest', + ZhtlcRecurringSyncMode.height => 'height', + ZhtlcRecurringSyncMode.date => 'date', + }; +} + +/// Persisted recurring sync policy for ZHTLC wallet activations. +class ZhtlcRecurringSyncPolicy { + ZhtlcRecurringSyncPolicy._({ + required this.mode, + this.height, + this.unixTimestamp, + }) : assert(switch (mode) { + ZhtlcRecurringSyncMode.recentTransactions || + ZhtlcRecurringSyncMode.earliest => + height == null && unixTimestamp == null, + ZhtlcRecurringSyncMode.height => height != null, + ZhtlcRecurringSyncMode.date => unixTimestamp != null, + }, 'Recurring sync policy data does not match its mode.'); + + factory ZhtlcRecurringSyncPolicy.recentTransactions() => + ZhtlcRecurringSyncPolicy._( + mode: ZhtlcRecurringSyncMode.recentTransactions, + ); + + factory ZhtlcRecurringSyncPolicy.earliest() => + ZhtlcRecurringSyncPolicy._(mode: ZhtlcRecurringSyncMode.earliest); + + factory ZhtlcRecurringSyncPolicy.height(int height) => + ZhtlcRecurringSyncPolicy._( + mode: ZhtlcRecurringSyncMode.height, + height: height, + ); + + factory ZhtlcRecurringSyncPolicy.date(int unixTimestamp) => + ZhtlcRecurringSyncPolicy._( + mode: ZhtlcRecurringSyncMode.date, + unixTimestamp: unixTimestamp, + ); + + factory ZhtlcRecurringSyncPolicy.fromJson(JsonMap json) { + final mode = ZhtlcRecurringSyncMode.tryParse( + json.valueOrNull('mode'), + ); + if (mode == null) { + throw ArgumentError.value( + json['mode'], + 'json.mode', + 'Unsupported recurring ZHTLC sync policy mode', + ); + } + + return switch (mode) { + ZhtlcRecurringSyncMode.recentTransactions => + ZhtlcRecurringSyncPolicy.recentTransactions(), + ZhtlcRecurringSyncMode.earliest => ZhtlcRecurringSyncPolicy.earliest(), + ZhtlcRecurringSyncMode.height => ZhtlcRecurringSyncPolicy.height( + json.value('height'), + ), + ZhtlcRecurringSyncMode.date => ZhtlcRecurringSyncPolicy.date( + json.value('unixTimestamp'), + ), + }; + } + + factory ZhtlcRecurringSyncPolicy.fromSyncParams(ZhtlcSyncParams syncParams) { + if (syncParams.isEarliest) { + return ZhtlcRecurringSyncPolicy.earliest(); + } + if (syncParams.height != null) { + return ZhtlcRecurringSyncPolicy.height(syncParams.height!); + } + if (syncParams.date != null) { + return ZhtlcRecurringSyncPolicy.date(syncParams.date!); + } + throw ArgumentError.value( + syncParams, + 'syncParams', + 'Unsupported ZHTLC sync params payload', + ); + } + + final ZhtlcRecurringSyncMode mode; + final int? height; + final int? unixTimestamp; + + JsonMap toJson() => { + 'mode': mode.jsonValue, + if (height != null) 'height': height, + if (unixTimestamp != null) 'unixTimestamp': unixTimestamp, + }; + + ZhtlcSyncParams toSyncParams({DateTime? now}) { + return switch (mode) { + ZhtlcRecurringSyncMode.recentTransactions => ZhtlcSyncParams.date( + (now ?? DateTime.now()) + .toUtc() + .subtract(const Duration(days: 2)) + .millisecondsSinceEpoch ~/ + 1000, + ), + ZhtlcRecurringSyncMode.earliest => ZhtlcSyncParams.earliest(), + ZhtlcRecurringSyncMode.height => ZhtlcSyncParams.height(height!), + ZhtlcRecurringSyncMode.date => ZhtlcSyncParams.date(unixTimestamp!), + }; + } +} + /// Simple key-value store abstraction for persisting activation configs. abstract class KeyValueStore { Future get(String key); @@ -45,6 +176,7 @@ class ZhtlcUserConfig { this.scanBlocksPerIteration = 1000, this.scanIntervalMs = 0, this.taskStatusPollingIntervalMs, + this.recurringSyncPolicy, this.syncParams, }); @@ -52,13 +184,39 @@ class ZhtlcUserConfig { final int scanBlocksPerIteration; final int scanIntervalMs; final int? taskStatusPollingIntervalMs; + final ZhtlcRecurringSyncPolicy? recurringSyncPolicy; + /// Optional, accepted for backward compatibility. Not persisted. /// If provided to saveZhtlcConfig, it will be applied as a one-shot /// sync override for the next activation and then discarded. final ZhtlcSyncParams? syncParams; - // Sync params are no longer persisted here; they are supplied one-shot - // via ActivationConfigService at activation time when the user requests - // an intentional resync. + // Sync params are supplied one-shot via ActivationConfigService when the + // user requests an immediate resync. Recurring sync behavior is persisted + // separately via [recurringSyncPolicy]. + + ZhtlcUserConfig copyWith({ + String? zcashParamsPath, + int? scanBlocksPerIteration, + int? scanIntervalMs, + int? taskStatusPollingIntervalMs, + ZhtlcRecurringSyncPolicy? recurringSyncPolicy, + bool clearRecurringSyncPolicy = false, + ZhtlcSyncParams? syncParams, + bool clearSyncParams = false, + }) { + return ZhtlcUserConfig( + zcashParamsPath: zcashParamsPath ?? this.zcashParamsPath, + scanBlocksPerIteration: + scanBlocksPerIteration ?? this.scanBlocksPerIteration, + scanIntervalMs: scanIntervalMs ?? this.scanIntervalMs, + taskStatusPollingIntervalMs: + taskStatusPollingIntervalMs ?? this.taskStatusPollingIntervalMs, + recurringSyncPolicy: clearRecurringSyncPolicy + ? null + : recurringSyncPolicy ?? this.recurringSyncPolicy, + syncParams: clearSyncParams ? null : syncParams ?? this.syncParams, + ); + } JsonMap toJson() => { 'zcashParamsPath': zcashParamsPath, @@ -66,6 +224,8 @@ class ZhtlcUserConfig { 'scanIntervalMs': scanIntervalMs, if (taskStatusPollingIntervalMs != null) 'taskStatusPollingIntervalMs': taskStatusPollingIntervalMs, + if (recurringSyncPolicy != null) + 'recurringSyncPolicy': recurringSyncPolicy!.toJson(), }; static ZhtlcUserConfig fromJson(JsonMap json) => ZhtlcUserConfig( @@ -76,6 +236,12 @@ class ZhtlcUserConfig { taskStatusPollingIntervalMs: json.valueOrNull( 'taskStatusPollingIntervalMs', ), + recurringSyncPolicy: + json.valueOrNull('recurringSyncPolicy') == null + ? null + : ZhtlcRecurringSyncPolicy.fromJson( + json.value('recurringSyncPolicy'), + ), ); } @@ -249,8 +415,12 @@ class ActivationConfigService { onTimeout: () => null, ); if (result == null) return null; - await repo.saveConfig(walletId, id, result); - return result; + if (result.syncParams != null) { + _oneShotSyncParams[key] = result.syncParams; + } + final normalizedConfig = _normalizeConfigForPersistence(result); + await repo.saveConfig(walletId, id, normalizedConfig); + return normalizedConfig; } finally { _awaitingControllers.remove(key); } @@ -258,12 +428,12 @@ class ActivationConfigService { Future saveZhtlcConfig(AssetId id, ZhtlcUserConfig config) async { final walletId = await _requireActiveWallet(); - // If legacy callers provide syncParams in the config, convert it to - // a one-shot sync override and do not persist it. - if (config.syncParams != null) { - _oneShotSyncParams[_WalletAssetKey(walletId, id)] = config.syncParams; + final oneShotSyncParams = config.syncParams; + final normalizedConfig = _normalizeConfigForPersistence(config); + if (oneShotSyncParams != null) { + _oneShotSyncParams[_WalletAssetKey(walletId, id)] = oneShotSyncParams; } - await repo.saveConfig(walletId, id, config); + await repo.saveConfig(walletId, id, normalizedConfig); } Future submitZhtlc(AssetId id, ZhtlcUserConfig config) async { @@ -290,6 +460,19 @@ class ActivationConfigService { return value; } + ZhtlcUserConfig _normalizeConfigForPersistence(ZhtlcUserConfig config) { + final recurringSyncPolicy = + config.recurringSyncPolicy ?? + (config.syncParams == null + ? null + : ZhtlcRecurringSyncPolicy.fromSyncParams(config.syncParams!)); + + return config.copyWith( + recurringSyncPolicy: recurringSyncPolicy, + clearSyncParams: true, + ); + } + /// Clears all one-shot sync params for the specified wallet. /// This should be called when a user signs out to prevent stale one-shot /// params from being applied on the next activation after re-login. @@ -307,8 +490,9 @@ class ActivationConfigService { {}; } +@immutable class _WalletAssetKey { - _WalletAssetKey(this.walletId, this.assetId); + const _WalletAssetKey(this.walletId, this.assetId); final WalletId walletId; final AssetId assetId; diff --git a/packages/komodo_defi_sdk/lib/src/errors/sdk_error_mapper.dart b/packages/komodo_defi_sdk/lib/src/errors/sdk_error_mapper.dart index dd7cede59..decbda903 100644 --- a/packages/komodo_defi_sdk/lib/src/errors/sdk_error_mapper.dart +++ b/packages/komodo_defi_sdk/lib/src/errors/sdk_error_mapper.dart @@ -229,6 +229,7 @@ class _AuthExceptionHandler extends SdkErrorHandler { case AuthExceptionType.alreadySignedIn: case AuthExceptionType.registrationNotAllowed: case AuthExceptionType.internalError: + case AuthExceptionType.legacyWalletAlreadyMigrated: return _build( code: SdkErrorCode.general, category: SdkErrorCategory.auth, diff --git a/packages/komodo_defi_sdk/test/activation/zhtlc_activation_strategy_test.dart b/packages/komodo_defi_sdk/test/activation/zhtlc_activation_strategy_test.dart new file mode 100644 index 000000000..141dfad74 --- /dev/null +++ b/packages/komodo_defi_sdk/test/activation/zhtlc_activation_strategy_test.dart @@ -0,0 +1,155 @@ +import 'dart:collection'; + +import 'package:komodo_defi_rpc_methods/komodo_defi_rpc_methods.dart'; +import 'package:komodo_defi_sdk/src/activation/protocol_strategies/zhtlc_activation_strategy.dart'; +import 'package:komodo_defi_sdk/src/activation_config/activation_config_service.dart'; +import 'package:komodo_defi_types/komodo_defi_types.dart'; +import 'package:test/test.dart'; + +class _QueueApiClient implements ApiClient { + _QueueApiClient({required Map> responsesByMethod}) + : _responsesByMethod = { + for (final entry in responsesByMethod.entries) + entry.key: Queue.from(entry.value), + }; + + final Map> _responsesByMethod; + final List requests = []; + + @override + Future executeRpc(JsonMap request) async { + requests.add(Map.from(request)); + final method = request['method'] as String?; + if (method == null || method.isEmpty) { + throw StateError('Missing RPC method in request: $request'); + } + + final queue = _responsesByMethod[method]; + if (queue == null || queue.isEmpty) { + throw StateError('No queued response for method $method'); + } + + return queue.removeFirst(); + } +} + +Asset _createZhtlcAsset() { + final protocol = ZhtlcProtocol.fromJson(const { + 'type': 'ZHTLC', + 'light_wallet_d_servers': ['https://lightd.example'], + 'electrum_servers': [ + {'url': 'electrum.example:50002', 'protocol': 'SSL'}, + ], + }); + + return Asset( + id: AssetId( + id: 'ARRR', + name: 'Pirate Chain', + symbol: AssetSymbol(assetConfigId: 'ARRR'), + chainId: AssetChainId(chainId: 1), + derivationPath: null, + subClass: CoinSubClass.zhtlc, + ), + protocol: protocol, + isWalletOnly: false, + signMessagePrefix: null, + ); +} + +void main() { + group('ZhtlcActivationStrategy', () { + test('applies one-shot sync params once and omits sync_params ' + 'on subsequent activations', () async { + final walletId = WalletId.fromName( + 'Test Wallet', + const AuthOptions(derivationMethod: DerivationMethod.iguana), + ); + final configService = ActivationConfigService( + JsonActivationConfigRepository(InMemoryKeyValueStore()), + walletIdResolver: () async => walletId, + ); + final asset = _createZhtlcAsset(); + + await configService.saveZhtlcConfig( + asset.id, + ZhtlcUserConfig( + zcashParamsPath: '/zcash-params', + recurringSyncPolicy: ZhtlcRecurringSyncPolicy.recentTransactions(), + syncParams: ZhtlcSyncParams.height(123456), + ), + ); + + final client = _QueueApiClient( + responsesByMethod: { + 'task::enable_z_coin::init': [ + { + 'mmrpc': '2.0', + 'result': {'task_id': 1}, + }, + { + 'mmrpc': '2.0', + 'result': {'task_id': 2}, + }, + ], + 'task::enable_z_coin::status': [ + { + 'mmrpc': '2.0', + 'result': {'status': 'Ok', 'details': 'done'}, + }, + { + 'mmrpc': '2.0', + 'result': {'status': 'Ok', 'details': 'done'}, + }, + ], + }, + ); + final strategy = ZhtlcActivationStrategy( + client, + const PrivateKeyPolicy.contextPrivKey(), + configService, + pollingInterval: const Duration(milliseconds: 1), + ); + + await strategy.activate(asset).toList(); + await strategy.activate(asset).toList(); + + final initRequests = client.requests + .where((request) => request['method'] == 'task::enable_z_coin::init') + .toList(growable: false); + expect(initRequests, hasLength(2)); + + final firstRpcData = + ((initRequests.first['params'] + as Map)['activation_params'] + as Map)['mode'] + as Map; + final firstModeRpcData = firstRpcData['rpc_data'] as Map; + expect(firstModeRpcData['sync_params'], { + 'height': 123456, + }); + + final secondRpcData = + ((initRequests.last['params'] + as Map)['activation_params'] + as Map)['mode'] + as Map; + final secondModeRpcData = + secondRpcData['rpc_data'] as Map; + expect(secondModeRpcData.containsKey('sync_params'), isFalse); + + final savedConfig = await configService.getSavedZhtlc(asset.id); + expect(savedConfig, isNotNull); + expect(savedConfig?.syncParams, isNull); + expect( + savedConfig?.recurringSyncPolicy?.mode, + ZhtlcRecurringSyncMode.recentTransactions, + ); + + final remainingOneShot = await configService.takeOneShotSyncParams( + asset.id, + ); + expect(remainingOneShot, isNull); + }); + }); +} diff --git a/packages/komodo_defi_sdk/test/src/activation_config/activation_config_service_test.dart b/packages/komodo_defi_sdk/test/src/activation_config/activation_config_service_test.dart new file mode 100644 index 000000000..3f51501e5 --- /dev/null +++ b/packages/komodo_defi_sdk/test/src/activation_config/activation_config_service_test.dart @@ -0,0 +1,75 @@ +import 'package:komodo_defi_rpc_methods/komodo_defi_rpc_methods.dart'; +import 'package:komodo_defi_sdk/komodo_defi_sdk.dart'; +import 'package:komodo_defi_types/komodo_defi_types.dart'; +import 'package:test/test.dart'; + +void main() { + group('ZhtlcRecurringSyncPolicy', () { + test('recentTransactions resolves to a runtime date sync param', () { + final policy = ZhtlcRecurringSyncPolicy.recentTransactions(); + final now = DateTime.utc(2026, 4, 10, 12); + final syncParams = policy.toSyncParams(now: now); + + expect(syncParams.isEarliest, isFalse); + expect(syncParams.height, isNull); + expect( + syncParams.date, + now.subtract(const Duration(days: 2)).millisecondsSinceEpoch ~/ 1000, + ); + }); + + test('serializes and deserializes date policies', () { + final policy = ZhtlcRecurringSyncPolicy.date(1775659200); + + final decoded = ZhtlcRecurringSyncPolicy.fromJson(policy.toJson()); + + expect(decoded.mode, ZhtlcRecurringSyncMode.date); + expect(decoded.unixTimestamp, 1775659200); + }); + }); + + group('ActivationConfigService', () { + test('saveZhtlcConfig persists recurring policy and stores one-shot ' + 'sync params', () async { + final walletId = WalletId.fromName( + 'Test Wallet', + const AuthOptions(derivationMethod: DerivationMethod.iguana), + ); + final assetId = AssetId( + id: 'ARRR', + name: 'Pirate Chain', + symbol: AssetSymbol(assetConfigId: 'ARRR'), + chainId: AssetChainId(chainId: 777, decimalsValue: 8), + derivationPath: null, + subClass: CoinSubClass.zhtlc, + ); + final service = ActivationConfigService( + JsonActivationConfigRepository(InMemoryKeyValueStore()), + walletIdResolver: () async => walletId, + ); + + await service.saveZhtlcConfig( + assetId, + ZhtlcUserConfig( + zcashParamsPath: '/zcash-params', + syncParams: ZhtlcSyncParams.height(123456), + ), + ); + + final savedConfig = await service.getSavedZhtlc(assetId); + final oneShotSync = await service.takeOneShotSyncParams(assetId); + final consumedSync = await service.takeOneShotSyncParams(assetId); + + expect(savedConfig, isNotNull); + expect(savedConfig?.syncParams, isNull); + expect(savedConfig?.zcashParamsPath, '/zcash-params'); + expect( + savedConfig?.recurringSyncPolicy?.mode, + ZhtlcRecurringSyncMode.height, + ); + expect(savedConfig?.recurringSyncPolicy?.height, 123456); + expect(oneShotSync?.height, 123456); + expect(consumedSync, isNull); + }); + }); +} diff --git a/packages/komodo_defi_types/lib/src/auth/exceptions/auth_exception.dart b/packages/komodo_defi_types/lib/src/auth/exceptions/auth_exception.dart index 5acd85651..b2e33a37b 100644 --- a/packages/komodo_defi_types/lib/src/auth/exceptions/auth_exception.dart +++ b/packages/komodo_defi_types/lib/src/auth/exceptions/auth_exception.dart @@ -14,20 +14,26 @@ enum AuthExceptionType { registrationNotAllowed, internalError, apiConnectionError, + + /// Legacy wallet already has a migrated KDF counterpart. + legacyWalletAlreadyMigrated, } class AuthException implements Exception { - AuthException( - this.message, { - required this.type, - this.details = const {}, - }); + AuthException(this.message, {required this.type, this.details = const {}}); // Common exception constructors convenience methods AuthException.notSignedIn() - : this('Not signed in', type: AuthExceptionType.unauthorized); + : this('Not signed in', type: AuthExceptionType.unauthorized); AuthException.notFound() - : this('Not found', type: AuthExceptionType.walletNotFound); + : this('Not found', type: AuthExceptionType.walletNotFound); + + AuthException.legacyWalletAlreadyMigrated(String migratedWalletName) + : this( + 'Legacy wallet already migrated', + type: AuthExceptionType.legacyWalletAlreadyMigrated, + details: {'migratedWalletName': migratedWalletName}, + ); /// The error message. final String message; @@ -95,49 +101,46 @@ class AuthException implements Exception { return matchingPatterns[AuthExceptionType.registrationNotAllowed]!; case AuthExceptionType.apiConnectionError: return matchingPatterns[AuthExceptionType.apiConnectionError]!; - // The following types don't originate from the API, so we return empty arrays + // The following types don't originate from the API, so we return empty + // arrays. case AuthExceptionType.generalAuthError: case AuthExceptionType.unauthorized: case AuthExceptionType.alreadySignedIn: case AuthExceptionType.internalError: case AuthExceptionType.invalidBip39Mnemonic: + case AuthExceptionType.legacyWalletAlreadyMigrated: return []; } } static Map> get matchingPatterns => { - AuthExceptionType.incorrectPassword: [ - 'Incorrect wallet password', - 'Error generating or decrypting mnemonic', - 'HMAC', - 'Error decrypting mnemonic: HMAC error: MAC tag mismatch', - 'MAC tag mismatch', - 'Error decrypting mnemonic', - ], - AuthExceptionType.walletAlreadyRunning: [ - 'Wallet is already running', - ], - AuthExceptionType.walletStartFailed: [ - 'Failed to start KDF', - ], - AuthExceptionType.walletNotFound: [ - 'Wallet does not exist', - 'No wallet found with the given name', - ], - AuthExceptionType.walletAlreadyExists: [ - 'Wallet already exists', - 'A wallet with this name already exists', - ], - AuthExceptionType.registrationNotAllowed: [ - 'wallet creation is disabled', - ], - AuthExceptionType.apiConnectionError: [ - 'Connection refused', - 'Connection timed out', - ], - // We don't include patterns for the following types as they don't originate from the API - // AuthExceptionType.generalAuthError - // AuthExceptionType.unauthorized - // AuthExceptionType.alreadySignedIn - }; + AuthExceptionType.incorrectPassword: [ + 'Incorrect wallet password', + 'Error generating or decrypting mnemonic', + 'HMAC', + 'Error decrypting mnemonic: HMAC error: MAC tag mismatch', + 'MAC tag mismatch', + 'Error decrypting mnemonic', + ], + AuthExceptionType.walletAlreadyRunning: ['Wallet is already running'], + AuthExceptionType.walletStartFailed: ['Failed to start KDF'], + AuthExceptionType.walletNotFound: [ + 'Wallet does not exist', + 'No wallet found with the given name', + ], + AuthExceptionType.walletAlreadyExists: [ + 'Wallet already exists', + 'A wallet with this name already exists', + ], + AuthExceptionType.registrationNotAllowed: ['wallet creation is disabled'], + AuthExceptionType.apiConnectionError: [ + 'Connection refused', + 'Connection timed out', + ], + // We don't include patterns for the following types as they don't + // originate from the API. + // AuthExceptionType.generalAuthError + // AuthExceptionType.unauthorized + // AuthExceptionType.alreadySignedIn + }; } diff --git a/packages/komodo_legacy_wallet_migration/lib/komodo_legacy_wallet_migration.dart b/packages/komodo_legacy_wallet_migration/lib/komodo_legacy_wallet_migration.dart index d42ad2e7a..a2916cb2d 100644 --- a/packages/komodo_legacy_wallet_migration/lib/komodo_legacy_wallet_migration.dart +++ b/packages/komodo_legacy_wallet_migration/lib/komodo_legacy_wallet_migration.dart @@ -1,4 +1,6 @@ -/// Legacy wallet migration utilities for Komodo/Gleec SDK apps. -library; - +// Legacy wallet migration utilities for Komodo/Gleec SDK apps. export 'src/komodo_legacy_wallet_migration.dart'; +export 'src/models/legacy_wallet_cleanup_result.dart'; +export 'src/models/legacy_wallet_migration_exception.dart'; +export 'src/models/legacy_wallet_record.dart'; +export 'src/models/legacy_wallet_secrets.dart'; diff --git a/packages/komodo_legacy_wallet_migration/lib/src/adapters/legacy_password_verifier.dart b/packages/komodo_legacy_wallet_migration/lib/src/adapters/legacy_password_verifier.dart new file mode 100644 index 000000000..23b09e33a --- /dev/null +++ b/packages/komodo_legacy_wallet_migration/lib/src/adapters/legacy_password_verifier.dart @@ -0,0 +1,33 @@ +import 'package:dargon2_flutter/dargon2_flutter.dart'; + +/// Verifies a legacy native wallet password against the stored seed hash. +// ignore: one_member_abstracts +abstract interface class LegacyPasswordVerifier { + /// Returns `true` when [password] matches the legacy [encodedHash]. + Future verifySeedPassword({ + required String password, + required String encodedHash, + }); +} + +/// Argon2id-based verifier for legacy native wallet seed passwords. +class Argon2LegacyPasswordVerifier implements LegacyPasswordVerifier { + /// Creates an Argon2-based verifier. + const Argon2LegacyPasswordVerifier(); + + @override + Future verifySeedPassword({ + required String password, + required String encodedHash, + }) async { + try { + return await argon2.verifyHashString( + password, + encodedHash, + type: Argon2Type.id, + ); + } on Object { + return false; + } + } +} diff --git a/packages/komodo_legacy_wallet_migration/lib/src/adapters/legacy_secure_storage.dart b/packages/komodo_legacy_wallet_migration/lib/src/adapters/legacy_secure_storage.dart new file mode 100644 index 000000000..3ae1250e0 --- /dev/null +++ b/packages/komodo_legacy_wallet_migration/lib/src/adapters/legacy_secure_storage.dart @@ -0,0 +1,42 @@ +import 'package:flutter_secure_storage/flutter_secure_storage.dart'; + +/// Minimal secure-storage interface for legacy native wallet migration. +abstract interface class LegacySecureStorage { + /// Reads the raw value stored under [key]. + Future read(String key); + + /// Deletes the value stored under [key]. + Future delete(String key); +} + +/// `flutter_secure_storage` adapter used by the production migration service. +class FlutterLegacySecureStorage implements LegacySecureStorage { + /// Creates a secure-storage adapter. + FlutterLegacySecureStorage({FlutterSecureStorage? storage}) + : _storage = + storage ?? + const FlutterSecureStorage( + // The current app/plugin version still needs this option so legacy + // migration opens the same Android secure-storage backend. + // ignore: deprecated_member_use + aOptions: AndroidOptions(encryptedSharedPreferences: true), + iOptions: IOSOptions( + accessibility: KeychainAccessibility.first_unlock, + ), + mOptions: MacOsOptions( + accessibility: KeychainAccessibility.first_unlock, + ), + ); + + final FlutterSecureStorage _storage; + + @override + Future read(String key) { + return _storage.read(key: key); + } + + @override + Future delete(String key) { + return _storage.delete(key: key); + } +} diff --git a/packages/komodo_legacy_wallet_migration/lib/src/adapters/legacy_shared_preferences_store.dart b/packages/komodo_legacy_wallet_migration/lib/src/adapters/legacy_shared_preferences_store.dart new file mode 100644 index 000000000..82ad0e87d --- /dev/null +++ b/packages/komodo_legacy_wallet_migration/lib/src/adapters/legacy_shared_preferences_store.dart @@ -0,0 +1,29 @@ +import 'package:shared_preferences/shared_preferences.dart'; + +/// Minimal shared-preferences interface for legacy native wallet migration. +abstract interface class LegacySharedPreferencesStore { + /// Reads the raw value stored under [key]. + Future read(String key); + + /// Deletes the value stored under [key]. + Future delete(String key); +} + +/// `shared_preferences` adapter used by the production migration service. +class FlutterLegacySharedPreferencesStore + implements LegacySharedPreferencesStore { + Future _instance() => SharedPreferences.getInstance(); + + @override + Future read(String key) async { + final prefs = await _instance(); + await prefs.reload(); + return prefs.get(key); + } + + @override + Future delete(String key) async { + final prefs = await _instance(); + await prefs.remove(key); + } +} diff --git a/packages/komodo_legacy_wallet_migration/lib/src/adapters/legacy_wallet_metadata_store.dart b/packages/komodo_legacy_wallet_migration/lib/src/adapters/legacy_wallet_metadata_store.dart new file mode 100644 index 000000000..c61be8169 --- /dev/null +++ b/packages/komodo_legacy_wallet_migration/lib/src/adapters/legacy_wallet_metadata_store.dart @@ -0,0 +1,188 @@ +import 'package:komodo_legacy_wallet_migration/src/models/legacy_wallet_migration_exception.dart'; +import 'package:komodo_legacy_wallet_migration/src/models/legacy_wallet_record.dart'; +import 'package:path/path.dart' as p; +import 'package:path_provider/path_provider.dart'; +import 'package:sqflite/sqflite.dart'; + +/// Minimal metadata-store interface for legacy native wallet migration. +abstract interface class LegacyWalletMetadataStore { + /// Lists legacy wallets discovered in the native wallet database. + Future> listWallets(); + + /// Deletes all persisted database rows associated with [walletId]. + Future deleteWalletData({required String walletId}); +} + +/// `sqflite` adapter for reading the legacy native wallet database. +class SqfliteLegacyWalletMetadataStore implements LegacyWalletMetadataStore { + /// Creates a metadata store backed by the legacy `AtomicDEX.db` database. + SqfliteLegacyWalletMetadataStore({ + Future Function()? documentsPathProvider, + }) : _documentsPathProvider = + documentsPathProvider ?? _defaultDocumentsPathProvider; + + final Future Function() _documentsPathProvider; + + static Future _defaultDocumentsPathProvider() async { + final directory = await getApplicationDocumentsDirectory(); + return directory.path; + } + + Future _databasePath() async { + final documentsPath = await _documentsPathProvider(); + return p.join(documentsPath, 'AtomicDEX.db'); + } + + @override + Future> listWallets() async { + try { + final databasePath = await _databasePath(); + if (!await databaseExists(databasePath)) { + return const []; + } + + final database = await openDatabase(databasePath, readOnly: true); + try { + final walletRows = await database.query( + 'Wallet', + columns: [ + 'id', + 'name', + 'activate_pin_protection', + 'activate_bio_protection', + 'switch_pin_log_out_on_exit', + 'enable_camo', + 'is_camo_active', + 'camo_fraction', + 'camo_balance', + 'camo_session_started_at', + ], + ); + final currentWalletRows = await database.query( + 'CurrentWallet', + columns: ['id'], + ); + final activatedCoinRows = await database.query( + 'ListOfCoinsActivated', + columns: ['wallet_id', 'coins'], + ); + + final currentWalletIds = currentWalletRows + .map((row) => row['id']) + .whereType() + .toSet(); + final activatedCoinsByWalletId = >{ + for (final row in activatedCoinRows) + if (row['wallet_id'] is String) + row['wallet_id']! as String: _parseActivatedCoins(row['coins']), + }; + + return walletRows + .map( + (row) => LegacyWalletRecord( + walletId: row['id'] as String? ?? '', + walletName: row['name'] as String? ?? '', + activatedCoins: + activatedCoinsByWalletId[row['id'] as String? ?? ''] ?? + const [], + isCurrentWallet: currentWalletIds.contains(row['id']), + walletExtras: _parseWalletExtras(row), + ), + ) + .where((wallet) => wallet.walletId.isNotEmpty) + .toList(growable: false); + } finally { + await database.close(); + } + } on Object catch (error) { + throw LegacyWalletMigrationException.storageAccessError( + 'Failed to read legacy wallet metadata: $error', + ); + } + } + + @override + Future deleteWalletData({required String walletId}) async { + try { + final databasePath = await _databasePath(); + if (!await databaseExists(databasePath)) { + return; + } + + final database = await openDatabase(databasePath); + try { + await database.transaction((txn) async { + await txn.delete('Wallet', where: 'id = ?', whereArgs: [walletId]); + await txn.delete( + 'CurrentWallet', + where: 'id = ?', + whereArgs: [walletId], + ); + await txn.delete( + 'ListOfCoinsActivated', + where: 'wallet_id = ?', + whereArgs: [walletId], + ); + await txn.delete( + 'WalletSnapshot', + where: 'wallet_id = ?', + whereArgs: [walletId], + ); + }); + } finally { + await database.close(); + } + } on Object catch (error) { + throw LegacyWalletMigrationException.storageAccessError( + 'Failed to delete legacy wallet metadata: $error', + ); + } + } + + List _parseActivatedCoins(Object? rawCoins) { + if (rawCoins is! String || rawCoins.trim().isEmpty) { + return const []; + } + + return rawCoins + .split(',') + .map((coin) => coin.trim()) + .where((coin) => coin.isNotEmpty) + .toList(growable: false); + } + + Map _parseWalletExtras(Map row) { + final extras = {}; + void addBool(String legacyKey, String outputKey) { + final value = row[legacyKey]; + if (value is int) { + extras[outputKey] = value == 1; + } else if (value is bool) { + extras[outputKey] = value; + } + } + + addBool('activate_pin_protection', 'activate_pin_protection'); + addBool('activate_bio_protection', 'activate_bio_protection'); + addBool('switch_pin_log_out_on_exit', 'switch_pin_log_out_on_exit'); + addBool('enable_camo', 'enable_camo'); + addBool('is_camo_active', 'is_camo_active'); + + final camoFraction = row['camo_fraction']; + if (camoFraction is int) { + extras['camo_fraction'] = camoFraction; + } + + final camoBalance = row['camo_balance']; + if (camoBalance is String && camoBalance.isNotEmpty) { + extras['camo_balance'] = camoBalance; + } + + final camoSessionStartedAt = row['camo_session_started_at']; + if (camoSessionStartedAt is int) { + extras['camo_session_started_at'] = camoSessionStartedAt; + } + + return extras; + } +} diff --git a/packages/komodo_legacy_wallet_migration/lib/src/adapters/legacy_wallet_platform.dart b/packages/komodo_legacy_wallet_migration/lib/src/adapters/legacy_wallet_platform.dart new file mode 100644 index 000000000..3da15a2d9 --- /dev/null +++ b/packages/komodo_legacy_wallet_migration/lib/src/adapters/legacy_wallet_platform.dart @@ -0,0 +1,23 @@ +import 'package:flutter/foundation.dart'; + +/// Minimal platform-gating interface for legacy native wallet migration. +abstract interface class LegacyWalletPlatform { + /// Whether the current runtime can access legacy native wallet storage. + bool get isSupportedPlatform; +} + +/// Default platform implementation for the production migration service. +class DefaultLegacyWalletPlatform implements LegacyWalletPlatform { + /// Creates the default platform detector. + const DefaultLegacyWalletPlatform(); + + @override + bool get isSupportedPlatform { + if (kIsWeb) { + return false; + } + + return defaultTargetPlatform == TargetPlatform.android || + defaultTargetPlatform == TargetPlatform.iOS; + } +} diff --git a/packages/komodo_legacy_wallet_migration/lib/src/komodo_legacy_wallet_migration.dart b/packages/komodo_legacy_wallet_migration/lib/src/komodo_legacy_wallet_migration.dart index 188619509..cff77aca5 100644 --- a/packages/komodo_legacy_wallet_migration/lib/src/komodo_legacy_wallet_migration.dart +++ b/packages/komodo_legacy_wallet_migration/lib/src/komodo_legacy_wallet_migration.dart @@ -1,7 +1,359 @@ -/// {@template komodo_legacy_wallet_migration} +import 'package:komodo_legacy_wallet_migration/src/adapters/legacy_password_verifier.dart'; +import 'package:komodo_legacy_wallet_migration/src/adapters/legacy_secure_storage.dart'; +import 'package:komodo_legacy_wallet_migration/src/adapters/legacy_shared_preferences_store.dart'; +import 'package:komodo_legacy_wallet_migration/src/adapters/legacy_wallet_metadata_store.dart'; +import 'package:komodo_legacy_wallet_migration/src/adapters/legacy_wallet_platform.dart'; +import 'package:komodo_legacy_wallet_migration/src/models/legacy_wallet_cleanup_result.dart'; +import 'package:komodo_legacy_wallet_migration/src/models/legacy_wallet_migration_exception.dart'; +import 'package:komodo_legacy_wallet_migration/src/models/legacy_wallet_record.dart'; +import 'package:komodo_legacy_wallet_migration/src/models/legacy_wallet_secrets.dart'; + /// Legacy wallet migration utilities for Komodo/Gleec SDK apps. -/// {@endtemplate} class KomodoLegacyWalletMigration { - /// {@macro komodo_legacy_wallet_migration} - const KomodoLegacyWalletMigration(); + /// Creates a migration service with injectable storage/platform adapters. + KomodoLegacyWalletMigration({ + LegacyWalletMetadataStore? metadataStore, + LegacySecureStorage? secureStorage, + LegacySharedPreferencesStore? sharedPreferencesStore, + LegacyPasswordVerifier? passwordVerifier, + LegacyWalletPlatform? platform, + }) : _metadataStore = metadataStore ?? SqfliteLegacyWalletMetadataStore(), + _secureStorage = secureStorage ?? FlutterLegacySecureStorage(), + _sharedPreferencesStore = + sharedPreferencesStore ?? FlutterLegacySharedPreferencesStore(), + _passwordVerifier = + passwordVerifier ?? const Argon2LegacyPasswordVerifier(), + _platform = platform ?? const DefaultLegacyWalletPlatform(); + + final LegacyWalletMetadataStore _metadataStore; + final LegacySecureStorage _secureStorage; + final LegacySharedPreferencesStore _sharedPreferencesStore; + final LegacyPasswordVerifier _passwordVerifier; + final LegacyWalletPlatform _platform; + + static const String _seedPrefix = 'KeyEncryption.SEED'; + static const String _pinPrefix = 'KeyEncryption.PIN'; + static const String _camoPinPrefix = 'KeyEncryption.CAMOPIN'; + static const String _passwordPrefix = 'password'; + static const String _genericPinKey = 'pin'; + static const String _genericCamoPinKey = 'camoPin'; + static const String _genericPassphraseKey = 'passphrase'; + static const String _zhtlcSyncTypeKey = 'zhtlcSyncType'; + static const String _zhtlcSyncStartDateKey = 'zhtlcSyncStartDate'; + static const String _zCoinActivationRequestedKeyPrefix = + 'z-coin-activation-requested-'; + static const String _disallowScreenshotKey = 'disallowScreenshot'; + + /// Whether the current runtime can access legacy native wallet storage. + bool get isSupportedPlatform => _platform.isSupportedPlatform; + + /// Lists legacy native wallets available for migration on this device. + Future> listLegacyWallets() async { + if (!isSupportedPlatform) { + return const []; + } + + return _metadataStore.listWallets(); + } + + /// Reads the plaintext seed and cleanup keys for [wallet]. + Future readWalletSecrets({ + required LegacyWalletRecord wallet, + required String password, + }) async { + try { + final wallets = await _listWallets(); + final sourceWallet = _findWalletById(wallet, wallets); + final cleanupKeys = _buildSecureStorageKeys( + wallet: sourceWallet, + password: password, + ); + final requestedZhtlcCoinIds = await _readRequestedZhtlcCoinIds( + sourceWallet.walletId, + ); + final zhtlcTickers = { + ...sourceWallet.activatedCoins, + ...requestedZhtlcCoinIds, + }; + final walletExtras = await _readWalletExtras(sourceWallet); + final seedPhrase = await _secureStorage.read(cleanupKeys.seedKey); + if (seedPhrase != null && seedPhrase.isNotEmpty) { + return LegacyWalletSecrets( + sourceWallet: sourceWallet, + seedPhrase: seedPhrase, + secureStorageKeysToDelete: [ + ...cleanupKeys.toDelete, + ...zhtlcTickers.map( + (ticker) => '${sourceWallet.walletId}_task_id_$ticker', + ), + ], + genericStorageKeysToDelete: + _shouldDeleteGenericSessionKeys( + sourceWallet: sourceWallet, + wallets: wallets, + ) + ? const [ + _genericPinKey, + _genericCamoPinKey, + _genericPassphraseKey, + ] + : const [], + sharedPreferencesKeysToDelete: [ + '$_zCoinActivationRequestedKeyPrefix${sourceWallet.walletId}', + ], + requestedZhtlcCoinIds: requestedZhtlcCoinIds, + legacyZhtlcSyncType: + (await _sharedPreferencesStore.read(_zhtlcSyncTypeKey)) + as String?, + legacyZhtlcSyncStartDate: _parseLegacySyncStartDate( + await _sharedPreferencesStore.read(_zhtlcSyncStartDateKey), + ), + walletExtras: walletExtras, + ); + } + + final hashKey = _passwordHashKey(sourceWallet); + final passwordHash = await _secureStorage.read(hashKey); + if (passwordHash != null) { + final isValid = await _passwordVerifier.verifySeedPassword( + password: password, + encodedHash: passwordHash, + ); + if (!isValid) { + throw const LegacyWalletMigrationException.incorrectPassword(); + } + } + + throw const LegacyWalletMigrationException.incorrectPassword(); + } on LegacyWalletMigrationException { + rethrow; + } on Object catch (error) { + throw LegacyWalletMigrationException.storageAccessError( + 'Failed to read legacy wallet secrets: $error', + ); + } + } + + /// Deletes legacy database rows and secure-storage keys for [wallet]. + Future deleteLegacyWalletData({ + required LegacyWalletRecord wallet, + required String password, + LegacyWalletSecrets? secrets, + }) async { + if (!isSupportedPlatform) { + return const LegacyWalletCleanupResult.skipped( + warningMessage: + 'Legacy native wallet cleanup skipped on unsupported platform.', + ); + } + + try { + final resolvedSecrets = + secrets ?? + await readWalletSecrets( + wallet: wallet, + password: password, + ); + + final criticalDeletionResult = await _deleteSecureStorageKeys( + resolvedSecrets.secureStorageKeysToDelete, + ); + final genericDeletionResult = await _deleteSecureStorageKeys( + resolvedSecrets.genericStorageKeysToDelete, + ); + final sharedPrefsDeletionResult = await _deleteSharedPreferencesKeys( + resolvedSecrets.sharedPreferencesKeysToDelete, + ); + + var metadataDeleted = false; + String? warningMessage; + final failedCriticalKeys = criticalDeletionResult.failedKeys; + if (failedCriticalKeys.isEmpty) { + try { + await _metadataStore.deleteWalletData( + walletId: resolvedSecrets.sourceWallet.walletId, + ); + metadataDeleted = true; + } on LegacyWalletMigrationException catch (error) { + warningMessage = error.message; + } + } else { + warningMessage = + 'Legacy secret cleanup incomplete. ' + 'The legacy wallet remains listed so cleanup can be retried.'; + } + + final deletedKeys = [ + ...criticalDeletionResult.deletedKeys, + ...genericDeletionResult.deletedKeys, + ]; + final failedKeys = [ + ...criticalDeletionResult.failedKeys, + ...genericDeletionResult.failedKeys, + ]; + final failedSharedPreferencesKeys = sharedPrefsDeletionResult.failedKeys; + + if (metadataDeleted && + failedKeys.isEmpty && + failedSharedPreferencesKeys.isEmpty) { + return LegacyWalletCleanupResult.complete( + metadataDeleted: true, + deletedSecureStorageKeys: deletedKeys, + ); + } + + return LegacyWalletCleanupResult.partial( + metadataDeleted: metadataDeleted, + deletedSecureStorageKeys: deletedKeys, + failedSecureStorageKeys: failedKeys, + warningMessage: + warningMessage ?? + (failedSharedPreferencesKeys.isNotEmpty + ? 'Legacy wallet cleanup incomplete. ' + 'Some legacy shared-preferences entries ' + 'could not be deleted.' + : 'Legacy wallet cleanup incomplete. ' + 'Some legacy secure-storage entries ' + 'could not be deleted.'), + ); + } on LegacyWalletMigrationException { + rethrow; + } on Object catch (error) { + throw LegacyWalletMigrationException.storageAccessError( + 'Failed to delete legacy wallet data: $error', + ); + } + } + + String _passwordHashKey(LegacyWalletRecord wallet) { + return '$_passwordPrefix$_seedPrefix${wallet.walletName}${wallet.walletId}'; + } + + _SecureStorageKeys _buildSecureStorageKeys({ + required LegacyWalletRecord wallet, + required String password, + }) { + final suffix = '${wallet.walletName}${wallet.walletId}'; + final seedKey = '$_seedPrefix$password$suffix'; + return _SecureStorageKeys( + seedKey: seedKey, + toDelete: [ + seedKey, + _passwordHashKey(wallet), + '$_pinPrefix$password$suffix', + '$_camoPinPrefix$password$suffix', + ], + ); + } + + Future> _listWallets() { + return _metadataStore.listWallets(); + } + + LegacyWalletRecord _findWalletById( + LegacyWalletRecord wallet, + List wallets, + ) { + for (final candidate in wallets) { + if (candidate.walletId == wallet.walletId) { + return candidate; + } + } + + throw LegacyWalletMigrationException.walletNotFound(wallet.walletId); + } + + bool _shouldDeleteGenericSessionKeys({ + required LegacyWalletRecord sourceWallet, + required List wallets, + }) { + return sourceWallet.isCurrentWallet || wallets.length == 1; + } + + Future<_KeyDeletionResult> _deleteSecureStorageKeys(List keys) async { + final deletedKeys = []; + final failedKeys = []; + + for (final key in keys) { + try { + await _secureStorage.delete(key); + deletedKeys.add(key); + } on Object { + failedKeys.add(key); + } + } + + return _KeyDeletionResult( + deletedKeys: deletedKeys, + failedKeys: failedKeys, + ); + } + + Future<_KeyDeletionResult> _deleteSharedPreferencesKeys( + List keys, + ) async { + final deletedKeys = []; + final failedKeys = []; + + for (final key in keys) { + try { + await _sharedPreferencesStore.delete(key); + deletedKeys.add(key); + } on Object { + failedKeys.add(key); + } + } + + return _KeyDeletionResult( + deletedKeys: deletedKeys, + failedKeys: failedKeys, + ); + } + + Future> _readRequestedZhtlcCoinIds(String walletId) async { + final rawValue = await _sharedPreferencesStore.read( + '$_zCoinActivationRequestedKeyPrefix$walletId', + ); + if (rawValue is List) { + return rawValue.whereType().toList(growable: false); + } + return const []; + } + + Future> _readWalletExtras( + LegacyWalletRecord sourceWallet, + ) async { + final extras = {...sourceWallet.walletExtras}; + final disallowScreenshot = await _sharedPreferencesStore.read( + _disallowScreenshotKey, + ); + if (disallowScreenshot is bool) { + extras['disallow_screenshot'] = disallowScreenshot; + } + return extras; + } + + DateTime? _parseLegacySyncStartDate(Object? rawValue) { + if (rawValue is String && rawValue.isNotEmpty) { + return DateTime.tryParse(rawValue)?.toUtc(); + } + return null; + } +} + +class _SecureStorageKeys { + const _SecureStorageKeys({required this.seedKey, required this.toDelete}); + + final String seedKey; + final List toDelete; +} + +class _KeyDeletionResult { + const _KeyDeletionResult({ + required this.deletedKeys, + required this.failedKeys, + }); + + final List deletedKeys; + final List failedKeys; } diff --git a/packages/komodo_legacy_wallet_migration/lib/src/models/legacy_wallet_cleanup_result.dart b/packages/komodo_legacy_wallet_migration/lib/src/models/legacy_wallet_cleanup_result.dart new file mode 100644 index 000000000..f2db63963 --- /dev/null +++ b/packages/komodo_legacy_wallet_migration/lib/src/models/legacy_wallet_cleanup_result.dart @@ -0,0 +1,76 @@ +import 'package:flutter/foundation.dart'; + +/// Cleanup status for legacy native wallet deletion. +enum LegacyWalletCleanupStatus { + /// All targeted legacy artifacts were deleted. + complete, + + /// Some legacy artifacts could not be deleted. + partial, + + /// Cleanup was skipped because the platform is unsupported. + skipped, +} + +/// Result of deleting legacy native wallet artifacts. +@immutable +class LegacyWalletCleanupResult { + /// Creates a cleanup result. + const LegacyWalletCleanupResult({ + required this.status, + required this.metadataDeleted, + this.deletedSecureStorageKeys = const [], + this.failedSecureStorageKeys = const [], + this.warningMessage, + }); + + /// Cleanup completed successfully. + const LegacyWalletCleanupResult.complete({ + required bool metadataDeleted, + required List deletedSecureStorageKeys, + }) : this( + status: LegacyWalletCleanupStatus.complete, + metadataDeleted: metadataDeleted, + deletedSecureStorageKeys: deletedSecureStorageKeys, + ); + + /// Cleanup completed only partially. + const LegacyWalletCleanupResult.partial({ + required bool metadataDeleted, + required List deletedSecureStorageKeys, + required List failedSecureStorageKeys, + required String warningMessage, + }) : this( + status: LegacyWalletCleanupStatus.partial, + metadataDeleted: metadataDeleted, + deletedSecureStorageKeys: deletedSecureStorageKeys, + failedSecureStorageKeys: failedSecureStorageKeys, + warningMessage: warningMessage, + ); + + /// Cleanup was skipped. + const LegacyWalletCleanupResult.skipped({String? warningMessage}) + : this( + status: LegacyWalletCleanupStatus.skipped, + metadataDeleted: false, + warningMessage: warningMessage, + ); + + /// Structured status for the cleanup operation. + final LegacyWalletCleanupStatus status; + + /// Whether legacy database rows were deleted. + final bool metadataDeleted; + + /// Secure-storage keys successfully deleted. + final List deletedSecureStorageKeys; + + /// Secure-storage keys that failed deletion. + final List failedSecureStorageKeys; + + /// Optional warning message for partial or skipped cleanup. + final String? warningMessage; + + /// Whether cleanup completed without any warnings or failures. + bool get isComplete => status == LegacyWalletCleanupStatus.complete; +} diff --git a/packages/komodo_legacy_wallet_migration/lib/src/models/legacy_wallet_migration_exception.dart b/packages/komodo_legacy_wallet_migration/lib/src/models/legacy_wallet_migration_exception.dart new file mode 100644 index 000000000..d0d7aee92 --- /dev/null +++ b/packages/komodo_legacy_wallet_migration/lib/src/models/legacy_wallet_migration_exception.dart @@ -0,0 +1,68 @@ +import 'package:flutter/foundation.dart'; + +/// Error types produced by legacy native wallet migration. +enum LegacyWalletMigrationExceptionType { + /// The runtime cannot access the legacy native wallet storage layout. + unsupportedPlatform, + + /// The requested legacy wallet could not be found in storage. + walletNotFound, + + /// The provided password did not match the legacy wallet secrets. + incorrectPassword, + + /// A storage access operation failed. + storageAccessError, +} + +/// Exception thrown by the legacy native wallet migration service. +@immutable +class LegacyWalletMigrationException implements Exception { + /// Creates a legacy wallet migration exception. + const LegacyWalletMigrationException( + this.message, { + required this.type, + }); + + /// Creates an unsupported-platform error. + const LegacyWalletMigrationException.unsupportedPlatform() + : this( + 'Legacy native wallet migration is not supported on this platform.', + type: LegacyWalletMigrationExceptionType.unsupportedPlatform, + ); + + /// Creates a wallet-not-found error for [walletId]. + const LegacyWalletMigrationException.walletNotFound(String walletId) + : this( + 'Legacy wallet not found: $walletId', + type: LegacyWalletMigrationExceptionType.walletNotFound, + ); + + /// Creates an incorrect-password error. + const LegacyWalletMigrationException.incorrectPassword() + : this( + 'Incorrect legacy wallet password.', + type: LegacyWalletMigrationExceptionType.incorrectPassword, + ); + + /// Creates a storage-access error. + const LegacyWalletMigrationException.storageAccessError([String? message]) + : this( + message ?? 'Failed to access legacy wallet storage.', + type: LegacyWalletMigrationExceptionType.storageAccessError, + ); + + /// Human-readable error message. + final String message; + + /// Structured error type for callers that need specific handling. + final LegacyWalletMigrationExceptionType type; + + @override + String toString() { + return 'LegacyWalletMigrationException(' + 'type: $type, ' + 'message: $message' + ')'; + } +} diff --git a/packages/komodo_legacy_wallet_migration/lib/src/models/legacy_wallet_record.dart b/packages/komodo_legacy_wallet_migration/lib/src/models/legacy_wallet_record.dart new file mode 100644 index 000000000..8156526a5 --- /dev/null +++ b/packages/komodo_legacy_wallet_migration/lib/src/models/legacy_wallet_record.dart @@ -0,0 +1,65 @@ +import 'package:flutter/foundation.dart'; + +/// Metadata describing a legacy native wallet discovered on device. +@immutable +class LegacyWalletRecord { + /// Creates a legacy wallet record. + const LegacyWalletRecord({ + required this.walletId, + required this.walletName, + this.activatedCoins = const [], + this.isCurrentWallet = false, + this.walletExtras = const {}, + }); + + /// Stable legacy wallet identifier from the native wallet database. + final String walletId; + + /// Original legacy wallet name used for secure-storage key lookup. + final String walletName; + + /// Activated coin IDs stored for this legacy wallet. + final List activatedCoins; + + /// Whether this wallet is marked as the current wallet in legacy storage. + final bool isCurrentWallet; + + /// Preserved legacy wallet-scoped extras from native DB/shared prefs state. + final Map walletExtras; + + @override + bool operator ==(Object other) { + if (identical(this, other)) { + return true; + } + + return other is LegacyWalletRecord && + other.walletId == walletId && + other.walletName == walletName && + listEquals(other.activatedCoins, activatedCoins) && + other.isCurrentWallet == isCurrentWallet && + mapEquals(other.walletExtras, walletExtras); + } + + @override + int get hashCode => Object.hash( + walletId, + walletName, + Object.hashAll(activatedCoins), + isCurrentWallet, + Object.hashAllUnordered( + walletExtras.entries.map((entry) => Object.hash(entry.key, entry.value)), + ), + ); + + @override + String toString() { + return 'LegacyWalletRecord(' + 'walletId: $walletId, ' + 'walletName: $walletName, ' + 'activatedCoins: $activatedCoins, ' + 'isCurrentWallet: $isCurrentWallet, ' + 'walletExtras: $walletExtras' + ')'; + } +} diff --git a/packages/komodo_legacy_wallet_migration/lib/src/models/legacy_wallet_secrets.dart b/packages/komodo_legacy_wallet_migration/lib/src/models/legacy_wallet_secrets.dart new file mode 100644 index 000000000..b95ec6902 --- /dev/null +++ b/packages/komodo_legacy_wallet_migration/lib/src/models/legacy_wallet_secrets.dart @@ -0,0 +1,103 @@ +import 'package:flutter/foundation.dart'; +import 'package:komodo_legacy_wallet_migration/src/models/legacy_wallet_record.dart'; + +/// Secrets read from a legacy native wallet during migration. +@immutable +class LegacyWalletSecrets { + /// Creates a legacy wallet secrets payload. + const LegacyWalletSecrets({ + required this.sourceWallet, + required this.seedPhrase, + required this.secureStorageKeysToDelete, + this.genericStorageKeysToDelete = const [], + this.sharedPreferencesKeysToDelete = const [], + this.requestedZhtlcCoinIds = const [], + this.legacyZhtlcSyncType, + this.legacyZhtlcSyncStartDate, + this.walletExtras = const {}, + }); + + /// Legacy wallet resolved from the native metadata store. + final LegacyWalletRecord sourceWallet; + + /// Plaintext seed phrase read directly from legacy secure storage. + final String seedPhrase; + + /// Per-wallet secure-storage keys that should be deleted after import. + final List secureStorageKeysToDelete; + + /// Generic session keys that can be deleted on a best-effort basis. + final List genericStorageKeysToDelete; + + /// Shared-preferences keys that can be deleted after import. + final List sharedPreferencesKeysToDelete; + + /// Requested but not yet fully activated legacy ZHTLC coin ids. + final List requestedZhtlcCoinIds; + + /// Raw legacy ZHTLC sync type stored by the native app. + final String? legacyZhtlcSyncType; + + /// Raw legacy ZHTLC sync start date stored by the native app. + final DateTime? legacyZhtlcSyncStartDate; + + /// Preserved legacy wallet-scoped extras. + final Map walletExtras; + + @override + bool operator ==(Object other) { + if (identical(this, other)) { + return true; + } + + return other is LegacyWalletSecrets && + other.sourceWallet == sourceWallet && + other.seedPhrase == seedPhrase && + listEquals( + other.secureStorageKeysToDelete, + secureStorageKeysToDelete, + ) && + listEquals( + other.genericStorageKeysToDelete, + genericStorageKeysToDelete, + ) && + listEquals( + other.sharedPreferencesKeysToDelete, + sharedPreferencesKeysToDelete, + ) && + listEquals(other.requestedZhtlcCoinIds, requestedZhtlcCoinIds) && + other.legacyZhtlcSyncType == legacyZhtlcSyncType && + other.legacyZhtlcSyncStartDate == legacyZhtlcSyncStartDate && + mapEquals(other.walletExtras, walletExtras); + } + + @override + int get hashCode => Object.hash( + sourceWallet, + seedPhrase, + Object.hashAll(secureStorageKeysToDelete), + Object.hashAll(genericStorageKeysToDelete), + Object.hashAll(sharedPreferencesKeysToDelete), + Object.hashAll(requestedZhtlcCoinIds), + legacyZhtlcSyncType, + legacyZhtlcSyncStartDate, + Object.hashAllUnordered( + walletExtras.entries.map((entry) => Object.hash(entry.key, entry.value)), + ), + ); + + @override + String toString() { + return 'LegacyWalletSecrets(' + 'sourceWallet: $sourceWallet, ' + 'seedPhrase: [redacted], ' + 'secureStorageKeysToDelete: $secureStorageKeysToDelete, ' + 'genericStorageKeysToDelete: $genericStorageKeysToDelete, ' + 'sharedPreferencesKeysToDelete: $sharedPreferencesKeysToDelete, ' + 'requestedZhtlcCoinIds: $requestedZhtlcCoinIds, ' + 'legacyZhtlcSyncType: $legacyZhtlcSyncType, ' + 'legacyZhtlcSyncStartDate: $legacyZhtlcSyncStartDate, ' + 'walletExtras: $walletExtras' + ')'; + } +} diff --git a/packages/komodo_legacy_wallet_migration/test/src/komodo_legacy_wallet_migration_test.dart b/packages/komodo_legacy_wallet_migration/test/src/komodo_legacy_wallet_migration_test.dart index cefa20cb8..a90eaa9d3 100644 --- a/packages/komodo_legacy_wallet_migration/test/src/komodo_legacy_wallet_migration_test.dart +++ b/packages/komodo_legacy_wallet_migration/test/src/komodo_legacy_wallet_migration_test.dart @@ -1,13 +1,533 @@ -// Not required for test files -// ignore_for_file: prefer_const_constructors - import 'package:flutter_test/flutter_test.dart'; import 'package:komodo_legacy_wallet_migration/komodo_legacy_wallet_migration.dart'; +import 'package:komodo_legacy_wallet_migration/src/adapters/legacy_password_verifier.dart'; +import 'package:komodo_legacy_wallet_migration/src/adapters/legacy_secure_storage.dart'; +import 'package:komodo_legacy_wallet_migration/src/adapters/legacy_shared_preferences_store.dart'; +import 'package:komodo_legacy_wallet_migration/src/adapters/legacy_wallet_metadata_store.dart'; +import 'package:komodo_legacy_wallet_migration/src/adapters/legacy_wallet_platform.dart'; void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + group('KomodoLegacyWalletMigration', () { - test('can be instantiated', () { - expect(KomodoLegacyWalletMigration(), isNotNull); + late _FakeMetadataStore metadataStore; + late _FakeSecureStorage secureStorage; + late _FakeSharedPreferencesStore sharedPreferencesStore; + late _FakePasswordVerifier passwordVerifier; + late _FakePlatform platform; + late KomodoLegacyWalletMigration migration; + + const storedWallet = LegacyWalletRecord( + walletId: 'wallet-1', + walletName: 'Legacy Wallet', + activatedCoins: ['KMD', 'BTC'], + isCurrentWallet: true, + ); + + setUp(() { + metadataStore = _FakeMetadataStore( + wallets: [storedWallet], + ); + secureStorage = _FakeSecureStorage(); + sharedPreferencesStore = _FakeSharedPreferencesStore(); + passwordVerifier = _FakePasswordVerifier(isValid: true); + platform = const _FakePlatform(isSupportedPlatform: true); + migration = KomodoLegacyWalletMigration( + metadataStore: metadataStore, + secureStorage: secureStorage, + sharedPreferencesStore: sharedPreferencesStore, + passwordVerifier: passwordVerifier, + platform: platform, + ); }); + + test( + 'listLegacyWallets returns metadata store wallets on supported platforms', + () async { + final wallets = await migration.listLegacyWallets(); + + expect(wallets, [storedWallet]); + expect(metadataStore.listWalletsCalls, 1); + }, + ); + + test( + 'listLegacyWallets returns empty list on unsupported platforms', + () async { + migration = KomodoLegacyWalletMigration( + metadataStore: metadataStore, + secureStorage: secureStorage, + sharedPreferencesStore: sharedPreferencesStore, + passwordVerifier: passwordVerifier, + platform: const _FakePlatform(isSupportedPlatform: false), + ); + + final wallets = await migration.listLegacyWallets(); + + expect(wallets, isEmpty); + expect(metadataStore.listWalletsCalls, 0); + }, + ); + + test( + 'readWalletSecrets prefers direct seed lookup and returns plaintext seed', + () async { + secureStorage.values.addAll({ + 'passwordKeyEncryption.SEEDLegacy Walletwallet-1': 'hash-value', + 'KeyEncryption.SEEDsecretLegacy Walletwallet-1': 'word1 word2 word3', + }); + + final secrets = await migration.readWalletSecrets( + wallet: storedWallet, + password: 'secret', + ); + + expect(passwordVerifier.passwordsChecked, isEmpty); + expect(passwordVerifier.hashesChecked, isEmpty); + expect( + secureStorage.readCalls, + ['KeyEncryption.SEEDsecretLegacy Walletwallet-1'], + ); + expect(secrets.seedPhrase, 'word1 word2 word3'); + expect(secrets.sourceWallet, storedWallet); + expect( + secrets.secureStorageKeysToDelete, + [ + 'KeyEncryption.SEEDsecretLegacy Walletwallet-1', + 'passwordKeyEncryption.SEEDLegacy Walletwallet-1', + 'KeyEncryption.PINsecretLegacy Walletwallet-1', + 'KeyEncryption.CAMOPINsecretLegacy Walletwallet-1', + 'wallet-1_task_id_KMD', + 'wallet-1_task_id_BTC', + ], + ); + expect( + secrets.genericStorageKeysToDelete, + ['pin', 'camoPin', 'passphrase'], + ); + }, + ); + + test( + 'readWalletSecrets returns the seed even if the optional verifier fails', + () async { + secureStorage.values.addAll({ + 'passwordKeyEncryption.SEEDLegacy Walletwallet-1': 'hash-value', + 'KeyEncryption.SEEDsecretLegacy Walletwallet-1': 'word1 word2 word3', + }); + passwordVerifier.isValid = false; + + final secrets = await migration.readWalletSecrets( + wallet: storedWallet, + password: 'secret', + ); + + expect(secrets.seedPhrase, 'word1 word2 word3'); + expect(passwordVerifier.passwordsChecked, isEmpty); + expect( + secureStorage.readCalls, + ['KeyEncryption.SEEDsecretLegacy Walletwallet-1'], + ); + }, + ); + + test( + 'readWalletSecrets rejects invalid passwords after the seed key misses', + () async { + secureStorage + .values['passwordKeyEncryption.SEEDLegacy Walletwallet-1'] = + 'hash-value'; + passwordVerifier.isValid = false; + + await expectLater( + () => migration.readWalletSecrets( + wallet: storedWallet, + password: 'wrong', + ), + throwsA( + isA().having( + (error) => error.type, + 'type', + LegacyWalletMigrationExceptionType.incorrectPassword, + ), + ), + ); + + expect( + secureStorage.readCalls, + [ + 'KeyEncryption.SEEDwrongLegacy Walletwallet-1', + 'passwordKeyEncryption.SEEDLegacy Walletwallet-1', + ], + ); + }, + ); + + test( + 'readWalletSecrets treats missing seed data key as incorrect password', + () async { + secureStorage + .values['passwordKeyEncryption.SEEDLegacy Walletwallet-1'] = + 'hash-value'; + + await expectLater( + () => migration.readWalletSecrets( + wallet: storedWallet, + password: 'wrong', + ), + throwsA( + isA().having( + (error) => error.type, + 'type', + LegacyWalletMigrationExceptionType.incorrectPassword, + ), + ), + ); + + expect( + secureStorage.readCalls, + [ + 'KeyEncryption.SEEDwrongLegacy Walletwallet-1', + 'passwordKeyEncryption.SEEDLegacy Walletwallet-1', + ], + ); + expect(passwordVerifier.passwordsChecked, ['wrong']); + expect(passwordVerifier.hashesChecked, ['hash-value']); + }, + ); + + test( + 'readWalletSecrets resolves the original wallet name by wallet id', + () async { + secureStorage + .values['passwordKeyEncryption.SEEDLegacy Walletwallet-1'] = + 'hash-value'; + secureStorage.values['KeyEncryption.SEEDsecretLegacy Walletwallet-1'] = + 'seed words'; + + final secrets = await migration.readWalletSecrets( + wallet: const LegacyWalletRecord( + walletId: 'wallet-1', + walletName: 'Sanitized_Name', + ), + password: 'secret', + ); + + expect(secrets.seedPhrase, 'seed words'); + expect( + secureStorage.readCalls.last, + 'KeyEncryption.SEEDsecretLegacy Walletwallet-1', + ); + }, + ); + + test( + 'readWalletSecrets includes legacy special-case state and cleanup keys', + () async { + const zhtlcWallet = LegacyWalletRecord( + walletId: 'wallet-z', + walletName: 'Z Wallet', + activatedCoins: ['ARRR'], + isCurrentWallet: true, + walletExtras: { + 'activate_pin_protection': true, + 'enable_camo': true, + }, + ); + metadataStore = _FakeMetadataStore( + wallets: const [zhtlcWallet], + ); + migration = KomodoLegacyWalletMigration( + metadataStore: metadataStore, + secureStorage: secureStorage, + sharedPreferencesStore: sharedPreferencesStore, + passwordVerifier: passwordVerifier, + platform: platform, + ); + secureStorage.values.addAll({ + 'passwordKeyEncryption.SEEDZ Walletwallet-z': 'hash-value', + 'KeyEncryption.SEEDsecretZ Walletwallet-z': 'seed words', + }); + sharedPreferencesStore.values.addAll({ + 'z-coin-activation-requested-wallet-z': ['ARRR'], + 'zhtlcSyncType': 'specifiedDate', + 'zhtlcSyncStartDate': '2025-01-02T03:04:05.000Z', + 'disallowScreenshot': true, + }); + + final secrets = await migration.readWalletSecrets( + wallet: zhtlcWallet, + password: 'secret', + ); + + expect(secrets.requestedZhtlcCoinIds, ['ARRR']); + expect(secrets.legacyZhtlcSyncType, 'specifiedDate'); + expect( + secrets.legacyZhtlcSyncStartDate, + DateTime.parse('2025-01-02T03:04:05.000Z').toUtc(), + ); + expect( + secrets.walletExtras, + { + 'activate_pin_protection': true, + 'enable_camo': true, + 'disallow_screenshot': true, + }, + ); + expect( + secrets.sharedPreferencesKeysToDelete, + ['z-coin-activation-requested-wallet-z'], + ); + expect( + secrets.secureStorageKeysToDelete, + contains('wallet-z_task_id_ARRR'), + ); + }, + ); + + test( + 'deleteLegacyWalletData removes DB rows and secure storage keys', + () async { + secureStorage.values.addAll({ + 'passwordKeyEncryption.SEEDLegacy Walletwallet-1': 'hash-value', + 'KeyEncryption.SEEDsecretLegacy Walletwallet-1': 'seed words', + }); + + final result = await migration.deleteLegacyWalletData( + wallet: storedWallet, + password: 'secret', + ); + + expect(result.isComplete, isTrue); + expect(metadataStore.deletedWalletIds, ['wallet-1']); + expect( + secureStorage.deleteCalls, + [ + 'KeyEncryption.SEEDsecretLegacy Walletwallet-1', + 'passwordKeyEncryption.SEEDLegacy Walletwallet-1', + 'KeyEncryption.PINsecretLegacy Walletwallet-1', + 'KeyEncryption.CAMOPINsecretLegacy Walletwallet-1', + 'wallet-1_task_id_KMD', + 'wallet-1_task_id_BTC', + 'pin', + 'camoPin', + 'passphrase', + ], + ); + }, + ); + + test( + 'deleteLegacyWalletData uses provided secrets without re-reading storage', + () async { + const secrets = LegacyWalletSecrets( + sourceWallet: storedWallet, + seedPhrase: 'seed words', + secureStorageKeysToDelete: [ + 'KeyEncryption.SEEDsecretLegacy Walletwallet-1', + ], + ); + + final result = await migration.deleteLegacyWalletData( + wallet: storedWallet, + password: 'secret', + secrets: secrets, + ); + + expect(result.isComplete, isTrue); + expect(secureStorage.readCalls, isEmpty); + expect(metadataStore.deletedWalletIds, ['wallet-1']); + }, + ); + + test( + 'deleteLegacyWalletData returns partial and keeps metadata when a ' + 'critical key fails deletion', + () async { + secureStorage.values.addAll({ + 'passwordKeyEncryption.SEEDLegacy Walletwallet-1': 'hash-value', + 'KeyEncryption.SEEDsecretLegacy Walletwallet-1': 'seed words', + }); + secureStorage.keysThatFailDeletion.add( + 'KeyEncryption.SEEDsecretLegacy Walletwallet-1', + ); + + final result = await migration.deleteLegacyWalletData( + wallet: storedWallet, + password: 'secret', + ); + + expect(result.status, LegacyWalletCleanupStatus.partial); + expect(result.metadataDeleted, isFalse); + expect(metadataStore.deletedWalletIds, isEmpty); + expect( + result.failedSecureStorageKeys, + ['KeyEncryption.SEEDsecretLegacy Walletwallet-1'], + ); + }, + ); + + test( + 'deleteLegacyWalletData returns partial when generic key deletion fails ' + 'after metadata cleanup', + () async { + secureStorage.values.addAll({ + 'passwordKeyEncryption.SEEDLegacy Walletwallet-1': 'hash-value', + 'KeyEncryption.SEEDsecretLegacy Walletwallet-1': 'seed words', + }); + secureStorage.keysThatFailDeletion.add('passphrase'); + + final result = await migration.deleteLegacyWalletData( + wallet: storedWallet, + password: 'secret', + ); + + expect(result.status, LegacyWalletCleanupStatus.partial); + expect(result.metadataDeleted, isTrue); + expect(metadataStore.deletedWalletIds, ['wallet-1']); + expect(result.failedSecureStorageKeys, ['passphrase']); + }, + ); + + test( + 'deleteLegacyWalletData returns partial when shared-preferences cleanup ' + 'fails after metadata cleanup', + () async { + secureStorage.values.addAll({ + 'passwordKeyEncryption.SEEDLegacy Walletwallet-1': 'hash-value', + 'KeyEncryption.SEEDsecretLegacy Walletwallet-1': 'seed words', + }); + sharedPreferencesStore.values['z-coin-activation-requested-wallet-1'] = + ['ARRR']; + sharedPreferencesStore.keysThatFailDeletion.add( + 'z-coin-activation-requested-wallet-1', + ); + + final result = await migration.deleteLegacyWalletData( + wallet: storedWallet, + password: 'secret', + ); + + expect(result.status, LegacyWalletCleanupStatus.partial); + expect(result.metadataDeleted, isTrue); + expect(result.warningMessage, contains('shared-preferences')); + }, + ); + + test( + 'readWalletSecrets omits generic session keys for a non-current wallet ' + 'when others remain', + () async { + const otherWallet = LegacyWalletRecord( + walletId: 'wallet-2', + walletName: 'Other Wallet', + activatedCoins: ['ETH'], + ); + metadataStore = _FakeMetadataStore( + wallets: const [storedWallet, otherWallet], + ); + migration = KomodoLegacyWalletMigration( + metadataStore: metadataStore, + secureStorage: secureStorage, + sharedPreferencesStore: sharedPreferencesStore, + passwordVerifier: passwordVerifier, + platform: platform, + ); + secureStorage.values.addAll({ + 'passwordKeyEncryption.SEEDOther Walletwallet-2': 'hash-value', + 'KeyEncryption.SEEDsecretOther Walletwallet-2': 'seed words', + }); + + final secrets = await migration.readWalletSecrets( + wallet: otherWallet, + password: 'secret', + ); + + expect(secrets.genericStorageKeysToDelete, isEmpty); + }, + ); }); } + +class _FakeMetadataStore implements LegacyWalletMetadataStore { + _FakeMetadataStore({required this.wallets}); + + final List wallets; + int listWalletsCalls = 0; + final List deletedWalletIds = []; + + @override + Future deleteWalletData({required String walletId}) async { + deletedWalletIds.add(walletId); + } + + @override + Future> listWallets() async { + listWalletsCalls += 1; + return wallets; + } +} + +class _FakeSecureStorage implements LegacySecureStorage { + final Map values = {}; + final List readCalls = []; + final List deleteCalls = []; + final Set keysThatFailDeletion = {}; + + @override + Future delete(String key) async { + if (keysThatFailDeletion.contains(key)) { + throw StateError('delete failed for $key'); + } + deleteCalls.add(key); + } + + @override + Future read(String key) async { + readCalls.add(key); + return values[key]; + } +} + +class _FakePasswordVerifier implements LegacyPasswordVerifier { + _FakePasswordVerifier({required this.isValid}); + + bool isValid; + final List passwordsChecked = []; + final List hashesChecked = []; + + @override + Future verifySeedPassword({ + required String password, + required String encodedHash, + }) async { + passwordsChecked.add(password); + hashesChecked.add(encodedHash); + return isValid; + } +} + +class _FakePlatform implements LegacyWalletPlatform { + const _FakePlatform({required this.isSupportedPlatform}); + + @override + final bool isSupportedPlatform; +} + +class _FakeSharedPreferencesStore implements LegacySharedPreferencesStore { + final Map values = {}; + final Set keysThatFailDeletion = {}; + final List deleteCalls = []; + + @override + Future delete(String key) async { + if (keysThatFailDeletion.contains(key)) { + throw StateError('delete failed for $key'); + } + deleteCalls.add(key); + values.remove(key); + } + + @override + Future read(String key) async => values[key]; +} From 4da15a65991eed6962a1ab5c2ae74d37bf13b65c Mon Sep 17 00:00:00 2001 From: CharlVS <77973576+CharlVS@users.noreply.github.com> Date: Mon, 13 Apr 2026 11:13:44 +0200 Subject: [PATCH 36/40] fix(migration): replace dargon2_flutter with pointycastle for WASM compat dargon2_flutter's web plugin uses JS interop APIs incompatible with Flutter WASM builds. Replaced with pointycastle's pure-Dart Argon2 implementation which works on all platforms. Removes 7 transitive dargon2 packages from the dependency tree. --- .../adapters/legacy_password_verifier.dart | 120 +++++++++++++++++- .../pubspec.yaml | 2 +- 2 files changed, 116 insertions(+), 6 deletions(-) diff --git a/packages/komodo_legacy_wallet_migration/lib/src/adapters/legacy_password_verifier.dart b/packages/komodo_legacy_wallet_migration/lib/src/adapters/legacy_password_verifier.dart index 23b09e33a..65ec84ff4 100644 --- a/packages/komodo_legacy_wallet_migration/lib/src/adapters/legacy_password_verifier.dart +++ b/packages/komodo_legacy_wallet_migration/lib/src/adapters/legacy_password_verifier.dart @@ -1,4 +1,8 @@ -import 'package:dargon2_flutter/dargon2_flutter.dart'; +import 'dart:convert'; +import 'dart:typed_data'; + +import 'package:pointycastle/key_derivators/api.dart'; +import 'package:pointycastle/key_derivators/argon2.dart'; /// Verifies a legacy native wallet password against the stored seed hash. // ignore: one_member_abstracts @@ -11,6 +15,9 @@ abstract interface class LegacyPasswordVerifier { } /// Argon2id-based verifier for legacy native wallet seed passwords. +/// +/// Uses pointycastle's pure-Dart Argon2 implementation, which is compatible +/// with all Dart platforms including Flutter Web WASM. class Argon2LegacyPasswordVerifier implements LegacyPasswordVerifier { /// Creates an Argon2-based verifier. const Argon2LegacyPasswordVerifier(); @@ -21,13 +28,116 @@ class Argon2LegacyPasswordVerifier implements LegacyPasswordVerifier { required String encodedHash, }) async { try { - return await argon2.verifyHashString( - password, - encodedHash, - type: Argon2Type.id, + final parsed = _parsePhcEncodedHash(encodedHash); + if (parsed == null) return false; + + final params = Argon2Parameters( + parsed.type, + parsed.salt, + desiredKeyLength: parsed.hash.length, + iterations: parsed.timeCost, + memory: parsed.memoryCost, + lanes: parsed.parallelism, + version: parsed.version, + ); + + final generator = Argon2BytesGenerator()..init(params); + final derived = generator.process( + Uint8List.fromList(utf8.encode(password)), ); + + return _constantTimeEquals(derived, parsed.hash); } on Object { return false; } } + + /// Parses the PHC string format used by Argon2 reference implementations. + /// + /// Format: + /// `$argon2$v=$m=,t=