diff --git a/packages/dragon_logs/CHANGELOG.md b/packages/dragon_logs/CHANGELOG.md index dc7c7e6c..b969474d 100644 --- a/packages/dragon_logs/CHANGELOG.md +++ b/packages/dragon_logs/CHANGELOG.md @@ -1,3 +1,14 @@ +## 1.2.0 + +- **BREAKING**: Add WASM web support with OPFS-only storage +- **BREAKING**: Remove `file_system_access_api` and `js` dependencies +- **BREAKING**: Require Dart SDK `>=3.3.0` for extension types support +- Add `package:web` for modern web APIs compatibility +- Migrate from `dart:html` and `dart:js` to `dart:js_interop` and `package:web` +- Add WASM-specific platform detection using `dart.tool.dart2wasm` +- Implement Origin Private File System (OPFS) using modern JS interop +- Maintain full API compatibility while supporting both regular web and WASM compilation + ## 1.1.0 - Bump packages to latest versions. @@ -24,20 +35,15 @@ Refactor to share more code between web and native platforms (focused mainly on - Stable release - Tweak: Localisation initialisation no longer needs to be inialised before logs. - ## 0.1.1-preview.1 - Memory improvement for log flushing. - Bug fixes. - ## 0.1.0-preview.1 - Bug fixes. - ## 0.0.1-preview.1 - Initial preview version. - - diff --git a/packages/dragon_logs/README.md b/packages/dragon_logs/README.md index 3de2e124..7d51ee22 100644 --- a/packages/dragon_logs/README.md +++ b/packages/dragon_logs/README.md @@ -1,16 +1,4 @@ -# 🚚 Repository Moved - -> **⚠️ This repository has been migrated to the Komodo DeFi SDK Flutter monorepo.** -> -> 📍 **New location:** [packages/dragon_logs_flutter](https://github.com/KomodoPlatform/komodo-defi-sdk-flutter/tree/main/packages/dragon_logs_flutter) -> -> 🔄 **Active development** continues in the monorepo. Please update your forks, bookmarks, and links. -> -> 💡 **For issues, PRs, and contributions**, please use the [main monorepo](https://github.com/KomodoPlatform/komodo-defi-sdk-flutter). - ---- - -# Dragon Logs (Archived) +# Dragon Logs

Pub @@ -28,7 +16,7 @@ Dragon Logs aims to simplify the logging and log storage process in your Flutter - ✅ Cross-platform log storage - ✅ Cross-platform logs download -- ⬜ Flutter web wasm support +- ✅ Flutter web wasm support - ⬜ Web multi-threading support - ⬜ Log levels (e.g. debug, info, warning, error) - ⬜ Performance metrics (in progress) diff --git a/packages/dragon_logs/example/pubspec.lock b/packages/dragon_logs/example/pubspec.lock index d81b2a59..886ae595 100644 --- a/packages/dragon_logs/example/pubspec.lock +++ b/packages/dragon_logs/example/pubspec.lock @@ -5,10 +5,10 @@ packages: dependency: transitive description: name: async - sha256: d2872f9c19731c2e5f10444b14686eb7cc85c76274bd6c16e1816bff9a3bab63 + sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb" url: "https://pub.dev" source: hosted - version: "2.12.0" + version: "2.13.0" boolean_selector: dependency: transitive description: @@ -63,23 +63,23 @@ packages: path: ".." relative: true source: path - version: "1.0.4" + version: "1.2.0+1" fake_async: dependency: transitive description: name: fake_async - sha256: "6a95e56b2449df2273fd8c45a662d6947ce1ebb7aafe80e550a3f68297f3cacc" + sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44" url: "https://pub.dev" source: hosted - version: "1.3.2" + version: "1.3.3" ffi: dependency: transitive description: name: ffi - sha256: "16ed7b077ef01ad6170a3d0c57caa4a112a38d7a2ed5602e0aca9ca6f3d98da6" + sha256: "289279317b4b16eb2bb7e271abccd4bf84ec9bdcbe999e278a94b804f5630418" url: "https://pub.dev" source: hosted - version: "2.1.3" + version: "2.1.4" file: dependency: transitive description: @@ -88,14 +88,6 @@ packages: url: "https://pub.dev" source: hosted version: "7.0.1" - file_system_access_api: - dependency: transitive - description: - name: file_system_access_api - sha256: c961c5020ab4e5f05200dbdd9809c5256c3dc4a1fe5746ca7d8cf8e8cc11c47d - url: "https://pub.dev" - source: hosted - version: "2.0.0" fixnum: dependency: transitive description: @@ -136,26 +128,18 @@ packages: dependency: transitive description: name: intl - sha256: d6f56758b7d3014a48af9701c085700aac781a92a87a62b1333b46d8879661cf + sha256: "3df61194eb431efc39c4ceba583b95633a403f46c9fd341e550ce0bfa50e9aa5" url: "https://pub.dev" source: hosted - version: "0.19.0" - js: - dependency: transitive - description: - name: js - sha256: "53385261521cc4a0c4658fd0ad07a7d14591cf8fc33abbceae306ddb974888dc" - url: "https://pub.dev" - source: hosted - version: "0.7.2" + version: "0.20.2" leak_tracker: dependency: transitive description: name: leak_tracker - sha256: c35baad643ba394b40aac41080300150a4f08fd0fd6a10378f8f7c6bc161acec + sha256: "6bb818ecbdffe216e81182c2f0714a2e62b593f4a4f13098713ff1685dfb6ab0" url: "https://pub.dev" source: hosted - version: "10.0.8" + version: "10.0.9" leak_tracker_flutter_testing: dependency: transitive description: @@ -232,10 +216,10 @@ packages: dependency: transitive description: name: path_provider_android - sha256: "4adf4fd5423ec60a29506c76581bc05854c55e3a0b72d35bb28d661c9686edf2" + sha256: d0d310befe2c8ab9e7f393288ccbb11b60c019c6b5afc21973eeee4dda2b35e9 url: "https://pub.dev" source: hosted - version: "2.2.15" + version: "2.2.17" path_provider_foundation: dependency: transitive description: @@ -389,10 +373,10 @@ packages: dependency: transitive description: name: url_launcher_web - sha256: "3ba963161bd0fe395917ba881d320b9c4f6dd3c4a233da62ab18a5025c85f1e9" + sha256: "4bd2b7b4dc4d4d0b94e5babfffbca8eac1a126c7f3d6ecbc1a11013faa3abba2" url: "https://pub.dev" source: hosted - version: "2.4.0" + version: "2.4.1" url_launcher_windows: dependency: transitive description: @@ -421,26 +405,26 @@ packages: dependency: transitive description: name: vm_service - sha256: "0968250880a6c5fe7edc067ed0a13d4bae1577fe2771dcf3010d52c4a9d3ca14" + sha256: ddfa8d30d89985b96407efce8acbdd124701f96741f2d981ca860662f1c0dc02 url: "https://pub.dev" source: hosted - version: "14.3.1" + version: "15.0.0" web: dependency: transitive description: name: web - sha256: cd3543bd5798f6ad290ea73d210f423502e71900302dde696f8bff84bf89a1cb + sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a" url: "https://pub.dev" source: hosted - version: "1.1.0" + version: "1.1.1" win32: dependency: transitive description: name: win32 - sha256: daf97c9d80197ed7b619040e86c8ab9a9dad285e7671ee7390f9180cc828a51e + sha256: "66814138c3562338d05613a6e368ed8cfb237ad6d64a9e9334be3f309acfca03" url: "https://pub.dev" source: hosted - version: "5.10.1" + version: "5.14.0" xdg_directories: dependency: transitive description: @@ -450,5 +434,5 @@ packages: source: hosted version: "1.1.0" sdks: - dart: ">=3.7.0-0 <4.0.0" + dart: ">=3.8.0 <4.0.0" flutter: ">=3.27.0" diff --git a/packages/dragon_logs/lib/src/storage/file_log_storage.dart b/packages/dragon_logs/lib/src/storage/file_log_storage.dart index 55693f20..452dc864 100644 --- a/packages/dragon_logs/lib/src/storage/file_log_storage.dart +++ b/packages/dragon_logs/lib/src/storage/file_log_storage.dart @@ -78,8 +78,8 @@ class FileLogStorage Future deleteOldLogs(int size) async { while (await getLogFolderSize() > size) { final files = await getLogFiles(); - final sortedFiles = files.entries.toList() - ..sort((a, b) => a.key.compareTo(b.key)); + final sortedFiles = + files.entries.toList()..sort((a, b) => a.key.compareTo(b.key)); await sortedFiles.first.value.delete(); } } @@ -101,8 +101,8 @@ class FileLogStorage Stream exportLogsStream() async* { final files = await getLogFiles(); - final sortedFiles = files.values.toList() - ..sort((a, b) => a.path.compareTo(b.path)); + final sortedFiles = + files.values.toList()..sort((a, b) => a.path.compareTo(b.path)); for (final file in sortedFiles) { final stats = file.statSync(); final sizeKb = stats.size / 1024; @@ -127,9 +127,10 @@ class FileLogStorage @override Future deleteExportedFiles() async { - final archives = _exportFilesDirectory - .listSync(followLinks: false, recursive: true) - .whereType(); + final archives = + _exportFilesDirectory + .listSync(followLinks: false, recursive: true) + .whereType(); final deleteArchivesFutures = archives.map((archive) => archive.delete()); @@ -216,10 +217,9 @@ class FileLogStorage await raf.close(); // Use share_plus to share the log file - await Share.shareXFiles( - [XFile(file.path, mimeType: 'text/plain')], - text: 'App log file export', - ); + await Share.shareXFiles([ + XFile(file.path, mimeType: 'text/plain'), + ], text: 'App log file export'); } static Future getLogFolderPath() async { diff --git a/packages/dragon_logs/lib/src/storage/log_storage.dart b/packages/dragon_logs/lib/src/storage/log_storage.dart index 64552313..8db04163 100644 --- a/packages/dragon_logs/lib/src/storage/log_storage.dart +++ b/packages/dragon_logs/lib/src/storage/log_storage.dart @@ -1,5 +1,6 @@ import 'package:dragon_logs/src/storage/platform_instance/log_storage_web_platform.dart' - if (dart.library.io) 'package:dragon_logs/src/storage/platform_instance/log_storage_native_platform.dart'; + if (dart.library.io) 'package:dragon_logs/src/storage/platform_instance/log_storage_native_platform.dart' + if (dart.tool.dart2wasm) 'package:dragon_logs/src/storage/platform_instance/log_storage_wasm_platform.dart'; abstract class LogStorage { Future init(); diff --git a/packages/dragon_logs/lib/src/storage/opfs_interop.dart b/packages/dragon_logs/lib/src/storage/opfs_interop.dart new file mode 100644 index 00000000..78715d52 --- /dev/null +++ b/packages/dragon_logs/lib/src/storage/opfs_interop.dart @@ -0,0 +1,74 @@ +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 { + external bool get done; + external JSAny? get value; +} + +/// JavaScript async iterator type +@JS() +@anonymous +extension type JSAsyncIterator._(JSObject _) implements JSObject { + external JSPromise next(); +} + +/// Extensions for FileSystemDirectoryHandle to provide missing async iterator methods +/// that are available in the JavaScript File System API but not exposed in Flutter's web package. +@JS() +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(); + + /// Returns an async iterator for the keys (names) in this directory. + /// Equivalent to calling `directoryHandle.keys()` in JavaScript. + 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(); +} + +/// Helper extensions to convert JavaScript async iterators to Dart async iterables +extension JSAsyncIteratorExtension on JSAsyncIterator { + /// Converts a JavaScript async iterator to a Dart Stream + Stream asStream() async* { + while (true) { + final result = await next().toDart; + if (result.done) break; + yield result.value; + } + } +} + +/// 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); + } + + /// Returns a Stream of file/directory names for async iteration over directory contents + Stream keysStream() { + return keys().asStream().map((jsValue) => (jsValue as JSString).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); + }); + } +} diff --git a/packages/dragon_logs/lib/src/storage/platform_instance/log_storage_wasm_platform.dart b/packages/dragon_logs/lib/src/storage/platform_instance/log_storage_wasm_platform.dart new file mode 100644 index 00000000..f4af52fa --- /dev/null +++ b/packages/dragon_logs/lib/src/storage/platform_instance/log_storage_wasm_platform.dart @@ -0,0 +1,4 @@ +import 'package:dragon_logs/src/storage/log_storage.dart'; +import 'package:dragon_logs/src/storage/web_log_storage_wasm.dart'; + +LogStorage getLogStorageInstance() => WebLogStorageWasm(); \ No newline at end of file diff --git a/packages/dragon_logs/lib/src/storage/platform_instance/log_storage_web_platform.dart b/packages/dragon_logs/lib/src/storage/platform_instance/log_storage_web_platform.dart index f42f53b6..4a6a79bd 100644 --- a/packages/dragon_logs/lib/src/storage/platform_instance/log_storage_web_platform.dart +++ b/packages/dragon_logs/lib/src/storage/platform_instance/log_storage_web_platform.dart @@ -1,4 +1,4 @@ import 'package:dragon_logs/src/storage/log_storage.dart'; -import 'package:dragon_logs/src/storage/web_log_storage.dart'; +import 'package:dragon_logs/src/storage/web_log_storage_wasm.dart'; -LogStorage getLogStorageInstance() => WebLogStorage(); +LogStorage getLogStorageInstance() => WebLogStorageWasm(); diff --git a/packages/dragon_logs/lib/src/storage/web_log_storage.dart b/packages/dragon_logs/lib/src/storage/web_log_storage.dart deleted file mode 100644 index 7446cc40..00000000 --- a/packages/dragon_logs/lib/src/storage/web_log_storage.dart +++ /dev/null @@ -1,249 +0,0 @@ -// TODO: Remove after completing wasm implementation -// ignore_for_file: deprecated_member_use - -import 'dart:async'; -import 'dart:html' as html; -import 'dart:html'; -import 'dart:typed_data'; - -import 'package:dragon_logs/src/storage/input_output_mixin.dart'; -import 'package:dragon_logs/src/storage/log_storage.dart'; -import 'package:dragon_logs/src/storage/queue_mixin.dart'; -import 'package:file_system_access_api/file_system_access_api.dart'; -import 'package:intl/intl.dart'; -import 'package:js/js.dart'; -import 'package:js/js_util.dart' as js; - -/// Declare navigator like in a Web Worker context. -@JS() -external dynamic get navigator; - -class WebLogStorage - with QueueMixin, CommonLogStorageOperations - implements LogStorage { - // TODO: Multi-day support - // final List _logHandles = []; - FileSystemDirectoryHandle? _logDirectory; - - FileSystemFileHandle? _currentLogFile; - FileSystemWritableFileStream? _currentLogStream; - String _currentLogFileName = ""; - - late Timer _flushTimer; - - late final StorageManager? storage = - js.getProperty(navigator, "storage") as StorageManager?; - - @override - Future init() async { - if (!FileSystemAccess.supported) { - throw Exception( - "FileSystemAccess not supported for log storage on this browser", - ); - } - - final now = DateTime.now(); - _currentLogFileName = logFileNameOfDate(now); - - FileSystemDirectoryHandle? root = await storage?.getDirectory(); - - if (root != null) { - _logDirectory = await root.getDirectoryHandle( - "dragon_logs", - create: true, - ); - - // await initWriteDate(now); - } else { - throw Exception("Could not get root directory"); - } - - initQueueFlusher(); - } - - @override - Future writeToTextFile(String logs) async { - if (_currentLogStream == null) { - await initWriteDate(DateTime.now()); - } - - try { - await _currentLogStream!.writeAsText('$logs\n'); - - await closeLogFile(); - await initWriteDate(DateTime.now()); - } catch (e) { - rethrow; - } - } - - @override - // TODO: implement so that we don't have to delete the whole file - Future deleteOldLogs(int size) async { - await startFlush(); - - try { - while (await getLogFolderSize() > size) { - final files = await _getLogFiles(); - - final sortedFiles = files - .where( - (handle) => CommonLogStorageOperations.isLogFileNameValid( - handle.name, - ), - ) - .toList() - ..sort((a, b) { - final aDate = CommonLogStorageOperations.tryParseLogFileDate( - a.name, - ); - final bDate = CommonLogStorageOperations.tryParseLogFileDate( - b.name, - ); - - if (aDate == null || bDate == null) { - return 0; - } - - return aDate.compareTo(bDate); - }); - await sortedFiles.first.remove(); - } - } catch (e) { - rethrow; - } finally { - endFlush(); - } - } - - Future initWriteDate(DateTime date) async { - await closeLogFile(); - - _currentLogFileName = logFileNameOfDate(date); - - _currentLogFile ??= await _logDirectory?.getFileHandle( - _currentLogFileName, - create: true, - ); - - final sizeBytes = (await _currentLogFile?.getFile())?.size ?? 0; - - _currentLogStream = await _currentLogFile?.createWritable( - keepExistingData: true, - ); - - await _currentLogStream?.seek(sizeBytes); - } - - @override - Future getLogFolderSize() async { - final files = await _getLogFiles(); - - final htmlFileObjects = await Future.wait( - files.map((e) => e.getFile()), - ); - - final int totalSize = htmlFileObjects.fold( - 0, - (int? previousValue, File file) => (previousValue ?? 0) + file.size, - ); - - return totalSize; - } - - //TODO! Move to web worker for web so we can access sync flush method instead - // of this workaround - @override - Future closeLogFile() async { - if (_currentLogStream != null) { - await _currentLogStream!.close(); - - _currentLogStream = null; - } - } - - @override - Stream exportLogsStream() async* { - for (final file in await _getLogFiles()) { - String content = await _readFileContent(await file.getFile()); - yield content; - } - } - - /// Returns a list of OPFS file handles for all log files EXCLUDING any - /// temporary write file (if it exists) identified by the `.crswap` extension. - Future> _getLogFiles() async { - final files = await _logDirectory?.values - .where((handle) => handle.kind == FileSystemKind.file) - .cast() - .where((handle) => !handle.name.endsWith('.crswap')) - .toList() ?? - []; - - print('_getLogFiles: ${files.map((e) => e.name).join(',\n')}'); - - return files..sort((a, b) => a.name.compareTo(b.name)); - } - - Future _readFileContent(html.File file) async { - final completer = Completer(); - final reader = html.FileReader(); - - StreamSubscription? loadEndSubscription; - StreamSubscription? errorSubscription; - - loadEndSubscription = reader.onLoadEnd.listen((event) { - completer.complete(reader.result as String); - }); - - errorSubscription = reader.onError.listen((error) { - completer.completeError("Error reading file: $error"); - }); - - reader.readAsText(file); - - return completer.future.whenComplete(() { - loadEndSubscription?.cancel(); - errorSubscription?.cancel(); - }); - } - - @override - Future deleteExportedFiles() async { - // Since it's a web implementation, we just need to ensure necessary permissions. - // Note: Real-world applications should handle permissions gracefully, prompting users as needed. - } - - @override - // TODO: Multi-threading support in web worker - Future exportLogsToDownload() async { - final bytesStream = exportLogsStream().asyncExpand((event) { - return Stream.fromIterable(event.codeUnits); - }); - - final formatter = DateFormat('yyyyMMdd_HHmmss'); - final filename = 'log_${formatter.format(DateTime.now())}.txt'; - - List bytes = await bytesStream.toList(); - final blob = html.Blob([Uint8List.fromList(bytes)]); - final url = html.Url.createObjectUrlFromBlob(blob); - // ignore: unused_local_variable - final anchor = html.AnchorElement(href: url) - ..target = 'blank' - ..download = filename - ..click(); - html.Url.revokeObjectUrl(url); - } - - void dispose() async { - _flushTimer.cancel(); - await closeLogFile(); // Close the log file once during the dispose method - } -} - -//TODO! -Future flushInWebWorker() async { - // final logStorage = WebLogStorage(); - // await logStorage.init(); - // await logStorage.flushLogQueue(); -} diff --git a/packages/dragon_logs/lib/src/storage/web_log_storage_wasm.dart b/packages/dragon_logs/lib/src/storage/web_log_storage_wasm.dart new file mode 100644 index 00000000..833ee934 --- /dev/null +++ b/packages/dragon_logs/lib/src/storage/web_log_storage_wasm.dart @@ -0,0 +1,232 @@ +import 'dart:async'; +import 'dart:js_interop'; +import 'dart:typed_data'; + +import 'package:dragon_logs/src/storage/input_output_mixin.dart'; +import 'package:dragon_logs/src/storage/log_storage.dart'; +import 'package:dragon_logs/src/storage/opfs_interop.dart'; +import 'package:dragon_logs/src/storage/queue_mixin.dart'; +import 'package:intl/intl.dart'; +import 'package:web/web.dart'; + +/// WASM-compatible web log storage implementation using OPFS +class WebLogStorageWasm + with QueueMixin, CommonLogStorageOperations + implements LogStorage { + FileSystemDirectoryHandle? _logDirectory; + FileSystemFileHandle? _currentLogFile; + FileSystemWritableFileStream? _currentLogStream; + String _currentLogFileName = ""; + + @override + Future init() async { + final now = DateTime.now(); + _currentLogFileName = logFileNameOfDate(now); + + // Get the OPFS root directory + final storageManager = window.navigator.storage; + final root = await storageManager.getDirectory().toDart; + + // Create or get the dragon_logs directory + _logDirectory = + await root + .getDirectoryHandle( + "dragon_logs", + FileSystemGetDirectoryOptions(create: true), + ) + .toDart; + + initQueueFlusher(); + } + + @override + Future writeToTextFile(String logs) async { + if (_currentLogStream == null) { + await initWriteDate(DateTime.now()); + } + + try { + await _currentLogStream!.write('$logs\n'.toJS).toDart; + await closeLogFile(); + await initWriteDate(DateTime.now()); + } catch (e) { + rethrow; + } + } + + Future initWriteDate(DateTime date) async { + await closeLogFile(); + + _currentLogFileName = logFileNameOfDate(date); + + _currentLogFile = + await _logDirectory! + .getFileHandle( + _currentLogFileName, + FileSystemGetFileOptions(create: true), + ) + .toDart; + + final file = await _currentLogFile!.getFile().toDart; + final sizeBytes = file.size.toInt(); + + _currentLogStream = + await _currentLogFile! + .createWritable( + FileSystemCreateWritableOptions(keepExistingData: true), + ) + .toDart; + + await _currentLogStream!.seek(sizeBytes).toDart; + } + + @override + Future deleteOldLogs(int size) async { + await startFlush(); + + try { + while (await getLogFolderSize() > size) { + final files = await _getLogFiles(); + + final sortedFiles = + files + .where( + (handle) => CommonLogStorageOperations.isLogFileNameValid( + handle.name, + ), + ) + .toList() + ..sort((a, b) { + final aDate = CommonLogStorageOperations.tryParseLogFileDate( + a.name, + ); + final bDate = CommonLogStorageOperations.tryParseLogFileDate( + b.name, + ); + + if (aDate == null || bDate == null) { + return 0; + } + + return aDate.compareTo(bDate); + }); + + if (sortedFiles.isEmpty) { + break; + } + + await _logDirectory! + .removeEntry( + sortedFiles.first.name, + FileSystemRemoveOptions(recursive: false), + ) + .toDart; + } + } catch (e) { + rethrow; + } finally { + endFlush(); + } + } + + @override + Future getLogFolderSize() async { + final files = await _getLogFiles(); + + int totalSize = 0; + for (final handle in files) { + final file = await handle.getFile().toDart; + totalSize += file.size.toInt(); + } + + return totalSize; + } + + @override + Future closeLogFile() async { + if (_currentLogStream != null) { + await _currentLogStream!.close().toDart; + _currentLogStream = null; + } + } + + @override + Stream exportLogsStream() async* { + final files = await _getLogFiles(); + + for (final fileHandle in files) { + final file = await fileHandle.getFile().toDart; + final content = await _readFileContent(file); + yield content; + } + } + + /// Returns a list of OPFS file handles for all log files EXCLUDING any + /// temporary write file (if it exists) identified by the `.crswap` extension. + Future> _getLogFiles() async { + final files = []; + + // Use the async iterator provided by FileSystemDirectoryHandle.values() + // via our custom interop extension + await for (final handle in _logDirectory!.valuesStream()) { + if (handle.kind == 'file' && !handle.name.endsWith('.crswap')) { + files.add(handle as FileSystemFileHandle); + } + } + + files.sort((a, b) => a.name.compareTo(b.name)); + return files; + } + + Future _readFileContent(File file) async { + final completer = Completer(); + final reader = FileReader(); + + reader.onLoadEnd.listen((event) { + final result = reader.result; + if (result != null) { + completer.complete(result.toString()); + } else { + completer.complete(''); + } + }); + + reader.readAsText(file); + return completer.future; + } + + @override + Future deleteExportedFiles() async { + // Since it's a web implementation, we just need to ensure necessary permissions. + // Note: Real-world applications should handle permissions gracefully, prompting users as needed. + } + + @override + Future exportLogsToDownload() async { + final bytesStream = exportLogsStream().asyncExpand((event) { + return Stream.fromIterable(event.codeUnits); + }); + + final formatter = DateFormat('yyyyMMdd_HHmmss'); + final filename = 'log_${formatter.format(DateTime.now())}.txt'; + + final bytes = await bytesStream.toList(); + final blob = Blob([Uint8List.fromList(bytes).toJS].toJS); + final url = URL.createObjectURL(blob); + + final anchor = + HTMLAnchorElement() + ..href = url + ..download = filename + ..style.display = 'none'; + + document.body!.appendChild(anchor); + anchor.click(); + document.body!.removeChild(anchor); + URL.revokeObjectURL(url); + } + + void dispose() async { + await closeLogFile(); + } +} diff --git a/packages/dragon_logs/pubspec.yaml b/packages/dragon_logs/pubspec.yaml index 00ee726d..1a32c712 100644 --- a/packages/dragon_logs/pubspec.yaml +++ b/packages/dragon_logs/pubspec.yaml @@ -1,12 +1,12 @@ name: dragon_logs description: An efficient cross-platform Flutter log storage framework with minimal dependencies. -version: 1.1.0 +version: 1.2.0+1 -repository: https://github.com/KomodoPlatform/dragon_logs_flutter +repository: https://github.com/KomodoPlatform/komodo-defi-sdk-flutter/tree/main/packages/dragon_logs homepage: https://komodoplatform.com environment: - sdk: ">=3.0.0 <4.0.0" + sdk: ^3.7.0 dev_dependencies: lints: ^5.1.1 @@ -33,13 +33,11 @@ dependencies: # Last approved via KW PR #1106 share_plus: ^10.1.4 - file_system_access_api: ^2.0.0 - # ====== Flutter.dev/Dart.dev approved ====== # Secure review for Flutter.dev/Dart.dev packages not strictly required since # they are Google/Dart products, but still recommended. - intl: ">=0.19.0 <0.21.0" # The latest `stable` Flutter version is pinned to 0.19 + intl: ^0.20.2 path_provider: ^2.1.5 path: ^1.8.3 - js: ^0.7.2 + web: ^1.1.0 diff --git a/packages/komodo_coin_updates/lib/src/data/coin_config_provider.dart b/packages/komodo_coin_updates/lib/src/data/coin_config_provider.dart index de6c7851..791952e2 100644 --- a/packages/komodo_coin_updates/lib/src/data/coin_config_provider.dart +++ b/packages/komodo_coin_updates/lib/src/data/coin_config_provider.dart @@ -16,11 +16,18 @@ class CoinConfigProvider { 'https://api.github.com/repos/KomodoPlatform/coins', this.coinsPath = 'coins', this.coinsConfigPath = 'utils/coins_config_unfiltered.json', + this.githubToken, }); - factory CoinConfigProvider.fromConfig(RuntimeUpdateConfig config) { + factory CoinConfigProvider.fromConfig( + RuntimeUpdateConfig config, { + String? githubToken, + }) { // TODO(Francois): derive all the values from the config - return CoinConfigProvider(branch: config.coinsRepoBranch); + return CoinConfigProvider( + branch: config.coinsRepoBranch, + githubToken: githubToken, + ); } final String branch; @@ -28,6 +35,7 @@ class CoinConfigProvider { final String coinsGithubApiUrl; final String coinsPath; final String coinsConfigPath; + final String? githubToken; /// Fetches the coins from the repository. /// [commit] is the commit hash to fetch the coins from. @@ -81,8 +89,30 @@ class CoinConfigProvider { final client = http.Client(); final url = Uri.parse('$coinsGithubApiUrl/branches/$branch'); final header = {'Accept': 'application/vnd.github+json'}; + + // Add authentication header if token is available + if (githubToken != null) { + header['Authorization'] = 'Bearer $githubToken'; + print('CoinConfigProvider: Using authentication for GitHub API request'); + } else { + print( + 'CoinConfigProvider: No GitHub token available - making unauthenticated request', + ); + } + final response = await client.get(url, headers: header); + if (response.statusCode != 200) { + print( + 'CoinConfigProvider: GitHub API request failed: ${response.statusCode} ${response.reasonPhrase}', + ); + print('CoinConfigProvider: Response body: ${response.body}'); + throw Exception( + 'Failed to retrieve latest commit hash: $branch' + '[${response.statusCode}]: ${response.reasonPhrase}', + ); + } + final json = jsonDecode(response.body) as Map; final commit = json['commit'] as Map; final latestCommitHash = commit['sha'] as String; diff --git a/packages/komodo_coin_updates/lib/src/data/coin_config_repository.dart b/packages/komodo_coin_updates/lib/src/data/coin_config_repository.dart index ab776e44..e4fd54ca 100644 --- a/packages/komodo_coin_updates/lib/src/data/coin_config_repository.dart +++ b/packages/komodo_coin_updates/lib/src/data/coin_config_repository.dart @@ -21,12 +21,17 @@ class CoinConfigRepository implements CoinConfigStorage { /// Creates a coin config storage provider with default databases. /// The default databases are HiveLazyBoxProvider. /// The default databases are named 'coins' and 'coins_settings'. - CoinConfigRepository.withDefaults(RuntimeUpdateConfig config) - : coinConfigProvider = CoinConfigProvider.fromConfig(config), - coinsDatabase = HiveLazyBoxProvider(name: 'coins'), - coinSettingsDatabase = HiveBoxProvider( - name: 'coins_settings', - ); + CoinConfigRepository.withDefaults( + RuntimeUpdateConfig config, { + String? githubToken, + }) : coinConfigProvider = CoinConfigProvider.fromConfig( + config, + githubToken: githubToken, + ), + coinsDatabase = HiveLazyBoxProvider(name: 'coins'), + coinSettingsDatabase = HiveBoxProvider( + name: 'coins_settings', + ); /// The provider that fetches the coins and coin configs. final CoinConfigProvider coinConfigProvider; diff --git a/packages/komodo_coin_updates/pubspec.yaml b/packages/komodo_coin_updates/pubspec.yaml index f5a837f7..838db99a 100644 --- a/packages/komodo_coin_updates/pubspec.yaml +++ b/packages/komodo_coin_updates/pubspec.yaml @@ -5,13 +5,13 @@ publish_to: none # publishable packages can't have git dependencies environment: sdk: ^3.7.0 - flutter: ">=3.29.0 <3.30.0" + flutter: ">=3.29.0 <3.36.0" # Add regular dependencies here. dependencies: equatable: ^2.0.7 flutter_bloc: ^9.1.1 - hive: 2.2.3 + hive: 2.2.3 hive_flutter: 1.1.0 http: ^1.4.0 komodo_defi_types: diff --git a/packages/komodo_coins/lib/src/komodo_coins_base.dart b/packages/komodo_coins/lib/src/komodo_coins_base.dart index af44046a..af5bd011 100644 --- a/packages/komodo_coins/lib/src/komodo_coins_base.dart +++ b/packages/komodo_coins/lib/src/komodo_coins_base.dart @@ -94,6 +94,12 @@ class KomodoCoins { assets[assetId] = asset; // } } + } + // Log exceptions related to missing config fields + on MissingProtocolFieldException catch (e) { + debugPrint( + 'Skipping asset ${entry.key} due to missing protocol field: $e', + ); } catch (e) { debugPrint( 'Error parsing asset ${entry.key}: $e , ' diff --git a/packages/komodo_defi_framework/app_build/build_config.json b/packages/komodo_defi_framework/app_build/build_config.json index 6bf6b554..e171eeab 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": "a9fbf6096257b802e0aac4d936dcc5d2f0c28461", - "branch": "main", + "api_commit_hash": "6172ba8d1df0541dd319d4193cad1cb26df50eee", + "branch": "dev", "fetch_at_build_enabled": true, "concurrent_downloads_enabled": true, "source_urls": [ @@ -12,49 +12,49 @@ "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": [ - "d4476637d566a87974ed54bd708e83f9d05a45a7dfae7ade57c0d00b0f0df631" + "abde4d74279850004445df3f1a6ecd095dbd5b12f4821c4bb2a14c1aa94ab770" ], "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": [ - "efd8e8e738541a4838a2b044edc60030db9a4ba14392e30fb1a152472d4f4313" + "8661a477437563e8978f47baf1486f9b6ed900e3f0fb030dedee60843ecfc882" ], "path": "ios" }, "macos": { "matching_pattern": "^(?:kdf_[a-f0-9]{7,40}-mac-arm64|mm2-[a-f0-9]{7,40}-Darwin-Release)\\.zip$", "valid_zip_sha256_checksums": [ - "bc411c8d95dbe565b0e56871babaea7412ccbd1ad7f525f3cf56a384a4a77ee7" + "142782fd8689c3106614f73065c6848192873f83f853eb357156f5d7c053fc92" ], "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": [ - "d9849d01962b4e05899cde7ec17f6b9e8ba9411f484369724c7a73a4b6a3fb80" + "8697178c85cd047a7f0a9331fd8f91dc256a6732dc311ad81b1fe7eba55baab9" ], "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": [ - "10ae609f3c7e4ed47e5a1134dd74da84375f9a1c6538c985afb1c148d58f8756" + "b3226bced064770a09eb556d411538d5d41e4c4513deb3feb05b5b3c04896c27" ], "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": [ - "ab4b5311e0d1b6b2f57ed1783d5b7a51c4b7558cbf0bad593d9235f6a32db906" + "e4111bbce3fa991430d632f8c0bf5d3ff55e505c34bde87f454db474f27eaa29" ], "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": [ - "913a165e434ed9696c0e8c9a1875bfd6448e291f85d2e7e8dae78618ef3534e3" + "cfe775cd2b4215bb2e5cb3516adb040ec8df64f8de9dc50e154f0ee863051e72" ], "path": "linux/bin" } @@ -63,7 +63,7 @@ "coins": { "fetch_at_build_enabled": true, "update_commit_on_build": true, - "bundled_coins_repo_commit": "1fe493212b34937d82c668e6118a1356d7eb2f06", + "bundled_coins_repo_commit": "322575ff3230d91e739be33861062173e1925cd3", "coins_repo_api_url": "https://api.github.com/repos/KomodoPlatform/coins", "coins_repo_content_url": "https://komodoplatform.github.io/coins", "coins_repo_branch": "master", 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 2e3d9b18..78dd7a6f 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 @@ -135,10 +135,11 @@ class KdfOperationsWasm implements IKdfOperations { // Check for code property if (jsObj.hasProperty('code'.toJS).toDart) { - final code = jsObj.getProperty('code'.toJS); - // Print all properties of the JSObject - if (isInstance(code, 'JSNumber')) { - final errorCode = (code! as js_interop.JSNumber).toDartInt; + final code = jsObj.getProperty('code'.toJS); + // Check if the property is a JSNumber + if (code != null && + isInstance(code, 'JSNumber')) { + final errorCode = code.toDartInt; _log( 'KdfOperationsWasm: Resolved as JSObject->JSNumber error code: $errorCode', ); @@ -147,9 +148,10 @@ class KdfOperationsWasm implements IKdfOperations { } // Try toNumber method - final asNumber = jsObj.callMethod('toNumber'.toJS); + final asNumber = + jsObj.callMethod('toNumber'.toJS); if (asNumber?.isDefinedAndNotNull ?? false) { - final errorCode = (asNumber! as js_interop.JSNumber).toDartInt; + final errorCode = asNumber!.toDartInt; _log( 'KdfOperationsWasm: Resolved as JSNumber error code: $errorCode', ); @@ -188,8 +190,10 @@ class KdfOperationsWasm implements IKdfOperations { @override Future kdfMainStatus() async { await _ensureLoaded(); - final status = _kdfModule!.callMethod('mm2_main_status'.toJS); - return MainStatus.fromDefaultInt(status! as int); + final status = _kdfModule! + .callMethod('mm2_main_status'.toJS) + ?.toDartInt; + return MainStatus.fromDefaultInt(status!); } @override @@ -431,9 +435,11 @@ class KdfOperationsWasm implements IKdfOperations { 'init_wasm', '__wbg_init', ], - value: (key) => - 'Has property: ${_kdfModule!.has(key as String)} with type: ' - '${_kdfModule!.getProperty(key.toJS).runtimeType}', + 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'); diff --git a/packages/komodo_defi_sdk/example/lib/main.dart b/packages/komodo_defi_sdk/example/lib/main.dart index 78ccd65a..bf3e53f0 100644 --- a/packages/komodo_defi_sdk/example/lib/main.dart +++ b/packages/komodo_defi_sdk/example/lib/main.dart @@ -1,6 +1,7 @@ // lib/main.dart import 'dart:async'; +import 'package:dragon_logs/dragon_logs.dart' as dragon; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:kdf_sdk_example/blocs/auth/auth_bloc.dart'; @@ -17,6 +18,7 @@ final GlobalKey _navigatorKey = GlobalKey(); void main() async { WidgetsFlutterBinding.ensureInitialized(); + await dragon.DragonLogs.init(); // Create instance manager final instanceManager = KdfInstanceManager(); @@ -24,9 +26,11 @@ void main() async { // Create default SDK instance with config final defaultSdk = KomodoDefiSdk(config: _config); await defaultSdk.initialize(); + dragon.log('Default SDK instance initialized'); // Register default instance await instanceManager.registerInstance('Local Instance', _config, defaultSdk); + dragon.log('Registered default instance'); runApp( MultiRepositoryProvider( @@ -115,6 +119,7 @@ class _KomodoAppState extends State { // Load known users await _fetchKnownUsers(instance); + dragon.log('Initialized instance ${instance.name}'); } void _updateInstanceUser(String instanceName, KdfUser? user) { @@ -125,6 +130,15 @@ class _KomodoAppState extends State { ? 'Current wallet: ${user.walletId.name}' : 'Not signed in'; }); + dragon.DragonLogs.setSessionMetadata({ + 'instance': instanceName, + if (user != null) 'user': user.walletId.compoundId, + }); + dragon.log( + user != null + ? 'User ${user.walletId.compoundId} authenticated in $instanceName' + : 'User signed out of $instanceName', + ); } Future _fetchKnownUsers(KdfInstanceState instance) async { @@ -135,7 +149,7 @@ class _KomodoAppState extends State { state.knownUsers = users; setState(() {}); } catch (e, s) { - print('Error fetching known users: $e'); + dragon.log('Error fetching known users: $e', 'ERROR'); debugPrintStack(stackTrace: s); } } @@ -182,6 +196,16 @@ class _KomodoAppState extends State { : Colors.red, child: const Icon(Icons.cloud), ), + IconButton( + icon: const Icon(Icons.download), + tooltip: 'Export Logs', + onPressed: () async { + await dragon.DragonLogs.exportLogsToDownload(); + _scaffoldKey.currentState?.showSnackBar( + const SnackBar(content: Text('Logs exported')), + ); + }, + ), const SizedBox(width: 16), ], ], diff --git a/packages/komodo_defi_sdk/example/linux/flutter/generated_plugin_registrant.cc b/packages/komodo_defi_sdk/example/linux/flutter/generated_plugin_registrant.cc index d0e7f797..38dd0bc6 100644 --- a/packages/komodo_defi_sdk/example/linux/flutter/generated_plugin_registrant.cc +++ b/packages/komodo_defi_sdk/example/linux/flutter/generated_plugin_registrant.cc @@ -7,9 +7,13 @@ #include "generated_plugin_registrant.h" #include +#include void fl_register_plugins(FlPluginRegistry* registry) { g_autoptr(FlPluginRegistrar) flutter_secure_storage_linux_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterSecureStorageLinuxPlugin"); flutter_secure_storage_linux_plugin_register_with_registrar(flutter_secure_storage_linux_registrar); + g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin"); + url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar); } diff --git a/packages/komodo_defi_sdk/example/linux/flutter/generated_plugins.cmake b/packages/komodo_defi_sdk/example/linux/flutter/generated_plugins.cmake index a9f2fe5a..a1cc4f39 100644 --- a/packages/komodo_defi_sdk/example/linux/flutter/generated_plugins.cmake +++ b/packages/komodo_defi_sdk/example/linux/flutter/generated_plugins.cmake @@ -4,6 +4,7 @@ list(APPEND FLUTTER_PLUGIN_LIST flutter_secure_storage_linux + url_launcher_linux ) list(APPEND FLUTTER_FFI_PLUGIN_LIST diff --git a/packages/komodo_defi_sdk/example/macos/Flutter/GeneratedPluginRegistrant.swift b/packages/komodo_defi_sdk/example/macos/Flutter/GeneratedPluginRegistrant.swift index 46b0a8b7..b83e6002 100644 --- a/packages/komodo_defi_sdk/example/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/packages/komodo_defi_sdk/example/macos/Flutter/GeneratedPluginRegistrant.swift @@ -9,6 +9,7 @@ import flutter_secure_storage_darwin import local_auth_darwin import mobile_scanner import path_provider_foundation +import share_plus import shared_preferences_foundation func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { @@ -16,5 +17,6 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { FLALocalAuthPlugin.register(with: registry.registrar(forPlugin: "FLALocalAuthPlugin")) MobileScannerPlugin.register(with: registry.registrar(forPlugin: "MobileScannerPlugin")) PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) + SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin")) SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) } diff --git a/packages/komodo_defi_sdk/example/pubspec.lock b/packages/komodo_defi_sdk/example/pubspec.lock index 20c959f6..1360f7b0 100644 --- a/packages/komodo_defi_sdk/example/pubspec.lock +++ b/packages/komodo_defi_sdk/example/pubspec.lock @@ -57,6 +57,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.19.1" + cross_file: + dependency: transitive + description: + name: cross_file + sha256: "7caf6a750a0c04effbb52a676dce9a4a592e10ad35c34d6d2d0e4811160d5670" + url: "https://pub.dev" + source: hosted + version: "0.3.4+2" crypto: dependency: transitive description: @@ -81,6 +89,13 @@ packages: url: "https://pub.dev" source: hosted version: "3.2.1" + dragon_logs: + dependency: "direct main" + description: + path: "../../dragon_logs" + relative: true + source: path + version: "1.2.0+1" equatable: dependency: "direct main" description: @@ -147,6 +162,11 @@ packages: url: "https://pub.dev" source: hosted version: "6.0.0" + flutter_localizations: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" flutter_plugin_android_lifecycle: dependency: transitive description: @@ -315,56 +335,56 @@ packages: path: "../../komodo_coins" relative: true source: path - version: "0.2.0+0" + version: "0.3.0+0" komodo_defi_framework: dependency: "direct overridden" description: path: "../../komodo_defi_framework" relative: true source: path - version: "0.2.0" + version: "0.3.0+0" komodo_defi_local_auth: dependency: "direct overridden" description: path: "../../komodo_defi_local_auth" relative: true source: path - version: "0.2.0+0" + version: "0.3.0+0" komodo_defi_rpc_methods: dependency: "direct overridden" description: path: "../../komodo_defi_rpc_methods" relative: true source: path - version: "0.2.0+0" + version: "0.3.0+0" komodo_defi_sdk: dependency: "direct main" description: path: ".." relative: true source: path - version: "0.2.0+0" + version: "0.3.0+0" komodo_defi_types: dependency: "direct main" description: path: "../../komodo_defi_types" relative: true source: path - version: "0.2.0+0" + version: "0.3.0+0" komodo_ui: dependency: "direct main" description: path: "../../komodo_ui" relative: true source: path - version: "0.2.0+0" + version: "0.3.0+0" komodo_wallet_build_transformer: dependency: "direct overridden" description: path: "../../komodo_wallet_build_transformer" relative: true source: path - version: "0.2.0+0" + version: "0.3.0+0" leak_tracker: dependency: transitive description: @@ -469,6 +489,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.16.0" + mime: + dependency: transitive + description: + name: mime + sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6" + url: "https://pub.dev" + source: hosted + version: "2.0.0" mobile_scanner: dependency: transitive description: @@ -589,6 +617,22 @@ packages: url: "https://pub.dev" source: hosted version: "2.2.3" + share_plus: + dependency: transitive + description: + name: share_plus + sha256: fce43200aa03ea87b91ce4c3ac79f0cecd52e2a7a56c7a4185023c271fbfa6da + url: "https://pub.dev" + source: hosted + version: "10.1.4" + share_plus_platform_interface: + dependency: transitive + description: + name: share_plus_platform_interface + sha256: cc012a23fc2d479854e6c80150696c4a5f5bb62cb89af4de1c505cf78d0a5d0b + url: "https://pub.dev" + source: hosted + version: "5.0.2" shared_preferences: dependency: transitive description: @@ -722,6 +766,38 @@ packages: url: "https://pub.dev" source: hosted version: "1.4.0" + url_launcher_linux: + dependency: transitive + description: + name: url_launcher_linux + sha256: "4e9ba368772369e3e08f231d2301b4ef72b9ff87c31192ef471b380ef29a4935" + url: "https://pub.dev" + source: hosted + version: "3.2.1" + url_launcher_platform_interface: + dependency: transitive + description: + name: url_launcher_platform_interface + sha256: "552f8a1e663569be95a8190206a38187b531910283c3e982193e4f2733f01029" + url: "https://pub.dev" + source: hosted + version: "2.3.2" + url_launcher_web: + dependency: transitive + description: + name: url_launcher_web + sha256: "4bd2b7b4dc4d4d0b94e5babfffbca8eac1a126c7f3d6ecbc1a11013faa3abba2" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + url_launcher_windows: + dependency: transitive + description: + name: url_launcher_windows + sha256: "3284b6d2ac454cf34f114e1d3319866fdd1e19cdc329999057e44ffe936cfa77" + url: "https://pub.dev" + source: hosted + version: "3.1.4" uuid: dependency: transitive description: diff --git a/packages/komodo_defi_sdk/example/pubspec.yaml b/packages/komodo_defi_sdk/example/pubspec.yaml index 77b2871a..9ae11b5a 100644 --- a/packages/komodo_defi_sdk/example/pubspec.yaml +++ b/packages/komodo_defi_sdk/example/pubspec.yaml @@ -14,6 +14,8 @@ dependencies: flutter_bloc: ^9.1.1 flutter_secure_storage: ^10.0.0-beta.4 + dragon_logs: + path: ../../dragon_logs komodo_defi_sdk: path: ../ diff --git a/packages/komodo_defi_sdk/example/pubspec_overrides.yaml b/packages/komodo_defi_sdk/example/pubspec_overrides.yaml index b5025daf..996f2849 100644 --- a/packages/komodo_defi_sdk/example/pubspec_overrides.yaml +++ b/packages/komodo_defi_sdk/example/pubspec_overrides.yaml @@ -1,5 +1,7 @@ -# melos_managed_dependency_overrides: komodo_cex_market_data,komodo_coin_updates,komodo_coins,komodo_defi_framework,komodo_defi_local_auth,komodo_defi_rpc_methods,komodo_defi_sdk,komodo_defi_types,komodo_ui,komodo_wallet_build_transformer +# melos_managed_dependency_overrides: dragon_logs,komodo_cex_market_data,komodo_coin_updates,komodo_coins,komodo_defi_framework,komodo_defi_local_auth,komodo_defi_rpc_methods,komodo_defi_sdk,komodo_defi_types,komodo_ui,komodo_wallet_build_transformer dependency_overrides: + dragon_logs: + path: ../../dragon_logs komodo_cex_market_data: path: ../../komodo_cex_market_data komodo_coin_updates: diff --git a/packages/komodo_defi_sdk/example/windows/flutter/generated_plugin_registrant.cc b/packages/komodo_defi_sdk/example/windows/flutter/generated_plugin_registrant.cc index 011734da..f4b698cb 100644 --- a/packages/komodo_defi_sdk/example/windows/flutter/generated_plugin_registrant.cc +++ b/packages/komodo_defi_sdk/example/windows/flutter/generated_plugin_registrant.cc @@ -8,10 +8,16 @@ #include #include +#include +#include void RegisterPlugins(flutter::PluginRegistry* registry) { FlutterSecureStorageWindowsPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("FlutterSecureStorageWindowsPlugin")); LocalAuthPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("LocalAuthPlugin")); + SharePlusWindowsPluginCApiRegisterWithRegistrar( + registry->GetRegistrarForPlugin("SharePlusWindowsPluginCApi")); + UrlLauncherWindowsRegisterWithRegistrar( + registry->GetRegistrarForPlugin("UrlLauncherWindows")); } diff --git a/packages/komodo_defi_sdk/example/windows/flutter/generated_plugins.cmake b/packages/komodo_defi_sdk/example/windows/flutter/generated_plugins.cmake index aa117f18..7b3a5a56 100644 --- a/packages/komodo_defi_sdk/example/windows/flutter/generated_plugins.cmake +++ b/packages/komodo_defi_sdk/example/windows/flutter/generated_plugins.cmake @@ -5,6 +5,8 @@ list(APPEND FLUTTER_PLUGIN_LIST flutter_secure_storage_windows local_auth_windows + share_plus + url_launcher_windows ) list(APPEND FLUTTER_FFI_PLUGIN_LIST 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 2bb6fdb6..9c43d5bf 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 @@ -1,5 +1,6 @@ import 'dart:async'; import 'dart:collection'; +import 'dart:developer'; import 'package:decimal/decimal.dart'; import 'package:komodo_cex_market_data/komodo_cex_market_data.dart'; @@ -127,6 +128,73 @@ class CexMarketDataManager implements MarketDataManager { return assetId.symbol.configSymbol; } + /// Determines if the request can be handled by Komodo price repository + /// NOTE: currently only supports USDT and USD fiat currencies + /// and does not support specific price dates (always uses current price) + bool _canUseKomodoRepository({ + DateTime? priceDate, + String fiatCurrency = 'usdt', + }) { + return priceDate == null && + (fiatCurrency.toLowerCase() == 'usdt' || + fiatCurrency.toLowerCase() == 'usd'); + } + + /// Attempts to get price from Komodo repository + Future _tryKomodoPrice(String symbol) async { + try { + final komodoPrices = await _komodoPriceRepository.getKomodoPrices(); + final priceData = komodoPrices[symbol]; + + if (priceData != null) { + return Decimal.parse(priceData.price.toString()); + } + } catch (e) { + log( + 'Failed to get price from Komodo repository for symbol: $symbol', + error: e, + ); + // Ignore errors and fall back + } + return null; + } + + /// Gets price with automatic fallback logic + Future _getPriceWithFallback( + AssetId assetId, { + DateTime? priceDate, + String fiatCurrency = 'usdt', + }) async { + final symbol = _getTradingSymbol(assetId); + + // Try Komodo repository first if applicable + if (_canUseKomodoRepository( + priceDate: priceDate, + fiatCurrency: fiatCurrency, + )) { + final komodoPrice = await _tryKomodoPrice(symbol); + if (komodoPrice != null) { + return komodoPrice; + } + } + + // Fallback to CEX repository + try { + final priceDouble = await _priceRepository.getCoinFiatPrice( + symbol, + priceDate: priceDate, + fiatCoinId: fiatCurrency, + ); + return Decimal.parse(priceDouble.toString()); + } catch (e) { + log( + 'Failed to get price from Cex Repository for symbol $symbol', + error: e, + ); + return null; + } + } + @override Decimal? priceIfKnown( AssetId assetId, { @@ -143,6 +211,7 @@ class CexMarketDataManager implements MarketDataManager { fiatCurrency: fiatCurrency, ); + // Check cache first return _priceCache[cacheKey]; } @@ -169,23 +238,20 @@ class CexMarketDataManager implements MarketDataManager { return cachedPrice; } - try { - final priceDouble = await _priceRepository.getCoinFiatPrice( - _getTradingSymbol(assetId), - priceDate: priceDate, - fiatCoinId: fiatCurrency, - ); + final price = await _getPriceWithFallback( + assetId, + priceDate: priceDate, + fiatCurrency: fiatCurrency, + ); - // Convert double to Decimal via string - final price = Decimal.parse(priceDouble.toString()); + if (price == null) { + throw StateError('Failed to get price for ${assetId.name}'); + } - // Cache the result - _priceCache[cacheKey] = price; + // Cache the result + _priceCache[cacheKey] = price; - return price; - } catch (e) { - throw StateError('Failed to get price for ${assetId.name}: $e'); - } + return price; } @override @@ -208,23 +274,31 @@ class CexMarketDataManager implements MarketDataManager { return cachedPrice; } + // Check if ticker is known in CEX repository for fallback scenarios final tradingSymbol = _getTradingSymbol(assetId); final isKnownTicker = _knownTickers?.contains(tradingSymbol) ?? false; - if (!isKnownTicker) { + // If not using Komodo repository and ticker is not known in CEX, return null + if (!_canUseKomodoRepository( + priceDate: priceDate, + fiatCurrency: fiatCurrency, + ) && + !isKnownTicker) { return null; } - try { - final price = await fiatPrice( - assetId, - priceDate: priceDate, - fiatCurrency: fiatCurrency, - ); - return price; - } catch (_) { - return null; + final price = await _getPriceWithFallback( + assetId, + priceDate: priceDate, + fiatCurrency: fiatCurrency, + ); + + if (price != null) { + // Cache the result + _priceCache[cacheKey] = price; } + + return price; } @override diff --git a/packages/komodo_defi_sdk/pubspec.yaml b/packages/komodo_defi_sdk/pubspec.yaml index 3e09ae61..0313a03d 100644 --- a/packages/komodo_defi_sdk/pubspec.yaml +++ b/packages/komodo_defi_sdk/pubspec.yaml @@ -13,7 +13,7 @@ publish_to: "none" environment: sdk: ^3.7.0 - flutter: ">=3.29.0 <3.30.0" + flutter: ">=3.29.0 <3.36.0" dependencies: collection: ^1.18.0 diff --git a/packages/komodo_defi_types/analysis_options.yaml b/packages/komodo_defi_types/analysis_options.yaml index a60f3c10..a36ca9e8 100644 --- a/packages/komodo_defi_types/analysis_options.yaml +++ b/packages/komodo_defi_types/analysis_options.yaml @@ -3,6 +3,7 @@ analyzer: public_member_api_docs: ignore invalid_annotation_target: ignore use_if_null_to_convert_nulls_to_bools: ignore + omit_local_variable_types: ignore include: package:very_good_analysis/analysis_options.6.0.0.yaml \ No newline at end of file diff --git a/packages/komodo_defi_types/lib/src/utils/mnemonic_validator.dart b/packages/komodo_defi_types/lib/src/utils/mnemonic_validator.dart index 9d2cb0ce..db4e8084 100644 --- a/packages/komodo_defi_types/lib/src/utils/mnemonic_validator.dart +++ b/packages/komodo_defi_types/lib/src/utils/mnemonic_validator.dart @@ -1,8 +1,12 @@ // TODO: This may be better suited to be moved to the UI package. +import 'dart:typed_data'; + +import 'package:crypto/crypto.dart'; import 'package:flutter/services.dart' show rootBundle; final Set _validMnemonicWords = {}; +final Map _wordToIndex = {}; const _validLengths = [12, 15, 18, 21, 24]; @@ -11,6 +15,8 @@ enum MnemonicFailedReason { customNotSupportedForHd, customNotAllowed, invalidLength, + invalidWord, + invalidChecksum, } class MnemonicValidator { @@ -19,7 +25,13 @@ class MnemonicValidator { final wordlist = await rootBundle.loadString( 'packages/komodo_defi_types/assets/bip-0039/english-wordlist.txt', ); - _validMnemonicWords.addAll(wordlist.split('\n').map((w) => w.trim())); + final words = wordlist.split('\n').map((w) => w.trim()).toList(); + _validMnemonicWords.addAll(words); + + // Build word-to-index mapping for BIP39 validation + for (int i = 0; i < words.length; i++) { + _wordToIndex[words[i]] = i; + } } } @@ -50,19 +62,31 @@ class MnemonicValidator { return MnemonicFailedReason.invalidLength; } - final isValidBip39 = validateBip39(input); + // Get detailed validation error if any + final detailedError = _getDetailedValidationError(input); - if (isValidBip39) { + // If no error, it's a valid BIP39 mnemonic + if (detailedError == null) { return null; } + // For specific errors, return them directly + if (detailedError == MnemonicFailedReason.empty || + detailedError == MnemonicFailedReason.invalidLength) { + return detailedError; + } + + // For HD wallets, any BIP39 error means it's not supported if (isHd) { return MnemonicFailedReason.customNotSupportedForHd; } + // For non-HD wallets, check if custom seeds are allowed if (!allowCustomSeed) { return MnemonicFailedReason.customNotAllowed; } + + // Custom seed is allowed, so return null (valid) return null; } @@ -73,7 +97,7 @@ class MnemonicValidator { 'Call MnemonicValidator.init() first.', ); - final inputWordsList = input.split(' '); + final inputWordsList = input.trim().split(' '); if (!_validLengths.contains(inputWordsList.length)) { return false; @@ -84,6 +108,94 @@ class MnemonicValidator { )) { return false; } - return true; + + // Validate checksum + return _validateChecksum(inputWordsList); + } + + /// Validates the BIP39 checksum for a given mnemonic + bool _validateChecksum(List words) { + try { + // Convert words to indices + final indices = []; + for (final word in words) { + final index = _wordToIndex[word]; + if (index == null) return false; + indices.add(index); + } + + // Convert indices to binary string (11 bits per word) + final binaryString = + indices.map((i) => i.toRadixString(2).padLeft(11, '0')).join(); + + // Calculate entropy and checksum lengths + final totalBits = binaryString.length; + final checksumBits = totalBits ~/ 33; // Checksum is 1 bit per 3 words + final entropyBits = totalBits - checksumBits; + + // Extract entropy and checksum + final entropyBinary = binaryString.substring(0, entropyBits); + final checksumBinary = binaryString.substring(entropyBits); + + // Convert entropy to bytes + final entropyBytes = _binaryToBytes(entropyBinary); + + // Calculate SHA256 hash of entropy + final hash = sha256.convert(entropyBytes); + final hashBits = _bytesToBinary(hash.bytes); + + // Extract first checksumBits from hash + final calculatedChecksum = hashBits.substring(0, checksumBits); + + // Compare checksums + return checksumBinary == calculatedChecksum; + } catch (e) { + return false; + } + } + + /// Converts a binary string to bytes + Uint8List _binaryToBytes(String binary) { + final bytes = []; + for (int i = 0; i < binary.length; i += 8) { + final byte = binary.substring(i, i + 8); + bytes.add(int.parse(byte, radix: 2)); + } + return Uint8List.fromList(bytes); + } + + /// Converts bytes to binary string + String _bytesToBinary(List bytes) { + return bytes.map((b) => b.toRadixString(2).padLeft(8, '0')).join(); } + + /// Gets detailed validation error for a mnemonic + MnemonicFailedReason? _getDetailedValidationError(String input) { + final words = input.trim().split(' '); + + if (words.isEmpty || words.every((w) => w.isEmpty)) { + return MnemonicFailedReason.empty; + } + + if (!_validLengths.contains(words.length)) { + return MnemonicFailedReason.invalidLength; + } + + // Check for invalid words + for (final word in words) { + if (!_validMnemonicWords.contains(word)) { + return MnemonicFailedReason.invalidWord; + } + } + + // Check checksum + if (!_validateChecksum(words)) { + return MnemonicFailedReason.invalidChecksum; + } + + return null; + } + + /// Checks if the wordlist has been initialized + bool get isInitialized => _validMnemonicWords.isNotEmpty; } diff --git a/packages/komodo_defi_types/pubspec.yaml b/packages/komodo_defi_types/pubspec.yaml index 4aa6b12f..e29c9051 100644 --- a/packages/komodo_defi_types/pubspec.yaml +++ b/packages/komodo_defi_types/pubspec.yaml @@ -7,9 +7,10 @@ publish_to: none environment: sdk: ">=3.7.0 <4.0.0" # TODO: Refactor to pure Dart package - flutter: ">=3.29.0 <3.30.0" + flutter: ">=3.29.0 <3.36.0" dependencies: + crypto: ^3.0.6 decimal: ^3.2.1 equatable: ^2.0.7 flutter: diff --git a/packages/komodo_ui/pubspec.yaml b/packages/komodo_ui/pubspec.yaml index 2216893b..6584b47d 100644 --- a/packages/komodo_ui/pubspec.yaml +++ b/packages/komodo_ui/pubspec.yaml @@ -6,7 +6,7 @@ publish_to: none environment: sdk: ^3.7.0 - flutter: ">=3.29.0 <3.30.0" + flutter: ">=3.29.0 <3.36.0" dependencies: decimal: ^3.2.1 diff --git a/packages/komodo_wallet_build_transformer/lib/src/steps/github/github_api_provider.dart b/packages/komodo_wallet_build_transformer/lib/src/steps/github/github_api_provider.dart index 2c6c16db..d0240fe0 100644 --- a/packages/komodo_wallet_build_transformer/lib/src/steps/github/github_api_provider.dart +++ b/packages/komodo_wallet_build_transformer/lib/src/steps/github/github_api_provider.dart @@ -17,8 +17,8 @@ class GithubApiProvider { required String repo, required String branch, String? token, - }) : _branch = branch, - _baseUrl = 'https://api.github.com/repos/$owner/$repo' { + }) : _branch = branch, + _baseUrl = 'https://api.github.com/repos/$owner/$repo' { if (token != null) { _log.finer('Using authentication token for GitHub API requests.'); _headers['Authorization'] = 'Bearer $token'; @@ -32,10 +32,11 @@ class GithubApiProvider { required String baseUrl, required String branch, String? token, - }) : _branch = branch, - _baseUrl = baseUrl { - final repoMatch = RegExp(r'^https://api\.github\.com/repos/([^/]+)/([^/]+)') - .firstMatch(baseUrl); + }) : _branch = branch, + _baseUrl = baseUrl { + final repoMatch = RegExp( + r'^https://api\.github\.com/repos/([^/]+)/([^/]+)', + ).firstMatch(baseUrl); assert(repoMatch != null, 'Invalid GitHub repository URL: $baseUrl'); if (token != null) { @@ -59,8 +60,10 @@ class GithubApiProvider { final fileMetadataUrl = '$_baseUrl/contents/$filePath?ref=$_branch'; _log.finest('Fetching file metadata from $fileMetadataUrl'); - final fileContentResponse = - await http.get(Uri.parse(fileMetadataUrl), headers: _headers); + final fileContentResponse = await http.get( + Uri.parse(fileMetadataUrl), + headers: _headers, + ); if (fileContentResponse.statusCode != 200) { throw Exception( 'Failed to fetch remote file metadata at $fileMetadataUrl: ' @@ -84,14 +87,21 @@ class GithubApiProvider { /// /// Returns a [Future] that completes with a [String] representing the latest /// commit hash. - Future getLatestCommitHash({ - String branch = 'master', - }) async { + Future getLatestCommitHash({String branch = 'master'}) async { final apiUrl = '$_baseUrl/commits/$branch'; - _log.finest('Fetching latest commit hash from $apiUrl'); + _log + ..finest('Fetching latest commit hash from $apiUrl') + ..finest('Using authentication: ${hasToken ? 'yes' : 'no'}'); final response = await http.get(Uri.parse(apiUrl), headers: _headers); if (response.statusCode != 200) { + _log + ..severe( + 'GitHub API request failed: ' + '${response.statusCode} ${response.reasonPhrase}', + ) + ..severe('Response body: ${response.body}') + ..severe('Request headers: $_headers'); throw Exception( 'Failed to retrieve latest commit hash: $branch' '[${response.statusCode}]: ${response.reasonPhrase}', @@ -126,14 +136,17 @@ class GithubApiProvider { final respString = response.body; final data = jsonDecode(respString) as List; - final files = data - .where( - (dynamic item) => (item as Map)['type'] == 'file', - ) - .map( - (dynamic file) => GitHubFile.fromJson(file as Map), - ) - .toList(); + final files = + data + .where( + (dynamic item) => + (item as Map)['type'] == 'file', + ) + .map( + (dynamic file) => + GitHubFile.fromJson(file as Map), + ) + .toList(); _log ..fine('Directory $repoPath contains ${data.length} items') diff --git a/packages/komodo_wallet_cli/bin/update_api_config.dart b/packages/komodo_wallet_cli/bin/update_api_config.dart index d9519d42..b4f643b3 100644 --- a/packages/komodo_wallet_cli/bin/update_api_config.dart +++ b/packages/komodo_wallet_cli/bin/update_api_config.dart @@ -110,7 +110,9 @@ void main(List arguments) async { final repo = args['repo'] as String; final configPath = args['config'] as String; final outputDir = args['output-dir'] as String; - final token = args['token'] as String?; + final token = + args['token'] as String? ?? + Platform.environment['GITHUB_API_PUBLIC_READONLY_TOKEN']; final platform = args['platform'] as String; final source = args['source'] as String; final mirrorUrl = args['mirror-url'] as String; diff --git a/packages/komodo_wallet_cli/pubspec.lock b/packages/komodo_wallet_cli/pubspec.lock index 7f826679..01ec342e 100644 --- a/packages/komodo_wallet_cli/pubspec.lock +++ b/packages/komodo_wallet_cli/pubspec.lock @@ -191,7 +191,7 @@ packages: path: "../komodo_wallet_build_transformer" relative: true source: path - version: "0.2.0+0" + version: "0.3.0+0" lints: dependency: transitive description: diff --git a/playground/pubspec.lock b/playground/pubspec.lock index 05aeabd6..fe1e8ba0 100644 --- a/playground/pubspec.lock +++ b/playground/pubspec.lock @@ -381,35 +381,35 @@ packages: path: "../packages/komodo_coins" relative: true source: path - version: "0.2.0+0" + version: "0.3.0+0" komodo_defi_framework: dependency: "direct main" description: path: "../packages/komodo_defi_framework" relative: true source: path - version: "0.2.0" + version: "0.3.0+0" komodo_defi_rpc_methods: dependency: "direct overridden" description: path: "../packages/komodo_defi_rpc_methods" relative: true source: path - version: "0.2.0+0" + version: "0.3.0+0" komodo_defi_types: dependency: "direct main" description: path: "../packages/komodo_defi_types" relative: true source: path - version: "0.2.0+0" + version: "0.3.0+0" komodo_wallet_build_transformer: dependency: "direct overridden" description: path: "../packages/komodo_wallet_build_transformer" relative: true source: path - version: "0.2.0+0" + version: "0.3.0+0" leak_tracker: dependency: transitive description: diff --git a/playground/pubspec.yaml b/playground/pubspec.yaml index 7c25bd5f..c2bb0fd9 100644 --- a/playground/pubspec.yaml +++ b/playground/pubspec.yaml @@ -21,7 +21,7 @@ version: 1.0.0+1 environment: sdk: ^3.7.0 - flutter: ">=3.29.0 <3.30.0" + flutter: ">=3.29.0 <3.36.0" # Dependencies specify other packages that your package needs in order to work. # To automatically upgrade your package dependencies to the latest versions diff --git a/products/dex_dungeon/pubspec.lock b/products/dex_dungeon/pubspec.lock index 33569640..1ba798c3 100644 --- a/products/dex_dungeon/pubspec.lock +++ b/products/dex_dungeon/pubspec.lock @@ -497,56 +497,56 @@ packages: path: "../../packages/komodo_coins" relative: true source: path - version: "0.2.0+0" + version: "0.3.0+0" komodo_defi_framework: dependency: transitive description: path: "../../packages/komodo_defi_framework" relative: true source: path - version: "0.2.0" + version: "0.3.0+0" komodo_defi_local_auth: dependency: transitive description: path: "../../packages/komodo_defi_local_auth" relative: true source: path - version: "0.2.0+0" + version: "0.3.0+0" komodo_defi_rpc_methods: dependency: transitive description: path: "../../packages/komodo_defi_rpc_methods" relative: true source: path - version: "0.2.0+0" + version: "0.3.0+0" komodo_defi_sdk: dependency: "direct main" description: path: "../../packages/komodo_defi_sdk" relative: true source: path - version: "0.2.0+0" + version: "0.3.0+0" komodo_defi_types: dependency: "direct main" description: path: "../../packages/komodo_defi_types" relative: true source: path - version: "0.2.0+0" + version: "0.3.0+0" komodo_ui: dependency: transitive description: path: "../../packages/komodo_ui" relative: true source: path - version: "0.2.0+0" + version: "0.3.0+0" komodo_wallet_build_transformer: dependency: transitive description: path: "../../packages/komodo_wallet_build_transformer" relative: true source: path - version: "0.2.0+0" + version: "0.3.0+0" leak_tracker: dependency: transitive description: