diff --git a/packages/dragon_charts/.gitignore b/packages/dragon_charts_flutter/.gitignore similarity index 100% rename from packages/dragon_charts/.gitignore rename to packages/dragon_charts_flutter/.gitignore diff --git a/packages/dragon_charts/.metadata b/packages/dragon_charts_flutter/.metadata similarity index 100% rename from packages/dragon_charts/.metadata rename to packages/dragon_charts_flutter/.metadata diff --git a/packages/dragon_charts/CHANGELOG.md b/packages/dragon_charts_flutter/CHANGELOG.md similarity index 100% rename from packages/dragon_charts/CHANGELOG.md rename to packages/dragon_charts_flutter/CHANGELOG.md diff --git a/packages/dragon_charts/CONTRIBUTING.md b/packages/dragon_charts_flutter/CONTRIBUTING.md similarity index 100% rename from packages/dragon_charts/CONTRIBUTING.md rename to packages/dragon_charts_flutter/CONTRIBUTING.md diff --git a/packages/dragon_charts/LICENSE b/packages/dragon_charts_flutter/LICENSE similarity index 100% rename from packages/dragon_charts/LICENSE rename to packages/dragon_charts_flutter/LICENSE diff --git a/packages/dragon_charts/README.md b/packages/dragon_charts_flutter/README.md similarity index 100% rename from packages/dragon_charts/README.md rename to packages/dragon_charts_flutter/README.md diff --git a/packages/dragon_charts/analysis_options.yaml b/packages/dragon_charts_flutter/analysis_options.yaml similarity index 100% rename from packages/dragon_charts/analysis_options.yaml rename to packages/dragon_charts_flutter/analysis_options.yaml diff --git a/packages/dragon_charts/example/.gitignore b/packages/dragon_charts_flutter/example/.gitignore similarity index 100% rename from packages/dragon_charts/example/.gitignore rename to packages/dragon_charts_flutter/example/.gitignore diff --git a/packages/dragon_charts/example/.metadata b/packages/dragon_charts_flutter/example/.metadata similarity index 100% rename from packages/dragon_charts/example/.metadata rename to packages/dragon_charts_flutter/example/.metadata diff --git a/packages/dragon_charts/example/README.md b/packages/dragon_charts_flutter/example/README.md similarity index 100% rename from packages/dragon_charts/example/README.md rename to packages/dragon_charts_flutter/example/README.md diff --git a/packages/dragon_charts/example/analysis_options.yaml b/packages/dragon_charts_flutter/example/analysis_options.yaml similarity index 100% rename from packages/dragon_charts/example/analysis_options.yaml rename to packages/dragon_charts_flutter/example/analysis_options.yaml diff --git a/packages/dragon_charts/example/android/.gitignore b/packages/dragon_charts_flutter/example/android/.gitignore similarity index 100% rename from packages/dragon_charts/example/android/.gitignore rename to packages/dragon_charts_flutter/example/android/.gitignore diff --git a/packages/dragon_charts/example/android/app/build.gradle b/packages/dragon_charts_flutter/example/android/app/build.gradle similarity index 100% rename from packages/dragon_charts/example/android/app/build.gradle rename to packages/dragon_charts_flutter/example/android/app/build.gradle diff --git a/packages/dragon_charts/example/android/app/src/debug/AndroidManifest.xml b/packages/dragon_charts_flutter/example/android/app/src/debug/AndroidManifest.xml similarity index 100% rename from packages/dragon_charts/example/android/app/src/debug/AndroidManifest.xml rename to packages/dragon_charts_flutter/example/android/app/src/debug/AndroidManifest.xml diff --git a/packages/dragon_charts/example/android/app/src/main/AndroidManifest.xml b/packages/dragon_charts_flutter/example/android/app/src/main/AndroidManifest.xml similarity index 100% rename from packages/dragon_charts/example/android/app/src/main/AndroidManifest.xml rename to packages/dragon_charts_flutter/example/android/app/src/main/AndroidManifest.xml diff --git a/packages/dragon_charts/example/android/app/src/main/kotlin/com/example/example/MainActivity.kt b/packages/dragon_charts_flutter/example/android/app/src/main/kotlin/com/example/example/MainActivity.kt similarity index 100% rename from packages/dragon_charts/example/android/app/src/main/kotlin/com/example/example/MainActivity.kt rename to packages/dragon_charts_flutter/example/android/app/src/main/kotlin/com/example/example/MainActivity.kt diff --git a/packages/dragon_charts/example/android/app/src/main/res/drawable-v21/launch_background.xml b/packages/dragon_charts_flutter/example/android/app/src/main/res/drawable-v21/launch_background.xml similarity index 100% rename from packages/dragon_charts/example/android/app/src/main/res/drawable-v21/launch_background.xml rename to packages/dragon_charts_flutter/example/android/app/src/main/res/drawable-v21/launch_background.xml diff --git a/packages/dragon_charts/example/android/app/src/main/res/drawable/launch_background.xml b/packages/dragon_charts_flutter/example/android/app/src/main/res/drawable/launch_background.xml similarity index 100% rename from packages/dragon_charts/example/android/app/src/main/res/drawable/launch_background.xml rename to packages/dragon_charts_flutter/example/android/app/src/main/res/drawable/launch_background.xml diff --git a/packages/dragon_charts/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/packages/dragon_charts_flutter/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png similarity index 100% rename from packages/dragon_charts/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png rename to packages/dragon_charts_flutter/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png diff --git a/packages/dragon_charts/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/packages/dragon_charts_flutter/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png similarity index 100% rename from packages/dragon_charts/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png rename to packages/dragon_charts_flutter/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png diff --git a/packages/dragon_charts/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/packages/dragon_charts_flutter/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png similarity index 100% rename from packages/dragon_charts/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png rename to packages/dragon_charts_flutter/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png diff --git a/packages/dragon_charts/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/packages/dragon_charts_flutter/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png similarity index 100% rename from packages/dragon_charts/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png rename to packages/dragon_charts_flutter/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png diff --git a/packages/dragon_charts/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/packages/dragon_charts_flutter/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png similarity index 100% rename from packages/dragon_charts/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png rename to packages/dragon_charts_flutter/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png diff --git a/packages/dragon_charts/example/android/app/src/main/res/values-night/styles.xml b/packages/dragon_charts_flutter/example/android/app/src/main/res/values-night/styles.xml similarity index 100% rename from packages/dragon_charts/example/android/app/src/main/res/values-night/styles.xml rename to packages/dragon_charts_flutter/example/android/app/src/main/res/values-night/styles.xml diff --git a/packages/dragon_charts/example/android/app/src/main/res/values/styles.xml b/packages/dragon_charts_flutter/example/android/app/src/main/res/values/styles.xml similarity index 100% rename from packages/dragon_charts/example/android/app/src/main/res/values/styles.xml rename to packages/dragon_charts_flutter/example/android/app/src/main/res/values/styles.xml diff --git a/packages/dragon_charts/example/android/app/src/profile/AndroidManifest.xml b/packages/dragon_charts_flutter/example/android/app/src/profile/AndroidManifest.xml similarity index 100% rename from packages/dragon_charts/example/android/app/src/profile/AndroidManifest.xml rename to packages/dragon_charts_flutter/example/android/app/src/profile/AndroidManifest.xml diff --git a/packages/dragon_charts/example/android/build.gradle b/packages/dragon_charts_flutter/example/android/build.gradle similarity index 100% rename from packages/dragon_charts/example/android/build.gradle rename to packages/dragon_charts_flutter/example/android/build.gradle diff --git a/packages/dragon_charts/example/android/gradle.properties b/packages/dragon_charts_flutter/example/android/gradle.properties similarity index 100% rename from packages/dragon_charts/example/android/gradle.properties rename to packages/dragon_charts_flutter/example/android/gradle.properties diff --git a/packages/dragon_charts/example/android/gradle/wrapper/gradle-wrapper.properties b/packages/dragon_charts_flutter/example/android/gradle/wrapper/gradle-wrapper.properties similarity index 100% rename from packages/dragon_charts/example/android/gradle/wrapper/gradle-wrapper.properties rename to packages/dragon_charts_flutter/example/android/gradle/wrapper/gradle-wrapper.properties diff --git a/packages/dragon_charts/example/android/settings.gradle b/packages/dragon_charts_flutter/example/android/settings.gradle similarity index 100% rename from packages/dragon_charts/example/android/settings.gradle rename to packages/dragon_charts_flutter/example/android/settings.gradle diff --git a/packages/dragon_charts/example/ios/.gitignore b/packages/dragon_charts_flutter/example/ios/.gitignore similarity index 100% rename from packages/dragon_charts/example/ios/.gitignore rename to packages/dragon_charts_flutter/example/ios/.gitignore diff --git a/packages/dragon_charts/example/ios/Flutter/AppFrameworkInfo.plist b/packages/dragon_charts_flutter/example/ios/Flutter/AppFrameworkInfo.plist similarity index 100% rename from packages/dragon_charts/example/ios/Flutter/AppFrameworkInfo.plist rename to packages/dragon_charts_flutter/example/ios/Flutter/AppFrameworkInfo.plist diff --git a/packages/dragon_charts/example/ios/Flutter/Debug.xcconfig b/packages/dragon_charts_flutter/example/ios/Flutter/Debug.xcconfig similarity index 100% rename from packages/dragon_charts/example/ios/Flutter/Debug.xcconfig rename to packages/dragon_charts_flutter/example/ios/Flutter/Debug.xcconfig diff --git a/packages/dragon_charts/example/ios/Flutter/Release.xcconfig b/packages/dragon_charts_flutter/example/ios/Flutter/Release.xcconfig similarity index 100% rename from packages/dragon_charts/example/ios/Flutter/Release.xcconfig rename to packages/dragon_charts_flutter/example/ios/Flutter/Release.xcconfig diff --git a/packages/dragon_charts/example/ios/Runner.xcodeproj/project.pbxproj b/packages/dragon_charts_flutter/example/ios/Runner.xcodeproj/project.pbxproj similarity index 100% rename from packages/dragon_charts/example/ios/Runner.xcodeproj/project.pbxproj rename to packages/dragon_charts_flutter/example/ios/Runner.xcodeproj/project.pbxproj diff --git a/packages/dragon_charts/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/packages/dragon_charts_flutter/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata similarity index 100% rename from packages/dragon_charts/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata rename to packages/dragon_charts_flutter/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata diff --git a/packages/dragon_charts/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/packages/dragon_charts_flutter/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist similarity index 100% rename from packages/dragon_charts/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist rename to packages/dragon_charts_flutter/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist diff --git a/packages/dragon_charts/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/packages/dragon_charts_flutter/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings similarity index 100% rename from packages/dragon_charts/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings rename to packages/dragon_charts_flutter/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings diff --git a/packages/dragon_charts/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/packages/dragon_charts_flutter/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme similarity index 100% rename from packages/dragon_charts/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme rename to packages/dragon_charts_flutter/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme diff --git a/packages/dragon_charts/example/ios/Runner.xcworkspace/contents.xcworkspacedata b/packages/dragon_charts_flutter/example/ios/Runner.xcworkspace/contents.xcworkspacedata similarity index 100% rename from packages/dragon_charts/example/ios/Runner.xcworkspace/contents.xcworkspacedata rename to packages/dragon_charts_flutter/example/ios/Runner.xcworkspace/contents.xcworkspacedata diff --git a/packages/dragon_charts/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/packages/dragon_charts_flutter/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist similarity index 100% rename from packages/dragon_charts/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist rename to packages/dragon_charts_flutter/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist diff --git a/packages/dragon_charts/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/packages/dragon_charts_flutter/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings similarity index 100% rename from packages/dragon_charts/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings rename to packages/dragon_charts_flutter/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings diff --git a/packages/dragon_charts/example/ios/Runner/AppDelegate.swift b/packages/dragon_charts_flutter/example/ios/Runner/AppDelegate.swift similarity index 100% rename from packages/dragon_charts/example/ios/Runner/AppDelegate.swift rename to packages/dragon_charts_flutter/example/ios/Runner/AppDelegate.swift diff --git a/packages/dragon_charts/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/packages/dragon_charts_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json similarity index 100% rename from packages/dragon_charts/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json rename to packages/dragon_charts_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json diff --git a/packages/dragon_charts/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/packages/dragon_charts_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png similarity index 100% rename from packages/dragon_charts/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png rename to packages/dragon_charts_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png diff --git a/packages/dragon_charts/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/packages/dragon_charts_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png similarity index 100% rename from packages/dragon_charts/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png rename to packages/dragon_charts_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png diff --git a/packages/dragon_charts/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/packages/dragon_charts_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png similarity index 100% rename from packages/dragon_charts/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png rename to packages/dragon_charts_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png diff --git a/packages/dragon_charts/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/packages/dragon_charts_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png similarity index 100% rename from packages/dragon_charts/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png rename to packages/dragon_charts_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png diff --git a/packages/dragon_charts/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/packages/dragon_charts_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png similarity index 100% rename from packages/dragon_charts/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png rename to packages/dragon_charts_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png diff --git a/packages/dragon_charts/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/packages/dragon_charts_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png similarity index 100% rename from packages/dragon_charts/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png rename to packages/dragon_charts_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png diff --git a/packages/dragon_charts/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/packages/dragon_charts_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png similarity index 100% rename from packages/dragon_charts/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png rename to packages/dragon_charts_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png diff --git a/packages/dragon_charts/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/packages/dragon_charts_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png similarity index 100% rename from packages/dragon_charts/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png rename to packages/dragon_charts_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png diff --git a/packages/dragon_charts/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/packages/dragon_charts_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png similarity index 100% rename from packages/dragon_charts/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png rename to packages/dragon_charts_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png diff --git a/packages/dragon_charts/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/packages/dragon_charts_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png similarity index 100% rename from packages/dragon_charts/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png rename to packages/dragon_charts_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png diff --git a/packages/dragon_charts/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/packages/dragon_charts_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png similarity index 100% rename from packages/dragon_charts/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png rename to packages/dragon_charts_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png diff --git a/packages/dragon_charts/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/packages/dragon_charts_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png similarity index 100% rename from packages/dragon_charts/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png rename to packages/dragon_charts_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png diff --git a/packages/dragon_charts/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/packages/dragon_charts_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png similarity index 100% rename from packages/dragon_charts/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png rename to packages/dragon_charts_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png diff --git a/packages/dragon_charts/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/packages/dragon_charts_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png similarity index 100% rename from packages/dragon_charts/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png rename to packages/dragon_charts_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png diff --git a/packages/dragon_charts/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/packages/dragon_charts_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png similarity index 100% rename from packages/dragon_charts/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png rename to packages/dragon_charts_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png diff --git a/packages/dragon_charts/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json b/packages/dragon_charts_flutter/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json similarity index 100% rename from packages/dragon_charts/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json rename to packages/dragon_charts_flutter/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json diff --git a/packages/dragon_charts/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png b/packages/dragon_charts_flutter/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png similarity index 100% rename from packages/dragon_charts/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png rename to packages/dragon_charts_flutter/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png diff --git a/packages/dragon_charts/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png b/packages/dragon_charts_flutter/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png similarity index 100% rename from packages/dragon_charts/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png rename to packages/dragon_charts_flutter/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png diff --git a/packages/dragon_charts/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png b/packages/dragon_charts_flutter/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png similarity index 100% rename from packages/dragon_charts/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png rename to packages/dragon_charts_flutter/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png diff --git a/packages/dragon_charts/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md b/packages/dragon_charts_flutter/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md similarity index 100% rename from packages/dragon_charts/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md rename to packages/dragon_charts_flutter/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md diff --git a/packages/dragon_charts/example/ios/Runner/Base.lproj/LaunchScreen.storyboard b/packages/dragon_charts_flutter/example/ios/Runner/Base.lproj/LaunchScreen.storyboard similarity index 100% rename from packages/dragon_charts/example/ios/Runner/Base.lproj/LaunchScreen.storyboard rename to packages/dragon_charts_flutter/example/ios/Runner/Base.lproj/LaunchScreen.storyboard diff --git a/packages/dragon_charts/example/ios/Runner/Base.lproj/Main.storyboard b/packages/dragon_charts_flutter/example/ios/Runner/Base.lproj/Main.storyboard similarity index 100% rename from packages/dragon_charts/example/ios/Runner/Base.lproj/Main.storyboard rename to packages/dragon_charts_flutter/example/ios/Runner/Base.lproj/Main.storyboard diff --git a/packages/dragon_charts/example/ios/Runner/Info.plist b/packages/dragon_charts_flutter/example/ios/Runner/Info.plist similarity index 100% rename from packages/dragon_charts/example/ios/Runner/Info.plist rename to packages/dragon_charts_flutter/example/ios/Runner/Info.plist diff --git a/packages/dragon_charts/example/ios/Runner/Runner-Bridging-Header.h b/packages/dragon_charts_flutter/example/ios/Runner/Runner-Bridging-Header.h similarity index 100% rename from packages/dragon_charts/example/ios/Runner/Runner-Bridging-Header.h rename to packages/dragon_charts_flutter/example/ios/Runner/Runner-Bridging-Header.h diff --git a/packages/dragon_charts/example/ios/RunnerTests/RunnerTests.swift b/packages/dragon_charts_flutter/example/ios/RunnerTests/RunnerTests.swift similarity index 100% rename from packages/dragon_charts/example/ios/RunnerTests/RunnerTests.swift rename to packages/dragon_charts_flutter/example/ios/RunnerTests/RunnerTests.swift diff --git a/packages/dragon_charts/example/lib/blocs/chart_bloc.dart b/packages/dragon_charts_flutter/example/lib/blocs/chart_bloc.dart similarity index 100% rename from packages/dragon_charts/example/lib/blocs/chart_bloc.dart rename to packages/dragon_charts_flutter/example/lib/blocs/chart_bloc.dart diff --git a/packages/dragon_charts/example/lib/blocs/chart_event.dart b/packages/dragon_charts_flutter/example/lib/blocs/chart_event.dart similarity index 100% rename from packages/dragon_charts/example/lib/blocs/chart_event.dart rename to packages/dragon_charts_flutter/example/lib/blocs/chart_event.dart diff --git a/packages/dragon_charts/example/lib/blocs/chart_state.dart b/packages/dragon_charts_flutter/example/lib/blocs/chart_state.dart similarity index 100% rename from packages/dragon_charts/example/lib/blocs/chart_state.dart rename to packages/dragon_charts_flutter/example/lib/blocs/chart_state.dart diff --git a/packages/dragon_charts/example/lib/main.dart b/packages/dragon_charts_flutter/example/lib/main.dart similarity index 100% rename from packages/dragon_charts/example/lib/main.dart rename to packages/dragon_charts_flutter/example/lib/main.dart diff --git a/packages/dragon_charts/example/lib/ui/chart_screen.dart b/packages/dragon_charts_flutter/example/lib/ui/chart_screen.dart similarity index 100% rename from packages/dragon_charts/example/lib/ui/chart_screen.dart rename to packages/dragon_charts_flutter/example/lib/ui/chart_screen.dart diff --git a/packages/dragon_charts/example/macos/.gitignore b/packages/dragon_charts_flutter/example/macos/.gitignore similarity index 100% rename from packages/dragon_charts/example/macos/.gitignore rename to packages/dragon_charts_flutter/example/macos/.gitignore diff --git a/packages/dragon_charts/example/macos/Flutter/Flutter-Debug.xcconfig b/packages/dragon_charts_flutter/example/macos/Flutter/Flutter-Debug.xcconfig similarity index 100% rename from packages/dragon_charts/example/macos/Flutter/Flutter-Debug.xcconfig rename to packages/dragon_charts_flutter/example/macos/Flutter/Flutter-Debug.xcconfig diff --git a/packages/dragon_charts/example/macos/Flutter/Flutter-Release.xcconfig b/packages/dragon_charts_flutter/example/macos/Flutter/Flutter-Release.xcconfig similarity index 100% rename from packages/dragon_charts/example/macos/Flutter/Flutter-Release.xcconfig rename to packages/dragon_charts_flutter/example/macos/Flutter/Flutter-Release.xcconfig diff --git a/packages/dragon_charts/example/macos/Flutter/GeneratedPluginRegistrant.swift b/packages/dragon_charts_flutter/example/macos/Flutter/GeneratedPluginRegistrant.swift similarity index 100% rename from packages/dragon_charts/example/macos/Flutter/GeneratedPluginRegistrant.swift rename to packages/dragon_charts_flutter/example/macos/Flutter/GeneratedPluginRegistrant.swift diff --git a/packages/dragon_charts/example/macos/Runner.xcodeproj/project.pbxproj b/packages/dragon_charts_flutter/example/macos/Runner.xcodeproj/project.pbxproj similarity index 100% rename from packages/dragon_charts/example/macos/Runner.xcodeproj/project.pbxproj rename to packages/dragon_charts_flutter/example/macos/Runner.xcodeproj/project.pbxproj diff --git a/packages/dragon_charts/example/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/packages/dragon_charts_flutter/example/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist similarity index 100% rename from packages/dragon_charts/example/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist rename to packages/dragon_charts_flutter/example/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist diff --git a/packages/dragon_charts/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/packages/dragon_charts_flutter/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme similarity index 100% rename from packages/dragon_charts/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme rename to packages/dragon_charts_flutter/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme diff --git a/packages/dragon_charts/example/macos/Runner.xcworkspace/contents.xcworkspacedata b/packages/dragon_charts_flutter/example/macos/Runner.xcworkspace/contents.xcworkspacedata similarity index 100% rename from packages/dragon_charts/example/macos/Runner.xcworkspace/contents.xcworkspacedata rename to packages/dragon_charts_flutter/example/macos/Runner.xcworkspace/contents.xcworkspacedata diff --git a/packages/dragon_charts/example/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/packages/dragon_charts_flutter/example/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist similarity index 100% rename from packages/dragon_charts/example/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist rename to packages/dragon_charts_flutter/example/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist diff --git a/packages/dragon_charts/example/macos/Runner/AppDelegate.swift b/packages/dragon_charts_flutter/example/macos/Runner/AppDelegate.swift similarity index 100% rename from packages/dragon_charts/example/macos/Runner/AppDelegate.swift rename to packages/dragon_charts_flutter/example/macos/Runner/AppDelegate.swift diff --git a/packages/dragon_charts/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/packages/dragon_charts_flutter/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json similarity index 100% rename from packages/dragon_charts/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json rename to packages/dragon_charts_flutter/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json diff --git a/packages/dragon_charts/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png b/packages/dragon_charts_flutter/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png similarity index 100% rename from packages/dragon_charts/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png rename to packages/dragon_charts_flutter/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png diff --git a/packages/dragon_charts/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png b/packages/dragon_charts_flutter/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png similarity index 100% rename from packages/dragon_charts/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png rename to packages/dragon_charts_flutter/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png diff --git a/packages/dragon_charts/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png b/packages/dragon_charts_flutter/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png similarity index 100% rename from packages/dragon_charts/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png rename to packages/dragon_charts_flutter/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png diff --git a/packages/dragon_charts/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png b/packages/dragon_charts_flutter/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png similarity index 100% rename from packages/dragon_charts/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png rename to packages/dragon_charts_flutter/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png diff --git a/packages/dragon_charts/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png b/packages/dragon_charts_flutter/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png similarity index 100% rename from packages/dragon_charts/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png rename to packages/dragon_charts_flutter/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png diff --git a/packages/dragon_charts/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png b/packages/dragon_charts_flutter/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png similarity index 100% rename from packages/dragon_charts/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png rename to packages/dragon_charts_flutter/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png diff --git a/packages/dragon_charts/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png b/packages/dragon_charts_flutter/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png similarity index 100% rename from packages/dragon_charts/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png rename to packages/dragon_charts_flutter/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png diff --git a/packages/dragon_charts/example/macos/Runner/Base.lproj/MainMenu.xib b/packages/dragon_charts_flutter/example/macos/Runner/Base.lproj/MainMenu.xib similarity index 100% rename from packages/dragon_charts/example/macos/Runner/Base.lproj/MainMenu.xib rename to packages/dragon_charts_flutter/example/macos/Runner/Base.lproj/MainMenu.xib diff --git a/packages/dragon_charts/example/macos/Runner/Configs/AppInfo.xcconfig b/packages/dragon_charts_flutter/example/macos/Runner/Configs/AppInfo.xcconfig similarity index 100% rename from packages/dragon_charts/example/macos/Runner/Configs/AppInfo.xcconfig rename to packages/dragon_charts_flutter/example/macos/Runner/Configs/AppInfo.xcconfig diff --git a/packages/dragon_charts/example/macos/Runner/Configs/Debug.xcconfig b/packages/dragon_charts_flutter/example/macos/Runner/Configs/Debug.xcconfig similarity index 100% rename from packages/dragon_charts/example/macos/Runner/Configs/Debug.xcconfig rename to packages/dragon_charts_flutter/example/macos/Runner/Configs/Debug.xcconfig diff --git a/packages/dragon_charts/example/macos/Runner/Configs/Release.xcconfig b/packages/dragon_charts_flutter/example/macos/Runner/Configs/Release.xcconfig similarity index 100% rename from packages/dragon_charts/example/macos/Runner/Configs/Release.xcconfig rename to packages/dragon_charts_flutter/example/macos/Runner/Configs/Release.xcconfig diff --git a/packages/dragon_charts/example/macos/Runner/Configs/Warnings.xcconfig b/packages/dragon_charts_flutter/example/macos/Runner/Configs/Warnings.xcconfig similarity index 100% rename from packages/dragon_charts/example/macos/Runner/Configs/Warnings.xcconfig rename to packages/dragon_charts_flutter/example/macos/Runner/Configs/Warnings.xcconfig diff --git a/packages/dragon_charts/example/macos/Runner/DebugProfile.entitlements b/packages/dragon_charts_flutter/example/macos/Runner/DebugProfile.entitlements similarity index 100% rename from packages/dragon_charts/example/macos/Runner/DebugProfile.entitlements rename to packages/dragon_charts_flutter/example/macos/Runner/DebugProfile.entitlements diff --git a/packages/dragon_charts/example/macos/Runner/Info.plist b/packages/dragon_charts_flutter/example/macos/Runner/Info.plist similarity index 100% rename from packages/dragon_charts/example/macos/Runner/Info.plist rename to packages/dragon_charts_flutter/example/macos/Runner/Info.plist diff --git a/packages/dragon_charts/example/macos/Runner/MainFlutterWindow.swift b/packages/dragon_charts_flutter/example/macos/Runner/MainFlutterWindow.swift similarity index 100% rename from packages/dragon_charts/example/macos/Runner/MainFlutterWindow.swift rename to packages/dragon_charts_flutter/example/macos/Runner/MainFlutterWindow.swift diff --git a/packages/dragon_charts/example/macos/Runner/Release.entitlements b/packages/dragon_charts_flutter/example/macos/Runner/Release.entitlements similarity index 100% rename from packages/dragon_charts/example/macos/Runner/Release.entitlements rename to packages/dragon_charts_flutter/example/macos/Runner/Release.entitlements diff --git a/packages/dragon_charts/example/macos/RunnerTests/RunnerTests.swift b/packages/dragon_charts_flutter/example/macos/RunnerTests/RunnerTests.swift similarity index 100% rename from packages/dragon_charts/example/macos/RunnerTests/RunnerTests.swift rename to packages/dragon_charts_flutter/example/macos/RunnerTests/RunnerTests.swift diff --git a/packages/dragon_charts/example/pubspec.lock b/packages/dragon_charts_flutter/example/pubspec.lock similarity index 72% rename from packages/dragon_charts/example/pubspec.lock rename to packages/dragon_charts_flutter/example/pubspec.lock index 84acc143..3400241f 100644 --- a/packages/dragon_charts/example/pubspec.lock +++ b/packages/dragon_charts_flutter/example/pubspec.lock @@ -5,10 +5,10 @@ packages: dependency: transitive description: name: async - sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c" + sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb" url: "https://pub.dev" source: hosted - version: "2.11.0" + version: "2.13.0" bloc: dependency: "direct main" description: @@ -21,41 +21,41 @@ packages: dependency: transitive description: name: boolean_selector - sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66" + sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea" url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.1.2" characters: dependency: transitive description: name: characters - sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605" + sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 url: "https://pub.dev" source: hosted - version: "1.3.0" + version: "1.4.0" clock: dependency: transitive description: name: clock - sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf + sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b url: "https://pub.dev" source: hosted - version: "1.1.1" + version: "1.1.2" collection: dependency: transitive description: name: collection - sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a + sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" url: "https://pub.dev" source: hosted - version: "1.18.0" + version: "1.19.1" dragon_charts_flutter: dependency: "direct main" description: path: ".." relative: true source: path - version: "0.1.0" + version: "0.1.1-dev.1" equatable: dependency: "direct main" description: @@ -68,10 +68,10 @@ packages: dependency: transitive description: name: fake_async - sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78" + sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44" url: "https://pub.dev" source: hosted - version: "1.3.1" + version: "1.3.3" flutter: dependency: "direct main" description: flutter @@ -102,18 +102,18 @@ packages: dependency: transitive description: name: leak_tracker - sha256: "7f0df31977cb2c0b88585095d168e689669a2cc9b97c309665e3386f3e9d341a" + sha256: "6bb818ecbdffe216e81182c2f0714a2e62b593f4a4f13098713ff1685dfb6ab0" url: "https://pub.dev" source: hosted - version: "10.0.4" + version: "10.0.9" leak_tracker_flutter_testing: dependency: transitive description: name: leak_tracker_flutter_testing - sha256: "06e98f569d004c1315b991ded39924b21af84cf14cc94791b8aea337d25b57f8" + sha256: f8b613e7e6a13ec79cfdc0e97638fddb3ab848452eff057653abd3edba760573 url: "https://pub.dev" source: hosted - version: "3.0.3" + version: "3.0.9" leak_tracker_testing: dependency: transitive description: @@ -134,26 +134,26 @@ packages: dependency: transitive description: name: matcher - sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb + sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 url: "https://pub.dev" source: hosted - version: "0.12.16+1" + version: "0.12.17" material_color_utilities: dependency: transitive description: name: material_color_utilities - sha256: "0e0a020085b65b6083975e499759762399b4475f766c21668c4ecca34ea74e5a" + sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec url: "https://pub.dev" source: hosted - version: "0.8.0" + version: "0.11.1" meta: dependency: transitive description: name: meta - sha256: "7687075e408b093f36e6bbf6c91878cc0d4cd10f409506f7bc996f68220b9136" + sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c url: "https://pub.dev" source: hosted - version: "1.12.0" + version: "1.16.0" nested: dependency: transitive description: @@ -166,10 +166,10 @@ packages: dependency: transitive description: name: path - sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af" + sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" url: "https://pub.dev" source: hosted - version: "1.9.0" + version: "1.9.1" provider: dependency: transitive description: @@ -182,55 +182,55 @@ packages: dependency: transitive description: flutter source: sdk - version: "0.0.99" + version: "0.0.0" source_span: dependency: transitive description: name: source_span - sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c" + sha256: "254ee5351d6cb365c859e20ee823c3bb479bf4a293c22d17a9f1bf144ce86f7c" url: "https://pub.dev" source: hosted - version: "1.10.0" + version: "1.10.1" stack_trace: dependency: transitive description: name: stack_trace - sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b" + sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" url: "https://pub.dev" source: hosted - version: "1.11.1" + version: "1.12.1" stream_channel: dependency: transitive description: name: stream_channel - sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7 + sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" url: "https://pub.dev" source: hosted - version: "2.1.2" + version: "2.1.4" string_scanner: dependency: transitive description: name: string_scanner - sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde" + sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43" url: "https://pub.dev" source: hosted - version: "1.2.0" + version: "1.4.1" term_glyph: dependency: transitive description: name: term_glyph - sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84 + sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e" url: "https://pub.dev" source: hosted - version: "1.2.1" + version: "1.2.2" test_api: dependency: transitive description: name: test_api - sha256: "9955ae474176f7ac8ee4e989dadfb411a58c30415bcfb648fa04b2b8a03afa7f" + sha256: fb31f383e2ee25fbbfe06b40fe21e1e458d14080e3c67e7ba0acfde4df4e0bbd url: "https://pub.dev" source: hosted - version: "0.7.0" + version: "0.7.4" vector_math: dependency: transitive description: @@ -243,10 +243,10 @@ packages: dependency: transitive description: name: vm_service - sha256: "3923c89304b715fb1eb6423f017651664a03bf5f4b29983627c4da791f74a4ec" + sha256: ddfa8d30d89985b96407efce8acbdd124701f96741f2d981ca860662f1c0dc02 url: "https://pub.dev" source: hosted - version: "14.2.1" + version: "15.0.0" sdks: - dart: ">=3.3.0 <4.0.0" + dart: ">=3.7.0-0 <4.0.0" flutter: ">=3.18.0-18.0.pre.54" diff --git a/packages/dragon_charts/example/pubspec.yaml b/packages/dragon_charts_flutter/example/pubspec.yaml similarity index 100% rename from packages/dragon_charts/example/pubspec.yaml rename to packages/dragon_charts_flutter/example/pubspec.yaml diff --git a/packages/dragon_charts/example/web/favicon.png b/packages/dragon_charts_flutter/example/web/favicon.png similarity index 100% rename from packages/dragon_charts/example/web/favicon.png rename to packages/dragon_charts_flutter/example/web/favicon.png diff --git a/packages/dragon_charts/example/web/icons/Icon-192.png b/packages/dragon_charts_flutter/example/web/icons/Icon-192.png similarity index 100% rename from packages/dragon_charts/example/web/icons/Icon-192.png rename to packages/dragon_charts_flutter/example/web/icons/Icon-192.png diff --git a/packages/dragon_charts/example/web/icons/Icon-512.png b/packages/dragon_charts_flutter/example/web/icons/Icon-512.png similarity index 100% rename from packages/dragon_charts/example/web/icons/Icon-512.png rename to packages/dragon_charts_flutter/example/web/icons/Icon-512.png diff --git a/packages/dragon_charts/example/web/icons/Icon-maskable-192.png b/packages/dragon_charts_flutter/example/web/icons/Icon-maskable-192.png similarity index 100% rename from packages/dragon_charts/example/web/icons/Icon-maskable-192.png rename to packages/dragon_charts_flutter/example/web/icons/Icon-maskable-192.png diff --git a/packages/dragon_charts/example/web/icons/Icon-maskable-512.png b/packages/dragon_charts_flutter/example/web/icons/Icon-maskable-512.png similarity index 100% rename from packages/dragon_charts/example/web/icons/Icon-maskable-512.png rename to packages/dragon_charts_flutter/example/web/icons/Icon-maskable-512.png diff --git a/packages/dragon_charts/example/web/index.html b/packages/dragon_charts_flutter/example/web/index.html similarity index 100% rename from packages/dragon_charts/example/web/index.html rename to packages/dragon_charts_flutter/example/web/index.html diff --git a/packages/dragon_charts/example/web/manifest.json b/packages/dragon_charts_flutter/example/web/manifest.json similarity index 100% rename from packages/dragon_charts/example/web/manifest.json rename to packages/dragon_charts_flutter/example/web/manifest.json diff --git a/packages/dragon_charts/example/windows/.gitignore b/packages/dragon_charts_flutter/example/windows/.gitignore similarity index 100% rename from packages/dragon_charts/example/windows/.gitignore rename to packages/dragon_charts_flutter/example/windows/.gitignore diff --git a/packages/dragon_charts/example/windows/CMakeLists.txt b/packages/dragon_charts_flutter/example/windows/CMakeLists.txt similarity index 100% rename from packages/dragon_charts/example/windows/CMakeLists.txt rename to packages/dragon_charts_flutter/example/windows/CMakeLists.txt diff --git a/packages/dragon_charts/example/windows/flutter/CMakeLists.txt b/packages/dragon_charts_flutter/example/windows/flutter/CMakeLists.txt similarity index 100% rename from packages/dragon_charts/example/windows/flutter/CMakeLists.txt rename to packages/dragon_charts_flutter/example/windows/flutter/CMakeLists.txt diff --git a/packages/dragon_charts/example/windows/flutter/generated_plugin_registrant.cc b/packages/dragon_charts_flutter/example/windows/flutter/generated_plugin_registrant.cc similarity index 100% rename from packages/dragon_charts/example/windows/flutter/generated_plugin_registrant.cc rename to packages/dragon_charts_flutter/example/windows/flutter/generated_plugin_registrant.cc diff --git a/packages/dragon_charts/example/windows/flutter/generated_plugin_registrant.h b/packages/dragon_charts_flutter/example/windows/flutter/generated_plugin_registrant.h similarity index 100% rename from packages/dragon_charts/example/windows/flutter/generated_plugin_registrant.h rename to packages/dragon_charts_flutter/example/windows/flutter/generated_plugin_registrant.h diff --git a/packages/dragon_charts/example/windows/flutter/generated_plugins.cmake b/packages/dragon_charts_flutter/example/windows/flutter/generated_plugins.cmake similarity index 100% rename from packages/dragon_charts/example/windows/flutter/generated_plugins.cmake rename to packages/dragon_charts_flutter/example/windows/flutter/generated_plugins.cmake diff --git a/packages/dragon_charts/example/windows/runner/CMakeLists.txt b/packages/dragon_charts_flutter/example/windows/runner/CMakeLists.txt similarity index 100% rename from packages/dragon_charts/example/windows/runner/CMakeLists.txt rename to packages/dragon_charts_flutter/example/windows/runner/CMakeLists.txt diff --git a/packages/dragon_charts/example/windows/runner/Runner.rc b/packages/dragon_charts_flutter/example/windows/runner/Runner.rc similarity index 100% rename from packages/dragon_charts/example/windows/runner/Runner.rc rename to packages/dragon_charts_flutter/example/windows/runner/Runner.rc diff --git a/packages/dragon_charts/example/windows/runner/flutter_window.cpp b/packages/dragon_charts_flutter/example/windows/runner/flutter_window.cpp similarity index 100% rename from packages/dragon_charts/example/windows/runner/flutter_window.cpp rename to packages/dragon_charts_flutter/example/windows/runner/flutter_window.cpp diff --git a/packages/dragon_charts/example/windows/runner/flutter_window.h b/packages/dragon_charts_flutter/example/windows/runner/flutter_window.h similarity index 100% rename from packages/dragon_charts/example/windows/runner/flutter_window.h rename to packages/dragon_charts_flutter/example/windows/runner/flutter_window.h diff --git a/packages/dragon_charts/example/windows/runner/main.cpp b/packages/dragon_charts_flutter/example/windows/runner/main.cpp similarity index 100% rename from packages/dragon_charts/example/windows/runner/main.cpp rename to packages/dragon_charts_flutter/example/windows/runner/main.cpp diff --git a/packages/dragon_charts/example/windows/runner/resource.h b/packages/dragon_charts_flutter/example/windows/runner/resource.h similarity index 100% rename from packages/dragon_charts/example/windows/runner/resource.h rename to packages/dragon_charts_flutter/example/windows/runner/resource.h diff --git a/packages/dragon_charts/example/windows/runner/resources/app_icon.ico b/packages/dragon_charts_flutter/example/windows/runner/resources/app_icon.ico similarity index 100% rename from packages/dragon_charts/example/windows/runner/resources/app_icon.ico rename to packages/dragon_charts_flutter/example/windows/runner/resources/app_icon.ico diff --git a/packages/dragon_charts/example/windows/runner/runner.exe.manifest b/packages/dragon_charts_flutter/example/windows/runner/runner.exe.manifest similarity index 100% rename from packages/dragon_charts/example/windows/runner/runner.exe.manifest rename to packages/dragon_charts_flutter/example/windows/runner/runner.exe.manifest diff --git a/packages/dragon_charts/example/windows/runner/utils.cpp b/packages/dragon_charts_flutter/example/windows/runner/utils.cpp similarity index 100% rename from packages/dragon_charts/example/windows/runner/utils.cpp rename to packages/dragon_charts_flutter/example/windows/runner/utils.cpp diff --git a/packages/dragon_charts/example/windows/runner/utils.h b/packages/dragon_charts_flutter/example/windows/runner/utils.h similarity index 100% rename from packages/dragon_charts/example/windows/runner/utils.h rename to packages/dragon_charts_flutter/example/windows/runner/utils.h diff --git a/packages/dragon_charts/example/windows/runner/win32_window.cpp b/packages/dragon_charts_flutter/example/windows/runner/win32_window.cpp similarity index 100% rename from packages/dragon_charts/example/windows/runner/win32_window.cpp rename to packages/dragon_charts_flutter/example/windows/runner/win32_window.cpp diff --git a/packages/dragon_charts/example/windows/runner/win32_window.h b/packages/dragon_charts_flutter/example/windows/runner/win32_window.h similarity index 100% rename from packages/dragon_charts/example/windows/runner/win32_window.h rename to packages/dragon_charts_flutter/example/windows/runner/win32_window.h diff --git a/packages/dragon_charts/lib/dragon_charts_flutter.dart b/packages/dragon_charts_flutter/lib/dragon_charts_flutter.dart similarity index 100% rename from packages/dragon_charts/lib/dragon_charts_flutter.dart rename to packages/dragon_charts_flutter/lib/dragon_charts_flutter.dart diff --git a/packages/dragon_charts/lib/src/chart_axis_labels.dart b/packages/dragon_charts_flutter/lib/src/chart_axis_labels.dart similarity index 100% rename from packages/dragon_charts/lib/src/chart_axis_labels.dart rename to packages/dragon_charts_flutter/lib/src/chart_axis_labels.dart diff --git a/packages/dragon_charts/lib/src/chart_data.dart b/packages/dragon_charts_flutter/lib/src/chart_data.dart similarity index 100% rename from packages/dragon_charts/lib/src/chart_data.dart rename to packages/dragon_charts_flutter/lib/src/chart_data.dart diff --git a/packages/dragon_charts/lib/src/chart_data_series.dart b/packages/dragon_charts_flutter/lib/src/chart_data_series.dart similarity index 100% rename from packages/dragon_charts/lib/src/chart_data_series.dart rename to packages/dragon_charts_flutter/lib/src/chart_data_series.dart diff --git a/packages/dragon_charts/lib/src/chart_data_transform.dart b/packages/dragon_charts_flutter/lib/src/chart_data_transform.dart similarity index 100% rename from packages/dragon_charts/lib/src/chart_data_transform.dart rename to packages/dragon_charts_flutter/lib/src/chart_data_transform.dart diff --git a/packages/dragon_charts/lib/src/chart_element.dart b/packages/dragon_charts_flutter/lib/src/chart_element.dart similarity index 100% rename from packages/dragon_charts/lib/src/chart_element.dart rename to packages/dragon_charts_flutter/lib/src/chart_element.dart diff --git a/packages/dragon_charts/lib/src/chart_grid_lines.dart b/packages/dragon_charts_flutter/lib/src/chart_grid_lines.dart similarity index 100% rename from packages/dragon_charts/lib/src/chart_grid_lines.dart rename to packages/dragon_charts_flutter/lib/src/chart_grid_lines.dart diff --git a/packages/dragon_charts/lib/src/chart_tooltip.dart b/packages/dragon_charts_flutter/lib/src/chart_tooltip.dart similarity index 100% rename from packages/dragon_charts/lib/src/chart_tooltip.dart rename to packages/dragon_charts_flutter/lib/src/chart_tooltip.dart diff --git a/packages/dragon_charts/lib/src/label_placement.dart b/packages/dragon_charts_flutter/lib/src/label_placement.dart similarity index 100% rename from packages/dragon_charts/lib/src/label_placement.dart rename to packages/dragon_charts_flutter/lib/src/label_placement.dart diff --git a/packages/dragon_charts/lib/src/line_chart.dart b/packages/dragon_charts_flutter/lib/src/line_chart.dart similarity index 100% rename from packages/dragon_charts/lib/src/line_chart.dart rename to packages/dragon_charts_flutter/lib/src/line_chart.dart diff --git a/packages/dragon_charts/lib/src/marker_selection_strategies/cartesian_selection_strategy.dart b/packages/dragon_charts_flutter/lib/src/marker_selection_strategies/cartesian_selection_strategy.dart similarity index 100% rename from packages/dragon_charts/lib/src/marker_selection_strategies/cartesian_selection_strategy.dart rename to packages/dragon_charts_flutter/lib/src/marker_selection_strategies/cartesian_selection_strategy.dart diff --git a/packages/dragon_charts/lib/src/marker_selection_strategies/marker_selection_strategies.dart b/packages/dragon_charts_flutter/lib/src/marker_selection_strategies/marker_selection_strategies.dart similarity index 100% rename from packages/dragon_charts/lib/src/marker_selection_strategies/marker_selection_strategies.dart rename to packages/dragon_charts_flutter/lib/src/marker_selection_strategies/marker_selection_strategies.dart diff --git a/packages/dragon_charts/lib/src/marker_selection_strategies/options.dart b/packages/dragon_charts_flutter/lib/src/marker_selection_strategies/options.dart similarity index 100% rename from packages/dragon_charts/lib/src/marker_selection_strategies/options.dart rename to packages/dragon_charts_flutter/lib/src/marker_selection_strategies/options.dart diff --git a/packages/dragon_charts/lib/src/marker_selection_strategies/point_selection_strategy.dart b/packages/dragon_charts_flutter/lib/src/marker_selection_strategies/point_selection_strategy.dart similarity index 100% rename from packages/dragon_charts/lib/src/marker_selection_strategies/point_selection_strategy.dart rename to packages/dragon_charts_flutter/lib/src/marker_selection_strategies/point_selection_strategy.dart diff --git a/packages/dragon_charts/lib/src/sparkline/sparkline_chart.dart b/packages/dragon_charts_flutter/lib/src/sparkline/sparkline_chart.dart similarity index 100% rename from packages/dragon_charts/lib/src/sparkline/sparkline_chart.dart rename to packages/dragon_charts_flutter/lib/src/sparkline/sparkline_chart.dart diff --git a/packages/dragon_charts/pubspec.yaml b/packages/dragon_charts_flutter/pubspec.yaml similarity index 100% rename from packages/dragon_charts/pubspec.yaml rename to packages/dragon_charts_flutter/pubspec.yaml diff --git a/packages/dragon_charts/test/chart_axis_labels_test.dart b/packages/dragon_charts_flutter/test/chart_axis_labels_test.dart similarity index 100% rename from packages/dragon_charts/test/chart_axis_labels_test.dart rename to packages/dragon_charts_flutter/test/chart_axis_labels_test.dart diff --git a/packages/dragon_charts/test/chart_data_series_test.dart b/packages/dragon_charts_flutter/test/chart_data_series_test.dart similarity index 100% rename from packages/dragon_charts/test/chart_data_series_test.dart rename to packages/dragon_charts_flutter/test/chart_data_series_test.dart diff --git a/packages/dragon_charts/test/chart_data_test.dart b/packages/dragon_charts_flutter/test/chart_data_test.dart similarity index 100% rename from packages/dragon_charts/test/chart_data_test.dart rename to packages/dragon_charts_flutter/test/chart_data_test.dart diff --git a/packages/dragon_charts/test/chart_grid_lines_test.dart b/packages/dragon_charts_flutter/test/chart_grid_lines_test.dart similarity index 100% rename from packages/dragon_charts/test/chart_grid_lines_test.dart rename to packages/dragon_charts_flutter/test/chart_grid_lines_test.dart diff --git a/packages/dragon_charts/test/dragon_charts_flutter.dart b/packages/dragon_charts_flutter/test/dragon_charts_flutter.dart similarity index 100% rename from packages/dragon_charts/test/dragon_charts_flutter.dart rename to packages/dragon_charts_flutter/test/dragon_charts_flutter.dart diff --git a/packages/dragon_charts/test/line_chart_test.dart b/packages/dragon_charts_flutter/test/line_chart_test.dart similarity index 100% rename from packages/dragon_charts/test/line_chart_test.dart rename to packages/dragon_charts_flutter/test/line_chart_test.dart diff --git a/packages/dragon_charts/test/sparkline_chart_test.dart b/packages/dragon_charts_flutter/test/sparkline_chart_test.dart similarity index 75% rename from packages/dragon_charts/test/sparkline_chart_test.dart rename to packages/dragon_charts_flutter/test/sparkline_chart_test.dart index 549b7a0d..6122ce64 100644 --- a/packages/dragon_charts/test/sparkline_chart_test.dart +++ b/packages/dragon_charts_flutter/test/sparkline_chart_test.dart @@ -1,17 +1,18 @@ +import 'package:dragon_charts_flutter/dragon_charts_flutter.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:dragon_charts/src/sparkline/sparkline_chart.dart'; void main() { group('SparklineChart', () { - testWidgets('handles empty data without crashing', (WidgetTester tester) async { + testWidgets('handles empty data without crashing', + (WidgetTester tester) async { await tester.pumpWidget( - MaterialApp( + const MaterialApp( home: SizedBox( width: 200, height: 100, child: SparklineChart( - data: const [], + data: [], positiveLineColor: Colors.green, negativeLineColor: Colors.red, lineThickness: 2, @@ -23,14 +24,15 @@ void main() { expect(tester.takeException(), isNull); }); - testWidgets('handles single data point without crashing', (WidgetTester tester) async { + testWidgets('handles single data point without crashing', + (WidgetTester tester) async { await tester.pumpWidget( - MaterialApp( + const MaterialApp( home: SizedBox( width: 200, height: 100, child: SparklineChart( - data: const [5.0], + data: [5.0], positiveLineColor: Colors.green, negativeLineColor: Colors.red, lineThickness: 2, @@ -42,14 +44,15 @@ void main() { expect(tester.takeException(), isNull); }); - testWidgets('handles all same values without crashing', (WidgetTester tester) async { + testWidgets('handles all same values without crashing', + (WidgetTester tester) async { await tester.pumpWidget( - MaterialApp( + const MaterialApp( home: SizedBox( width: 200, height: 100, child: SparklineChart( - data: const [5.0, 5.0, 5.0, 5.0], + data: [5.0, 5.0, 5.0, 5.0], positiveLineColor: Colors.green, negativeLineColor: Colors.red, lineThickness: 2, @@ -61,14 +64,15 @@ void main() { expect(tester.takeException(), isNull); }); - testWidgets('handles negative values correctly', (WidgetTester tester) async { + testWidgets('handles negative values correctly', + (WidgetTester tester) async { await tester.pumpWidget( - MaterialApp( + const MaterialApp( home: SizedBox( width: 200, height: 100, child: SparklineChart( - data: const [-5.0, -2.0, 3.0, 1.0, -1.0], + data: [-5.0, -2.0, 3.0, 1.0, -1.0], positiveLineColor: Colors.green, negativeLineColor: Colors.red, lineThickness: 2, @@ -82,12 +86,12 @@ void main() { testWidgets('handles curved line option', (WidgetTester tester) async { await tester.pumpWidget( - MaterialApp( + const MaterialApp( home: SizedBox( width: 200, height: 100, child: SparklineChart( - data: const [1.0, 5.0, 2.0, 8.0, 3.0], + data: [1.0, 5.0, 2.0, 8.0, 3.0], positiveLineColor: Colors.green, negativeLineColor: Colors.red, lineThickness: 2, @@ -102,12 +106,12 @@ void main() { testWidgets('handles zero values', (WidgetTester tester) async { await tester.pumpWidget( - MaterialApp( + const MaterialApp( home: SizedBox( width: 200, height: 100, child: SparklineChart( - data: const [0.0, 0.0, 0.0], + data: [0.0, 0.0, 0.0], positiveLineColor: Colors.green, negativeLineColor: Colors.red, lineThickness: 2, @@ -119,4 +123,4 @@ void main() { expect(tester.takeException(), isNull); }); }); -} \ No newline at end of file +} diff --git a/packages/komodo_defi_local_auth/.gitignore b/packages/komodo_defi_local_auth/.gitignore index 56682126..0176a593 100644 --- a/packages/komodo_defi_local_auth/.gitignore +++ b/packages/komodo_defi_local_auth/.gitignore @@ -31,6 +31,7 @@ migrate_working_dir/ /build/ pubspec.lock build/ +web/ # Web related lib/generated_plugin_registrant.dart diff --git a/packages/komodo_defi_local_auth/lib/src/trezor/_trezor_index.dart b/packages/komodo_defi_local_auth/lib/src/trezor/_trezor_index.dart index 7e54330d..53516baf 100644 --- a/packages/komodo_defi_local_auth/lib/src/trezor/_trezor_index.dart +++ b/packages/komodo_defi_local_auth/lib/src/trezor/_trezor_index.dart @@ -4,6 +4,8 @@ library _trezor; export 'trezor_auth_service.dart'; +export 'trezor_connection_monitor.dart'; +export 'trezor_connection_status.dart'; export 'trezor_exception.dart'; export 'trezor_initialization_state.dart'; export 'trezor_repository.dart'; diff --git a/packages/komodo_defi_local_auth/lib/src/trezor/trezor_auth_service.dart b/packages/komodo_defi_local_auth/lib/src/trezor/trezor_auth_service.dart index 1a0eec35..eb5f07fd 100644 --- a/packages/komodo_defi_local_auth/lib/src/trezor/trezor_auth_service.dart +++ b/packages/komodo_defi_local_auth/lib/src/trezor/trezor_auth_service.dart @@ -16,7 +16,17 @@ import 'package:logging/logging.dart'; /// handling passphrase requirements and ignoring PIN prompts. All other /// [IAuthService] methods are delegated to the composed auth service. class TrezorAuthService implements IAuthService { - TrezorAuthService(this._authService, this._trezor); + TrezorAuthService( + this._authService, + this._trezor, { + TrezorConnectionMonitor? connectionMonitor, + FlutterSecureStorage? secureStorage, + String Function(int length)? passwordGenerator, + }) : _connectionMonitor = + connectionMonitor ?? TrezorConnectionMonitor(_trezor), + _secureStorage = secureStorage ?? const FlutterSecureStorage(), + _generatePassword = + passwordGenerator ?? SecurityUtils.generatePasswordSecure; static const String trezorWalletName = 'My Trezor'; static const String _passwordKey = 'trezor_wallet_password'; @@ -24,7 +34,9 @@ class TrezorAuthService implements IAuthService { final IAuthService _authService; final TrezorRepository _trezor; - final FlutterSecureStorage _secureStorage = const FlutterSecureStorage(); + final FlutterSecureStorage _secureStorage; + final TrezorConnectionMonitor _connectionMonitor; + final String Function(int length) _generatePassword; Future provideTrezorPin(int taskId, String pin) => _trezor.providePin(taskId, pin); @@ -100,10 +112,16 @@ class TrezorAuthService implements IAuthService { Stream get authStateChanges => _authService.authStateChanges; @override - Future dispose() => _authService.dispose(); + Future dispose() async { + _connectionMonitor.dispose(); + await _authService.dispose(); + } @override - Future signOut() => _authService.signOut(); + Future signOut() async { + await _stopConnectionMonitoring(); + await _authService.signOut(); + } @override Future deleteWallet({ @@ -126,7 +144,11 @@ class TrezorAuthService implements IAuthService { } try { - return await _initializeTrezorWithPassphrase(passphrase: password); + final user = await _initializeTrezorWithPassphrase(passphrase: password); + + _startConnectionMonitoring(); + + return user; } catch (e) { await _signOutCurrentTrezorUser(); @@ -154,7 +176,11 @@ class TrezorAuthService implements IAuthService { } try { - return await _initializeTrezorWithPassphrase(passphrase: password); + final user = await _initializeTrezorWithPassphrase(passphrase: password); + + _startConnectionMonitoring(); + + return user; } catch (e) { await _signOutCurrentTrezorUser(); @@ -180,7 +206,7 @@ class TrezorAuthService implements IAuthService { if (existing != null) return existing; - final newPassword = SecurityUtils.generatePasswordSecure(16); + final newPassword = _generatePassword(16); await _secureStorage.write(key: _passwordKey, value: newPassword); return newPassword; } @@ -189,11 +215,34 @@ class TrezorAuthService implements IAuthService { Future clearTrezorPassword() => _secureStorage.delete(key: _passwordKey); + /// Start monitoring Trezor connection status after successful authentication. + /// This will automatically sign out if the device becomes disconnected. + void _startConnectionMonitoring({String? devicePubkey}) { + _connectionMonitor.startMonitoring( + devicePubkey: devicePubkey, + onConnectionLost: () async { + _log.warning('Trezor connection lost, signing out user'); + await _signOutCurrentTrezorUser(); + }, + onStatusChanged: (status) { + _log.fine('Trezor connection status: ${status.value}'); + }, + ); + } + + /// Stop monitoring Trezor connection status. + Future _stopConnectionMonitoring() async { + if (_connectionMonitor.isMonitoring) { + await _connectionMonitor.stopMonitoring(); + } + } + /// Signs out the current user if they are using the Trezor wallet Future _signOutCurrentTrezorUser() async { final current = await _authService.getActiveUser(); if (current?.walletId.name == trezorWalletName) { _log.warning("Signing out current '${current?.walletId.name}' user"); + await _stopConnectionMonitoring(); try { await _authService.signOut(); } catch (_) { @@ -286,6 +335,7 @@ class TrezorAuthService implements IAuthService { if (trezorState.status == AuthenticationStatus.completed) { final user = await _authService.getActiveUser(); if (user != null) { + _startConnectionMonitoring(); yield AuthenticationState.completed(user); } else { yield AuthenticationState.error( diff --git a/packages/komodo_defi_local_auth/lib/src/trezor/trezor_connection_monitor.dart b/packages/komodo_defi_local_auth/lib/src/trezor/trezor_connection_monitor.dart new file mode 100644 index 00000000..1b420c33 --- /dev/null +++ b/packages/komodo_defi_local_auth/lib/src/trezor/trezor_connection_monitor.dart @@ -0,0 +1,122 @@ +import 'dart:async'; + +import 'package:flutter/foundation.dart'; +import 'package:komodo_defi_local_auth/src/trezor/trezor_connection_status.dart'; +import 'package:komodo_defi_local_auth/src/trezor/trezor_repository.dart'; +import 'package:logging/logging.dart'; + +/// Service responsible for monitoring Trezor device connection status +/// and providing callbacks for connection state changes. +class TrezorConnectionMonitor { + TrezorConnectionMonitor(this._trezorRepository); + + static final _log = Logger('TrezorConnectionMonitor'); + + final TrezorRepository _trezorRepository; + StreamSubscription? _connectionSubscription; + TrezorConnectionStatus? _lastStatus; + + /// Start monitoring the Trezor connection status. + /// + /// [onConnectionLost] will be called when the device becomes disconnected + /// or unreachable. + /// [onConnectionRestored] will be called when the device becomes connected + /// after being disconnected/unreachable. + /// [onStatusChanged] will be called for any status change. + /// [maxDuration] sets the maximum time to monitor before timing out. If null, + /// monitoring continues indefinitely until stopped or disconnected. + void startMonitoring({ + String? devicePubkey, + Duration pollInterval = const Duration(seconds: 1), + Duration? maxDuration, + VoidCallback? onConnectionLost, + VoidCallback? onConnectionRestored, + void Function(TrezorConnectionStatus)? onStatusChanged, + }) { + _log.info('Starting Trezor connection monitoring'); + + // Stop any existing monitoring safely before starting a new one. + final previousSubscription = _connectionSubscription; + if (previousSubscription != null) { + _log.info('Stopping previous Trezor connection monitoring'); + _connectionSubscription = null; + _lastStatus = null; + unawaited(previousSubscription.cancel()); + } + + _connectionSubscription = _trezorRepository + .watchConnectionStatus( + devicePubkey: devicePubkey, + pollInterval: pollInterval, + maxDuration: maxDuration, + ) + .listen( + (status) { + _log.fine('Connection status changed: ${status.value}'); + + final previousStatus = _lastStatus; + _lastStatus = status; + + onStatusChanged?.call(status); + + final previouslyAvailable = previousStatus?.isAvailable ?? true; + if (status.isUnavailable && previouslyAvailable) { + _log.warning('Trezor connection lost: ${status.value}'); + onConnectionLost?.call(); + } + + final previouslyUnavailable = + previousStatus?.isUnavailable ?? false; + if (status.isAvailable && previouslyUnavailable) { + _log.info('Trezor connection restored'); + onConnectionRestored?.call(); + } + }, + onError: (Object error, StackTrace stackTrace) { + _log.severe( + 'Error monitoring Trezor connection: $error', + error, + stackTrace, + ); + // Only call onConnectionLost if this is a real connection error, + // not a disposal + if (_connectionSubscription != null) { + onConnectionLost?.call(); + } + }, + onDone: () { + _log.info('Trezor connection monitoring stopped'); + // Underlying stream ended; mark as not monitoring while keeping + // the last known status for inspection. + _connectionSubscription = null; + }, + ); + } + + /// Stop monitoring the Trezor connection status. + Future stopMonitoring() async { + if (_connectionSubscription != null) { + _log.info('Stopping Trezor connection monitoring'); + await _connectionSubscription?.cancel(); + _connectionSubscription = null; + _lastStatus = null; + } + } + + /// Get the last known connection status. + TrezorConnectionStatus? get lastKnownStatus => _lastStatus; + + /// Check if monitoring is currently active. + bool get isMonitoring => _connectionSubscription != null; + + /// Dispose of the monitor and clean up resources. + void dispose() { + // Make monitoring appear stopped synchronously. + final previousSubscription = _connectionSubscription; + _connectionSubscription = null; + _lastStatus = null; + if (previousSubscription != null) { + unawaited(previousSubscription.cancel()); + } + } +} diff --git a/packages/komodo_defi_local_auth/lib/src/trezor/trezor_connection_status.dart b/packages/komodo_defi_local_auth/lib/src/trezor/trezor_connection_status.dart new file mode 100644 index 00000000..05f061c0 --- /dev/null +++ b/packages/komodo_defi_local_auth/lib/src/trezor/trezor_connection_status.dart @@ -0,0 +1,65 @@ +/// Enum representing Trezor device connection status +enum TrezorConnectionStatus { + /// Device is connected and ready for operations + connected, + + /// Device is disconnected + disconnected, + + /// Device is busy with another operation + busy, + + /// Device is unreachable (possibly hardware issue or driver problem) + unreachable, + + /// Unknown status (for unrecognized status strings) + unknown; + + /// Parse a string status from the API response into enum + static TrezorConnectionStatus fromString(String status) { + switch (status.toLowerCase()) { + case 'connected': + return TrezorConnectionStatus.connected; + case 'disconnected': + return TrezorConnectionStatus.disconnected; + case 'busy': + return TrezorConnectionStatus.busy; + case 'unreachable': + return TrezorConnectionStatus.unreachable; + default: + return TrezorConnectionStatus.unknown; + } + } + + /// Human-readable label for display purposes + String get value { + switch (this) { + case TrezorConnectionStatus.connected: + return 'Connected'; + case TrezorConnectionStatus.disconnected: + return 'Disconnected'; + case TrezorConnectionStatus.busy: + return 'Busy'; + case TrezorConnectionStatus.unreachable: + return 'Unreachable'; + case TrezorConnectionStatus.unknown: + return 'Unknown'; + } + } + + /// Lowercase identifier used by the API + String get apiValue => name; // matches fromString expectations + + /// Check if the status indicates the device is available for operations + bool get isAvailable => this == TrezorConnectionStatus.connected; + + /// Check if the status indicates the device is not available + bool get isUnavailable => + this == TrezorConnectionStatus.disconnected || + this == TrezorConnectionStatus.unreachable || + this == TrezorConnectionStatus.busy; + + /// Check if the device should continue being monitored + bool get shouldContinueMonitoring => + this != TrezorConnectionStatus.disconnected; +} diff --git a/packages/komodo_defi_local_auth/lib/src/trezor/trezor_initialization_state.dart b/packages/komodo_defi_local_auth/lib/src/trezor/trezor_initialization_state.dart index 728eed89..93cfe37b 100644 --- a/packages/komodo_defi_local_auth/lib/src/trezor/trezor_initialization_state.dart +++ b/packages/komodo_defi_local_auth/lib/src/trezor/trezor_initialization_state.dart @@ -1,6 +1,7 @@ import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:komodo_defi_local_auth/komodo_defi_local_auth.dart'; import 'package:komodo_defi_rpc_methods/komodo_defi_rpc_methods.dart'; +import 'package:komodo_defi_types/komodo_defi_types.dart'; part 'trezor_initialization_state.freezed.dart'; diff --git a/packages/komodo_defi_local_auth/lib/src/trezor/trezor_repository.dart b/packages/komodo_defi_local_auth/lib/src/trezor/trezor_repository.dart index 858f3a14..1744d50f 100644 --- a/packages/komodo_defi_local_auth/lib/src/trezor/trezor_repository.dart +++ b/packages/komodo_defi_local_auth/lib/src/trezor/trezor_repository.dart @@ -1,6 +1,7 @@ import 'dart:async' show StreamController, Timer, unawaited; import 'package:komodo_defi_local_auth/src/auth/auth_state.dart'; +import 'package:komodo_defi_local_auth/src/trezor/trezor_connection_status.dart'; import 'package:komodo_defi_local_auth/src/trezor/trezor_exception.dart'; import 'package:komodo_defi_local_auth/src/trezor/trezor_initialization_state.dart'; import 'package:komodo_defi_types/komodo_defi_types.dart'; @@ -48,9 +49,11 @@ class TrezorRepository { }) async* { int? taskId; StreamController? controller; + // Ensure we can always cancel the timer even if the stream is cancelled + // by the subscriber. + Timer? statusTimer; try { - // Start initialization yield const TrezorInitializationState( status: AuthenticationStatus.initializing, message: 'Starting Trezor initialization...', @@ -70,8 +73,6 @@ class TrezorRepository { taskId: taskId, ); - // Poll for status updates - Timer? statusTimer; var isComplete = false; Future pollStatus() async { @@ -115,7 +116,9 @@ class TrezorRepository { } } - // Start polling + // Do not immediately emit the first status update to avoid race + // conditions (i.e. KDF task not yet created). Use the provided polling + // interval for the first status check. statusTimer = Timer.periodic( pollingInterval, (_) => unawaited(pollStatus()), @@ -128,9 +131,10 @@ class TrezorRepository { error: 'Initialization failed: $e', taskId: taskId, ); - - throw TrezorException('Failed to initialize Trezor device', e.toString()); } finally { + // Always cancel the timer to avoid leaks if the subscriber cancels + // the stream or if we exit early for any reason. + statusTimer?.cancel(); if (taskId != null) { _activeInitializations.remove(taskId); if (controller != null && !controller.isClosed) { @@ -187,6 +191,61 @@ class TrezorRepository { } } + /// Returns the current connection status as a parsed enum. + Future getConnectionStatus({ + String? devicePubkey, + }) async { + final response = await _client.rpc.trezor.connectionStatus( + devicePubkey: devicePubkey, + ); + return TrezorConnectionStatus.fromString(response.status); + } + + /// Continuously polls the Trezor connection status and emits parsed enum updates. + /// + /// The stream immediately yields the current status, then continues to poll + /// using [pollInterval]. If the status changes, a new value is emitted. + /// The stream closes once a `Disconnected` status is observed. If + /// [maxDuration] is provided, the stream will also end after the duration + /// elapses by emitting `TrezorConnectionStatus.unreachable`. If `maxDuration` + /// is null (default), the polling continues without a time limit. + Stream watchConnectionStatus({ + String? devicePubkey, + Duration pollInterval = const Duration(seconds: 1), + Duration? maxDuration, + }) async* { + TrezorConnectionStatus last; + final stopwatch = maxDuration != null ? (Stopwatch()..start()) : null; + + try { + last = await getConnectionStatus(devicePubkey: devicePubkey); + yield last; + } catch (e) { + // If initial status check fails, treat as disconnected and end stream + yield TrezorConnectionStatus.disconnected; + return; + } + + while (last.shouldContinueMonitoring && + (maxDuration == null || stopwatch!.elapsed < maxDuration)) { + await Future.delayed(pollInterval); + try { + final current = await getConnectionStatus(devicePubkey: devicePubkey); + if (current != last) { + last = current; + yield current; + } + } catch (e) { + yield TrezorConnectionStatus.disconnected; + return; + } + } + + if (maxDuration != null && stopwatch!.elapsed >= maxDuration) { + yield TrezorConnectionStatus.unreachable; + } + } + /// Cancel all active initializations and clean up resources Future dispose() async { final activeTaskIds = _activeInitializations.keys.toList(); diff --git a/packages/komodo_defi_local_auth/test/src/trezor/trezor_auth_service_test.dart b/packages/komodo_defi_local_auth/test/src/trezor/trezor_auth_service_test.dart new file mode 100644 index 00000000..af0cbd67 --- /dev/null +++ b/packages/komodo_defi_local_auth/test/src/trezor/trezor_auth_service_test.dart @@ -0,0 +1,537 @@ +import 'dart:async'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter_secure_storage/flutter_secure_storage.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:komodo_defi_local_auth/komodo_defi_local_auth.dart'; +import 'package:komodo_defi_local_auth/src/auth/auth_service.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'; + +class _DummyApiClient implements ApiClient { + @override + FutureOr executeRpc(JsonMap request) => {}; +} + +class _FakeTrezorRepository extends TrezorRepository { + _FakeTrezorRepository() : super(_DummyApiClient()); + + final StreamController _controller = + StreamController.broadcast(); + + final Map providedPassphrases = {}; + final Map providedPins = {}; + int? lastCancelledTaskId; + + void emit(TrezorInitializationState state) { + _controller.add(state); + } + + Future close() async => _controller.close(); + + @override + Stream initializeDevice({ + String? devicePubkey, + Duration pollingInterval = const Duration(seconds: 1), + }) async* { + yield* _controller.stream; + } + + @override + Future providePassphrase(int taskId, String passphrase) async { + providedPassphrases[taskId] = passphrase; + } + + @override + Future providePin(int taskId, String pin) async { + providedPins[taskId] = pin; + } + + @override + Future cancelInitialization(int taskId) async { + lastCancelledTaskId = taskId; + return true; + } +} + +class _FakeConnectionMonitor extends TrezorConnectionMonitor { + _FakeConnectionMonitor() : super(_FakeTrezorRepository()); + + bool started = false; + bool stopped = false; + int startCalls = 0; + int stopCalls = 0; + String? lastDevicePubkey; + + @override + void startMonitoring({ + String? devicePubkey, + Duration pollInterval = const Duration(seconds: 1), + Duration? maxDuration, + VoidCallback? onConnectionLost, + VoidCallback? onConnectionRestored, + void Function(TrezorConnectionStatus)? onStatusChanged, + }) { + started = true; + stopped = false; + startCalls += 1; + lastDevicePubkey = devicePubkey; + } + + @override + Future stopMonitoring() async { + stopCalls += 1; + started = false; + stopped = true; + } + + @override + bool get isMonitoring => started; + + @override + void dispose() { + stopped = true; + started = false; + } +} + +class _FakeAuthService implements IAuthService { + final StreamController _authStateController = + StreamController.broadcast(); + + List users = []; + KdfUser? activeUser; + bool signOutCalled = false; + ({String walletName, String password, AuthOptions options})? lastSignInArgs; + ({String walletName, String password, AuthOptions options})? lastRegisterArgs; + + @override + Stream get authStateChanges => _authStateController.stream; + + @override + Future deleteWallet({ + required String walletName, + required String password, + }) async => throw UnimplementedError(); + + @override + Future dispose() async { + await _authStateController.close(); + } + + @override + Future getActiveUser() async => activeUser; + + @override + Future getMnemonic({ + required bool encrypted, + required String? walletPassword, + }) async => throw UnimplementedError(); + + @override + Future> getUsers() async => users; + + @override + Future isSignedIn() async => activeUser != null; + + @override + Future restoreSession(KdfUser user) async { + activeUser = user; + _authStateController.add(user); + } + + @override + Future signIn({ + required String walletName, + required String password, + required AuthOptions options, + }) async { + lastSignInArgs = ( + walletName: walletName, + password: password, + options: options, + ); + final user = KdfUser( + walletId: WalletId.fromName(walletName, options), + isBip39Seed: true, + ); + activeUser = user; + _authStateController.add(user); + return user; + } + + @override + Future signOut() async { + signOutCalled = true; + activeUser = null; + _authStateController.add(null); + } + + @override + Future register({ + required String walletName, + required String password, + required AuthOptions options, + Mnemonic? mnemonic, + }) async { + lastRegisterArgs = ( + walletName: walletName, + password: password, + options: options, + ); + final user = KdfUser( + walletId: WalletId.fromName(walletName, options), + isBip39Seed: true, + ); + activeUser = user; + _authStateController.add(user); + return user; + } + + @override + Future setActiveUserMetadata(JsonMap metadata) async => + throw UnimplementedError(); + + @override + Future updatePassword({ + required String currentPassword, + required String newPassword, + }) async => throw UnimplementedError(); +} + +void main() { + group('TrezorAuthService - DI and basic behavior', () { + test('signIn throws if privKeyPolicy is not trezor', () async { + final auth = _FakeAuthService(); + final repo = _FakeTrezorRepository(); + final monitor = _FakeConnectionMonitor(); + + final service = TrezorAuthService( + auth, + repo, + connectionMonitor: monitor, + secureStorage: const FlutterSecureStorage(), + passwordGenerator: (_) => 'gen', + ); + + expect( + () => service.signIn( + walletName: 'anything', + password: 'irrelevant', + options: const AuthOptions( + derivationMethod: DerivationMethod.hdWallet, + ), + ), + throwsA(isA()), + ); + + await repo.close(); + }); + + test('register throws if privKeyPolicy is not trezor', () async { + final auth = _FakeAuthService(); + final repo = _FakeTrezorRepository(); + final monitor = _FakeConnectionMonitor(); + + final service = TrezorAuthService( + auth, + repo, + connectionMonitor: monitor, + secureStorage: const FlutterSecureStorage(), + passwordGenerator: (_) => 'gen', + ); + + expect( + () => service.register( + walletName: 'anything', + password: 'irrelevant', + options: const AuthOptions( + derivationMethod: DerivationMethod.hdWallet, + ), + ), + throwsA(isA()), + ); + + await repo.close(); + }); + + test( + 'signIn success: registers new wallet, sends passphrase, starts monitor', + () async { + final auth = + _FakeAuthService() + // No existing users => new user => register branch + ..users = []; + + final repo = _FakeTrezorRepository(); + final monitor = _FakeConnectionMonitor(); + + // initialize storage state + FlutterSecureStorage.setMockInitialValues({}); + const storage = FlutterSecureStorage(); + + final service = TrezorAuthService( + auth, + repo, + connectionMonitor: monitor, + secureStorage: storage, + passwordGenerator: (_) => 'generated-pass', + ); + + final future = service.signIn( + walletName: 'ignored-by-service', + password: 'user-passphrase', + options: const AuthOptions( + derivationMethod: DerivationMethod.hdWallet, + privKeyPolicy: PrivateKeyPolicy.trezor(), + ), + ); + + // Drive the repo stream after a brief delay to ensure listeners + // are attached + const taskId = 1; + // ignore: discarded_futures + Future.delayed(const Duration(milliseconds: 5), () { + repo + ..emit( + const TrezorInitializationState( + status: AuthenticationStatus.initializing, + taskId: taskId, + ), + ) + ..emit( + const TrezorInitializationState( + status: AuthenticationStatus.passphraseRequired, + taskId: taskId, + ), + ) + ..emit( + const TrezorInitializationState( + status: AuthenticationStatus.completed, + taskId: taskId, + ), + ); + }); + + final user = await future; + + // Ensured register path used generated wallet password + expect(auth.lastRegisterArgs, isNotNull); + expect( + auth.lastRegisterArgs!.walletName, + TrezorAuthService.trezorWalletName, + ); + expect(auth.lastRegisterArgs!.password, 'generated-pass'); + expect( + auth.lastRegisterArgs!.options.privKeyPolicy, + const PrivateKeyPolicy.trezor(), + ); + + // Passphrase forwarded to repo + expect(repo.providedPassphrases[taskId], 'user-passphrase'); + + // Password stored + final all = await storage.read(key: 'trezor_wallet_password'); + expect(all, 'generated-pass'); + + // Monitoring started + expect(monitor.started, isTrue); + + // Returned user is active user + expect(user.walletId.name, TrezorAuthService.trezorWalletName); + + await repo.close(); + }, + ); + + test( + 'signInStreamed yields states and starts monitor on completion', + () async { + final auth = _FakeAuthService()..users = []; + + final repo = _FakeTrezorRepository(); + final monitor = _FakeConnectionMonitor(); + + final service = TrezorAuthService( + auth, + repo, + connectionMonitor: monitor, + secureStorage: const FlutterSecureStorage(), + passwordGenerator: (_) => 'gen', + ); + + final states = []; + final sub = service + .signInStreamed( + options: const AuthOptions( + derivationMethod: DerivationMethod.hdWallet, + privKeyPolicy: PrivateKeyPolicy.trezor(), + ), + ) + .listen(states.add); + + const taskId = 2; + Future.delayed(const Duration(milliseconds: 5), () { + repo + ..emit( + const TrezorInitializationState( + status: AuthenticationStatus.initializing, + taskId: taskId, + ), + ) + ..emit( + const TrezorInitializationState( + status: AuthenticationStatus.completed, + taskId: taskId, + ), + ); + }); + + // Allow stream to process + await Future.delayed(const Duration(milliseconds: 10)); + await sub.cancel(); + + expect( + states.map((e) => e.status), + contains(AuthenticationStatus.initializing), + ); + expect(states.last.status, AuthenticationStatus.completed); + expect(monitor.started, isTrue); + + await repo.close(); + }, + ); + + test('signIn errors on trezor init error and signs out', () async { + final auth = _FakeAuthService()..users = []; + final repo = _FakeTrezorRepository(); + final monitor = _FakeConnectionMonitor(); + + final service = TrezorAuthService( + auth, + repo, + connectionMonitor: monitor, + secureStorage: const FlutterSecureStorage(), + passwordGenerator: (_) => 'gen', + ); + + final future = service.signIn( + walletName: 'w', + password: 'p', + options: const AuthOptions( + derivationMethod: DerivationMethod.hdWallet, + privKeyPolicy: PrivateKeyPolicy.trezor(), + ), + ); + + Future.delayed(const Duration(milliseconds: 5), () { + repo.emit( + const TrezorInitializationState( + status: AuthenticationStatus.error, + message: 'boom', + taskId: 3, + ), + ); + }); + + await expectLater(future, throwsA(isA())); + // Active user should be cleared by signOut in error path + expect(auth.signOutCalled, isTrue); + await repo.close(); + }); + + test('existing user without stored password throws before auth', () async { + final auth = + _FakeAuthService() + // Pre-existing Trezor user + ..users = [ + KdfUser( + walletId: WalletId.fromName( + TrezorAuthService.trezorWalletName, + const AuthOptions( + derivationMethod: DerivationMethod.hdWallet, + privKeyPolicy: PrivateKeyPolicy.trezor(), + ), + ), + isBip39Seed: true, + ), + ]; + + final repo = _FakeTrezorRepository(); + final monitor = _FakeConnectionMonitor(); + + // Ensure storage has no saved password for this test + FlutterSecureStorage.setMockInitialValues({}); + + final service = TrezorAuthService( + auth, + repo, + connectionMonitor: monitor, + secureStorage: const FlutterSecureStorage(), // missing stored password + passwordGenerator: (_) => 'gen', + ); + + await expectLater( + service.signIn( + walletName: 'ignored', + password: 'user-pass', + options: const AuthOptions( + derivationMethod: DerivationMethod.hdWallet, + privKeyPolicy: PrivateKeyPolicy.trezor(), + ), + ), + throwsA(isA()), + ); + + await repo.close(); + }); + + test('clearTrezorPassword deletes the key in secure storage', () async { + final auth = _FakeAuthService(); + final repo = _FakeTrezorRepository(); + final monitor = _FakeConnectionMonitor(); + + FlutterSecureStorage.setMockInitialValues({ + 'trezor_wallet_password': 'to-remove', + }); + const storage = FlutterSecureStorage(); + + final service = TrezorAuthService( + auth, + repo, + connectionMonitor: monitor, + secureStorage: storage, + passwordGenerator: (_) => 'gen', + ); + + await service.clearTrezorPassword(); + final value = await storage.read(key: 'trezor_wallet_password'); + expect(value, isNull); + await repo.close(); + }); + + test( + 'signOut stops monitoring and calls underlying auth signOut', + () async { + final auth = _FakeAuthService(); + final repo = _FakeTrezorRepository(); + final monitor = + _FakeConnectionMonitor()..started = true; // simulate active + + final service = TrezorAuthService( + auth, + repo, + connectionMonitor: monitor, + secureStorage: const FlutterSecureStorage(), + passwordGenerator: (_) => 'gen', + ); + + await service.signOut(); + expect(monitor.stopCalls, 1); + expect(auth.signOutCalled, isTrue); + await repo.close(); + }, + ); + }); +} diff --git a/packages/komodo_defi_local_auth/test/src/trezor/trezor_connection_monitor_test.dart b/packages/komodo_defi_local_auth/test/src/trezor/trezor_connection_monitor_test.dart new file mode 100644 index 00000000..7fa06ec7 --- /dev/null +++ b/packages/komodo_defi_local_auth/test/src/trezor/trezor_connection_monitor_test.dart @@ -0,0 +1,342 @@ +import 'dart:async'; + +import 'package:flutter_test/flutter_test.dart'; +import 'package:komodo_defi_local_auth/komodo_defi_local_auth.dart'; +import 'package:komodo_defi_types/komodo_defi_type_utils.dart'; +import 'package:komodo_defi_types/komodo_defi_types.dart'; + +class _DummyApiClient implements ApiClient { + @override + FutureOr executeRpc(JsonMap request) => {}; +} + +class _TestTrezorRepository extends TrezorRepository { + _TestTrezorRepository() : super(_DummyApiClient()); + + StreamController? lastController; + String? lastDevicePubkey; + Duration? lastPollInterval; + Duration? lastMaxDuration; + + @override + Stream watchConnectionStatus({ + String? devicePubkey, + Duration pollInterval = const Duration(seconds: 1), + Duration? maxDuration, + }) { + lastDevicePubkey = devicePubkey; + lastPollInterval = pollInterval; + lastMaxDuration = maxDuration; + + final controller = StreamController(); + lastController = controller; + return controller.stream; + } + + void emit(TrezorConnectionStatus status) { + lastController?.add(status); + } + + void emitError(Object error) { + lastController?.addError(error); + } + + Future close() async { + await lastController?.close(); + } + + void complete() { + lastController?.close(); + } +} + +void main() { + group('TrezorConnectionMonitor', () { + test('emits onStatusChanged and updates lastKnownStatus', () async { + final repo = _TestTrezorRepository(); + final monitor = TrezorConnectionMonitor(repo); + + final statuses = []; + + monitor.startMonitoring(onStatusChanged: statuses.add); + + // Emit a sequence of statuses + repo + ..emit(TrezorConnectionStatus.connected) + ..emit(TrezorConnectionStatus.busy) + ..emit(TrezorConnectionStatus.unreachable) + ..emit(TrezorConnectionStatus.connected) + ..emit(TrezorConnectionStatus.disconnected); + + // Allow events to flow + await Future.delayed(const Duration(milliseconds: 10)); + + expect(statuses, isNotEmpty); + expect(monitor.lastKnownStatus, TrezorConnectionStatus.disconnected); + expect(monitor.isMonitoring, isTrue); + + await monitor.stopMonitoring(); + expect(monitor.isMonitoring, isFalse); + expect(monitor.lastKnownStatus, isNull); + await repo.close(); + }); + + test( + 'calls onConnectionLost only on available -> unavailable transitions', + () async { + final repo = _TestTrezorRepository(); + final monitor = TrezorConnectionMonitor(repo); + + var lostCount = 0; + var restoredCount = 0; + + monitor.startMonitoring( + onConnectionLost: () => lostCount++, + onConnectionRestored: () => restoredCount++, + ); + + // Initial unavailable should NOT trigger lost (no previous status) + repo + ..emit(TrezorConnectionStatus.unreachable) + // Transition to available -> should trigger restored + ..emit(TrezorConnectionStatus.connected) + // Available -> unavailable -> lost + ..emit(TrezorConnectionStatus.busy) + // Unavailable -> available -> restored + ..emit(TrezorConnectionStatus.connected) + // Available -> unavailable -> lost + ..emit(TrezorConnectionStatus.unreachable) + // Unavailable -> unavailable (no change) -> no callbacks + ..emit(TrezorConnectionStatus.disconnected); + + await Future.delayed(const Duration(milliseconds: 10)); + + expect(lostCount, 2); + expect(restoredCount, 2); + + await monitor.stopMonitoring(); + await repo.close(); + }, + ); + + test( + 'onConnectionRestored only when transitioning from unavailable to available', + () async { + final repo = _TestTrezorRepository(); + final monitor = TrezorConnectionMonitor(repo); + + var restoredCount = 0; + monitor.startMonitoring(onConnectionRestored: () => restoredCount++); + + // Initial available should NOT trigger restored + repo + ..emit(TrezorConnectionStatus.connected) + // available -> available, still no restored + ..emit(TrezorConnectionStatus.connected) + // unavailable -> available -> restored once + ..emit(TrezorConnectionStatus.busy) + ..emit(TrezorConnectionStatus.connected); + + await Future.delayed(const Duration(milliseconds: 10)); + + expect(restoredCount, 1); + + await monitor.stopMonitoring(); + await repo.close(); + }, + ); + + test('forwards parameters to repository', () async { + final repo = _TestTrezorRepository(); + final monitor = TrezorConnectionMonitor(repo); + + const pubkey = 'pub-xyz'; + const poll = Duration(milliseconds: 250); + const max = Duration(seconds: 3); + + monitor.startMonitoring( + devicePubkey: pubkey, + pollInterval: poll, + maxDuration: max, + ); + + // Allow the start to invoke repo.watchConnectionStatus + await Future.delayed(const Duration(milliseconds: 5)); + + expect(repo.lastDevicePubkey, pubkey); + expect(repo.lastPollInterval, poll); + expect(repo.lastMaxDuration, max); + + await monitor.stopMonitoring(); + await repo.close(); + }); + + test( + 'stopMonitoring cancels subscription and ignores further events', + () async { + final repo = _TestTrezorRepository(); + final monitor = TrezorConnectionMonitor(repo); + + final statuses = []; + monitor.startMonitoring(onStatusChanged: statuses.add); + + repo.emit(TrezorConnectionStatus.connected); + await Future.delayed(const Duration(milliseconds: 5)); + await monitor.stopMonitoring(); + + // After stop, lastKnown should be cleared and events ignored + expect(monitor.lastKnownStatus, isNull); + repo.emit(TrezorConnectionStatus.busy); + await Future.delayed(const Duration(milliseconds: 5)); + + // Only the first event should be recorded + expect(statuses, [TrezorConnectionStatus.connected]); + await repo.close(); + }, + ); + + test('onError triggers onConnectionLost while monitoring', () async { + final repo = _TestTrezorRepository(); + final monitor = TrezorConnectionMonitor(repo); + + var lost = 0; + var statusCount = 0; + monitor.startMonitoring( + onConnectionLost: () => lost++, + onStatusChanged: (_) => statusCount++, + ); + + // Emit any status to set previousStatus + repo.emit(TrezorConnectionStatus.connected); + await Future.delayed(const Duration(milliseconds: 5)); + expect(statusCount, 1); + + // Now emit error from repository stream + repo.emitError(Exception('stream failure')); + await Future.delayed(const Duration(milliseconds: 5)); + + expect(lost, 1); + + // Verify monitoring continues after error if not stopped + repo.emit(TrezorConnectionStatus.busy); + await Future.delayed(const Duration(milliseconds: 5)); + expect(statusCount, 2); + + await monitor.stopMonitoring(); + await repo.close(); + }); + + test('startMonitoring replaces previous monitoring session', () async { + final repo = _TestTrezorRepository(); + final monitor = TrezorConnectionMonitor(repo); + + final statuses = []; + + monitor.startMonitoring(onStatusChanged: statuses.add); + repo.emit(TrezorConnectionStatus.connected); + await Future.delayed(const Duration(milliseconds: 5)); + + // Start a new session; should cancel the previous + monitor.startMonitoring(onStatusChanged: statuses.add); + // Emit from the new stream + repo.emit(TrezorConnectionStatus.busy); + await Future.delayed(const Duration(milliseconds: 5)); + + // We should have seen both events, and isMonitoring should be true + expect(statuses, [ + TrezorConnectionStatus.connected, + TrezorConnectionStatus.busy, + ]); + expect(monitor.isMonitoring, isTrue); + + await monitor.stopMonitoring(); + await repo.close(); + }); + + test('dispose stops monitoring', () async { + final repo = _TestTrezorRepository(); + final monitor = TrezorConnectionMonitor(repo); + + monitor.startMonitoring(); + expect(monitor.isMonitoring, isTrue); + monitor.dispose(); + expect(monitor.isMonitoring, isFalse); + expect(monitor.lastKnownStatus, isNull); + await repo.close(); + }); + + test( + 'isMonitoring becomes false when underlying stream completes', + () async { + final repo = _TestTrezorRepository(); + final monitor = TrezorConnectionMonitor(repo); + + monitor.startMonitoring(); + expect(monitor.isMonitoring, isTrue); + + // Complete the repository stream + repo + ..emit(TrezorConnectionStatus.connected) + ..complete(); + + await Future.delayed(const Duration(milliseconds: 5)); + + // Monitor should reflect completion + expect(monitor.isMonitoring, isFalse); + // Last status should remain available for inspection + expect(monitor.lastKnownStatus, TrezorConnectionStatus.connected); + + await repo.close(); + }, + ); + + test('errors after stopMonitoring are ignored', () async { + final repo = _TestTrezorRepository(); + final monitor = TrezorConnectionMonitor(repo); + + var lost = 0; + monitor.startMonitoring(onConnectionLost: () => lost++); + + repo.emit(TrezorConnectionStatus.connected); + await Future.delayed(const Duration(milliseconds: 5)); + + await monitor.stopMonitoring(); + repo.emitError(Exception('late error')); + await Future.delayed(const Duration(milliseconds: 5)); + + // No new lost invocations after stop + expect(lost, 0); + + await repo.close(); + }); + + test( + 'startMonitoring without events then stopMonitoring remains quiet', + () async { + final repo = _TestTrezorRepository(); + final monitor = TrezorConnectionMonitor(repo); + + var lost = 0; + var restored = 0; + final statuses = []; + + monitor.startMonitoring( + onStatusChanged: statuses.add, + onConnectionLost: () => lost++, + onConnectionRestored: () => restored++, + ); + + // No emissions; then stop + await Future.delayed(const Duration(milliseconds: 5)); + await monitor.stopMonitoring(); + await Future.delayed(const Duration(milliseconds: 5)); + + expect(statuses, isEmpty); + expect(lost, 0); + expect(restored, 0); + + await repo.close(); + }, + ); + }); +} diff --git a/packages/komodo_defi_local_auth/test/src/trezor/trezor_repository_test.dart b/packages/komodo_defi_local_auth/test/src/trezor/trezor_repository_test.dart new file mode 100644 index 00000000..55eaf3bb --- /dev/null +++ b/packages/komodo_defi_local_auth/test/src/trezor/trezor_repository_test.dart @@ -0,0 +1,733 @@ +// ignore_for_file: prefer_const_constructors, avoid_print + +import 'dart:async'; + +import 'package:flutter_test/flutter_test.dart'; +import 'package:komodo_defi_local_auth/komodo_defi_local_auth.dart'; +// ignore: unused_import +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'; + +/// A lightweight fake ApiClient that returns queued responses per method. +class FakeApiClient implements ApiClient { + final Map> _methodResponders = + {}; + + final List calls = []; + + void enqueueResponder( + String method, + JsonMap Function(JsonMap request) responder, + ) { + _methodResponders + .putIfAbsent(method, () => []) + .add(responder); + } + + void enqueueStaticResponse(String method, JsonMap response) { + enqueueResponder(method, (_) => response); + } + + @override + FutureOr executeRpc(JsonMap request) { + calls.add(request); + final method = request['method'] as String?; + if (method == null) { + throw StateError('Missing method in request: $request'); + } + + final queue = _methodResponders[method]; + if (queue == null || queue.isEmpty) { + throw StateError('No responder queued for method $method'); + } + + final responder = queue.removeAt(0); + return responder(request); + } +} + +/// Helpers to craft API-shaped responses quickly +JsonMap newTaskResponse({required int taskId}) => { + 'mmrpc': '2.0', + 'result': {'task_id': taskId}, +}; + +JsonMap trezorStatusOk({required JsonMap deviceInfo}) => { + 'mmrpc': '2.0', + 'result': {'status': 'Ok', 'details': deviceInfo}, +}; + +JsonMap trezorStatusError({required String error}) => { + 'mmrpc': '2.0', + 'result': { + 'status': 'Error', + 'details': { + 'error': error, + 'error_path': '', + 'error_trace': '', + 'error_type': 'TestError', + }, + }, +}; + +JsonMap trezorStatusInProgress(String? description) => { + 'mmrpc': '2.0', + 'result': {'status': 'InProgress', 'details': description}, +}; + +JsonMap trezorStatusUserActionRequired(String description) => { + 'mmrpc': '2.0', + 'result': {'status': 'UserActionRequired', 'details': description}, +}; + +JsonMap trezorCancelOk() => {'mmrpc': '2.0', 'result': 'success'}; + +JsonMap trezorUserActionOk() => {'mmrpc': '2.0', 'result': 'ok'}; + +JsonMap connectionStatusResponse(String status) => { + 'mmrpc': '2.0', + 'result': {'status': status}, +}; + +void main() { + group('TrezorRepository.initializeDevice', () { + test( + 'emits initializing, then mapped status updates, and completes on Ok', + () async { + final client = FakeApiClient(); + final repo = TrezorRepository(client); + + const taskId = 42; + final deviceInfo = { + 'device_id': 'dev-123', + 'device_pubkey': 'pub-abc', + 'type': 'trezor', + 'model': 'T', + 'device_name': 'MyTrezor', + }; + + // init -> task id + client + ..enqueueStaticResponse( + 'task::init_trezor::init', + newTaskResponse(taskId: taskId), + ) + // status polls sequence + ..enqueueStaticResponse( + 'task::init_trezor::status', + trezorStatusInProgress('Waiting to connect the device'), + ) + ..enqueueStaticResponse( + 'task::init_trezor::status', + trezorStatusUserActionRequired('EnterTrezorPin'), + ) + ..enqueueStaticResponse( + 'task::init_trezor::status', + trezorStatusInProgress('Follow the instructions on device'), + ) + ..enqueueStaticResponse( + 'task::init_trezor::status', + trezorStatusOk(deviceInfo: deviceInfo), + ); + + final events = []; + final stream = repo.initializeDevice( + pollingInterval: Duration(milliseconds: 5), + ); + + await stream.forEach(events.add); + + // Verify sequence + expect(events.length, greaterThanOrEqualTo(5)); + expect(events[0].status, AuthenticationStatus.initializing); + expect(events[0].message, contains('Starting')); + + expect(events[1].status, AuthenticationStatus.initializing); + expect(events[1].message, contains('Initialization started')); + expect(events[1].taskId, taskId); + + // Mapped states from our status responses + // Waiting to connect -> waitingForDevice + expect( + events.map((e) => e.status), + contains(AuthenticationStatus.waitingForDevice), + ); + // EnterTrezorPin -> pinRequired + expect( + events.map((e) => e.status), + contains(AuthenticationStatus.pinRequired), + ); + // Follow instructions -> waitingForDeviceConfirmation + expect( + events.map((e) => e.status), + contains(AuthenticationStatus.waitingForDeviceConfirmation), + ); + + // Completed with device info + final completed = events.last; + expect(completed.status, AuthenticationStatus.completed); + expect(completed.deviceInfo, isNotNull); + expect(completed.deviceInfo!.deviceId, equals('dev-123')); + expect(completed.deviceInfo!.devicePubkey, equals('pub-abc')); + }, + ); + + test('adds stream error when status returns Error', () async { + final client = FakeApiClient(); + final repo = TrezorRepository(client); + + const taskId = 7; + + client + ..enqueueStaticResponse( + 'task::init_trezor::init', + newTaskResponse(taskId: taskId), + ) + ..enqueueStaticResponse( + 'task::init_trezor::status', + trezorStatusError(error: 'Device not ready'), + ); + + final completer = Completer(); + var sawError = false; + final sub = repo + .initializeDevice(pollingInterval: Duration(milliseconds: 5)) + .listen( + (_) {}, + onError: (Object err, _) { + expect(err, isA()); + expect(err.toString(), contains('Status check failed')); + expect(err.toString(), contains('Device not ready')); + sawError = true; + completer.complete(); + }, + ); + + await completer.future; + await sub.cancel(); + expect(sawError, isTrue); + }); + + test('adds stream error if status throws', () async { + final client = FakeApiClient(); + final repo = TrezorRepository(client); + + const taskId = 99; + client + ..enqueueStaticResponse( + 'task::init_trezor::init', + newTaskResponse(taskId: taskId), + ) + // Make the status call throw by not enqueueing any responder and intercepting + ..enqueueResponder('task::init_trezor::status', (_) { + throw Exception('Network down'); + }); + + final stream = repo.initializeDevice( + pollingInterval: Duration(milliseconds: 5), + ); + + final completer = Completer(); + var sawStreamError = false; + final sub = stream.listen( + (_) {}, + onError: (Object error, _) { + expect(error, isA()); + sawStreamError = true; + }, + onDone: completer.complete, + ); + + await completer.future; + await sub.cancel(); + expect(sawStreamError, isTrue); + }); + }); + + group('TrezorRepository input validation', () { + test('providePin throws on empty or non-digit input', () async { + final client = FakeApiClient(); + final repo = TrezorRepository(client); + + expect(() => repo.providePin(1, ''), throwsA(isA())); + expect(() => repo.providePin(1, '12a3'), throwsA(isA())); + }); + + test('providePin forwards valid request', () async { + final client = FakeApiClient(); + final repo = TrezorRepository(client); + + client.enqueueStaticResponse( + 'task::init_trezor::user_action', + trezorUserActionOk(), + ); + await repo.providePin(10, '1234'); + expect(client.calls.last['method'], 'task::init_trezor::user_action'); + }); + + test('providePassphrase forwards request', () async { + final client = FakeApiClient(); + final repo = TrezorRepository(client); + + client.enqueueStaticResponse( + 'task::init_trezor::user_action', + trezorUserActionOk(), + ); + await repo.providePassphrase(10, ''); + expect(client.calls.last['method'], 'task::init_trezor::user_action'); + }); + }); + + group('TrezorRepository.cancelInitialization', () { + test('emits cancelled state and returns true (no poll race)', () async { + final client = FakeApiClient(); + final repo = TrezorRepository(client); + + const taskId = 5; + client + ..enqueueStaticResponse( + 'task::init_trezor::init', + newTaskResponse(taskId: taskId), + ) + // Immediate status poll now occurs; provide a benign response + ..enqueueStaticResponse( + 'task::init_trezor::status', + trezorStatusInProgress('Waiting to connect the device'), + ) + ..enqueueStaticResponse('task::init_trezor::cancel', trezorCancelOk()); + + final received = []; + final sub = repo + .initializeDevice(pollingInterval: Duration(hours: 1)) + .listen(received.add); + + // Wait a tick to ensure we have a task id in stream + await Future.delayed(Duration(milliseconds: 10)); + + final cancelled = await repo.cancelInitialization(taskId); + expect(cancelled, isTrue); + + // Allow stream to receive the cancelled event + await Future.delayed(Duration(milliseconds: 5)); + await sub.cancel(); + + expect( + received.map((e) => e.status), + contains(AuthenticationStatus.cancelled), + ); + }); + }); + + group('TrezorRepository connection status', () { + test('getConnectionStatus maps API status strings to enum', () async { + final client = FakeApiClient(); + final repo = TrezorRepository(client); + + client.enqueueStaticResponse( + 'trezor_connection_status', + connectionStatusResponse('connected'), + ); + expect( + await repo.getConnectionStatus(), + TrezorConnectionStatus.connected, + ); + + client.enqueueStaticResponse( + 'trezor_connection_status', + connectionStatusResponse('busy'), + ); + expect(await repo.getConnectionStatus(), TrezorConnectionStatus.busy); + + client.enqueueStaticResponse( + 'trezor_connection_status', + connectionStatusResponse('unreachable'), + ); + expect( + await repo.getConnectionStatus(), + TrezorConnectionStatus.unreachable, + ); + }); + + test( + 'watchConnectionStatus emits on change and stops on disconnected', + () async { + final client = FakeApiClient(); + final repo = TrezorRepository(client); + + // Initial + 3 polls + client + ..enqueueStaticResponse( + 'trezor_connection_status', + connectionStatusResponse('connected'), + ) + ..enqueueStaticResponse( + 'trezor_connection_status', + connectionStatusResponse('busy'), + ) + ..enqueueStaticResponse( + 'trezor_connection_status', + connectionStatusResponse('busy'), + ) + ..enqueueStaticResponse( + 'trezor_connection_status', + connectionStatusResponse('connected'), + ) + ..enqueueStaticResponse( + 'trezor_connection_status', + connectionStatusResponse('disconnected'), + ); + + final statuses = []; + await repo + .watchConnectionStatus( + pollInterval: Duration(milliseconds: 5), + maxDuration: Duration(seconds: 1), + ) + .forEach(statuses.add); + + expect(statuses, [ + TrezorConnectionStatus.connected, // initial + TrezorConnectionStatus.busy, // change + TrezorConnectionStatus.connected, // change + TrezorConnectionStatus.disconnected, // stream ends after this + ]); + }, + ); + + test('watchConnectionStatus stops polling when listener cancels', () async { + final client = FakeApiClient(); + final repo = TrezorRepository(client); + + // Initial + many polls queued (should not be consumed after cancel) + client.enqueueStaticResponse( + 'trezor_connection_status', + connectionStatusResponse('connected'), + ); + for (var i = 0; i < 20; i++) { + client.enqueueStaticResponse( + 'trezor_connection_status', + connectionStatusResponse('connected'), + ); + } + + final firstEvent = Completer(); + late StreamSubscription sub; + sub = repo + .watchConnectionStatus( + pollInterval: Duration(milliseconds: 15), + maxDuration: Duration(seconds: 1), + ) + .listen((_) async { + if (!firstEvent.isCompleted) { + firstEvent.complete(); + await sub.cancel(); + } + }); + + await firstEvent.future; + + final callsAfterCancel = + client.calls + .where((c) => c['method'] == 'trezor_connection_status') + .length; + + // Wait longer than one polling interval; there should be no further calls + await Future.delayed(Duration(milliseconds: 60)); + + final callsLater = + client.calls + .where((c) => c['method'] == 'trezor_connection_status') + .length; + expect(callsLater, callsAfterCancel); + }); + + test( + 'watchConnectionStatus makes no further polls after disconnected', + () async { + final client = FakeApiClient(); + final repo = TrezorRepository(client); + + // Initial + change + disconnected, followed by extra responses that + // should never be consumed + client + ..enqueueStaticResponse( + 'trezor_connection_status', + connectionStatusResponse('connected'), + ) + ..enqueueStaticResponse( + 'trezor_connection_status', + connectionStatusResponse('busy'), + ) + ..enqueueStaticResponse( + 'trezor_connection_status', + connectionStatusResponse('disconnected'), + ); + for (var i = 0; i < 5; i++) { + client.enqueueStaticResponse( + 'trezor_connection_status', + connectionStatusResponse('connected'), + ); + } + + final statuses = []; + await repo + .watchConnectionStatus( + pollInterval: Duration(milliseconds: 10), + maxDuration: Duration(seconds: 1), + ) + .forEach(statuses.add); + + // Expect exactly the three statuses including the terminal one + expect(statuses, [ + TrezorConnectionStatus.connected, + TrezorConnectionStatus.busy, + TrezorConnectionStatus.disconnected, + ]); + + // Ensure only 3 RPC calls were made (initial + 2 polls) + final callCount = + client.calls + .where((c) => c['method'] == 'trezor_connection_status') + .length; + expect(callCount, 3); + }, + ); + + test( + 'watchConnectionStatus yields unreachable after maxDuration timeout', + () async { + final client = FakeApiClient(); + final repo = TrezorRepository(client); + + // Initial connected, then stay connected until timeout + client.enqueueStaticResponse( + 'trezor_connection_status', + connectionStatusResponse('connected'), + ); + // A few polls during the short duration + for (var i = 0; i < 5; i++) { + client.enqueueStaticResponse( + 'trezor_connection_status', + connectionStatusResponse('connected'), + ); + } + + final statuses = []; + await repo + .watchConnectionStatus( + pollInterval: Duration(milliseconds: 10), + maxDuration: Duration(milliseconds: 35), + ) + .forEach(statuses.add); + + expect(statuses.first, TrezorConnectionStatus.connected); + expect(statuses.last, TrezorConnectionStatus.unreachable); + }, + ); + + test( + 'watchConnectionStatus emits disconnected and returns on error', + () async { + final client = FakeApiClient(); + final repo = TrezorRepository(client); + + client + ..enqueueStaticResponse( + 'trezor_connection_status', + connectionStatusResponse('connected'), + ) + ..enqueueResponder( + 'trezor_connection_status', + (_) => throw Exception('RPC failure'), + ); + + final statuses = []; + await repo + .watchConnectionStatus( + pollInterval: Duration(milliseconds: 5), + maxDuration: Duration(seconds: 1), + ) + .forEach(statuses.add); + + expect(statuses, [ + TrezorConnectionStatus.connected, + TrezorConnectionStatus.disconnected, + ]); + }, + ); + }); + + group('TrezorRepository.dispose', () { + test('cancels active initializations', () async { + final client = FakeApiClient(); + final repo = TrezorRepository(client); + + const taskId = 77; + client + ..enqueueStaticResponse( + 'task::init_trezor::init', + newTaskResponse(taskId: taskId), + ) + // Immediate status poll now occurs; provide a benign response + ..enqueueStaticResponse( + 'task::init_trezor::status', + trezorStatusInProgress('Waiting to connect the device'), + ) + ..enqueueStaticResponse('task::init_trezor::cancel', trezorCancelOk()); + + final sub = repo + .initializeDevice(pollingInterval: Duration(hours: 1)) + .listen((_) {}); + + // Give time for init to complete and task to be registered + await Future.delayed(Duration(milliseconds: 5)); + + // Dispose should cancel the active initialization via RPC + await repo.dispose(); + + // Ensure the cancel call was invoked + expect( + client.calls.where((c) => c['method'] == 'task::init_trezor::cancel'), + isNotEmpty, + ); + + await sub.cancel(); + }); + }); + + group('TrezorRepository immediate poll and timer lifecycle', () { + test( + 'watchConnectionStatus with null maxDuration does not yield unreachable and continues until disconnected', + () async { + final client = FakeApiClient(); + final repo = TrezorRepository(client); + + client + ..enqueueStaticResponse( + 'trezor_connection_status', + connectionStatusResponse('connected'), + ) + ..enqueueStaticResponse( + 'trezor_connection_status', + connectionStatusResponse('busy'), + ) + ..enqueueStaticResponse( + 'trezor_connection_status', + connectionStatusResponse('busy'), + ) + ..enqueueStaticResponse( + 'trezor_connection_status', + connectionStatusResponse('connected'), + ) + ..enqueueStaticResponse( + 'trezor_connection_status', + connectionStatusResponse('disconnected'), + ); + + final statuses = []; + await repo + .watchConnectionStatus( + pollInterval: Duration(milliseconds: 5), + // maxDuration omitted (null) + ) + .forEach(statuses.add); + + expect(statuses.last, isNot(TrezorConnectionStatus.unreachable)); + expect(statuses.last, TrezorConnectionStatus.disconnected); + }, + ); + test( + 'initializeDevice does not poll immediately when interval is long', + () async { + final client = FakeApiClient(); + final repo = TrezorRepository(client); + + const taskId = 123; + client + ..enqueueStaticResponse( + 'task::init_trezor::init', + newTaskResponse(taskId: taskId), + ) + ..enqueueStaticResponse( + 'task::init_trezor::status', + trezorStatusInProgress('Waiting to connect the device'), + ); + + final events = []; + final sub = repo + .initializeDevice(pollingInterval: Duration(hours: 1)) + .listen(events.add); + + // Give a short time; no poll should occur yet due to long interval + await Future.delayed(Duration(milliseconds: 15)); + await sub.cancel(); + + // Only the initial two initializing events should be present + expect(events.length, 2); + expect(events[0].status, AuthenticationStatus.initializing); + expect(events[1].status, AuthenticationStatus.initializing); + final statusCalls = + client.calls + .where((c) => c['method'] == 'task::init_trezor::status') + .length; + expect(statusCalls, 0); + }, + ); + + test( + 'timer is cancelled when stream is cancelled (no further polls)', + () async { + final client = FakeApiClient(); + final repo = TrezorRepository(client); + + const taskId = 456; + client + ..enqueueStaticResponse( + 'task::init_trezor::init', + newTaskResponse(taskId: taskId), + ) + // Provide many status responses so if the timer were not cancelled, + // additional polls would succeed and be counted. + ..enqueueStaticResponse( + 'task::init_trezor::status', + trezorStatusInProgress('Waiting to connect the device'), + ); + for (var i = 0; i < 10; i++) { + client.enqueueStaticResponse( + 'task::init_trezor::status', + trezorStatusInProgress('Waiting to connect the device'), + ); + } + + late StreamSubscription sub; + final firstEvent = Completer(); + sub = repo + .initializeDevice(pollingInterval: Duration(milliseconds: 30)) + .listen((event) async { + if (event.status == AuthenticationStatus.waitingForDevice && + !firstEvent.isCompleted) { + firstEvent.complete(); + await sub.cancel(); + } + }); + + await firstEvent.future; + + final callsAfterCancel = + client.calls + .where((c) => c['method'] == 'task::init_trezor::status') + .length; + + // Wait longer than one polling interval; there should be no further calls + await Future.delayed(Duration(milliseconds: 80)); + final callsLater = + client.calls + .where((c) => c['method'] == 'task::init_trezor::status') + .length; + + expect(callsLater, callsAfterCancel); + }, + ); + }); +} diff --git a/packages/komodo_defi_rpc_methods/lib/src/rpc_methods/trezor/trezor_rpc_namespace.dart b/packages/komodo_defi_rpc_methods/lib/src/rpc_methods/trezor/trezor_rpc_namespace.dart index a531d263..4c98b548 100644 --- a/packages/komodo_defi_rpc_methods/lib/src/rpc_methods/trezor/trezor_rpc_namespace.dart +++ b/packages/komodo_defi_rpc_methods/lib/src/rpc_methods/trezor/trezor_rpc_namespace.dart @@ -1,5 +1,7 @@ import 'package:komodo_defi_rpc_methods/komodo_defi_rpc_methods.dart'; import 'package:komodo_defi_types/komodo_defi_type_utils.dart'; +import 'package:komodo_defi_types/komodo_defi_types.dart' + show TrezorDeviceInfo, TrezorUserActionData; /// Trezor hardware wallet methods namespace class TrezorMethodsNamespace extends BaseRpcMethodNamespace { @@ -85,10 +87,7 @@ class TrezorMethodsNamespace extends BaseRpcMethodNamespace { return userAction( taskId: taskId, - userAction: TrezorUserActionData( - actionType: TrezorUserActionType.trezorPin, - pin: pin, - ), + userAction: TrezorUserActionData.pin(pin), ); } @@ -99,9 +98,18 @@ class TrezorMethodsNamespace extends BaseRpcMethodNamespace { }) { return userAction( taskId: taskId, - userAction: TrezorUserActionData( - actionType: TrezorUserActionType.trezorPassphrase, - passphrase: passphrase, + userAction: TrezorUserActionData.passphrase(passphrase), + ); + } + + /// Check if a Trezor device is connected and ready for use. + Future connectionStatus({ + String? devicePubkey, + }) { + return execute( + TrezorConnectionStatusRequest( + rpcPass: rpcPass ?? '', + devicePubkey: devicePubkey, ), ); } @@ -193,44 +201,26 @@ class TaskInitTrezorUserAction } } -// Response and data classes +class TrezorConnectionStatusRequest + extends BaseRequest { + TrezorConnectionStatusRequest({this.devicePubkey, super.rpcPass}) + : super(method: 'trezor_connection_status', mmrpc: '2.0'); -class TrezorDeviceInfo { - TrezorDeviceInfo({ - required this.deviceId, - required this.devicePubkey, - this.type, - this.model, - this.deviceName, - }); - - factory TrezorDeviceInfo.fromJson(JsonMap json) { - return TrezorDeviceInfo( - type: json.valueOrNull('type'), - model: json.valueOrNull('model'), - deviceName: json.valueOrNull('device_name'), - deviceId: json.value('device_id'), - devicePubkey: json.value('device_pubkey'), - ); - } + final String? devicePubkey; - final String? type; - final String? model; - final String? deviceName; - final String deviceId; - final String devicePubkey; + @override + Map toJson() => { + ...super.toJson(), + 'params': {if (devicePubkey != null) 'device_pubkey': devicePubkey}, + }; - JsonMap toJson() { - return { - if (type != null) 'type': type, - if (model != null) 'model': model, - if (deviceName != null) 'device_name': deviceName, - 'device_id': deviceId, - 'device_pubkey': devicePubkey, - }; + @override + TrezorConnectionStatusResponse parse(Map json) { + return TrezorConnectionStatusResponse.fromJson(json); } } +// Response classes class TrezorStatusResponse extends BaseResponse { TrezorStatusResponse({ required super.mmrpc, @@ -305,72 +295,6 @@ class TrezorCancelResponse extends BaseResponse { } } -enum TrezorUserActionType { - trezorPin('TrezorPin'), - trezorPassphrase('TrezorPassphrase'); - - const TrezorUserActionType(this.value); - final String value; -} - -class TrezorUserActionData { - /// ⚠️ SECURITY WARNING: This class contains sensitive data (PIN and passphrase). - /// - DO NOT log instances of this class or its fields - /// - Use [clearSensitiveData] to securely overwrite sensitive fields when no longer needed - /// - Avoid keeping references to this object longer than necessary - TrezorUserActionData({required this.actionType, this.pin, this.passphrase}) - : assert( - (actionType == TrezorUserActionType.trezorPin && pin != null) || - (actionType == TrezorUserActionType.trezorPassphrase && - passphrase != null), - 'PIN must be provided for TrezorPin action, passphrase for ' - 'TrezorPassphrase action', - ); - - final TrezorUserActionType actionType; - String? pin; - String? passphrase; - - JsonMap toJson() { - return { - 'action_type': actionType.value, - if (pin != null) 'pin': pin, - if (passphrase != null) 'passphrase': passphrase, - }; - } - - /// Securely clears sensitive data by overwriting PIN and passphrase fields. - /// Call this method when the sensitive data is no longer needed to minimize - /// exposure in memory. - void clearSensitiveData() { - _secureClearString(pin); - _secureClearString(passphrase); - pin = null; - passphrase = null; - } - - /// Securely overwrites a string by replacing its characters with zeros. - /// This provides a best-effort attempt to clear sensitive data from memory, - /// though complete removal cannot be guaranteed due to Dart's string - /// immutability and garbage collection behavior. - void _secureClearString(String? value) { - if (value == null) return; - - // Note: Due to Dart's string immutability, we cannot directly overwrite - // the string content in memory. The best we can do is null the reference - // and rely on garbage collection. For more secure memory handling, - // consider using typed_data Uint8List for sensitive data in future versions. - } - - @override - String toString() { - // Override toString to prevent accidental logging of sensitive data - return 'TrezorUserActionData(actionType: $actionType, ' - 'pin: ${pin != null ? '[REDACTED]' : 'null'}, ' - 'passphrase: ${passphrase != null ? '[REDACTED]' : 'null'})'; - } -} - class TrezorUserActionResponse extends BaseResponse { TrezorUserActionResponse({required super.mmrpc, required this.result}); @@ -388,3 +312,24 @@ class TrezorUserActionResponse extends BaseResponse { return {'mmrpc': mmrpc, 'result': result}; } } + +class TrezorConnectionStatusResponse extends BaseResponse { + TrezorConnectionStatusResponse({required super.mmrpc, required this.status}); + + factory TrezorConnectionStatusResponse.fromJson(JsonMap json) { + return TrezorConnectionStatusResponse( + mmrpc: json.valueOrNull('mmrpc'), + status: json.value('result').value('status'), + ); + } + + final String status; + + @override + JsonMap toJson() { + return { + 'mmrpc': mmrpc, + 'result': {'status': status}, + }; + } +} diff --git a/packages/komodo_defi_sdk/example/lib/blocs/auth/auth_bloc.dart b/packages/komodo_defi_sdk/example/lib/blocs/auth/auth_bloc.dart index b7e738d3..1053af65 100644 --- a/packages/komodo_defi_sdk/example/lib/blocs/auth/auth_bloc.dart +++ b/packages/komodo_defi_sdk/example/lib/blocs/auth/auth_bloc.dart @@ -6,11 +6,12 @@ import 'package:flutter_bloc/flutter_bloc.dart'; 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:logging/logging.dart'; part 'auth_event.dart'; part 'auth_state.dart'; -part 'trezor_auth_mixin.dart'; part 'trezor_auth_event.dart'; +part 'trezor_auth_mixin.dart'; class AuthBloc extends Bloc with TrezorAuthMixin { AuthBloc({required KomodoDefiSdk sdk}) diff --git a/packages/komodo_defi_sdk/example/lib/blocs/auth/trezor_auth_mixin.dart b/packages/komodo_defi_sdk/example/lib/blocs/auth/trezor_auth_mixin.dart index e29ede19..842fdeee 100644 --- a/packages/komodo_defi_sdk/example/lib/blocs/auth/trezor_auth_mixin.dart +++ b/packages/komodo_defi_sdk/example/lib/blocs/auth/trezor_auth_mixin.dart @@ -4,12 +4,15 @@ part of 'auth_bloc.dart'; mixin TrezorAuthMixin on Bloc { KomodoDefiSdk get _sdk; + static final Logger _log = Logger('TrezorAuthMixin'); + /// Registers handlers for Trezor specific events. /// /// Note: PIN and passphrase handling is now automatic in the stream-based approach. /// The PIN and passphrase events are kept for backward compatibility but may not /// be needed in the new implementation. void setupTrezorEventHandlers() { + _log.finer('Registering Trezor event handlers'); on(_onTrezorInitAndAuth); on(_onTrezorProvidePin); on(_onTrezorProvidePassphrase); @@ -21,6 +24,9 @@ mixin TrezorAuthMixin on Bloc { Emitter emit, ) async { try { + _log.fine( + 'Trezor init/auth started (isRegister=${event.isRegister}, method=${event.derivationMethod})', + ); final authOptions = AuthOptions( derivationMethod: event.derivationMethod, privKeyPolicy: const PrivateKeyPolicy.trezor(), @@ -30,12 +36,14 @@ mixin TrezorAuthMixin on Bloc { // and manages PIN/passphrase handling through the streamed events. final Stream authStream; if (event.isRegister) { + _log.finer('Creating auth.registerStream'); authStream = _sdk.auth.registerStream( walletName: 'My Trezor', password: '', options: authOptions, ); } else { + _log.finer('Creating auth.signInStream'); authStream = _sdk.auth.signInStream( walletName: 'My Trezor', password: '', @@ -44,16 +52,21 @@ mixin TrezorAuthMixin on Bloc { } await for (final authState in authStream) { + _log.finer( + 'Auth stream event: ${authState.status} taskId=${authState.taskId}', + ); final mappedState = _handleAuthenticationState(authState); emit(mappedState); if (authState.status == AuthenticationStatus.completed || authState.status == AuthenticationStatus.error || authState.status == AuthenticationStatus.cancelled) { + _log.fine('Auth stream terminal status: ${authState.status}'); break; } } - } catch (e) { + } catch (e, s) { + _log.severe('Trezor initialization error', e, s); emit( AuthState.error( message: 'Trezor initialization error: $e', @@ -65,6 +78,8 @@ mixin TrezorAuthMixin on Bloc { } AuthState _handleAuthenticationState(AuthenticationState authState) { + // Conservative logging + _log.finer('Handling auth state: ${authState.status}'); switch (authState.status) { case AuthenticationStatus.initializing: return AuthState.trezorInitializing( @@ -98,20 +113,24 @@ mixin TrezorAuthMixin on Bloc { return AuthState.loading(); case AuthenticationStatus.completed: if (authState.user != null) { + _log.fine('Trezor authentication completed with user'); return AuthState.authenticated( user: authState.user!, knownUsers: state.knownUsers, ); } else { + _log.fine('Trezor device is ready (no user)'); return AuthState.trezorReady(deviceInfo: null); } case AuthenticationStatus.error: + _log.warning('Trezor authentication failed: ${authState.message}'); return AuthState.error( message: 'Trezor authentication failed: ${authState.message}', walletName: 'My Trezor', knownUsers: state.knownUsers, ); case AuthenticationStatus.cancelled: + _log.fine('Trezor authentication was cancelled'); return AuthState.error( message: 'Trezor authentication was cancelled', walletName: 'My Trezor', @@ -129,8 +148,10 @@ mixin TrezorAuthMixin on Bloc { Emitter emit, ) async { try { + _log.fine('Providing Trezor PIN for taskId=${event.taskId}'); await _sdk.auth.setHardwareDevicePin(event.taskId, event.pin); } catch (e) { + _log.severe('Failed to provide PIN', e); emit( AuthState.error( message: 'Failed to provide PIN: $e', @@ -146,11 +167,13 @@ mixin TrezorAuthMixin on Bloc { Emitter emit, ) async { try { + _log.fine('Providing Trezor passphrase for taskId=${event.taskId}'); await _sdk.auth.setHardwareDevicePassphrase( event.taskId, event.passphrase, ); } catch (e) { + _log.severe('Failed to provide passphrase', e); emit( AuthState.error( message: 'Failed to provide passphrase: $e', @@ -167,6 +190,7 @@ mixin TrezorAuthMixin on Bloc { ) async { // Cancellation is handled by stopping the stream subscription // This method is kept for backward compatibility + _log.info('Trezor authentication cancelled by user'); emit(AuthState.unauthenticated(knownUsers: await _fetchKnownUsers())); } diff --git a/packages/komodo_defi_sdk/example/pubspec.lock b/packages/komodo_defi_sdk/example/pubspec.lock index 9b069d34..442ee240 100644 --- a/packages/komodo_defi_sdk/example/pubspec.lock +++ b/packages/komodo_defi_sdk/example/pubspec.lock @@ -100,10 +100,9 @@ packages: dragon_charts_flutter: dependency: "direct main" description: - name: dragon_charts_flutter - sha256: "663e73aeae425ec503942bde4ea40caa665c82250e760d20a1df2b89a16ffb3c" - url: "https://pub.dev" - source: hosted + path: "../../dragon_charts_flutter" + relative: true + source: path version: "0.1.1-dev.1" dragon_logs: dependency: "direct main" @@ -367,7 +366,7 @@ packages: source: path version: "0.3.0+0" komodo_defi_rpc_methods: - dependency: "direct overridden" + dependency: "direct main" description: path: "../../komodo_defi_rpc_methods" relative: true @@ -474,7 +473,7 @@ packages: source: hosted version: "1.0.11" logging: - dependency: transitive + dependency: "direct main" description: name: logging sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61 diff --git a/packages/komodo_defi_sdk/example/pubspec.yaml b/packages/komodo_defi_sdk/example/pubspec.yaml index a831ac9b..780268db 100644 --- a/packages/komodo_defi_sdk/example/pubspec.yaml +++ b/packages/komodo_defi_sdk/example/pubspec.yaml @@ -10,14 +10,20 @@ dependencies: bloc: ^9.0.0 bloc_concurrency: 0.3.0 decimal: ^3.2.1 + + dragon_charts_flutter: + path: ../../dragon_charts_flutter + dragon_logs: + path: ../../dragon_logs equatable: ^2.0.7 flutter: sdk: flutter flutter_bloc: ^9.1.1 flutter_secure_storage: ^10.0.0-beta.4 - dragon_logs: - path: ../../dragon_logs + komodo_defi_rpc_methods: + path: ../../komodo_defi_rpc_methods + komodo_defi_sdk: path: ../ @@ -27,17 +33,16 @@ dependencies: komodo_ui: path: ../../komodo_ui - dragon_charts_flutter: 0.1.1-dev.1 + logging: ^1.3.0 dev_dependencies: flutter_lints: ^6.0.0 flutter_test: sdk: flutter - integration_test: - sdk: flutter - flutter_web_plugins: sdk: flutter + integration_test: + sdk: flutter very_good_analysis: ^8.0.0 diff --git a/packages/komodo_defi_types/lib/komodo_defi_type_utils.dart b/packages/komodo_defi_types/lib/komodo_defi_type_utils.dart index e62867c4..949b69f4 100644 --- a/packages/komodo_defi_types/lib/komodo_defi_type_utils.dart +++ b/packages/komodo_defi_types/lib/komodo_defi_type_utils.dart @@ -9,5 +9,6 @@ export 'src/utils/json_type_utils.dart'; export 'src/utils/live_data.dart'; export 'src/utils/live_data_builder.dart'; export 'src/utils/mnemonic_validator.dart'; +export 'src/utils/poll_utils.dart'; export 'src/utils/retry_utils.dart'; export 'src/utils/security_utils.dart'; diff --git a/packages/komodo_defi_types/lib/src/trezor/trezor_device_info.dart b/packages/komodo_defi_types/lib/src/trezor/trezor_device_info.dart new file mode 100644 index 00000000..4a74c06e --- /dev/null +++ b/packages/komodo_defi_types/lib/src/trezor/trezor_device_info.dart @@ -0,0 +1,23 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:komodo_defi_types/komodo_defi_type_utils.dart'; + +part 'trezor_device_info.freezed.dart'; +part 'trezor_device_info.g.dart'; + +/// Information about a connected Trezor device. +@freezed +abstract class TrezorDeviceInfo with _$TrezorDeviceInfo { + /// Create a new [TrezorDeviceInfo]. + @JsonSerializable(fieldRename: FieldRename.snake) + const factory TrezorDeviceInfo({ + required String deviceId, + required String devicePubkey, + String? type, + String? model, + String? deviceName, + }) = _TrezorDeviceInfo; + + /// Construct a [TrezorDeviceInfo] from json. + factory TrezorDeviceInfo.fromJson(JsonMap json) => + _$TrezorDeviceInfoFromJson(json); +} diff --git a/packages/komodo_defi_types/lib/src/trezor/trezor_device_info.freezed.dart b/packages/komodo_defi_types/lib/src/trezor/trezor_device_info.freezed.dart new file mode 100644 index 00000000..43e15cf8 --- /dev/null +++ b/packages/komodo_defi_types/lib/src/trezor/trezor_device_info.freezed.dart @@ -0,0 +1,160 @@ +// dart format width=80 +// coverage:ignore-file +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of 'trezor_device_info.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +// dart format off +T _$identity(T value) => value; + +/// @nodoc +mixin _$TrezorDeviceInfo { + + String get deviceId; String get devicePubkey; String? get type; String? get model; String? get deviceName; +/// Create a copy of TrezorDeviceInfo +/// with the given fields replaced by the non-null parameter values. +@JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +$TrezorDeviceInfoCopyWith get copyWith => _$TrezorDeviceInfoCopyWithImpl(this as TrezorDeviceInfo, _$identity); + + /// Serializes this TrezorDeviceInfo to a JSON map. + Map toJson(); + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is TrezorDeviceInfo&&(identical(other.deviceId, deviceId) || other.deviceId == deviceId)&&(identical(other.devicePubkey, devicePubkey) || other.devicePubkey == devicePubkey)&&(identical(other.type, type) || other.type == type)&&(identical(other.model, model) || other.model == model)&&(identical(other.deviceName, deviceName) || other.deviceName == deviceName)); +} + +@JsonKey(includeFromJson: false, includeToJson: false) +@override +int get hashCode => Object.hash(runtimeType,deviceId,devicePubkey,type,model,deviceName); + +@override +String toString() { + return 'TrezorDeviceInfo(deviceId: $deviceId, devicePubkey: $devicePubkey, type: $type, model: $model, deviceName: $deviceName)'; +} + + +} + +/// @nodoc +abstract mixin class $TrezorDeviceInfoCopyWith<$Res> { + factory $TrezorDeviceInfoCopyWith(TrezorDeviceInfo value, $Res Function(TrezorDeviceInfo) _then) = _$TrezorDeviceInfoCopyWithImpl; +@useResult +$Res call({ + String deviceId, String devicePubkey, String? type, String? model, String? deviceName +}); + + + + +} +/// @nodoc +class _$TrezorDeviceInfoCopyWithImpl<$Res> + implements $TrezorDeviceInfoCopyWith<$Res> { + _$TrezorDeviceInfoCopyWithImpl(this._self, this._then); + + final TrezorDeviceInfo _self; + final $Res Function(TrezorDeviceInfo) _then; + +/// Create a copy of TrezorDeviceInfo +/// with the given fields replaced by the non-null parameter values. +@pragma('vm:prefer-inline') @override $Res call({Object? deviceId = null,Object? devicePubkey = null,Object? type = freezed,Object? model = freezed,Object? deviceName = freezed,}) { + return _then(_self.copyWith( +deviceId: null == deviceId ? _self.deviceId : deviceId // ignore: cast_nullable_to_non_nullable +as String,devicePubkey: null == devicePubkey ? _self.devicePubkey : devicePubkey // ignore: cast_nullable_to_non_nullable +as String,type: freezed == type ? _self.type : type // ignore: cast_nullable_to_non_nullable +as String?,model: freezed == model ? _self.model : model // ignore: cast_nullable_to_non_nullable +as String?,deviceName: freezed == deviceName ? _self.deviceName : deviceName // ignore: cast_nullable_to_non_nullable +as String?, + )); +} + +} + + +/// @nodoc + +@JsonSerializable(fieldRename: FieldRename.snake) +class _TrezorDeviceInfo implements TrezorDeviceInfo { + const _TrezorDeviceInfo({required this.deviceId, required this.devicePubkey, this.type, this.model, this.deviceName}); + factory _TrezorDeviceInfo.fromJson(Map json) => _$TrezorDeviceInfoFromJson(json); + +@override final String deviceId; +@override final String devicePubkey; +@override final String? type; +@override final String? model; +@override final String? deviceName; + +/// Create a copy of TrezorDeviceInfo +/// with the given fields replaced by the non-null parameter values. +@override @JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +_$TrezorDeviceInfoCopyWith<_TrezorDeviceInfo> get copyWith => __$TrezorDeviceInfoCopyWithImpl<_TrezorDeviceInfo>(this, _$identity); + +@override +Map toJson() { + return _$TrezorDeviceInfoToJson(this, ); +} + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is _TrezorDeviceInfo&&(identical(other.deviceId, deviceId) || other.deviceId == deviceId)&&(identical(other.devicePubkey, devicePubkey) || other.devicePubkey == devicePubkey)&&(identical(other.type, type) || other.type == type)&&(identical(other.model, model) || other.model == model)&&(identical(other.deviceName, deviceName) || other.deviceName == deviceName)); +} + +@JsonKey(includeFromJson: false, includeToJson: false) +@override +int get hashCode => Object.hash(runtimeType,deviceId,devicePubkey,type,model,deviceName); + +@override +String toString() { + return 'TrezorDeviceInfo(deviceId: $deviceId, devicePubkey: $devicePubkey, type: $type, model: $model, deviceName: $deviceName)'; +} + + +} + +/// @nodoc +abstract mixin class _$TrezorDeviceInfoCopyWith<$Res> implements $TrezorDeviceInfoCopyWith<$Res> { + factory _$TrezorDeviceInfoCopyWith(_TrezorDeviceInfo value, $Res Function(_TrezorDeviceInfo) _then) = __$TrezorDeviceInfoCopyWithImpl; +@override @useResult +$Res call({ + String deviceId, String devicePubkey, String? type, String? model, String? deviceName +}); + + + + +} +/// @nodoc +class __$TrezorDeviceInfoCopyWithImpl<$Res> + implements _$TrezorDeviceInfoCopyWith<$Res> { + __$TrezorDeviceInfoCopyWithImpl(this._self, this._then); + + final _TrezorDeviceInfo _self; + final $Res Function(_TrezorDeviceInfo) _then; + +/// Create a copy of TrezorDeviceInfo +/// with the given fields replaced by the non-null parameter values. +@override @pragma('vm:prefer-inline') $Res call({Object? deviceId = null,Object? devicePubkey = null,Object? type = freezed,Object? model = freezed,Object? deviceName = freezed,}) { + return _then(_TrezorDeviceInfo( +deviceId: null == deviceId ? _self.deviceId : deviceId // ignore: cast_nullable_to_non_nullable +as String,devicePubkey: null == devicePubkey ? _self.devicePubkey : devicePubkey // ignore: cast_nullable_to_non_nullable +as String,type: freezed == type ? _self.type : type // ignore: cast_nullable_to_non_nullable +as String?,model: freezed == model ? _self.model : model // ignore: cast_nullable_to_non_nullable +as String?,deviceName: freezed == deviceName ? _self.deviceName : deviceName // ignore: cast_nullable_to_non_nullable +as String?, + )); +} + + +} + +// dart format on diff --git a/packages/komodo_defi_types/lib/src/trezor/trezor_device_info.g.dart b/packages/komodo_defi_types/lib/src/trezor/trezor_device_info.g.dart new file mode 100644 index 00000000..6631ba67 --- /dev/null +++ b/packages/komodo_defi_types/lib/src/trezor/trezor_device_info.g.dart @@ -0,0 +1,25 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'trezor_device_info.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +_TrezorDeviceInfo _$TrezorDeviceInfoFromJson(Map json) => + _TrezorDeviceInfo( + deviceId: json['device_id'] as String, + devicePubkey: json['device_pubkey'] as String, + type: json['type'] as String?, + model: json['model'] as String?, + deviceName: json['device_name'] as String?, + ); + +Map _$TrezorDeviceInfoToJson(_TrezorDeviceInfo instance) => + { + 'device_id': instance.deviceId, + 'device_pubkey': instance.devicePubkey, + 'type': instance.type, + 'model': instance.model, + 'device_name': instance.deviceName, + }; diff --git a/packages/komodo_defi_types/lib/src/trezor/trezor_user_action_data.dart b/packages/komodo_defi_types/lib/src/trezor/trezor_user_action_data.dart new file mode 100644 index 00000000..1e4e6897 --- /dev/null +++ b/packages/komodo_defi_types/lib/src/trezor/trezor_user_action_data.dart @@ -0,0 +1,62 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:komodo_defi_types/komodo_defi_type_utils.dart'; + +// ignore_for_file: non_abstract_class_inherits_abstract_member + +part 'trezor_user_action_data.freezed.dart'; +part 'trezor_user_action_data.g.dart'; + +/// Type of user action required by the Trezor device. +@JsonEnum(valueField: 'value') +enum TrezorUserActionType { + trezorPin('TrezorPin'), + trezorPassphrase('TrezorPassphrase'); + + const TrezorUserActionType(this.value); + final String value; +} + +/// Data sent to the API when providing a PIN or passphrase to a Trezor device. +@Freezed(toStringOverride: false) +abstract class TrezorUserActionData with _$TrezorUserActionData { + @JsonSerializable(fieldRename: FieldRename.snake) + const factory TrezorUserActionData({ + required TrezorUserActionType actionType, + @SensitiveStringConverter() SensitiveString? pin, + @SensitiveStringConverter() SensitiveString? passphrase, + }) = _TrezorUserActionData; + + const TrezorUserActionData._(); + + /// Convenience factory for PIN actions with strong validation. + factory TrezorUserActionData.pin(String pin) { + if (pin.isEmpty || !_pinRegex.hasMatch(pin)) { + throw ArgumentError('PIN must contain only digits and cannot be empty.'); + } + return TrezorUserActionData( + actionType: TrezorUserActionType.trezorPin, + pin: SensitiveString(pin), + ); + } + + /// Convenience factory for passphrase actions with strong validation. + factory TrezorUserActionData.passphrase(String passphrase) { + // Empty passphrase is allowed to access default wallet + return TrezorUserActionData( + actionType: TrezorUserActionType.trezorPassphrase, + passphrase: SensitiveString(passphrase), + ); + } + + factory TrezorUserActionData.fromJson(JsonMap json) => + _$TrezorUserActionDataFromJson(json); + + static final RegExp _pinRegex = RegExp(r'^\d+$'); + + @override + String toString() { + final pinRedacted = pin == null ? 'null' : '[REDACTED]'; + final passphraseRedacted = passphrase == null ? 'null' : '[REDACTED]'; + return 'TrezorUserActionData(actionType: $actionType, pin: $pinRedacted, passphrase: $passphraseRedacted)'; + } +} diff --git a/packages/komodo_defi_types/lib/src/trezor/trezor_user_action_data.freezed.dart b/packages/komodo_defi_types/lib/src/trezor/trezor_user_action_data.freezed.dart new file mode 100644 index 00000000..641f1f51 --- /dev/null +++ b/packages/komodo_defi_types/lib/src/trezor/trezor_user_action_data.freezed.dart @@ -0,0 +1,146 @@ +// dart format width=80 +// coverage:ignore-file +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of 'trezor_user_action_data.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +// dart format off +T _$identity(T value) => value; + +/// @nodoc +mixin _$TrezorUserActionData { + + TrezorUserActionType get actionType;@SensitiveStringConverter() SensitiveString? get pin;@SensitiveStringConverter() SensitiveString? get passphrase; +/// Create a copy of TrezorUserActionData +/// with the given fields replaced by the non-null parameter values. +@JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +$TrezorUserActionDataCopyWith get copyWith => _$TrezorUserActionDataCopyWithImpl(this as TrezorUserActionData, _$identity); + + /// Serializes this TrezorUserActionData to a JSON map. + Map toJson(); + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is TrezorUserActionData&&(identical(other.actionType, actionType) || other.actionType == actionType)&&(identical(other.pin, pin) || other.pin == pin)&&(identical(other.passphrase, passphrase) || other.passphrase == passphrase)); +} + +@JsonKey(includeFromJson: false, includeToJson: false) +@override +int get hashCode => Object.hash(runtimeType,actionType,pin,passphrase); + + + +} + +/// @nodoc +abstract mixin class $TrezorUserActionDataCopyWith<$Res> { + factory $TrezorUserActionDataCopyWith(TrezorUserActionData value, $Res Function(TrezorUserActionData) _then) = _$TrezorUserActionDataCopyWithImpl; +@useResult +$Res call({ + TrezorUserActionType actionType,@SensitiveStringConverter() SensitiveString? pin,@SensitiveStringConverter() SensitiveString? passphrase +}); + + + + +} +/// @nodoc +class _$TrezorUserActionDataCopyWithImpl<$Res> + implements $TrezorUserActionDataCopyWith<$Res> { + _$TrezorUserActionDataCopyWithImpl(this._self, this._then); + + final TrezorUserActionData _self; + final $Res Function(TrezorUserActionData) _then; + +/// Create a copy of TrezorUserActionData +/// with the given fields replaced by the non-null parameter values. +@pragma('vm:prefer-inline') @override $Res call({Object? actionType = null,Object? pin = freezed,Object? passphrase = freezed,}) { + return _then(_self.copyWith( +actionType: null == actionType ? _self.actionType : actionType // ignore: cast_nullable_to_non_nullable +as TrezorUserActionType,pin: freezed == pin ? _self.pin : pin // ignore: cast_nullable_to_non_nullable +as SensitiveString?,passphrase: freezed == passphrase ? _self.passphrase : passphrase // ignore: cast_nullable_to_non_nullable +as SensitiveString?, + )); +} + +} + + +/// @nodoc + +@JsonSerializable(fieldRename: FieldRename.snake) +class _TrezorUserActionData extends TrezorUserActionData { + const _TrezorUserActionData({required this.actionType, @SensitiveStringConverter() this.pin, @SensitiveStringConverter() this.passphrase}): super._(); + factory _TrezorUserActionData.fromJson(Map json) => _$TrezorUserActionDataFromJson(json); + +@override final TrezorUserActionType actionType; +@override@SensitiveStringConverter() final SensitiveString? pin; +@override@SensitiveStringConverter() final SensitiveString? passphrase; + +/// Create a copy of TrezorUserActionData +/// with the given fields replaced by the non-null parameter values. +@override @JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +_$TrezorUserActionDataCopyWith<_TrezorUserActionData> get copyWith => __$TrezorUserActionDataCopyWithImpl<_TrezorUserActionData>(this, _$identity); + +@override +Map toJson() { + return _$TrezorUserActionDataToJson(this, ); +} + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is _TrezorUserActionData&&(identical(other.actionType, actionType) || other.actionType == actionType)&&(identical(other.pin, pin) || other.pin == pin)&&(identical(other.passphrase, passphrase) || other.passphrase == passphrase)); +} + +@JsonKey(includeFromJson: false, includeToJson: false) +@override +int get hashCode => Object.hash(runtimeType,actionType,pin,passphrase); + + + +} + +/// @nodoc +abstract mixin class _$TrezorUserActionDataCopyWith<$Res> implements $TrezorUserActionDataCopyWith<$Res> { + factory _$TrezorUserActionDataCopyWith(_TrezorUserActionData value, $Res Function(_TrezorUserActionData) _then) = __$TrezorUserActionDataCopyWithImpl; +@override @useResult +$Res call({ + TrezorUserActionType actionType,@SensitiveStringConverter() SensitiveString? pin,@SensitiveStringConverter() SensitiveString? passphrase +}); + + + + +} +/// @nodoc +class __$TrezorUserActionDataCopyWithImpl<$Res> + implements _$TrezorUserActionDataCopyWith<$Res> { + __$TrezorUserActionDataCopyWithImpl(this._self, this._then); + + final _TrezorUserActionData _self; + final $Res Function(_TrezorUserActionData) _then; + +/// Create a copy of TrezorUserActionData +/// with the given fields replaced by the non-null parameter values. +@override @pragma('vm:prefer-inline') $Res call({Object? actionType = null,Object? pin = freezed,Object? passphrase = freezed,}) { + return _then(_TrezorUserActionData( +actionType: null == actionType ? _self.actionType : actionType // ignore: cast_nullable_to_non_nullable +as TrezorUserActionType,pin: freezed == pin ? _self.pin : pin // ignore: cast_nullable_to_non_nullable +as SensitiveString?,passphrase: freezed == passphrase ? _self.passphrase : passphrase // ignore: cast_nullable_to_non_nullable +as SensitiveString?, + )); +} + + +} + +// dart format on diff --git a/packages/komodo_defi_types/lib/src/trezor/trezor_user_action_data.g.dart b/packages/komodo_defi_types/lib/src/trezor/trezor_user_action_data.g.dart new file mode 100644 index 00000000..7384dc68 --- /dev/null +++ b/packages/komodo_defi_types/lib/src/trezor/trezor_user_action_data.g.dart @@ -0,0 +1,30 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'trezor_user_action_data.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +_TrezorUserActionData _$TrezorUserActionDataFromJson( + Map json, +) => _TrezorUserActionData( + actionType: $enumDecode(_$TrezorUserActionTypeEnumMap, json['action_type']), + pin: const SensitiveStringConverter().fromJson(json['pin'] as String?), + passphrase: const SensitiveStringConverter().fromJson( + json['passphrase'] as String?, + ), +); + +Map _$TrezorUserActionDataToJson( + _TrezorUserActionData instance, +) => { + 'action_type': _$TrezorUserActionTypeEnumMap[instance.actionType]!, + 'pin': const SensitiveStringConverter().toJson(instance.pin), + 'passphrase': const SensitiveStringConverter().toJson(instance.passphrase), +}; + +const _$TrezorUserActionTypeEnumMap = { + TrezorUserActionType.trezorPin: 'TrezorPin', + TrezorUserActionType.trezorPassphrase: 'TrezorPassphrase', +}; diff --git a/packages/komodo_defi_types/lib/src/types.dart b/packages/komodo_defi_types/lib/src/types.dart index 3a253d13..5d469920 100644 --- a/packages/komodo_defi_types/lib/src/types.dart +++ b/packages/komodo_defi_types/lib/src/types.dart @@ -58,6 +58,8 @@ export 'transactions/transaction.dart'; export 'transactions/transaction_history_strategy.dart'; export 'transactions/transaction_pagination_strategy.dart'; export 'transactions/transaction_results_page.dart'; +export 'trezor/trezor_device_info.dart'; +export 'trezor/trezor_user_action_data.dart'; export 'withdrawal/withdrawal_enums.dart'; export 'withdrawal/withdrawal_exceptions.dart'; export 'withdrawal/withdrawal_fee_options.dart'; diff --git a/packages/komodo_defi_types/lib/src/utils/poll_utils.dart b/packages/komodo_defi_types/lib/src/utils/poll_utils.dart new file mode 100644 index 00000000..bd1c6896 --- /dev/null +++ b/packages/komodo_defi_types/lib/src/utils/poll_utils.dart @@ -0,0 +1,90 @@ +import 'dart:async'; + +import 'package:komodo_defi_types/src/utils/backoff_strategy.dart'; + +/// Poll utility with configurable backoff strategy and optional timeout. +/// +/// Executes [functionToPoll] repeatedly until [isComplete] returns true or +/// [maxDuration] is exceeded. Errors are rethrown unless [shouldContinueOnError] +/// returns true. +Future poll( + Future Function() functionToPoll, { + required bool Function(T result) isComplete, + Duration maxDuration = const Duration(seconds: 30), + BackoffStrategy? backoffStrategy, + bool Function(Object error)? shouldContinueOnError, + void Function(int attempt, Duration delay)? onPoll, +}) async { + backoffStrategy ??= const ConstantBackoff(); + final strategy = backoffStrategy.clone(); + var attempt = 0; + var delay = Duration.zero; + final stopwatch = Stopwatch()..start(); + + while (true) { + // Check timeout before invoking the function to avoid starting a call that would exceed the budget + final remainingBeforeCall = maxDuration - stopwatch.elapsed; + if (remainingBeforeCall <= Duration.zero) { + throw TimeoutException( + 'Polling timed out after ${stopwatch.elapsed}', + maxDuration, + ); + } + + try { + // Ensure the call itself respects the remaining time budget + final result = await functionToPoll().timeout(remainingBeforeCall); + if (isComplete(result)) { + return result; + } + delay = strategy.nextDelay(attempt, delay); + onPoll?.call(attempt, delay); + attempt++; + + // Cap or skip delay based on remaining budget after the call + final remainingBeforeDelay = maxDuration - stopwatch.elapsed; + if (remainingBeforeDelay <= Duration.zero) { + throw TimeoutException( + 'Polling timed out after ${stopwatch.elapsed}', + maxDuration, + ); + } + + final effectiveDelay = _calculateEffectiveDelay(delay, remainingBeforeDelay); + if (effectiveDelay > Duration.zero) { + await Future.delayed(effectiveDelay); + } + } catch (e) { + // Always propagate timeouts immediately + if (e is TimeoutException) { + rethrow; + } + if (shouldContinueOnError != null && shouldContinueOnError(e)) { + delay = strategy.nextDelay(attempt, delay); + onPoll?.call(attempt, delay); + attempt++; + + final remainingBeforeDelay = maxDuration - stopwatch.elapsed; + if (remainingBeforeDelay <= Duration.zero) { + throw TimeoutException( + 'Polling timed out after ${stopwatch.elapsed}', + maxDuration, + ); + } + + final effectiveDelay = _calculateEffectiveDelay(delay, remainingBeforeDelay); + if (effectiveDelay > Duration.zero) { + await Future.delayed(effectiveDelay); + } + continue; + } + rethrow; + } + } +} + +/// Returns the smaller of the desired delay and the remaining time budget, ensuring non-negative. +Duration _calculateEffectiveDelay(Duration desiredDelay, Duration remainingBudget) { + if (remainingBudget <= Duration.zero) return Duration.zero; + return desiredDelay <= remainingBudget ? desiredDelay : remainingBudget; +} diff --git a/packages/komodo_defi_types/lib/src/utils/security_utils.dart b/packages/komodo_defi_types/lib/src/utils/security_utils.dart index 5f678dd7..dc0b8e94 100644 --- a/packages/komodo_defi_types/lib/src/utils/security_utils.dart +++ b/packages/komodo_defi_types/lib/src/utils/security_utils.dart @@ -1,6 +1,7 @@ import 'dart:math'; import 'package:characters/characters.dart'; +import 'package:json_annotation/json_annotation.dart'; import 'package:komodo_defi_types/komodo_defi_type_utils.dart'; /// Enum representing different types of password validation errors @@ -50,9 +51,9 @@ abstract class SecurityUtils { return PasswordValidationError.tooShort; } - if (password - .toLowerCase() - .contains(RegExp('password', caseSensitive: false, unicode: true))) { + if (password.toLowerCase().contains( + RegExp('password', caseSensitive: false, unicode: true), + )) { return PasswordValidationError.containsPassword; } @@ -103,7 +104,8 @@ abstract class SecurityUtils { const extendedSpecial = r'~`$^*+=<>?'; - final allCharacters = upperCaseLetters + + final allCharacters = + upperCaseLetters + lowerCaseLetters + digits + specialCharacters + @@ -148,6 +150,7 @@ extension CensoredJsonMap on JsonMap { const sensitive = [ 'seed', 'userpass', + 'pin', 'passphrase', 'password', 'mnemonic', @@ -167,11 +170,38 @@ extension CensoredJsonMap on JsonMap { } } +/// Wrapper for sensitive strings that should never reveal their value when +/// implicitly stringified (e.g. in logs via interpolation). +class SensitiveString { + const SensitiveString(this.value); + + final String value; + + @override + String toString() => '[REDACTED]'; +} + +/// JSON converter for [SensitiveString] that preserves the raw string in +/// serialized JSON while restoring it as a [SensitiveString] on deserialization. +class SensitiveStringConverter + implements JsonConverter { + const SensitiveStringConverter(); + + @override + SensitiveString? fromJson(String? json) => + json == null ? null : SensitiveString(json); + + @override + String? toJson(SensitiveString? object) => object?.value; +} + // Example Test void main() { final password = SecurityUtils.generatePasswordSecure(24); - final extendedPassword = - SecurityUtils.generatePasswordSecure(24, extendedSpecialCharacters: true); + final extendedPassword = SecurityUtils.generatePasswordSecure( + 24, + extendedSpecialCharacters: true, + ); // ignore: avoid_print print('Password: $password'); diff --git a/packages/komodo_defi_types/test/trezor/trezor_user_action_data_redaction_test.dart b/packages/komodo_defi_types/test/trezor/trezor_user_action_data_redaction_test.dart new file mode 100644 index 00000000..91059ac5 --- /dev/null +++ b/packages/komodo_defi_types/test/trezor/trezor_user_action_data_redaction_test.dart @@ -0,0 +1,31 @@ +import 'package:komodo_defi_types/komodo_defi_types.dart'; +import 'package:test/test.dart'; + +void main() { + group('TrezorUserActionData redaction', () { + test('toString() redacts pin and passphrase', () { + final pinData = TrezorUserActionData.pin('1234'); + final passphraseData = TrezorUserActionData.passphrase('hello world'); + + expect(pinData.toString(), contains('pin: [REDACTED]')); + expect(pinData.toString(), contains('passphrase: null')); + + expect(passphraseData.toString(), contains('pin: null')); + expect(passphraseData.toString(), contains('passphrase: [REDACTED]')); + }); + + test('JSON uses raw values for API', () { + final pinData = TrezorUserActionData.pin('9876'); + final passphraseData = TrezorUserActionData.passphrase('secret pass'); + + final pinJson = pinData.toJson(); + final passphraseJson = passphraseData.toJson(); + + expect(pinJson['pin'], '9876'); + expect(pinJson['passphrase'], isNull); + + expect(passphraseJson['pin'], isNull); + expect(passphraseJson['passphrase'], 'secret pass'); + }); + }); +} diff --git a/packages/komodo_defi_types/test/utils/poll_utils_test.dart b/packages/komodo_defi_types/test/utils/poll_utils_test.dart new file mode 100644 index 00000000..8a8ad5eb --- /dev/null +++ b/packages/komodo_defi_types/test/utils/poll_utils_test.dart @@ -0,0 +1,247 @@ +import 'dart:async'; + +import 'package:komodo_defi_types/src/utils/backoff_strategy.dart'; +import 'package:komodo_defi_types/src/utils/poll_utils.dart'; +import 'package:test/test.dart'; + +class _RecoverableError implements Exception { + _RecoverableError(this.message); + final String message; + @override + String toString() => 'RecoverableError: $message'; +} + +class _FatalError implements Exception { + _FatalError(this.message); + final String message; + @override + String toString() => 'FatalError: $message'; +} + +void main() { + group('poll function', () { + test('returns immediately when isComplete is true on first result', () async { + var callCount = 0; + var onPollCalls = 0; + + final result = await poll( + () async { + callCount++; + return 42; + }, + isComplete: (value) => true, + maxDuration: const Duration(milliseconds: 200), + onPoll: (_, __) => onPollCalls++, + ); + + expect(result, equals(42)); + expect(callCount, equals(1)); + expect(onPollCalls, equals(0)); + }); + + test('retries until isComplete becomes true (using constant backoff)', () async { + var callCount = 0; + final attempts = []; + final delays = []; + + final result = await poll( + () async => ++callCount, + isComplete: (value) => value >= 3, + maxDuration: const Duration(seconds: 2), + backoffStrategy: const ConstantBackoff(delay: Duration(milliseconds: 10)), + onPoll: (attempt, delay) { + attempts.add(attempt); + delays.add(delay); + }, + ); + + expect(result, equals(3)); + expect(callCount, equals(3)); + // onPoll is called only for iterations that will continue (before the next attempt) + expect(attempts, equals([0, 1])); + expect(delays, everyElement(equals(const Duration(milliseconds: 10)))); + }); + + test('continues on recoverable errors and eventually completes', () async { + var callCount = 0; + final attempts = []; + final delays = []; + + final result = await poll( + () async { + callCount++; + if (callCount <= 2) { + throw _RecoverableError('temporary'); + } + return 'ok'; + }, + isComplete: (value) => value == 'ok', + maxDuration: const Duration(seconds: 2), + backoffStrategy: const ConstantBackoff(delay: Duration(milliseconds: 10)), + shouldContinueOnError: (e) => e is _RecoverableError, + onPoll: (attempt, delay) { + attempts.add(attempt); + delays.add(delay); + }, + ); + + expect(result, equals('ok')); + expect(callCount, equals(3)); + // Two recoverable errors => two onPoll calls for attempts 0 and 1 + expect(attempts, equals([0, 1])); + expect(delays, everyElement(equals(const Duration(milliseconds: 10)))); + }); + + test('propagates non-recoverable error without retry', () async { + var callCount = 0; + var onPollCalls = 0; + + expect( + () => poll( + () async { + callCount++; + throw _FatalError('boom'); + }, + isComplete: (_) => false, + maxDuration: const Duration(seconds: 1), + shouldContinueOnError: (e) => e is _RecoverableError, + onPoll: (_, __) => onPollCalls++, + ), + throwsA(isA<_FatalError>()), + ); + + expect(callCount, equals(1)); + expect(onPollCalls, equals(0)); + }); + + test('times out when never complete even if calls are quick', () async { + final stopwatch = Stopwatch()..start(); + const max = Duration(milliseconds: 150); + + await expectLater( + poll( + () async => 1, + isComplete: (_) => false, + maxDuration: max, + backoffStrategy: const ConstantBackoff(delay: Duration(milliseconds: 30)), + ), + throwsA(isA()), + ); + + stopwatch.stop(); + // Ensure overall time budget was respected (allow some overhead) + expect(stopwatch.elapsed, lessThanOrEqualTo(max + const Duration(milliseconds: 250))); + }); + + test('per-call timeout: hung function throws TimeoutException within maxDuration and does not invoke shouldContinueOnError', () async { + var continueOnErrorCalls = 0; + final stopwatch = Stopwatch()..start(); + const max = Duration(milliseconds: 200); + + await expectLater( + poll( + () async { + // Simulate a future that never completes + return Completer().future; + }, + isComplete: (_) => false, + maxDuration: max, + shouldContinueOnError: (e) { + continueOnErrorCalls++; + return true; + }, + ), + throwsA(isA()), + ); + + stopwatch.stop(); + expect(continueOnErrorCalls, equals(0)); + expect(stopwatch.elapsed, lessThanOrEqualTo(max + const Duration(milliseconds: 250))); + }); + + test('onPoll receives correct attempt indexes and delays for exponential backoff', () async { + var callCount = 0; + final attempts = []; + final delays = []; + + final result = await poll( + () async => ++callCount, + isComplete: (value) => value >= 3, + maxDuration: const Duration(seconds: 2), + backoffStrategy: ExponentialBackoff( + initialDelay: const Duration(milliseconds: 10), + maxDelay: const Duration(milliseconds: 100), + withJitter: false, + ), + onPoll: (attempt, delay) { + attempts.add(attempt); + delays.add(delay); + }, + ); + + expect(result, equals(3)); + expect(attempts, equals([0, 1])); + expect( + delays, + equals([ + const Duration(milliseconds: 10), + const Duration(milliseconds: 20), + ]), + ); + }); + + test('errors thrown by isComplete can be continued via shouldContinueOnError', () async { + var callCount = 0; + var checkCount = 0; + final attempts = []; + + final result = await poll( + () async => ++callCount, + isComplete: (value) { + checkCount++; + if (checkCount == 1) { + throw _RecoverableError('from isComplete'); + } + return value >= 2; + }, + maxDuration: const Duration(seconds: 2), + backoffStrategy: const ConstantBackoff(delay: Duration(milliseconds: 10)), + shouldContinueOnError: (e) => e is _RecoverableError, + onPoll: (attempt, _) => attempts.add(attempt), + ); + + expect(result, equals(2)); + expect(callCount, equals(2)); + // One continuation due to isComplete error => one onPoll call for attempt 0 + expect(attempts, equals([0])); + }); + + test('delay is effectively capped by remaining time budget', () async { + // Use a very large backoff delay compared to the budget and ensure + // overall elapsed time is bounded by the maxDuration (i.e., delay is capped). + final stopwatch = Stopwatch()..start(); + const budget = Duration(milliseconds: 120); + + await expectLater( + poll( + () async => 1, + isComplete: (_) => false, + maxDuration: budget, + backoffStrategy: const LinearBackoff( + initialDelay: Duration(seconds: 1), + increment: Duration(seconds: 1), + maxDelay: Duration(seconds: 5), + ), + ), + throwsA(isA()), + ); + + stopwatch.stop(); + // Must be well under the 1 second initial delay and close to the budget. + expect(stopwatch.elapsed, lessThan(const Duration(milliseconds: 600))); + expect(stopwatch.elapsed, lessThanOrEqualTo(budget + const Duration(milliseconds: 250))); + }); + }); +} + + diff --git a/packages/komodo_defi_types/test/utils/sensitive_string_test.dart b/packages/komodo_defi_types/test/utils/sensitive_string_test.dart new file mode 100644 index 00000000..379b16f3 --- /dev/null +++ b/packages/komodo_defi_types/test/utils/sensitive_string_test.dart @@ -0,0 +1,35 @@ +import 'package:komodo_defi_types/komodo_defi_type_utils.dart'; +import 'package:test/test.dart'; + +void main() { + group('SensitiveString', () { + test('toString() redacts content', () { + const original = 'mySecretPassword'; + const sensitive = SensitiveString(original); + + expect(sensitive.toString(), '[REDACTED]'); + expect('$sensitive', '[REDACTED]'); + }); + + test('SensitiveStringConverter serializes raw value', () { + const original = 'rawSecret'; + const converter = SensitiveStringConverter(); + + final jsonValue = converter.toJson(const SensitiveString(original)); + expect(jsonValue, original); + }); + + test( + 'SensitiveStringConverter deserializes to wrapper preserving value', + () { + const original = 'anotherSecret'; + const converter = SensitiveStringConverter(); + + final wrapper = converter.fromJson(original); + expect(wrapper, isA()); + expect(wrapper?.value, original); + expect(wrapper.toString(), '[REDACTED]'); + }, + ); + }); +} diff --git a/products/komodo_compliance_console/lib/l10n/arb/app_localizations.dart b/products/komodo_compliance_console/lib/l10n/arb/app_localizations.dart index 0136a731..0446308f 100644 --- a/products/komodo_compliance_console/lib/l10n/arb/app_localizations.dart +++ b/products/komodo_compliance_console/lib/l10n/arb/app_localizations.dart @@ -63,7 +63,7 @@ import 'app_localizations_es.dart'; /// property. abstract class AppLocalizations { AppLocalizations(String locale) - : localeName = intl.Intl.canonicalizedLocale(locale.toString()); + : localeName = intl.Intl.canonicalizedLocale(locale.toString()); final String localeName; @@ -86,16 +86,16 @@ abstract class AppLocalizations { /// of delegates is preferred or required. static const List> localizationsDelegates = >[ - delegate, - GlobalMaterialLocalizations.delegate, - GlobalCupertinoLocalizations.delegate, - GlobalWidgetsLocalizations.delegate, - ]; + delegate, + GlobalMaterialLocalizations.delegate, + GlobalCupertinoLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + ]; /// A list of this localizations delegate's supported locales. static const List supportedLocales = [ Locale('en'), - Locale('es') + Locale('es'), ]; /// Text shown in the AppBar of the Counter Page @@ -132,8 +132,9 @@ AppLocalizations lookupAppLocalizations(Locale locale) { } throw FlutterError( - 'AppLocalizations.delegate failed to load unsupported locale "$locale". This is likely ' - 'an issue with the localizations generation tool. Please file an issue ' - 'on GitHub with a reproducible sample app and the gen-l10n configuration ' - 'that was used.'); + 'AppLocalizations.delegate failed to load unsupported locale "$locale". This is likely ' + 'an issue with the localizations generation tool. Please file an issue ' + 'on GitHub with a reproducible sample app and the gen-l10n configuration ' + 'that was used.', + ); }