diff --git a/CHANGELOG.md b/CHANGELOG.md index 0b664a5ea..84d6b9fcb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,105 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## 2026-05-02 + +### Changes + +--- + +Packages with breaking changes: + + - [`komodo_defi_rpc_methods` - `v0.5.0`](#komodo_defi_rpc_methods---v050) + - [`komodo_defi_sdk` - `v0.6.0`](#komodo_defi_sdk---v060) + +Packages with other changes: + + - [`komodo_cex_market_data` - `v0.1.0+1`](#komodo_cex_market_data---v0101) + - [`komodo_defi_framework` - `v0.4.1`](#komodo_defi_framework---v041) + - [`komodo_defi_local_auth` - `v0.4.1`](#komodo_defi_local_auth---v041) + - [`komodo_defi_types` - `v0.4.1`](#komodo_defi_types---v041) + - [`komodo_legacy_wallet_migration` - `v0.1.0`](#komodo_legacy_wallet_migration---v010) + - [`komodo_ui` - `v0.3.2`](#komodo_ui---v032) + - [`komodo_wallet_build_transformer` - `v0.4.2`](#komodo_wallet_build_transformer---v042) + - [`komodo_wallet_cli` - `v0.5.1`](#komodo_wallet_cli---v051) + - [`komodo_coins` - `v0.3.2+1`](#komodo_coins---v0321) + - [`komodo_coin_updates` - `v2.0.1`](#komodo_coin_updates---v201) + +Packages with dependency updates only: + +> Packages listed below depend on other packages in this workspace that have had changes. Their versions have been incremented to bump the minimum dependency versions of the packages they depend upon in this project. + + - `komodo_coins` - `v0.3.2+1` + - `komodo_coin_updates` - `v2.0.1` + +--- + +#### `komodo_defi_rpc_methods` - `v0.5.0` + + - **FIX**(errors): preserve RPC method hints when parsing ambiguous KDF error responses (#342). + - **FIX**(models): accept numeric JSON values encoded as either `int` or `num` across RPC models (#336). + - **FEAT**(auth): add the RPC request and activation parameter support needed by legacy wallet migration. + - **BREAKING** **FEAT**(sia): move SIA withdrawal handling onto hardened SIA-specific RPC models and namespace methods (#343). + +#### `komodo_defi_sdk` - `v0.6.0` + + - **FIX**(activation): restore coordinated TRX activation and market-data lookup (#340). + - **FIX**(explorers): support TRON explorer URL templates in SDK transaction flows (#338). + - **FIX**(market-data): keep last-known spot prices available while rotating cache snapshots (#335). + - **FEAT**(migration): add SDK integration for legacy wallet discovery, verification, import, and cleanup. + - **FEAT**(balances): add balance recovery mode and richer fee information plumbing (#341). + - **FEAT**(transaction-history): add a Tronscan strategy with address, cursor, and fixed-scale amount codecs (#339). + - **BREAKING** **FEAT**(sia): route SIA activation and withdrawals through the hardened SIA strategy and RPC namespace (#343). + +#### `komodo_legacy_wallet_migration` - `v0.1.0` + + - **FEAT**(migration): add legacy wallet discovery, metadata parsing, password verification, import, and cleanup utilities. + - **FIX**(migration): use a PointyCastle-based Argon2 verifier for WASM compatibility. + - **FIX**(migration): guard unsupported platforms and wait for KDF RPC readiness before migration work. + +#### `komodo_cex_market_data` - `v0.1.0+1` + + - **FIX**(coingecko): add a failure cooldown to avoid repeated failing requests (#346). + - **FIX**(tron): restore TRX market-data ID resolution and repository fallback behaviour (#340). + - **FIX**(models): accept numeric API values encoded as either `int` or `num` (#336). + +#### `komodo_defi_framework` - `v0.4.1` + + - **CHORE**(build): update bundled KDF to staging commit `52ba4f9` and use the TRON coins source for release builds. + - **FIX**(config): carry TRON explorer URL support through bundled build configuration (#338). + - **FIX**(web): harden numeric JS interop parsing for KDF responses (#336). + - **FEAT**(migration): expose the framework hooks needed for legacy wallet migration. + - **FEAT**(build): align build configuration with the balance recovery and fee-info release inputs (#341). + +#### `komodo_defi_local_auth` - `v0.4.1` + + - **FIX**(auth,migration): wait for KDF RPC readiness and guard unsupported platforms during migration. + - **FEAT**(migration): add local-auth integration for legacy wallet verification and import flows. + +#### `komodo_defi_types` - `v0.4.1` + + - **FIX**(tron): support TRON explorer URL templates and correct TRC20 badge classification (#338, #344). + - **FIX**(models): accept numeric JSON values encoded as either `int` or `num` (#336). + - **FEAT**(migration): add auth error and wallet metadata types used by legacy wallet migration. + - **FEAT**(fees): expose richer fee information for balance recovery flows (#341). + - **FEAT**(transaction-history): add strategy metadata needed by the Tronscan history provider (#339). + +#### `komodo_ui` - `v0.3.2` + + - **FIX**(asset-icons): avoid duplicate icon precache requests (#345). + - **FIX**(asset-icons): show the correct TRC20 chain badge (#344). + - **FEAT**(fees): display richer fee information from SDK balance recovery flows (#341). + +#### `komodo_wallet_build_transformer` - `v0.4.2` + + - **FIX**(github): accept numeric GitHub API values encoded as either `int` or `num` (#336). + - **FEAT**(build): support the build inputs needed by balance recovery and fee-info updates (#341). + +#### `komodo_wallet_cli` - `v0.5.1` + + - **FEAT**(build): update API config tooling for the balance recovery and fee-info release inputs (#341). + + ## 2026-03-23 ### Changes diff --git a/packages/komodo_cex_market_data/CHANGELOG.md b/packages/komodo_cex_market_data/CHANGELOG.md index 6f1b9ca12..e292afb1c 100644 --- a/packages/komodo_cex_market_data/CHANGELOG.md +++ b/packages/komodo_cex_market_data/CHANGELOG.md @@ -1,3 +1,9 @@ +## 0.1.0+1 + + - **FIX**(coingecko): add a failure cooldown to avoid repeated failing requests (#346). + - **FIX**(tron): restore TRX market-data ID resolution and repository fallback behaviour (#340). + - **FIX**(models): accept numeric API values encoded as either `int` or `num` (#336). + ## 0.1.0 > Note: This release has breaking changes. diff --git a/packages/komodo_cex_market_data/pubspec.yaml b/packages/komodo_cex_market_data/pubspec.yaml index ecc411c92..3971efff0 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.1.0 +version: 0.1.0+1 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.4.0 + komodo_defi_types: ^0.4.1 # 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 3df80de26..b8b274590 100644 --- a/packages/komodo_coin_updates/CHANGELOG.md +++ b/packages/komodo_coin_updates/CHANGELOG.md @@ -1,3 +1,7 @@ +## 2.0.1 + + - Update a dependency to the latest release. + ## 2.0.0 > Note: This release has breaking changes. diff --git a/packages/komodo_coin_updates/docs/README.md b/packages/komodo_coin_updates/doc/README.md similarity index 100% rename from packages/komodo_coin_updates/docs/README.md rename to packages/komodo_coin_updates/doc/README.md diff --git a/packages/komodo_coin_updates/docs/advanced.md b/packages/komodo_coin_updates/doc/advanced.md similarity index 100% rename from packages/komodo_coin_updates/docs/advanced.md rename to packages/komodo_coin_updates/doc/advanced.md diff --git a/packages/komodo_coin_updates/docs/api.md b/packages/komodo_coin_updates/doc/api.md similarity index 100% rename from packages/komodo_coin_updates/docs/api.md rename to packages/komodo_coin_updates/doc/api.md diff --git a/packages/komodo_coin_updates/docs/build-and-dev.md b/packages/komodo_coin_updates/doc/build-and-dev.md similarity index 100% rename from packages/komodo_coin_updates/docs/build-and-dev.md rename to packages/komodo_coin_updates/doc/build-and-dev.md diff --git a/packages/komodo_coin_updates/docs/configuration.md b/packages/komodo_coin_updates/doc/configuration.md similarity index 100% rename from packages/komodo_coin_updates/docs/configuration.md rename to packages/komodo_coin_updates/doc/configuration.md diff --git a/packages/komodo_coin_updates/docs/faq.md b/packages/komodo_coin_updates/doc/faq.md similarity index 100% rename from packages/komodo_coin_updates/docs/faq.md rename to packages/komodo_coin_updates/doc/faq.md diff --git a/packages/komodo_coin_updates/docs/getting-started.md b/packages/komodo_coin_updates/doc/getting-started.md similarity index 100% rename from packages/komodo_coin_updates/docs/getting-started.md rename to packages/komodo_coin_updates/doc/getting-started.md diff --git a/packages/komodo_coin_updates/docs/providers.md b/packages/komodo_coin_updates/doc/providers.md similarity index 100% rename from packages/komodo_coin_updates/docs/providers.md rename to packages/komodo_coin_updates/doc/providers.md diff --git a/packages/komodo_coin_updates/docs/storage.md b/packages/komodo_coin_updates/doc/storage.md similarity index 100% rename from packages/komodo_coin_updates/docs/storage.md rename to packages/komodo_coin_updates/doc/storage.md diff --git a/packages/komodo_coin_updates/docs/testing.md b/packages/komodo_coin_updates/doc/testing.md similarity index 100% rename from packages/komodo_coin_updates/docs/testing.md rename to packages/komodo_coin_updates/doc/testing.md diff --git a/packages/komodo_coin_updates/docs/usage.md b/packages/komodo_coin_updates/doc/usage.md similarity index 100% rename from packages/komodo_coin_updates/docs/usage.md rename to packages/komodo_coin_updates/doc/usage.md diff --git a/packages/komodo_coin_updates/pubspec.yaml b/packages/komodo_coin_updates/pubspec.yaml index 0c7f89558..851ec133a 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: 2.0.0 +version: 2.0.1 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.4.0 + komodo_defi_types: ^0.4.1 logging: ^1.3.0 dev_dependencies: diff --git a/packages/komodo_coins/CHANGELOG.md b/packages/komodo_coins/CHANGELOG.md index 241a53a1b..85486c511 100644 --- a/packages/komodo_coins/CHANGELOG.md +++ b/packages/komodo_coins/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.3.2+1 + + - Update a dependency to the latest release. + ## 0.3.2 - **PERF**(logs): reduce market metrics log verbosity and duplication (#223). diff --git a/packages/komodo_coins/pubspec.yaml b/packages/komodo_coins/pubspec.yaml index e0f06b4cb..5336cb49d 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.2 +version: 0.3.2+1 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: ^2.0.0 - komodo_defi_types: ^0.4.0 + komodo_coin_updates: ^2.0.1 + komodo_defi_types: ^0.4.1 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 2ad2ee499..3d60e2e6b 100644 --- a/packages/komodo_defi_framework/CHANGELOG.md +++ b/packages/komodo_defi_framework/CHANGELOG.md @@ -1,3 +1,11 @@ +## 0.4.1 + + - **CHORE**(build): update bundled KDF to staging commit `52ba4f9` and use the TRON coins source for release builds. + - **FIX**(config): carry TRON explorer URL support through bundled build configuration (#338). + - **FIX**(web): harden numeric JS interop parsing for KDF responses (#336). + - **FEAT**(migration): expose the framework hooks needed for legacy wallet migration. + - **FEAT**(build): align build configuration with the balance recovery and fee-info release inputs (#341). + ## 0.4.0 > Note: This release has breaking changes. diff --git a/packages/komodo_defi_framework/app_build/build_config.json b/packages/komodo_defi_framework/app_build/build_config.json index 3612b9282..3159c9608 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": "883f1863cc71ad5f7a04878e4b7073d5f9f98dcf", "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_framework/pubspec.yaml b/packages/komodo_defi_framework/pubspec.yaml index 4a74984b2..e381a41a3 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.4.0 +version: 0.4.1 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: ^2.0.0 - komodo_coins: ^0.3.2 - komodo_defi_types: ^0.4.0 - komodo_wallet_build_transformer: ^0.4.1 + komodo_coin_updates: ^2.0.1 + komodo_coins: ^0.3.2+1 + komodo_defi_types: ^0.4.1 + komodo_wallet_build_transformer: ^0.4.2 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 d7026567b..7de350a62 100644 --- a/packages/komodo_defi_local_auth/CHANGELOG.md +++ b/packages/komodo_defi_local_auth/CHANGELOG.md @@ -1,3 +1,8 @@ +## 0.4.1 + + - **FIX**(auth,migration): wait for KDF RPC readiness and guard unsupported platforms during migration. + - **FEAT**(migration): add local-auth integration for legacy wallet verification and import flows. + ## 0.4.0 > Note: This release has breaking changes. @@ -55,4 +60,3 @@ - **FEAT**(dev): Install `melos`. - **FEAT**(sdk): Balance manager WIP. - **BREAKING** **FEAT**(sdk): Multi-SDK instance support. - 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..b3d4809bd 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,107 @@ 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; + try { + final version = await _kdfFramework.version().timeout( + KdfAuthService._kdfRpcProbeTimeout, + onTimeout: () => null, + ); + if (version != null) { + _logger.info( + '[$_sessionId] _waitUntilKdfRpcReady: RPC ready in ' + '${stopwatch.elapsedMilliseconds}ms', + ); + return; + } + } on SocketException catch (e) { + _logger.fine( + '[$_sessionId] _waitUntilKdfRpcReady: version probe transport ' + 'error (will retry): $e', + ); + } on HttpException catch (e) { + _logger.fine( + '[$_sessionId] _waitUntilKdfRpcReady: version probe transport ' + 'error (will retry): $e', + ); + } on HandshakeException catch (e) { + _logger.fine( + '[$_sessionId] _waitUntilKdfRpcReady: version probe transport ' + 'error (will retry): $e', + ); + } } - 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_local_auth/pubspec.yaml b/packages/komodo_defi_local_auth/pubspec.yaml index 8f4f7216b..a8e812594 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.4.0 +version: 0.4.1 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.4.0 - komodo_defi_rpc_methods: ^0.4.0 - komodo_defi_types: ^0.4.0 + komodo_defi_framework: ^0.4.1 + komodo_defi_rpc_methods: ^0.5.0 + komodo_defi_types: ^0.4.1 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 d0a05cd70..754ef8d1d 100644 --- a/packages/komodo_defi_rpc_methods/CHANGELOG.md +++ b/packages/komodo_defi_rpc_methods/CHANGELOG.md @@ -1,3 +1,12 @@ +## 0.5.0 + +> Note: This release has breaking changes. + + - **FIX**(errors): preserve RPC method hints when parsing ambiguous KDF error responses (#342). + - **FIX**(models): accept numeric JSON values encoded as either `int` or `num` across RPC models (#336). + - **FEAT**(auth): add the RPC request and activation parameter support needed by legacy wallet migration. + - **BREAKING** **FEAT**(sia): move SIA withdrawal handling onto hardened SIA-specific RPC models and namespace methods (#343). + ## 0.4.0 > Note: This release has breaking changes. 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_rpc_methods/pubspec.yaml b/packages/komodo_defi_rpc_methods/pubspec.yaml index 1d0c3b34f..cb2e1e1d6 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.4.0 +version: 0.5.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.4.0 + komodo_defi_types: ^0.4.1 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 063339d47..35c65f3be 100644 --- a/packages/komodo_defi_sdk/CHANGELOG.md +++ b/packages/komodo_defi_sdk/CHANGELOG.md @@ -1,3 +1,15 @@ +## 0.6.0 + +> Note: This release has breaking changes. + + - **FIX**(activation): restore coordinated TRX activation and market-data lookup (#340). + - **FIX**(explorers): support TRON explorer URL templates in SDK transaction flows (#338). + - **FIX**(market-data): keep last-known spot prices available while rotating cache snapshots (#335). + - **FEAT**(migration): add SDK integration for legacy wallet discovery, verification, import, and cleanup. + - **FEAT**(balances): add balance recovery mode and richer fee information plumbing (#341). + - **FEAT**(transaction-history): add a Tronscan strategy with address, cursor, and fixed-scale amount codecs (#339). + - **BREAKING** **FEAT**(sia): route SIA activation and withdrawals through the hardened SIA strategy and RPC namespace (#343). + ## 0.5.0 > Note: This release has breaking changes. 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/pubspec.yaml b/packages/komodo_defi_sdk/pubspec.yaml index 54efceb9f..2012c9bd7 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.5.0 +version: 0.6.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.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 + komodo_cex_market_data: ^0.1.0+1 + komodo_coins: ^0.3.2+1 + komodo_defi_framework: ^0.4.1 + komodo_defi_local_auth: ^0.4.1 + komodo_defi_rpc_methods: ^0.5.0 + komodo_defi_types: ^0.4.1 + komodo_ui: ^0.3.2 logging: ^1.3.0 mutex: ^3.1.0 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/CHANGELOG.md b/packages/komodo_defi_types/CHANGELOG.md index 8cfecbab0..2815eb95d 100644 --- a/packages/komodo_defi_types/CHANGELOG.md +++ b/packages/komodo_defi_types/CHANGELOG.md @@ -1,3 +1,11 @@ +## 0.4.1 + + - **FIX**(tron): support TRON explorer URL templates and correct TRC20 badge classification (#338, #344). + - **FIX**(models): accept numeric JSON values encoded as either `int` or `num` (#336). + - **FEAT**(migration): add auth error and wallet metadata types used by legacy wallet migration. + - **FEAT**(fees): expose richer fee information for balance recovery flows (#341). + - **FEAT**(transaction-history): add strategy metadata needed by the Tronscan history provider (#339). + ## 0.4.0 > Note: This release has breaking changes. 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_defi_types/pubspec.yaml b/packages/komodo_defi_types/pubspec.yaml index 760d820db..f340f8013 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.4.0 +version: 0.4.1 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.4.0 + komodo_defi_rpc_methods: ^0.5.0 logging: ^1.3.0 meta: ^1.15.0 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/CHANGELOG.md b/packages/komodo_legacy_wallet_migration/CHANGELOG.md new file mode 100644 index 000000000..6e1776b9a --- /dev/null +++ b/packages/komodo_legacy_wallet_migration/CHANGELOG.md @@ -0,0 +1,5 @@ +## 0.1.0 + + - **FEAT**(migration): add legacy wallet discovery, metadata parsing, password verification, import, and cleanup utilities. + - **FIX**(migration): use a PointyCastle-based Argon2 verifier for WASM compatibility. + - **FIX**(migration): guard unsupported platforms and wait for KDF RPC readiness before migration work. 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..a2916cb2d --- /dev/null +++ b/packages/komodo_legacy_wallet_migration/lib/komodo_legacy_wallet_migration.dart @@ -0,0 +1,6 @@ +// 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..65ec84ff4 --- /dev/null +++ b/packages/komodo_legacy_wallet_migration/lib/src/adapters/legacy_password_verifier.dart @@ -0,0 +1,143 @@ +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 +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. +/// +/// 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(); + + @override + Future verifySeedPassword({ + required String password, + required String encodedHash, + }) async { + try { + 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=