diff --git a/assets/images/forest/ethereum.png b/assets/images/forest/ethereum.png new file mode 100644 index 000000000..2827ad56e Binary files /dev/null and b/assets/images/forest/ethereum.png differ diff --git a/assets/images/fruitSorbet/ethereum.png b/assets/images/fruitSorbet/ethereum.png new file mode 100644 index 000000000..2827ad56e Binary files /dev/null and b/assets/images/fruitSorbet/ethereum.png differ diff --git a/assets/images/fruitSorbet/ethereum.svg b/assets/images/fruitSorbet/ethereum.svg new file mode 100644 index 000000000..df9a44d1e --- /dev/null +++ b/assets/images/fruitSorbet/ethereum.svg @@ -0,0 +1,174 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/images/light/ethereum.png b/assets/images/light/ethereum.png new file mode 100644 index 000000000..2827ad56e Binary files /dev/null and b/assets/images/light/ethereum.png differ diff --git a/assets/images/light/ethereum.svg b/assets/images/light/ethereum.svg new file mode 100644 index 000000000..df9a44d1e --- /dev/null +++ b/assets/images/light/ethereum.svg @@ -0,0 +1,174 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/images/oceanBreeze/ethereum.png b/assets/images/oceanBreeze/ethereum.png new file mode 100644 index 000000000..2827ad56e Binary files /dev/null and b/assets/images/oceanBreeze/ethereum.png differ diff --git a/assets/images/oceanBreeze/ethereum.svg b/assets/images/oceanBreeze/ethereum.svg new file mode 100644 index 000000000..df9a44d1e --- /dev/null +++ b/assets/images/oceanBreeze/ethereum.svg @@ -0,0 +1,174 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/images/oledBlack/ethereum.png b/assets/images/oledBlack/ethereum.png new file mode 100644 index 000000000..2827ad56e Binary files /dev/null and b/assets/images/oledBlack/ethereum.png differ diff --git a/assets/images/oledBlack/ethereum.svg b/assets/images/oledBlack/ethereum.svg new file mode 100644 index 000000000..df9a44d1e --- /dev/null +++ b/assets/images/oledBlack/ethereum.svg @@ -0,0 +1,174 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/svg/cc.svg b/assets/svg/cc.svg new file mode 100644 index 000000000..646ae64ce --- /dev/null +++ b/assets/svg/cc.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/assets/svg/circle-plus.svg b/assets/svg/circle-plus.svg new file mode 100644 index 000000000..a09b12711 --- /dev/null +++ b/assets/svg/circle-plus.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/assets/svg/coin_icons/Ethereum.svg b/assets/svg/coin_icons/Ethereum.svg new file mode 100644 index 000000000..77020df7b --- /dev/null +++ b/assets/svg/coin_icons/Ethereum.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/svg/coin_icons/bnb_icon.svg b/assets/svg/coin_icons/bnb_icon.svg new file mode 100644 index 000000000..d32653823 --- /dev/null +++ b/assets/svg/coin_icons/bnb_icon.svg @@ -0,0 +1,161 @@ + + + + + + image/svg+xml + + + + + + + + diff --git a/assets/svg/themed/dark/ethereum.svg b/assets/svg/themed/dark/ethereum.svg new file mode 100644 index 000000000..df9a44d1e --- /dev/null +++ b/assets/svg/themed/dark/ethereum.svg @@ -0,0 +1,174 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/svg/themed/fruitSorbet/ethereum.svg b/assets/svg/themed/fruitSorbet/ethereum.svg new file mode 100644 index 000000000..df9a44d1e --- /dev/null +++ b/assets/svg/themed/fruitSorbet/ethereum.svg @@ -0,0 +1,174 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/svg/themed/light/ethereum.svg b/assets/svg/themed/light/ethereum.svg new file mode 100644 index 000000000..df9a44d1e --- /dev/null +++ b/assets/svg/themed/light/ethereum.svg @@ -0,0 +1,174 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/svg/themed/oceanBreeze/ethereum.svg b/assets/svg/themed/oceanBreeze/ethereum.svg new file mode 100644 index 000000000..df9a44d1e --- /dev/null +++ b/assets/svg/themed/oceanBreeze/ethereum.svg @@ -0,0 +1,174 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/svg/themed/oledBlack/ethereum.svg b/assets/svg/themed/oledBlack/ethereum.svg new file mode 100644 index 000000000..df9a44d1e --- /dev/null +++ b/assets/svg/themed/oledBlack/ethereum.svg @@ -0,0 +1,174 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/svg/tokens.svg b/assets/svg/tokens.svg new file mode 100644 index 000000000..be52b614c --- /dev/null +++ b/assets/svg/tokens.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/ios/Podfile.lock b/ios/Podfile.lock index bb92ea24c..8cca5a81c 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -61,6 +61,8 @@ PODS: - cw_wownero/Wownero (0.0.2): - cw_shared_external - Flutter + - device_info_plus (0.0.1): + - Flutter - devicelocale (0.0.1): - Flutter - DKImagePickerController/Core (4.3.4): @@ -119,8 +121,9 @@ PODS: - MTBBarcodeScanner (5.0.11) - package_info_plus (0.4.5): - Flutter - - path_provider_ios (0.0.1): + - path_provider_foundation (0.0.1): - Flutter + - FlutterMacOS - permission_handler_apple (9.0.4): - Flutter - ReachabilitySwift (5.0.0) @@ -129,8 +132,9 @@ PODS: - SDWebImage/Core (5.13.2) - share_plus (0.0.1): - Flutter - - shared_preferences_ios (0.0.1): + - shared_preferences_foundation (0.0.1): - Flutter + - FlutterMacOS - stack_wallet_backup (0.0.1): - Flutter - SwiftProtobuf (1.19.0) @@ -147,6 +151,7 @@ DEPENDENCIES: - cw_monero (from `.symlinks/plugins/cw_monero/ios`) - cw_shared_external (from `.symlinks/plugins/cw_shared_external/ios`) - cw_wownero (from `.symlinks/plugins/cw_wownero/ios`) + - device_info_plus (from `.symlinks/plugins/device_info_plus/ios`) - devicelocale (from `.symlinks/plugins/devicelocale/ios`) - file_picker (from `.symlinks/plugins/file_picker/ios`) - Flutter (from `Flutter`) @@ -160,10 +165,10 @@ DEPENDENCIES: - lelantus (from `.symlinks/plugins/lelantus/ios`) - local_auth (from `.symlinks/plugins/local_auth/ios`) - package_info_plus (from `.symlinks/plugins/package_info_plus/ios`) - - path_provider_ios (from `.symlinks/plugins/path_provider_ios/ios`) + - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/ios`) - permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`) - share_plus (from `.symlinks/plugins/share_plus/ios`) - - shared_preferences_ios (from `.symlinks/plugins/shared_preferences_ios/ios`) + - shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/ios`) - stack_wallet_backup (from `.symlinks/plugins/stack_wallet_backup/ios`) - url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`) - wakelock (from `.symlinks/plugins/wakelock/ios`) @@ -191,6 +196,8 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/cw_shared_external/ios" cw_wownero: :path: ".symlinks/plugins/cw_wownero/ios" + device_info_plus: + :path: ".symlinks/plugins/device_info_plus/ios" devicelocale: :path: ".symlinks/plugins/devicelocale/ios" file_picker: @@ -217,14 +224,14 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/local_auth/ios" package_info_plus: :path: ".symlinks/plugins/package_info_plus/ios" - path_provider_ios: - :path: ".symlinks/plugins/path_provider_ios/ios" + path_provider_foundation: + :path: ".symlinks/plugins/path_provider_foundation/ios" permission_handler_apple: :path: ".symlinks/plugins/permission_handler_apple/ios" share_plus: :path: ".symlinks/plugins/share_plus/ios" - shared_preferences_ios: - :path: ".symlinks/plugins/shared_preferences_ios/ios" + shared_preferences_foundation: + :path: ".symlinks/plugins/shared_preferences_foundation/ios" stack_wallet_backup: :path: ".symlinks/plugins/stack_wallet_backup/ios" url_launcher_ios: @@ -239,10 +246,11 @@ SPEC CHECKSUMS: cw_monero: 9816991daff0e3ad0a8be140e31933b5526babd4 cw_shared_external: 2972d872b8917603478117c9957dfca611845a92 cw_wownero: 08e5713fe311a3be95efd7f3c1bf9d47d9cfafde + device_info_plus: e5c5da33f982a436e103237c0c85f9031142abed devicelocale: b22617f40038496deffba44747101255cee005b0 DKImagePickerController: b512c28220a2b8ac7419f21c491fc8534b7601ac DKPhotoGallery: fdfad5125a9fdda9cc57df834d49df790dbb4179 - file_picker: 817ab1d8cd2da9d2da412a417162deee3500fc95 + file_picker: ce3938a0df3cc1ef404671531facef740d03f920 Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854 flutter_libepiccash: 36241aa7d3126f6521529985ccb3dc5eaf7bb317 flutter_libmonero: da68a616b73dd0374a8419c684fa6b6df2c44ffe @@ -250,23 +258,23 @@ SPEC CHECKSUMS: flutter_native_splash: 52501b97d1c0a5f898d687f1646226c1f93c56ef flutter_secure_storage: 7953c38a04c3fdbb00571bcd87d8e3b5ceb9daec integration_test: a1e7d09bd98eca2fc37aefd79d4f41ad37bdbbe5 - isar_flutter_libs: bfb66f35a1fa9db9ec96b93539a03329ce147738 + isar_flutter_libs: b69f437aeab9c521821c3f376198c4371fa21073 lelantus: 97ab4ecc648423278f807e499b3a717dea9268f8 local_auth: 1740f55d7af0a2e2a8684ce225fe79d8931e808c MTBBarcodeScanner: f453b33c4b7dfe545d8c6484ed744d55671788cb package_info_plus: 6c92f08e1f853dc01228d6f553146438dafcd14e - path_provider_ios: 14f3d2fd28c4fdb42f44e0f751d12861c43cee02 + path_provider_foundation: 37748e03f12783f9de2cb2c4eadfaa25fe6d4852 permission_handler_apple: 44366e37eaf29454a1e7b1b7d736c2cceaeb17ce ReachabilitySwift: 985039c6f7b23a1da463388634119492ff86c825 SDWebImage: 72f86271a6f3139cc7e4a89220946489d4b9a866 share_plus: 056a1e8ac890df3e33cb503afffaf1e9b4fbae68 - shared_preferences_ios: 548a61f8053b9b8a49ac19c1ffbc8b92c50d68ad + shared_preferences_foundation: 297b3ebca31b34ec92be11acd7fb0ba932c822ca stack_wallet_backup: 5b8563aba5d8ffbf2ce1944331ff7294a0ec7c03 SwiftProtobuf: 6ef3f0e422ef90d6605ca20b21a94f6c1324d6b3 SwiftyGif: 6c3eafd0ce693cad58bb63d2b2fb9bacb8552780 - url_launcher_ios: 839c58cdb4279282219f5e248c3321761ff3c4de + url_launcher_ios: fb12c43172927bb5cf75aeebd073f883801f1993 wakelock: d0fc7c864128eac40eba1617cb5264d9c940b46f -PODFILE CHECKSUM: fe0e1ee7f3d1f7d00b11b474b62dd62134535aea +PODFILE CHECKSUM: 57c8aed26fba39d3ec9424816221f294a07c58eb COCOAPODS: 1.11.3 diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 17990457d..0279825a9 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -17,7 +17,7 @@ 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; - C64D72F92051288D5CB5033D /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 08E84808BC00DEE3447AF47E /* Pods_Runner.framework */; }; + B49D91439948369648AB0603 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 51604430FD0FD1FA5C4767A0 /* Pods_Runner.framework */; }; /* End PBXBuildFile section */ /* Begin PBXCopyFilesBuildPhase section */ @@ -35,11 +35,12 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ - 08E84808BC00DEE3447AF47E /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; + 15168938F13F6113519C963B /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; + 1B82E12F9C5D326CBB2ADF7E /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; - 689E9A74C0452C94E3479BEA /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; + 51604430FD0FD1FA5C4767A0 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; @@ -53,7 +54,6 @@ 7E8A4F15288D645200F18717 /* flutter_libepiccash.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = flutter_libepiccash.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 7E8A4F19288D721300F18717 /* flutter_libepiccash.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = flutter_libepiccash.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 7E8A4F1D288D72D100F18717 /* flutter_libepiccash.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = flutter_libepiccash.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - 7F0C23A93667326FB8E95604 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -61,7 +61,7 @@ 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - F69E2FD9CB433963DAA9B09E /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; + E6F536731AC506735EB76340 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -72,7 +72,7 @@ 7E1603B5288D73EA002F7A6F /* libepic_cash_wallet.a in Frameworks */, 7E729AE82893C1B1009BBD65 /* flutter_libepiccash.framework in Frameworks */, 7E569F992798D47200056D51 /* mobileliblelantus.framework in Frameworks */, - C64D72F92051288D5CB5033D /* Pods_Runner.framework in Frameworks */, + B49D91439948369648AB0603 /* Pods_Runner.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -82,9 +82,9 @@ 4D9BC5822E8E05B80CC958A0 /* Pods */ = { isa = PBXGroup; children = ( - 7F0C23A93667326FB8E95604 /* Pods-Runner.debug.xcconfig */, - F69E2FD9CB433963DAA9B09E /* Pods-Runner.release.xcconfig */, - 689E9A74C0452C94E3479BEA /* Pods-Runner.profile.xcconfig */, + 1B82E12F9C5D326CBB2ADF7E /* Pods-Runner.debug.xcconfig */, + E6F536731AC506735EB76340 /* Pods-Runner.release.xcconfig */, + 15168938F13F6113519C963B /* Pods-Runner.profile.xcconfig */, ); path = Pods; sourceTree = ""; @@ -102,7 +102,7 @@ 7E8A4F06288D5A9300F18717 /* libepic_cash_wallet.a */, 7E8A4F02288D57DE00F18717 /* flutter_libepiccash.framework */, 7E569F982798D47200056D51 /* mobileliblelantus.framework */, - 08E84808BC00DEE3447AF47E /* Pods_Runner.framework */, + 51604430FD0FD1FA5C4767A0 /* Pods_Runner.framework */, ); name = Frameworks; sourceTree = ""; @@ -167,14 +167,14 @@ isa = PBXNativeTarget; buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; buildPhases = ( - D7C3965259DE109272702285 /* [CP] Check Pods Manifest.lock */, + B108E043921CDEDDCB9E1E86 /* [CP] Check Pods Manifest.lock */, 9740EEB61CF901F6004384FC /* Run Script */, 97C146EA1CF9000F007C117D /* Sources */, 97C146EB1CF9000F007C117D /* Frameworks */, 97C146EC1CF9000F007C117D /* Resources */, 9705A1C41CF9048500538489 /* Embed Frameworks */, 3B06AD1E1E4923F5004D2608 /* Thin Binary */, - 30277110A528C730AF372175 /* [CP] Embed Pods Frameworks */, + FD1CA371131604E6658D4146 /* [CP] Embed Pods Frameworks */, ); buildRules = ( ); @@ -235,7 +235,57 @@ /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ - 30277110A528C730AF372175 /* [CP] Embed Pods Frameworks */ = { + 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Thin Binary"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin\n"; + }; + 9740EEB61CF901F6004384FC /* Run Script */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Run Script"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build\n"; + }; + B108E043921CDEDDCB9E1E86 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + FD1CA371131604E6658D4146 /* [CP] Embed Pods Frameworks */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( @@ -267,9 +317,9 @@ "${BUILT_PRODUCTS_DIR}/lelantus/lelantus.framework", "${BUILT_PRODUCTS_DIR}/local_auth/local_auth.framework", "${BUILT_PRODUCTS_DIR}/package_info_plus/package_info_plus.framework", - "${BUILT_PRODUCTS_DIR}/path_provider_ios/path_provider_ios.framework", + "${BUILT_PRODUCTS_DIR}/path_provider_foundation/path_provider_foundation.framework", "${BUILT_PRODUCTS_DIR}/share_plus/share_plus.framework", - "${BUILT_PRODUCTS_DIR}/shared_preferences_ios/shared_preferences_ios.framework", + "${BUILT_PRODUCTS_DIR}/shared_preferences_foundation/shared_preferences_foundation.framework", "${BUILT_PRODUCTS_DIR}/stack_wallet_backup/stack_wallet_backup.framework", "${BUILT_PRODUCTS_DIR}/url_launcher_ios/url_launcher_ios.framework", "${BUILT_PRODUCTS_DIR}/wakelock/wakelock.framework", @@ -301,9 +351,9 @@ "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/lelantus.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/local_auth.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/package_info_plus.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/path_provider_ios.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/path_provider_foundation.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/share_plus.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/shared_preferences_ios.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/shared_preferences_foundation.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/stack_wallet_backup.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/url_launcher_ios.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/wakelock.framework", @@ -313,56 +363,6 @@ shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; showEnvVarsInLog = 0; }; - 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputPaths = ( - ); - name = "Thin Binary"; - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin\n"; - }; - 9740EEB61CF901F6004384FC /* Run Script */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputPaths = ( - ); - name = "Run Script"; - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build\n"; - }; - D7C3965259DE109272702285 /* [CP] Check Pods Manifest.lock */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - ); - inputPaths = ( - "${PODS_PODFILE_DIR_PATH}/Podfile.lock", - "${PODS_ROOT}/Manifest.lock", - ); - name = "[CP] Check Pods Manifest.lock"; - outputFileListPaths = ( - ); - outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; - showEnvVarsInLog = 0; - }; /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ diff --git a/lib/hive/db.dart b/lib/db/hive/db.dart similarity index 99% rename from lib/hive/db.dart rename to lib/db/hive/db.dart index d56e5d243..1efa94cb2 100644 --- a/lib/hive/db.dart +++ b/lib/db/hive/db.dart @@ -253,4 +253,5 @@ abstract class DBKeys { static const String isFavorite = "isFavorite"; static const String id = "id"; static const String storedChainHeight = "storedChainHeight"; + static const String ethTokenContracts = "ethTokenContracts"; } diff --git a/lib/db/main_db.dart b/lib/db/isar/main_db.dart similarity index 92% rename from lib/db/main_db.dart rename to lib/db/isar/main_db.dart index 1fb672d34..76a60676f 100644 --- a/lib/db/main_db.dart +++ b/lib/db/isar/main_db.dart @@ -3,12 +3,12 @@ import 'package:flutter_native_splash/cli_commands.dart'; import 'package:isar/isar.dart'; import 'package:stackwallet/exceptions/main_db/main_db_exception.dart'; import 'package:stackwallet/models/isar/models/isar_models.dart'; +import 'package:stackwallet/utilities/amount/amount.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; -import 'package:stackwallet/utilities/format.dart'; import 'package:stackwallet/utilities/stack_file_system.dart'; import 'package:tuple/tuple.dart'; -part 'queries/queries.dart'; +part '../queries/queries.dart'; class MainDB { MainDB._(); @@ -32,6 +32,7 @@ class MainDB { UTXOSchema, AddressSchema, AddressLabelSchema, + EthContractSchema, ], directory: (await StackFileSystem.applicationIsarDirectory()).path, // inspector: kDebugMode, @@ -395,4 +396,26 @@ class MainDB { throw MainDBException("failed addNewTransactionData", e); } } + + // ========== Ethereum ======================================================= + + // eth contracts + + QueryBuilder getEthContracts() => + isar.ethContracts.where(); + + Future getEthContract(String contractAddress) => + isar.ethContracts.where().addressEqualTo(contractAddress).findFirst(); + + EthContract? getEthContractSync(String contractAddress) => + isar.ethContracts.where().addressEqualTo(contractAddress).findFirstSync(); + + Future putEthContract(EthContract contract) => isar.writeTxn(() async { + return await isar.ethContracts.put(contract); + }); + + Future putEthContracts(List contracts) => + isar.writeTxn(() async { + await isar.ethContracts.putAll(contracts); + }); } diff --git a/lib/db/queries/queries.dart b/lib/db/queries/queries.dart index 2995de06d..65fc6d26e 100644 --- a/lib/db/queries/queries.dart +++ b/lib/db/queries/queries.dart @@ -1,4 +1,4 @@ -part of 'package:stackwallet/db/main_db.dart'; +part of 'package:stackwallet/db/isar/main_db.dart'; enum CCFilter { all, @@ -67,10 +67,10 @@ extension MainDBQueries on MainDB { final maybeDecimal = Decimal.tryParse(searchTerm); if (maybeDecimal != null) { qq = qq.or().valueEqualTo( - Format.decimalAmountToSatoshis( + Amount.fromDecimal( maybeDecimal, - coin, - ), + fractionDigits: coin.decimals, + ).raw.toInt(), ); } @@ -139,10 +139,10 @@ extension MainDBQueries on MainDB { final maybeDecimal = Decimal.tryParse(searchTerm); if (maybeDecimal != null) { qq = qq.or().valueEqualTo( - Format.decimalAmountToSatoshis( + Amount.fromDecimal( maybeDecimal, - coin, - ), + fractionDigits: coin.decimals, + ).raw.toInt(), ); } diff --git a/lib/dto/ethereum/eth_token_tx_dto.dart b/lib/dto/ethereum/eth_token_tx_dto.dart new file mode 100644 index 000000000..e7329ef79 --- /dev/null +++ b/lib/dto/ethereum/eth_token_tx_dto.dart @@ -0,0 +1,169 @@ +/// address : "0x1f9840a85d5af5bf1d1762f925bdaddc4201f984" +/// blockNumber : 16484149 +/// logIndex : 61 +/// topics : ["0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef","0x0000000000000000000000003a5cc8689d1b0cef2c317bc5c0ad6ce88b27d597","0x000000000000000000000000c5e81fc2401b8104966637d5334cbce92f01dbf7"] +/// data : "0x0000000000000000000000000000000000000000000000002dac1c4be587d800" +/// articulatedLog : {"name":"Transfer","inputs":{"_amount":"3291036540000000000","_from":"0x3a5cc8689d1b0cef2c317bc5c0ad6ce88b27d597","_to":"0xc5e81fc2401b8104966637d5334cbce92f01dbf7"}} +/// compressedLog : "{name:Transfer|inputs:{_amount:3291036540000000000|_from:0x3a5cc8689d1b0cef2c317bc5c0ad6ce88b27d597|_to:0xc5e81fc2401b8104966637d5334cbce92f01dbf7}}" +/// transactionHash : "0x5b59559a77fa5f1c70528d41f4fa2e5fa5a00b21fc2f3bc26b208b3062e46333" +/// transactionIndex : 25 + +class EthTokenTxDto { + EthTokenTxDto({ + required this.address, + required this.blockNumber, + required this.logIndex, + required this.topics, + required this.data, + required this.articulatedLog, + required this.compressedLog, + required this.transactionHash, + required this.transactionIndex, + }); + + EthTokenTxDto.fromMap(Map map) + : address = map['address'] as String, + blockNumber = map['blockNumber'] as int, + logIndex = map['logIndex'] as int, + topics = List.from(map['topics'] as List), + data = map['data'] as String, + articulatedLog = map['articulatedLog'] == null + ? null + : ArticulatedLog.fromMap( + Map.from( + map['articulatedLog'] as Map, + ), + ), + compressedLog = map['compressedLog'] as String, + transactionHash = map['transactionHash'] as String, + transactionIndex = map['transactionIndex'] as int; + + final String address; + final int blockNumber; + final int logIndex; + final List topics; + final String data; + final ArticulatedLog? articulatedLog; + final String compressedLog; + final String transactionHash; + final int transactionIndex; + + EthTokenTxDto copyWith({ + String? address, + int? blockNumber, + int? logIndex, + List? topics, + String? data, + ArticulatedLog? articulatedLog, + String? compressedLog, + String? transactionHash, + int? transactionIndex, + }) => + EthTokenTxDto( + address: address ?? this.address, + blockNumber: blockNumber ?? this.blockNumber, + logIndex: logIndex ?? this.logIndex, + topics: topics ?? this.topics, + data: data ?? this.data, + articulatedLog: articulatedLog ?? this.articulatedLog, + compressedLog: compressedLog ?? this.compressedLog, + transactionHash: transactionHash ?? this.transactionHash, + transactionIndex: transactionIndex ?? this.transactionIndex, + ); + + Map toMap() { + final map = {}; + map['address'] = address; + map['blockNumber'] = blockNumber; + map['logIndex'] = logIndex; + map['topics'] = topics; + map['data'] = data; + map['articulatedLog'] = articulatedLog?.toMap(); + map['compressedLog'] = compressedLog; + map['transactionHash'] = transactionHash; + map['transactionIndex'] = transactionIndex; + return map; + } + + @override + String toString() { + return toMap().toString(); + } +} + +/// name : "Transfer" +/// inputs : {"_amount":"3291036540000000000","_from":"0x3a5cc8689d1b0cef2c317bc5c0ad6ce88b27d597","_to":"0xc5e81fc2401b8104966637d5334cbce92f01dbf7"} + +class ArticulatedLog { + ArticulatedLog({ + required this.name, + required this.inputs, + }); + + ArticulatedLog.fromMap(Map map) + : name = map['name'] as String, + inputs = Inputs.fromMap( + Map.from( + map['inputs'] as Map, + ), + ); + + final String name; + final Inputs inputs; + + ArticulatedLog copyWith({ + String? name, + Inputs? inputs, + }) => + ArticulatedLog( + name: name ?? this.name, + inputs: inputs ?? this.inputs, + ); + + Map toMap() { + final map = {}; + map['name'] = name; + map['inputs'] = inputs.toMap(); + return map; + } +} + +/// _amount : "3291036540000000000" +/// _from : "0x3a5cc8689d1b0cef2c317bc5c0ad6ce88b27d597" +/// _to : "0xc5e81fc2401b8104966637d5334cbce92f01dbf7" +/// +class Inputs { + Inputs({ + required this.amount, + required this.from, + required this.to, + }); + + Inputs.fromMap(Map map) + : amount = map['_amount'] as String, + from = map['_from'] as String, + to = map['_to'] as String; + + final String amount; + final String from; + final String to; + + Inputs copyWith({ + String? amount, + String? from, + String? to, + }) => + Inputs( + amount: amount ?? this.amount, + from: from ?? this.from, + to: to ?? this.to, + ); + + Map toMap() { + final map = {}; + map['_amount'] = amount; + map['_from'] = from; + map['_to'] = to; + return map; + } +} diff --git a/lib/dto/ethereum/eth_token_tx_extra_dto.dart b/lib/dto/ethereum/eth_token_tx_extra_dto.dart new file mode 100644 index 000000000..bbc457b0c --- /dev/null +++ b/lib/dto/ethereum/eth_token_tx_extra_dto.dart @@ -0,0 +1,121 @@ +import 'dart:convert'; + +import 'package:stackwallet/utilities/amount/amount.dart'; +import 'package:stackwallet/utilities/enums/coin_enum.dart'; + +class EthTokenTxExtraDTO { + EthTokenTxExtraDTO({ + required this.blockHash, + required this.blockNumber, + required this.from, + required this.gas, + required this.gasCost, + required this.gasPrice, + required this.gasUsed, + required this.hash, + required this.input, + required this.nonce, + required this.timestamp, + required this.to, + required this.transactionIndex, + required this.value, + }); + + factory EthTokenTxExtraDTO.fromMap(Map map) => + EthTokenTxExtraDTO( + hash: map['hash'] as String, + blockHash: map['blockHash'] as String, + blockNumber: map['blockNumber'] as int, + transactionIndex: map['transactionIndex'] as int, + timestamp: map['timestamp'] as int, + from: map['from'] as String, + to: map['to'] as String, + value: Amount( + rawValue: BigInt.parse(map['value'] as String), + fractionDigits: Coin.ethereum.decimals, + ), + gas: _amountFromJsonNum(map['gas']), + gasPrice: _amountFromJsonNum(map['gasPrice']), + nonce: map['nonce'] as int, + input: map['input'] as String, + gasCost: _amountFromJsonNum(map['gasCost']), + gasUsed: _amountFromJsonNum(map['gasUsed']), + ); + + final String hash; + final String blockHash; + final int blockNumber; + final int transactionIndex; + final int timestamp; + final String from; + final String to; + final Amount value; + final Amount gas; + final Amount gasPrice; + final String input; + final int nonce; + final Amount gasCost; + final Amount gasUsed; + + static Amount _amountFromJsonNum(dynamic json) { + return Amount( + rawValue: BigInt.from(json as num), + fractionDigits: Coin.ethereum.decimals, + ); + } + + EthTokenTxExtraDTO copyWith({ + String? hash, + String? blockHash, + int? blockNumber, + int? transactionIndex, + int? timestamp, + String? from, + String? to, + Amount? value, + Amount? gas, + Amount? gasPrice, + int? nonce, + String? input, + Amount? gasCost, + Amount? gasUsed, + }) => + EthTokenTxExtraDTO( + hash: hash ?? this.hash, + blockHash: blockHash ?? this.blockHash, + blockNumber: blockNumber ?? this.blockNumber, + transactionIndex: transactionIndex ?? this.transactionIndex, + timestamp: timestamp ?? this.timestamp, + from: from ?? this.from, + to: to ?? this.to, + value: value ?? this.value, + gas: gas ?? this.gas, + gasPrice: gasPrice ?? this.gasPrice, + nonce: nonce ?? this.nonce, + input: input ?? this.input, + gasCost: gasCost ?? this.gasCost, + gasUsed: gasUsed ?? this.gasUsed, + ); + + Map toMap() { + final map = {}; + map['hash'] = hash; + map['blockHash'] = blockHash; + map['blockNumber'] = blockNumber; + map['transactionIndex'] = transactionIndex; + map['timestamp'] = timestamp; + map['from'] = from; + map['to'] = to; + map['value'] = value; + map['gas'] = gas; + map['gasPrice'] = gasPrice; + map['input'] = input; + map['nonce'] = nonce; + map['gasCost'] = gasCost; + map['gasUsed'] = gasUsed; + return map; + } + + @override + String toString() => jsonEncode(toMap()); +} diff --git a/lib/dto/ethereum/eth_tx_dto.dart b/lib/dto/ethereum/eth_tx_dto.dart new file mode 100644 index 000000000..a4345dec5 --- /dev/null +++ b/lib/dto/ethereum/eth_tx_dto.dart @@ -0,0 +1,135 @@ +import 'dart:convert'; + +import 'package:stackwallet/utilities/amount/amount.dart'; +import 'package:stackwallet/utilities/enums/coin_enum.dart'; + +class EthTxDTO { + EthTxDTO({ + required this.hash, + required this.blockHash, + required this.blockNumber, + required this.transactionIndex, + required this.timestamp, + required this.from, + required this.to, + required this.value, + required this.gas, + required this.gasPrice, + required this.maxFeePerGas, + required this.maxPriorityFeePerGas, + required this.isError, + required this.hasToken, + required this.compressedTx, + required this.gasCost, + required this.gasUsed, + }); + + factory EthTxDTO.fromMap(Map map) => EthTxDTO( + hash: map['hash'] as String, + blockHash: map['blockHash'] as String, + blockNumber: map['blockNumber'] as int, + transactionIndex: map['transactionIndex'] as int, + timestamp: map['timestamp'] as int, + from: map['from'] as String, + to: map['to'] as String, + value: _amountFromJsonNum(map['value']), + gas: _amountFromJsonNum(map['gas']), + gasPrice: _amountFromJsonNum(map['gasPrice']), + maxFeePerGas: _amountFromJsonNum(map['maxFeePerGas']), + maxPriorityFeePerGas: _amountFromJsonNum(map['maxPriorityFeePerGas']), + isError: map['isError'] as int, + hasToken: map['hasToken'] as int, + compressedTx: map['compressedTx'] as String, + gasCost: _amountFromJsonNum(map['gasCost']), + gasUsed: _amountFromJsonNum(map['gasUsed']), + ); + + final String hash; + final String blockHash; + final int blockNumber; + final int transactionIndex; + final int timestamp; + final String from; + final String to; + final Amount value; + final Amount gas; + final Amount gasPrice; + final Amount maxFeePerGas; + final Amount maxPriorityFeePerGas; + final int isError; + final int hasToken; + final String compressedTx; + final Amount gasCost; + final Amount gasUsed; + + static Amount _amountFromJsonNum(dynamic json) { + return Amount( + rawValue: BigInt.from(json as num), + fractionDigits: Coin.ethereum.decimals, + ); + } + + EthTxDTO copyWith({ + String? hash, + String? blockHash, + int? blockNumber, + int? transactionIndex, + int? timestamp, + String? from, + String? to, + Amount? value, + Amount? gas, + Amount? gasPrice, + Amount? maxFeePerGas, + Amount? maxPriorityFeePerGas, + int? isError, + int? hasToken, + String? compressedTx, + Amount? gasCost, + Amount? gasUsed, + }) => + EthTxDTO( + hash: hash ?? this.hash, + blockHash: blockHash ?? this.blockHash, + blockNumber: blockNumber ?? this.blockNumber, + transactionIndex: transactionIndex ?? this.transactionIndex, + timestamp: timestamp ?? this.timestamp, + from: from ?? this.from, + to: to ?? this.to, + value: value ?? this.value, + gas: gas ?? this.gas, + gasPrice: gasPrice ?? this.gasPrice, + maxFeePerGas: maxFeePerGas ?? this.maxFeePerGas, + maxPriorityFeePerGas: maxPriorityFeePerGas ?? this.maxPriorityFeePerGas, + isError: isError ?? this.isError, + hasToken: hasToken ?? this.hasToken, + compressedTx: compressedTx ?? this.compressedTx, + gasCost: gasCost ?? this.gasCost, + gasUsed: gasUsed ?? this.gasUsed, + ); + + Map toMap() { + final map = {}; + map['hash'] = hash; + map['blockHash'] = blockHash; + map['blockNumber'] = blockNumber; + map['transactionIndex'] = transactionIndex; + map['timestamp'] = timestamp; + map['from'] = from; + map['to'] = to; + map['value'] = value; + map['gas'] = gas; + map['gasPrice'] = gasPrice; + map['maxFeePerGas'] = maxFeePerGas; + map['maxPriorityFeePerGas'] = maxPriorityFeePerGas; + map['isError'] = isError; + map['hasToken'] = hasToken; + map['compressedTx'] = compressedTx; + map['gasCost'] = gasCost; + map['gasUsed'] = gasUsed; + return map; + } + + @override + String toString() => jsonEncode(toMap()); +} diff --git a/lib/dto/ethereum/pending_eth_tx_dto.dart b/lib/dto/ethereum/pending_eth_tx_dto.dart new file mode 100644 index 000000000..0c1fa03b6 --- /dev/null +++ b/lib/dto/ethereum/pending_eth_tx_dto.dart @@ -0,0 +1,150 @@ +/// blockHash : null +/// blockNumber : null +/// from : "0x..." +/// gas : "0x7e562" +/// maxPriorityFeePerGas : "0x444380" +/// maxFeePerGas : "0x342570c00" +/// hash : "0x...da64e4" +/// input : "....." +/// nonce : "0x70" +/// to : "0x00....." +/// transactionIndex : null +/// value : "0x0" +/// type : "0x2" +/// accessList : [] +/// chainId : "0x1" +/// v : "0x0" +/// r : "0xd..." +/// s : "0x17d...6e6" + +class PendingEthTxDto { + PendingEthTxDto({ + required this.blockHash, + required this.blockNumber, + required this.from, + required this.gas, + required this.maxPriorityFeePerGas, + required this.maxFeePerGas, + required this.hash, + required this.input, + required this.nonce, + required this.to, + required this.transactionIndex, + required this.value, + required this.type, + required this.accessList, + required this.chainId, + required this.v, + required this.r, + required this.s, + }); + + factory PendingEthTxDto.fromMap(Map map) => PendingEthTxDto( + blockHash: map['blockHash'] as String?, + blockNumber: map['blockNumber'] as int?, + from: map['from'] as String, + gas: map['gas'] as String, + maxPriorityFeePerGas: map['maxPriorityFeePerGas'] as String, + maxFeePerGas: map['maxFeePerGas'] as String, + hash: map['hash'] as String, + input: map['input'] as String, + nonce: map['nonce'] as String, + to: map['to'] as String, + transactionIndex: map['transactionIndex'] as int?, + value: map['value'] as String, + type: map['type'] as String, + accessList: map['accessList'] as List? ?? [], + chainId: map['chainId'] as String, + v: map['v'] as String, + r: map['r'] as String, + s: map['s'] as String, + ); + + final String? blockHash; + final int? blockNumber; + final String from; + final String gas; + final String maxPriorityFeePerGas; + final String maxFeePerGas; + final String hash; + final String input; + final String nonce; + final String to; + final int? transactionIndex; + final String value; + final String type; + final List accessList; + final String chainId; + final String v; + final String r; + final String s; + + PendingEthTxDto copyWith({ + String? blockHash, + int? blockNumber, + String? from, + String? gas, + String? maxPriorityFeePerGas, + String? maxFeePerGas, + String? hash, + String? input, + String? nonce, + String? to, + int? transactionIndex, + String? value, + String? type, + List? accessList, + String? chainId, + String? v, + String? r, + String? s, + }) => + PendingEthTxDto( + blockHash: blockHash ?? this.blockHash, + blockNumber: blockNumber ?? this.blockNumber, + from: from ?? this.from, + gas: gas ?? this.gas, + maxPriorityFeePerGas: maxPriorityFeePerGas ?? this.maxPriorityFeePerGas, + maxFeePerGas: maxFeePerGas ?? this.maxFeePerGas, + hash: hash ?? this.hash, + input: input ?? this.input, + nonce: nonce ?? this.nonce, + to: to ?? this.to, + transactionIndex: transactionIndex ?? this.transactionIndex, + value: value ?? this.value, + type: type ?? this.type, + accessList: accessList ?? this.accessList, + chainId: chainId ?? this.chainId, + v: v ?? this.v, + r: r ?? this.r, + s: s ?? this.s, + ); + + Map toMap() { + final map = {}; + map['blockHash'] = blockHash; + map['blockNumber'] = blockNumber; + map['from'] = from; + map['gas'] = gas; + map['maxPriorityFeePerGas'] = maxPriorityFeePerGas; + map['maxFeePerGas'] = maxFeePerGas; + map['hash'] = hash; + map['input'] = input; + map['nonce'] = nonce; + map['to'] = to; + map['transactionIndex'] = transactionIndex; + map['value'] = value; + map['type'] = type; + map['accessList'] = accessList; + map['chainId'] = chainId; + map['v'] = v; + map['r'] = r; + map['s'] = s; + return map; + } + + @override + String toString() { + return toMap().toString(); + } +} diff --git a/lib/electrumx_rpc/cached_electrumx.dart b/lib/electrumx_rpc/cached_electrumx.dart index e7d815eab..935e8605e 100644 --- a/lib/electrumx_rpc/cached_electrumx.dart +++ b/lib/electrumx_rpc/cached_electrumx.dart @@ -1,7 +1,7 @@ import 'dart:convert'; +import 'package:stackwallet/db/hive/db.dart'; import 'package:stackwallet/electrumx_rpc/electrumx.dart'; -import 'package:stackwallet/hive/db.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:stackwallet/utilities/logger.dart'; import 'package:stackwallet/utilities/prefs.dart'; diff --git a/lib/main.dart b/lib/main.dart index f5208c48f..06a97e881 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -18,12 +18,12 @@ import 'package:hive_flutter/hive_flutter.dart'; import 'package:isar/isar.dart'; import 'package:keyboard_dismisser/keyboard_dismisser.dart'; import 'package:path_provider/path_provider.dart'; -import 'package:stackwallet/db/main_db.dart'; -import 'package:stackwallet/hive/db.dart'; +import 'package:stackwallet/db/hive/db.dart'; +import 'package:stackwallet/db/isar/main_db.dart'; import 'package:stackwallet/models/exchange/change_now/exchange_transaction.dart'; import 'package:stackwallet/models/exchange/change_now/exchange_transaction_status.dart'; import 'package:stackwallet/models/exchange/response_objects/trade.dart'; -import 'package:stackwallet/models/isar/models/log.dart'; +import 'package:stackwallet/models/isar/models/isar_models.dart'; import 'package:stackwallet/models/models.dart'; import 'package:stackwallet/models/node_model.dart'; import 'package:stackwallet/models/notification_model.dart'; @@ -251,6 +251,12 @@ class _MaterialAppWithThemeState extends ConsumerState await ref.read(storageCryptoHandlerProvider).hasPassword(); } await MainDB.instance.initMainDB(); + ref + .read(priceAnd24hChangeNotifierProvider) + .tokenContractAddressesToCheck + .addAll( + await MainDB.instance.getEthContracts().addressProperty().findAll(), + ); } Future load() async { diff --git a/lib/models/add_wallet_list_entity/add_wallet_list_entity.dart b/lib/models/add_wallet_list_entity/add_wallet_list_entity.dart new file mode 100644 index 000000000..3dd24d7b1 --- /dev/null +++ b/lib/models/add_wallet_list_entity/add_wallet_list_entity.dart @@ -0,0 +1,8 @@ +import 'package:equatable/equatable.dart'; +import 'package:stackwallet/utilities/enums/coin_enum.dart'; + +abstract class AddWalletListEntity extends Equatable { + Coin get coin; + String get name; + String get ticker; +} diff --git a/lib/models/add_wallet_list_entity/sub_classes/coin_entity.dart b/lib/models/add_wallet_list_entity/sub_classes/coin_entity.dart new file mode 100644 index 000000000..770a9d1cf --- /dev/null +++ b/lib/models/add_wallet_list_entity/sub_classes/coin_entity.dart @@ -0,0 +1,20 @@ +import 'package:stackwallet/models/add_wallet_list_entity/add_wallet_list_entity.dart'; +import 'package:stackwallet/utilities/enums/coin_enum.dart'; + +class CoinEntity extends AddWalletListEntity { + CoinEntity(this._coin); + + final Coin _coin; + + @override + Coin get coin => _coin; + + @override + String get name => coin.prettyName; + + @override + String get ticker => coin.ticker; + + @override + List get props => [coin, name, ticker]; +} diff --git a/lib/models/add_wallet_list_entity/sub_classes/eth_token_entity.dart b/lib/models/add_wallet_list_entity/sub_classes/eth_token_entity.dart new file mode 100644 index 000000000..ccc0da239 --- /dev/null +++ b/lib/models/add_wallet_list_entity/sub_classes/eth_token_entity.dart @@ -0,0 +1,21 @@ +import 'package:stackwallet/models/add_wallet_list_entity/add_wallet_list_entity.dart'; +import 'package:stackwallet/models/isar/models/ethereum/eth_contract.dart'; +import 'package:stackwallet/utilities/enums/coin_enum.dart'; + +class EthTokenEntity extends AddWalletListEntity { + EthTokenEntity(this.token); + + final EthContract token; + + @override + Coin get coin => Coin.ethereum; + + @override + String get name => token.name; + + @override + String get ticker => token.symbol; + + @override + List get props => [coin, name, ticker, token.address]; +} diff --git a/lib/models/balance.dart b/lib/models/balance.dart index 0589ac90d..aa9f04bde 100644 --- a/lib/models/balance.dart +++ b/lib/models/balance.dart @@ -1,15 +1,21 @@ import 'dart:convert'; -import 'package:decimal/decimal.dart'; +import 'package:stackwallet/utilities/amount/amount.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; -import 'package:stackwallet/utilities/format.dart'; + +enum Unit { + base, + u, + m, + normal; +} class Balance { final Coin coin; - final int total; - final int spendable; - final int blockedTotal; - final int pendingSpendable; + final Amount total; + final Amount spendable; + final Amount blockedTotal; + final Amount pendingSpendable; Balance({ required this.coin, @@ -19,36 +25,64 @@ class Balance { required this.pendingSpendable, }); - Decimal getTotal({bool includeBlocked = true}) => Format.satoshisToAmount( - includeBlocked ? total : total - blockedTotal, - coin: coin, - ); + // Decimal getTotal({bool includeBlocked = true}) => Format.satoshisToAmount( + // includeBlocked ? total : total - blockedTotal, + // coin: coin, + // ); + // + // Decimal getSpendable() => Format.satoshisToAmount( + // spendable, + // coin: coin, + // ); + // + // Decimal getPending() => Format.satoshisToAmount( + // pendingSpendable, + // coin: coin, + // ); + // + // Decimal getBlocked() => Format.satoshisToAmount( + // blockedTotal, + // coin: coin, + // ); - Decimal getSpendable() => Format.satoshisToAmount( - spendable, - coin: coin, - ); - - Decimal getPending() => Format.satoshisToAmount( - pendingSpendable, - coin: coin, - ); - - Decimal getBlocked() => Format.satoshisToAmount( - blockedTotal, - coin: coin, - ); - - String toJsonIgnoreCoin() => jsonEncode(toMap()..remove("coin")); + String toJsonIgnoreCoin() => jsonEncode({ + "total": total.toJsonString(), + "spendable": spendable.toJsonString(), + "blockedTotal": blockedTotal.toJsonString(), + "pendingSpendable": pendingSpendable.toJsonString(), + }); + // need to fall back to parsing from in due to cached balances being previously + // stored as int values instead of Amounts factory Balance.fromJson(String json, Coin coin) { final decoded = jsonDecode(json); return Balance( coin: coin, - total: decoded["total"] as int, - spendable: decoded["spendable"] as int, - blockedTotal: decoded["blockedTotal"] as int, - pendingSpendable: decoded["pendingSpendable"] as int, + total: decoded["total"] is String + ? Amount.fromSerializedJsonString(decoded["total"] as String) + : Amount( + rawValue: BigInt.from(decoded["total"] as int), + fractionDigits: coin.decimals, + ), + spendable: decoded["spendable"] is String + ? Amount.fromSerializedJsonString(decoded["spendable"] as String) + : Amount( + rawValue: BigInt.from(decoded["spendable"] as int), + fractionDigits: coin.decimals, + ), + blockedTotal: decoded["blockedTotal"] is String + ? Amount.fromSerializedJsonString(decoded["blockedTotal"] as String) + : Amount( + rawValue: BigInt.from(decoded["blockedTotal"] as int), + fractionDigits: coin.decimals, + ), + pendingSpendable: decoded["pendingSpendable"] is String + ? Amount.fromSerializedJsonString( + decoded["pendingSpendable"] as String) + : Amount( + rawValue: BigInt.from(decoded["pendingSpendable"] as int), + fractionDigits: coin.decimals, + ), ); } diff --git a/lib/models/isar/exchange_cache/currency.dart b/lib/models/isar/exchange_cache/currency.dart index 81471a3d1..1744f9350 100644 --- a/lib/models/isar/exchange_cache/currency.dart +++ b/lib/models/isar/exchange_cache/currency.dart @@ -44,11 +44,16 @@ class Currency { @Index() final bool isStackCoin; - @ignore - bool get supportsFixedRate => rateType == SupportedRateType.fixed || rateType == SupportedRateType.both; + final String? tokenContract; @ignore - bool get supportsEstimatedRate => rateType == SupportedRateType.estimated || rateType == SupportedRateType.both; + bool get supportsFixedRate => + rateType == SupportedRateType.fixed || rateType == SupportedRateType.both; + + @ignore + bool get supportsEstimatedRate => + rateType == SupportedRateType.estimated || + rateType == SupportedRateType.both; Currency({ required this.exchangeName, @@ -61,6 +66,7 @@ class Currency { required this.rateType, this.isAvailable, required this.isStackCoin, + required this.tokenContract, }); factory Currency.fromJson( @@ -83,6 +89,7 @@ class Currency { isAvailable: json["isAvailable"] as bool?, isStackCoin: json["isStackCoin"] as bool? ?? Currency.checkIsStackCoin(ticker), + tokenContract: json["tokenContract"] as String?, )..id = json["id"] as int?; } catch (e) { rethrow; @@ -102,6 +109,7 @@ class Currency { "rateType": rateType, "isAvailable": isAvailable, "isStackCoin": isStackCoin, + "tokenContract": tokenContract, }; return map; @@ -119,6 +127,7 @@ class Currency { SupportedRateType? rateType, bool? isAvailable, bool? isStackCoin, + String? tokenContract, }) { return Currency( exchangeName: exchangeName ?? this.exchangeName, @@ -131,6 +140,7 @@ class Currency { rateType: rateType ?? this.rateType, isAvailable: isAvailable ?? this.isAvailable, isStackCoin: isStackCoin ?? this.isStackCoin, + tokenContract: tokenContract ?? this.tokenContract, )..id = id ?? this.id; } diff --git a/lib/models/isar/exchange_cache/currency.g.dart b/lib/models/isar/exchange_cache/currency.g.dart index 02c58cadd..8f81bfdc0 100644 --- a/lib/models/isar/exchange_cache/currency.g.dart +++ b/lib/models/isar/exchange_cache/currency.g.dart @@ -67,6 +67,11 @@ const CurrencySchema = CollectionSchema( id: 9, name: r'ticker', type: IsarType.string, + ), + r'tokenContract': PropertySchema( + id: 10, + name: r'tokenContract', + type: IsarType.string, ) }, estimateSize: _currencyEstimateSize, @@ -150,6 +155,12 @@ int _currencyEstimateSize( bytesCount += 3 + object.name.length * 3; bytesCount += 3 + object.network.length * 3; bytesCount += 3 + object.ticker.length * 3; + { + final value = object.tokenContract; + if (value != null) { + bytesCount += 3 + value.length * 3; + } + } return bytesCount; } @@ -169,6 +180,7 @@ void _currencySerialize( writer.writeString(offsets[7], object.network); writer.writeByte(offsets[8], object.rateType.index); writer.writeString(offsets[9], object.ticker); + writer.writeString(offsets[10], object.tokenContract); } Currency _currencyDeserialize( @@ -190,6 +202,7 @@ Currency _currencyDeserialize( _CurrencyrateTypeValueEnumMap[reader.readByteOrNull(offsets[8])] ?? SupportedRateType.fixed, ticker: reader.readString(offsets[9]), + tokenContract: reader.readStringOrNull(offsets[10]), ); object.id = id; return object; @@ -223,6 +236,8 @@ P _currencyDeserializeProp

( SupportedRateType.fixed) as P; case 9: return (reader.readString(offset)) as P; + case 10: + return (reader.readStringOrNull(offset)) as P; default: throw IsarError('Unknown property with id $propertyId'); } @@ -1533,6 +1548,158 @@ extension CurrencyQueryFilter )); }); } + + QueryBuilder + tokenContractIsNull() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(const FilterCondition.isNull( + property: r'tokenContract', + )); + }); + } + + QueryBuilder + tokenContractIsNotNull() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(const FilterCondition.isNotNull( + property: r'tokenContract', + )); + }); + } + + QueryBuilder tokenContractEqualTo( + String? value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'tokenContract', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + tokenContractGreaterThan( + String? value, { + bool include = false, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + include: include, + property: r'tokenContract', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder tokenContractLessThan( + String? value, { + bool include = false, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.lessThan( + include: include, + property: r'tokenContract', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder tokenContractBetween( + String? lower, + String? upper, { + bool includeLower = true, + bool includeUpper = true, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.between( + property: r'tokenContract', + lower: lower, + includeLower: includeLower, + upper: upper, + includeUpper: includeUpper, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + tokenContractStartsWith( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.startsWith( + property: r'tokenContract', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder tokenContractEndsWith( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.endsWith( + property: r'tokenContract', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder tokenContractContains( + String value, + {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.contains( + property: r'tokenContract', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder tokenContractMatches( + String pattern, + {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.matches( + property: r'tokenContract', + wildcard: pattern, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + tokenContractIsEmpty() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'tokenContract', + value: '', + )); + }); + } + + QueryBuilder + tokenContractIsNotEmpty() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + property: r'tokenContract', + value: '', + )); + }); + } } extension CurrencyQueryObject @@ -1661,6 +1828,18 @@ extension CurrencyQuerySortBy on QueryBuilder { return query.addSortBy(r'ticker', Sort.desc); }); } + + QueryBuilder sortByTokenContract() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'tokenContract', Sort.asc); + }); + } + + QueryBuilder sortByTokenContractDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'tokenContract', Sort.desc); + }); + } } extension CurrencyQuerySortThenBy @@ -1796,6 +1975,18 @@ extension CurrencyQuerySortThenBy return query.addSortBy(r'ticker', Sort.desc); }); } + + QueryBuilder thenByTokenContract() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'tokenContract', Sort.asc); + }); + } + + QueryBuilder thenByTokenContractDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'tokenContract', Sort.desc); + }); + } } extension CurrencyQueryWhereDistinct @@ -1865,6 +2056,14 @@ extension CurrencyQueryWhereDistinct return query.addDistinctBy(r'ticker', caseSensitive: caseSensitive); }); } + + QueryBuilder distinctByTokenContract( + {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addDistinctBy(r'tokenContract', + caseSensitive: caseSensitive); + }); + } } extension CurrencyQueryProperty @@ -1935,4 +2134,10 @@ extension CurrencyQueryProperty return query.addPropertyName(r'ticker'); }); } + + QueryBuilder tokenContractProperty() { + return QueryBuilder.apply(this, (query) { + return query.addPropertyName(r'tokenContract'); + }); + } } diff --git a/lib/models/isar/models/blockchain_data/address.dart b/lib/models/isar/models/blockchain_data/address.dart index 770af4a6b..25281a629 100644 --- a/lib/models/isar/models/blockchain_data/address.dart +++ b/lib/models/isar/models/blockchain_data/address.dart @@ -122,7 +122,8 @@ enum AddressType { cryptonote, mimbleWimble, unknown, - nonWallet; + nonWallet, + ethereum; String get readableName { switch (this) { @@ -140,6 +141,8 @@ enum AddressType { return "Unknown"; case AddressType.nonWallet: return "Non wallet/unknown"; + case AddressType.ethereum: + return "Ethereum"; } } } diff --git a/lib/models/isar/models/blockchain_data/address.g.dart b/lib/models/isar/models/blockchain_data/address.g.dart index 37186584b..49188b395 100644 --- a/lib/models/isar/models/blockchain_data/address.g.dart +++ b/lib/models/isar/models/blockchain_data/address.g.dart @@ -260,6 +260,7 @@ const _AddresstypeEnumValueMap = { 'mimbleWimble': 4, 'unknown': 5, 'nonWallet': 6, + 'ethereum': 7, }; const _AddresstypeValueEnumMap = { 0: AddressType.p2pkh, @@ -269,6 +270,7 @@ const _AddresstypeValueEnumMap = { 4: AddressType.mimbleWimble, 5: AddressType.unknown, 6: AddressType.nonWallet, + 7: AddressType.ethereum, }; Id _addressGetId(Address object) { diff --git a/lib/models/isar/models/blockchain_data/transaction.dart b/lib/models/isar/models/blockchain_data/transaction.dart index 191f62b46..39d2cc0af 100644 --- a/lib/models/isar/models/blockchain_data/transaction.dart +++ b/lib/models/isar/models/blockchain_data/transaction.dart @@ -5,6 +5,7 @@ import 'package:isar/isar.dart'; import 'package:stackwallet/models/isar/models/blockchain_data/address.dart'; import 'package:stackwallet/models/isar/models/blockchain_data/input.dart'; import 'package:stackwallet/models/isar/models/blockchain_data/output.dart'; +import 'package:stackwallet/utilities/amount/amount.dart'; import 'package:tuple/tuple.dart'; part 'transaction.g.dart'; @@ -18,6 +19,7 @@ class Transaction { required this.type, required this.subType, required this.amount, + required this.amountString, required this.fee, required this.height, required this.isCancelled, @@ -26,6 +28,7 @@ class Transaction { required this.otherData, required this.inputs, required this.outputs, + required this.nonce, }); Tuple2 copyWith({ @@ -35,6 +38,7 @@ class Transaction { TransactionType? type, TransactionSubType? subType, int? amount, + String? amountString, int? fee, int? height, bool? isCancelled, @@ -43,6 +47,7 @@ class Transaction { String? otherData, List? inputs, List? outputs, + int? nonce, Id? id, Address? address, }) { @@ -54,12 +59,14 @@ class Transaction { type: type ?? this.type, subType: subType ?? this.subType, amount: amount ?? this.amount, + amountString: amountString ?? this.amountString, fee: fee ?? this.fee, height: height ?? this.height, isCancelled: isCancelled ?? this.isCancelled, isLelantus: isLelantus ?? this.isLelantus, slateId: slateId ?? this.slateId, otherData: otherData ?? this.otherData, + nonce: nonce ?? this.nonce, inputs: inputs ?? this.inputs, outputs: outputs ?? this.outputs) ..id = id ?? this.id, @@ -84,8 +91,11 @@ class Transaction { @enumerated late final TransactionSubType subType; + @Deprecated("May be inaccurate for large amounts (eth for example)") late final int amount; + late String? amountString; + late final int fee; late final int? height; @@ -98,6 +108,8 @@ class Transaction { late final String? otherData; + late final int? nonce; + late final List inputs; late final List outputs; @@ -105,6 +117,13 @@ class Transaction { @Backlink(to: "transactions") final address = IsarLink

(); + @ignore + Amount? _cachedAmount; + + @ignore + Amount get realAmount => + _cachedAmount ??= Amount.fromSerializedJsonString(amountString!); + int getConfirmations(int currentChainHeight) { if (height == null || height! <= 0) return 0; return max(0, currentChainHeight - (height! - 1)); @@ -124,12 +143,14 @@ class Transaction { "type: ${type.name}, " "subType: ${subType.name}, " "amount: $amount, " + "amountString: $amountString, " "fee: $fee, " "height: $height, " "isCancelled: $isCancelled, " "isLelantus: $isLelantus, " "slateId: $slateId, " "otherData: $otherData, " + "nonce: $nonce, " "address: ${address.value}, " "inputsLength: ${inputs.length}, " "outputsLength: ${outputs.length}, " @@ -143,12 +164,14 @@ class Transaction { "type": type.name, "subType": subType.name, "amount": amount, + "amountString": amountString, "fee": fee, "height": height, "isCancelled": isCancelled, "isLelantus": isLelantus, "slateId": slateId, "otherData": otherData, + "nonce": nonce, "address": address.value?.toJsonString(), "inputs": inputs.map((e) => e.toJsonString()).toList(), "outputs": outputs.map((e) => e.toJsonString()).toList(), @@ -168,12 +191,14 @@ class Transaction { type: TransactionType.values.byName(json["type"] as String), subType: TransactionSubType.values.byName(json["subType"] as String), amount: json["amount"] as int, + amountString: json["amountString"] as String, fee: json["fee"] as int, height: json["height"] as int?, isCancelled: json["isCancelled"] as bool, isLelantus: json["isLelantus"] as bool?, slateId: json["slateId"] as String?, otherData: json["otherData"] as String?, + nonce: json["nonce"] as int?, inputs: List.from(json["inputs"] as List) .map((e) => Input.fromJsonString(e)) .toList(), @@ -207,5 +232,6 @@ enum TransactionSubType { none, bip47Notification, // bip47 payment code notification transaction flag mint, // firo specific - join; // firo specific + join, // firo specific + ethToken; // eth token } diff --git a/lib/models/isar/models/blockchain_data/transaction.g.dart b/lib/models/isar/models/blockchain_data/transaction.g.dart index 53acce84f..74a5a1652 100644 --- a/lib/models/isar/models/blockchain_data/transaction.g.dart +++ b/lib/models/isar/models/blockchain_data/transaction.g.dart @@ -22,72 +22,82 @@ const TransactionSchema = CollectionSchema( name: r'amount', type: IsarType.long, ), - r'fee': PropertySchema( + r'amountString': PropertySchema( id: 1, + name: r'amountString', + type: IsarType.string, + ), + r'fee': PropertySchema( + id: 2, name: r'fee', type: IsarType.long, ), r'height': PropertySchema( - id: 2, + id: 3, name: r'height', type: IsarType.long, ), r'inputs': PropertySchema( - id: 3, + id: 4, name: r'inputs', type: IsarType.objectList, target: r'Input', ), r'isCancelled': PropertySchema( - id: 4, + id: 5, name: r'isCancelled', type: IsarType.bool, ), r'isLelantus': PropertySchema( - id: 5, + id: 6, name: r'isLelantus', type: IsarType.bool, ), + r'nonce': PropertySchema( + id: 7, + name: r'nonce', + type: IsarType.long, + ), r'otherData': PropertySchema( - id: 6, + id: 8, name: r'otherData', type: IsarType.string, ), r'outputs': PropertySchema( - id: 7, + id: 9, name: r'outputs', type: IsarType.objectList, target: r'Output', ), r'slateId': PropertySchema( - id: 8, + id: 10, name: r'slateId', type: IsarType.string, ), r'subType': PropertySchema( - id: 9, + id: 11, name: r'subType', type: IsarType.byte, enumMap: _TransactionsubTypeEnumValueMap, ), r'timestamp': PropertySchema( - id: 10, + id: 12, name: r'timestamp', type: IsarType.long, ), r'txid': PropertySchema( - id: 11, + id: 13, name: r'txid', type: IsarType.string, ), r'type': PropertySchema( - id: 12, + id: 14, name: r'type', type: IsarType.byte, enumMap: _TransactiontypeEnumValueMap, ), r'walletId': PropertySchema( - id: 13, + id: 15, name: r'walletId', type: IsarType.string, ) @@ -165,6 +175,12 @@ int _transactionEstimateSize( Map> allOffsets, ) { var bytesCount = offsets.last; + { + final value = object.amountString; + if (value != null) { + bytesCount += 3 + value.length * 3; + } + } bytesCount += 3 + object.inputs.length * 3; { final offsets = allOffsets[Input]!; @@ -205,29 +221,31 @@ void _transactionSerialize( Map> allOffsets, ) { writer.writeLong(offsets[0], object.amount); - writer.writeLong(offsets[1], object.fee); - writer.writeLong(offsets[2], object.height); + writer.writeString(offsets[1], object.amountString); + writer.writeLong(offsets[2], object.fee); + writer.writeLong(offsets[3], object.height); writer.writeObjectList( - offsets[3], + offsets[4], allOffsets, InputSchema.serialize, object.inputs, ); - writer.writeBool(offsets[4], object.isCancelled); - writer.writeBool(offsets[5], object.isLelantus); - writer.writeString(offsets[6], object.otherData); + writer.writeBool(offsets[5], object.isCancelled); + writer.writeBool(offsets[6], object.isLelantus); + writer.writeLong(offsets[7], object.nonce); + writer.writeString(offsets[8], object.otherData); writer.writeObjectList( - offsets[7], + offsets[9], allOffsets, OutputSchema.serialize, object.outputs, ); - writer.writeString(offsets[8], object.slateId); - writer.writeByte(offsets[9], object.subType.index); - writer.writeLong(offsets[10], object.timestamp); - writer.writeString(offsets[11], object.txid); - writer.writeByte(offsets[12], object.type.index); - writer.writeString(offsets[13], object.walletId); + writer.writeString(offsets[10], object.slateId); + writer.writeByte(offsets[11], object.subType.index); + writer.writeLong(offsets[12], object.timestamp); + writer.writeString(offsets[13], object.txid); + writer.writeByte(offsets[14], object.type.index); + writer.writeString(offsets[15], object.walletId); } Transaction _transactionDeserialize( @@ -238,34 +256,36 @@ Transaction _transactionDeserialize( ) { final object = Transaction( amount: reader.readLong(offsets[0]), - fee: reader.readLong(offsets[1]), - height: reader.readLongOrNull(offsets[2]), + amountString: reader.readStringOrNull(offsets[1]), + fee: reader.readLong(offsets[2]), + height: reader.readLongOrNull(offsets[3]), inputs: reader.readObjectList( - offsets[3], + offsets[4], InputSchema.deserialize, allOffsets, Input(), ) ?? [], - isCancelled: reader.readBool(offsets[4]), - isLelantus: reader.readBoolOrNull(offsets[5]), - otherData: reader.readStringOrNull(offsets[6]), + isCancelled: reader.readBool(offsets[5]), + isLelantus: reader.readBoolOrNull(offsets[6]), + nonce: reader.readLongOrNull(offsets[7]), + otherData: reader.readStringOrNull(offsets[8]), outputs: reader.readObjectList( - offsets[7], + offsets[9], OutputSchema.deserialize, allOffsets, Output(), ) ?? [], - slateId: reader.readStringOrNull(offsets[8]), + slateId: reader.readStringOrNull(offsets[10]), subType: - _TransactionsubTypeValueEnumMap[reader.readByteOrNull(offsets[9])] ?? + _TransactionsubTypeValueEnumMap[reader.readByteOrNull(offsets[11])] ?? TransactionSubType.none, - timestamp: reader.readLong(offsets[10]), - txid: reader.readString(offsets[11]), - type: _TransactiontypeValueEnumMap[reader.readByteOrNull(offsets[12])] ?? + timestamp: reader.readLong(offsets[12]), + txid: reader.readString(offsets[13]), + type: _TransactiontypeValueEnumMap[reader.readByteOrNull(offsets[14])] ?? TransactionType.outgoing, - walletId: reader.readString(offsets[13]), + walletId: reader.readString(offsets[15]), ); object.id = id; return object; @@ -281,10 +301,12 @@ P _transactionDeserializeProp

( case 0: return (reader.readLong(offset)) as P; case 1: - return (reader.readLong(offset)) as P; + return (reader.readStringOrNull(offset)) as P; case 2: - return (reader.readLongOrNull(offset)) as P; + return (reader.readLong(offset)) as P; case 3: + return (reader.readLongOrNull(offset)) as P; + case 4: return (reader.readObjectList( offset, InputSchema.deserialize, @@ -292,13 +314,15 @@ P _transactionDeserializeProp

( Input(), ) ?? []) as P; - case 4: - return (reader.readBool(offset)) as P; case 5: - return (reader.readBoolOrNull(offset)) as P; + return (reader.readBool(offset)) as P; case 6: - return (reader.readStringOrNull(offset)) as P; + return (reader.readBoolOrNull(offset)) as P; case 7: + return (reader.readLongOrNull(offset)) as P; + case 8: + return (reader.readStringOrNull(offset)) as P; + case 9: return (reader.readObjectList( offset, OutputSchema.deserialize, @@ -306,19 +330,19 @@ P _transactionDeserializeProp

( Output(), ) ?? []) as P; - case 8: + case 10: return (reader.readStringOrNull(offset)) as P; - case 9: + case 11: return (_TransactionsubTypeValueEnumMap[reader.readByteOrNull(offset)] ?? TransactionSubType.none) as P; - case 10: - return (reader.readLong(offset)) as P; - case 11: - return (reader.readString(offset)) as P; case 12: + return (reader.readLong(offset)) as P; + case 13: + return (reader.readString(offset)) as P; + case 14: return (_TransactiontypeValueEnumMap[reader.readByteOrNull(offset)] ?? TransactionType.outgoing) as P; - case 13: + case 15: return (reader.readString(offset)) as P; default: throw IsarError('Unknown property with id $propertyId'); @@ -330,12 +354,14 @@ const _TransactionsubTypeEnumValueMap = { 'bip47Notification': 1, 'mint': 2, 'join': 3, + 'ethToken': 4, }; const _TransactionsubTypeValueEnumMap = { 0: TransactionSubType.none, 1: TransactionSubType.bip47Notification, 2: TransactionSubType.mint, 3: TransactionSubType.join, + 4: TransactionSubType.ethToken, }; const _TransactiontypeEnumValueMap = { 'outgoing': 0, @@ -819,6 +845,160 @@ extension TransactionQueryFilter }); } + QueryBuilder + amountStringIsNull() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(const FilterCondition.isNull( + property: r'amountString', + )); + }); + } + + QueryBuilder + amountStringIsNotNull() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(const FilterCondition.isNotNull( + property: r'amountString', + )); + }); + } + + QueryBuilder + amountStringEqualTo( + String? value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'amountString', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + amountStringGreaterThan( + String? value, { + bool include = false, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + include: include, + property: r'amountString', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + amountStringLessThan( + String? value, { + bool include = false, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.lessThan( + include: include, + property: r'amountString', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + amountStringBetween( + String? lower, + String? upper, { + bool includeLower = true, + bool includeUpper = true, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.between( + property: r'amountString', + lower: lower, + includeLower: includeLower, + upper: upper, + includeUpper: includeUpper, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + amountStringStartsWith( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.startsWith( + property: r'amountString', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + amountStringEndsWith( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.endsWith( + property: r'amountString', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + amountStringContains(String value, {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.contains( + property: r'amountString', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + amountStringMatches(String pattern, {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.matches( + property: r'amountString', + wildcard: pattern, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + amountStringIsEmpty() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'amountString', + value: '', + )); + }); + } + + QueryBuilder + amountStringIsNotEmpty() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + property: r'amountString', + value: '', + )); + }); + } + QueryBuilder feeEqualTo( int value) { return QueryBuilder.apply(this, (query) { @@ -1123,6 +1303,77 @@ extension TransactionQueryFilter }); } + QueryBuilder nonceIsNull() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(const FilterCondition.isNull( + property: r'nonce', + )); + }); + } + + QueryBuilder + nonceIsNotNull() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(const FilterCondition.isNotNull( + property: r'nonce', + )); + }); + } + + QueryBuilder nonceEqualTo( + int? value) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'nonce', + value: value, + )); + }); + } + + QueryBuilder + nonceGreaterThan( + int? value, { + bool include = false, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + include: include, + property: r'nonce', + value: value, + )); + }); + } + + QueryBuilder nonceLessThan( + int? value, { + bool include = false, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.lessThan( + include: include, + property: r'nonce', + value: value, + )); + }); + } + + QueryBuilder nonceBetween( + int? lower, + int? upper, { + bool includeLower = true, + bool includeUpper = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.between( + property: r'nonce', + lower: lower, + includeLower: includeLower, + upper: upper, + includeUpper: includeUpper, + )); + }); + } + QueryBuilder otherDataIsNull() { return QueryBuilder.apply(this, (query) { @@ -1996,6 +2247,19 @@ extension TransactionQuerySortBy }); } + QueryBuilder sortByAmountString() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'amountString', Sort.asc); + }); + } + + QueryBuilder + sortByAmountStringDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'amountString', Sort.desc); + }); + } + QueryBuilder sortByFee() { return QueryBuilder.apply(this, (query) { return query.addSortBy(r'fee', Sort.asc); @@ -2044,6 +2308,18 @@ extension TransactionQuerySortBy }); } + QueryBuilder sortByNonce() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'nonce', Sort.asc); + }); + } + + QueryBuilder sortByNonceDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'nonce', Sort.desc); + }); + } + QueryBuilder sortByOtherData() { return QueryBuilder.apply(this, (query) { return query.addSortBy(r'otherData', Sort.asc); @@ -2143,6 +2419,19 @@ extension TransactionQuerySortThenBy }); } + QueryBuilder thenByAmountString() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'amountString', Sort.asc); + }); + } + + QueryBuilder + thenByAmountStringDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'amountString', Sort.desc); + }); + } + QueryBuilder thenByFee() { return QueryBuilder.apply(this, (query) { return query.addSortBy(r'fee', Sort.asc); @@ -2203,6 +2492,18 @@ extension TransactionQuerySortThenBy }); } + QueryBuilder thenByNonce() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'nonce', Sort.asc); + }); + } + + QueryBuilder thenByNonceDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'nonce', Sort.desc); + }); + } + QueryBuilder thenByOtherData() { return QueryBuilder.apply(this, (query) { return query.addSortBy(r'otherData', Sort.asc); @@ -2296,6 +2597,13 @@ extension TransactionQueryWhereDistinct }); } + QueryBuilder distinctByAmountString( + {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addDistinctBy(r'amountString', caseSensitive: caseSensitive); + }); + } + QueryBuilder distinctByFee() { return QueryBuilder.apply(this, (query) { return query.addDistinctBy(r'fee'); @@ -2320,6 +2628,12 @@ extension TransactionQueryWhereDistinct }); } + QueryBuilder distinctByNonce() { + return QueryBuilder.apply(this, (query) { + return query.addDistinctBy(r'nonce'); + }); + } + QueryBuilder distinctByOtherData( {bool caseSensitive = true}) { return QueryBuilder.apply(this, (query) { @@ -2381,6 +2695,12 @@ extension TransactionQueryProperty }); } + QueryBuilder amountStringProperty() { + return QueryBuilder.apply(this, (query) { + return query.addPropertyName(r'amountString'); + }); + } + QueryBuilder feeProperty() { return QueryBuilder.apply(this, (query) { return query.addPropertyName(r'fee'); @@ -2411,6 +2731,12 @@ extension TransactionQueryProperty }); } + QueryBuilder nonceProperty() { + return QueryBuilder.apply(this, (query) { + return query.addPropertyName(r'nonce'); + }); + } + QueryBuilder otherDataProperty() { return QueryBuilder.apply(this, (query) { return query.addPropertyName(r'otherData'); diff --git a/lib/models/isar/models/contract.dart b/lib/models/isar/models/contract.dart new file mode 100644 index 000000000..88e2a454c --- /dev/null +++ b/lib/models/isar/models/contract.dart @@ -0,0 +1,3 @@ +abstract class Contract { + // for possible future use +} diff --git a/lib/models/isar/models/ethereum/eth_contract.dart b/lib/models/isar/models/ethereum/eth_contract.dart new file mode 100644 index 000000000..d969cd503 --- /dev/null +++ b/lib/models/isar/models/ethereum/eth_contract.dart @@ -0,0 +1,60 @@ +import 'package:isar/isar.dart'; +import 'package:stackwallet/models/isar/models/contract.dart'; + +part 'eth_contract.g.dart'; + +@collection +class EthContract extends Contract { + EthContract({ + required this.address, + required this.name, + required this.symbol, + required this.decimals, + required this.type, + this.abi, + }); + + Id id = Isar.autoIncrement; + + @Index(unique: true, replace: true) + late final String address; + + late final String name; + + late final String symbol; + + late final int decimals; + + late final String? abi; + + @enumerated + late final EthContractType type; + + EthContract copyWith({ + Id? id, + String? address, + String? name, + String? symbol, + int? decimals, + EthContractType? type, + List? walletIds, + String? abi, + String? otherData, + }) => + EthContract( + address: address ?? this.address, + name: name ?? this.name, + symbol: symbol ?? this.symbol, + decimals: decimals ?? this.decimals, + type: type ?? this.type, + abi: abi ?? this.abi, + )..id = id ?? this.id; +} + +// Used in Isar db and stored there as int indexes so adding/removing values +// in this definition should be done extremely carefully in production +enum EthContractType { + unknown, + erc20, + erc721; +} diff --git a/lib/models/isar/models/ethereum/eth_contract.g.dart b/lib/models/isar/models/ethereum/eth_contract.g.dart new file mode 100644 index 000000000..bc9548e8d --- /dev/null +++ b/lib/models/isar/models/ethereum/eth_contract.g.dart @@ -0,0 +1,1322 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'eth_contract.dart'; + +// ************************************************************************** +// IsarCollectionGenerator +// ************************************************************************** + +// coverage:ignore-file +// ignore_for_file: duplicate_ignore, non_constant_identifier_names, constant_identifier_names, invalid_use_of_protected_member, unnecessary_cast, prefer_const_constructors, lines_longer_than_80_chars, require_trailing_commas, inference_failure_on_function_invocation, unnecessary_parenthesis, unnecessary_raw_strings, unnecessary_null_checks, join_return_with_assignment, prefer_final_locals, avoid_js_rounded_ints, avoid_positional_boolean_parameters + +extension GetEthContractCollection on Isar { + IsarCollection get ethContracts => this.collection(); +} + +const EthContractSchema = CollectionSchema( + name: r'EthContract', + id: 3784021410994159165, + properties: { + r'abi': PropertySchema( + id: 0, + name: r'abi', + type: IsarType.string, + ), + r'address': PropertySchema( + id: 1, + name: r'address', + type: IsarType.string, + ), + r'decimals': PropertySchema( + id: 2, + name: r'decimals', + type: IsarType.long, + ), + r'name': PropertySchema( + id: 3, + name: r'name', + type: IsarType.string, + ), + r'symbol': PropertySchema( + id: 4, + name: r'symbol', + type: IsarType.string, + ), + r'type': PropertySchema( + id: 5, + name: r'type', + type: IsarType.byte, + enumMap: _EthContracttypeEnumValueMap, + ) + }, + estimateSize: _ethContractEstimateSize, + serialize: _ethContractSerialize, + deserialize: _ethContractDeserialize, + deserializeProp: _ethContractDeserializeProp, + idName: r'id', + indexes: { + r'address': IndexSchema( + id: -259407546592846288, + name: r'address', + unique: true, + replace: true, + properties: [ + IndexPropertySchema( + name: r'address', + type: IndexType.hash, + caseSensitive: true, + ) + ], + ) + }, + links: {}, + embeddedSchemas: {}, + getId: _ethContractGetId, + getLinks: _ethContractGetLinks, + attach: _ethContractAttach, + version: '3.0.5', +); + +int _ethContractEstimateSize( + EthContract object, + List offsets, + Map> allOffsets, +) { + var bytesCount = offsets.last; + { + final value = object.abi; + if (value != null) { + bytesCount += 3 + value.length * 3; + } + } + bytesCount += 3 + object.address.length * 3; + bytesCount += 3 + object.name.length * 3; + bytesCount += 3 + object.symbol.length * 3; + return bytesCount; +} + +void _ethContractSerialize( + EthContract object, + IsarWriter writer, + List offsets, + Map> allOffsets, +) { + writer.writeString(offsets[0], object.abi); + writer.writeString(offsets[1], object.address); + writer.writeLong(offsets[2], object.decimals); + writer.writeString(offsets[3], object.name); + writer.writeString(offsets[4], object.symbol); + writer.writeByte(offsets[5], object.type.index); +} + +EthContract _ethContractDeserialize( + Id id, + IsarReader reader, + List offsets, + Map> allOffsets, +) { + final object = EthContract( + abi: reader.readStringOrNull(offsets[0]), + address: reader.readString(offsets[1]), + decimals: reader.readLong(offsets[2]), + name: reader.readString(offsets[3]), + symbol: reader.readString(offsets[4]), + type: _EthContracttypeValueEnumMap[reader.readByteOrNull(offsets[5])] ?? + EthContractType.unknown, + ); + object.id = id; + return object; +} + +P _ethContractDeserializeProp

( + IsarReader reader, + int propertyId, + int offset, + Map> allOffsets, +) { + switch (propertyId) { + case 0: + return (reader.readStringOrNull(offset)) as P; + case 1: + return (reader.readString(offset)) as P; + case 2: + return (reader.readLong(offset)) as P; + case 3: + return (reader.readString(offset)) as P; + case 4: + return (reader.readString(offset)) as P; + case 5: + return (_EthContracttypeValueEnumMap[reader.readByteOrNull(offset)] ?? + EthContractType.unknown) as P; + default: + throw IsarError('Unknown property with id $propertyId'); + } +} + +const _EthContracttypeEnumValueMap = { + 'unknown': 0, + 'erc20': 1, + 'erc721': 2, +}; +const _EthContracttypeValueEnumMap = { + 0: EthContractType.unknown, + 1: EthContractType.erc20, + 2: EthContractType.erc721, +}; + +Id _ethContractGetId(EthContract object) { + return object.id; +} + +List> _ethContractGetLinks(EthContract object) { + return []; +} + +void _ethContractAttach( + IsarCollection col, Id id, EthContract object) { + object.id = id; +} + +extension EthContractByIndex on IsarCollection { + Future getByAddress(String address) { + return getByIndex(r'address', [address]); + } + + EthContract? getByAddressSync(String address) { + return getByIndexSync(r'address', [address]); + } + + Future deleteByAddress(String address) { + return deleteByIndex(r'address', [address]); + } + + bool deleteByAddressSync(String address) { + return deleteByIndexSync(r'address', [address]); + } + + Future> getAllByAddress(List addressValues) { + final values = addressValues.map((e) => [e]).toList(); + return getAllByIndex(r'address', values); + } + + List getAllByAddressSync(List addressValues) { + final values = addressValues.map((e) => [e]).toList(); + return getAllByIndexSync(r'address', values); + } + + Future deleteAllByAddress(List addressValues) { + final values = addressValues.map((e) => [e]).toList(); + return deleteAllByIndex(r'address', values); + } + + int deleteAllByAddressSync(List addressValues) { + final values = addressValues.map((e) => [e]).toList(); + return deleteAllByIndexSync(r'address', values); + } + + Future putByAddress(EthContract object) { + return putByIndex(r'address', object); + } + + Id putByAddressSync(EthContract object, {bool saveLinks = true}) { + return putByIndexSync(r'address', object, saveLinks: saveLinks); + } + + Future> putAllByAddress(List objects) { + return putAllByIndex(r'address', objects); + } + + List putAllByAddressSync(List objects, + {bool saveLinks = true}) { + return putAllByIndexSync(r'address', objects, saveLinks: saveLinks); + } +} + +extension EthContractQueryWhereSort + on QueryBuilder { + QueryBuilder anyId() { + return QueryBuilder.apply(this, (query) { + return query.addWhereClause(const IdWhereClause.any()); + }); + } +} + +extension EthContractQueryWhere + on QueryBuilder { + QueryBuilder idEqualTo(Id id) { + return QueryBuilder.apply(this, (query) { + return query.addWhereClause(IdWhereClause.between( + lower: id, + upper: id, + )); + }); + } + + QueryBuilder idNotEqualTo( + Id id) { + return QueryBuilder.apply(this, (query) { + if (query.whereSort == Sort.asc) { + return query + .addWhereClause( + IdWhereClause.lessThan(upper: id, includeUpper: false), + ) + .addWhereClause( + IdWhereClause.greaterThan(lower: id, includeLower: false), + ); + } else { + return query + .addWhereClause( + IdWhereClause.greaterThan(lower: id, includeLower: false), + ) + .addWhereClause( + IdWhereClause.lessThan(upper: id, includeUpper: false), + ); + } + }); + } + + QueryBuilder idGreaterThan(Id id, + {bool include = false}) { + return QueryBuilder.apply(this, (query) { + return query.addWhereClause( + IdWhereClause.greaterThan(lower: id, includeLower: include), + ); + }); + } + + QueryBuilder idLessThan(Id id, + {bool include = false}) { + return QueryBuilder.apply(this, (query) { + return query.addWhereClause( + IdWhereClause.lessThan(upper: id, includeUpper: include), + ); + }); + } + + QueryBuilder idBetween( + Id lowerId, + Id upperId, { + bool includeLower = true, + bool includeUpper = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addWhereClause(IdWhereClause.between( + lower: lowerId, + includeLower: includeLower, + upper: upperId, + includeUpper: includeUpper, + )); + }); + } + + QueryBuilder addressEqualTo( + String address) { + return QueryBuilder.apply(this, (query) { + return query.addWhereClause(IndexWhereClause.equalTo( + indexName: r'address', + value: [address], + )); + }); + } + + QueryBuilder addressNotEqualTo( + String address) { + return QueryBuilder.apply(this, (query) { + if (query.whereSort == Sort.asc) { + return query + .addWhereClause(IndexWhereClause.between( + indexName: r'address', + lower: [], + upper: [address], + includeUpper: false, + )) + .addWhereClause(IndexWhereClause.between( + indexName: r'address', + lower: [address], + includeLower: false, + upper: [], + )); + } else { + return query + .addWhereClause(IndexWhereClause.between( + indexName: r'address', + lower: [address], + includeLower: false, + upper: [], + )) + .addWhereClause(IndexWhereClause.between( + indexName: r'address', + lower: [], + upper: [address], + includeUpper: false, + )); + } + }); + } +} + +extension EthContractQueryFilter + on QueryBuilder { + QueryBuilder abiIsNull() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(const FilterCondition.isNull( + property: r'abi', + )); + }); + } + + QueryBuilder abiIsNotNull() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(const FilterCondition.isNotNull( + property: r'abi', + )); + }); + } + + QueryBuilder abiEqualTo( + String? value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'abi', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder abiGreaterThan( + String? value, { + bool include = false, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + include: include, + property: r'abi', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder abiLessThan( + String? value, { + bool include = false, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.lessThan( + include: include, + property: r'abi', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder abiBetween( + String? lower, + String? upper, { + bool includeLower = true, + bool includeUpper = true, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.between( + property: r'abi', + lower: lower, + includeLower: includeLower, + upper: upper, + includeUpper: includeUpper, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder abiStartsWith( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.startsWith( + property: r'abi', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder abiEndsWith( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.endsWith( + property: r'abi', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder abiContains( + String value, + {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.contains( + property: r'abi', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder abiMatches( + String pattern, + {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.matches( + property: r'abi', + wildcard: pattern, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder abiIsEmpty() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'abi', + value: '', + )); + }); + } + + QueryBuilder + abiIsNotEmpty() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + property: r'abi', + value: '', + )); + }); + } + + QueryBuilder addressEqualTo( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'address', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + addressGreaterThan( + String value, { + bool include = false, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + include: include, + property: r'address', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder addressLessThan( + String value, { + bool include = false, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.lessThan( + include: include, + property: r'address', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder addressBetween( + String lower, + String upper, { + bool includeLower = true, + bool includeUpper = true, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.between( + property: r'address', + lower: lower, + includeLower: includeLower, + upper: upper, + includeUpper: includeUpper, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + addressStartsWith( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.startsWith( + property: r'address', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder addressEndsWith( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.endsWith( + property: r'address', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder addressContains( + String value, + {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.contains( + property: r'address', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder addressMatches( + String pattern, + {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.matches( + property: r'address', + wildcard: pattern, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + addressIsEmpty() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'address', + value: '', + )); + }); + } + + QueryBuilder + addressIsNotEmpty() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + property: r'address', + value: '', + )); + }); + } + + QueryBuilder decimalsEqualTo( + int value) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'decimals', + value: value, + )); + }); + } + + QueryBuilder + decimalsGreaterThan( + int value, { + bool include = false, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + include: include, + property: r'decimals', + value: value, + )); + }); + } + + QueryBuilder + decimalsLessThan( + int value, { + bool include = false, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.lessThan( + include: include, + property: r'decimals', + value: value, + )); + }); + } + + QueryBuilder decimalsBetween( + int lower, + int upper, { + bool includeLower = true, + bool includeUpper = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.between( + property: r'decimals', + lower: lower, + includeLower: includeLower, + upper: upper, + includeUpper: includeUpper, + )); + }); + } + + QueryBuilder idEqualTo( + Id value) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'id', + value: value, + )); + }); + } + + QueryBuilder idGreaterThan( + Id value, { + bool include = false, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + include: include, + property: r'id', + value: value, + )); + }); + } + + QueryBuilder idLessThan( + Id value, { + bool include = false, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.lessThan( + include: include, + property: r'id', + value: value, + )); + }); + } + + QueryBuilder idBetween( + Id lower, + Id upper, { + bool includeLower = true, + bool includeUpper = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.between( + property: r'id', + lower: lower, + includeLower: includeLower, + upper: upper, + includeUpper: includeUpper, + )); + }); + } + + QueryBuilder nameEqualTo( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'name', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder nameGreaterThan( + String value, { + bool include = false, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + include: include, + property: r'name', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder nameLessThan( + String value, { + bool include = false, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.lessThan( + include: include, + property: r'name', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder nameBetween( + String lower, + String upper, { + bool includeLower = true, + bool includeUpper = true, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.between( + property: r'name', + lower: lower, + includeLower: includeLower, + upper: upper, + includeUpper: includeUpper, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder nameStartsWith( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.startsWith( + property: r'name', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder nameEndsWith( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.endsWith( + property: r'name', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder nameContains( + String value, + {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.contains( + property: r'name', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder nameMatches( + String pattern, + {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.matches( + property: r'name', + wildcard: pattern, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder nameIsEmpty() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'name', + value: '', + )); + }); + } + + QueryBuilder + nameIsNotEmpty() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + property: r'name', + value: '', + )); + }); + } + + QueryBuilder symbolEqualTo( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'symbol', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + symbolGreaterThan( + String value, { + bool include = false, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + include: include, + property: r'symbol', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder symbolLessThan( + String value, { + bool include = false, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.lessThan( + include: include, + property: r'symbol', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder symbolBetween( + String lower, + String upper, { + bool includeLower = true, + bool includeUpper = true, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.between( + property: r'symbol', + lower: lower, + includeLower: includeLower, + upper: upper, + includeUpper: includeUpper, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + symbolStartsWith( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.startsWith( + property: r'symbol', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder symbolEndsWith( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.endsWith( + property: r'symbol', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder symbolContains( + String value, + {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.contains( + property: r'symbol', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder symbolMatches( + String pattern, + {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.matches( + property: r'symbol', + wildcard: pattern, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + symbolIsEmpty() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'symbol', + value: '', + )); + }); + } + + QueryBuilder + symbolIsNotEmpty() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + property: r'symbol', + value: '', + )); + }); + } + + QueryBuilder typeEqualTo( + EthContractType value) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'type', + value: value, + )); + }); + } + + QueryBuilder typeGreaterThan( + EthContractType value, { + bool include = false, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + include: include, + property: r'type', + value: value, + )); + }); + } + + QueryBuilder typeLessThan( + EthContractType value, { + bool include = false, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.lessThan( + include: include, + property: r'type', + value: value, + )); + }); + } + + QueryBuilder typeBetween( + EthContractType lower, + EthContractType upper, { + bool includeLower = true, + bool includeUpper = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.between( + property: r'type', + lower: lower, + includeLower: includeLower, + upper: upper, + includeUpper: includeUpper, + )); + }); + } +} + +extension EthContractQueryObject + on QueryBuilder {} + +extension EthContractQueryLinks + on QueryBuilder {} + +extension EthContractQuerySortBy + on QueryBuilder { + QueryBuilder sortByAbi() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'abi', Sort.asc); + }); + } + + QueryBuilder sortByAbiDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'abi', Sort.desc); + }); + } + + QueryBuilder sortByAddress() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'address', Sort.asc); + }); + } + + QueryBuilder sortByAddressDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'address', Sort.desc); + }); + } + + QueryBuilder sortByDecimals() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'decimals', Sort.asc); + }); + } + + QueryBuilder sortByDecimalsDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'decimals', Sort.desc); + }); + } + + QueryBuilder sortByName() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'name', Sort.asc); + }); + } + + QueryBuilder sortByNameDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'name', Sort.desc); + }); + } + + QueryBuilder sortBySymbol() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'symbol', Sort.asc); + }); + } + + QueryBuilder sortBySymbolDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'symbol', Sort.desc); + }); + } + + QueryBuilder sortByType() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'type', Sort.asc); + }); + } + + QueryBuilder sortByTypeDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'type', Sort.desc); + }); + } +} + +extension EthContractQuerySortThenBy + on QueryBuilder { + QueryBuilder thenByAbi() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'abi', Sort.asc); + }); + } + + QueryBuilder thenByAbiDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'abi', Sort.desc); + }); + } + + QueryBuilder thenByAddress() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'address', Sort.asc); + }); + } + + QueryBuilder thenByAddressDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'address', Sort.desc); + }); + } + + QueryBuilder thenByDecimals() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'decimals', Sort.asc); + }); + } + + QueryBuilder thenByDecimalsDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'decimals', Sort.desc); + }); + } + + QueryBuilder thenById() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'id', Sort.asc); + }); + } + + QueryBuilder thenByIdDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'id', Sort.desc); + }); + } + + QueryBuilder thenByName() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'name', Sort.asc); + }); + } + + QueryBuilder thenByNameDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'name', Sort.desc); + }); + } + + QueryBuilder thenBySymbol() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'symbol', Sort.asc); + }); + } + + QueryBuilder thenBySymbolDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'symbol', Sort.desc); + }); + } + + QueryBuilder thenByType() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'type', Sort.asc); + }); + } + + QueryBuilder thenByTypeDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'type', Sort.desc); + }); + } +} + +extension EthContractQueryWhereDistinct + on QueryBuilder { + QueryBuilder distinctByAbi( + {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addDistinctBy(r'abi', caseSensitive: caseSensitive); + }); + } + + QueryBuilder distinctByAddress( + {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addDistinctBy(r'address', caseSensitive: caseSensitive); + }); + } + + QueryBuilder distinctByDecimals() { + return QueryBuilder.apply(this, (query) { + return query.addDistinctBy(r'decimals'); + }); + } + + QueryBuilder distinctByName( + {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addDistinctBy(r'name', caseSensitive: caseSensitive); + }); + } + + QueryBuilder distinctBySymbol( + {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addDistinctBy(r'symbol', caseSensitive: caseSensitive); + }); + } + + QueryBuilder distinctByType() { + return QueryBuilder.apply(this, (query) { + return query.addDistinctBy(r'type'); + }); + } +} + +extension EthContractQueryProperty + on QueryBuilder { + QueryBuilder idProperty() { + return QueryBuilder.apply(this, (query) { + return query.addPropertyName(r'id'); + }); + } + + QueryBuilder abiProperty() { + return QueryBuilder.apply(this, (query) { + return query.addPropertyName(r'abi'); + }); + } + + QueryBuilder addressProperty() { + return QueryBuilder.apply(this, (query) { + return query.addPropertyName(r'address'); + }); + } + + QueryBuilder decimalsProperty() { + return QueryBuilder.apply(this, (query) { + return query.addPropertyName(r'decimals'); + }); + } + + QueryBuilder nameProperty() { + return QueryBuilder.apply(this, (query) { + return query.addPropertyName(r'name'); + }); + } + + QueryBuilder symbolProperty() { + return QueryBuilder.apply(this, (query) { + return query.addPropertyName(r'symbol'); + }); + } + + QueryBuilder typeProperty() { + return QueryBuilder.apply(this, (query) { + return query.addPropertyName(r'type'); + }); + } +} diff --git a/lib/models/isar/models/isar_models.dart b/lib/models/isar/models/isar_models.dart index 4fb7c8b24..6b244ee48 100644 --- a/lib/models/isar/models/isar_models.dart +++ b/lib/models/isar/models/isar_models.dart @@ -4,5 +4,6 @@ export 'blockchain_data/input.dart'; export 'blockchain_data/output.dart'; export 'blockchain_data/transaction.dart'; export 'blockchain_data/utxo.dart'; +export 'ethereum/eth_contract.dart'; export 'log.dart'; export 'transaction_note.dart'; diff --git a/lib/models/paymint/transactions_model.dart b/lib/models/paymint/transactions_model.dart index 1d40ac01c..cee48b8eb 100644 --- a/lib/models/paymint/transactions_model.dart +++ b/lib/models/paymint/transactions_model.dart @@ -96,7 +96,8 @@ class TransactionChunk { .toList(); return TransactionChunk( - timestamp: json['timestamp'] as int, transactions: txList); + timestamp: int.parse(json['timestamp'].toString()), + transactions: txList); } @override @@ -192,13 +193,13 @@ class Transaction { return Transaction( txid: json['txid'] as String, confirmedStatus: json['confirmed_status'] as bool, - timestamp: json['timestamp'] as int, + timestamp: int.parse(json['timestamp'].toString()), txType: json['txType'] as String, amount: json['amount'] as int, aliens: json['aliens'] as List, worthNow: json['worthNow'] as String? ?? "", worthAtBlockTimestamp: json['worthAtBlockTimestamp'] as String? ?? "", - fees: json['fees'] as int, + fees: int.parse(json['fees'].toString()), inputSize: json['inputSize'] as int, outputSize: json['outputSize'] as int, inputs: inputList, diff --git a/lib/models/token_balance.dart b/lib/models/token_balance.dart new file mode 100644 index 000000000..f2606f04d --- /dev/null +++ b/lib/models/token_balance.dart @@ -0,0 +1,62 @@ +import 'dart:convert'; + +import 'package:stackwallet/models/balance.dart'; +import 'package:stackwallet/utilities/amount/amount.dart'; +import 'package:stackwallet/utilities/enums/coin_enum.dart'; + +class TokenBalance extends Balance { + TokenBalance({ + required this.contractAddress, + required super.total, + required super.spendable, + required super.blockedTotal, + required super.pendingSpendable, + super.coin = Coin.ethereum, + }); + + final String contractAddress; + + @override + String toJsonIgnoreCoin() => jsonEncode({ + "contractAddress": contractAddress, + "total": total.toJsonString(), + "spendable": spendable.toJsonString(), + "blockedTotal": blockedTotal.toJsonString(), + "pendingSpendable": pendingSpendable.toJsonString(), + }); + + factory TokenBalance.fromJson( + String json, + int fractionDigits, + ) { + final decoded = jsonDecode(json); + return TokenBalance( + contractAddress: decoded["contractAddress"] as String, + total: decoded["total"] is String + ? Amount.fromSerializedJsonString(decoded["total"] as String) + : Amount( + rawValue: BigInt.from(decoded["total"] as int), + fractionDigits: fractionDigits, + ), + spendable: decoded["spendable"] is String + ? Amount.fromSerializedJsonString(decoded["spendable"] as String) + : Amount( + rawValue: BigInt.from(decoded["spendable"] as int), + fractionDigits: fractionDigits, + ), + blockedTotal: decoded["blockedTotal"] is String + ? Amount.fromSerializedJsonString(decoded["blockedTotal"] as String) + : Amount( + rawValue: BigInt.from(decoded["blockedTotal"] as int), + fractionDigits: fractionDigits, + ), + pendingSpendable: decoded["pendingSpendable"] is String + ? Amount.fromSerializedJsonString( + decoded["pendingSpendable"] as String) + : Amount( + rawValue: BigInt.from(decoded["pendingSpendable"] as int), + fractionDigits: fractionDigits, + ), + ); + } +} diff --git a/lib/models/transaction_filter.dart b/lib/models/transaction_filter.dart index 7ef5f0bff..7ea18aac0 100644 --- a/lib/models/transaction_filter.dart +++ b/lib/models/transaction_filter.dart @@ -1,10 +1,12 @@ +import 'package:stackwallet/utilities/amount/amount.dart'; + class TransactionFilter { final bool sent; final bool received; final bool trade; final DateTime? from; final DateTime? to; - final int? amount; + final Amount? amount; final String keyword; TransactionFilter({ @@ -23,7 +25,7 @@ class TransactionFilter { bool? trade, DateTime? from, DateTime? to, - int? amount, + Amount? amount, String? keyword, }) { return TransactionFilter( diff --git a/lib/pages/add_wallet_views/add_token_view/add_custom_token_view.dart b/lib/pages/add_wallet_views/add_token_view/add_custom_token_view.dart new file mode 100644 index 000000000..76dfceb45 --- /dev/null +++ b/lib/pages/add_wallet_views/add_token_view/add_custom_token_view.dart @@ -0,0 +1,293 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:stackwallet/models/isar/models/ethereum/eth_contract.dart'; +import 'package:stackwallet/services/ethereum/ethereum_api.dart'; +import 'package:stackwallet/utilities/show_loading.dart'; +import 'package:stackwallet/utilities/text_styles.dart'; +import 'package:stackwallet/utilities/theme/stack_colors.dart'; +import 'package:stackwallet/utilities/util.dart'; +import 'package:stackwallet/widgets/background.dart'; +import 'package:stackwallet/widgets/conditional_parent.dart'; +import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; +import 'package:stackwallet/widgets/desktop/desktop_dialog_close_button.dart'; +import 'package:stackwallet/widgets/desktop/primary_button.dart'; +import 'package:stackwallet/widgets/desktop/secondary_button.dart'; +import 'package:stackwallet/widgets/stack_dialog.dart'; + +class AddCustomTokenView extends ConsumerStatefulWidget { + const AddCustomTokenView({ + Key? key, + }) : super(key: key); + + static const routeName = "/addCustomToken"; + + @override + ConsumerState createState() => _AddCustomTokenViewState(); +} + +class _AddCustomTokenViewState extends ConsumerState { + final isDesktop = Util.isDesktop; + + final contractController = TextEditingController(); + final nameController = TextEditingController(); + final symbolController = TextEditingController(); + final decimalsController = TextEditingController(); + + bool enableSubFields = false; + bool addTokenButtonEnabled = false; + + EthContract? currentToken; + + @override + Widget build(BuildContext context) { + return ConditionalParent( + condition: !isDesktop, + builder: (child) => Background( + child: Scaffold( + backgroundColor: + Theme.of(context).extension()!.background, + appBar: AppBar( + leading: AppBarBackButton( + onPressed: () { + Navigator.of(context).pop(); + }, + ), + ), + body: Padding( + padding: const EdgeInsets.only( + top: 10, + left: 16, + right: 16, + bottom: 16, + ), + child: child, + ), + ), + ), + child: ConditionalParent( + condition: isDesktop, + builder: (child) => Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Padding( + padding: const EdgeInsets.only( + left: 32, + ), + child: Text( + "Add custom ETH token", + style: STextStyles.desktopH3(context), + ), + ), + const DesktopDialogCloseButton(), + ], + ), + Flexible( + child: Padding( + padding: const EdgeInsets.only( + left: 32, + right: 32, + bottom: 32, + top: 16, + ), + child: child, + ), + ), + ], + ), + child: Column( + children: [ + if (!isDesktop) + Text( + "Add custom ETH token", + style: STextStyles.pageTitleH1(context), + ), + if (!isDesktop) + const SizedBox( + height: 16, + ), + TextField( + autocorrect: !isDesktop, + enableSuggestions: !isDesktop, + controller: contractController, + style: STextStyles.field(context), + decoration: InputDecoration( + hintText: "Contract address", + hintStyle: STextStyles.fieldLabel(context), + ), + ), + SizedBox( + height: isDesktop ? 16 : 8, + ), + PrimaryButton( + label: "Search", + onPressed: () async { + final response = await showLoading( + whileFuture: EthereumAPI.getTokenContractInfoByAddress( + contractController.text), + context: context, + message: "Looking up contract", + ); + currentToken = response.value; + if (currentToken != null) { + nameController.text = currentToken!.name; + symbolController.text = currentToken!.symbol; + decimalsController.text = currentToken!.decimals.toString(); + } else { + nameController.text = ""; + symbolController.text = ""; + decimalsController.text = ""; + if (mounted) { + unawaited( + showDialog( + context: context, + builder: (context) => StackOkDialog( + title: "Failed to look up token", + message: response.exception?.message, + ), + ), + ); + } + } + setState(() { + addTokenButtonEnabled = currentToken != null; + }); + }, + ), + SizedBox( + height: isDesktop ? 16 : 8, + ), + TextField( + enabled: enableSubFields, + autocorrect: !isDesktop, + enableSuggestions: !isDesktop, + controller: nameController, + style: STextStyles.field(context), + decoration: InputDecoration( + hintText: "Token name", + hintStyle: STextStyles.fieldLabel(context), + ), + ), + SizedBox( + height: isDesktop ? 16 : 8, + ), + if (isDesktop) + Row( + children: [ + Expanded( + child: TextField( + enabled: enableSubFields, + autocorrect: !isDesktop, + enableSuggestions: !isDesktop, + controller: symbolController, + style: STextStyles.field(context), + decoration: InputDecoration( + hintText: "Ticker", + hintStyle: STextStyles.fieldLabel(context), + ), + ), + ), + const SizedBox( + width: 16, + ), + Expanded( + child: TextField( + enabled: enableSubFields, + autocorrect: !isDesktop, + enableSuggestions: !isDesktop, + controller: decimalsController, + style: STextStyles.field(context), + inputFormatters: [ + TextInputFormatter.withFunction((oldValue, newValue) => + RegExp(r'^([0-9]*)$').hasMatch(newValue.text) + ? newValue + : oldValue), + ], + keyboardType: const TextInputType.numberWithOptions( + signed: false, + decimal: false, + ), + decoration: InputDecoration( + hintText: "Decimals", + hintStyle: STextStyles.fieldLabel(context), + ), + ), + ), + ], + ), + if (!isDesktop) + TextField( + enabled: enableSubFields, + autocorrect: !isDesktop, + enableSuggestions: !isDesktop, + controller: symbolController, + style: STextStyles.field(context), + decoration: InputDecoration( + hintText: "Ticker", + hintStyle: STextStyles.fieldLabel(context), + ), + ), + if (!isDesktop) + const SizedBox( + height: 8, + ), + if (!isDesktop) + TextField( + enabled: enableSubFields, + autocorrect: !isDesktop, + enableSuggestions: !isDesktop, + controller: decimalsController, + style: STextStyles.field(context), + inputFormatters: [ + TextInputFormatter.withFunction((oldValue, newValue) => + RegExp(r'^([0-9]*)$').hasMatch(newValue.text) + ? newValue + : oldValue), + ], + keyboardType: const TextInputType.numberWithOptions( + signed: false, + decimal: false, + ), + decoration: InputDecoration( + hintText: "Decimals", + hintStyle: STextStyles.fieldLabel(context), + ), + ), + const SizedBox( + height: 16, + ), + const Spacer(), + Row( + children: [ + if (isDesktop) + Expanded( + child: SecondaryButton( + label: "Cancel", + onPressed: Navigator.of(context).pop, + ), + ), + if (isDesktop) + const SizedBox( + width: 16, + ), + Expanded( + child: PrimaryButton( + label: "Add token", + enabled: addTokenButtonEnabled, + onPressed: () { + Navigator.of(context).pop(currentToken!); + }, + ), + ), + ], + ), + ], + ), + ), + ); + } +} diff --git a/lib/pages/add_wallet_views/add_token_view/edit_wallet_tokens_view.dart b/lib/pages/add_wallet_views/add_token_view/edit_wallet_tokens_view.dart new file mode 100644 index 000000000..936ca5405 --- /dev/null +++ b/lib/pages/add_wallet_views/add_token_view/edit_wallet_tokens_view.dart @@ -0,0 +1,545 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_svg/svg.dart'; +import 'package:isar/isar.dart'; +import 'package:stackwallet/db/isar/main_db.dart'; +import 'package:stackwallet/models/isar/models/ethereum/eth_contract.dart'; +import 'package:stackwallet/notifications/show_flush_bar.dart'; +import 'package:stackwallet/pages/add_wallet_views/add_token_view/add_custom_token_view.dart'; +import 'package:stackwallet/pages/add_wallet_views/add_token_view/sub_widgets/add_token_list.dart'; +import 'package:stackwallet/pages/add_wallet_views/add_token_view/sub_widgets/add_token_list_element.dart'; +import 'package:stackwallet/pages/add_wallet_views/add_token_view/sub_widgets/add_token_text.dart'; +import 'package:stackwallet/pages_desktop_specific/desktop_home_view.dart'; +import 'package:stackwallet/providers/global/wallets_provider.dart'; +import 'package:stackwallet/services/coins/ethereum/ethereum_wallet.dart'; +import 'package:stackwallet/utilities/assets.dart'; +import 'package:stackwallet/utilities/constants.dart'; +import 'package:stackwallet/utilities/default_eth_tokens.dart'; +import 'package:stackwallet/utilities/text_styles.dart'; +import 'package:stackwallet/utilities/theme/stack_colors.dart'; +import 'package:stackwallet/utilities/util.dart'; +import 'package:stackwallet/widgets/background.dart'; +import 'package:stackwallet/widgets/conditional_parent.dart'; +import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; +import 'package:stackwallet/widgets/desktop/desktop_app_bar.dart'; +import 'package:stackwallet/widgets/desktop/desktop_dialog.dart'; +import 'package:stackwallet/widgets/desktop/desktop_dialog_close_button.dart'; +import 'package:stackwallet/widgets/desktop/desktop_scaffold.dart'; +import 'package:stackwallet/widgets/desktop/primary_button.dart'; +import 'package:stackwallet/widgets/desktop/secondary_button.dart'; +import 'package:stackwallet/widgets/icon_widgets/x_icon.dart'; +import 'package:stackwallet/widgets/rounded_white_container.dart'; +import 'package:stackwallet/widgets/stack_text_field.dart'; +import 'package:stackwallet/widgets/textfield_icon_button.dart'; + +class EditWalletTokensView extends ConsumerStatefulWidget { + const EditWalletTokensView({ + Key? key, + required this.walletId, + this.contractsToMarkSelected, + this.isDesktopPopup = false, + }) : super(key: key); + + final String walletId; + final List? contractsToMarkSelected; + final bool isDesktopPopup; + + static const routeName = "/editWalletTokens"; + + @override + ConsumerState createState() => + _EditWalletTokensViewState(); +} + +class _EditWalletTokensViewState extends ConsumerState { + late final TextEditingController _searchFieldController; + late final FocusNode _searchFocusNode; + + String _searchTerm = ""; + + final List tokenEntities = []; + + final bool isDesktop = Util.isDesktop; + + List filter( + String text, + List entities, + ) { + final _entities = [...entities]; + if (text.isNotEmpty) { + final lowercaseTerm = text.toLowerCase(); + _entities.retainWhere( + (e) => + e.token.name.toLowerCase().contains(lowercaseTerm) || + e.token.symbol.toLowerCase().contains(lowercaseTerm) || + e.token.address.toLowerCase().contains(lowercaseTerm), + ); + } + + return _entities; + } + + Future onNextPressed() async { + final selectedTokens = tokenEntities + .where((e) => e.selected) + .map((e) => e.token.address) + .toList(); + + final ethWallet = ref + .read(walletsChangeNotifierProvider) + .getManager(widget.walletId) + .wallet as EthereumWallet; + + await ethWallet.updateTokenContracts(selectedTokens); + if (mounted) { + if (widget.contractsToMarkSelected == null) { + Navigator.of(context).pop(42); + } else { + Navigator.of(context).popUntil( + ModalRoute.withName(DesktopHomeView.routeName), + ); + unawaited( + showFloatingFlushBar( + type: FlushBarType.success, + message: "${ethWallet.walletName} tokens saved", + context: context, + ), + ); + } + } + } + + Future _addToken() async { + EthContract? contract; + + if (isDesktop) { + contract = await showDialog( + context: context, + builder: (context) => const DesktopDialog( + maxWidth: 580, + maxHeight: 500, + child: AddCustomTokenView(), + ), + ); + } else { + contract = await Navigator.of(context).pushNamed( + AddCustomTokenView.routeName, + ); + } + + if (contract != null) { + await MainDB.instance.putEthContract(contract); + if (mounted) { + setState(() { + if (tokenEntities + .where((e) => e.token.address == contract!.address) + .isEmpty) { + tokenEntities + .add(AddTokenListElementData(contract!)..selected = true); + tokenEntities.sort((a, b) => a.token.name.compareTo(b.token.name)); + } + }); + } + } + } + + @override + void initState() { + _searchFieldController = TextEditingController(); + _searchFocusNode = FocusNode(); + + final contracts = + MainDB.instance.getEthContracts().sortByName().findAllSync(); + + if (contracts.isEmpty) { + contracts.addAll(DefaultTokens.list); + MainDB.instance.putEthContracts(contracts); + } + + tokenEntities.addAll(contracts.map((e) => AddTokenListElementData(e))); + + final walletContracts = (ref + .read(walletsChangeNotifierProvider) + .getManager(widget.walletId) + .wallet as EthereumWallet) + .getWalletTokenContractAddresses(); + + final shouldMarkAsSelectedContracts = [ + ...walletContracts, + ...(widget.contractsToMarkSelected ?? []), + ]; + + for (final e in tokenEntities) { + e.selected = shouldMarkAsSelectedContracts.contains(e.token.address); + } + + super.initState(); + } + + @override + void dispose() { + _searchFieldController.dispose(); + _searchFocusNode.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + debugPrint("BUILD: $runtimeType"); + final walletName = ref.watch(walletsChangeNotifierProvider + .select((value) => value.getManager(widget.walletId).walletName)); + + if (isDesktop) { + return ConditionalParent( + condition: !widget.isDesktopPopup, + builder: (child) => DesktopScaffold( + appBar: DesktopAppBar( + isCompactHeight: false, + useSpacers: false, + leading: const AppBarBackButton(), + overlayCenter: Text( + walletName, + style: STextStyles.desktopSubtitleH2(context), + ), + trailing: widget.contractsToMarkSelected == null + ? Padding( + padding: const EdgeInsets.only( + right: 24, + ), + child: SizedBox( + height: 56, + child: TextButton( + style: Theme.of(context) + .extension()! + .getSmallSecondaryEnabledButtonStyle(context), + onPressed: _addToken, + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 30, + ), + child: Text( + "Add custom token", + style: + STextStyles.desktopButtonSmallSecondaryEnabled( + context), + ), + ), + ), + ), + ) + : null, + ), + body: SizedBox( + width: 480, + child: Column( + children: [ + const AddTokenText( + isDesktop: true, + ), + const SizedBox( + height: 16, + ), + Expanded( + child: RoundedWhiteContainer( + radiusMultiplier: 2, + padding: const EdgeInsets.only( + left: 20, + top: 20, + right: 20, + bottom: 0, + ), + child: child, + ), + ), + const SizedBox( + height: 26, + ), + SizedBox( + height: 70, + width: 480, + child: PrimaryButton( + label: widget.contractsToMarkSelected != null + ? "Save" + : "Next", + onPressed: onNextPressed, + ), + ), + const SizedBox( + height: 32, + ), + ], + ), + ), + ), + child: ConditionalParent( + condition: widget.isDesktopPopup, + builder: (child) => DesktopDialog( + maxHeight: 670, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Padding( + padding: const EdgeInsets.only( + left: 32, + ), + child: Text( + "Edit tokens", + style: STextStyles.desktopH3(context), + ), + ), + const DesktopDialogCloseButton(), + ], + ), + Expanded( + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 32, + vertical: 16, + ), + child: child, + ), + ), + Padding( + padding: const EdgeInsets.symmetric( + horizontal: 32, + ), + child: Row( + children: [ + Expanded( + child: SecondaryButton( + label: "Add custom token", + buttonHeight: ButtonHeight.l, + onPressed: _addToken, + ), + ), + const SizedBox( + width: 16, + ), + Expanded( + child: PrimaryButton( + label: "Done", + buttonHeight: ButtonHeight.l, + onPressed: onNextPressed, + ), + ), + ], + ), + ), + const SizedBox( + height: 32, + ), + ], + ), + ), + child: Column( + children: [ + ClipRRect( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + child: TextField( + autocorrect: Util.isDesktop ? false : true, + enableSuggestions: Util.isDesktop ? false : true, + controller: _searchFieldController, + focusNode: _searchFocusNode, + onChanged: (value) { + setState(() { + _searchTerm = value; + }); + }, + style: STextStyles.desktopTextMedium(context).copyWith( + height: 2, + ), + decoration: standardInputDecoration( + "Search", + _searchFocusNode, + context, + ).copyWith( + contentPadding: const EdgeInsets.symmetric( + vertical: 10, + ), + prefixIcon: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 16, + // vertical: 20, + ), + child: SvgPicture.asset( + Assets.svg.search, + width: 24, + height: 24, + color: Theme.of(context) + .extension()! + .textFieldDefaultSearchIconLeft, + ), + ), + suffixIcon: _searchFieldController.text.isNotEmpty + ? Padding( + padding: const EdgeInsets.only(right: 10), + child: UnconstrainedBox( + child: Row( + children: [ + TextFieldIconButton( + child: const XIcon( + width: 24, + height: 24, + ), + onTap: () async { + setState(() { + _searchFieldController.text = ""; + _searchTerm = ""; + }); + }, + ), + ], + ), + ), + ) + : null, + ), + ), + ), + const SizedBox( + height: 12, + ), + Expanded( + child: AddTokenList( + walletId: widget.walletId, + items: filter(_searchTerm, tokenEntities), + addFunction: isDesktop ? null : _addToken, + ), + ), + const SizedBox( + height: 12, + ), + ], + ), + ), + ); + } else { + return Background( + child: Scaffold( + backgroundColor: + Theme.of(context).extension()!.background, + appBar: AppBar( + leading: AppBarBackButton( + onPressed: () { + Navigator.of(context).pop(); + }, + ), + actions: [ + AspectRatio( + aspectRatio: 1, + child: Padding( + padding: const EdgeInsets.all(8.0), + child: AppBarIconButton( + icon: SvgPicture.asset( + Assets.svg.circlePlusFilled, + color: Theme.of(context) + .extension()! + .topNavIconPrimary, + ), + onPressed: _addToken, + ), + ), + ), + ], + ), + body: Container( + color: Theme.of(context).extension()!.background, + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + AddTokenText( + isDesktop: false, + walletName: walletName, + ), + const SizedBox( + height: 16, + ), + ClipRRect( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + child: TextField( + autofocus: isDesktop, + autocorrect: !isDesktop, + enableSuggestions: !isDesktop, + controller: _searchFieldController, + focusNode: _searchFocusNode, + onChanged: (value) => setState(() => _searchTerm = value), + style: STextStyles.field(context), + decoration: standardInputDecoration( + "Search", + _searchFocusNode, + context, + desktopMed: isDesktop, + ).copyWith( + prefixIcon: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 10, + vertical: 16, + ), + child: SvgPicture.asset( + Assets.svg.search, + width: 16, + height: 16, + ), + ), + suffixIcon: _searchFieldController.text.isNotEmpty + ? Padding( + padding: const EdgeInsets.only(right: 0), + child: UnconstrainedBox( + child: Row( + children: [ + TextFieldIconButton( + child: const XIcon(), + onTap: () async { + setState(() { + _searchFieldController.text = ""; + _searchTerm = ""; + }); + }, + ), + ], + ), + ), + ) + : null, + ), + ), + ), + const SizedBox( + height: 10, + ), + Expanded( + child: AddTokenList( + walletId: widget.walletId, + items: filter(_searchTerm, tokenEntities), + addFunction: _addToken, + ), + ), + const SizedBox( + height: 16, + ), + PrimaryButton( + label: widget.contractsToMarkSelected != null + ? "Save" + : "Next", + onPressed: onNextPressed, + ), + ], + ), + ), + ), + ), + ); + } + } +} diff --git a/lib/pages/add_wallet_views/add_token_view/sub_widgets/add_custom_token_selector.dart b/lib/pages/add_wallet_views/add_token_view/sub_widgets/add_custom_token_selector.dart new file mode 100644 index 000000000..d84db9b6c --- /dev/null +++ b/lib/pages/add_wallet_views/add_token_view/sub_widgets/add_custom_token_selector.dart @@ -0,0 +1,60 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:stackwallet/utilities/assets.dart'; +import 'package:stackwallet/utilities/constants.dart'; +import 'package:stackwallet/utilities/text_styles.dart'; +import 'package:stackwallet/utilities/theme/stack_colors.dart'; +import 'package:stackwallet/utilities/util.dart'; + +class AddCustomTokenSelector extends StatelessWidget { + const AddCustomTokenSelector({ + Key? key, + required this.addFunction, + }) : super(key: key); + + final VoidCallback addFunction; + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 4), + child: MaterialButton( + key: const Key("coinSelectItemButtonKey_add_custom"), + padding: Util.isDesktop + ? const EdgeInsets.only(left: 24) + : const EdgeInsets.all(12), + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + ), + onPressed: addFunction, + child: ConstrainedBox( + constraints: BoxConstraints( + minHeight: Util.isDesktop ? 70 : 0, + ), + child: Row( + children: [ + SvgPicture.asset( + Assets.svg.circlePlusFilled, + color: Theme.of(context).extension()!.textDark, + width: 26, + height: 26, + ), + const SizedBox( + width: 12, + ), + Text( + "Add custom token", + style: Util.isDesktop + ? STextStyles.desktopTextMedium(context) + : STextStyles.w600_14(context), + ), + ], + ), + ), + ), + ); + } +} diff --git a/lib/pages/add_wallet_views/add_token_view/sub_widgets/add_token_list.dart b/lib/pages/add_wallet_views/add_token_view/sub_widgets/add_token_list.dart new file mode 100644 index 000000000..a8532f307 --- /dev/null +++ b/lib/pages/add_wallet_views/add_token_view/sub_widgets/add_token_list.dart @@ -0,0 +1,46 @@ +import 'package:flutter/material.dart'; +import 'package:stackwallet/pages/add_wallet_views/add_token_view/sub_widgets/add_custom_token_selector.dart'; +import 'package:stackwallet/pages/add_wallet_views/add_token_view/sub_widgets/add_token_list_element.dart'; +import 'package:stackwallet/widgets/conditional_parent.dart'; + +class AddTokenList extends StatelessWidget { + const AddTokenList({ + Key? key, + required this.walletId, + required this.items, + required this.addFunction, + }) : super(key: key); + + final String walletId; + final List items; + final VoidCallback? addFunction; + + @override + Widget build(BuildContext context) { + return ListView.builder( + shrinkWrap: true, + primary: false, + itemCount: items.length, + itemBuilder: (ctx, index) { + return ConditionalParent( + condition: index == items.length - 1 && addFunction != null, + builder: (child) => Column( + mainAxisSize: MainAxisSize.min, + children: [ + child, + AddCustomTokenSelector( + addFunction: addFunction!, + ), + ], + ), + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 4), + child: AddTokenListElement( + data: items[index], + ), + ), + ); + }, + ); + } +} diff --git a/lib/pages/add_wallet_views/add_token_view/sub_widgets/add_token_list_element.dart b/lib/pages/add_wallet_views/add_token_view/sub_widgets/add_token_list_element.dart new file mode 100644 index 000000000..359ad4900 --- /dev/null +++ b/lib/pages/add_wallet_views/add_token_view/sub_widgets/add_token_list_element.dart @@ -0,0 +1,129 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:isar/isar.dart'; +import 'package:stackwallet/models/isar/exchange_cache/currency.dart'; +import 'package:stackwallet/models/isar/models/ethereum/eth_contract.dart'; +import 'package:stackwallet/services/exchange/change_now/change_now_exchange.dart'; +import 'package:stackwallet/services/exchange/exchange_data_loading_service.dart'; +import 'package:stackwallet/utilities/assets.dart'; +import 'package:stackwallet/utilities/text_styles.dart'; +import 'package:stackwallet/utilities/theme/stack_colors.dart'; +import 'package:stackwallet/utilities/util.dart'; +import 'package:stackwallet/widgets/conditional_parent.dart'; +import 'package:stackwallet/widgets/custom_buttons/draggable_switch_button.dart'; +import 'package:stackwallet/widgets/rounded_white_container.dart'; + +class AddTokenListElementData { + AddTokenListElementData(this.token); + + final EthContract token; + bool selected = false; +} + +class AddTokenListElement extends StatefulWidget { + const AddTokenListElement({Key? key, required this.data}) : super(key: key); + + final AddTokenListElementData data; + + @override + State createState() => _AddTokenListElementState(); +} + +class _AddTokenListElementState extends State { + final bool isDesktop = Util.isDesktop; + + @override + Widget build(BuildContext context) { + final currency = ExchangeDataLoadingService.instance.isar.currencies + .where() + .exchangeNameEqualTo(ChangeNowExchange.exchangeName) + .filter() + .tokenContractEqualTo( + widget.data.token.address, + caseSensitive: false, + ) + .and() + .imageIsNotEmpty() + .findFirstSync(); + + final String mainLabel = widget.data.token.name; + final double iconSize = isDesktop ? 32 : 24; + + return RoundedWhiteContainer( + padding: EdgeInsets.all(isDesktop ? 16 : 12), + borderColor: isDesktop + ? Theme.of(context).extension()!.backgroundAppBar + : null, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + currency != null + ? SvgPicture.network( + currency.image, + width: iconSize, + height: iconSize, + ) + : SvgPicture.asset( + widget.data.token.symbol == "BNB" + ? Assets.svg.bnbIcon + : Assets.svg.ethereum, + width: iconSize, + height: iconSize, + ), + const SizedBox( + width: 12, + ), + ConditionalParent( + condition: isDesktop, + builder: (child) => Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + child, + const SizedBox( + height: 2, + ), + Text( + widget.data.token.symbol, + style: STextStyles.desktopTextExtraExtraSmall(context), + overflow: TextOverflow.ellipsis, + ), + ], + ), + child: Text( + isDesktop + ? mainLabel + : "$mainLabel (${widget.data.token.symbol})", + style: isDesktop + ? STextStyles.desktopTextSmall(context) + : STextStyles.w600_14(context), + overflow: TextOverflow.ellipsis, + ), + ), + ], + ), + const SizedBox( + width: 4, + ), + isDesktop + ? Checkbox( + value: widget.data.selected, + onChanged: (newValue) => + setState(() => widget.data.selected = newValue!), + ) + : SizedBox( + height: 20, + width: 40, + child: DraggableSwitchButton( + isOn: widget.data.selected, + onValueChanged: (newValue) { + widget.data.selected = newValue; + }, + ), + ), + ], + ), + ); + } +} diff --git a/lib/pages/add_wallet_views/add_token_view/sub_widgets/add_token_text.dart b/lib/pages/add_wallet_views/add_token_view/sub_widgets/add_token_text.dart new file mode 100644 index 000000000..115d8be5c --- /dev/null +++ b/lib/pages/add_wallet_views/add_token_view/sub_widgets/add_token_text.dart @@ -0,0 +1,51 @@ +import 'package:flutter/material.dart'; +import 'package:stackwallet/utilities/text_styles.dart'; + +class AddTokenText extends StatelessWidget { + const AddTokenText({ + Key? key, + required this.isDesktop, + this.walletName, + }) : super(key: key); + + final String? walletName; + final bool isDesktop; + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + if (walletName != null) + Text( + walletName!, + textAlign: TextAlign.center, + style: isDesktop + ? STextStyles.sectionLabelMedium12(context) // todo: fixme + : STextStyles.sectionLabelMedium12(context), + ), + if (walletName != null) + const SizedBox( + height: 4, + ), + Text( + "Edit Tokens", + textAlign: TextAlign.center, + style: isDesktop + ? STextStyles.desktopH2(context) + : STextStyles.pageTitleH1(context), + ), + const SizedBox( + height: 8, + ), + Text( + "You can also do it later in your wallet", + textAlign: TextAlign.center, + style: isDesktop + ? STextStyles.desktopSubtitleH2(context) + : STextStyles.subtitle(context), + ), + ], + ); + } +} diff --git a/lib/pages/add_wallet_views/add_wallet_view/add_wallet_view.dart b/lib/pages/add_wallet_views/add_wallet_view/add_wallet_view.dart index a672ef332..2307d0591 100644 --- a/lib/pages/add_wallet_views/add_wallet_view/add_wallet_view.dart +++ b/lib/pages/add_wallet_views/add_wallet_view/add_wallet_view.dart @@ -1,14 +1,24 @@ import 'dart:io'; import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/svg.dart'; +import 'package:isar/isar.dart'; +import 'package:stackwallet/db/isar/main_db.dart'; +import 'package:stackwallet/models/add_wallet_list_entity/add_wallet_list_entity.dart'; +import 'package:stackwallet/models/add_wallet_list_entity/sub_classes/coin_entity.dart'; +import 'package:stackwallet/models/add_wallet_list_entity/sub_classes/eth_token_entity.dart'; +import 'package:stackwallet/models/isar/models/ethereum/eth_contract.dart'; +import 'package:stackwallet/pages/add_wallet_views/add_token_view/add_custom_token_view.dart'; +import 'package:stackwallet/pages/add_wallet_views/add_token_view/sub_widgets/add_custom_token_selector.dart'; import 'package:stackwallet/pages/add_wallet_views/add_wallet_view/sub_widgets/add_wallet_text.dart'; -import 'package:stackwallet/pages/add_wallet_views/add_wallet_view/sub_widgets/mobile_coin_list.dart'; +import 'package:stackwallet/pages/add_wallet_views/add_wallet_view/sub_widgets/expanding_sub_list_item.dart'; import 'package:stackwallet/pages/add_wallet_views/add_wallet_view/sub_widgets/next_button.dart'; -import 'package:stackwallet/pages/add_wallet_views/add_wallet_view/sub_widgets/searchable_coin_list.dart'; import 'package:stackwallet/pages_desktop_specific/my_stack_view/exit_to_my_stack_button.dart'; +import 'package:stackwallet/providers/providers.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/constants.dart'; +import 'package:stackwallet/utilities/default_eth_tokens.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; @@ -16,40 +26,122 @@ import 'package:stackwallet/utilities/util.dart'; import 'package:stackwallet/widgets/background.dart'; import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; import 'package:stackwallet/widgets/desktop/desktop_app_bar.dart'; +import 'package:stackwallet/widgets/desktop/desktop_dialog.dart'; import 'package:stackwallet/widgets/desktop/desktop_scaffold.dart'; +import 'package:stackwallet/widgets/expandable.dart'; import 'package:stackwallet/widgets/icon_widgets/x_icon.dart'; import 'package:stackwallet/widgets/rounded_white_container.dart'; import 'package:stackwallet/widgets/stack_text_field.dart'; import 'package:stackwallet/widgets/textfield_icon_button.dart'; -class AddWalletView extends StatefulWidget { +class AddWalletView extends ConsumerStatefulWidget { const AddWalletView({Key? key}) : super(key: key); static const routeName = "/addWallet"; @override - State createState() => _AddWalletViewState(); + ConsumerState createState() => _AddWalletViewState(); } -class _AddWalletViewState extends State { +class _AddWalletViewState extends ConsumerState { late final TextEditingController _searchFieldController; late final FocusNode _searchFocusNode; String _searchTerm = ""; - final List coins = [...Coin.values]; + final List _coinsTestnet = [ + ...Coin.values.sublist(Coin.values.length - kTestNetCoinCount - 1), + ]; + final List _coins = [ + ...Coin.values.sublist(0, Coin.values.length - kTestNetCoinCount - 1) + ]; + final List coinEntities = []; + final List tokenEntities = []; final bool isDesktop = Util.isDesktop; + List filter( + String text, + List entities, + ) { + final _entities = [...entities]; + if (text.isNotEmpty) { + final lowercaseTerm = text.toLowerCase(); + _entities.retainWhere( + (e) => + e.ticker.toLowerCase().contains(lowercaseTerm) || + e.name.toLowerCase().contains(lowercaseTerm) || + e.coin.name.toLowerCase().contains(lowercaseTerm) || + (e is EthTokenEntity && + e.token.address.toLowerCase().contains(lowercaseTerm)), + ); + } + + return _entities; + } + + Future _addToken() async { + EthContract? contract; + if (isDesktop) { + contract = await showDialog( + context: context, + builder: (context) => const DesktopDialog( + maxWidth: 580, + maxHeight: 500, + child: AddCustomTokenView(), + ), + ); + } else { + contract = await Navigator.of(context).pushNamed( + AddCustomTokenView.routeName, + ); + } + + if (contract != null) { + await MainDB.instance.putEthContract(contract); + if (mounted) { + setState(() { + if (tokenEntities + .where((e) => e.token.address == contract!.address) + .isEmpty) { + tokenEntities.add(EthTokenEntity(contract!)); + tokenEntities.sort((a, b) => a.token.name.compareTo(b.token.name)); + } + }); + } + } + } + @override void initState() { _searchFieldController = TextEditingController(); _searchFocusNode = FocusNode(); - coins.remove(Coin.firoTestNet); + _coinsTestnet.remove(Coin.firoTestNet); if (Platform.isWindows) { - coins.remove(Coin.monero); - coins.remove(Coin.wownero); + _coins.remove(Coin.monero); + _coins.remove(Coin.wownero); } + + coinEntities.addAll(_coins.map((e) => CoinEntity(e))); + + if (ref.read(prefsChangeNotifierProvider).showTestNetCoins) { + coinEntities.addAll(_coinsTestnet.map((e) => CoinEntity(e))); + } + + final contracts = + MainDB.instance.getEthContracts().sortByName().findAllSync(); + + if (contracts.isEmpty) { + contracts.addAll(DefaultTokens.list); + MainDB.instance.putEthContracts(contracts); + } + + tokenEntities.addAll(contracts.map((e) => EthTokenEntity(e))); + + WidgetsBinding.instance.addPostFrameCallback((_) { + ref.refresh(addWalletSelectedEntityStateProvider); + }); + super.initState(); } @@ -162,13 +254,33 @@ class _AddWalletViewState extends State { ), ), ), + const SizedBox( + height: 8, + ), Expanded( - child: SearchableCoinList( - coins: coins, - isDesktop: true, - searchTerm: _searchTerm, + child: SingleChildScrollView( + child: Column( + children: [ + ExpandingSubListItem( + title: "Coins", + entities: filter(_searchTerm, coinEntities), + initialState: ExpandableState.expanded, + ), + ExpandingSubListItem( + title: "Tokens", + entities: filter(_searchTerm, tokenEntities), + initialState: ExpandableState.expanded, + trailing: AddCustomTokenSelector( + addFunction: _addToken, + ), + ), + ], + ), ), ), + const SizedBox( + height: 20, + ), ], ), ), @@ -215,9 +327,77 @@ class _AddWalletViewState extends State { const SizedBox( height: 16, ), + ClipRRect( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + child: TextField( + autofocus: isDesktop, + autocorrect: !isDesktop, + enableSuggestions: !isDesktop, + controller: _searchFieldController, + focusNode: _searchFocusNode, + onChanged: (value) => setState(() => _searchTerm = value), + style: STextStyles.field(context), + decoration: standardInputDecoration( + "Search", + _searchFocusNode, + context, + desktopMed: isDesktop, + ).copyWith( + prefixIcon: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 10, + vertical: 16, + ), + child: SvgPicture.asset( + Assets.svg.search, + width: 16, + height: 16, + ), + ), + suffixIcon: _searchFieldController.text.isNotEmpty + ? Padding( + padding: const EdgeInsets.only(right: 0), + child: UnconstrainedBox( + child: Row( + children: [ + TextFieldIconButton( + child: const XIcon(), + onTap: () async { + setState(() { + _searchFieldController.text = ""; + _searchTerm = ""; + }); + }, + ), + ], + ), + ), + ) + : null, + ), + ), + ), + const SizedBox( + height: 10, + ), Expanded( - child: MobileCoinList( - coins: coins, + child: SingleChildScrollView( + child: Column( + children: [ + ExpandingSubListItem( + title: "Coins", + entities: filter(_searchTerm, coinEntities), + initialState: ExpandableState.expanded, + ), + ExpandingSubListItem( + title: "Tokens", + entities: filter(_searchTerm, tokenEntities), + initialState: ExpandableState.expanded, + ), + ], + ), ), ), const SizedBox( diff --git a/lib/pages/add_wallet_views/add_wallet_view/sub_widgets/add_wallet_entity_list.dart b/lib/pages/add_wallet_views/add_wallet_view/sub_widgets/add_wallet_entity_list.dart new file mode 100644 index 000000000..c503daa88 --- /dev/null +++ b/lib/pages/add_wallet_views/add_wallet_view/sub_widgets/add_wallet_entity_list.dart @@ -0,0 +1,38 @@ +import 'package:flutter/material.dart'; +import 'package:stackwallet/models/add_wallet_list_entity/add_wallet_list_entity.dart'; +import 'package:stackwallet/pages/add_wallet_views/add_wallet_view/sub_widgets/coin_select_item.dart'; + +class AddWalletEntityList extends StatelessWidget { + const AddWalletEntityList({ + Key? key, + required this.entities, + this.trailing, + }) : super(key: key); + + final List entities; + final Widget? trailing; + + @override + Widget build(BuildContext context) { + return ListView.builder( + shrinkWrap: true, + primary: false, + itemCount: trailing != null ? entities.length + 1 : entities.length, + itemBuilder: (ctx, index) { + if (trailing != null && index == entities.length) { + return Padding( + padding: const EdgeInsets.all(4), + child: trailing, + ); + } else { + return Padding( + padding: const EdgeInsets.all(4), + child: CoinSelectItem( + entity: entities[index], + ), + ); + } + }, + ); + } +} diff --git a/lib/pages/add_wallet_views/add_wallet_view/sub_widgets/coin_select_item.dart b/lib/pages/add_wallet_views/add_wallet_view/sub_widgets/coin_select_item.dart index 87ba95e64..4f2a84a9a 100644 --- a/lib/pages/add_wallet_views/add_wallet_view/sub_widgets/coin_select_item.dart +++ b/lib/pages/add_wallet_views/add_wallet_view/sub_widgets/coin_select_item.dart @@ -1,10 +1,15 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/svg.dart'; +import 'package:isar/isar.dart'; +import 'package:stackwallet/models/add_wallet_list_entity/add_wallet_list_entity.dart'; +import 'package:stackwallet/models/add_wallet_list_entity/sub_classes/eth_token_entity.dart'; +import 'package:stackwallet/models/isar/exchange_cache/currency.dart'; import 'package:stackwallet/providers/providers.dart'; +import 'package:stackwallet/services/exchange/change_now/change_now_exchange.dart'; +import 'package:stackwallet/services/exchange/exchange_data_loading_service.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/constants.dart'; -import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; import 'package:stackwallet/utilities/util.dart'; @@ -12,30 +17,44 @@ import 'package:stackwallet/utilities/util.dart'; class CoinSelectItem extends ConsumerWidget { const CoinSelectItem({ Key? key, - required this.coin, + required this.entity, }) : super(key: key); - final Coin coin; + final AddWalletListEntity entity; @override Widget build(BuildContext context, WidgetRef ref) { - debugPrint("BUILD: CoinSelectItem for ${coin.name}"); - final selectedCoin = ref.watch(addWalletSelectedCoinStateProvider); + debugPrint("BUILD: CoinSelectItem for ${entity.name}"); + final selectedEntity = ref.watch(addWalletSelectedEntityStateProvider); final isDesktop = Util.isDesktop; + String? tokenImageUri; + if (entity is EthTokenEntity) { + final currency = ExchangeDataLoadingService.instance.isar.currencies + .where() + .exchangeNameEqualTo(ChangeNowExchange.exchangeName) + .filter() + .tokenContractEqualTo( + (entity as EthTokenEntity).token.address, + caseSensitive: false, + ) + .and() + .imageIsNotEmpty() + .findFirstSync(); + tokenImageUri = currency?.image; + } + return Container( decoration: BoxDecoration( - // color: selectedCoin == coin ? CFColors.selection : CFColors.white, - color: selectedCoin == coin + color: selectedEntity == entity ? Theme.of(context).extension()!.textFieldActiveBG : Theme.of(context).extension()!.popupBG, borderRadius: BorderRadius.circular(Constants.size.circularBorderRadius), ), child: MaterialButton( - // splashColor: Theme.of(context).extension()!.highlight, - key: Key("coinSelectItemButtonKey_${coin.name}"), + key: Key("coinSelectItemButtonKey_${entity.name}${entity.ticker}"), padding: isDesktop ? const EdgeInsets.only(left: 24) : const EdgeInsets.all(12), @@ -50,24 +69,30 @@ class CoinSelectItem extends ConsumerWidget { ), child: Row( children: [ - SvgPicture.asset( - Assets.svg.iconFor(coin: coin), - width: 26, - height: 26, - ), + tokenImageUri != null + ? SvgPicture.network( + tokenImageUri, + width: 26, + height: 26, + ) + : SvgPicture.asset( + Assets.svg.iconFor(coin: entity.coin), + width: 26, + height: 26, + ), SizedBox( width: isDesktop ? 12 : 10, ), Text( - coin.prettyName, + "${entity.name} (${entity.ticker})", style: isDesktop ? STextStyles.desktopTextMedium(context) : STextStyles.subtitle600(context).copyWith( fontSize: 14, ), ), - if (isDesktop && selectedCoin == coin) const Spacer(), - if (isDesktop && selectedCoin == coin) + if (isDesktop && selectedEntity == entity) const Spacer(), + if (isDesktop && selectedEntity == entity) Padding( padding: const EdgeInsets.only( right: 18, @@ -86,8 +111,9 @@ class CoinSelectItem extends ConsumerWidget { ], ), ), - onPressed: () => - ref.read(addWalletSelectedCoinStateProvider.state).state = coin, + onPressed: () { + ref.read(addWalletSelectedEntityStateProvider.state).state = entity; + }, ), ); } diff --git a/lib/pages/add_wallet_views/add_wallet_view/sub_widgets/expanding_sub_list_item.dart b/lib/pages/add_wallet_views/add_wallet_view/sub_widgets/expanding_sub_list_item.dart new file mode 100644 index 000000000..67c748b99 --- /dev/null +++ b/lib/pages/add_wallet_views/add_wallet_view/sub_widgets/expanding_sub_list_item.dart @@ -0,0 +1,123 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_svg/svg.dart'; +import 'package:stackwallet/models/add_wallet_list_entity/add_wallet_list_entity.dart'; +import 'package:stackwallet/pages/add_wallet_views/add_wallet_view/sub_widgets/add_wallet_entity_list.dart'; +import 'package:stackwallet/utilities/assets.dart'; +import 'package:stackwallet/utilities/text_styles.dart'; +import 'package:stackwallet/utilities/theme/stack_colors.dart'; +import 'package:stackwallet/utilities/util.dart'; +import 'package:stackwallet/widgets/animated_widgets/rotate_icon.dart'; +import 'package:stackwallet/widgets/expandable.dart'; + +class ExpandingSubListItem extends StatefulWidget { + const ExpandingSubListItem({ + Key? key, + required this.title, + required this.entities, + this.trailing, + required this.initialState, + double? animationDurationMultiplier, + this.curve = Curves.easeInOutCubicEmphasized, + }) : animationDurationMultiplier = + animationDurationMultiplier ?? entities.length * 0.11, + super(key: key); + + final String title; + final List entities; + final Widget? trailing; + final ExpandableState initialState; + final double animationDurationMultiplier; + final Curve curve; + + @override + State createState() => _ExpandingSubListItemState(); +} + +class _ExpandingSubListItemState extends State { + final isDesktop = Util.isDesktop; + + late final ExpandableController _controller; + late final RotateIconController _rotateIconController; + + @override + void initState() { + _controller = ExpandableController(); + _rotateIconController = RotateIconController(); + WidgetsBinding.instance.addPostFrameCallback((_) { + if (widget.initialState == ExpandableState.expanded) { + _controller.toggle?.call(); + } + }); + super.initState(); + } + + @override + void dispose() { + _controller.toggle = null; + _rotateIconController.forward = null; + _rotateIconController.reverse = null; + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Expandable( + animationDurationMultiplier: widget.animationDurationMultiplier, + curve: widget.curve, + controller: _controller, + onExpandWillChange: (state) { + if (state == ExpandableState.expanded) { + _rotateIconController.forward?.call(); + } else { + _rotateIconController.reverse?.call(); + } + }, + header: Container( + color: Colors.transparent, + child: Padding( + padding: const EdgeInsets.only( + top: 8.0, + bottom: 8.0, + right: 10, + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + widget.title, + style: isDesktop + ? STextStyles.desktopTextMedium(context).copyWith( + color: Theme.of(context) + .extension()! + .textDark3, + ) + : STextStyles.smallMed12(context), + textAlign: TextAlign.left, + ), + RotateIcon( + icon: SvgPicture.asset( + Assets.svg.chevronDown, + width: isDesktop ? 20 : 12, + height: isDesktop ? 10 : 6, + color: Theme.of(context) + .extension()! + .textFieldActiveSearchIconRight, + ), + curve: widget.curve, + animationDurationMultiplier: widget.animationDurationMultiplier, + controller: _rotateIconController, + ), + ], + ), + ), + ), + body: SingleChildScrollView( + primary: false, + child: AddWalletEntityList( + entities: widget.entities, + trailing: widget.trailing, + ), + ), + ); + } +} diff --git a/lib/pages/add_wallet_views/add_wallet_view/sub_widgets/mobile_coin_list.dart b/lib/pages/add_wallet_views/add_wallet_view/sub_widgets/mobile_coin_list.dart deleted file mode 100644 index fd950963c..000000000 --- a/lib/pages/add_wallet_views/add_wallet_view/sub_widgets/mobile_coin_list.dart +++ /dev/null @@ -1,38 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:stackwallet/pages/add_wallet_views/add_wallet_view/sub_widgets/coin_select_item.dart'; -import 'package:stackwallet/providers/global/prefs_provider.dart'; -import 'package:stackwallet/utilities/enums/coin_enum.dart'; - -class MobileCoinList extends StatelessWidget { - const MobileCoinList({ - Key? key, - required this.coins, - }) : super(key: key); - - final List coins; - - @override - Widget build(BuildContext context) { - return Consumer( - builder: (_, ref, __) { - bool showTestNet = ref.watch( - prefsChangeNotifierProvider.select((value) => value.showTestNetCoins), - ); - - return ListView.builder( - itemCount: - showTestNet ? coins.length : coins.length - (kTestNetCoinCount), - itemBuilder: (ctx, index) { - return Padding( - padding: const EdgeInsets.all(4), - child: CoinSelectItem( - coin: coins[index], - ), - ); - }, - ); - }, - ); - } -} diff --git a/lib/pages/add_wallet_views/add_wallet_view/sub_widgets/next_button.dart b/lib/pages/add_wallet_views/add_wallet_view/sub_widgets/next_button.dart index 57216b182..edb325fd9 100644 --- a/lib/pages/add_wallet_views/add_wallet_view/sub_widgets/next_button.dart +++ b/lib/pages/add_wallet_views/add_wallet_view/sub_widgets/next_button.dart @@ -1,6 +1,8 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:stackwallet/models/add_wallet_list_entity/sub_classes/eth_token_entity.dart'; import 'package:stackwallet/pages/add_wallet_views/create_or_restore_wallet_view/create_or_restore_wallet_view.dart'; +import 'package:stackwallet/pages/add_wallet_views/select_wallet_for_token_view.dart'; import 'package:stackwallet/providers/providers.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; @@ -17,7 +19,7 @@ class AddWalletNextButton extends ConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { debugPrint("BUILD: NextButton"); final selectedCoin = - ref.watch(addWalletSelectedCoinStateProvider.state).state; + ref.watch(addWalletSelectedEntityStateProvider.state).state; final enabled = selectedCoin != null; @@ -25,13 +27,17 @@ class AddWalletNextButton extends ConsumerWidget { onPressed: !enabled ? null : () { - final selectedCoin = - ref.read(addWalletSelectedCoinStateProvider.state).state; - - Navigator.of(context).pushNamed( - CreateOrRestoreWalletView.routeName, - arguments: selectedCoin, - ); + if (selectedCoin is EthTokenEntity) { + Navigator.of(context).pushNamed( + SelectWalletForTokenView.routeName, + arguments: selectedCoin, + ); + } else { + Navigator.of(context).pushNamed( + CreateOrRestoreWalletView.routeName, + arguments: selectedCoin, + ); + } }, style: enabled ? Theme.of(context) diff --git a/lib/pages/add_wallet_views/add_wallet_view/sub_widgets/searchable_coin_list.dart b/lib/pages/add_wallet_views/add_wallet_view/sub_widgets/searchable_coin_list.dart deleted file mode 100644 index 935b5f231..000000000 --- a/lib/pages/add_wallet_views/add_wallet_view/sub_widgets/searchable_coin_list.dart +++ /dev/null @@ -1,63 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:stackwallet/pages/add_wallet_views/add_wallet_view/sub_widgets/coin_select_item.dart'; -import 'package:stackwallet/providers/global/prefs_provider.dart'; -import 'package:stackwallet/utilities/enums/coin_enum.dart'; - -class SearchableCoinList extends ConsumerWidget { - const SearchableCoinList({ - Key? key, - required this.coins, - required this.isDesktop, - required this.searchTerm, - }) : super(key: key); - - final List coins; - final bool isDesktop; - final String searchTerm; - - List filterCoins(String text, bool showTestNetCoins) { - final _coins = [...coins]; - if (text.isNotEmpty) { - final lowercaseTerm = text.toLowerCase(); - _coins.retainWhere((e) => - e.ticker.toLowerCase().contains(lowercaseTerm) || - e.prettyName.toLowerCase().contains(lowercaseTerm) || - e.name.toLowerCase().contains(lowercaseTerm)); - } - if (!showTestNetCoins) { - _coins.removeWhere( - (e) => e.name.endsWith("TestNet") || e == Coin.bitcoincashTestnet); - } - // remove firo testnet regardless - _coins.remove(Coin.firoTestNet); - - // Kidgloves for Wownero on desktop - // if(isDesktop) { - // _coins.remove(Coin.wownero); - // } - - return _coins; - } - - @override - Widget build(BuildContext context, WidgetRef ref) { - bool showTestNet = ref.watch( - prefsChangeNotifierProvider.select((value) => value.showTestNetCoins), - ); - - final _coins = filterCoins(searchTerm, showTestNet); - - return ListView.builder( - itemCount: _coins.length, - itemBuilder: (ctx, index) { - return Padding( - padding: const EdgeInsets.all(4), - child: CoinSelectItem( - coin: _coins[index], - ), - ); - }, - ); - } -} diff --git a/lib/pages/add_wallet_views/create_or_restore_wallet_view/create_or_restore_wallet_view.dart b/lib/pages/add_wallet_views/create_or_restore_wallet_view/create_or_restore_wallet_view.dart index 38880f076..802910275 100644 --- a/lib/pages/add_wallet_views/create_or_restore_wallet_view/create_or_restore_wallet_view.dart +++ b/lib/pages/add_wallet_views/create_or_restore_wallet_view/create_or_restore_wallet_view.dart @@ -1,10 +1,10 @@ import 'package:flutter/material.dart'; +import 'package:stackwallet/models/add_wallet_list_entity/add_wallet_list_entity.dart'; import 'package:stackwallet/pages/add_wallet_views/create_or_restore_wallet_view/sub_widgets/coin_image.dart'; import 'package:stackwallet/pages/add_wallet_views/create_or_restore_wallet_view/sub_widgets/create_or_restore_wallet_subtitle.dart'; import 'package:stackwallet/pages/add_wallet_views/create_or_restore_wallet_view/sub_widgets/create_or_restore_wallet_title.dart'; import 'package:stackwallet/pages/add_wallet_views/create_or_restore_wallet_view/sub_widgets/create_wallet_button_group.dart'; import 'package:stackwallet/pages_desktop_specific/my_stack_view/exit_to_my_stack_button.dart'; -import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:stackwallet/utilities/theme/color_theme.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; import 'package:stackwallet/utilities/util.dart'; @@ -16,12 +16,12 @@ import 'package:stackwallet/widgets/desktop/desktop_scaffold.dart'; class CreateOrRestoreWalletView extends StatelessWidget { const CreateOrRestoreWalletView({ Key? key, - required this.coin, + required this.entity, }) : super(key: key); static const routeName = "/createOrRestoreWallet"; - final Coin coin; + final AddWalletListEntity entity; @override Widget build(BuildContext context) { @@ -45,7 +45,7 @@ class CreateOrRestoreWalletView extends StatelessWidget { flex: 10, ), CreateRestoreWalletTitle( - coin: coin, + coin: entity.coin, isDesktop: isDesktop, ), const SizedBox( @@ -61,7 +61,7 @@ class CreateOrRestoreWalletView extends StatelessWidget { height: 32, ), CoinImage( - coin: coin, + coin: entity.coin, width: isDesktop ? 324 : MediaQuery.of(context).size.width / 1.6, height: @@ -71,7 +71,7 @@ class CreateOrRestoreWalletView extends StatelessWidget { height: 32, ), CreateWalletButtonGroup( - coin: coin, + coin: entity.coin, isDesktop: isDesktop, ), const Spacer( @@ -119,7 +119,7 @@ class CreateOrRestoreWalletView extends StatelessWidget { flex: 2, ), CoinImage( - coin: coin, + coin: entity.coin, width: isDesktop ? 324 : MediaQuery.of(context).size.width / 1.6, @@ -131,7 +131,7 @@ class CreateOrRestoreWalletView extends StatelessWidget { flex: 2, ), CreateRestoreWalletTitle( - coin: coin, + coin: entity.coin, isDesktop: isDesktop, ), const SizedBox( @@ -144,7 +144,7 @@ class CreateOrRestoreWalletView extends StatelessWidget { flex: 5, ), CreateWalletButtonGroup( - coin: coin, + coin: entity.coin, isDesktop: isDesktop, ), ], diff --git a/lib/pages/add_wallet_views/restore_wallet_view/restore_wallet_view.dart b/lib/pages/add_wallet_views/restore_wallet_view/restore_wallet_view.dart index 45df3e2fc..ec0d5bc2c 100644 --- a/lib/pages/add_wallet_views/restore_wallet_view/restore_wallet_view.dart +++ b/lib/pages/add_wallet_views/restore_wallet_view/restore_wallet_view.dart @@ -12,10 +12,13 @@ import 'package:flutter_libmonero/wownero/wownero.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:stackwallet/notifications/show_flush_bar.dart'; +import 'package:stackwallet/pages/add_wallet_views/add_token_view/edit_wallet_tokens_view.dart'; import 'package:stackwallet/pages/add_wallet_views/restore_wallet_view/confirm_recovery_dialog.dart'; import 'package:stackwallet/pages/add_wallet_views/restore_wallet_view/sub_widgets/restore_failed_dialog.dart'; import 'package:stackwallet/pages/add_wallet_views/restore_wallet_view/sub_widgets/restore_succeeded_dialog.dart'; import 'package:stackwallet/pages/add_wallet_views/restore_wallet_view/sub_widgets/restoring_dialog.dart'; +import 'package:stackwallet/pages/add_wallet_views/select_wallet_for_token_view.dart'; +import 'package:stackwallet/pages/add_wallet_views/verify_recovery_phrase_view/verify_recovery_phrase_view.dart'; import 'package:stackwallet/pages/home_view/home_view.dart'; import 'package:stackwallet/pages_desktop_specific/desktop_home_view.dart'; import 'package:stackwallet/pages_desktop_specific/my_stack_view/exit_to_my_stack_button.dart'; @@ -310,24 +313,62 @@ class _RestoreWalletViewState extends ConsumerState { .read(walletsChangeNotifierProvider.notifier) .addWallet(walletId: manager.walletId, manager: manager); - if (mounted) { - if (isDesktop) { - Navigator.of(context) - .popUntil(ModalRoute.withName(DesktopHomeView.routeName)); - } else { - unawaited(Navigator.of(context).pushNamedAndRemoveUntil( - HomeView.routeName, (route) => false)); - } + final isCreateSpecialEthWallet = + ref.read(createSpecialEthWalletRoutingFlag); + if (isCreateSpecialEthWallet) { + ref.read(createSpecialEthWalletRoutingFlag.notifier).state = + false; + ref + .read(newEthWalletTriggerTempUntilHiveCompletelyDeleted.state) + .state = + !ref + .read(newEthWalletTriggerTempUntilHiveCompletelyDeleted + .state) + .state; + } + + if (mounted) { + if (isDesktop) { + Navigator.of(context).popUntil( + ModalRoute.withName( + DesktopHomeView.routeName, + ), + ); + } else { + if (isCreateSpecialEthWallet) { + Navigator.of(context).popUntil( + ModalRoute.withName( + SelectWalletForTokenView.routeName, + ), + ); + } else { + unawaited( + Navigator.of(context).pushNamedAndRemoveUntil( + HomeView.routeName, + (route) => false, + ), + ); + if (manager.coin == Coin.ethereum) { + unawaited( + Navigator.of(context).pushNamed( + EditWalletTokensView.routeName, + arguments: manager.walletId, + ), + ); + } + } + } + + await showDialog( + context: context, + useSafeArea: false, + barrierDismissible: true, + builder: (context) { + return const RestoreSucceededDialog(); + }, + ); } - await showDialog( - context: context, - useSafeArea: false, - barrierDismissible: true, - builder: (context) { - return const RestoreSucceededDialog(); - }, - ); if (!Platform.isLinux && !isDesktop) { await Wakelock.disable(); } diff --git a/lib/pages/add_wallet_views/select_wallet_for_token_view.dart b/lib/pages/add_wallet_views/select_wallet_for_token_view.dart new file mode 100644 index 000000000..fbb23c2ef --- /dev/null +++ b/lib/pages/add_wallet_views/select_wallet_for_token_view.dart @@ -0,0 +1,286 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:stackwallet/db/hive/db.dart'; +import 'package:stackwallet/models/add_wallet_list_entity/sub_classes/coin_entity.dart'; +import 'package:stackwallet/models/add_wallet_list_entity/sub_classes/eth_token_entity.dart'; +import 'package:stackwallet/pages/add_wallet_views/add_token_view/edit_wallet_tokens_view.dart'; +import 'package:stackwallet/pages/add_wallet_views/create_or_restore_wallet_view/create_or_restore_wallet_view.dart'; +import 'package:stackwallet/pages/add_wallet_views/verify_recovery_phrase_view/verify_recovery_phrase_view.dart'; +import 'package:stackwallet/providers/global/wallets_service_provider.dart'; +import 'package:stackwallet/services/wallets_service.dart'; +import 'package:stackwallet/utilities/text_styles.dart'; +import 'package:stackwallet/utilities/theme/stack_colors.dart'; +import 'package:stackwallet/utilities/util.dart'; +import 'package:stackwallet/widgets/background.dart'; +import 'package:stackwallet/widgets/conditional_parent.dart'; +import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; +import 'package:stackwallet/widgets/desktop/desktop_app_bar.dart'; +import 'package:stackwallet/widgets/desktop/desktop_scaffold.dart'; +import 'package:stackwallet/widgets/desktop/primary_button.dart'; +import 'package:stackwallet/widgets/eth_wallet_radio.dart'; +import 'package:stackwallet/widgets/rounded_container.dart'; +import 'package:stackwallet/widgets/rounded_white_container.dart'; +import 'package:stackwallet/widgets/wallet_info_row/wallet_info_row.dart'; +import 'package:tuple/tuple.dart'; + +final newEthWalletTriggerTempUntilHiveCompletelyDeleted = + StateProvider((ref) => false); + +class SelectWalletForTokenView extends ConsumerStatefulWidget { + const SelectWalletForTokenView({ + Key? key, + required this.entity, + }) : super(key: key); + + static const String routeName = "/selectWalletForTokenView"; + + final EthTokenEntity entity; + + @override + ConsumerState createState() => + _SelectWalletForTokenViewState(); +} + +class _SelectWalletForTokenViewState + extends ConsumerState { + final isDesktop = Util.isDesktop; + late final List ethWalletIds; + bool _hasEthWallets = false; + + String? _selectedWalletId; + + void _onContinue() { + Navigator.of(context).pushNamed( + EditWalletTokensView.routeName, + arguments: Tuple2( + _selectedWalletId!, + [widget.entity.token.address], + ), + ); + } + + void _onAddNewEthWallet() { + ref.read(createSpecialEthWalletRoutingFlag.notifier).state = true; + Navigator.of(context).pushNamed( + CreateOrRestoreWalletView.routeName, + arguments: CoinEntity(widget.entity.coin), + ); + } + + late int _cachedWalletCount; + + void _updateWalletsList(Map walletsData) { + _cachedWalletCount = walletsData.length; + + walletsData.removeWhere((key, value) => value.coin != widget.entity.coin); + ethWalletIds.clear(); + + _hasEthWallets = walletsData.isNotEmpty; + + // TODO: proper wallet data class instead of this Hive silliness + for (final walletId in walletsData.values.map((e) => e.walletId).toList()) { + final walletContracts = DB.instance.get( + boxName: walletId, + key: DBKeys.ethTokenContracts, + ) as List? ?? + []; + if (!walletContracts.contains(widget.entity.token.address)) { + ethWalletIds.add(walletId); + } + } + } + + @override + void initState() { + ethWalletIds = []; + + final walletsData = + ref.read(walletsServiceChangeNotifierProvider).fetchWalletsData(); + _updateWalletsList(walletsData); + + super.initState(); + } + + @override + Widget build(BuildContext context) { + // dumb hack + ref.watch(newEthWalletTriggerTempUntilHiveCompletelyDeleted); + final walletsData = + ref.read(walletsServiceChangeNotifierProvider).fetchWalletsData(); + if (walletsData.length != _cachedWalletCount) { + _updateWalletsList(walletsData); + } + + return WillPopScope( + onWillPop: () async { + ref.read(createSpecialEthWalletRoutingFlag.notifier).state = false; + return true; + }, + child: ConditionalParent( + condition: !isDesktop, + builder: (child) => Background( + child: Scaffold( + backgroundColor: + Theme.of(context).extension()!.background, + appBar: AppBar( + leading: AppBarBackButton( + onPressed: () { + Navigator.of(context).pop(); + }, + ), + ), + body: SafeArea( + child: Padding( + padding: const EdgeInsets.all(16), + child: child, + ), + + // child: LayoutBuilder( + // builder: (ctx, constraints) { + // return SingleChildScrollView( + // child: ConstrainedBox( + // constraints: + // BoxConstraints(minHeight: constraints.maxHeight), + // child: IntrinsicHeight( + // child: child, + // ), + // ), + // ); + // }, + // ), + ), + ), + ), + child: ConditionalParent( + condition: isDesktop, + builder: (child) => DesktopScaffold( + appBar: const DesktopAppBar( + isCompactHeight: false, + leading: AppBarBackButton(), + ), + body: SizedBox( + width: 500, + child: child, + ), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + if (isDesktop) + const SizedBox( + height: 24, + ), + Text( + "Select Ethereum wallet", + textAlign: TextAlign.center, + style: isDesktop + ? STextStyles.desktopH2(context) + : STextStyles.pageTitleH1(context), + ), + SizedBox( + height: isDesktop ? 16 : 8, + ), + Text( + "You are adding an ETH token.", + textAlign: TextAlign.center, + style: isDesktop + ? STextStyles.desktopSubtitleH2(context) + : STextStyles.subtitle(context), + ), + const SizedBox( + height: 8, + ), + Text( + "You must choose an Ethereum wallet in order to use ${widget.entity.name}", + textAlign: TextAlign.center, + style: isDesktop + ? STextStyles.desktopSubtitleH2(context) + : STextStyles.subtitle(context), + ), + SizedBox( + height: isDesktop ? 60 : 16, + ), + ethWalletIds.isEmpty + ? RoundedWhiteContainer( + padding: EdgeInsets.all(isDesktop ? 16 : 12), + child: Text( + _hasEthWallets + ? "All current Ethereum wallets already have ${widget.entity.name}" + : "You do not have any Ethereum wallets", + style: isDesktop + ? STextStyles.desktopSubtitleH2(context) + : STextStyles.label(context), + ), + ) + : ConditionalParent( + condition: !isDesktop, + builder: (child) => Expanded( + child: Column( + children: [ + RoundedWhiteContainer( + padding: const EdgeInsets.all(8), + child: child, + ), + ], + ), + ), + child: ListView.separated( + itemCount: ethWalletIds.length, + shrinkWrap: true, + separatorBuilder: (_, __) => SizedBox( + height: isDesktop ? 12 : 6, + ), + itemBuilder: (_, index) { + return RoundedContainer( + padding: EdgeInsets.all(isDesktop ? 16 : 8), + onPressed: () { + setState(() { + _selectedWalletId = ethWalletIds[index]; + }); + }, + color: isDesktop + ? Theme.of(context) + .extension()! + .popupBG + : _selectedWalletId == ethWalletIds[index] + ? Theme.of(context) + .extension()! + .highlight + : Colors.transparent, + child: isDesktop + ? EthWalletRadio( + walletId: ethWalletIds[index], + selectedWalletId: _selectedWalletId, + ) + : WalletInfoRow( + walletId: ethWalletIds[index], + ), + ); + }, + ), + ), + if (ethWalletIds.isEmpty || isDesktop) + const SizedBox( + height: 16, + ), + if (isDesktop) + const SizedBox( + height: 16, + ), + ethWalletIds.isEmpty + ? PrimaryButton( + label: "Add new Ethereum wallet", + onPressed: _onAddNewEthWallet, + ) + : PrimaryButton( + label: "Continue", + enabled: _selectedWalletId != null, + onPressed: _onContinue, + ), + ], + ), + ), + ), + ); + } +} diff --git a/lib/pages/add_wallet_views/verify_recovery_phrase_view/verify_recovery_phrase_view.dart b/lib/pages/add_wallet_views/verify_recovery_phrase_view/verify_recovery_phrase_view.dart index 643783369..33d3628d6 100644 --- a/lib/pages/add_wallet_views/verify_recovery_phrase_view/verify_recovery_phrase_view.dart +++ b/lib/pages/add_wallet_views/verify_recovery_phrase_view/verify_recovery_phrase_view.dart @@ -5,7 +5,9 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:stackwallet/notifications/show_flush_bar.dart'; +import 'package:stackwallet/pages/add_wallet_views/add_token_view/edit_wallet_tokens_view.dart'; import 'package:stackwallet/pages/add_wallet_views/new_wallet_recovery_phrase_view/new_wallet_recovery_phrase_view.dart'; +import 'package:stackwallet/pages/add_wallet_views/select_wallet_for_token_view.dart'; import 'package:stackwallet/pages/add_wallet_views/verify_recovery_phrase_view/sub_widgets/word_table.dart'; import 'package:stackwallet/pages/home_view/home_view.dart'; import 'package:stackwallet/pages_desktop_specific/desktop_home_view.dart'; @@ -14,6 +16,7 @@ import 'package:stackwallet/providers/providers.dart'; import 'package:stackwallet/services/coins/manager.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/constants.dart'; +import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; import 'package:stackwallet/utilities/util.dart'; @@ -22,6 +25,8 @@ import 'package:stackwallet/widgets/desktop/desktop_app_bar.dart'; import 'package:stackwallet/widgets/desktop/desktop_scaffold.dart'; import 'package:tuple/tuple.dart'; +final createSpecialEthWalletRoutingFlag = StateProvider((ref) => false); + class VerifyRecoveryPhraseView extends ConsumerStatefulWidget { const VerifyRecoveryPhraseView({ Key? key, @@ -93,29 +98,75 @@ class _VerifyRecoveryPhraseViewState .read(walletsChangeNotifierProvider.notifier) .addWallet(walletId: _manager.walletId, manager: _manager); - if (mounted) { - if (isDesktop) { - Navigator.of(context).popUntil( - ModalRoute.withName( - DesktopHomeView.routeName, - ), - ); - } else { - unawaited( - Navigator.of(context).pushNamedAndRemoveUntil( - HomeView.routeName, - (route) => false, - ), - ); - } + final isCreateSpecialEthWallet = + ref.read(createSpecialEthWalletRoutingFlag); + if (isCreateSpecialEthWallet) { + ref.read(createSpecialEthWalletRoutingFlag.notifier).state = false; + ref + .read(newEthWalletTriggerTempUntilHiveCompletelyDeleted.state) + .state = + !ref + .read(newEthWalletTriggerTempUntilHiveCompletelyDeleted.state) + .state; } - unawaited(showFloatingFlushBar( - type: FlushBarType.success, - message: "Correct! Your wallet is set up.", - iconAsset: Assets.svg.check, - context: context, - )); + if (mounted) { + if (isDesktop) { + if (isCreateSpecialEthWallet) { + Navigator.of(context).popUntil( + ModalRoute.withName( + SelectWalletForTokenView.routeName, + ), + ); + } else { + Navigator.of(context).popUntil( + ModalRoute.withName( + DesktopHomeView.routeName, + ), + ); + if (widget.manager.coin == Coin.ethereum) { + unawaited( + Navigator.of(context).pushNamed( + EditWalletTokensView.routeName, + arguments: widget.manager.walletId, + ), + ); + } + } + } else { + if (isCreateSpecialEthWallet) { + Navigator.of(context).popUntil( + ModalRoute.withName( + SelectWalletForTokenView.routeName, + ), + ); + } else { + unawaited( + Navigator.of(context).pushNamedAndRemoveUntil( + HomeView.routeName, + (route) => false, + ), + ); + if (widget.manager.coin == Coin.ethereum) { + unawaited( + Navigator.of(context).pushNamed( + EditWalletTokensView.routeName, + arguments: widget.manager.walletId, + ), + ); + } + } + } + + unawaited( + showFloatingFlushBar( + type: FlushBarType.success, + message: "Correct! Your wallet is set up.", + iconAsset: Assets.svg.check, + context: context, + ), + ); + } } else { unawaited(showFloatingFlushBar( type: FlushBarType.warning, diff --git a/lib/pages/address_book_views/subviews/contact_details_view.dart b/lib/pages/address_book_views/subviews/contact_details_view.dart index 4f417c9a5..b1ed4f2da 100644 --- a/lib/pages/address_book_views/subviews/contact_details_view.dart +++ b/lib/pages/address_book_views/subviews/contact_details_view.dart @@ -27,7 +27,7 @@ import 'package:stackwallet/widgets/stack_dialog.dart'; import 'package:stackwallet/widgets/transaction_card.dart'; import 'package:tuple/tuple.dart'; -import '../../../db/main_db.dart'; +import '../../../db/isar/main_db.dart'; class ContactDetailsView extends ConsumerStatefulWidget { const ContactDetailsView({ diff --git a/lib/pages/buy_view/buy_form.dart b/lib/pages/buy_view/buy_form.dart index feea9579d..bd77af223 100644 --- a/lib/pages/buy_view/buy_form.dart +++ b/lib/pages/buy_view/buy_form.dart @@ -10,6 +10,7 @@ import 'package:stackwallet/models/buy/response_objects/crypto.dart'; import 'package:stackwallet/models/buy/response_objects/fiat.dart'; import 'package:stackwallet/models/buy/response_objects/quote.dart'; import 'package:stackwallet/models/contact_address_entry.dart'; +import 'package:stackwallet/models/isar/models/ethereum/eth_contract.dart'; import 'package:stackwallet/pages/address_book_views/address_book_view.dart'; import 'package:stackwallet/pages/buy_view/buy_quote_preview.dart'; import 'package:stackwallet/pages/buy_view/sub_widgets/crypto_selection_view.dart'; @@ -49,6 +50,7 @@ class BuyForm extends ConsumerStatefulWidget { const BuyForm({ Key? key, this.coin, + this.tokenContract, this.clipboard = const ClipboardWrapper(), this.scanner = const BarcodeScannerWrapper(), }) : super(key: key); @@ -57,6 +59,7 @@ class BuyForm extends ConsumerStatefulWidget { final ClipboardInterface clipboard; final BarcodeScannerInterface scanner; + final EthContract? tokenContract; @override ConsumerState createState() => _BuyFormState(); @@ -102,11 +105,11 @@ class _BuyFormState extends ConsumerState { static Decimal minFiat = Decimal.fromInt(50); static Decimal maxFiat = Decimal.fromInt(20000); - // We can't get crypto min and max without asking for a quote - static Decimal minCrypto = Decimal.parse((0.00000001) - .toString()); // lol how to go from double->Decimal more easily? - static Decimal maxCrypto = Decimal.parse((10000.00000000).toString()); - static String boundedCryptoTicker = ''; + // // We can't get crypto min and max without asking for a quote + // static Decimal minCrypto = Decimal.parse((0.00000001) + // .toString()); // lol how to go from double->Decimal more easily? + // static Decimal maxCrypto = Decimal.parse((10000.00000000).toString()); + // static String boundedCryptoTicker = ''; String _amountOutOfRangeErrorString = ""; void validateAmount() { @@ -165,13 +168,13 @@ class _BuyFormState extends ConsumerState { coins: ref.read(simplexProvider).supportedCryptos, onSelected: (crypto) { setState(() { - if (selectedCrypto?.ticker != _BuyFormState.boundedCryptoTicker) { - // Reset crypto mins and maxes ... we don't know these bounds until we request a quote - _BuyFormState.minCrypto = Decimal.parse((0.00000001) - .toString()); // lol how to go from double->Decimal more easily? - _BuyFormState.maxCrypto = - Decimal.parse((10000.00000000).toString()); - } + // if (selectedCrypto?.ticker != _BuyFormState.boundedCryptoTicker) { + // // Reset crypto mins and maxes ... we don't know these bounds until we request a quote + // _BuyFormState.minCrypto = Decimal.parse((0.00000001) + // .toString()); // lol how to go from double->Decimal more easily? + // _BuyFormState.maxCrypto = + // Decimal.parse((10000.00000000).toString()); + // } selectedCrypto = crypto; }); }, @@ -461,7 +464,7 @@ class _BuyFormState extends ConsumerState { // TODO launch URL }, ); - } else { + } else if (mounted) { await showDialog( context: context, barrierDismissible: true, @@ -529,7 +532,7 @@ class _BuyFormState extends ConsumerState { }, ); } - } else { + } else if (mounted) { // Error; probably amount out of bounds // String errorMessage = "${quoteResponse.exception?.errorMessage}"; // if (errorMessage.contains('must be between')) { @@ -744,6 +747,18 @@ class _BuyFormState extends ConsumerState { 'name': widget.coin?.prettyName ?? 'Bitcoin' }); + // THIS IS BAD. No way to be certain the simplex ticker points to the same + // contract as the ticker symbol of this contract + // if (widget.tokenContract != null && + // DefaultTokens.list + // .where((e) => e.address == widget.tokenContract!.address) + // .isNotEmpty) { + // selectedCrypto = Crypto.fromJson({ + // 'ticker': widget.tokenContract!.symbol, + // 'name': widget.tokenContract!.name, + // }); + // } + // TODO set initial crypto to open wallet if a wallet is open super.initState(); diff --git a/lib/pages/buy_view/buy_in_wallet_view.dart b/lib/pages/buy_view/buy_in_wallet_view.dart index 09cbb6857..e46446a7e 100644 --- a/lib/pages/buy_view/buy_in_wallet_view.dart +++ b/lib/pages/buy_view/buy_in_wallet_view.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:stackwallet/models/isar/models/ethereum/eth_contract.dart'; import 'package:stackwallet/pages/buy_view/buy_view.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:stackwallet/utilities/text_styles.dart'; @@ -10,11 +11,13 @@ class BuyInWalletView extends StatefulWidget { const BuyInWalletView({ Key? key, required this.coin, + this.contract, }) : super(key: key); static const String routeName = "/stackBuyInWalletView"; final Coin? coin; + final EthContract? contract; @override State createState() => _BuyInWalletViewState(); @@ -41,7 +44,10 @@ class _BuyInWalletViewState extends State { style: STextStyles.navBarTitle(context), ), ), - body: BuyView(coin: widget.coin), + body: BuyView( + coin: widget.coin, + tokenContract: widget.contract, + ), ), ); } diff --git a/lib/pages/buy_view/buy_view.dart b/lib/pages/buy_view/buy_view.dart index dca907b59..4136bc5e1 100644 --- a/lib/pages/buy_view/buy_view.dart +++ b/lib/pages/buy_view/buy_view.dart @@ -1,23 +1,19 @@ import 'package:flutter/material.dart'; +import 'package:stackwallet/models/isar/models/ethereum/eth_contract.dart'; import 'package:stackwallet/pages/buy_view/buy_form.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; -class BuyView extends StatefulWidget { +class BuyView extends StatelessWidget { const BuyView({ Key? key, this.coin, + this.tokenContract, }) : super(key: key); static const String routeName = "/stackBuyView"; final Coin? coin; - - @override - State createState() => _BuyViewState(); -} - -class _BuyViewState extends State { - late final Coin? coin; + final EthContract? tokenContract; @override Widget build(BuildContext context) { @@ -30,7 +26,10 @@ class _BuyViewState extends State { right: 16, top: 16, ), - child: BuyForm(coin: widget.coin), + child: BuyForm( + coin: coin, + tokenContract: tokenContract, + ), ), ); } diff --git a/lib/pages/coin_control/coin_control_view.dart b/lib/pages/coin_control/coin_control_view.dart index 0eeff1089..d1228f9c7 100644 --- a/lib/pages/coin_control/coin_control_view.dart +++ b/lib/pages/coin_control/coin_control_view.dart @@ -4,18 +4,20 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:isar/isar.dart'; -import 'package:stackwallet/db/main_db.dart'; +import 'package:stackwallet/db/isar/main_db.dart'; import 'package:stackwallet/models/isar/models/isar_models.dart'; import 'package:stackwallet/pages/coin_control/utxo_card.dart'; import 'package:stackwallet/pages/coin_control/utxo_details_view.dart'; +import 'package:stackwallet/providers/global/locale_provider.dart'; import 'package:stackwallet/providers/global/wallets_provider.dart'; import 'package:stackwallet/services/mixins/coin_control_interface.dart'; +import 'package:stackwallet/utilities/amount/amount.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; -import 'package:stackwallet/utilities/format.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; +import 'package:stackwallet/widgets/animated_widgets/rotate_icon.dart'; import 'package:stackwallet/widgets/app_bar_field.dart'; import 'package:stackwallet/widgets/background.dart'; import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; @@ -24,13 +26,11 @@ import 'package:stackwallet/widgets/desktop/primary_button.dart'; import 'package:stackwallet/widgets/desktop/secondary_button.dart'; import 'package:stackwallet/widgets/expandable2.dart'; import 'package:stackwallet/widgets/icon_widgets/x_icon.dart'; +import 'package:stackwallet/widgets/rounded_container.dart'; import 'package:stackwallet/widgets/rounded_white_container.dart'; import 'package:stackwallet/widgets/toggle.dart'; import 'package:tuple/tuple.dart'; -import '../../widgets/animated_widgets/rotate_icon.dart'; -import '../../widgets/rounded_container.dart'; - enum CoinControlViewType { manage, use; @@ -683,12 +683,14 @@ class _CoinControlViewState extends ConsumerState { value += element, ); return Text( - "${Format.satoshisToAmount( - selectedSum, - coin: coin, - ).toStringAsFixed( - coin.decimals, - )} ${coin.ticker}", + "${selectedSum.toAmountAsRaw(fractionDigits: coin.decimals).localizedStringAsFixed( + locale: ref.watch( + localeServiceChangeNotifierProvider + .select( + (value) => value.locale, + ), + ), + )} ${coin.ticker}", style: widget.requestedTotal == null ? STextStyles.w600_14(context) : STextStyles.w600_14(context).copyWith( @@ -729,12 +731,14 @@ class _CoinControlViewState extends ConsumerState { style: STextStyles.w600_14(context), ), Text( - "${Format.satoshisToAmount( - widget.requestedTotal!, - coin: coin, - ).toStringAsFixed( - coin.decimals, - )} ${coin.ticker}", + "${widget.requestedTotal!.toAmountAsRaw(fractionDigits: coin.decimals).localizedStringAsFixed( + locale: ref.watch( + localeServiceChangeNotifierProvider + .select( + (value) => value.locale, + ), + ), + )} ${coin.ticker}", style: STextStyles.w600_14(context), ), ], diff --git a/lib/pages/coin_control/utxo_card.dart b/lib/pages/coin_control/utxo_card.dart index e2900a48b..d0cba1f39 100644 --- a/lib/pages/coin_control/utxo_card.dart +++ b/lib/pages/coin_control/utxo_card.dart @@ -1,11 +1,12 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:stackwallet/db/main_db.dart'; +import 'package:stackwallet/db/isar/main_db.dart'; import 'package:stackwallet/models/isar/models/isar_models.dart'; +import 'package:stackwallet/providers/global/locale_provider.dart'; import 'package:stackwallet/providers/global/wallets_provider.dart'; +import 'package:stackwallet/utilities/amount/amount.dart'; import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; -import 'package:stackwallet/utilities/format.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; import 'package:stackwallet/widgets/conditional_parent.dart'; @@ -123,10 +124,13 @@ class _UtxoCardState extends ConsumerState { mainAxisSize: MainAxisSize.min, children: [ Text( - "${Format.satoshisToAmount( - utxo.value, - coin: coin, - ).toStringAsFixed(coin.decimals)} ${coin.ticker}", + "${utxo.value.toAmountAsRaw(fractionDigits: coin.decimals).localizedStringAsFixed( + locale: ref.watch( + localeServiceChangeNotifierProvider.select( + (value) => value.locale, + ), + ), + )}} ${coin.ticker}", style: STextStyles.w600_14(context), ), const SizedBox( diff --git a/lib/pages/coin_control/utxo_details_view.dart b/lib/pages/coin_control/utxo_details_view.dart index ca7236862..f74a2c4b1 100644 --- a/lib/pages/coin_control/utxo_details_view.dart +++ b/lib/pages/coin_control/utxo_details_view.dart @@ -3,12 +3,13 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:isar/isar.dart'; -import 'package:stackwallet/db/main_db.dart'; +import 'package:stackwallet/db/isar/main_db.dart'; import 'package:stackwallet/models/isar/models/isar_models.dart'; import 'package:stackwallet/pages/wallet_view/transaction_views/transaction_details_view.dart'; +import 'package:stackwallet/providers/global/locale_provider.dart'; import 'package:stackwallet/providers/global/wallets_provider.dart'; +import 'package:stackwallet/utilities/amount/amount.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; -import 'package:stackwallet/utilities/format.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; import 'package:stackwallet/utilities/util.dart'; @@ -239,12 +240,13 @@ class _UtxoDetailsViewState extends ConsumerState { width: 16, ), Text( - "${Format.satoshisToAmount( - utxo!.value, - coin: coin, - ).toStringAsFixed( - coin.decimals, - )} ${coin.ticker}", + "${utxo!.value.toAmountAsRaw(fractionDigits: coin.decimals).localizedStringAsFixed( + locale: ref.watch( + localeServiceChangeNotifierProvider.select( + (value) => value.locale, + ), + ), + )} ${coin.ticker}", style: STextStyles.pageTitleH2(context), ), ], diff --git a/lib/pages/exchange_view/choose_from_stack_view.dart b/lib/pages/exchange_view/choose_from_stack_view.dart index 7c7669430..505554a7c 100644 --- a/lib/pages/exchange_view/choose_from_stack_view.dart +++ b/lib/pages/exchange_view/choose_from_stack_view.dart @@ -112,7 +112,7 @@ class _ChooseFromStackViewState extends ConsumerState { const SizedBox( height: 2, ), - WalletInfoRowBalanceFuture( + WalletInfoRowBalance( walletId: walletIds[index], ), ], diff --git a/lib/pages/exchange_view/confirm_change_now_send.dart b/lib/pages/exchange_view/confirm_change_now_send.dart index 23712f0fd..5d9ed25c7 100644 --- a/lib/pages/exchange_view/confirm_change_now_send.dart +++ b/lib/pages/exchange_view/confirm_change_now_send.dart @@ -11,9 +11,9 @@ import 'package:stackwallet/pages_desktop_specific/my_stack_view/wallet_view/sub import 'package:stackwallet/providers/providers.dart'; import 'package:stackwallet/route_generator.dart'; import 'package:stackwallet/services/coins/firo/firo_wallet.dart'; +import 'package:stackwallet/utilities/amount/amount.dart'; import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; -import 'package:stackwallet/utilities/format.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; import 'package:stackwallet/utilities/util.dart'; @@ -359,14 +359,10 @@ class _ConfirmChangeNowSendViewState mainAxisAlignment: MainAxisAlignment.end, children: [ Text( - "${Format.satoshiAmountToPrettyString( - (transactionInfo["fee"] as int), - ref.watch( - localeServiceChangeNotifierProvider - .select((value) => value.locale), - ), - ref.watch( - managerProvider.select((value) => value.coin), + "${(transactionInfo["fee"] as int).toAmountAsRaw( + fractionDigits: ref.watch( + managerProvider + .select((value) => value.coin.decimals), ), )} ${ref.watch( managerProvider.select((value) => value.coin), @@ -400,26 +396,37 @@ class _ConfirmChangeNowSendViewState .textConfirmTotalAmount, ), ), - Text( - "${Format.satoshiAmountToPrettyString( - (transactionInfo["fee"] as int) + - (transactionInfo["recipientAmt"] as int), - ref.watch( - localeServiceChangeNotifierProvider - .select((value) => value.locale), - ), - ref.watch( + Builder( + builder: (context) { + final coin = ref.watch( managerProvider.select((value) => value.coin), - ), - )} ${ref.watch( - managerProvider.select((value) => value.coin), - ).ticker}", - style: STextStyles.itemSubtitle12(context).copyWith( - color: Theme.of(context) - .extension()! - .textConfirmTotalAmount, - ), - textAlign: TextAlign.right, + ); + final fee = + (transactionInfo["fee"] as int).toAmountAsRaw( + fractionDigits: coin.decimals, + ); + final amount = + transactionInfo["recipientAmt"] as Amount; + final total = amount + fee; + final locale = ref.watch( + localeServiceChangeNotifierProvider.select( + (value) => value.locale, + ), + ); + return Text( + "${total.localizedStringAsFixed( + locale: locale, + )}" + " ${coin.ticker}", + style: STextStyles.itemSubtitle12(context) + .copyWith( + color: Theme.of(context) + .extension()! + .textConfirmTotalAmount, + ), + textAlign: TextAlign.right, + ); + }, ), ], ), @@ -570,16 +577,20 @@ class _ConfirmChangeNowSendViewState final price = ref.watch( priceAnd24hChangeNotifierProvider .select((value) => value.getPrice(coin))); - final amount = Format.satoshisToAmount( - transactionInfo["recipientAmt"] as int, - coin: coin, - ); - final value = price.item1 * amount; + final amount = + transactionInfo["recipientAmt"] as Amount; + final value = (price.item1 * amount.decimal) + .toAmount(fractionDigits: 2); final currency = ref.watch(prefsChangeNotifierProvider .select((value) => value.currency)); + final locale = ref.watch( + localeServiceChangeNotifierProvider.select( + (value) => value.locale, + ), + ); return Text( - " | ${value.toStringAsFixed(Constants.decimalPlacesForCoin(coin))} $currency", + " | ${value.localizedStringAsFixed(locale: locale)} $currency", style: STextStyles.desktopTextExtraExtraSmall(context) .copyWith( @@ -592,12 +603,13 @@ class _ConfirmChangeNowSendViewState ], ), child: Text( - "${Format.satoshiAmountToPrettyString(transactionInfo["recipientAmt"] as int, ref.watch( - localeServiceChangeNotifierProvider - .select((value) => value.locale), - ), ref.watch( - managerProvider.select((value) => value.coin), - ))} ${ref.watch( + "${(transactionInfo["recipientAmt"] as Amount).localizedStringAsFixed( + locale: ref.watch( + localeServiceChangeNotifierProvider.select( + (value) => value.locale, + ), + ), + )} ${ref.watch( managerProvider.select((value) => value.coin), ).ticker}", style: STextStyles.itemSubtitle12(context), @@ -625,12 +637,17 @@ class _ConfirmChangeNowSendViewState style: STextStyles.smallMed12(context), ), Text( - "${Format.satoshiAmountToPrettyString(transactionInfo["fee"] as int, ref.watch( - localeServiceChangeNotifierProvider - .select((value) => value.locale), - ), ref.watch( - managerProvider.select((value) => value.coin), - ))} ${ref.watch( + "${(transactionInfo["fee"] as int).toAmountAsRaw(fractionDigits: ref.watch( + managerProvider.select( + (value) => value.coin.decimals, + ), + )).localizedStringAsFixed( + locale: ref.watch( + localeServiceChangeNotifierProvider.select( + (value) => value.locale, + ), + ), + )} ${ref.watch( managerProvider.select((value) => value.coin), ).ticker}", style: STextStyles.itemSubtitle12(context), @@ -711,21 +728,36 @@ class _ConfirmChangeNowSendViewState .textConfirmTotalAmount, ), ), - Text( - "${Format.satoshiAmountToPrettyString((transactionInfo["fee"] as int) + (transactionInfo["recipientAmt"] as int), ref.watch( - localeServiceChangeNotifierProvider - .select((value) => value.locale), - ), ref.watch( - managerProvider.select((value) => value.coin), - ))} ${ref.watch( - managerProvider.select((value) => value.coin), - ).ticker}", - style: STextStyles.itemSubtitle12(context).copyWith( - color: Theme.of(context) - .extension()! - .textConfirmTotalAmount, - ), - textAlign: TextAlign.right, + Builder( + builder: (context) { + final coin = ref.watch( + managerProvider.select((value) => value.coin), + ); + final fee = + (transactionInfo["fee"] as int).toAmountAsRaw( + fractionDigits: coin.decimals, + ); + final amount = + transactionInfo["recipientAmt"] as Amount; + final total = amount + fee; + final locale = ref.watch( + localeServiceChangeNotifierProvider.select( + (value) => value.locale, + ), + ); + return Text( + "${total.localizedStringAsFixed( + locale: locale, + )}" + " ${coin.ticker}", + style: STextStyles.itemSubtitle12(context).copyWith( + color: Theme.of(context) + .extension()! + .textConfirmTotalAmount, + ), + textAlign: TextAlign.right, + ); + }, ), ], ), diff --git a/lib/pages/exchange_view/exchange_form.dart b/lib/pages/exchange_view/exchange_form.dart index ab7cab56e..76bba50ad 100644 --- a/lib/pages/exchange_view/exchange_form.dart +++ b/lib/pages/exchange_view/exchange_form.dart @@ -10,6 +10,7 @@ import 'package:stackwallet/models/exchange/aggregate_currency.dart'; import 'package:stackwallet/models/exchange/incomplete_exchange.dart'; import 'package:stackwallet/models/isar/exchange_cache/currency.dart'; import 'package:stackwallet/models/isar/exchange_cache/pair.dart'; +import 'package:stackwallet/models/isar/models/ethereum/eth_contract.dart'; import 'package:stackwallet/pages/exchange_view/exchange_coin_selection/exchange_currency_selection_view.dart'; import 'package:stackwallet/pages/exchange_view/exchange_step_views/step_1_view.dart'; import 'package:stackwallet/pages/exchange_view/exchange_step_views/step_2_view.dart'; @@ -43,10 +44,12 @@ class ExchangeForm extends ConsumerStatefulWidget { Key? key, this.walletId, this.coin, + this.contract, }) : super(key: key); final String? walletId; final Coin? coin; + final EthContract? contract; @override ConsumerState createState() => _ExchangeFormState(); @@ -161,10 +164,16 @@ class _ExchangeFormState extends ConsumerState { final type = (ref.read(exchangeFormStateProvider).exchangeRateType); final fromTicker = ref.read(exchangeFormStateProvider).fromTicker ?? ""; - if (walletInitiated && - fromTicker.toLowerCase() == coin!.ticker.toLowerCase()) { - // do not allow changing away from wallet coin - return; + if (walletInitiated) { + if (widget.contract != null && + fromTicker.toLowerCase() == widget.contract!.symbol.toLowerCase()) { + return; + } + + if (fromTicker.toLowerCase() == coin!.ticker.toLowerCase()) { + // do not allow changing away from wallet coin + return; + } } final selectedCurrency = await _showCurrencySelectionSheet( @@ -620,7 +629,7 @@ class _ExchangeFormState extends ConsumerState { ref.read(exchangeFormStateProvider).reset(shouldNotifyListeners: true); ExchangeDataLoadingService.instance .getAggregateCurrency( - coin!.ticker, + widget.contract == null ? coin!.ticker : widget.contract!.symbol, ExchangeRateType.estimated, ) .then((value) { diff --git a/lib/pages/exchange_view/exchange_step_views/step_4_view.dart b/lib/pages/exchange_view/exchange_step_views/step_4_view.dart index 16eb3923f..4a86254c4 100644 --- a/lib/pages/exchange_view/exchange_step_views/step_4_view.dart +++ b/lib/pages/exchange_view/exchange_step_views/step_4_view.dart @@ -15,11 +15,11 @@ import 'package:stackwallet/pages/send_view/sub_widgets/building_transaction_dia import 'package:stackwallet/pages/wallet_view/wallet_view.dart'; import 'package:stackwallet/providers/providers.dart'; import 'package:stackwallet/route_generator.dart'; +import 'package:stackwallet/utilities/amount/amount.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/clipboard_interface.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:stackwallet/utilities/enums/fee_rate_type_enum.dart'; -import 'package:stackwallet/utilities/format.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; import 'package:stackwallet/widgets/background.dart'; @@ -526,10 +526,11 @@ class _Step4ViewState extends ConsumerState { walletsChangeNotifierProvider) .getManager(tuple.item1); - final amount = - Format.decimalAmountToSatoshis( - model.sendAmount, - manager.coin); + final Amount amount = + model.sendAmount.toAmount( + fractionDigits: + manager.coin.decimals, + ); final address = model.trade!.payInAddress; @@ -565,7 +566,7 @@ class _Step4ViewState extends ConsumerState { final txDataFuture = manager.prepareSend( address: address, - satoshiAmount: amount, + amount: amount, args: { "feeRate": FeeRateType.average, @@ -670,12 +671,17 @@ class _Step4ViewState extends ConsumerState { .useMaterialPageRoute, builder: (BuildContext context) { + final coin = + coinFromTickerCaseInsensitive( + model.trade! + .payInCurrency); return SendFromView( - coin: - coinFromTickerCaseInsensitive( - model.trade! - .payInCurrency), - amount: model.sendAmount, + coin: coin, + amount: model.sendAmount + .toAmount( + fractionDigits: + coin.decimals, + ), address: model .trade!.payInAddress, trade: model.trade!, diff --git a/lib/pages/exchange_view/exchange_view.dart b/lib/pages/exchange_view/exchange_view.dart index f96ed39c3..d7a988574 100644 --- a/lib/pages/exchange_view/exchange_view.dart +++ b/lib/pages/exchange_view/exchange_view.dart @@ -17,7 +17,7 @@ import 'package:stackwallet/widgets/custom_loading_overlay.dart'; import 'package:stackwallet/widgets/trade_card.dart'; import 'package:tuple/tuple.dart'; -import '../../db/main_db.dart'; +import '../../db/isar/main_db.dart'; class ExchangeView extends ConsumerStatefulWidget { const ExchangeView({Key? key}) : super(key: key); diff --git a/lib/pages/exchange_view/send_from_view.dart b/lib/pages/exchange_view/send_from_view.dart index 780a88d92..36055d508 100644 --- a/lib/pages/exchange_view/send_from_view.dart +++ b/lib/pages/exchange_view/send_from_view.dart @@ -1,6 +1,5 @@ import 'dart:async'; -import 'package:decimal/decimal.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/svg.dart'; @@ -13,15 +12,14 @@ import 'package:stackwallet/providers/providers.dart'; import 'package:stackwallet/route_generator.dart'; import 'package:stackwallet/services/coins/firo/firo_wallet.dart'; import 'package:stackwallet/services/coins/manager.dart'; +import 'package:stackwallet/utilities/amount/amount.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:stackwallet/utilities/enums/fee_rate_type_enum.dart'; -import 'package:stackwallet/utilities/format.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; import 'package:stackwallet/utilities/util.dart'; -import 'package:stackwallet/widgets/animated_text.dart'; import 'package:stackwallet/widgets/background.dart'; import 'package:stackwallet/widgets/conditional_parent.dart'; import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; @@ -45,7 +43,7 @@ class SendFromView extends ConsumerStatefulWidget { static const String routeName = "/sendFrom"; final Coin coin; - final Decimal amount; + final Amount amount; final String address; final Trade trade; final bool shouldPopRoot; @@ -57,14 +55,10 @@ class SendFromView extends ConsumerStatefulWidget { class _SendFromViewState extends ConsumerState { late final Coin coin; - late final Decimal amount; + late final Amount amount; late final String address; late final Trade trade; - String formatAmount(Decimal amount, Coin coin) { - return amount.toStringAsFixed(Constants.decimalPlacesForCoin(coin)); - } - @override void initState() { coin = widget.coin; @@ -151,7 +145,13 @@ class _SendFromViewState extends ConsumerState { Row( children: [ Text( - "You need to send ${formatAmount(amount, coin)} ${coin.ticker}", + "You need to send ${amount.localizedStringAsFixed( + locale: ref.watch( + localeServiceChangeNotifierProvider.select( + (value) => value.locale, + ), + ), + )} ${coin.ticker}", style: isDesktop ? STextStyles.desktopTextExtraExtraSmall(context) : STextStyles.itemSubtitle(context), @@ -202,7 +202,7 @@ class SendFromCard extends ConsumerStatefulWidget { }) : super(key: key); final String walletId; - final Decimal amount; + final Amount amount; final String address; final Trade trade; final bool fromDesktopStep4; @@ -213,13 +213,11 @@ class SendFromCard extends ConsumerStatefulWidget { class _SendFromCardState extends ConsumerState { late final String walletId; - late final Decimal amount; + late final Amount amount; late final String address; late final Trade trade; Future _send(Manager manager, {bool? shouldSendPublicFiroFunds}) async { - final _amount = Format.decimalAmountToSatoshis(amount, manager.coin); - try { bool wasCancelled = false; @@ -265,7 +263,7 @@ class _SendFromCardState extends ConsumerState { if (shouldSendPublicFiroFunds == null) { txDataFuture = manager.prepareSend( address: address, - satoshiAmount: _amount, + amount: amount, args: { "feeRate": FeeRateType.average, // ref.read(feeRateTypeStateProvider) @@ -277,7 +275,7 @@ class _SendFromCardState extends ConsumerState { if (shouldSendPublicFiroFunds) { txDataFuture = firoWallet.prepareSendPublic( address: address, - satoshiAmount: _amount, + amount: amount, args: { "feeRate": FeeRateType.average, // ref.read(feeRateTypeStateProvider) @@ -286,7 +284,7 @@ class _SendFromCardState extends ConsumerState { } else { txDataFuture = firoWallet.prepareSend( address: address, - satoshiAmount: _amount, + amount: amount, args: { "feeRate": FeeRateType.average, // ref.read(feeRateTypeStateProvider) @@ -452,37 +450,11 @@ class _SendFromCardState extends ConsumerState { "Use private balance", style: STextStyles.itemSubtitle(context), ), - FutureBuilder( - // TODO redo this widget now that its not actually a future - future: Future(() => - (manager.wallet as FiroWallet) - .availablePrivateBalance()), - builder: (builderContext, - AsyncSnapshot snapshot) { - if (snapshot.connectionState == - ConnectionState.done && - snapshot.hasData) { - return Text( - "${Format.localizedStringAsFixed( - value: snapshot.data!, - locale: locale, - decimalPlaces: - Constants.decimalPlacesForCoin(coin), - )} ${coin.ticker}", - style: STextStyles.itemSubtitle(context), - ); - } else { - return AnimatedText( - stringsToLoopThrough: const [ - "Loading balance", - "Loading balance.", - "Loading balance..", - "Loading balance..." - ], - style: STextStyles.itemSubtitle(context), - ); - } - }, + Text( + "${(manager.wallet as FiroWallet).availablePrivateBalance().localizedStringAsFixed( + locale: locale, + )} ${coin.ticker}", + style: STextStyles.itemSubtitle(context), ), ], ), @@ -540,37 +512,11 @@ class _SendFromCardState extends ConsumerState { "Use public balance", style: STextStyles.itemSubtitle(context), ), - FutureBuilder( - // TODO redo this widget now that its not actually a future - future: Future(() => - (manager.wallet as FiroWallet) - .availablePublicBalance()), - builder: (builderContext, - AsyncSnapshot snapshot) { - if (snapshot.connectionState == - ConnectionState.done && - snapshot.hasData) { - return Text( - "${Format.localizedStringAsFixed( - value: snapshot.data!, - locale: locale, - decimalPlaces: - Constants.decimalPlacesForCoin(coin), - )} ${coin.ticker}", - style: STextStyles.itemSubtitle(context), - ); - } else { - return AnimatedText( - stringsToLoopThrough: const [ - "Loading balance", - "Loading balance.", - "Loading balance..", - "Loading balance..." - ], - style: STextStyles.itemSubtitle(context), - ); - } - }, + Text( + "${(manager.wallet as FiroWallet).availablePublicBalance().localizedStringAsFixed( + locale: locale, + )} ${coin.ticker}", + style: STextStyles.itemSubtitle(context), ), ], ), @@ -652,35 +598,11 @@ class _SendFromCardState extends ConsumerState { height: 2, ), if (!isFiro) - FutureBuilder( - // TODO redo this widget now that its not actually a future - future: Future(() => manager.balance.getTotal()), - builder: - (builderContext, AsyncSnapshot snapshot) { - if (snapshot.connectionState == - ConnectionState.done && - snapshot.hasData) { - return Text( - "${Format.localizedStringAsFixed( - value: snapshot.data!, - locale: locale, - decimalPlaces: - Constants.decimalPlacesForCoin(coin), - )} ${coin.ticker}", - style: STextStyles.itemSubtitle(context), - ); - } else { - return AnimatedText( - stringsToLoopThrough: const [ - "Loading balance", - "Loading balance.", - "Loading balance..", - "Loading balance..." - ], - style: STextStyles.itemSubtitle(context), - ); - } - }, + Text( + "${manager.balance.spendable.localizedStringAsFixed( + locale: locale, + )} ${coin.ticker}", + style: STextStyles.itemSubtitle(context), ), ], ), diff --git a/lib/pages/exchange_view/sub_widgets/exchange_provider_options.dart b/lib/pages/exchange_view/sub_widgets/exchange_provider_options.dart index c0f4312d0..c00e7dfe8 100644 --- a/lib/pages/exchange_view/sub_widgets/exchange_provider_options.dart +++ b/lib/pages/exchange_view/sub_widgets/exchange_provider_options.dart @@ -9,10 +9,9 @@ import 'package:stackwallet/providers/providers.dart'; import 'package:stackwallet/services/exchange/change_now/change_now_exchange.dart'; import 'package:stackwallet/services/exchange/exchange_response.dart'; import 'package:stackwallet/services/exchange/majestic_bank/majestic_bank_exchange.dart'; +import 'package:stackwallet/utilities/amount/amount.dart'; import 'package:stackwallet/utilities/assets.dart'; -import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; -import 'package:stackwallet/utilities/format.dart'; import 'package:stackwallet/utilities/logger.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; @@ -198,18 +197,6 @@ class _ExchangeProviderOptionsState snapshot.hasData) { final estimate = snapshot.data?.value; if (estimate != null) { - Decimal rate; - if (estimate.reversed) { - rate = (toAmount / - estimate.estimatedAmount) - .toDecimal( - scaleOnInfinitePrecision: 12); - } else { - rate = (estimate.estimatedAmount / - fromAmount) - .toDecimal( - scaleOnInfinitePrecision: 12); - } Coin coin; try { coin = coinFromTickerCaseInsensitive( @@ -217,18 +204,32 @@ class _ExchangeProviderOptionsState } catch (_) { coin = Coin.bitcoin; } + Amount rate; + if (estimate.reversed) { + rate = (toAmount / + estimate.estimatedAmount) + .toDecimal( + scaleOnInfinitePrecision: 18) + .toAmount( + fractionDigits: + coin.decimals); + } else { + rate = (estimate.estimatedAmount / + fromAmount) + .toDecimal( + scaleOnInfinitePrecision: 18) + .toAmount( + fractionDigits: + coin.decimals); + } return Text( - "1 ${sendCurrency.ticker.toUpperCase()} ~ ${Format.localizedStringAsFixed( - value: rate, + "1 ${sendCurrency.ticker.toUpperCase()} ~ ${rate.localizedStringAsFixed( locale: ref.watch( localeServiceChangeNotifierProvider .select( (value) => value.locale), ), - decimalPlaces: - Constants.decimalPlacesForCoin( - coin), )} ${receivingCurrency.ticker.toUpperCase()}", style: STextStyles.itemSubtitle12( context) @@ -435,18 +436,6 @@ class _ExchangeProviderOptionsState snapshot.hasData) { final estimate = snapshot.data?.value; if (estimate != null) { - Decimal rate; - if (estimate.reversed) { - rate = (toAmount / - estimate.estimatedAmount) - .toDecimal( - scaleOnInfinitePrecision: 12); - } else { - rate = (estimate.estimatedAmount / - fromAmount) - .toDecimal( - scaleOnInfinitePrecision: 12); - } Coin coin; try { coin = coinFromTickerCaseInsensitive( @@ -454,18 +443,32 @@ class _ExchangeProviderOptionsState } catch (_) { coin = Coin.bitcoin; } + Amount rate; + if (estimate.reversed) { + rate = (toAmount / + estimate.estimatedAmount) + .toDecimal( + scaleOnInfinitePrecision: 18) + .toAmount( + fractionDigits: coin.decimals, + ); + } else { + rate = (estimate.estimatedAmount / + fromAmount) + .toDecimal( + scaleOnInfinitePrecision: 18) + .toAmount( + fractionDigits: coin.decimals, + ); + } return Text( - "1 ${sendCurrency.ticker.toUpperCase()} ~ ${Format.localizedStringAsFixed( - value: rate, + "1 ${sendCurrency.ticker.toUpperCase()} ~ ${rate.localizedStringAsFixed( locale: ref.watch( localeServiceChangeNotifierProvider .select( (value) => value.locale), ), - decimalPlaces: - Constants.decimalPlacesForCoin( - coin), )} ${receivingCurrency.ticker.toUpperCase()}", style: STextStyles.itemSubtitle12( context) diff --git a/lib/pages/exchange_view/trade_details_view.dart b/lib/pages/exchange_view/trade_details_view.dart index 76653bd18..f0cb7b7a8 100644 --- a/lib/pages/exchange_view/trade_details_view.dart +++ b/lib/pages/exchange_view/trade_details_view.dart @@ -20,6 +20,7 @@ import 'package:stackwallet/services/exchange/change_now/change_now_exchange.dar import 'package:stackwallet/services/exchange/exchange.dart'; import 'package:stackwallet/services/exchange/majestic_bank/majestic_bank_exchange.dart'; import 'package:stackwallet/services/exchange/simpleswap/simpleswap_exchange.dart'; +import 'package:stackwallet/utilities/amount/amount.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/clipboard_interface.dart'; import 'package:stackwallet/utilities/constants.dart'; @@ -256,11 +257,11 @@ class _TradeDetailsViewState extends ConsumerState { label: "Send from Stack", buttonHeight: ButtonHeight.l, onPressed: () { - final amount = sendAmount; - final address = trade.payInAddress; - final coin = coinFromTickerCaseInsensitive(trade.payInCurrency); + final amount = + sendAmount.toAmount(fractionDigits: coin.decimals); + final address = trade.payInAddress; Navigator.of(context).pushNamed( SendFromView.routeName, @@ -339,13 +340,32 @@ class _TradeDetailsViewState extends ConsumerState { const SizedBox( height: 4, ), - SelectableText( - "-${Format.localizedStringAsFixed(value: sendAmount, locale: ref.watch( - localeServiceChangeNotifierProvider - .select((value) => value.locale), - ), decimalPlaces: trade.payInCurrency.toLowerCase() == "xmr" ? 12 : 8)} ${trade.payInCurrency.toUpperCase()}", - style: STextStyles.itemSubtitle(context), - ), + Builder(builder: (context) { + String text; + try { + final coin = coinFromTickerCaseInsensitive( + trade.payInCurrency); + final amount = sendAmount.toAmount( + fractionDigits: coin.decimals); + text = amount.localizedStringAsFixed( + locale: ref.watch( + localeServiceChangeNotifierProvider.select( + (value) => value.locale, + ), + ), + ); + } catch (_) { + text = sendAmount.toStringAsFixed( + trade.payInCurrency.toLowerCase() == "xmr" + ? 12 + : 8); + } + + return SelectableText( + "-$text ${trade.payInCurrency.toUpperCase()}", + style: STextStyles.itemSubtitle(context), + ); + }), ], ), if (!isDesktop) diff --git a/lib/pages/exchange_view/wallet_initiated_exchange_view.dart b/lib/pages/exchange_view/wallet_initiated_exchange_view.dart index 99ba04979..c2fd2d092 100644 --- a/lib/pages/exchange_view/wallet_initiated_exchange_view.dart +++ b/lib/pages/exchange_view/wallet_initiated_exchange_view.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:stackwallet/models/isar/models/isar_models.dart'; import 'package:stackwallet/pages/exchange_view/exchange_form.dart'; import 'package:stackwallet/pages/exchange_view/sub_widgets/step_row.dart'; import 'package:stackwallet/providers/exchange/exchange_form_state_provider.dart'; @@ -20,12 +21,14 @@ class WalletInitiatedExchangeView extends ConsumerStatefulWidget { Key? key, required this.walletId, required this.coin, + this.contract, }) : super(key: key); static const String routeName = "/walletInitiatedExchange"; final String walletId; final Coin coin; + final EthContract? contract; @override ConsumerState createState() => @@ -172,6 +175,7 @@ class _WalletInitiatedExchangeViewState ExchangeForm( walletId: walletId, coin: coin, + contract: widget.contract, ), ], ), diff --git a/lib/pages/paynym/dialogs/confirm_paynym_connect_dialog.dart b/lib/pages/paynym/dialogs/confirm_paynym_connect_dialog.dart index 780db2f81..94603cf62 100644 --- a/lib/pages/paynym/dialogs/confirm_paynym_connect_dialog.dart +++ b/lib/pages/paynym/dialogs/confirm_paynym_connect_dialog.dart @@ -1,9 +1,8 @@ import 'package:flutter/material.dart'; import 'package:flutter_svg/svg.dart'; +import 'package:stackwallet/utilities/amount/amount.dart'; import 'package:stackwallet/utilities/assets.dart'; -import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; -import 'package:stackwallet/utilities/format.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; import 'package:stackwallet/utilities/util.dart'; @@ -17,25 +16,22 @@ class ConfirmPaynymConnectDialog extends StatelessWidget { const ConfirmPaynymConnectDialog({ Key? key, required this.nymName, + required this.locale, required this.onConfirmPressed, required this.amount, required this.coin, }) : super(key: key); final String nymName; + final String locale; final VoidCallback onConfirmPressed; - final int amount; + final Amount amount; final Coin coin; String get title => "Connect to $nymName"; String get message => "A one-time connection fee of " - "${Format.satoshisToAmount( - amount, - coin: coin, - ).toStringAsFixed( - Constants.decimalPlacesForCoin(coin), - )} ${coin.ticker} " + "${amount.localizedStringAsFixed(locale: locale)} ${coin.ticker} " "will be charged to connect to this PayNym.\n\nThis fee " "covers the cost of creating a one-time transaction to create a " "record on the blockchain. This keeps PayNyms decentralized."; diff --git a/lib/pages/paynym/dialogs/paynym_details_popup.dart b/lib/pages/paynym/dialogs/paynym_details_popup.dart index c6dedeb39..6c4a8b691 100644 --- a/lib/pages/paynym/dialogs/paynym_details_popup.dart +++ b/lib/pages/paynym/dialogs/paynym_details_popup.dart @@ -13,9 +13,11 @@ import 'package:stackwallet/pages/paynym/paynym_home_view.dart'; import 'package:stackwallet/pages/paynym/subwidgets/paynym_bot.dart'; import 'package:stackwallet/pages/send_view/confirm_transaction_view.dart'; import 'package:stackwallet/pages/send_view/send_view.dart'; +import 'package:stackwallet/providers/global/locale_provider.dart'; import 'package:stackwallet/providers/global/wallets_provider.dart'; import 'package:stackwallet/route_generator.dart'; import 'package:stackwallet/services/mixins/paynym_wallet_interface.dart'; +import 'package:stackwallet/utilities/amount/amount.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:stackwallet/utilities/text_styles.dart'; @@ -134,6 +136,7 @@ class _PaynymDetailsPopupState extends ConsumerState { context: context, builder: (context) => ConfirmPaynymConnectDialog( nymName: widget.accountLite.nymName, + locale: ref.read(localeServiceChangeNotifierProvider).locale, onConfirmPressed: () { // print("CONFIRM NOTIF TX: $preparedTx"); @@ -156,7 +159,10 @@ class _PaynymDetailsPopupState extends ConsumerState { ), ); }, - amount: (preparedTx["amount"] as int) + (preparedTx["fee"] as int), + amount: (preparedTx["amount"] as Amount) + + (preparedTx["fee"] as int).toAmountAsRaw( + fractionDigits: manager.coin.decimals, + ), coin: manager.coin, ), ); diff --git a/lib/pages/paynym/subwidgets/desktop_paynym_details.dart b/lib/pages/paynym/subwidgets/desktop_paynym_details.dart index ad01b1f7e..a6929d0b0 100644 --- a/lib/pages/paynym/subwidgets/desktop_paynym_details.dart +++ b/lib/pages/paynym/subwidgets/desktop_paynym_details.dart @@ -12,8 +12,10 @@ import 'package:stackwallet/pages/paynym/dialogs/confirm_paynym_connect_dialog.d import 'package:stackwallet/pages/paynym/subwidgets/paynym_bot.dart'; import 'package:stackwallet/pages/send_view/confirm_transaction_view.dart'; import 'package:stackwallet/pages_desktop_specific/my_stack_view/paynym/desktop_paynym_send_dialog.dart'; +import 'package:stackwallet/providers/global/locale_provider.dart'; import 'package:stackwallet/providers/global/wallets_provider.dart'; import 'package:stackwallet/services/mixins/paynym_wallet_interface.dart'; +import 'package:stackwallet/utilities/amount/amount.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:stackwallet/utilities/text_styles.dart'; @@ -102,6 +104,7 @@ class _PaynymDetailsPopupState extends ConsumerState { context: context, builder: (context) => ConfirmPaynymConnectDialog( nymName: widget.accountLite.nymName, + locale: ref.read(localeServiceChangeNotifierProvider).locale, onConfirmPressed: () { Navigator.of(context, rootNavigator: true).pop(); unawaited( @@ -139,7 +142,10 @@ class _PaynymDetailsPopupState extends ConsumerState { ), ); }, - amount: (preparedTx["amount"] as int) + (preparedTx["fee"] as int), + amount: (preparedTx["amount"] as Amount) + + (preparedTx["fee"] as int).toAmountAsRaw( + fractionDigits: manager.coin.decimals, + ), coin: manager.coin, ), ); diff --git a/lib/pages/receive_view/addresses/address_card.dart b/lib/pages/receive_view/addresses/address_card.dart index 092809f94..7137b8872 100644 --- a/lib/pages/receive_view/addresses/address_card.dart +++ b/lib/pages/receive_view/addresses/address_card.dart @@ -3,7 +3,7 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:isar/isar.dart'; -import 'package:stackwallet/db/main_db.dart'; +import 'package:stackwallet/db/isar/main_db.dart'; import 'package:stackwallet/models/isar/models/isar_models.dart'; import 'package:stackwallet/pages/receive_view/addresses/address_tag.dart'; import 'package:stackwallet/utilities/assets.dart'; diff --git a/lib/pages/receive_view/addresses/address_details_view.dart b/lib/pages/receive_view/addresses/address_details_view.dart index ac1345657..ccb1e9fdb 100644 --- a/lib/pages/receive_view/addresses/address_details_view.dart +++ b/lib/pages/receive_view/addresses/address_details_view.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:isar/isar.dart'; import 'package:qr_flutter/qr_flutter.dart'; -import 'package:stackwallet/db/main_db.dart'; +import 'package:stackwallet/db/isar/main_db.dart'; import 'package:stackwallet/models/isar/models/isar_models.dart'; import 'package:stackwallet/pages/receive_view/addresses/address_tag.dart'; import 'package:stackwallet/pages/wallet_view/sub_widgets/no_transactions_found.dart'; diff --git a/lib/pages/receive_view/addresses/edit_address_label_view.dart b/lib/pages/receive_view/addresses/edit_address_label_view.dart index e143cddd4..7d9aa6635 100644 --- a/lib/pages/receive_view/addresses/edit_address_label_view.dart +++ b/lib/pages/receive_view/addresses/edit_address_label_view.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:isar/isar.dart'; -import 'package:stackwallet/db/main_db.dart'; +import 'package:stackwallet/db/isar/main_db.dart'; import 'package:stackwallet/models/isar/models/address_label.dart'; import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/text_styles.dart'; diff --git a/lib/pages/receive_view/addresses/wallet_addresses_view.dart b/lib/pages/receive_view/addresses/wallet_addresses_view.dart index aa0948528..be1ded23a 100644 --- a/lib/pages/receive_view/addresses/wallet_addresses_view.dart +++ b/lib/pages/receive_view/addresses/wallet_addresses_view.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/svg.dart'; import 'package:isar/isar.dart'; -import 'package:stackwallet/db/main_db.dart'; +import 'package:stackwallet/db/isar/main_db.dart'; import 'package:stackwallet/models/isar/models/isar_models.dart'; import 'package:stackwallet/pages/receive_view/addresses/address_card.dart'; import 'package:stackwallet/pages/receive_view/addresses/address_details_view.dart'; diff --git a/lib/pages/receive_view/receive_view.dart b/lib/pages/receive_view/receive_view.dart index 3c4eb9f7b..94cec7c14 100644 --- a/lib/pages/receive_view/receive_view.dart +++ b/lib/pages/receive_view/receive_view.dart @@ -287,11 +287,11 @@ class _ReceiveViewState extends ConsumerState { ), ), ), - if (coin != Coin.epicCash) + if (coin != Coin.epicCash && coin != Coin.ethereum) const SizedBox( height: 12, ), - if (coin != Coin.epicCash) + if (coin != Coin.epicCash && coin != Coin.ethereum) TextButton( onPressed: generateNewAddress, style: Theme.of(context) diff --git a/lib/pages/send_view/confirm_transaction_view.dart b/lib/pages/send_view/confirm_transaction_view.dart index ac7e41592..8cb4f42c9 100644 --- a/lib/pages/send_view/confirm_transaction_view.dart +++ b/lib/pages/send_view/confirm_transaction_view.dart @@ -8,6 +8,7 @@ import 'package:stackwallet/models/paynym/paynym_account_lite.dart'; import 'package:stackwallet/notifications/show_flush_bar.dart'; import 'package:stackwallet/pages/pinpad_views/lock_screen_view.dart'; import 'package:stackwallet/pages/send_view/sub_widgets/sending_transaction_dialog.dart'; +import 'package:stackwallet/pages/token_view/token_view.dart'; import 'package:stackwallet/pages/wallet_view/wallet_view.dart'; import 'package:stackwallet/pages_desktop_specific/coin_control/desktop_coin_control_use_dialog.dart'; import 'package:stackwallet/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_auth_send.dart'; @@ -17,10 +18,10 @@ import 'package:stackwallet/route_generator.dart'; import 'package:stackwallet/services/coins/epiccash/epiccash_wallet.dart'; import 'package:stackwallet/services/coins/firo/firo_wallet.dart'; import 'package:stackwallet/services/mixins/paynym_wallet_interface.dart'; +import 'package:stackwallet/utilities/amount/amount.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; -import 'package:stackwallet/utilities/format.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; import 'package:stackwallet/utilities/util.dart'; @@ -46,6 +47,7 @@ class ConfirmTransactionView extends ConsumerStatefulWidget { this.isTradeTransaction = false, this.isPaynymTransaction = false, this.isPaynymNotificationTransaction = false, + this.isTokenTx = false, this.onSuccessInsteadOfRouteOnSuccess, }) : super(key: key); @@ -57,6 +59,7 @@ class ConfirmTransactionView extends ConsumerStatefulWidget { final bool isTradeTransaction; final bool isPaynymTransaction; final bool isPaynymNotificationTransaction; + final bool isTokenTx; final VoidCallback? onSuccessInsteadOfRouteOnSuccess; @override @@ -102,7 +105,11 @@ class _ConfirmTransactionViewState final note = noteController.text; try { - if (widget.isPaynymNotificationTransaction) { + if (widget.isTokenTx) { + txidFuture = ref + .read(tokenServiceProvider)! + .confirmSend(txData: transactionInfo); + } else if (widget.isPaynymNotificationTransaction) { txidFuture = (manager.wallet as PaynymWalletInterface) .broadcastNotificationTx(preparedTx: transactionInfo); } else if (widget.isPaynymTransaction) { @@ -132,7 +139,11 @@ class _ConfirmTransactionViewState .read(notesServiceChangeNotifierProvider(walletId)) .editOrAddNote(txid: txid, note: note); - unawaited(manager.refresh()); + if (widget.isTokenTx) { + unawaited(ref.read(tokenServiceProvider)!.refresh()); + } else { + unawaited(manager.refresh()); + } // pop back to wallet if (mounted) { @@ -258,6 +269,15 @@ class _ConfirmTransactionViewState final managerProvider = ref.watch(walletsChangeNotifierProvider .select((value) => value.getManagerProvider(walletId))); + final String unit; + if (widget.isTokenTx) { + unit = ref.watch( + tokenServiceProvider.select((value) => value!.tokenContract.symbol)); + } else { + unit = ref.watch(walletsChangeNotifierProvider + .select((value) => value.getManager(walletId).coin.ticker)); + } + return ConditionalParent( condition: !isDesktop, builder: (child) => Background( @@ -324,7 +344,7 @@ class _ConfirmTransactionViewState ).pop(), ), Text( - "Confirm ${ref.watch(managerProvider.select((value) => value.coin.ticker.toUpperCase()))} transaction", + "Confirm $unit transaction", style: STextStyles.desktopH3(context), ), ], @@ -341,7 +361,7 @@ class _ConfirmTransactionViewState crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Text( - "Send ${ref.watch(managerProvider.select((value) => value.coin)).ticker}", + "Send $unit", style: STextStyles.pageTitleH1(context), ), const SizedBox( @@ -383,14 +403,12 @@ class _ConfirmTransactionViewState style: STextStyles.smallMed12(context), ), Text( - "${Format.satoshiAmountToPrettyString(transactionInfo["recipientAmt"] as int, ref.watch( - localeServiceChangeNotifierProvider - .select((value) => value.locale), - ), ref.watch( - managerProvider.select((value) => value.coin), - ))} ${ref.watch( - managerProvider.select((value) => value.coin), - ).ticker}", + "${(transactionInfo["recipientAmt"] as Amount).localizedStringAsFixed( + locale: ref.watch( + localeServiceChangeNotifierProvider + .select((value) => value.locale), + ), + )} $unit", style: STextStyles.itemSubtitle12(context), textAlign: TextAlign.right, ), @@ -409,12 +427,18 @@ class _ConfirmTransactionViewState style: STextStyles.smallMed12(context), ), Text( - "${Format.satoshiAmountToPrettyString(transactionInfo["fee"] as int, ref.watch( - localeServiceChangeNotifierProvider - .select((value) => value.locale), - ), ref.watch( - managerProvider.select((value) => value.coin), - ))} ${ref.watch( + "${(transactionInfo["fee"] as int).toAmountAsRaw( + fractionDigits: ref.watch( + managerProvider.select( + (value) => value.coin.decimals, + ), + ), + ).localizedStringAsFixed( + locale: ref.watch( + localeServiceChangeNotifierProvider + .select((value) => value.locale), + ), + )} ${ref.watch( managerProvider.select((value) => value.coin), ).ticker}", style: STextStyles.itemSubtitle12(context), @@ -492,10 +516,7 @@ class _ConfirmTransactionViewState width: 16, ), Text( - "Send ${ref.watch( - managerProvider - .select((value) => value.coin), - ).ticker}", + "Send $unit", style: STextStyles.desktopTextMedium(context), ), ], @@ -519,7 +540,7 @@ class _ConfirmTransactionViewState Builder( builder: (context) { final amount = - transactionInfo["recipientAmt"] as int; + transactionInfo["recipientAmt"] as Amount; final coin = ref.watch( managerProvider.select( (value) => value.coin, @@ -536,30 +557,26 @@ class _ConfirmTransactionViewState .getPrice(coin) .item1; if (price > Decimal.zero) { - fiatAmount = Format.localizedStringAsFixed( - value: Format.satoshisToAmount(amount, - coin: coin) * - price, - locale: ref - .read( - localeServiceChangeNotifierProvider) - .locale, - decimalPlaces: 2, - ); + fiatAmount = (amount.decimal * price) + .toAmount(fractionDigits: 2) + .localizedStringAsFixed( + locale: ref + .read( + localeServiceChangeNotifierProvider) + .locale, + ); } } return Row( children: [ Text( - "${Format.satoshiAmountToPrettyString( - amount, - ref.watch( + "${amount.localizedStringAsFixed( + locale: ref.watch( localeServiceChangeNotifierProvider .select((value) => value.locale), ), - coin, - )} ${coin.ticker}", + )} $unit", style: STextStyles .desktopTextExtraExtraSmall( context) @@ -661,19 +678,19 @@ class _ConfirmTransactionViewState value.getManager(walletId))) .coin; - final fee = Format.satoshisToAmount( - transactionInfo["fee"] as int, - coin: coin, + final fee = (transactionInfo["fee"] as int) + .toAmountAsRaw( + fractionDigits: coin.decimals, ); return Text( - "${Format.localizedStringAsFixed( - value: fee, + "${fee.localizedStringAsFixed( locale: ref.watch( - localeServiceChangeNotifierProvider - .select((value) => value.locale)), - decimalPlaces: - Constants.decimalPlacesForCoin(coin), + localeServiceChangeNotifierProvider + .select( + (value) => value.locale, + ), + ), )} ${coin.ticker}", style: STextStyles.desktopTextExtraExtraSmall( @@ -840,17 +857,17 @@ class _ConfirmTransactionViewState .select((value) => value.getManager(walletId))) .coin; - final fee = Format.satoshisToAmount( - transactionInfo["fee"] as int, - coin: coin, + final fee = (transactionInfo["fee"] as int).toAmountAsRaw( + fractionDigits: coin.decimals, ); return Text( - "${Format.localizedStringAsFixed( - value: fee, - locale: ref.watch(localeServiceChangeNotifierProvider - .select((value) => value.locale)), - decimalPlaces: Constants.decimalPlacesForCoin(coin), + "${fee.localizedStringAsFixed( + locale: ref.watch( + localeServiceChangeNotifierProvider.select( + (value) => value.locale, + ), + ), )} ${coin.ticker}", style: STextStyles.itemSubtitle(context), ); @@ -896,34 +913,37 @@ class _ConfirmTransactionViewState .textConfirmTotalAmount, ), ), - Text( - "${Format.satoshiAmountToPrettyString( - (transactionInfo["fee"] as int) + - (transactionInfo["recipientAmt"] as int), - ref.watch( - localeServiceChangeNotifierProvider - .select((value) => value.locale), - ), - ref.watch( - managerProvider.select((value) => value.coin), - ), - )} ${ref.watch( - managerProvider.select((value) => value.coin), - ).ticker}", - style: isDesktop - ? STextStyles.desktopTextExtraExtraSmall(context) - .copyWith( - color: Theme.of(context) - .extension()! - .textConfirmTotalAmount, - ) - : STextStyles.itemSubtitle12(context).copyWith( - color: Theme.of(context) - .extension()! - .textConfirmTotalAmount, - ), - textAlign: TextAlign.right, - ), + Builder(builder: (context) { + final coin = ref.watch(walletsChangeNotifierProvider + .select((value) => value.getManager(walletId).coin)); + final fee = (transactionInfo["fee"] as int) + .toAmountAsRaw(fractionDigits: coin.decimals); + final locale = ref.watch( + localeServiceChangeNotifierProvider + .select((value) => value.locale), + ); + final amount = transactionInfo["recipientAmt"] as Amount; + return Text( + "${(amount + fee).localizedStringAsFixed( + locale: locale, + )} ${ref.watch( + managerProvider.select((value) => value.coin), + ).ticker}", + style: isDesktop + ? STextStyles.desktopTextExtraExtraSmall(context) + .copyWith( + color: Theme.of(context) + .extension()! + .textConfirmTotalAmount, + ) + : STextStyles.itemSubtitle12(context).copyWith( + color: Theme.of(context) + .extension()! + .textConfirmTotalAmount, + ), + textAlign: TextAlign.right, + ); + }), ], ), ), diff --git a/lib/pages/send_view/send_view.dart b/lib/pages/send_view/send_view.dart index 508b9d6cc..fc2212098 100644 --- a/lib/pages/send_view/send_view.dart +++ b/lib/pages/send_view/send_view.dart @@ -25,13 +25,13 @@ import 'package:stackwallet/services/coins/firo/firo_wallet.dart'; import 'package:stackwallet/services/coins/manager.dart'; import 'package:stackwallet/services/mixins/paynym_wallet_interface.dart'; import 'package:stackwallet/utilities/address_utils.dart'; +import 'package:stackwallet/utilities/amount/amount.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/barcode_scanner_interface.dart'; import 'package:stackwallet/utilities/clipboard_interface.dart'; import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:stackwallet/utilities/enums/fee_rate_type_enum.dart'; -import 'package:stackwallet/utilities/format.dart'; import 'package:stackwallet/utilities/logger.dart'; import 'package:stackwallet/utilities/prefs.dart'; import 'package:stackwallet/utilities/text_styles.dart'; @@ -94,8 +94,8 @@ class _SendViewState extends ConsumerState { final _cryptoFocus = FocusNode(); final _baseFocus = FocusNode(); - Decimal? _amountToSend; - Decimal? _cachedAmountToSend; + Amount? _amountToSend; + Amount? _cachedAmountToSend; String? _address; String? _privateBalanceString; @@ -106,7 +106,7 @@ class _SendViewState extends ConsumerState { bool _cryptoAmountChangeLock = false; late VoidCallback onCryptoAmountChanged; - Decimal? _cachedBalance; + Amount? _cachedBalance; Set selectedUTXOs = {}; @@ -118,7 +118,9 @@ class _SendViewState extends ConsumerState { cryptoAmount != ",") { _amountToSend = cryptoAmount.contains(",") ? Decimal.parse(cryptoAmount.replaceFirst(",", ".")) - : Decimal.parse(cryptoAmount); + .toAmount(fractionDigits: coin.decimals) + : Decimal.parse(cryptoAmount) + .toAmount(fractionDigits: coin.decimals); if (_cachedAmountToSend != null && _cachedAmountToSend == _amountToSend) { return; @@ -131,13 +133,13 @@ class _SendViewState extends ConsumerState { ref.read(priceAnd24hChangeNotifierProvider).getPrice(coin).item1; if (price > Decimal.zero) { - final String fiatAmountString = Format.localizedStringAsFixed( - value: _amountToSend! * price, - locale: ref.read(localeServiceChangeNotifierProvider).locale, - decimalPlaces: 2, - ); - - baseAmountController.text = fiatAmountString; + baseAmountController.text = (_amountToSend!.decimal * price) + .toAmount( + fractionDigits: 2, + ) + .localizedStringAsFixed( + locale: ref.read(localeServiceChangeNotifierProvider).locale, + ); } } else { _amountToSend = null; @@ -152,11 +154,8 @@ class _SendViewState extends ConsumerState { setState(() { _calculateFeesFuture = calculateFees( _amountToSend == null - ? 0 - : Format.decimalAmountToSatoshis( - _amountToSend!, - coin, - ), + ? 0.toAmountAsRaw(fractionDigits: coin.decimals) + : _amountToSend!, ); }); } @@ -176,24 +175,19 @@ class _SendViewState extends ConsumerState { setState(() { _calculateFeesFuture = calculateFees( _amountToSend == null - ? 0 - : Format.decimalAmountToSatoshis( - _amountToSend!, - coin, - ), + ? 0.toAmountAsRaw(fractionDigits: coin.decimals) + : _amountToSend!, ); }); } }); } - int _currentFee = 0; + late Amount _currentFee; void _setCurrentFee(String fee, bool shouldSetState) { - final value = Format.decimalAmountToSatoshis( - Decimal.parse(fee), - coin, - ); + final value = Decimal.parse(fee).toAmount(fractionDigits: coin.decimals); + if (shouldSetState) { setState(() => _currentFee = value); } else { @@ -211,28 +205,28 @@ class _SendViewState extends ConsumerState { return null; } - void _updatePreviewButtonState(String? address, Decimal? amount) { + void _updatePreviewButtonState(String? address, Amount? amount) { if (isPaynymSend) { ref.read(previewTxButtonStateProvider.state).state = - (amount != null && amount > Decimal.zero); + (amount != null && amount > Amount.zero); } else { final isValidAddress = ref .read(walletsChangeNotifierProvider) .getManager(walletId) .validateAddress(address ?? ""); ref.read(previewTxButtonStateProvider.state).state = - (isValidAddress && amount != null && amount > Decimal.zero); + (isValidAddress && amount != null && amount > Amount.zero); } } late Future _calculateFeesFuture; - Map cachedFees = {}; - Map cachedFiroPrivateFees = {}; - Map cachedFiroPublicFees = {}; + Map cachedFees = {}; + Map cachedFiroPrivateFees = {}; + Map cachedFiroPublicFees = {}; - Future calculateFees(int amount) async { - if (amount <= 0) { + Future calculateFees(Amount amount) async { + if (amount <= Amount.zero) { return "0"; } @@ -269,7 +263,8 @@ class _SendViewState extends ConsumerState { break; } - int fee; + final String locale = ref.read(localeServiceChangeNotifierProvider).locale; + Amount fee; if (coin == Coin.monero) { MoneroTransactionPriority specialMoneroId; switch (ref.read(feeRateTypeStateProvider.state).state) { @@ -285,8 +280,7 @@ class _SendViewState extends ConsumerState { } fee = await manager.estimateFeeFor(amount, specialMoneroId.raw!); - cachedFees[amount] = Format.satoshisToAmount(fee, coin: coin) - .toStringAsFixed(Constants.decimalPlacesForCoin(coin)); + cachedFees[amount] = fee.localizedStringAsFixed(locale: locale); return cachedFees[amount]!; } else if (coin == Coin.firo || coin == Coin.firoTestNet) { @@ -294,23 +288,22 @@ class _SendViewState extends ConsumerState { "Private") { fee = await manager.estimateFeeFor(amount, feeRate); - cachedFiroPrivateFees[amount] = Format.satoshisToAmount(fee, coin: coin) - .toStringAsFixed(Constants.decimalPlacesForCoin(coin)); + cachedFiroPrivateFees[amount] = + fee.localizedStringAsFixed(locale: locale); return cachedFiroPrivateFees[amount]!; } else { fee = await (manager.wallet as FiroWallet) .estimateFeeForPublic(amount, feeRate); - cachedFiroPublicFees[amount] = Format.satoshisToAmount(fee, coin: coin) - .toStringAsFixed(Constants.decimalPlacesForCoin(coin)); + cachedFiroPublicFees[amount] = + fee.localizedStringAsFixed(locale: locale); return cachedFiroPublicFees[amount]!; } } else { fee = await manager.estimateFeeFor(amount, feeRate); - cachedFees[amount] = Format.satoshisToAmount(fee, coin: coin) - .toStringAsFixed(Constants.decimalPlacesForCoin(coin)); + cachedFees[amount] = fee.localizedStringAsFixed(locale: locale); return cachedFees[amount]!; } @@ -321,7 +314,7 @@ class _SendViewState extends ConsumerState { final wallet = ref.read(provider).wallet as FiroWallet?; if (wallet != null) { - Decimal? balance; + Amount? balance; if (ref.read(publicPrivateBalanceStateProvider.state).state == "Private") { balance = wallet.availablePrivateBalance(); @@ -329,8 +322,9 @@ class _SendViewState extends ConsumerState { balance = wallet.availablePublicBalance(); } - return Format.localizedStringAsFixed( - value: balance, locale: locale, decimalPlaces: 8); + return balance.localizedStringAsFixed( + locale: ref.read(localeServiceChangeNotifierProvider).locale, + ); } return null; @@ -345,26 +339,26 @@ class _SendViewState extends ConsumerState { final manager = ref.read(walletsChangeNotifierProvider).getManager(walletId); - final amount = Format.decimalAmountToSatoshis(_amountToSend!, coin); - int availableBalance; + final Amount amount = _amountToSend!; + final Amount availableBalance; if ((coin == Coin.firo || coin == Coin.firoTestNet)) { if (ref.read(publicPrivateBalanceStateProvider.state).state == "Private") { - availableBalance = Format.decimalAmountToSatoshis( - (manager.wallet as FiroWallet).availablePrivateBalance(), coin); + availableBalance = + (manager.wallet as FiroWallet).availablePrivateBalance(); } else { - availableBalance = Format.decimalAmountToSatoshis( - (manager.wallet as FiroWallet).availablePublicBalance(), coin); + availableBalance = + (manager.wallet as FiroWallet).availablePublicBalance(); } } else { - availableBalance = - Format.decimalAmountToSatoshis(manager.balance.getSpendable(), coin); + availableBalance = manager.balance.spendable; } final coinControlEnabled = ref.read(prefsChangeNotifierProvider).enableCoinControl; - if (!(manager.hasCoinControlSupport && coinControlEnabled) || + if (coin != Coin.ethereum && + !(manager.hasCoinControlSupport && coinControlEnabled) || (manager.hasCoinControlSupport && coinControlEnabled && selectedUTXOs.isEmpty)) { @@ -461,7 +455,7 @@ class _SendViewState extends ConsumerState { final feeRate = ref.read(feeRateTypeStateProvider); txDataFuture = wallet.preparePaymentCodeSend( paymentCode: paymentCode, - satoshiAmount: amount, + amount: amount, args: { "feeRate": feeRate, "UTXOs": (manager.hasCoinControlSupport && @@ -476,13 +470,13 @@ class _SendViewState extends ConsumerState { "Private") { txDataFuture = (manager.wallet as FiroWallet).prepareSendPublic( address: _address!, - satoshiAmount: amount, + amount: amount, args: {"feeRate": ref.read(feeRateTypeStateProvider)}, ); } else { txDataFuture = manager.prepareSend( address: _address!, - satoshiAmount: amount, + amount: amount, args: { "feeRate": ref.read(feeRateTypeStateProvider), "UTXOs": (manager.hasCoinControlSupport && @@ -564,12 +558,14 @@ class _SendViewState extends ConsumerState { @override void initState() { + coin = widget.coin; ref.refresh(feeSheetSessionCacheProvider); + _currentFee = 0.toAmountAsRaw(fractionDigits: coin.decimals); - _calculateFeesFuture = calculateFees(0); + _calculateFeesFuture = + calculateFees(0.toAmountAsRaw(fractionDigits: coin.decimals)); _data = widget.autoFillData; walletId = widget.walletId; - coin = widget.coin; clipboard = widget.clipboard; scanner = widget.barcodeScanner; @@ -675,12 +671,14 @@ class _SendViewState extends ConsumerState { ref.listen(publicPrivateBalanceStateProvider, (previous, next) { if (_amountToSend == null) { setState(() { - _calculateFeesFuture = calculateFees(0); + _calculateFeesFuture = + calculateFees(0.toAmountAsRaw(fractionDigits: coin.decimals)); }); } else { setState(() { _calculateFeesFuture = calculateFees( - Format.decimalAmountToSatoshis(_amountToSend!, coin)); + _amountToSend!, + ); }); } }); @@ -801,7 +799,7 @@ class _SendViewState extends ConsumerState { coin != Coin.firoTestNet) ? Future(() => ref.watch( provider.select((value) => - value.balance.getSpendable()))) + value.balance.spendable))) : ref.watch(publicPrivateBalanceStateProvider.state).state == "Private" ? Future(() => (ref @@ -813,7 +811,7 @@ class _SendViewState extends ConsumerState { .wallet as FiroWallet) .availablePublicBalance()), builder: - (_, AsyncSnapshot snapshot) { + (_, AsyncSnapshot snapshot) { if (snapshot.connectionState == ConnectionState.done && snapshot.hasData) { @@ -824,10 +822,9 @@ class _SendViewState extends ConsumerState { return GestureDetector( onTap: () { cryptoAmountController.text = - _cachedBalance!.toStringAsFixed( - Constants - .decimalPlacesForCoin( - coin)); + _cachedBalance! + .localizedStringAsFixed( + locale: locale); }, child: Container( color: Colors.transparent, @@ -836,10 +833,8 @@ class _SendViewState extends ConsumerState { CrossAxisAlignment.end, children: [ Text( - "${Format.localizedStringAsFixed( - value: _cachedBalance!, + "${_cachedBalance!.localizedStringAsFixed( locale: locale, - decimalPlaces: 8, )} ${coin.ticker}", style: STextStyles.titleBold12( @@ -850,17 +845,11 @@ class _SendViewState extends ConsumerState { textAlign: TextAlign.right, ), Text( - "${Format.localizedStringAsFixed( - value: _cachedBalance! * - ref.watch(priceAnd24hChangeNotifierProvider - .select((value) => - value - .getPrice( - coin) - .item1)), - locale: locale, - decimalPlaces: 2, - )} ${ref.watch(prefsChangeNotifierProvider.select((value) => value.currency))}", + "${(_cachedBalance!.decimal * ref.watch(priceAnd24hChangeNotifierProvider.select((value) => value.getPrice(coin).item1))).toAmount( + fractionDigits: 2, + ).localizedStringAsFixed( + locale: locale, + )} ${ref.watch(prefsChangeNotifierProvider.select((value) => value.currency))}", style: STextStyles.subtitle( context) .copyWith( @@ -1133,23 +1122,19 @@ class _SendViewState extends ConsumerState { // autofill amount field if (results["amount"] != null) { - final amount = + final Amount amount = Decimal.parse(results[ - "amount"]!); + "amount"]!) + .toAmount( + fractionDigits: + coin.decimals, + ); cryptoAmountController .text = - Format + amount .localizedStringAsFixed( - value: amount, - locale: ref - .read( - localeServiceChangeNotifierProvider) - .locale, - decimalPlaces: Constants - .decimalPlacesForCoin( - coin), + locale: locale, ); - amount.toString(); _amountToSend = amount; } @@ -1395,43 +1380,42 @@ class _SendViewState extends ConsumerState { style: STextStyles.smallMed12(context), textAlign: TextAlign.left, ), - CustomTextButton( - text: "Send all ${coin.ticker}", - onTap: () async { - if (coin == Coin.firo || - coin == Coin.firoTestNet) { - final firoWallet = - ref.read(provider).wallet as FiroWallet; - if (ref - .read( - publicPrivateBalanceStateProvider - .state) - .state == - "Private") { - cryptoAmountController.text = firoWallet - .availablePrivateBalance() - .toStringAsFixed( - Constants.decimalPlacesForCoin( - coin)); + if (coin != Coin.ethereum) + CustomTextButton( + text: "Send all ${coin.ticker}", + onTap: () async { + if (coin == Coin.firo || + coin == Coin.firoTestNet) { + final firoWallet = ref + .read(provider) + .wallet as FiroWallet; + if (ref + .read( + publicPrivateBalanceStateProvider + .state) + .state == + "Private") { + cryptoAmountController.text = firoWallet + .availablePrivateBalance() + .localizedStringAsFixed( + locale: locale); + } else { + cryptoAmountController.text = firoWallet + .availablePublicBalance() + .localizedStringAsFixed( + locale: locale); + } } else { - cryptoAmountController.text = firoWallet - .availablePublicBalance() - .toStringAsFixed( - Constants.decimalPlacesForCoin( - coin)); + cryptoAmountController.text = ref + .read(provider) + .balance + .spendable + .localizedStringAsFixed( + locale: locale); } - } else { - cryptoAmountController.text = (ref - .read(provider) - .balance - .getSpendable()) - .toStringAsFixed( - Constants.decimalPlacesForCoin( - coin)); - } - _cryptoAmountChanged(); - }, - ), + _cryptoAmountChanged(); + }, + ), ], ), const SizedBox( @@ -1528,26 +1512,33 @@ class _SendViewState extends ConsumerState { if (baseAmountString.isNotEmpty && baseAmountString != "." && baseAmountString != ",") { - final baseAmount = + final Amount baseAmount = baseAmountString.contains(",") ? Decimal.parse(baseAmountString - .replaceFirst(",", ".")) - : Decimal.parse(baseAmountString); + .replaceFirst(",", ".")) + .toAmount(fractionDigits: 2) + : Decimal.parse(baseAmountString) + .toAmount(fractionDigits: 2); - var _price = ref + final Decimal _price = ref .read(priceAnd24hChangeNotifierProvider) .getPrice(coin) .item1; if (_price == Decimal.zero) { - _amountToSend = Decimal.zero; + _amountToSend = 0.toAmountAsRaw( + fractionDigits: coin.decimals); } else { - _amountToSend = baseAmount <= Decimal.zero - ? Decimal.zero - : (baseAmount / _price).toDecimal( - scaleOnInfinitePrecision: - Constants.decimalPlacesForCoin( - coin)); + _amountToSend = baseAmount <= Amount.zero + ? 0.toAmountAsRaw( + fractionDigits: coin.decimals) + : (baseAmount.decimal / _price) + .toDecimal( + scaleOnInfinitePrecision: + coin.decimals, + ) + .toAmount( + fractionDigits: coin.decimals); } if (_cachedAmountToSend != null && _cachedAmountToSend == _amountToSend) { @@ -1559,21 +1550,19 @@ class _SendViewState extends ConsumerState { level: LogLevel.Info); final amountString = - Format.localizedStringAsFixed( - value: _amountToSend!, + _amountToSend!.localizedStringAsFixed( locale: ref .read( localeServiceChangeNotifierProvider) .locale, - decimalPlaces: - Constants.decimalPlacesForCoin(coin), ); _cryptoAmountChangeLock = true; cryptoAmountController.text = amountString; _cryptoAmountChangeLock = false; } else { - _amountToSend = Decimal.zero; + _amountToSend = 0.toAmountAsRaw( + fractionDigits: coin.decimals); _cryptoAmountChangeLock = true; cryptoAmountController.text = ""; _cryptoAmountChangeLock = false; @@ -1651,13 +1640,9 @@ class _SendViewState extends ConsumerState { .balance .spendable; - int? amount; + Amount? amount; if (_amountToSend != null) { - amount = - Format.decimalAmountToSatoshis( - _amountToSend!, - coin, - ); + amount = _amountToSend!; if (spendable == amount) { // this is now a send all @@ -1800,10 +1785,13 @@ class _SendViewState extends ConsumerState { builder: (_) => TransactionFeeSelectionSheet( walletId: walletId, - amount: Decimal.tryParse( - cryptoAmountController - .text) ?? - Decimal.zero, + amount: (Decimal.tryParse( + cryptoAmountController + .text) ?? + Decimal.zero) + .toAmount( + fractionDigits: coin.decimals, + ), updateChosen: (String fee) { _setCurrentFee( fee, diff --git a/lib/pages/send_view/sub_widgets/firo_balance_selection_sheet.dart b/lib/pages/send_view/sub_widgets/firo_balance_selection_sheet.dart index dc14f2c41..f776a13e0 100644 --- a/lib/pages/send_view/sub_widgets/firo_balance_selection_sheet.dart +++ b/lib/pages/send_view/sub_widgets/firo_balance_selection_sheet.dart @@ -1,4 +1,3 @@ -import 'package:decimal/decimal.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:stackwallet/providers/providers.dart'; @@ -8,7 +7,6 @@ import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; -import 'package:stackwallet/widgets/animated_text.dart'; class FiroBalanceSelectionSheet extends ConsumerStatefulWidget { const FiroBalanceSelectionSheet({ @@ -153,29 +151,18 @@ class _FiroBalanceSelectionSheetState const SizedBox( width: 2, ), - FutureBuilder( - // TODO redo this widget now that its not actually a future - future: Future( - () => firoWallet.availablePrivateBalance()), - builder: - (context, AsyncSnapshot snapshot) { - if (snapshot.connectionState == - ConnectionState.done && - snapshot.hasData) { - return Text( - "${snapshot.data!.toStringAsFixed(8)} ${manager.coin.ticker}", - style: STextStyles.itemSubtitle(context), - textAlign: TextAlign.left, - ); - } else { - return AnimatedText( - stringsToLoopThrough: - stringsToLoopThrough, - style: STextStyles.itemSubtitle(context), - ); - } - }, - ) + Text( + "${firoWallet.availablePrivateBalance().localizedStringAsFixed( + locale: ref.watch( + localeServiceChangeNotifierProvider + .select( + (value) => value.locale, + ), + ), + )} ${manager.coin.ticker}", + style: STextStyles.itemSubtitle(context), + textAlign: TextAlign.left, + ), ], ), // ], @@ -245,31 +232,18 @@ class _FiroBalanceSelectionSheetState const SizedBox( width: 2, ), - FutureBuilder( - // TODO redo this widget now that its not actually a future - future: Future( - () => firoWallet.availablePublicBalance()), - builder: - (context, AsyncSnapshot snapshot) { - if (snapshot.connectionState == - ConnectionState.done && - snapshot.hasData) { - return Text( - "${snapshot.data!.toStringAsFixed(8)} ${manager.coin.ticker}", - style: STextStyles.itemSubtitle(context), - textAlign: TextAlign.left, - ); - } else { - return AnimatedText( - stringsToLoopThrough: - stringsToLoopThrough, - style: STextStyles.itemSubtitle(context), - ); - } - }, - ) - // ], - // ), + Text( + "${firoWallet.availablePublicBalance().localizedStringAsFixed( + locale: ref.watch( + localeServiceChangeNotifierProvider + .select( + (value) => value.locale, + ), + ), + )} ${manager.coin.ticker}", + style: STextStyles.itemSubtitle(context), + textAlign: TextAlign.left, + ), ], ), ), diff --git a/lib/pages/send_view/sub_widgets/transaction_fee_selection_sheet.dart b/lib/pages/send_view/sub_widgets/transaction_fee_selection_sheet.dart index 8ad6d0ca5..cba5f779f 100644 --- a/lib/pages/send_view/sub_widgets/transaction_fee_selection_sheet.dart +++ b/lib/pages/send_view/sub_widgets/transaction_fee_selection_sheet.dart @@ -3,14 +3,16 @@ import 'package:decimal/decimal.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:stackwallet/models/paymint/fee_object_model.dart'; +import 'package:stackwallet/pages/token_view/token_view.dart'; import 'package:stackwallet/providers/providers.dart'; import 'package:stackwallet/providers/ui/fee_rate_type_state_provider.dart'; import 'package:stackwallet/providers/wallet/public_private_balance_state_provider.dart'; import 'package:stackwallet/services/coins/firo/firo_wallet.dart'; +import 'package:stackwallet/utilities/amount/amount.dart'; import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:stackwallet/utilities/enums/fee_rate_type_enum.dart'; -import 'package:stackwallet/utilities/format.dart'; +import 'package:stackwallet/utilities/logger.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; import 'package:stackwallet/widgets/animated_text.dart'; @@ -21,9 +23,9 @@ final feeSheetSessionCacheProvider = }); class FeeSheetSessionCache extends ChangeNotifier { - final Map fast = {}; - final Map average = {}; - final Map slow = {}; + final Map fast = {}; + final Map average = {}; + final Map slow = {}; void notify() => notifyListeners(); } @@ -34,11 +36,13 @@ class TransactionFeeSelectionSheet extends ConsumerStatefulWidget { required this.walletId, required this.amount, required this.updateChosen, + this.isToken = false, }) : super(key: key); final String walletId; - final Decimal amount; + final Amount amount; final Function updateChosen; + final bool isToken; @override ConsumerState createState() => @@ -48,7 +52,7 @@ class TransactionFeeSelectionSheet extends ConsumerStatefulWidget { class _TransactionFeeSelectionSheetState extends ConsumerState { late final String walletId; - late final Decimal amount; + late final Amount amount; FeeObject? feeObject; @@ -59,8 +63,8 @@ class _TransactionFeeSelectionSheetState "Calculating...", ]; - Future feeFor({ - required int amount, + Future feeFor({ + required Amount amount, required FeeRateType feeRateType, required int feeRate, required Coin coin, @@ -68,88 +72,82 @@ class _TransactionFeeSelectionSheetState switch (feeRateType) { case FeeRateType.fast: if (ref.read(feeSheetSessionCacheProvider).fast[amount] == null) { - final manager = - ref.read(walletsChangeNotifierProvider).getManager(walletId); + if (widget.isToken == false) { + final manager = + ref.read(walletsChangeNotifierProvider).getManager(walletId); - if (coin == Coin.monero || coin == Coin.wownero) { - final fee = await manager.estimateFeeFor( - amount, MoneroTransactionPriority.fast.raw!); - ref.read(feeSheetSessionCacheProvider).fast[amount] = - Format.satoshisToAmount( - fee, - coin: coin, - ); - } else if ((coin == Coin.firo || coin == Coin.firoTestNet) && - ref.read(publicPrivateBalanceStateProvider.state).state != - "Private") { - ref.read(feeSheetSessionCacheProvider).fast[amount] = - Format.satoshisToAmount( - await (manager.wallet as FiroWallet) - .estimateFeeForPublic(amount, feeRate), - coin: coin); + if (coin == Coin.monero || coin == Coin.wownero) { + final fee = await manager.estimateFeeFor( + amount, MoneroTransactionPriority.fast.raw!); + ref.read(feeSheetSessionCacheProvider).fast[amount] = fee; + } else if ((coin == Coin.firo || coin == Coin.firoTestNet) && + ref.read(publicPrivateBalanceStateProvider.state).state != + "Private") { + ref.read(feeSheetSessionCacheProvider).fast[amount] = + await (manager.wallet as FiroWallet) + .estimateFeeForPublic(amount, feeRate); + } else { + ref.read(feeSheetSessionCacheProvider).fast[amount] = + await manager.estimateFeeFor(amount, feeRate); + } } else { - ref.read(feeSheetSessionCacheProvider).fast[amount] = - Format.satoshisToAmount( - await manager.estimateFeeFor(amount, feeRate), - coin: coin); + final tokenWallet = ref.read(tokenServiceProvider)!; + final fee = tokenWallet.estimateFeeFor(feeRate); + ref.read(feeSheetSessionCacheProvider).fast[amount] = fee; } } return ref.read(feeSheetSessionCacheProvider).fast[amount]!; case FeeRateType.average: if (ref.read(feeSheetSessionCacheProvider).average[amount] == null) { - final manager = - ref.read(walletsChangeNotifierProvider).getManager(walletId); - if (coin == Coin.monero || coin == Coin.wownero) { - final fee = await manager.estimateFeeFor( - amount, MoneroTransactionPriority.regular.raw!); - ref.read(feeSheetSessionCacheProvider).average[amount] = - Format.satoshisToAmount( - fee, - coin: coin, - ); - } else if ((coin == Coin.firo || coin == Coin.firoTestNet) && - ref.read(publicPrivateBalanceStateProvider.state).state != - "Private") { - ref.read(feeSheetSessionCacheProvider).average[amount] = - Format.satoshisToAmount( - await (manager.wallet as FiroWallet) - .estimateFeeForPublic(amount, feeRate), - coin: coin); + if (widget.isToken == false) { + final manager = + ref.read(walletsChangeNotifierProvider).getManager(walletId); + if (coin == Coin.monero || coin == Coin.wownero) { + final fee = await manager.estimateFeeFor( + amount, MoneroTransactionPriority.regular.raw!); + ref.read(feeSheetSessionCacheProvider).average[amount] = fee; + } else if ((coin == Coin.firo || coin == Coin.firoTestNet) && + ref.read(publicPrivateBalanceStateProvider.state).state != + "Private") { + ref.read(feeSheetSessionCacheProvider).average[amount] = + await (manager.wallet as FiroWallet) + .estimateFeeForPublic(amount, feeRate); + } else { + ref.read(feeSheetSessionCacheProvider).average[amount] = + await manager.estimateFeeFor(amount, feeRate); + } } else { - ref.read(feeSheetSessionCacheProvider).average[amount] = - Format.satoshisToAmount( - await manager.estimateFeeFor(amount, feeRate), - coin: coin); + final tokenWallet = ref.read(tokenServiceProvider)!; + final fee = tokenWallet.estimateFeeFor(feeRate); + ref.read(feeSheetSessionCacheProvider).average[amount] = fee; } } return ref.read(feeSheetSessionCacheProvider).average[amount]!; case FeeRateType.slow: if (ref.read(feeSheetSessionCacheProvider).slow[amount] == null) { - final manager = - ref.read(walletsChangeNotifierProvider).getManager(walletId); - if (coin == Coin.monero || coin == Coin.wownero) { - final fee = await manager.estimateFeeFor( - amount, MoneroTransactionPriority.slow.raw!); - ref.read(feeSheetSessionCacheProvider).slow[amount] = - Format.satoshisToAmount( - fee, - coin: coin, - ); - } else if ((coin == Coin.firo || coin == Coin.firoTestNet) && - ref.read(publicPrivateBalanceStateProvider.state).state != - "Private") { - ref.read(feeSheetSessionCacheProvider).slow[amount] = - Format.satoshisToAmount( - await (manager.wallet as FiroWallet) - .estimateFeeForPublic(amount, feeRate), - coin: coin); + if (widget.isToken == false) { + final manager = + ref.read(walletsChangeNotifierProvider).getManager(walletId); + if (coin == Coin.monero || coin == Coin.wownero) { + final fee = await manager.estimateFeeFor( + amount, MoneroTransactionPriority.slow.raw!); + ref.read(feeSheetSessionCacheProvider).slow[amount] = fee; + } else if ((coin == Coin.firo || coin == Coin.firoTestNet) && + ref.read(publicPrivateBalanceStateProvider.state).state != + "Private") { + ref.read(feeSheetSessionCacheProvider).slow[amount] = + await (manager.wallet as FiroWallet) + .estimateFeeForPublic(amount, feeRate); + } else { + ref.read(feeSheetSessionCacheProvider).slow[amount] = + await manager.estimateFeeFor(amount, feeRate); + } } else { - ref.read(feeSheetSessionCacheProvider).slow[amount] = - Format.satoshisToAmount( - await manager.estimateFeeFor(amount, feeRate), - coin: coin); + final tokenWallet = ref.read(tokenServiceProvider)!; + final fee = tokenWallet.estimateFeeFor(feeRate); + ref.read(feeSheetSessionCacheProvider).slow[amount] = fee; } } return ref.read(feeSheetSessionCacheProvider).slow[amount]!; @@ -231,7 +229,9 @@ class _TransactionFeeSelectionSheetState height: 36, ), FutureBuilder( - future: manager.fees, + future: widget.isToken + ? ref.read(tokenServiceProvider)!.fees + : manager.fees, builder: (context, AsyncSnapshot snapshot) { if (snapshot.connectionState == ConnectionState.done && snapshot.hasData) { @@ -320,23 +320,25 @@ class _TransactionFeeSelectionSheetState if (feeObject != null) FutureBuilder( future: feeFor( - coin: manager.coin, - feeRateType: FeeRateType.fast, - feeRate: feeObject!.fast, - amount: Format - .decimalAmountToSatoshis( - amount, manager.coin)), + coin: manager.coin, + feeRateType: FeeRateType.fast, + feeRate: feeObject!.fast, + amount: amount, + ), // future: manager.estimateFeeFor( // Format.decimalAmountToSatoshis( // amount), // feeObject!.fast), builder: (_, - AsyncSnapshot snapshot) { + AsyncSnapshot snapshot) { if (snapshot.connectionState == ConnectionState.done && snapshot.hasData) { return Text( - "(~${snapshot.data!} ${manager.coin.ticker})", + "(~${snapshot.data!.decimal.toStringAsFixed( + manager.coin.decimals, + )}" + " ${manager.coin.ticker})", style: STextStyles.itemSubtitle( context), textAlign: TextAlign.left, @@ -452,23 +454,23 @@ class _TransactionFeeSelectionSheetState if (feeObject != null) FutureBuilder( future: feeFor( - coin: manager.coin, - feeRateType: FeeRateType.average, - feeRate: feeObject!.medium, - amount: Format - .decimalAmountToSatoshis( - amount, manager.coin)), + coin: manager.coin, + feeRateType: FeeRateType.average, + feeRate: feeObject!.medium, + amount: amount, + ), // future: manager.estimateFeeFor( // Format.decimalAmountToSatoshis( // amount), // feeObject!.fast), builder: (_, - AsyncSnapshot snapshot) { + AsyncSnapshot snapshot) { if (snapshot.connectionState == ConnectionState.done && snapshot.hasData) { return Text( - "(~${snapshot.data!} ${manager.coin.ticker})", + "(~${snapshot.data!.decimal.toStringAsFixed(manager.coin.decimals)}" + " ${manager.coin.ticker})", style: STextStyles.itemSubtitle( context), textAlign: TextAlign.left, @@ -523,7 +525,6 @@ class _TransactionFeeSelectionSheetState FeeRateType.slow; } String? fee = getAmount(FeeRateType.slow, manager.coin); - print("fee $fee"); if (fee != null) { widget.updateChosen(fee); } @@ -586,23 +587,22 @@ class _TransactionFeeSelectionSheetState if (feeObject != null) FutureBuilder( future: feeFor( - coin: manager.coin, - feeRateType: FeeRateType.slow, - feeRate: feeObject!.slow, - amount: Format - .decimalAmountToSatoshis( - amount, manager.coin)), + coin: manager.coin, + feeRateType: FeeRateType.slow, + feeRate: feeObject!.slow, + amount: amount, + ), // future: manager.estimateFeeFor( // Format.decimalAmountToSatoshis( // amount), // feeObject!.fast), builder: (_, - AsyncSnapshot snapshot) { + AsyncSnapshot snapshot) { if (snapshot.connectionState == ConnectionState.done && snapshot.hasData) { return Text( - "(~${snapshot.data!} ${manager.coin.ticker})", + "(~${snapshot.data!.decimal.toStringAsFixed(manager.coin.decimals)} ${manager.coin.ticker})", style: STextStyles.itemSubtitle( context), textAlign: TextAlign.left, @@ -660,18 +660,12 @@ class _TransactionFeeSelectionSheetState String? getAmount(FeeRateType feeRateType, Coin coin) { try { - print(feeRateType); - var amount = Format.decimalAmountToSatoshis(this.amount, coin); - print(amount); - print(ref.read(feeSheetSessionCacheProvider).fast); - print(ref.read(feeSheetSessionCacheProvider).average); - print(ref.read(feeSheetSessionCacheProvider).slow); switch (feeRateType) { case FeeRateType.fast: if (ref.read(feeSheetSessionCacheProvider).fast[amount] != null) { return (ref.read(feeSheetSessionCacheProvider).fast[amount] as Decimal) - .toString(); + .toStringAsFixed(coin.decimals); } return null; @@ -679,22 +673,20 @@ class _TransactionFeeSelectionSheetState if (ref.read(feeSheetSessionCacheProvider).average[amount] != null) { return (ref.read(feeSheetSessionCacheProvider).average[amount] as Decimal) - .toString(); + .toStringAsFixed(coin.decimals); } return null; case FeeRateType.slow: - print(ref.read(feeSheetSessionCacheProvider).slow); - print(ref.read(feeSheetSessionCacheProvider).slow[amount]); if (ref.read(feeSheetSessionCacheProvider).slow[amount] != null) { return (ref.read(feeSheetSessionCacheProvider).slow[amount] as Decimal) - .toString(); + .toStringAsFixed(coin.decimals); } return null; } } catch (e, s) { - print("$e $s"); + Logging.instance.log("$e $s", level: LogLevel.Warning); return null; } } diff --git a/lib/pages/send_view/token_send_view.dart b/lib/pages/send_view/token_send_view.dart new file mode 100644 index 000000000..e79842256 --- /dev/null +++ b/lib/pages/send_view/token_send_view.dart @@ -0,0 +1,1237 @@ +import 'dart:async'; + +import 'package:decimal/decimal.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:stackwallet/models/isar/models/isar_models.dart'; +import 'package:stackwallet/models/send_view_auto_fill_data.dart'; +import 'package:stackwallet/pages/address_book_views/address_book_view.dart'; +import 'package:stackwallet/pages/send_view/confirm_transaction_view.dart'; +import 'package:stackwallet/pages/send_view/sub_widgets/building_transaction_dialog.dart'; +import 'package:stackwallet/pages/send_view/sub_widgets/transaction_fee_selection_sheet.dart'; +import 'package:stackwallet/pages/token_view/token_view.dart'; +import 'package:stackwallet/providers/providers.dart'; +import 'package:stackwallet/providers/ui/fee_rate_type_state_provider.dart'; +import 'package:stackwallet/providers/ui/preview_tx_button_state_provider.dart'; +import 'package:stackwallet/route_generator.dart'; +import 'package:stackwallet/services/coins/manager.dart'; +import 'package:stackwallet/utilities/address_utils.dart'; +import 'package:stackwallet/utilities/amount/amount.dart'; +import 'package:stackwallet/utilities/assets.dart'; +import 'package:stackwallet/utilities/barcode_scanner_interface.dart'; +import 'package:stackwallet/utilities/clipboard_interface.dart'; +import 'package:stackwallet/utilities/constants.dart'; +import 'package:stackwallet/utilities/enums/coin_enum.dart'; +import 'package:stackwallet/utilities/enums/fee_rate_type_enum.dart'; +import 'package:stackwallet/utilities/logger.dart'; +import 'package:stackwallet/utilities/prefs.dart'; +import 'package:stackwallet/utilities/text_styles.dart'; +import 'package:stackwallet/utilities/theme/stack_colors.dart'; +import 'package:stackwallet/utilities/util.dart'; +import 'package:stackwallet/widgets/animated_text.dart'; +import 'package:stackwallet/widgets/background.dart'; +import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; +import 'package:stackwallet/widgets/icon_widgets/addressbook_icon.dart'; +import 'package:stackwallet/widgets/icon_widgets/clipboard_icon.dart'; +import 'package:stackwallet/widgets/icon_widgets/eth_token_icon.dart'; +import 'package:stackwallet/widgets/icon_widgets/qrcode_icon.dart'; +import 'package:stackwallet/widgets/icon_widgets/x_icon.dart'; +import 'package:stackwallet/widgets/stack_dialog.dart'; +import 'package:stackwallet/widgets/stack_text_field.dart'; +import 'package:stackwallet/widgets/textfield_icon_button.dart'; + +class TokenSendView extends ConsumerStatefulWidget { + const TokenSendView({ + Key? key, + required this.walletId, + required this.coin, + required this.tokenContract, + this.autoFillData, + this.clipboard = const ClipboardWrapper(), + this.barcodeScanner = const BarcodeScannerWrapper(), + }) : super(key: key); + + static const String routeName = "/tokenSendView"; + + final String walletId; + final Coin coin; + final EthContract tokenContract; + final SendViewAutoFillData? autoFillData; + final ClipboardInterface clipboard; + final BarcodeScannerInterface barcodeScanner; + + @override + ConsumerState createState() => _TokenSendViewState(); +} + +class _TokenSendViewState extends ConsumerState { + late final String walletId; + late final Coin coin; + late final EthContract tokenContract; + late final ClipboardInterface clipboard; + late final BarcodeScannerInterface scanner; + + late TextEditingController sendToController; + late TextEditingController cryptoAmountController; + late TextEditingController baseAmountController; + late TextEditingController noteController; + late TextEditingController feeController; + + late final SendViewAutoFillData? _data; + + final _addressFocusNode = FocusNode(); + final _noteFocusNode = FocusNode(); + final _cryptoFocus = FocusNode(); + final _baseFocus = FocusNode(); + + Amount? _amountToSend; + Amount? _cachedAmountToSend; + String? _address; + + bool _addressToggleFlag = false; + + bool _cryptoAmountChangeLock = false; + late VoidCallback onCryptoAmountChanged; + + final updateFeesTimerDuration = const Duration(milliseconds: 500); + + Timer? _cryptoAmountChangedFeeUpdateTimer; + Timer? _baseAmountChangedFeeUpdateTimer; + late Future _calculateFeesFuture; + String cachedFees = ""; + + void _onTokenSendViewPasteAddressFieldButtonPressed() async { + final ClipboardData? data = await clipboard.getData(Clipboard.kTextPlain); + if (data?.text != null && data!.text!.isNotEmpty) { + String content = data.text!.trim(); + if (content.contains("\n")) { + content = content.substring(0, content.indexOf("\n")); + } + sendToController.text = content.trim(); + _address = content.trim(); + + _updatePreviewButtonState(_address, _amountToSend); + setState(() { + _addressToggleFlag = sendToController.text.isNotEmpty; + }); + } + } + + void _onTokenSendViewScanQrButtonPressed() async { + try { + // ref + // .read( + // shouldShowLockscreenOnResumeStateProvider + // .state) + // .state = false; + if (FocusScope.of(context).hasFocus) { + FocusScope.of(context).unfocus(); + await Future.delayed(const Duration(milliseconds: 75)); + } + + final qrResult = await scanner.scan(); + + // Future.delayed( + // const Duration(seconds: 2), + // () => ref + // .read( + // shouldShowLockscreenOnResumeStateProvider + // .state) + // .state = true, + // ); + + Logging.instance.log("qrResult content: ${qrResult.rawContent}", + level: LogLevel.Info); + + final results = AddressUtils.parseUri(qrResult.rawContent); + + Logging.instance.log("qrResult parsed: $results", level: LogLevel.Info); + + if (results.isNotEmpty && results["scheme"] == coin.uriScheme) { + // auto fill address + _address = (results["address"] ?? "").trim(); + sendToController.text = _address!; + + // autofill notes field + if (results["message"] != null) { + noteController.text = results["message"]!; + } else if (results["label"] != null) { + noteController.text = results["label"]!; + } + + // autofill amount field + if (results["amount"] != null) { + final Amount amount = Decimal.parse(results["amount"]!).toAmount( + fractionDigits: tokenContract.decimals, + ); + cryptoAmountController.text = amount.localizedStringAsFixed( + locale: ref.read(localeServiceChangeNotifierProvider).locale, + ); + _amountToSend = amount; + } + + _updatePreviewButtonState(_address, _amountToSend); + setState(() { + _addressToggleFlag = sendToController.text.isNotEmpty; + }); + + // now check for non standard encoded basic address + } else if (ref + .read(walletsChangeNotifierProvider) + .getManager(walletId) + .validateAddress(qrResult.rawContent)) { + _address = qrResult.rawContent.trim(); + sendToController.text = _address ?? ""; + + _updatePreviewButtonState(_address, _amountToSend); + setState(() { + _addressToggleFlag = sendToController.text.isNotEmpty; + }); + } + } on PlatformException catch (e, s) { + // ref + // .read( + // shouldShowLockscreenOnResumeStateProvider + // .state) + // .state = true; + // here we ignore the exception caused by not giving permission + // to use the camera to scan a qr code + Logging.instance.log( + "Failed to get camera permissions while trying to scan qr code in SendView: $e\n$s", + level: LogLevel.Warning); + } + } + + void _onFiatAmountFieldChanged(String baseAmountString) { + if (baseAmountString.isNotEmpty && + baseAmountString != "." && + baseAmountString != ",") { + final baseAmount = Amount.fromDecimal( + baseAmountString.contains(",") + ? Decimal.parse(baseAmountString.replaceFirst(",", ".")) + : Decimal.parse(baseAmountString), + fractionDigits: tokenContract.decimals, + ); + + final _price = ref + .read(priceAnd24hChangeNotifierProvider) + .getTokenPrice(tokenContract.address) + .item1; + + if (_price == Decimal.zero) { + _amountToSend = Amount.zero; + } else { + _amountToSend = baseAmount <= Amount.zero + ? Amount.zero + : Amount.fromDecimal( + (baseAmount.decimal / _price).toDecimal( + scaleOnInfinitePrecision: tokenContract.decimals), + fractionDigits: tokenContract.decimals); + } + if (_cachedAmountToSend != null && _cachedAmountToSend == _amountToSend) { + return; + } + _cachedAmountToSend = _amountToSend; + Logging.instance.log("it changed $_amountToSend $_cachedAmountToSend", + level: LogLevel.Info); + + _cryptoAmountChangeLock = true; + cryptoAmountController.text = _amountToSend!.localizedStringAsFixed( + locale: ref.read(localeServiceChangeNotifierProvider).locale, + ); + _cryptoAmountChangeLock = false; + } else { + _amountToSend = Amount.zero; + _cryptoAmountChangeLock = true; + cryptoAmountController.text = ""; + _cryptoAmountChangeLock = false; + } + // setState(() { + // _calculateFeesFuture = calculateFees( + // Format.decimalAmountToSatoshis( + // _amountToSend!)); + // }); + _updatePreviewButtonState(_address, _amountToSend); + } + + void _cryptoAmountChanged() async { + if (!_cryptoAmountChangeLock) { + final String cryptoAmount = cryptoAmountController.text; + if (cryptoAmount.isNotEmpty && + cryptoAmount != "." && + cryptoAmount != ",") { + _amountToSend = Amount.fromDecimal( + cryptoAmount.contains(",") + ? Decimal.parse(cryptoAmount.replaceFirst(",", ".")) + : Decimal.parse(cryptoAmount), + fractionDigits: tokenContract.decimals); + if (_cachedAmountToSend != null && + _cachedAmountToSend == _amountToSend) { + return; + } + _cachedAmountToSend = _amountToSend; + Logging.instance.log("it changed $_amountToSend $_cachedAmountToSend", + level: LogLevel.Info); + + final price = ref + .read(priceAnd24hChangeNotifierProvider) + .getTokenPrice(tokenContract.address) + .item1; + + if (price > Decimal.zero) { + baseAmountController.text = (_amountToSend!.decimal * price) + .toAmount( + fractionDigits: 2, + ) + .localizedStringAsFixed( + locale: ref.read(localeServiceChangeNotifierProvider).locale, + ); + } + } else { + _amountToSend = null; + baseAmountController.text = ""; + } + + _updatePreviewButtonState(_address, _amountToSend); + + _cryptoAmountChangedFeeUpdateTimer?.cancel(); + _cryptoAmountChangedFeeUpdateTimer = Timer(updateFeesTimerDuration, () { + if (coin != Coin.epicCash && !_baseFocus.hasFocus) { + setState(() { + _calculateFeesFuture = calculateFees(); + }); + } + }); + } + } + + void _baseAmountChanged() { + _baseAmountChangedFeeUpdateTimer?.cancel(); + _baseAmountChangedFeeUpdateTimer = Timer(updateFeesTimerDuration, () { + if (coin != Coin.epicCash && !_cryptoFocus.hasFocus) { + setState(() { + _calculateFeesFuture = calculateFees(); + }); + } + }); + } + + String? _updateInvalidAddressText(String address, Manager manager) { + if (_data != null && _data!.contactLabel == address) { + return null; + } + if (address.isNotEmpty && !manager.validateAddress(address)) { + return "Invalid address"; + } + return null; + } + + void _updatePreviewButtonState(String? address, Amount? amount) { + final isValidAddress = ref + .read(walletsChangeNotifierProvider) + .getManager(walletId) + .validateAddress(address ?? ""); + ref.read(previewTxButtonStateProvider.state).state = + (isValidAddress && amount != null && amount > Amount.zero); + } + + Future calculateFees() async { + final wallet = ref.read(tokenServiceProvider)!; + final feeObject = await wallet.fees; + + late final int feeRate; + + switch (ref.read(feeRateTypeStateProvider.state).state) { + case FeeRateType.fast: + feeRate = feeObject.fast; + break; + case FeeRateType.average: + feeRate = feeObject.medium; + break; + case FeeRateType.slow: + feeRate = feeObject.slow; + break; + } + + final Amount fee = wallet.estimateFeeFor(feeRate); + cachedFees = fee.localizedStringAsFixed( + locale: ref.read(localeServiceChangeNotifierProvider).locale, + ); + + return cachedFees; + } + + Future _previewTransaction() async { + // wait for keyboard to disappear + FocusScope.of(context).unfocus(); + await Future.delayed( + const Duration(milliseconds: 100), + ); + final manager = + ref.read(walletsChangeNotifierProvider).getManager(walletId); + final tokenWallet = ref.read(tokenServiceProvider)!; + + final Amount amount = _amountToSend!; + + // // confirm send all + // if (amount == availableBalance) { + // bool? shouldSendAll; + // if (mounted) { + // shouldSendAll = await showDialog( + // context: context, + // useSafeArea: false, + // barrierDismissible: true, + // builder: (context) { + // return StackDialog( + // title: "Confirm send all", + // message: + // "You are about to send your entire balance. Would you like to continue?", + // leftButton: TextButton( + // style: Theme.of(context) + // .extension()! + // .getSecondaryEnabledButtonStyle(context), + // child: Text( + // "Cancel", + // style: STextStyles.button(context).copyWith( + // color: Theme.of(context) + // .extension()! + // .accentColorDark), + // ), + // onPressed: () { + // Navigator.of(context).pop(false); + // }, + // ), + // rightButton: TextButton( + // style: Theme.of(context) + // .extension()! + // .getPrimaryEnabledButtonStyle(context), + // child: Text( + // "Yes", + // style: STextStyles.button(context), + // ), + // onPressed: () { + // Navigator.of(context).pop(true); + // }, + // ), + // ); + // }, + // ); + // } + // + // if (shouldSendAll == null || shouldSendAll == false) { + // // cancel preview + // return; + // } + // } + + try { + bool wasCancelled = false; + + if (mounted) { + unawaited( + showDialog( + context: context, + useSafeArea: false, + barrierDismissible: false, + builder: (context) { + return BuildingTransactionDialog( + coin: manager.coin, + onCancel: () { + wasCancelled = true; + + Navigator.of(context).pop(); + }, + ); + }, + ), + ); + } + + final time = Future.delayed( + const Duration( + milliseconds: 2500, + ), + ); + + Map txData; + Future> txDataFuture; + + txDataFuture = tokenWallet.prepareSend( + address: _address!, + amount: amount, + args: { + "feeRate": ref.read(feeRateTypeStateProvider), + }, + ); + + final results = await Future.wait([ + txDataFuture, + time, + ]); + + txData = results.first as Map; + + if (!wasCancelled && mounted) { + // pop building dialog + Navigator.of(context).pop(); + txData["note"] = noteController.text; + + txData["address"] = _address; + + unawaited(Navigator.of(context).push( + RouteGenerator.getRoute( + shouldUseMaterialRoute: RouteGenerator.useMaterialPageRoute, + builder: (_) => ConfirmTransactionView( + transactionInfo: txData, + walletId: walletId, + isTokenTx: true, + ), + settings: const RouteSettings( + name: ConfirmTransactionView.routeName, + ), + ), + )); + } + } catch (e) { + if (mounted) { + // pop building dialog + Navigator.of(context).pop(); + + unawaited(showDialog( + context: context, + useSafeArea: false, + barrierDismissible: true, + builder: (context) { + return StackDialog( + title: "Transaction failed", + message: e.toString(), + rightButton: TextButton( + style: Theme.of(context) + .extension()! + .getSecondaryEnabledButtonStyle(context), + child: Text( + "Ok", + style: STextStyles.button(context).copyWith( + color: Theme.of(context) + .extension()! + .accentColorDark), + ), + onPressed: () { + Navigator.of(context).pop(); + }, + ), + ); + }, + )); + } + } + } + + @override + void initState() { + ref.refresh(feeSheetSessionCacheProvider); + + _calculateFeesFuture = calculateFees(); + _data = widget.autoFillData; + walletId = widget.walletId; + coin = widget.coin; + tokenContract = widget.tokenContract; + clipboard = widget.clipboard; + scanner = widget.barcodeScanner; + + sendToController = TextEditingController(); + cryptoAmountController = TextEditingController(); + baseAmountController = TextEditingController(); + noteController = TextEditingController(); + feeController = TextEditingController(); + + onCryptoAmountChanged = _cryptoAmountChanged; + cryptoAmountController.addListener(onCryptoAmountChanged); + baseAmountController.addListener(_baseAmountChanged); + + if (_data != null) { + if (_data!.amount != null) { + cryptoAmountController.text = _data!.amount!.toString(); + } + sendToController.text = _data!.contactLabel; + _address = _data!.address.trim(); + _addressToggleFlag = true; + } + + super.initState(); + } + + @override + void dispose() { + _cryptoAmountChangedFeeUpdateTimer?.cancel(); + _baseAmountChangedFeeUpdateTimer?.cancel(); + + cryptoAmountController.removeListener(onCryptoAmountChanged); + baseAmountController.removeListener(_baseAmountChanged); + + sendToController.dispose(); + cryptoAmountController.dispose(); + baseAmountController.dispose(); + noteController.dispose(); + feeController.dispose(); + + _noteFocusNode.dispose(); + _addressFocusNode.dispose(); + _cryptoFocus.dispose(); + _baseFocus.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + debugPrint("BUILD: $runtimeType"); + final provider = ref.watch(walletsChangeNotifierProvider + .select((value) => value.getManagerProvider(walletId))); + final String locale = ref.watch( + localeServiceChangeNotifierProvider.select((value) => value.locale)); + + return Background( + child: Scaffold( + backgroundColor: Theme.of(context).extension()!.background, + appBar: AppBar( + leading: AppBarBackButton( + onPressed: () async { + if (FocusScope.of(context).hasFocus) { + FocusScope.of(context).unfocus(); + await Future.delayed(const Duration(milliseconds: 50)); + } + if (mounted) { + Navigator.of(context).pop(); + } + }, + ), + title: Text( + "Send ${tokenContract.symbol}", + style: STextStyles.navBarTitle(context), + ), + ), + body: LayoutBuilder( + builder: (builderContext, constraints) { + return Padding( + padding: const EdgeInsets.only( + left: 12, + top: 12, + right: 12, + ), + child: SingleChildScrollView( + child: ConstrainedBox( + constraints: BoxConstraints( + // subtract top and bottom padding set in parent + minHeight: constraints.maxHeight - 24, + ), + child: IntrinsicHeight( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 4), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Container( + decoration: BoxDecoration( + color: Theme.of(context) + .extension()! + .popupBG, + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + ), + child: Padding( + padding: const EdgeInsets.all(12.0), + child: Row( + children: [ + EthTokenIcon( + contractAddress: tokenContract.address, + ), + const SizedBox( + width: 6, + ), + Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Text( + ref.watch(provider.select( + (value) => value.walletName)), + style: STextStyles.titleBold12(context) + .copyWith(fontSize: 14), + overflow: TextOverflow.ellipsis, + maxLines: 1, + ), + Text( + "Available balance", + style: STextStyles.label(context) + .copyWith(fontSize: 10), + ), + ], + ), + const Spacer(), + GestureDetector( + onTap: () { + cryptoAmountController.text = ref + .read(tokenServiceProvider)! + .balance + .spendable + .localizedStringAsFixed( + locale: ref + .read( + localeServiceChangeNotifierProvider) + .locale, + ); + }, + child: Container( + color: Colors.transparent, + child: Column( + crossAxisAlignment: + CrossAxisAlignment.end, + children: [ + Text( + "${ref.watch( + tokenServiceProvider.select( + (value) => value! + .balance.spendable + .localizedStringAsFixed( + locale: ref.watch( + localeServiceChangeNotifierProvider + .select( + (value) => value.locale, + ), + ), + ), + ), + )} ${tokenContract.symbol}", + style: + STextStyles.titleBold12(context) + .copyWith( + fontSize: 10, + ), + textAlign: TextAlign.right, + ), + Text( + "${(ref.watch(tokenServiceProvider.select((value) => value!.balance.spendable.decimal)) * ref.watch(priceAnd24hChangeNotifierProvider.select((value) => value.getTokenPrice(tokenContract.address).item1))).toAmount( + fractionDigits: 2, + ).localizedStringAsFixed( + locale: locale, + )} ${ref.watch(prefsChangeNotifierProvider.select((value) => value.currency))}", + style: STextStyles.subtitle(context) + .copyWith( + fontSize: 8, + ), + textAlign: TextAlign.right, + ) + ], + ), + ), + ), + ], + ), + ), + ), + const SizedBox( + height: 16, + ), + Text( + "Send to", + style: STextStyles.smallMed12(context), + textAlign: TextAlign.left, + ), + const SizedBox( + height: 8, + ), + ClipRRect( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + child: TextField( + key: const Key("tokenSendViewAddressFieldKey"), + controller: sendToController, + readOnly: false, + autocorrect: false, + enableSuggestions: false, + toolbarOptions: const ToolbarOptions( + copy: false, + cut: false, + paste: true, + selectAll: false, + ), + onChanged: (newValue) { + _address = newValue.trim(); + _updatePreviewButtonState( + _address, _amountToSend); + + setState(() { + _addressToggleFlag = newValue.isNotEmpty; + }); + }, + focusNode: _addressFocusNode, + style: STextStyles.field(context), + decoration: standardInputDecoration( + "Enter ${tokenContract.symbol} address", + _addressFocusNode, + context, + ).copyWith( + contentPadding: const EdgeInsets.only( + left: 16, + top: 6, + bottom: 8, + right: 5, + ), + suffixIcon: Padding( + padding: sendToController.text.isEmpty + ? const EdgeInsets.only(right: 8) + : const EdgeInsets.only(right: 0), + child: UnconstrainedBox( + child: Row( + mainAxisAlignment: + MainAxisAlignment.spaceAround, + children: [ + _addressToggleFlag + ? TextFieldIconButton( + key: const Key( + "tokenSendViewClearAddressFieldButtonKey"), + onTap: () { + sendToController.text = ""; + _address = ""; + _updatePreviewButtonState( + _address, _amountToSend); + setState(() { + _addressToggleFlag = false; + }); + }, + child: const XIcon(), + ) + : TextFieldIconButton( + key: const Key( + "tokenSendViewPasteAddressFieldButtonKey"), + onTap: + _onTokenSendViewPasteAddressFieldButtonPressed, + child: sendToController + .text.isEmpty + ? const ClipboardIcon() + : const XIcon(), + ), + if (sendToController.text.isEmpty) + TextFieldIconButton( + key: const Key( + "sendViewAddressBookButtonKey"), + onTap: () { + Navigator.of(context).pushNamed( + AddressBookView.routeName, + arguments: widget.coin, + ); + }, + child: const AddressBookIcon(), + ), + if (sendToController.text.isEmpty) + TextFieldIconButton( + key: const Key( + "sendViewScanQrButtonKey"), + onTap: + _onTokenSendViewScanQrButtonPressed, + child: const QrCodeIcon(), + ) + ], + ), + ), + ), + ), + ), + ), + Builder( + builder: (_) { + final error = _updateInvalidAddressText( + _address ?? "", + ref + .read(walletsChangeNotifierProvider) + .getManager(walletId), + ); + + if (error == null || error.isEmpty) { + return Container(); + } else { + return Align( + alignment: Alignment.topLeft, + child: Padding( + padding: const EdgeInsets.only( + left: 12.0, + top: 4.0, + ), + child: Text( + error, + textAlign: TextAlign.left, + style: + STextStyles.label(context).copyWith( + color: Theme.of(context) + .extension()! + .textError, + ), + ), + ), + ); + } + }, + ), + const SizedBox( + height: 12, + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Amount", + style: STextStyles.smallMed12(context), + textAlign: TextAlign.left, + ), + // CustomTextButton( + // text: "Send all ${tokenContract.symbol}", + // onTap: () async { + // cryptoAmountController.text = ref + // .read(tokenServiceProvider)! + // .balance + // .getSpendable() + // .toStringAsFixed(tokenContract.decimals); + // + // _cryptoAmountChanged(); + // }, + // ), + ], + ), + const SizedBox( + height: 8, + ), + TextField( + autocorrect: Util.isDesktop ? false : true, + enableSuggestions: Util.isDesktop ? false : true, + style: STextStyles.smallMed14(context).copyWith( + color: Theme.of(context) + .extension()! + .textDark, + ), + key: + const Key("amountInputFieldCryptoTextFieldKey"), + controller: cryptoAmountController, + focusNode: _cryptoFocus, + keyboardType: Util.isDesktop + ? null + : const TextInputType.numberWithOptions( + signed: false, + decimal: true, + ), + textAlign: TextAlign.right, + inputFormatters: [ + // regex to validate a crypto amount with 8 decimal places + TextInputFormatter.withFunction((oldValue, + newValue) => + RegExp(r'^([0-9]*[,.]?[0-9]{0,8}|[,.][0-9]{0,8})$') + .hasMatch(newValue.text) + ? newValue + : oldValue), + ], + decoration: InputDecoration( + contentPadding: const EdgeInsets.only( + top: 12, + right: 12, + ), + hintText: "0", + hintStyle: + STextStyles.fieldLabel(context).copyWith( + fontSize: 14, + ), + prefixIcon: FittedBox( + fit: BoxFit.scaleDown, + child: Padding( + padding: const EdgeInsets.all(12), + child: Text( + tokenContract.symbol, + style: STextStyles.smallMed14(context) + .copyWith( + color: Theme.of(context) + .extension()! + .accentColorDark), + ), + ), + ), + ), + ), + if (Prefs.instance.externalCalls) + const SizedBox( + height: 8, + ), + if (Prefs.instance.externalCalls) + TextField( + autocorrect: Util.isDesktop ? false : true, + enableSuggestions: Util.isDesktop ? false : true, + style: STextStyles.smallMed14(context).copyWith( + color: Theme.of(context) + .extension()! + .textDark, + ), + key: + const Key("amountInputFieldFiatTextFieldKey"), + controller: baseAmountController, + focusNode: _baseFocus, + keyboardType: Util.isDesktop + ? null + : const TextInputType.numberWithOptions( + signed: false, + decimal: true, + ), + textAlign: TextAlign.right, + inputFormatters: [ + // regex to validate a fiat amount with 2 decimal places + TextInputFormatter.withFunction((oldValue, + newValue) => + RegExp(r'^([0-9]*[,.]?[0-9]{0,2}|[,.][0-9]{0,2})$') + .hasMatch(newValue.text) + ? newValue + : oldValue), + ], + onChanged: _onFiatAmountFieldChanged, + decoration: InputDecoration( + contentPadding: const EdgeInsets.only( + top: 12, + right: 12, + ), + hintText: "0", + hintStyle: + STextStyles.fieldLabel(context).copyWith( + fontSize: 14, + ), + prefixIcon: FittedBox( + fit: BoxFit.scaleDown, + child: Padding( + padding: const EdgeInsets.all(12), + child: Text( + ref.watch(prefsChangeNotifierProvider + .select((value) => value.currency)), + style: STextStyles.smallMed14(context) + .copyWith( + color: Theme.of(context) + .extension()! + .accentColorDark), + ), + ), + ), + ), + ), + const SizedBox( + height: 12, + ), + Text( + "Note (optional)", + style: STextStyles.smallMed12(context), + textAlign: TextAlign.left, + ), + const SizedBox( + height: 8, + ), + ClipRRect( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + child: TextField( + autocorrect: Util.isDesktop ? false : true, + enableSuggestions: Util.isDesktop ? false : true, + controller: noteController, + focusNode: _noteFocusNode, + style: STextStyles.field(context), + onChanged: (_) => setState(() {}), + decoration: standardInputDecoration( + "Type something...", + _noteFocusNode, + context, + ).copyWith( + suffixIcon: noteController.text.isNotEmpty + ? Padding( + padding: + const EdgeInsets.only(right: 0), + child: UnconstrainedBox( + child: Row( + children: [ + TextFieldIconButton( + child: const XIcon(), + onTap: () async { + setState(() { + noteController.text = ""; + }); + }, + ), + ], + ), + ), + ) + : null, + ), + ), + ), + const SizedBox( + height: 12, + ), + if (coin != Coin.epicCash) + Text( + "Transaction fee (estimated)", + style: STextStyles.smallMed12(context), + textAlign: TextAlign.left, + ), + const SizedBox( + height: 8, + ), + Stack( + children: [ + TextField( + autocorrect: Util.isDesktop ? false : true, + enableSuggestions: + Util.isDesktop ? false : true, + controller: feeController, + readOnly: true, + textInputAction: TextInputAction.none, + ), + Padding( + padding: const EdgeInsets.symmetric( + horizontal: 12, + ), + child: RawMaterialButton( + splashColor: Theme.of(context) + .extension()! + .highlight, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + ), + onPressed: () { + showModalBottomSheet( + backgroundColor: Colors.transparent, + context: context, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.vertical( + top: Radius.circular(20), + ), + ), + builder: (_) => + TransactionFeeSelectionSheet( + walletId: walletId, + isToken: true, + amount: (Decimal.tryParse( + cryptoAmountController + .text) ?? + Decimal.zero) + .toAmount( + fractionDigits: + tokenContract.decimals, + ), + updateChosen: (String fee) { + setState(() { + _calculateFeesFuture = + Future(() => fee); + }); + }, + ), + ); + }, + child: Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + Text( + ref + .watch(feeRateTypeStateProvider + .state) + .state + .prettyName, + style: STextStyles.itemSubtitle12( + context), + ), + const SizedBox( + width: 10, + ), + FutureBuilder( + future: _calculateFeesFuture, + builder: (context, snapshot) { + if (snapshot.connectionState == + ConnectionState.done && + snapshot.hasData) { + return Text( + "~${snapshot.data! as String} ${coin.ticker}", + style: + STextStyles.itemSubtitle( + context), + ); + } else { + return AnimatedText( + stringsToLoopThrough: const [ + "Calculating", + "Calculating.", + "Calculating..", + "Calculating...", + ], + style: + STextStyles.itemSubtitle( + context), + ); + } + }, + ), + ], + ), + SvgPicture.asset( + Assets.svg.chevronDown, + width: 8, + height: 4, + color: Theme.of(context) + .extension()! + .textSubtitle2, + ), + ], + ), + ), + ) + ], + ), + const Spacer(), + const SizedBox( + height: 12, + ), + TextButton( + onPressed: ref + .watch(previewTxButtonStateProvider.state) + .state + ? _previewTransaction + : null, + style: ref + .watch(previewTxButtonStateProvider.state) + .state + ? Theme.of(context) + .extension()! + .getPrimaryEnabledButtonStyle(context) + : Theme.of(context) + .extension()! + .getPrimaryDisabledButtonStyle(context), + child: Text( + "Preview", + style: STextStyles.button(context), + ), + ), + const SizedBox( + height: 4, + ), + ], + ), + ), + ), + ), + ), + ); + }, + ), + ), + ); + } +} diff --git a/lib/pages/settings_views/global_settings_view/hidden_settings.dart b/lib/pages/settings_views/global_settings_view/hidden_settings.dart index 55afd845e..d0f63db3f 100644 --- a/lib/pages/settings_views/global_settings_view/hidden_settings.dart +++ b/lib/pages/settings_views/global_settings_view/hidden_settings.dart @@ -5,7 +5,6 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:stackwallet/notifications/show_flush_bar.dart'; import 'package:stackwallet/providers/global/debug_service_provider.dart'; import 'package:stackwallet/providers/providers.dart'; -import 'package:stackwallet/services/exchange/majestic_bank/majestic_bank_api.dart'; import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; @@ -164,9 +163,59 @@ class HiddenSettings extends StatelessWidget { Consumer(builder: (_, ref, __) { return GestureDetector( onTap: () async { - final x = await MajesticBankAPI.instance - .getLimit(fromCurrency: 'btc'); - print(x); + ref + .read(priceAnd24hChangeNotifierProvider) + .tokenContractAddressesToCheck + .add( + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"); + ref + .read(priceAnd24hChangeNotifierProvider) + .tokenContractAddressesToCheck + .add( + "0xdAC17F958D2ee523a2206206994597C13D831ec7"); + await ref + .read(priceAnd24hChangeNotifierProvider) + .updatePrice(); + + final x = ref + .read(priceAnd24hChangeNotifierProvider) + .getTokenPrice( + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"); + + print( + "PRICE 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48: $x"); + }, + child: RoundedWhiteContainer( + child: Text( + "Click me", + style: STextStyles.button(context).copyWith( + color: Theme.of(context) + .extension()! + .accentColorDark), + ), + ), + ); + }), + const SizedBox( + height: 12, + ), + Consumer(builder: (_, ref, __) { + return GestureDetector( + onTap: () async { + // final erc20 = Erc20ContractInfo( + // contractAddress: 'some con', + // name: "loonamsn", + // symbol: "DD", + // decimals: 19, + // ); + // + // final json = erc20.toJson(); + // + // print(json); + // + // final ee = EthContractInfo.fromJson(json); + // + // print(ee); }, child: RoundedWhiteContainer( child: Text( diff --git a/lib/pages/settings_views/global_settings_view/manage_nodes_views/add_edit_node_view.dart b/lib/pages/settings_views/global_settings_view/manage_nodes_views/add_edit_node_view.dart index 1dd34f330..6afe7ff7f 100644 --- a/lib/pages/settings_views/global_settings_view/manage_nodes_views/add_edit_node_view.dart +++ b/lib/pages/settings_views/global_settings_view/manage_nodes_views/add_edit_node_view.dart @@ -3,7 +3,7 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:flutter_svg/svg.dart'; +import 'package:flutter_svg/flutter_svg.dart'; import 'package:stackwallet/electrumx_rpc/electrumx.dart'; import 'package:stackwallet/models/node_model.dart'; import 'package:stackwallet/notifications/show_flush_bar.dart'; @@ -30,6 +30,7 @@ import 'package:stackwallet/widgets/stack_dialog.dart'; import 'package:stackwallet/widgets/stack_text_field.dart'; import 'package:stackwallet/widgets/textfield_icon_button.dart'; import 'package:uuid/uuid.dart'; +// import 'package:web3dart/web3dart.dart'; enum AddEditNodeViewType { add, edit } @@ -172,6 +173,14 @@ class _AddEditNodeViewState extends ConsumerState { } break; + + case Coin.ethereum: + // final client = Web3Client( + // "https://mainnet.infura.io/v3/22677300bf774e49a458b73313ee56ba", + // Client()); + try { + // await client.getSyncStatus(); + } catch (_) {} } if (showFlushBar && mounted) { @@ -710,6 +719,7 @@ class _NodeFormState extends ConsumerState { case Coin.epicCash: return false; + case Coin.ethereum: case Coin.monero: case Coin.wownero: return true; diff --git a/lib/pages/settings_views/global_settings_view/stack_backup_views/helpers/restore_create_backup.dart b/lib/pages/settings_views/global_settings_view/stack_backup_views/helpers/restore_create_backup.dart index 6521dec0f..28a926663 100644 --- a/lib/pages/settings_views/global_settings_view/stack_backup_views/helpers/restore_create_backup.dart +++ b/lib/pages/settings_views/global_settings_view/stack_backup_views/helpers/restore_create_backup.dart @@ -4,7 +4,7 @@ import 'dart:io'; import 'dart:typed_data'; import 'package:stack_wallet_backup/stack_wallet_backup.dart'; -import 'package:stackwallet/hive/db.dart'; +import 'package:stackwallet/db/hive/db.dart'; import 'package:stackwallet/models/contact.dart'; import 'package:stackwallet/models/contact_address_entry.dart'; import 'package:stackwallet/models/exchange/change_now/exchange_transaction.dart'; diff --git a/lib/pages/settings_views/global_settings_view/syncing_preferences_views/wallet_syncing_options_view.dart b/lib/pages/settings_views/global_settings_view/syncing_preferences_views/wallet_syncing_options_view.dart index 9c398fbf6..edbc9a448 100644 --- a/lib/pages/settings_views/global_settings_view/syncing_preferences_views/wallet_syncing_options_view.dart +++ b/lib/pages/settings_views/global_settings_view/syncing_preferences_views/wallet_syncing_options_view.dart @@ -1,4 +1,3 @@ -import 'package:decimal/decimal.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/svg.dart'; @@ -7,11 +6,9 @@ import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:stackwallet/utilities/enums/sync_type_enum.dart'; -import 'package:stackwallet/utilities/format.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; import 'package:stackwallet/utilities/util.dart'; -import 'package:stackwallet/widgets/animated_text.dart'; import 'package:stackwallet/widgets/background.dart'; import 'package:stackwallet/widgets/conditional_parent.dart'; import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; @@ -144,40 +141,18 @@ class WalletSyncingOptionsView extends ConsumerWidget { const SizedBox( height: 2, ), - FutureBuilder( - future: Future( - () => manager.balance.getTotal()), - builder: (builderContext, - AsyncSnapshot snapshot) { - if (snapshot.connectionState == - ConnectionState.done && - snapshot.hasData) { - return Text( - "${Format.localizedStringAsFixed( - value: snapshot.data!, - locale: ref.watch( - localeServiceChangeNotifierProvider - .select((value) => - value.locale)), - decimalPlaces: 8, - )} ${manager.coin.ticker}", - style: STextStyles.itemSubtitle( - context), - ); - } else { - return AnimatedText( - stringsToLoopThrough: const [ - "Loading balance", - "Loading balance.", - "Loading balance..", - "Loading balance..." - ], - style: STextStyles.itemSubtitle( - context), - ); - } - }, - ), + Text( + "${manager.balance.total.localizedStringAsFixed( + locale: ref.watch( + localeServiceChangeNotifierProvider + .select( + (value) => value.locale, + ), + ), + )} ${manager.coin.ticker}", + style: + STextStyles.itemSubtitle(context), + ) ], ), const Spacer(), diff --git a/lib/pages/stack_privacy_calls.dart b/lib/pages/stack_privacy_calls.dart index e3b0534d1..27ea3aa80 100644 --- a/lib/pages/stack_privacy_calls.dart +++ b/lib/pages/stack_privacy_calls.dart @@ -3,7 +3,7 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/svg.dart'; -import 'package:stackwallet/hive/db.dart'; +import 'package:stackwallet/db/hive/db.dart'; import 'package:stackwallet/pages/pinpad_views/create_pin_view.dart'; import 'package:stackwallet/pages_desktop_specific/password/create_password_view.dart'; import 'package:stackwallet/providers/global/prefs_provider.dart'; diff --git a/lib/pages/token_view/my_tokens_view.dart b/lib/pages/token_view/my_tokens_view.dart new file mode 100644 index 000000000..78fa352d0 --- /dev/null +++ b/lib/pages/token_view/my_tokens_view.dart @@ -0,0 +1,237 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_svg/svg.dart'; +import 'package:stackwallet/pages/add_wallet_views/add_token_view/edit_wallet_tokens_view.dart'; +import 'package:stackwallet/pages/token_view/sub_widgets/my_tokens_list.dart'; +import 'package:stackwallet/providers/global/wallets_provider.dart'; +import 'package:stackwallet/services/coins/ethereum/ethereum_wallet.dart'; +import 'package:stackwallet/utilities/assets.dart'; +import 'package:stackwallet/utilities/constants.dart'; +import 'package:stackwallet/utilities/text_styles.dart'; +import 'package:stackwallet/utilities/theme/stack_colors.dart'; +import 'package:stackwallet/utilities/util.dart'; +import 'package:stackwallet/widgets/background.dart'; +import 'package:stackwallet/widgets/conditional_parent.dart'; +import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; +import 'package:stackwallet/widgets/icon_widgets/x_icon.dart'; +import 'package:stackwallet/widgets/stack_text_field.dart'; +import 'package:stackwallet/widgets/textfield_icon_button.dart'; + +class MyTokensView extends ConsumerStatefulWidget { + const MyTokensView({ + Key? key, + required this.walletId, + }) : super(key: key); + + static const String routeName = "/myTokens"; + final String walletId; + + @override + ConsumerState createState() => _MyTokensViewState(); +} + +class _MyTokensViewState extends ConsumerState { + final bool isDesktop = Util.isDesktop; + + late final String walletAddress; + late final TextEditingController _searchController; + final searchFieldFocusNode = FocusNode(); + String _searchString = ""; + + @override + void initState() { + _searchController = TextEditingController(); + super.initState(); + } + + @override + void dispose() { + _searchController.dispose(); + searchFieldFocusNode.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + debugPrint("BUILD: $runtimeType"); + + return ConditionalParent( + condition: !isDesktop, + builder: (child) => Background( + child: Scaffold( + backgroundColor: + Theme.of(context).extension()!.background, + appBar: AppBar( + backgroundColor: + Theme.of(context).extension()!.background, + leading: AppBarBackButton( + onPressed: () async { + if (FocusScope.of(context).hasFocus) { + FocusScope.of(context).unfocus(); + await Future.delayed(const Duration(milliseconds: 75)); + } + if (mounted) { + Navigator.of(context).pop(); + } + }, + ), + title: Text( + "${ref.watch( + walletsChangeNotifierProvider.select( + (value) => value.getManager(widget.walletId).walletName), + )} Tokens", + style: STextStyles.navBarTitle(context), + ), + actions: [ + Padding( + padding: const EdgeInsets.only( + top: 10, + bottom: 10, + right: 20, + ), + child: AspectRatio( + aspectRatio: 1, + child: AppBarIconButton( + key: const Key("addTokenAppBarIconButtonKey"), + size: 36, + shadows: const [], + color: + Theme.of(context).extension()!.background, + icon: SvgPicture.asset( + Assets.svg.circlePlusDark, + color: Theme.of(context) + .extension()! + .accentColorDark, + width: 20, + height: 20, + ), + onPressed: () async { + final result = await Navigator.of(context).pushNamed( + EditWalletTokensView.routeName, + arguments: widget.walletId, + ); + + if (mounted && result == 42) { + setState(() {}); + } + }, + ), + ), + ), + ], + ), + body: Padding( + padding: const EdgeInsets.only( + left: 12, + top: 12, + right: 12, + ), + child: child, + ), + ), + ), + child: Column( + children: [ + Padding( + padding: EdgeInsets.all(isDesktop ? 0 : 4), + child: Row( + children: [ + ConditionalParent( + condition: isDesktop, + builder: (child) => Expanded( + child: child, + ), + child: ConditionalParent( + condition: !isDesktop, + builder: (child) => Expanded( + child: child, + ), + child: ClipRRect( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + child: TextField( + autocorrect: !isDesktop, + enableSuggestions: !isDesktop, + controller: _searchController, + focusNode: searchFieldFocusNode, + onChanged: (value) { + setState(() { + _searchString = value; + }); + }, + style: isDesktop + ? STextStyles.desktopTextExtraSmall(context) + .copyWith( + color: Theme.of(context) + .extension()! + .textFieldActiveText, + height: 1.8, + ) + : STextStyles.field(context), + decoration: standardInputDecoration( + "Search...", + searchFieldFocusNode, + context, + desktopMed: isDesktop, + ).copyWith( + prefixIcon: Padding( + padding: EdgeInsets.symmetric( + horizontal: isDesktop ? 12 : 10, + vertical: isDesktop ? 18 : 16, + ), + child: SvgPicture.asset( + Assets.svg.search, + width: isDesktop ? 20 : 16, + height: isDesktop ? 20 : 16, + ), + ), + suffixIcon: _searchController.text.isNotEmpty + ? Padding( + padding: const EdgeInsets.only(right: 0), + child: UnconstrainedBox( + child: Row( + children: [ + TextFieldIconButton( + child: const XIcon(), + onTap: () async { + setState(() { + _searchController.text = ""; + _searchString = ""; + }); + }, + ), + ], + ), + ), + ) + : null, + ), + ), + ), + ), + ), + ], + ), + ), + const SizedBox( + height: 8, + ), + Expanded( + child: MyTokensList( + walletId: widget.walletId, + searchTerm: _searchString, + tokenContracts: ref + .watch(walletsChangeNotifierProvider.select((value) => value + .getManager(widget.walletId) + .wallet as EthereumWallet)) + .getWalletTokenContractAddresses(), + ), + ), + ], + ), + ); + } +} diff --git a/lib/pages/token_view/sub_widgets/my_token_select_item.dart b/lib/pages/token_view/sub_widgets/my_token_select_item.dart new file mode 100644 index 000000000..f6a327f52 --- /dev/null +++ b/lib/pages/token_view/sub_widgets/my_token_select_item.dart @@ -0,0 +1,182 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:stackwallet/models/isar/models/ethereum/eth_contract.dart'; +import 'package:stackwallet/pages/token_view/token_view.dart'; +import 'package:stackwallet/pages_desktop_specific/my_stack_view/wallet_view/desktop_token_view.dart'; +import 'package:stackwallet/providers/global/secure_store_provider.dart'; +import 'package:stackwallet/providers/providers.dart'; +import 'package:stackwallet/services/coins/ethereum/ethereum_wallet.dart'; +import 'package:stackwallet/services/ethereum/cached_eth_token_balance.dart'; +import 'package:stackwallet/services/ethereum/ethereum_token_service.dart'; +import 'package:stackwallet/services/transaction_notification_tracker.dart'; +import 'package:stackwallet/utilities/constants.dart'; +import 'package:stackwallet/utilities/show_loading.dart'; +import 'package:stackwallet/utilities/text_styles.dart'; +import 'package:stackwallet/utilities/util.dart'; +import 'package:stackwallet/widgets/icon_widgets/eth_token_icon.dart'; +import 'package:stackwallet/widgets/rounded_white_container.dart'; + +class MyTokenSelectItem extends ConsumerStatefulWidget { + const MyTokenSelectItem({ + Key? key, + required this.walletId, + required this.token, + }) : super(key: key); + + final String walletId; + final EthContract token; + + @override + ConsumerState createState() => _MyTokenSelectItemState(); +} + +class _MyTokenSelectItemState extends ConsumerState { + final bool isDesktop = Util.isDesktop; + + late final CachedEthTokenBalance cachedBalance; + + void _onPressed() async { + ref.read(tokenServiceStateProvider.state).state = EthTokenWallet( + token: widget.token, + secureStore: ref.read(secureStoreProvider), + ethWallet: ref + .read(walletsChangeNotifierProvider) + .getManager(widget.walletId) + .wallet as EthereumWallet, + tracker: TransactionNotificationTracker( + walletId: widget.walletId, + ), + ); + + await showLoading( + whileFuture: ref.read(tokenServiceProvider)!.initialize(), + context: context, + isDesktop: isDesktop, + message: "Loading ${widget.token.name}", + ); + + if (mounted) { + await Navigator.of(context).pushNamed( + isDesktop ? DesktopTokenView.routeName : TokenView.routeName, + arguments: widget.walletId, + ); + } + } + + @override + void initState() { + cachedBalance = CachedEthTokenBalance(widget.walletId, widget.token); + + WidgetsBinding.instance.addPostFrameCallback((_) async { + final address = await ref + .read(walletsChangeNotifierProvider) + .getManager(widget.walletId) + .currentReceivingAddress; + await cachedBalance.fetchAndUpdateCachedBalance(address); + if (mounted) { + setState(() {}); + } + }); + + super.initState(); + } + + @override + Widget build(BuildContext context) { + return RoundedWhiteContainer( + padding: const EdgeInsets.all(0), + child: MaterialButton( + key: Key("walletListItemButtonKey_${widget.token.symbol}"), + padding: isDesktop + ? const EdgeInsets.symmetric(horizontal: 28, vertical: 24) + : const EdgeInsets.symmetric(horizontal: 12, vertical: 13), + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + shape: RoundedRectangleBorder( + borderRadius: + BorderRadius.circular(Constants.size.circularBorderRadius), + ), + onPressed: _onPressed, + child: Row( + children: [ + EthTokenIcon( + contractAddress: widget.token.address, + size: isDesktop ? 32 : 28, + ), + SizedBox( + width: isDesktop ? 12 : 10, + ), + Expanded( + child: Consumer( + builder: (_, ref, __) { + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Row( + children: [ + Text( + widget.token.name, + style: isDesktop + ? STextStyles.desktopTextExtraSmall(context) + : STextStyles.titleBold12(context), + ), + const Spacer(), + Text( + "${cachedBalance.getCachedBalance().total.localizedStringAsFixed( + locale: ref.watch( + localeServiceChangeNotifierProvider.select( + (value) => value.locale, + ), + ), + )} " + "${widget.token.symbol}", + style: isDesktop + ? STextStyles.desktopTextExtraSmall(context) + : STextStyles.itemSubtitle(context), + ), + ], + ), + const SizedBox( + height: 2, + ), + Row( + children: [ + Text( + widget.token.symbol, + style: isDesktop + ? STextStyles.desktopTextExtraExtraSmall( + context) + : STextStyles.itemSubtitle(context), + ), + const Spacer(), + Text( + "${ref.watch( + priceAnd24hChangeNotifierProvider.select( + (value) => value + .getTokenPrice(widget.token.address) + .item1 + .toStringAsFixed(2), + ), + )} " + "${ref.watch( + prefsChangeNotifierProvider.select( + (value) => value.currency, + ), + )}", + style: isDesktop + ? STextStyles.desktopTextExtraExtraSmall( + context) + : STextStyles.itemSubtitle(context), + ), + ], + ), + ], + ); + }, + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/pages/token_view/sub_widgets/my_tokens_list.dart b/lib/pages/token_view/sub_widgets/my_tokens_list.dart new file mode 100644 index 000000000..659ed5221 --- /dev/null +++ b/lib/pages/token_view/sub_widgets/my_tokens_list.dart @@ -0,0 +1,80 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:isar/isar.dart'; +import 'package:stackwallet/db/isar/main_db.dart'; +import 'package:stackwallet/models/isar/models/ethereum/eth_contract.dart'; +import 'package:stackwallet/pages/token_view/sub_widgets/my_token_select_item.dart'; +import 'package:stackwallet/utilities/util.dart'; + +class MyTokensList extends StatelessWidget { + const MyTokensList({ + Key? key, + required this.walletId, + required this.searchTerm, + required this.tokenContracts, + }) : super(key: key); + + final String walletId; + final String searchTerm; + final List tokenContracts; + + List _filter(String searchTerm) { + if (tokenContracts.isEmpty) { + return []; + } + + if (searchTerm.isNotEmpty) { + final term = searchTerm.toLowerCase(); + return MainDB.instance + .getEthContracts() + .filter() + .anyOf( + tokenContracts, (q, e) => q.addressEqualTo(e)) + .and() + .group( + (q) => q + .nameContains(term, caseSensitive: false) + .or() + .symbolContains(term, caseSensitive: false) + .or() + .addressContains(term, caseSensitive: false), + ) + .findAllSync(); + // return tokens.toList(); + } + //implement search/filter + return MainDB.instance + .getEthContracts() + .filter() + .anyOf( + tokenContracts, (q, e) => q.addressEqualTo(e)) + .findAllSync(); + } + + @override + Widget build(BuildContext context) { + final bool isDesktop = Util.isDesktop; + + return Consumer( + builder: (_, ref, __) { + final tokens = _filter(searchTerm); + return ListView.builder( + itemCount: tokens.length, + itemBuilder: (ctx, index) { + final token = tokens[index]; + return Padding( + key: Key(token.address), + padding: isDesktop + ? const EdgeInsets.symmetric(vertical: 5) + : const EdgeInsets.all(4), + child: MyTokenSelectItem( + walletId: walletId, + token: token, + ), + ); + }, + ); + }, + ); + } +} diff --git a/lib/pages/token_view/sub_widgets/no_tokens_found.dart b/lib/pages/token_view/sub_widgets/no_tokens_found.dart new file mode 100644 index 000000000..4fbfff50e --- /dev/null +++ b/lib/pages/token_view/sub_widgets/no_tokens_found.dart @@ -0,0 +1,25 @@ +import 'package:flutter/cupertino.dart'; +import 'package:stackwallet/utilities/text_styles.dart'; +import 'package:stackwallet/widgets/rounded_white_container.dart'; + +class NoTokensFound extends StatelessWidget { + const NoTokensFound({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + RoundedWhiteContainer( + child: FittedBox( + fit: BoxFit.scaleDown, + child: Text( + "You do not have any tokens", + style: STextStyles.itemSubtitle(context), + ), + ), + ), + ], + ); + } +} diff --git a/lib/pages/token_view/sub_widgets/token_summary.dart b/lib/pages/token_view/sub_widgets/token_summary.dart new file mode 100644 index 000000000..2d9db8b62 --- /dev/null +++ b/lib/pages/token_view/sub_widgets/token_summary.dart @@ -0,0 +1,316 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_svg/svg.dart'; +import 'package:stackwallet/models/isar/models/ethereum/eth_contract.dart'; +import 'package:stackwallet/pages/buy_view/buy_in_wallet_view.dart'; +import 'package:stackwallet/pages/exchange_view/wallet_initiated_exchange_view.dart'; +import 'package:stackwallet/pages/receive_view/receive_view.dart'; +import 'package:stackwallet/pages/send_view/token_send_view.dart'; +import 'package:stackwallet/pages/token_view/token_view.dart'; +import 'package:stackwallet/pages/wallet_view/sub_widgets/wallet_refresh_button.dart'; +import 'package:stackwallet/providers/global/locale_provider.dart'; +import 'package:stackwallet/providers/global/prefs_provider.dart'; +import 'package:stackwallet/providers/global/price_provider.dart'; +import 'package:stackwallet/providers/global/wallets_provider.dart'; +import 'package:stackwallet/services/event_bus/events/global/wallet_sync_status_changed_event.dart'; +import 'package:stackwallet/utilities/amount/amount.dart'; +import 'package:stackwallet/utilities/assets.dart'; +import 'package:stackwallet/utilities/constants.dart'; +import 'package:stackwallet/utilities/enums/coin_enum.dart'; +import 'package:stackwallet/utilities/text_styles.dart'; +import 'package:stackwallet/utilities/theme/stack_colors.dart'; +import 'package:stackwallet/widgets/rounded_container.dart'; +import 'package:tuple/tuple.dart'; + +class TokenSummary extends ConsumerWidget { + const TokenSummary({ + Key? key, + required this.walletId, + required this.initialSyncStatus, + }) : super(key: key); + + final String walletId; + final WalletSyncStatus initialSyncStatus; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final token = + ref.watch(tokenServiceProvider.select((value) => value!.tokenContract)); + final balance = + ref.watch(tokenServiceProvider.select((value) => value!.balance)); + + return Stack( + children: [ + RoundedContainer( + color: const Color(0xFFE9EAFF), // todo: fix color + // color: Theme.of(context).extension()!., + padding: const EdgeInsets.all(24), + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + SvgPicture.asset( + Assets.svg.walletDesktop, + color: const Color(0xFF8488AB), // todo: fix color + width: 12, + height: 12, + ), + const SizedBox( + width: 6, + ), + Text( + ref.watch( + walletsChangeNotifierProvider.select( + (value) => value.getManager(walletId).walletName, + ), + ), + style: STextStyles.w500_12(context).copyWith( + color: const Color(0xFF8488AB), // todo: fix color + ), + ), + ], + ), + const SizedBox( + height: 6, + ), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + "${balance.total.localizedStringAsFixed( + locale: ref.watch( + localeServiceChangeNotifierProvider.select( + (value) => value.locale, + ), + ), + )}" + " ${token.symbol}", + style: STextStyles.pageTitleH1(context), + ), + const SizedBox( + width: 10, + ), + CoinTickerTag( + walletId: walletId, + ), + ], + ), + const SizedBox( + height: 6, + ), + Text( + "${(balance.total.decimal * ref.watch( + priceAnd24hChangeNotifierProvider.select( + (value) => value.getTokenPrice(token.address).item1, + ), + )).toAmount( + fractionDigits: 2, + ).localizedStringAsFixed( + locale: ref.watch( + localeServiceChangeNotifierProvider.select( + (value) => value.locale, + ), + ), + )} ${ref.watch( + prefsChangeNotifierProvider.select( + (value) => value.currency, + ), + )}", + style: STextStyles.subtitle500(context), + ), + const SizedBox( + height: 20, + ), + TokenWalletOptions( + walletId: walletId, + tokenContract: token, + ), + ], + ), + ), + Positioned( + top: 10, + right: 10, + child: WalletRefreshButton( + walletId: walletId, + initialSyncStatus: initialSyncStatus, + tokenContractAddress: ref.watch(tokenServiceProvider + .select((value) => value!.tokenContract.address)), + ), + ), + ], + ); + } +} + +class TokenWalletOptions extends StatelessWidget { + const TokenWalletOptions({ + Key? key, + required this.walletId, + required this.tokenContract, + }) : super(key: key); + + final String walletId; + final EthContract tokenContract; + + void _onExchangePressed(BuildContext context) async { + unawaited( + Navigator.of(context).pushNamed( + WalletInitiatedExchangeView.routeName, + arguments: Tuple3( + walletId, + Coin.ethereum, + tokenContract, + ), + ), + ); + } + + void _onBuyPressed(BuildContext context) { + unawaited( + Navigator.of(context).pushNamed( + BuyInWalletView.routeName, + arguments: Tuple2( + Coin.ethereum, + tokenContract, + ), + ), + ); + } + + @override + Widget build(BuildContext context) { + return Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + TokenOptionsButton( + onPressed: () { + Navigator.of(context).pushNamed( + ReceiveView.routeName, + arguments: Tuple2( + walletId, + Coin.ethereum, + ), + ); + }, + subLabel: "Receive", + iconAssetSVG: Assets.svg.receive(context), + ), + const SizedBox( + width: 16, + ), + TokenOptionsButton( + onPressed: () { + Navigator.of(context).pushNamed( + TokenSendView.routeName, + arguments: Tuple3( + walletId, + Coin.ethereum, + tokenContract, + ), + ); + }, + subLabel: "Send", + iconAssetSVG: Assets.svg.send(context), + ), + const SizedBox( + width: 16, + ), + TokenOptionsButton( + onPressed: () => _onExchangePressed(context), + subLabel: "Swap", + iconAssetSVG: Assets.svg.exchange(context), + ), + const SizedBox( + width: 16, + ), + TokenOptionsButton( + onPressed: () => _onBuyPressed(context), + subLabel: "Buy", + iconAssetSVG: Assets.svg.creditCard, + ), + ], + ); + } +} + +class TokenOptionsButton extends StatelessWidget { + const TokenOptionsButton({ + Key? key, + required this.onPressed, + required this.subLabel, + required this.iconAssetSVG, + }) : super(key: key); + + final VoidCallback onPressed; + final String subLabel; + final String iconAssetSVG; + + @override + Widget build(BuildContext context) { + return Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + RawMaterialButton( + fillColor: Theme.of(context).extension()!.popupBG, + elevation: 0, + focusElevation: 0, + hoverElevation: 0, + highlightElevation: 0, + constraints: const BoxConstraints(), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + ), + onPressed: onPressed, + child: Padding( + padding: const EdgeInsets.all(10), + child: SvgPicture.asset( + iconAssetSVG, + color: const Color(0xFF424A97), // todo: fix color + width: 24, + height: 24, + ), + ), + ), + const SizedBox( + height: 6, + ), + Text( + subLabel, + style: STextStyles.w500_12(context), + ) + ], + ); + } +} + +class CoinTickerTag extends ConsumerWidget { + const CoinTickerTag({ + Key? key, + required this.walletId, + }) : super(key: key); + + final String walletId; + + @override + Widget build(BuildContext context, WidgetRef ref) { + return RoundedContainer( + padding: const EdgeInsets.symmetric(vertical: 2, horizontal: 4), + radiusMultiplier: 0.25, + color: const Color(0xFF4D5798), // TODO: color theme for multi themes + child: Text( + ref.watch(walletsChangeNotifierProvider + .select((value) => value.getManager(walletId).coin.ticker)), + style: STextStyles.w600_12(context).copyWith( + color: Colors.white, // TODO: design is wrong? + ), + ), + ); + } +} diff --git a/lib/pages/token_view/sub_widgets/token_transaction_list_widget.dart b/lib/pages/token_view/sub_widgets/token_transaction_list_widget.dart new file mode 100644 index 000000000..1db04f592 --- /dev/null +++ b/lib/pages/token_view/sub_widgets/token_transaction_list_widget.dart @@ -0,0 +1,292 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:stackwallet/models/isar/models/isar_models.dart'; +import 'package:stackwallet/pages/exchange_view/trade_details_view.dart'; +import 'package:stackwallet/pages/token_view/token_view.dart'; +import 'package:stackwallet/pages/wallet_view/sub_widgets/no_transactions_found.dart'; +import 'package:stackwallet/providers/global/trades_service_provider.dart'; +import 'package:stackwallet/providers/global/wallets_provider.dart'; +import 'package:stackwallet/route_generator.dart'; +import 'package:stackwallet/utilities/constants.dart'; +import 'package:stackwallet/utilities/enums/coin_enum.dart'; +import 'package:stackwallet/utilities/text_styles.dart'; +import 'package:stackwallet/utilities/theme/stack_colors.dart'; +import 'package:stackwallet/utilities/util.dart'; +import 'package:stackwallet/widgets/desktop/desktop_dialog.dart'; +import 'package:stackwallet/widgets/desktop/desktop_dialog_close_button.dart'; +import 'package:stackwallet/widgets/loading_indicator.dart'; +import 'package:stackwallet/widgets/trade_card.dart'; +import 'package:stackwallet/widgets/transaction_card.dart'; +import 'package:tuple/tuple.dart'; + +class TokenTransactionsList extends ConsumerStatefulWidget { + const TokenTransactionsList({ + Key? key, + required this.walletId, + }) : super(key: key); + + final String walletId; + + @override + ConsumerState createState() => + _TransactionsListState(); +} + +class _TransactionsListState extends ConsumerState { + // + bool _hasLoaded = false; + List _transactions2 = []; + + BorderRadius get _borderRadiusFirst { + return BorderRadius.only( + topLeft: Radius.circular( + Constants.size.circularBorderRadius, + ), + topRight: Radius.circular( + Constants.size.circularBorderRadius, + ), + ); + } + + BorderRadius get _borderRadiusLast { + return BorderRadius.only( + bottomLeft: Radius.circular( + Constants.size.circularBorderRadius, + ), + bottomRight: Radius.circular( + Constants.size.circularBorderRadius, + ), + ); + } + + Widget itemBuilder( + BuildContext context, + Transaction tx, + BorderRadius? radius, + Coin coin, + ) { + final matchingTrades = ref + .read(tradesServiceProvider) + .trades + .where((e) => e.payInTxid == tx.txid || e.payOutTxid == tx.txid); + + if (tx.type == TransactionType.outgoing && matchingTrades.isNotEmpty) { + final trade = matchingTrades.first; + return Container( + decoration: BoxDecoration( + color: Theme.of(context).extension()!.popupBG, + borderRadius: radius, + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + TransactionCard( + // this may mess with combined firo transactions + key: tx.isConfirmed( + ref.watch(walletsChangeNotifierProvider.select((value) => + value.getManager(widget.walletId).currentHeight)), + coin.requiredConfirmations) + ? Key(tx.txid + tx.type.name + tx.address.value.toString()) + : UniqueKey(), // + transaction: tx, + walletId: widget.walletId, + ), + TradeCard( + // this may mess with combined firo transactions + key: Key(tx.txid + + tx.type.name + + tx.address.value.toString() + + trade.uuid), // + trade: trade, + onTap: () async { + final walletName = ref + .read(walletsChangeNotifierProvider) + .getManager(widget.walletId) + .walletName; + if (Util.isDesktop) { + await showDialog( + context: context, + builder: (context) => Navigator( + initialRoute: TradeDetailsView.routeName, + onGenerateRoute: RouteGenerator.generateRoute, + onGenerateInitialRoutes: (_, __) { + return [ + FadePageRoute( + DesktopDialog( + maxHeight: null, + maxWidth: 580, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Padding( + padding: const EdgeInsets.only( + left: 32, + bottom: 16, + ), + child: Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + Text( + "Trade details", + style: STextStyles.desktopH3(context), + ), + DesktopDialogCloseButton( + onPressedOverride: Navigator.of( + context, + rootNavigator: true, + ).pop, + ), + ], + ), + ), + Flexible( + child: TradeDetailsView( + tradeId: trade.tradeId, + transactionIfSentFromStack: tx, + walletName: walletName, + walletId: widget.walletId, + ), + ), + ], + ), + ), + const RouteSettings( + name: TradeDetailsView.routeName, + ), + ), + ]; + }, + ), + ); + } else { + unawaited( + Navigator.of(context).pushNamed( + TradeDetailsView.routeName, + arguments: Tuple4( + trade.tradeId, + tx, + widget.walletId, + walletName, + ), + ), + ); + } + }, + ) + ], + ), + ); + } else { + return Container( + decoration: BoxDecoration( + color: Theme.of(context).extension()!.popupBG, + borderRadius: radius, + ), + child: TransactionCard( + // this may mess with combined firo transactions + key: tx.isConfirmed( + ref.watch(walletsChangeNotifierProvider.select((value) => + value.getManager(widget.walletId).currentHeight)), + coin.requiredConfirmations) + ? Key(tx.txid + tx.type.name + tx.address.value.toString()) + : UniqueKey(), + transaction: tx, + walletId: widget.walletId, + ), + ); + } + } + + @override + Widget build(BuildContext context) { + final manager = ref.watch(walletsChangeNotifierProvider + .select((value) => value.getManager(widget.walletId))); + + return FutureBuilder( + future: ref + .watch(tokenServiceProvider.select((value) => value!.transactions)), + builder: (fbContext, AsyncSnapshot> snapshot) { + if (snapshot.connectionState == ConnectionState.done && + snapshot.hasData) { + _transactions2 = snapshot.data!; + _hasLoaded = true; + } + if (!_hasLoaded) { + return Column( + children: const [ + Spacer(), + Center( + child: LoadingIndicator( + height: 50, + width: 50, + ), + ), + Spacer( + flex: 4, + ), + ], + ); + } + if (_transactions2.isEmpty) { + return const NoTransActionsFound(); + } else { + _transactions2.sort((a, b) => b.timestamp - a.timestamp); + return RefreshIndicator( + onRefresh: () async { + if (!ref.read(tokenServiceProvider)!.isRefreshing) { + unawaited(ref.read(tokenServiceProvider)!.refresh()); + } + }, + child: Util.isDesktop + ? ListView.separated( + itemBuilder: (context, index) { + BorderRadius? radius; + if (_transactions2.length == 1) { + radius = BorderRadius.circular( + Constants.size.circularBorderRadius, + ); + } else if (index == _transactions2.length - 1) { + radius = _borderRadiusLast; + } else if (index == 0) { + radius = _borderRadiusFirst; + } + final tx = _transactions2[index]; + return itemBuilder(context, tx, radius, manager.coin); + }, + separatorBuilder: (context, index) { + return Container( + width: double.infinity, + height: 2, + color: Theme.of(context) + .extension()! + .background, + ); + }, + itemCount: _transactions2.length, + ) + : ListView.builder( + itemCount: _transactions2.length, + itemBuilder: (context, index) { + BorderRadius? radius; + if (_transactions2.length == 1) { + radius = BorderRadius.circular( + Constants.size.circularBorderRadius, + ); + } else if (index == _transactions2.length - 1) { + radius = _borderRadiusLast; + } else if (index == 0) { + radius = _borderRadiusFirst; + } + final tx = _transactions2[index]; + return itemBuilder(context, tx, radius, manager.coin); + }, + ), + ); + } + }, + ); + } +} diff --git a/lib/pages/token_view/token_contract_details_view.dart b/lib/pages/token_view/token_contract_details_view.dart new file mode 100644 index 000000000..c73ea6a75 --- /dev/null +++ b/lib/pages/token_view/token_contract_details_view.dart @@ -0,0 +1,193 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:isar/isar.dart'; +import 'package:stackwallet/db/isar/main_db.dart'; +import 'package:stackwallet/models/isar/models/isar_models.dart'; +import 'package:stackwallet/utilities/text_styles.dart'; +import 'package:stackwallet/utilities/theme/stack_colors.dart'; +import 'package:stackwallet/utilities/util.dart'; +import 'package:stackwallet/widgets/background.dart'; +import 'package:stackwallet/widgets/conditional_parent.dart'; +import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; +import 'package:stackwallet/widgets/custom_buttons/simple_copy_button.dart'; +import 'package:stackwallet/widgets/rounded_white_container.dart'; + +class TokenContractDetailsView extends ConsumerStatefulWidget { + const TokenContractDetailsView({ + Key? key, + required this.contractAddress, + required this.walletId, + }) : super(key: key); + + static const String routeName = "/tokenContractDetailsView"; + + final String contractAddress; + final String walletId; + + @override + ConsumerState createState() => + _TokenContractDetailsViewState(); +} + +class _TokenContractDetailsViewState + extends ConsumerState { + final isDesktop = Util.isDesktop; + + late EthContract contract; + + @override + void initState() { + contract = MainDB.instance.isar.ethContracts + .where() + .addressEqualTo(widget.contractAddress) + .findFirstSync()!; + + super.initState(); + } + + @override + Widget build(BuildContext context) { + return ConditionalParent( + condition: !isDesktop, + builder: (child) => Background( + child: Scaffold( + backgroundColor: + Theme.of(context).extension()!.background, + appBar: AppBar( + backgroundColor: + Theme.of(context).extension()!.backgroundAppBar, + leading: AppBarBackButton( + onPressed: () { + Navigator.of(context).pop(); + }, + ), + titleSpacing: 0, + title: Text( + "Contract details", + style: STextStyles.navBarTitle(context), + ), + ), + body: SafeArea( + child: LayoutBuilder( + builder: (builderContext, constraints) { + return SingleChildScrollView( + child: ConstrainedBox( + constraints: BoxConstraints( + minHeight: constraints.maxHeight, + ), + child: Padding( + padding: const EdgeInsets.all(16), + child: child, + ), + ), + ); + }, + ), + ), + ), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + _Item( + title: "Contract address", + data: contract.address, + button: SimpleCopyButton( + data: contract.address, + ), + ), + const SizedBox( + height: 12, + ), + _Item( + title: "Name", + data: contract.name, + button: SimpleCopyButton( + data: contract.name, + ), + ), + const SizedBox( + height: 12, + ), + _Item( + title: "Symbol", + data: contract.symbol, + button: SimpleCopyButton( + data: contract.symbol, + ), + ), + const SizedBox( + height: 12, + ), + _Item( + title: "Type", + data: contract.type.name, + button: SimpleCopyButton( + data: contract.type.name, + ), + ), + const SizedBox( + height: 12, + ), + _Item( + title: "Decimals", + data: contract.decimals.toString(), + button: SimpleCopyButton( + data: contract.decimals.toString(), + ), + ), + ], + ), + ); + } +} + +class _Item extends StatelessWidget { + const _Item({ + Key? key, + required this.title, + required this.data, + required this.button, + }) : super(key: key); + + final String title; + final String data; + final Widget button; + + @override + Widget build(BuildContext context) { + return RoundedWhiteContainer( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + title, + style: STextStyles.itemSubtitle(context), + ), + button, + ], + ), + const SizedBox( + height: 5, + ), + data.isNotEmpty + ? SelectableText( + data, + style: STextStyles.w500_14(context), + ) + : Text( + "$title will appear here", + style: STextStyles.w500_14(context).copyWith( + color: Theme.of(context) + .extension()! + .textSubtitle3, + ), + ), + ], + ), + ); + } +} diff --git a/lib/pages/token_view/token_view.dart b/lib/pages/token_view/token_view.dart new file mode 100644 index 000000000..851119f06 --- /dev/null +++ b/lib/pages/token_view/token_view.dart @@ -0,0 +1,215 @@ +import 'package:event_bus/event_bus.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_svg/svg.dart'; +import 'package:stackwallet/pages/token_view/sub_widgets/token_summary.dart'; +import 'package:stackwallet/pages/token_view/sub_widgets/token_transaction_list_widget.dart'; +import 'package:stackwallet/pages/token_view/token_contract_details_view.dart'; +import 'package:stackwallet/pages/wallet_view/transaction_views/all_transactions_view.dart'; +import 'package:stackwallet/services/ethereum/ethereum_token_service.dart'; +import 'package:stackwallet/services/event_bus/events/global/wallet_sync_status_changed_event.dart'; +import 'package:stackwallet/utilities/assets.dart'; +import 'package:stackwallet/utilities/constants.dart'; +import 'package:stackwallet/utilities/text_styles.dart'; +import 'package:stackwallet/utilities/theme/stack_colors.dart'; +import 'package:stackwallet/widgets/background.dart'; +import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; +import 'package:stackwallet/widgets/custom_buttons/blue_text_button.dart'; +import 'package:stackwallet/widgets/icon_widgets/eth_token_icon.dart'; +import 'package:tuple/tuple.dart'; + +final tokenServiceStateProvider = StateProvider((ref) => null); +final tokenServiceProvider = ChangeNotifierProvider( + (ref) => ref.watch(tokenServiceStateProvider)); + +/// [eventBus] should only be set during testing +class TokenView extends ConsumerStatefulWidget { + const TokenView({ + Key? key, + required this.walletId, + this.eventBus, + }) : super(key: key); + + static const String routeName = "/token"; + + final String walletId; + final EventBus? eventBus; + + @override + ConsumerState createState() => _TokenViewState(); +} + +class _TokenViewState extends ConsumerState { + late final WalletSyncStatus initialSyncStatus; + + @override + void initState() { + initialSyncStatus = ref.read(tokenServiceProvider)!.isRefreshing + ? WalletSyncStatus.syncing + : WalletSyncStatus.synced; + super.initState(); + } + + @override + void dispose() { + super.dispose(); + } + + @override + Widget build(BuildContext context) { + debugPrint("BUILD: $runtimeType"); + + return Background( + child: Scaffold( + backgroundColor: Theme.of(context).extension()!.background, + appBar: AppBar( + leading: AppBarBackButton( + onPressed: () { + Navigator.of(context).pop(); + }, + ), + centerTitle: true, + title: Row( + mainAxisAlignment: MainAxisAlignment.center, + mainAxisSize: MainAxisSize.min, + children: [ + Expanded( + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + EthTokenIcon( + contractAddress: ref.watch( + tokenServiceProvider.select( + (value) => value!.tokenContract.address, + ), + ), + size: 24, + ), + const SizedBox( + width: 10, + ), + Flexible( + child: Text( + ref.watch(tokenServiceProvider + .select((value) => value!.tokenContract.name)), + style: STextStyles.navBarTitle(context), + overflow: TextOverflow.ellipsis, + textAlign: TextAlign.center, + ), + ), + ], + ), + ), + ], + ), + actions: [ + Padding( + padding: const EdgeInsets.only(right: 2), + child: AspectRatio( + aspectRatio: 1, + child: AppBarIconButton( + icon: SvgPicture.asset( + Assets.svg.verticalEllipsis, + ), + onPressed: () { + // todo: context menu + Navigator.of(context).pushNamed( + TokenContractDetailsView.routeName, + arguments: Tuple2( + ref.watch(tokenServiceProvider + .select((value) => value!.tokenContract.address)), + widget.walletId, + ), + ); + }, + ), + ), + ), + ], + ), + body: Container( + color: Theme.of(context).extension()!.background, + child: Column( + children: [ + const SizedBox( + height: 10, + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: TokenSummary( + walletId: widget.walletId, + initialSyncStatus: initialSyncStatus, + ), + ), + const SizedBox( + height: 20, + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Transactions", + style: STextStyles.itemSubtitle(context).copyWith( + color: Theme.of(context) + .extension()! + .textDark3, + ), + ), + CustomTextButton( + text: "See all", + onTap: () { + Navigator.of(context).pushNamed( + AllTransactionsView.routeName, + arguments: widget.walletId, + ); + }, + ), + ], + ), + ), + const SizedBox( + height: 12, + ), + Expanded( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: ClipRRect( + borderRadius: BorderRadius.vertical( + top: Radius.circular( + Constants.size.circularBorderRadius, + ), + bottom: Radius.circular( + // TokenView.navBarHeight / 2.0, + Constants.size.circularBorderRadius, + ), + ), + child: Container( + decoration: BoxDecoration( + color: Colors.transparent, + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Expanded( + child: TokenTransactionsList( + walletId: widget.walletId, + ), + ), + ], + ), + ), + ), + ), + ), + ], + ), + ), + ), + ); + } +} diff --git a/lib/pages/wallet_view/sub_widgets/transactions_list.dart b/lib/pages/wallet_view/sub_widgets/transactions_list.dart index 3d9b9c95e..bdfd00394 100644 --- a/lib/pages/wallet_view/sub_widgets/transactions_list.dart +++ b/lib/pages/wallet_view/sub_widgets/transactions_list.dart @@ -252,6 +252,7 @@ class _TransactionsListState extends ConsumerState { }, child: Util.isDesktop ? ListView.separated( + shrinkWrap: true, itemBuilder: (context, index) { BorderRadius? radius; if (_transactions2.length == 1) { diff --git a/lib/pages/wallet_view/sub_widgets/wallet_balance_toggle_sheet.dart b/lib/pages/wallet_view/sub_widgets/wallet_balance_toggle_sheet.dart index de9d95f7e..7c6778878 100644 --- a/lib/pages/wallet_view/sub_widgets/wallet_balance_toggle_sheet.dart +++ b/lib/pages/wallet_view/sub_widgets/wallet_balance_toggle_sheet.dart @@ -1,4 +1,3 @@ -import 'package:decimal/decimal.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:stackwallet/models/balance.dart'; @@ -6,6 +5,7 @@ import 'package:stackwallet/providers/providers.dart'; import 'package:stackwallet/providers/wallet/public_private_balance_state_provider.dart'; import 'package:stackwallet/providers/wallet/wallet_balance_toggle_state_provider.dart'; import 'package:stackwallet/services/coins/firo/firo_wallet.dart'; +import 'package:stackwallet/utilities/amount/amount.dart'; import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:stackwallet/utilities/enums/wallet_balance_toggle_state.dart'; @@ -112,7 +112,7 @@ class WalletBalanceToggleSheet extends ConsumerWidget { BalanceSelector( title: "Available balance", coin: coin, - balance: balance.getSpendable(), + balance: balance.spendable, onPressed: () { ref.read(walletBalanceToggleStateProvider.state).state = WalletBalanceToggleState.available; @@ -138,7 +138,7 @@ class WalletBalanceToggleSheet extends ConsumerWidget { BalanceSelector( title: "Available private balance", coin: coin, - balance: balanceSecondary.getSpendable(), + balance: balanceSecondary.spendable, onPressed: () { ref.read(walletBalanceToggleStateProvider.state).state = WalletBalanceToggleState.available; @@ -162,7 +162,7 @@ class WalletBalanceToggleSheet extends ConsumerWidget { BalanceSelector( title: "Full balance", coin: coin, - balance: balance.getTotal(), + balance: balance.total, onPressed: () { ref.read(walletBalanceToggleStateProvider.state).state = WalletBalanceToggleState.full; @@ -188,7 +188,7 @@ class WalletBalanceToggleSheet extends ConsumerWidget { BalanceSelector( title: "Full private balance", coin: coin, - balance: balanceSecondary.getTotal(), + balance: balanceSecondary.total, onPressed: () { ref.read(walletBalanceToggleStateProvider.state).state = WalletBalanceToggleState.full; @@ -217,7 +217,7 @@ class WalletBalanceToggleSheet extends ConsumerWidget { } } -class BalanceSelector extends StatelessWidget { +class BalanceSelector extends ConsumerWidget { const BalanceSelector({ Key? key, required this.title, @@ -231,14 +231,14 @@ class BalanceSelector extends StatelessWidget { final String title; final Coin coin; - final Decimal balance; + final Amount balance; final VoidCallback onPressed; final void Function(T?) onChanged; final T value; final T? groupValue; @override - Widget build(BuildContext context) { + Widget build(BuildContext context, WidgetRef ref) { return RawMaterialButton( shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular( @@ -278,7 +278,13 @@ class BalanceSelector extends StatelessWidget { height: 2, ), Text( - "${balance.toStringAsFixed(Constants.decimalPlacesForCoin(coin))} ${coin.ticker}", + "${balance.localizedStringAsFixed( + locale: ref.watch( + localeServiceChangeNotifierProvider.select( + (value) => value.locale, + ), + ), + )} ${coin.ticker}", style: STextStyles.itemSubtitle12(context).copyWith( color: Theme.of(context) .extension()! diff --git a/lib/pages/wallet_view/sub_widgets/wallet_navigation_bar.dart b/lib/pages/wallet_view/sub_widgets/wallet_navigation_bar.dart new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/lib/pages/wallet_view/sub_widgets/wallet_navigation_bar.dart @@ -0,0 +1 @@ + diff --git a/lib/pages/wallet_view/sub_widgets/wallet_refresh_button.dart b/lib/pages/wallet_view/sub_widgets/wallet_refresh_button.dart index 603b72338..453762b80 100644 --- a/lib/pages/wallet_view/sub_widgets/wallet_refresh_button.dart +++ b/lib/pages/wallet_view/sub_widgets/wallet_refresh_button.dart @@ -4,6 +4,7 @@ import 'package:event_bus/event_bus.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/svg.dart'; +import 'package:stackwallet/pages/token_view/token_view.dart'; import 'package:stackwallet/providers/global/wallets_provider.dart'; import 'package:stackwallet/services/event_bus/events/global/wallet_sync_status_changed_event.dart'; import 'package:stackwallet/services/event_bus/global_event_bus.dart'; @@ -18,12 +19,14 @@ class WalletRefreshButton extends ConsumerStatefulWidget { Key? key, required this.walletId, required this.initialSyncStatus, + this.tokenContractAddress, this.onPressed, this.eventBus, }) : super(key: key); final String walletId; final WalletSyncStatus initialSyncStatus; + final String? tokenContractAddress; final VoidCallback? onPressed; final EventBus? eventBus; @@ -62,7 +65,21 @@ class _RefreshButtonState extends ConsumerState _syncStatusSubscription = eventBus.on().listen( (event) async { - if (event.walletId == widget.walletId) { + if (event.walletId == widget.walletId && + widget.tokenContractAddress == null) { + switch (event.newStatus) { + case WalletSyncStatus.unableToSync: + _spinController?.stop(); + break; + case WalletSyncStatus.synced: + _spinController?.stop(); + break; + case WalletSyncStatus.syncing: + unawaited(_spinController?.repeat()); + break; + } + } else if (widget.tokenContractAddress != null && + event.walletId == widget.walletId + widget.tokenContractAddress!) { switch (event.newStatus) { case WalletSyncStatus.unableToSync: _spinController?.stop(); @@ -104,16 +121,22 @@ class _RefreshButtonState extends ConsumerState : null, splashColor: Theme.of(context).extension()!.highlight, onPressed: () { - final managerProvider = ref - .read(walletsChangeNotifierProvider) - .getManagerProvider(widget.walletId); - final isRefreshing = ref.read(managerProvider).isRefreshing; - if (!isRefreshing) { - _spinController?.repeat(); - ref - .read(managerProvider) - .refresh() - .then((_) => _spinController?.stop()); + if (widget.tokenContractAddress == null) { + final managerProvider = ref + .read(walletsChangeNotifierProvider) + .getManagerProvider(widget.walletId); + final isRefreshing = ref.read(managerProvider).isRefreshing; + if (!isRefreshing) { + _spinController?.repeat(); + ref + .read(managerProvider) + .refresh() + .then((_) => _spinController?.stop()); + } + } else { + if (!ref.read(tokenServiceProvider)!.isRefreshing) { + ref.read(tokenServiceProvider)!.refresh(); + } } }, elevation: 0, diff --git a/lib/pages/wallet_view/sub_widgets/wallet_summary_info.dart b/lib/pages/wallet_view/sub_widgets/wallet_summary_info.dart index 6339692f5..947919b8d 100644 --- a/lib/pages/wallet_view/sub_widgets/wallet_summary_info.dart +++ b/lib/pages/wallet_view/sub_widgets/wallet_summary_info.dart @@ -1,26 +1,24 @@ import 'dart:async'; -import 'package:decimal/decimal.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:stackwallet/pages/wallet_view/sub_widgets/wallet_balance_toggle_sheet.dart'; import 'package:stackwallet/pages/wallet_view/sub_widgets/wallet_refresh_button.dart'; import 'package:stackwallet/providers/providers.dart'; +import 'package:stackwallet/providers/wallet/public_private_balance_state_provider.dart'; import 'package:stackwallet/providers/wallet/wallet_balance_toggle_state_provider.dart'; import 'package:stackwallet/services/coins/firo/firo_wallet.dart'; import 'package:stackwallet/services/event_bus/events/global/balance_refreshed_event.dart'; import 'package:stackwallet/services/event_bus/events/global/wallet_sync_status_changed_event.dart'; import 'package:stackwallet/services/event_bus/global_event_bus.dart'; +import 'package:stackwallet/utilities/amount/amount.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:stackwallet/utilities/enums/wallet_balance_toggle_state.dart'; -import 'package:stackwallet/utilities/format.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; -import '../../../providers/wallet/public_private_balance_state_provider.dart'; - class WalletSummaryInfo extends ConsumerStatefulWidget { const WalletSummaryInfo({ Key? key, @@ -94,7 +92,7 @@ class _WalletSummaryInfoState extends ConsumerState { ref.watch(walletBalanceToggleStateProvider.state).state == WalletBalanceToggleState.available; - final Decimal balanceToShow; + final Amount balanceToShow; String title; if (coin == Coin.firo || coin == Coin.firoTestNet) { @@ -106,12 +104,11 @@ class _WalletSummaryInfoState extends ConsumerState { final bal = _showPrivate ? firoWallet.balancePrivate : firoWallet.balance; - balanceToShow = _showAvailable ? bal.getSpendable() : bal.getTotal(); + balanceToShow = _showAvailable ? bal.spendable : bal.total; title = _showAvailable ? "Available" : "Full"; title += _showPrivate ? " private balance" : " public balance"; } else { - balanceToShow = - _showAvailable ? balance.getSpendable() : balance.getTotal(); + balanceToShow = _showAvailable ? balance.spendable : balance.total; title = _showAvailable ? "Available balance" : "Full balance"; } @@ -151,10 +148,8 @@ class _WalletSummaryInfoState extends ConsumerState { FittedBox( fit: BoxFit.scaleDown, child: SelectableText( - "${Format.localizedStringAsFixed( - value: balanceToShow, + "${balanceToShow.localizedStringAsFixed( locale: locale, - decimalPlaces: 8, )} ${coin.ticker}", style: STextStyles.pageTitleH1(context).copyWith( fontSize: 24, @@ -166,11 +161,11 @@ class _WalletSummaryInfoState extends ConsumerState { ), if (externalCalls) Text( - "${Format.localizedStringAsFixed( - value: priceTuple.item1 * balanceToShow, - locale: locale, - decimalPlaces: 2, - )} $baseCurrency", + "${(priceTuple.item1 * balanceToShow.decimal).toAmount( + fractionDigits: 2, + ).localizedStringAsFixed( + locale: locale, + )} $baseCurrency", style: STextStyles.subtitle500(context).copyWith( color: Theme.of(context) .extension()! diff --git a/lib/pages/wallet_view/transaction_views/all_transactions_view.dart b/lib/pages/wallet_view/transaction_views/all_transactions_view.dart index 5b72331a3..f0247b429 100644 --- a/lib/pages/wallet_view/transaction_views/all_transactions_view.dart +++ b/lib/pages/wallet_view/transaction_views/all_transactions_view.dart @@ -13,6 +13,7 @@ import 'package:stackwallet/pages/wallet_view/transaction_views/transaction_sear import 'package:stackwallet/providers/global/address_book_service_provider.dart'; import 'package:stackwallet/providers/providers.dart'; import 'package:stackwallet/providers/ui/transaction_filter_provider.dart'; +import 'package:stackwallet/utilities/amount/amount.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; @@ -111,7 +112,7 @@ class _TransactionDetailsViewState extends ConsumerState { return false; } - if (filter.amount != null && filter.amount != tx.amount) { + if (filter.amount != null && filter.amount! != tx.realAmount) { return false; } @@ -953,9 +954,11 @@ class _DesktopTransactionCardRowState flex: 6, child: Builder( builder: (_) { - final amount = _transaction.amount; + final amount = _transaction.realAmount; return Text( - "$prefix${Format.satoshiAmountToPrettyString(amount, locale, coin)} ${coin.ticker}", + "$prefix${amount.localizedStringAsFixed( + locale: locale, + )} ${coin.ticker}", style: STextStyles.desktopTextExtraExtraSmall(context) .copyWith( color: Theme.of(context) @@ -972,15 +975,14 @@ class _DesktopTransactionCardRowState flex: 4, child: Builder( builder: (_) { - int value = _transaction.amount; + final amount = _transaction.realAmount; return Text( - "$prefix${Format.localizedStringAsFixed( - value: Format.satoshisToAmount(value, coin: coin) * - price, - locale: locale, - decimalPlaces: 2, - )} $baseCurrency", + "$prefix${(amount.decimal * price).toAmount( + fractionDigits: 2, + ).localizedStringAsFixed( + locale: locale, + )} $baseCurrency", style: STextStyles.desktopTextExtraExtraSmall(context), ); }, diff --git a/lib/pages/wallet_view/transaction_views/transaction_details_view.dart b/lib/pages/wallet_view/transaction_views/transaction_details_view.dart index 056b8da4b..ed5e78c64 100644 --- a/lib/pages/wallet_view/transaction_views/transaction_details_view.dart +++ b/lib/pages/wallet_view/transaction_views/transaction_details_view.dart @@ -1,6 +1,5 @@ import 'dart:async'; -import 'package:decimal/decimal.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; @@ -11,10 +10,12 @@ import 'package:stackwallet/pages/wallet_view/sub_widgets/tx_icon.dart'; import 'package:stackwallet/pages/wallet_view/transaction_views/dialogs/cancelling_transaction_progress_dialog.dart'; import 'package:stackwallet/pages/wallet_view/transaction_views/edit_note_view.dart'; import 'package:stackwallet/pages/wallet_view/wallet_view.dart'; +import 'package:stackwallet/providers/db/main_db_provider.dart'; import 'package:stackwallet/providers/global/address_book_service_provider.dart'; import 'package:stackwallet/providers/providers.dart'; import 'package:stackwallet/services/coins/epiccash/epiccash_wallet.dart'; import 'package:stackwallet/services/coins/manager.dart'; +import 'package:stackwallet/utilities/amount/amount.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/block_explorers.dart'; import 'package:stackwallet/utilities/constants.dart'; @@ -65,9 +66,11 @@ class _TransactionDetailsViewState late final String walletId; late final Coin coin; - late final Decimal amount; - late final Decimal fee; + late final Amount amount; + late final Amount fee; late final String amountPrefix; + late final String unit; + late final bool isTokenTx; bool showFeePending = false; @@ -75,11 +78,12 @@ class _TransactionDetailsViewState void initState() { isDesktop = Util.isDesktop; _transaction = widget.transaction; + isTokenTx = _transaction.subType == TransactionSubType.ethToken; walletId = widget.walletId; coin = widget.coin; - amount = Format.satoshisToAmount(_transaction.amount, coin: coin); - fee = Format.satoshisToAmount(_transaction.fee, coin: coin); + amount = _transaction.realAmount; + fee = _transaction.fee.toAmountAsRaw(fractionDigits: coin.decimals); if ((coin == Coin.firo || coin == Coin.firoTestNet) && _transaction.subType == TransactionSubType.mint) { @@ -88,6 +92,13 @@ class _TransactionDetailsViewState amountPrefix = _transaction.type == TransactionType.outgoing ? "-" : "+"; } + unit = isTokenTx + ? ref + .read(mainDBProvider) + .getEthContractSync(_transaction.otherData!)! + .symbol + : coin.ticker; + // if (coin == Coin.firo || coin == Coin.firoTestNet) { // showFeePending = true; // } else { @@ -432,16 +443,13 @@ class _TransactionDetailsViewState : CrossAxisAlignment.start, children: [ SelectableText( - "$amountPrefix${Format.localizedStringAsFixed( - value: amount, + "$amountPrefix${amount.localizedStringAsFixed( locale: ref.watch( localeServiceChangeNotifierProvider .select((value) => value.locale), ), - decimalPlaces: Constants - .decimalPlacesForCoin(coin), - )} ${coin.ticker}", + )} $unit", style: isDesktop ? STextStyles .desktopTextExtraExtraSmall( @@ -463,23 +471,26 @@ class _TransactionDetailsViewState .select((value) => value.externalCalls))) SelectableText( - "$amountPrefix${Format.localizedStringAsFixed( - value: amount * - ref.watch( - priceAnd24hChangeNotifierProvider - .select((value) => - value - .getPrice( - coin) - .item1), + "$amountPrefix${(amount.decimal * ref.watch( + priceAnd24hChangeNotifierProvider.select( + (value) => isTokenTx + ? value + .getTokenPrice( + _transaction + .otherData!) + .item1 + : value + .getPrice( + coin) + .item1), + )).toAmount(fractionDigits: 2).localizedStringAsFixed( + locale: ref.watch( + localeServiceChangeNotifierProvider + .select( + (value) => value.locale, + ), ), - locale: ref.watch( - localeServiceChangeNotifierProvider - .select((value) => - value.locale), - ), - decimalPlaces: 2, - )} ${ref.watch( + )} ${ref.watch( prefsChangeNotifierProvider .select( (value) => value.currency, @@ -873,29 +884,28 @@ class _TransactionDetailsViewState ? const EdgeInsets.all(16) : const EdgeInsets.all(12), child: Builder(builder: (context) { - final feeString = showFeePending + String feeString = showFeePending ? _transaction.isConfirmed( currentHeight, coin.requiredConfirmations, ) - ? Format.localizedStringAsFixed( - value: fee, + ? fee.localizedStringAsFixed( locale: ref.watch( - localeServiceChangeNotifierProvider - .select((value) => - value.locale)), - decimalPlaces: - Constants.decimalPlacesForCoin( - coin)) + localeServiceChangeNotifierProvider + .select( + (value) => value.locale), + ), + ) : "Pending" - : Format.localizedStringAsFixed( - value: fee, + : fee.localizedStringAsFixed( locale: ref.watch( - localeServiceChangeNotifierProvider - .select( - (value) => value.locale)), - decimalPlaces: - Constants.decimalPlacesForCoin(coin)); + localeServiceChangeNotifierProvider + .select((value) => value.locale), + ), + ); + if (isTokenTx) { + feeString += " ${coin.ticker}"; + } return Row( mainAxisAlignment: @@ -1048,6 +1058,46 @@ class _TransactionDetailsViewState ); }), ), + if (coin == Coin.ethereum) + isDesktop + ? const _Divider() + : const SizedBox( + height: 12, + ), + if (coin == Coin.ethereum) + RoundedWhiteContainer( + padding: isDesktop + ? const EdgeInsets.all(16) + : const EdgeInsets.all(12), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + Text( + "Nonce", + style: isDesktop + ? STextStyles + .desktopTextExtraExtraSmall( + context) + : STextStyles.itemSubtitle(context), + ), + SelectableText( + _transaction.nonce.toString(), + style: isDesktop + ? STextStyles + .desktopTextExtraExtraSmall( + context) + .copyWith( + color: Theme.of(context) + .extension()! + .textDark, + ) + : STextStyles.itemSubtitle12(context), + ), + ], + ), + ), isDesktop ? const _Divider() : const SizedBox( @@ -1137,18 +1187,20 @@ class _TransactionDetailsViewState .externalApplication, ); } catch (_) { - unawaited( - showDialog( - context: context, - builder: (_) => - StackOkDialog( - title: - "Could not open in block explorer", - message: - "Failed to open \"${uri.toString()}\"", + if (mounted) { + unawaited( + showDialog( + context: context, + builder: (_) => + StackOkDialog( + title: + "Could not open in block explorer", + message: + "Failed to open \"${uri.toString()}\"", + ), ), - ), - ); + ); + } } finally { // Future.delayed( // const Duration(seconds: 1), @@ -1454,13 +1506,15 @@ class IconCopyButton extends StatelessWidget { ), onPressed: () async { await Clipboard.setData(ClipboardData(text: data)); - unawaited( - showFloatingFlushBar( - type: FlushBarType.info, - message: "Copied to clipboard", - context: context, - ), - ); + if (context.mounted) { + unawaited( + showFloatingFlushBar( + type: FlushBarType.info, + message: "Copied to clipboard", + context: context, + ), + ); + } }, child: Padding( padding: const EdgeInsets.all(5), diff --git a/lib/pages/wallet_view/transaction_views/transaction_search_filter_view.dart b/lib/pages/wallet_view/transaction_views/transaction_search_filter_view.dart index d0e013d83..ac1295f55 100644 --- a/lib/pages/wallet_view/transaction_views/transaction_search_filter_view.dart +++ b/lib/pages/wallet_view/transaction_views/transaction_search_filter_view.dart @@ -8,6 +8,7 @@ import 'package:stackwallet/models/transaction_filter.dart'; import 'package:stackwallet/providers/providers.dart'; import 'package:stackwallet/providers/ui/color_theme_provider.dart'; import 'package:stackwallet/providers/ui/transaction_filter_provider.dart'; +import 'package:stackwallet/utilities/amount/amount.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; @@ -75,13 +76,12 @@ class _TransactionSearchViewState _toDateString = _selectedToDate == null ? "" : Format.formatDate(_selectedToDate!); - // TODO: Fix XMR (modify Format.funcs to take optional Coin parameter) - // final amt = Format.satoshisToAmount(widget.coin == Coin.monero ? ) - String amount = ""; - if (filterState.amount != null) { - amount = Format.satoshiAmountToPrettyString(filterState.amount!, - ref.read(localeServiceChangeNotifierProvider).locale, widget.coin); - } + final String amount = filterState.amount?.localizedStringAsFixed( + locale: ref.read(localeServiceChangeNotifierProvider).locale, + decimalPlaces: widget.coin.decimals, + ) ?? + ""; + _amountTextEditingController.text = amount; } @@ -966,15 +966,13 @@ class _TransactionSearchViewState Future _onApplyPressed() async { final amountText = _amountTextEditingController.text; - Decimal? amountDecimal; + Amount? amount; if (amountText.isNotEmpty && !(amountText == "," || amountText == ".")) { - amountDecimal = amountText.contains(",") + amount = amountText.contains(",") ? Decimal.parse(amountText.replaceFirst(",", ".")) - : Decimal.parse(amountText); - } - int? amount; - if (amountDecimal != null) { - amount = Format.decimalAmountToSatoshis(amountDecimal, widget.coin); + .toAmount(fractionDigits: widget.coin.decimals) + : Decimal.parse(amountText) + .toAmount(fractionDigits: widget.coin.decimals); } final TransactionFilter filter = TransactionFilter( diff --git a/lib/pages/wallet_view/wallet_view.dart b/lib/pages/wallet_view/wallet_view.dart index f586eaa80..82adc3b6a 100644 --- a/lib/pages/wallet_view/wallet_view.dart +++ b/lib/pages/wallet_view/wallet_view.dart @@ -1,6 +1,5 @@ import 'dart:async'; -import 'package:decimal/decimal.dart'; import 'package:event_bus/event_bus.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; @@ -19,6 +18,7 @@ import 'package:stackwallet/pages/receive_view/receive_view.dart'; import 'package:stackwallet/pages/send_view/send_view.dart'; import 'package:stackwallet/pages/settings_views/wallet_settings_view/wallet_network_settings_view/wallet_network_settings_view.dart'; import 'package:stackwallet/pages/settings_views/wallet_settings_view/wallet_settings_view.dart'; +import 'package:stackwallet/pages/token_view/my_tokens_view.dart'; import 'package:stackwallet/pages/wallet_view/sub_widgets/transactions_list.dart'; import 'package:stackwallet/pages/wallet_view/sub_widgets/wallet_summary.dart'; import 'package:stackwallet/pages/wallet_view/transaction_views/all_transactions_view.dart'; @@ -37,6 +37,7 @@ import 'package:stackwallet/services/event_bus/events/global/wallet_sync_status_ import 'package:stackwallet/services/event_bus/global_event_bus.dart'; import 'package:stackwallet/services/exchange/exchange_data_loading_service.dart'; import 'package:stackwallet/services/mixins/paynym_wallet_interface.dart'; +import 'package:stackwallet/utilities/amount/amount.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/clipboard_interface.dart'; import 'package:stackwallet/utilities/constants.dart'; @@ -276,11 +277,26 @@ class _WalletViewState extends ConsumerState { ), ); } else { - final currency = await showLoading( - whileFuture: ExchangeDataLoadingService.instance.isar.currencies + Future _future; + try { + _future = ExchangeDataLoadingService.instance.isar.currencies .where() .tickerEqualToAnyExchangeNameName(coin.ticker) - .findFirst(), + .findFirst(); + } catch (_) { + _future = ExchangeDataLoadingService.instance + .init() + .then( + (_) => ExchangeDataLoadingService.instance.loadAll(), + ) + .then((_) => ExchangeDataLoadingService.instance.isar.currencies + .where() + .tickerEqualToAnyExchangeNameName(coin.ticker) + .findFirst()); + } + + final currency = await showLoading( + whileFuture: _future, context: context, message: "Loading...", ); @@ -337,8 +353,8 @@ class _WalletViewState extends ConsumerState { ); final firoWallet = ref.read(managerProvider).wallet as FiroWallet; - final publicBalance = firoWallet.availablePublicBalance(); - if (publicBalance <= Decimal.zero) { + final Amount publicBalance = firoWallet.availablePublicBalance(); + if (publicBalance <= Amount.zero) { shouldPop = true; if (mounted) { Navigator.of(context).popUntil( @@ -836,6 +852,22 @@ class _WalletViewState extends ConsumerState { ), ], moreItems: [ + if (ref.watch( + walletsChangeNotifierProvider.select( + (value) => + value.getManager(widget.walletId).hasTokenSupport, + ), + )) + WalletNavigationBarItemData( + label: "Tokens", + icon: const CoinControlNavIcon(), + onTap: () { + Navigator.of(context).pushNamed( + MyTokensView.routeName, + arguments: walletId, + ); + }, + ), if (ref.watch( walletsChangeNotifierProvider.select( (value) => value diff --git a/lib/pages/wallets_view/eth_wallets_overview.dart b/lib/pages/wallets_view/eth_wallets_overview.dart new file mode 100644 index 000000000..35e1b4411 --- /dev/null +++ b/lib/pages/wallets_view/eth_wallets_overview.dart @@ -0,0 +1,73 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:stackwallet/providers/providers.dart'; +import 'package:stackwallet/utilities/enums/coin_enum.dart'; +import 'package:stackwallet/utilities/text_styles.dart'; +import 'package:stackwallet/utilities/theme/stack_colors.dart'; +import 'package:stackwallet/utilities/util.dart'; +import 'package:stackwallet/widgets/background.dart'; +import 'package:stackwallet/widgets/conditional_parent.dart'; +import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; +import 'package:stackwallet/widgets/master_wallet_card.dart'; + +class EthWalletsOverview extends ConsumerStatefulWidget { + const EthWalletsOverview({Key? key}) : super(key: key); + + static const routeName = "/ethWalletsOverview"; + + @override + ConsumerState createState() => _EthWalletsOverviewState(); +} + +class _EthWalletsOverviewState extends ConsumerState { + final isDesktop = Util.isDesktop; + + final List ethWalletIds = []; + + @override + void initState() { + final walletsData = + ref.read(walletsServiceChangeNotifierProvider).fetchWalletsData(); + walletsData.removeWhere((key, value) => value.coin != Coin.ethereum); + ethWalletIds.clear(); + + ethWalletIds.addAll(walletsData.values.map((e) => e.walletId)); + + super.initState(); + } + + @override + Widget build(BuildContext context) { + return Background( + child: ConditionalParent( + condition: !isDesktop, + builder: (child) => Scaffold( + backgroundColor: + Theme.of(context).extension()!.background, + appBar: AppBar( + leading: const AppBarBackButton(), + title: Text( + "Ethereum (ETH) wallets", + style: STextStyles.navBarTitle(context), + ), + ), + body: SafeArea( + child: Padding( + padding: const EdgeInsets.all(16), + child: child, + ), + ), + ), + child: ListView.separated( + itemCount: ethWalletIds.length, + separatorBuilder: (_, __) => const SizedBox( + height: 8, + ), + itemBuilder: (_, index) => MasterWalletCard( + walletId: ethWalletIds[index], + ), + ), + ), + ); + } +} diff --git a/lib/pages/wallets_view/sub_widgets/favorite_card.dart b/lib/pages/wallets_view/sub_widgets/favorite_card.dart index 318f8e301..72b2e3ffa 100644 --- a/lib/pages/wallets_view/sub_widgets/favorite_card.dart +++ b/lib/pages/wallets_view/sub_widgets/favorite_card.dart @@ -1,4 +1,3 @@ -import 'package:decimal/decimal.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/flutter_svg.dart'; @@ -6,10 +5,10 @@ import 'package:stackwallet/pages/wallet_view/wallet_view.dart'; import 'package:stackwallet/pages_desktop_specific/my_stack_view/wallet_view/desktop_wallet_view.dart'; import 'package:stackwallet/providers/providers.dart'; import 'package:stackwallet/services/coins/manager.dart'; +import 'package:stackwallet/utilities/amount/amount.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; -import 'package:stackwallet/utilities/format.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; import 'package:stackwallet/utilities/util.dart'; @@ -38,8 +37,8 @@ class _FavoriteCardState extends ConsumerState { late final String walletId; late final ChangeNotifierProvider managerProvider; - Decimal _cachedBalance = Decimal.zero; - Decimal _cachedFiatValue = Decimal.zero; + Amount _cachedBalance = Amount.zero; + Amount _cachedFiatValue = Amount.zero; @override void initState() { @@ -223,21 +222,23 @@ class _FavoriteCardState extends ConsumerState { ), FutureBuilder( future: Future(() => ref.watch(managerProvider - .select((value) => value.balance.getTotal()))), - builder: (builderContext, AsyncSnapshot snapshot) { + .select((value) => value.balance.total))), + builder: (builderContext, AsyncSnapshot snapshot) { if (snapshot.connectionState == ConnectionState.done && snapshot.hasData) { if (snapshot.data != null) { _cachedBalance = snapshot.data!; - if (externalCalls) { - _cachedFiatValue = _cachedBalance * - ref - .watch( - priceAnd24hChangeNotifierProvider.select( - (value) => value.getPrice(coin), - ), - ) - .item1; + if (externalCalls && _cachedBalance > Amount.zero) { + _cachedFiatValue = (_cachedBalance.decimal * + ref + .watch( + priceAnd24hChangeNotifierProvider + .select( + (value) => value.getPrice(coin), + ), + ) + .item1) + .toAmount(fractionDigits: 2); } } } @@ -247,13 +248,13 @@ class _FavoriteCardState extends ConsumerState { FittedBox( fit: BoxFit.scaleDown, child: Text( - "${Format.localizedStringAsFixed( - decimalPlaces: 8, - value: _cachedBalance, + "${_cachedBalance.localizedStringAsFixed( locale: ref.watch( localeServiceChangeNotifierProvider .select((value) => value.locale), ), + decimalPlaces: ref.watch(managerProvider + .select((value) => value.coin.decimals)), )} ${coin.ticker}", style: STextStyles.titleBold12(context).copyWith( fontSize: 16, @@ -269,13 +270,12 @@ class _FavoriteCardState extends ConsumerState { ), if (externalCalls) Text( - "${Format.localizedStringAsFixed( - decimalPlaces: 2, - value: _cachedFiatValue, + "${_cachedFiatValue.localizedStringAsFixed( locale: ref.watch( localeServiceChangeNotifierProvider .select((value) => value.locale), ), + decimalPlaces: 2, )} ${ref.watch( prefsChangeNotifierProvider .select((value) => value.currency), diff --git a/lib/pages/wallets_view/sub_widgets/wallet_list_item.dart b/lib/pages/wallets_view/sub_widgets/wallet_list_item.dart index 01b78c33f..ac4c6fb85 100644 --- a/lib/pages/wallets_view/sub_widgets/wallet_list_item.dart +++ b/lib/pages/wallets_view/sub_widgets/wallet_list_item.dart @@ -5,11 +5,12 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/svg.dart'; import 'package:stackwallet/pages/wallet_view/wallet_view.dart'; import 'package:stackwallet/pages/wallets_sheet/wallets_sheet.dart'; +import 'package:stackwallet/pages/wallets_view/eth_wallets_overview.dart'; import 'package:stackwallet/providers/providers.dart'; +import 'package:stackwallet/utilities/amount/amount.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; -import 'package:stackwallet/utilities/format.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; import 'package:stackwallet/widgets/rounded_white_container.dart'; @@ -45,7 +46,13 @@ class WalletListItem extends ConsumerWidget { BorderRadius.circular(Constants.size.circularBorderRadius), ), onPressed: () async { - if (walletCount == 1) { + if (coin == Coin.ethereum) { + unawaited( + Navigator.of(context).pushNamed( + EthWalletsOverview.routeName, + ), + ); + } else if (walletCount == 1) { final providersByCoin = ref .watch(walletsChangeNotifierProvider .select((value) => value.getManagerProvidersByCoin())) @@ -57,15 +64,17 @@ class WalletListItem extends ConsumerWidget { if (coin == Coin.monero || coin == Coin.wownero) { await manager.initializeExisting(); } - unawaited( - Navigator.of(context).pushNamed( - WalletView.routeName, - arguments: Tuple2( - manager.walletId, - providersByCoin.first, + if (context.mounted) { + unawaited( + Navigator.of(context).pushNamed( + WalletView.routeName, + arguments: Tuple2( + manager.walletId, + providersByCoin.first, + ), ), - ), - ); + ); + } } else { unawaited( showModalBottomSheet( @@ -99,13 +108,12 @@ class WalletListItem extends ConsumerWidget { final calls = ref.watch(prefsChangeNotifierProvider).externalCalls; - final priceString = Format.localizedStringAsFixed( - value: tuple.item1, - locale: ref - .watch(localeServiceChangeNotifierProvider.notifier) - .locale, - decimalPlaces: 2, - ); + final priceString = tuple.item1 + .toAmount(fractionDigits: 2) + .localizedStringAsFixed( + locale: ref.watch(localeServiceChangeNotifierProvider + .select((value) => value.locale)), + ); final double percentChange = tuple.item2; diff --git a/lib/pages_desktop_specific/address_book_view/subwidgets/desktop_contact_details.dart b/lib/pages_desktop_specific/address_book_view/subwidgets/desktop_contact_details.dart index 4b2eb33a8..6544e1acd 100644 --- a/lib/pages_desktop_specific/address_book_view/subwidgets/desktop_contact_details.dart +++ b/lib/pages_desktop_specific/address_book_view/subwidgets/desktop_contact_details.dart @@ -24,7 +24,7 @@ import 'package:stackwallet/widgets/rounded_white_container.dart'; import 'package:stackwallet/widgets/transaction_card.dart'; import 'package:tuple/tuple.dart'; -import '../../../db/main_db.dart'; +import '../../../db/isar/main_db.dart'; class DesktopContactDetails extends ConsumerStatefulWidget { const DesktopContactDetails({ diff --git a/lib/pages_desktop_specific/addresses/desktop_wallet_addresses_view.dart b/lib/pages_desktop_specific/addresses/desktop_wallet_addresses_view.dart index ae750554a..54c5f4daa 100644 --- a/lib/pages_desktop_specific/addresses/desktop_wallet_addresses_view.dart +++ b/lib/pages_desktop_specific/addresses/desktop_wallet_addresses_view.dart @@ -2,10 +2,10 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/svg.dart'; import 'package:isar/isar.dart'; -import 'package:stackwallet/db/main_db.dart'; import 'package:stackwallet/models/isar/models/isar_models.dart'; import 'package:stackwallet/pages/receive_view/addresses/address_details_view.dart'; import 'package:stackwallet/pages_desktop_specific/addresses/sub_widgets/desktop_address_list.dart'; +import 'package:stackwallet/providers/db/main_db_provider.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; @@ -45,8 +45,11 @@ class _DesktopWalletAddressesViewState @override void initState() { - addressCollectionWatcher = - MainDB.instance.isar.addresses.watchLazy(fireImmediately: true); + addressCollectionWatcher = ref + .read(mainDBProvider) + .isar + .addresses + .watchLazy(fireImmediately: true); addressCollectionWatcher.listen((_) => _onAddressCollectionWatcherEvent()); super.initState(); diff --git a/lib/pages_desktop_specific/addresses/sub_widgets/desktop_address_list.dart b/lib/pages_desktop_specific/addresses/sub_widgets/desktop_address_list.dart index 419d245e5..16004fc0f 100644 --- a/lib/pages_desktop_specific/addresses/sub_widgets/desktop_address_list.dart +++ b/lib/pages_desktop_specific/addresses/sub_widgets/desktop_address_list.dart @@ -2,10 +2,10 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/svg.dart'; import 'package:isar/isar.dart'; -import 'package:stackwallet/db/main_db.dart'; import 'package:stackwallet/models/isar/models/isar_models.dart'; import 'package:stackwallet/pages/receive_view/addresses/address_card.dart'; import 'package:stackwallet/pages_desktop_specific/addresses/desktop_wallet_addresses_view.dart'; +import 'package:stackwallet/providers/db/main_db_provider.dart'; import 'package:stackwallet/providers/global/wallets_provider.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/constants.dart'; @@ -41,7 +41,8 @@ class _DesktopAddressListState extends ConsumerState { List _search(String term) { if (term.isEmpty) { - return MainDB.instance + return ref + .read(mainDBProvider) .getAddresses(widget.walletId) .filter() .group((q) => q @@ -60,7 +61,8 @@ class _DesktopAddressListState extends ConsumerState { .findAllSync(); } - final labels = MainDB.instance + final labels = ref + .read(mainDBProvider) .getAddressLabels(widget.walletId) .filter() .group( @@ -82,7 +84,8 @@ class _DesktopAddressListState extends ConsumerState { return []; } - return MainDB.instance + return ref + .read(mainDBProvider) .getAddresses(widget.walletId) .filter() .anyOf( diff --git a/lib/pages_desktop_specific/coin_control/desktop_coin_control_use_dialog.dart b/lib/pages_desktop_specific/coin_control/desktop_coin_control_use_dialog.dart index d0ba9ecbd..71b3e9d97 100644 --- a/lib/pages_desktop_specific/coin_control/desktop_coin_control_use_dialog.dart +++ b/lib/pages_desktop_specific/coin_control/desktop_coin_control_use_dialog.dart @@ -1,16 +1,15 @@ -import 'package:decimal/decimal.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/svg.dart'; import 'package:isar/isar.dart'; -import 'package:stackwallet/db/main_db.dart'; +import 'package:stackwallet/db/isar/main_db.dart'; import 'package:stackwallet/models/isar/models/blockchain_data/utxo.dart'; import 'package:stackwallet/pages_desktop_specific/coin_control/utxo_row.dart'; import 'package:stackwallet/providers/global/wallets_provider.dart'; +import 'package:stackwallet/utilities/amount/amount.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; -import 'package:stackwallet/utilities/format.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; import 'package:stackwallet/widgets/animated_widgets/rotate_icon.dart'; @@ -37,7 +36,7 @@ class DesktopCoinControlUseDialog extends ConsumerStatefulWidget { }) : super(key: key); final String walletId; - final Decimal? amountToSend; + final Amount? amountToSend; @override ConsumerState createState() => @@ -114,12 +113,16 @@ class _DesktopCoinControlUseDialogState ); } - final selectedSum = Format.satoshisToAmount( - _selectedUTXOs - .map((e) => e.value) - .fold(0, (value, element) => value += element), - coin: coin, - ); + final Amount selectedSum = _selectedUTXOs.map((e) => e.value).fold( + Amount( + rawValue: BigInt.zero, + fractionDigits: coin.decimals, + ), + (value, element) => value += Amount( + rawValue: BigInt.from(element), + fractionDigits: coin.decimals, + ), + ); final enableApply = widget.amountToSend == null ? selectedChanged(_selectedUTXOs) @@ -470,7 +473,7 @@ class _DesktopCoinControlUseDialogState ), ), Text( - "${widget.amountToSend!.toStringAsFixed( + "${widget.amountToSend!.decimal.toStringAsFixed( coin.decimals, )}" " ${coin.ticker}", @@ -505,7 +508,7 @@ class _DesktopCoinControlUseDialogState ), ), Text( - "${selectedSum.toStringAsFixed( + "${selectedSum.decimal.toStringAsFixed( coin.decimals, )} ${coin.ticker}", style: STextStyles.desktopTextExtraExtraSmall( diff --git a/lib/pages_desktop_specific/coin_control/desktop_coin_control_view.dart b/lib/pages_desktop_specific/coin_control/desktop_coin_control_view.dart index cf27cc80e..593666c3c 100644 --- a/lib/pages_desktop_specific/coin_control/desktop_coin_control_view.dart +++ b/lib/pages_desktop_specific/coin_control/desktop_coin_control_view.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/svg.dart'; import 'package:isar/isar.dart'; -import 'package:stackwallet/db/main_db.dart'; +import 'package:stackwallet/db/isar/main_db.dart'; import 'package:stackwallet/models/isar/models/blockchain_data/utxo.dart'; import 'package:stackwallet/pages_desktop_specific/coin_control/freeze_button.dart'; import 'package:stackwallet/pages_desktop_specific/coin_control/utxo_row.dart'; diff --git a/lib/pages_desktop_specific/coin_control/freeze_button.dart b/lib/pages_desktop_specific/coin_control/freeze_button.dart index f50e4661e..b33bc89fa 100644 --- a/lib/pages_desktop_specific/coin_control/freeze_button.dart +++ b/lib/pages_desktop_specific/coin_control/freeze_button.dart @@ -1,7 +1,7 @@ import 'package:async/async.dart'; import 'package:flutter/material.dart'; import 'package:isar/isar.dart'; -import 'package:stackwallet/db/main_db.dart'; +import 'package:stackwallet/db/isar/main_db.dart'; import 'package:stackwallet/models/isar/models/blockchain_data/utxo.dart'; import 'package:stackwallet/pages_desktop_specific/coin_control/utxo_row.dart'; import 'package:stackwallet/utilities/logger.dart'; diff --git a/lib/pages_desktop_specific/coin_control/utxo_row.dart b/lib/pages_desktop_specific/coin_control/utxo_row.dart index e5ddf2f06..7521c8ec0 100644 --- a/lib/pages_desktop_specific/coin_control/utxo_row.dart +++ b/lib/pages_desktop_specific/coin_control/utxo_row.dart @@ -1,12 +1,13 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:isar/isar.dart'; -import 'package:stackwallet/db/main_db.dart'; +import 'package:stackwallet/db/isar/main_db.dart'; import 'package:stackwallet/models/isar/models/isar_models.dart'; import 'package:stackwallet/pages/coin_control/utxo_details_view.dart'; +import 'package:stackwallet/providers/global/locale_provider.dart'; import 'package:stackwallet/providers/global/wallets_provider.dart'; +import 'package:stackwallet/utilities/amount/amount.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; -import 'package:stackwallet/utilities/format.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; import 'package:stackwallet/widgets/conditional_parent.dart'; @@ -143,10 +144,16 @@ class _UtxoRowState extends ConsumerState { ), if (!widget.compact) Text( - "${Format.satoshisToAmount( - utxo.value, - coin: coin, - ).toStringAsFixed(coin.decimals)} ${coin.ticker}", + "${Amount( + rawValue: BigInt.from(utxo.value), + fractionDigits: coin.decimals, + ).localizedStringAsFixed( + locale: ref.watch( + localeServiceChangeNotifierProvider.select( + (value) => value.locale, + ), + ), + )} ${coin.ticker}", textAlign: TextAlign.right, style: STextStyles.w600_14(context), ), @@ -163,10 +170,16 @@ class _UtxoRowState extends ConsumerState { mainAxisSize: MainAxisSize.min, children: [ Text( - "${Format.satoshisToAmount( - utxo.value, - coin: coin, - ).toStringAsFixed(coin.decimals)} ${coin.ticker}", + "${Amount( + rawValue: BigInt.from(utxo.value), + fractionDigits: coin.decimals, + ).localizedStringAsFixed( + locale: ref.watch( + localeServiceChangeNotifierProvider.select( + (value) => value.locale, + ), + ), + )} ${coin.ticker}", textAlign: TextAlign.right, style: STextStyles.w600_14(context), ), diff --git a/lib/pages_desktop_specific/desktop_exchange/desktop_all_trades_view.dart b/lib/pages_desktop_specific/desktop_exchange/desktop_all_trades_view.dart index d4e79c42a..34d688b21 100644 --- a/lib/pages_desktop_specific/desktop_exchange/desktop_all_trades_view.dart +++ b/lib/pages_desktop_specific/desktop_exchange/desktop_all_trades_view.dart @@ -29,7 +29,7 @@ import 'package:stackwallet/widgets/stack_text_field.dart'; import 'package:stackwallet/widgets/textfield_icon_button.dart'; import 'package:tuple/tuple.dart'; -import '../../db/main_db.dart'; +import '../../db/isar/main_db.dart'; class DesktopAllTradesView extends ConsumerStatefulWidget { const DesktopAllTradesView({Key? key}) : super(key: key); diff --git a/lib/pages_desktop_specific/desktop_exchange/exchange_steps/step_scaffold.dart b/lib/pages_desktop_specific/desktop_exchange/exchange_steps/step_scaffold.dart index 1c4ef0143..203ae2d9b 100644 --- a/lib/pages_desktop_specific/desktop_exchange/exchange_steps/step_scaffold.dart +++ b/lib/pages_desktop_specific/desktop_exchange/exchange_steps/step_scaffold.dart @@ -17,6 +17,7 @@ import 'package:stackwallet/providers/global/trades_service_provider.dart'; import 'package:stackwallet/route_generator.dart'; import 'package:stackwallet/services/exchange/exchange_response.dart'; import 'package:stackwallet/services/notifications_api.dart'; +import 'package:stackwallet/utilities/amount/amount.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:stackwallet/utilities/enums/exchange_rate_type_enum.dart'; @@ -182,10 +183,11 @@ class _StepScaffoldState extends ConsumerState { void sendFromStack() { final trade = ref.read(desktopExchangeModelProvider)!.trade!; - final amount = Decimal.parse(trade.payInAmount); final address = trade.payInAddress; - final coin = coinFromTickerCaseInsensitive(trade.payInCurrency); + final amount = Decimal.parse(trade.payInAmount).toAmount( + fractionDigits: coin.decimals, + ); showDialog( context: context, diff --git a/lib/pages_desktop_specific/desktop_exchange/subwidgets/desktop_choose_from_stack.dart b/lib/pages_desktop_specific/desktop_exchange/subwidgets/desktop_choose_from_stack.dart index 2c1d8a44d..76bd0168a 100644 --- a/lib/pages_desktop_specific/desktop_exchange/subwidgets/desktop_choose_from_stack.dart +++ b/lib/pages_desktop_specific/desktop_exchange/subwidgets/desktop_choose_from_stack.dart @@ -1,4 +1,3 @@ -import 'package:decimal/decimal.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/svg.dart'; @@ -6,10 +5,8 @@ import 'package:stackwallet/providers/providers.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; -import 'package:stackwallet/utilities/format.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; -import 'package:stackwallet/widgets/animated_text.dart'; import 'package:stackwallet/widgets/custom_buttons/blue_text_button.dart'; import 'package:stackwallet/widgets/desktop/secondary_button.dart'; import 'package:stackwallet/widgets/icon_widgets/x_icon.dart'; @@ -268,7 +265,7 @@ class _DesktopChooseFromStackState } } -class BalanceDisplay extends ConsumerStatefulWidget { +class BalanceDisplay extends ConsumerWidget { const BalanceDisplay({ Key? key, required this.walletId, @@ -277,65 +274,19 @@ class BalanceDisplay extends ConsumerStatefulWidget { final String walletId; @override - ConsumerState createState() => _BalanceDisplayState(); -} - -class _BalanceDisplayState extends ConsumerState { - late final String walletId; - - Decimal? _cachedBalance; - - static const loopedText = [ - "Loading balance ", - "Loading balance. ", - "Loading balance.. ", - "Loading balance..." - ]; - - @override - void initState() { - walletId = widget.walletId; - super.initState(); - } - - @override - Widget build(BuildContext context) { + Widget build(BuildContext context, WidgetRef ref) { final manager = ref.watch(walletsChangeNotifierProvider .select((value) => value.getManager(walletId))); final locale = ref.watch( localeServiceChangeNotifierProvider.select((value) => value.locale)); - // TODO redo this widget now that its not actually a future - return FutureBuilder( - future: Future(() => manager.balance.getSpendable()), - builder: (context, AsyncSnapshot snapshot) { - if (snapshot.connectionState == ConnectionState.done && - snapshot.hasData && - snapshot.data != null) { - _cachedBalance = snapshot.data; - } - - if (_cachedBalance == null) { - return AnimatedText( - stringsToLoopThrough: loopedText, - style: STextStyles.desktopTextExtraSmall(context).copyWith( - color: Theme.of(context).extension()!.textSubtitle1, - ), - ); - } else { - return Text( - "${Format.localizedStringAsFixed( - value: _cachedBalance!, - locale: locale, - decimalPlaces: 8, - )} ${manager.coin.ticker}", - style: STextStyles.desktopTextExtraSmall(context).copyWith( - color: Theme.of(context).extension()!.textSubtitle1, - ), - textAlign: TextAlign.right, - ); - } - }, + return Text( + "${manager.balance.spendable.localizedStringAsFixed(locale: locale)} " + "${manager.coin.ticker}", + style: STextStyles.desktopTextExtraSmall(context).copyWith( + color: Theme.of(context).extension()!.textSubtitle1, + ), + textAlign: TextAlign.right, ); } } diff --git a/lib/pages_desktop_specific/desktop_exchange/subwidgets/desktop_trade_history.dart b/lib/pages_desktop_specific/desktop_exchange/subwidgets/desktop_trade_history.dart index b87ca6c6b..a7dcca99f 100644 --- a/lib/pages_desktop_specific/desktop_exchange/subwidgets/desktop_trade_history.dart +++ b/lib/pages_desktop_specific/desktop_exchange/subwidgets/desktop_trade_history.dart @@ -19,7 +19,7 @@ import 'package:stackwallet/widgets/desktop/desktop_dialog_close_button.dart'; import 'package:stackwallet/widgets/rounded_white_container.dart'; import 'package:stackwallet/widgets/trade_card.dart'; -import '../../../db/main_db.dart'; +import '../../../db/isar/main_db.dart'; class DesktopTradeHistory extends ConsumerStatefulWidget { const DesktopTradeHistory({Key? key}) : super(key: key); diff --git a/lib/pages_desktop_specific/desktop_menu.dart b/lib/pages_desktop_specific/desktop_menu.dart index 8db3f3425..53a4cff7c 100644 --- a/lib/pages_desktop_specific/desktop_menu.dart +++ b/lib/pages_desktop_specific/desktop_menu.dart @@ -1,3 +1,5 @@ +import 'dart:io'; + import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; @@ -225,19 +227,20 @@ class _DesktopMenuState extends ConsumerState { controller: controllers[7], ), const Spacer(), - DesktopMenuItem( - duration: duration, - labelLength: 123, - icon: const DesktopExitIcon(), - label: "Exit", - value: 7, - onChanged: (_) { - // todo: save stuff/ notify before exit? - // exit(0); - SystemNavigator.pop(); - }, - controller: controllers[8], - ), + if (!Platform.isIOS) + DesktopMenuItem( + duration: duration, + labelLength: 123, + icon: const DesktopExitIcon(), + label: "Exit", + value: 7, + onChanged: (_) { + // todo: save stuff/ notify before exit? + // exit(0); + SystemNavigator.pop(); + }, + controller: controllers[8], + ), ], ), ), diff --git a/lib/pages_desktop_specific/my_stack_view/dialogs/desktop_coin_wallets_dialog.dart b/lib/pages_desktop_specific/my_stack_view/dialogs/desktop_coin_wallets_dialog.dart new file mode 100644 index 000000000..46d8a3b19 --- /dev/null +++ b/lib/pages_desktop_specific/my_stack_view/dialogs/desktop_coin_wallets_dialog.dart @@ -0,0 +1,435 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:flutter_svg/svg.dart'; +import 'package:stackwallet/models/isar/models/ethereum/eth_contract.dart'; +import 'package:stackwallet/providers/db/main_db_provider.dart'; +import 'package:stackwallet/providers/providers.dart'; +import 'package:stackwallet/services/coins/ethereum/ethereum_wallet.dart'; +import 'package:stackwallet/services/coins/manager.dart'; +import 'package:stackwallet/utilities/assets.dart'; +import 'package:stackwallet/utilities/constants.dart'; +import 'package:stackwallet/utilities/enums/coin_enum.dart'; +import 'package:stackwallet/utilities/text_styles.dart'; +import 'package:stackwallet/utilities/theme/stack_colors.dart'; +import 'package:stackwallet/utilities/util.dart'; +import 'package:stackwallet/widgets/animated_widgets/rotate_icon.dart'; +import 'package:stackwallet/widgets/expandable.dart'; +import 'package:stackwallet/widgets/icon_widgets/x_icon.dart'; +import 'package:stackwallet/widgets/rounded_white_container.dart'; +import 'package:stackwallet/widgets/stack_text_field.dart'; +import 'package:stackwallet/widgets/textfield_icon_button.dart'; +import 'package:stackwallet/widgets/wallet_card.dart'; +import 'package:stackwallet/widgets/wallet_info_row/sub_widgets/wallet_info_row_balance_future.dart'; +import 'package:stackwallet/widgets/wallet_info_row/sub_widgets/wallet_info_row_coin_icon.dart'; +import 'package:tuple/tuple.dart'; + +class DesktopCoinWalletsDialog extends ConsumerStatefulWidget { + const DesktopCoinWalletsDialog({ + Key? key, + required this.coin, + required this.navigatorState, + }) : super(key: key); + + final Coin coin; + final NavigatorState navigatorState; + + @override + ConsumerState createState() => + _DesktopCoinWalletsDialogState(); +} + +class _DesktopCoinWalletsDialogState + extends ConsumerState { + final isDesktop = Util.isDesktop; + + late final TextEditingController _searchController; + late final FocusNode searchFieldFocusNode; + + String _searchString = ""; + + final List>> wallets = []; + + List>> _filter(String searchTerm) { + if (searchTerm.isEmpty) { + return wallets; + } + + final List>> results = []; + final term = searchTerm.toLowerCase(); + + for (final tuple in wallets) { + bool includeManager = false; + // search wallet name and total balance + includeManager |= _elementContains(tuple.item1.walletName, term); + includeManager |= _elementContains( + tuple.item1.balance.total.decimal.toString(), + term, + ); + + final List contracts = []; + + for (final contract in tuple.item2) { + if (_elementContains(contract.name, term)) { + contracts.add(contract); + } else if (_elementContains(contract.symbol, term)) { + contracts.add(contract); + } else if (_elementContains(contract.type.name, term)) { + contracts.add(contract); + } else if (_elementContains(contract.address, term)) { + contracts.add(contract); + } + } + + if (includeManager || contracts.isNotEmpty) { + results.add(Tuple2(tuple.item1, contracts)); + } + } + + return results; + } + + bool _elementContains(String element, String term) { + return element.toLowerCase().contains(term); + } + + @override + void initState() { + _searchController = TextEditingController(); + searchFieldFocusNode = FocusNode(); + + final walletsData = + ref.read(walletsServiceChangeNotifierProvider).fetchWalletsData(); + walletsData.removeWhere((key, value) => value.coin != widget.coin); + + if (widget.coin == Coin.ethereum) { + for (final data in walletsData.values) { + final List contracts = []; + final manager = + ref.read(walletsChangeNotifierProvider).getManager(data.walletId); + final contractAddresses = (manager.wallet as EthereumWallet) + .getWalletTokenContractAddresses(); + + // fetch each contract + for (final contractAddress in contractAddresses) { + final contract = ref + .read( + mainDBProvider, + ) + .getEthContractSync( + contractAddress, + ); + + // add it to list if it exists in DB + if (contract != null) { + contracts.add(contract); + } + } + + // add tuple to list + wallets.add( + Tuple2( + ref.read(walletsChangeNotifierProvider).getManager( + data.walletId, + ), + contracts, + ), + ); + } + } else { + // add non token wallet tuple to list + for (final data in walletsData.values) { + wallets.add( + Tuple2( + ref.read(walletsChangeNotifierProvider).getManager( + data.walletId, + ), + [], + ), + ); + } + } + + super.initState(); + } + + @override + void dispose() { + _searchController.dispose(); + searchFieldFocusNode.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Column( + children: [ + ClipRRect( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + child: TextField( + autocorrect: !isDesktop, + enableSuggestions: !isDesktop, + controller: _searchController, + focusNode: searchFieldFocusNode, + onChanged: (value) { + setState(() { + _searchString = value; + }); + }, + style: isDesktop + ? STextStyles.desktopTextExtraSmall(context).copyWith( + color: Theme.of(context) + .extension()! + .textFieldActiveText, + height: 1.8, + ) + : STextStyles.field(context), + decoration: standardInputDecoration( + "Search...", + searchFieldFocusNode, + context, + desktopMed: isDesktop, + ).copyWith( + prefixIcon: Padding( + padding: EdgeInsets.symmetric( + horizontal: isDesktop ? 12 : 10, + vertical: isDesktop ? 18 : 16, + ), + child: SvgPicture.asset( + Assets.svg.search, + width: isDesktop ? 20 : 16, + height: isDesktop ? 20 : 16, + ), + ), + suffixIcon: _searchController.text.isNotEmpty + ? Padding( + padding: const EdgeInsets.only(right: 0), + child: UnconstrainedBox( + child: Row( + children: [ + TextFieldIconButton( + child: const XIcon(), + onTap: () async { + setState(() { + _searchController.text = ""; + _searchString = ""; + }); + }, + ), + ], + ), + ), + ) + : null, + ), + ), + ), + const SizedBox( + height: 16, + ), + Expanded( + child: Builder(builder: (context) { + final data = _filter(_searchString); + return ListView.separated( + itemBuilder: (_, index) => widget.coin == Coin.ethereum + ? _DesktopWalletCard( + key: Key( + "${data[index].item1.walletName}_${data[index].item2.map((e) => e.address).join()}"), + data: data[index], + navigatorState: widget.navigatorState, + ) + : RoundedWhiteContainer( + padding: const EdgeInsets.symmetric( + vertical: 14, + horizontal: 20, + ), + borderColor: Theme.of(context) + .extension()! + .backgroundAppBar, + child: WalletSheetCard( + walletId: data[index].item1.walletId, + popPrevious: true, + desktopNavigatorState: widget.navigatorState, + ), + ), + separatorBuilder: (_, __) => const SizedBox( + height: 10, + ), + itemCount: data.length, + ); + }), + ), + ], + ); + } +} + +class _DesktopWalletCard extends StatefulWidget { + const _DesktopWalletCard({ + Key? key, + required this.data, + required this.navigatorState, + }) : super(key: key); + + final Tuple2> data; + final NavigatorState navigatorState; + + @override + State<_DesktopWalletCard> createState() => _DesktopWalletCardState(); +} + +class _DesktopWalletCardState extends State<_DesktopWalletCard> { + final expandableController = ExpandableController(); + final rotateIconController = RotateIconController(); + final List tokenContractAddresses = []; + + @override + void initState() { + if (widget.data.item1.hasTokenSupport) { + tokenContractAddresses.addAll( + widget.data.item2.map((e) => e.address), + ); + } + + super.initState(); + } + + @override + Widget build(BuildContext context) { + return RoundedWhiteContainer( + padding: EdgeInsets.zero, + borderColor: Theme.of(context).extension()!.backgroundAppBar, + child: Expandable( + initialState: widget.data.item1.hasTokenSupport + ? ExpandableState.expanded + : ExpandableState.collapsed, + controller: expandableController, + expandOverride: () {}, + header: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 20, + vertical: 14, + ), + child: Row( + children: [ + Expanded( + child: Row( + children: [ + Expanded( + flex: 2, + child: Row( + children: [ + WalletInfoCoinIcon( + coin: widget.data.item1.coin, + ), + const SizedBox( + width: 12, + ), + Text( + widget.data.item1.walletName, + style: STextStyles.desktopTextExtraSmall(context) + .copyWith( + color: Theme.of(context) + .extension()! + .textDark, + ), + ), + ], + ), + ), + Expanded( + flex: 4, + child: WalletInfoRowBalance( + walletId: widget.data.item1.walletId, + ), + ), + ], + ), + ), + MaterialButton( + padding: const EdgeInsets.all(5), + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + minWidth: 32, + height: 32, + color: Theme.of(context) + .extension()! + .textFieldDefaultBG, + elevation: 0, + hoverElevation: 0, + disabledElevation: 0, + highlightElevation: 0, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + ), + onPressed: () { + if (expandableController.state == ExpandableState.collapsed) { + rotateIconController.forward?.call(); + } else { + rotateIconController.reverse?.call(); + } + expandableController.toggle?.call(); + }, + child: RotateIcon( + controller: rotateIconController, + icon: RotatedBox( + quarterTurns: 2, + child: SvgPicture.asset( + Assets.svg.chevronDown, + width: 14, + ), + ), + curve: Curves.easeInOut, + ), + ), + ], + ), + ), + body: ListView( + shrinkWrap: true, + primary: false, + children: [ + Container( + width: double.infinity, + height: 1, + color: + Theme.of(context).extension()!.backgroundAppBar, + ), + Padding( + padding: const EdgeInsets.only( + left: 32, + right: 14, + top: 14, + bottom: 14, + ), + child: WalletSheetCard( + walletId: widget.data.item1.walletId, + popPrevious: true, + desktopNavigatorState: widget.navigatorState, + ), + ), + ...tokenContractAddresses.map( + (e) => Padding( + padding: const EdgeInsets.only( + left: 32, + right: 14, + top: 14, + bottom: 14, + ), + child: WalletSheetCard( + walletId: widget.data.item1.walletId, + contractAddress: e, + popPrevious: true, + desktopNavigatorState: widget.navigatorState, + ), + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/pages_desktop_specific/my_stack_view/my_wallets.dart b/lib/pages_desktop_specific/my_stack_view/my_wallets.dart index 1dd345f74..c2a08a35a 100644 --- a/lib/pages_desktop_specific/my_stack_view/my_wallets.dart +++ b/lib/pages_desktop_specific/my_stack_view/my_wallets.dart @@ -22,32 +22,48 @@ class _MyWalletsState extends ConsumerState { .select((value) => value.showFavoriteWallets)); return Padding( - padding: const EdgeInsets.all(24), + padding: const EdgeInsets.only( + top: 24, + left: 14, + right: 14, + bottom: 0, + ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - if (showFavorites) const DesktopFavoriteWallets(), - Row( - children: [ - Text( - "All wallets", - style: STextStyles.desktopTextExtraSmall(context).copyWith( - color: Theme.of(context) - .extension()! - .textFieldActiveSearchIconRight, + if (showFavorites) + const Padding( + padding: EdgeInsets.symmetric( + horizontal: 10, + ), + child: DesktopFavoriteWallets(), + ), + Padding( + padding: const EdgeInsets.all( + 10, + ), + child: Row( + children: [ + Text( + "All wallets", + style: STextStyles.desktopTextExtraSmall(context).copyWith( + color: Theme.of(context) + .extension()! + .textFieldActiveSearchIconRight, + ), ), - ), - const Spacer(), - CustomTextButton( - text: "Add new wallet", - onTap: () { - Navigator.of( - context, - rootNavigator: true, - ).pushNamed(AddWalletView.routeName); - }, - ), - ], + const Spacer(), + CustomTextButton( + text: "Add new wallet", + onTap: () { + Navigator.of( + context, + rootNavigator: true, + ).pushNamed(AddWalletView.routeName); + }, + ), + ], + ), ), const SizedBox( height: 20, diff --git a/lib/pages_desktop_specific/my_stack_view/paynym/desktop_paynym_send_dialog.dart b/lib/pages_desktop_specific/my_stack_view/paynym/desktop_paynym_send_dialog.dart index df24e69c3..77c78c369 100644 --- a/lib/pages_desktop_specific/my_stack_view/paynym/desktop_paynym_send_dialog.dart +++ b/lib/pages_desktop_specific/my_stack_view/paynym/desktop_paynym_send_dialog.dart @@ -10,11 +10,11 @@ import 'package:stackwallet/providers/global/price_provider.dart'; import 'package:stackwallet/providers/global/wallets_provider.dart'; import 'package:stackwallet/providers/wallet/public_private_balance_state_provider.dart'; import 'package:stackwallet/services/coins/firo/firo_wallet.dart'; +import 'package:stackwallet/utilities/amount/amount.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/barcode_scanner_interface.dart'; import 'package:stackwallet/utilities/clipboard_interface.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; -import 'package:stackwallet/utilities/format.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; import 'package:stackwallet/widgets/desktop/desktop_dialog.dart'; @@ -120,22 +120,15 @@ class _DesktopPaynymSendDialogState crossAxisAlignment: CrossAxisAlignment.end, children: [ Text( - "${Format.localizedStringAsFixed( - value: !isFiro - ? manager.balance.getSpendable() - : ref - .watch( - publicPrivateBalanceStateProvider - .state) - .state == - "Private" - ? (manager.wallet as FiroWallet) - .availablePrivateBalance() - : (manager.wallet as FiroWallet) - .availablePublicBalance(), - locale: locale, - decimalPlaces: 8, - )} ${coin.ticker}", + "${!isFiro ? manager.balance.spendable.localizedStringAsFixed( + locale: locale, + ) : ref.watch( + publicPrivateBalanceStateProvider.state, + ).state == "Private" ? (manager.wallet as FiroWallet).availablePrivateBalance().localizedStringAsFixed( + locale: locale, + ) : (manager.wallet as FiroWallet).availablePublicBalance().localizedStringAsFixed( + locale: locale, + )} ${coin.ticker}", style: STextStyles.titleBold12(context), textAlign: TextAlign.right, ), @@ -143,25 +136,7 @@ class _DesktopPaynymSendDialogState height: 2, ), Text( - "${Format.localizedStringAsFixed( - value: (!isFiro - ? manager.balance.getSpendable() - : ref - .watch( - publicPrivateBalanceStateProvider - .state) - .state == - "Private" - ? (manager.wallet as FiroWallet) - .availablePrivateBalance() - : (manager.wallet as FiroWallet) - .availablePublicBalance()) * - ref.watch( - priceAnd24hChangeNotifierProvider.select( - (value) => value.getPrice(coin).item1)), - locale: locale, - decimalPlaces: 2, - )} ${ref.watch(prefsChangeNotifierProvider.select((value) => value.currency))}", + "${((!isFiro ? manager.balance.spendable.decimal : ref.watch(publicPrivateBalanceStateProvider.state).state == "Private" ? (manager.wallet as FiroWallet).availablePrivateBalance().decimal : (manager.wallet as FiroWallet).availablePublicBalance().decimal) * ref.watch(priceAnd24hChangeNotifierProvider.select((value) => value.getPrice(coin).item1))).toAmount(fractionDigits: 2).localizedStringAsFixed(locale: locale)} ${ref.watch(prefsChangeNotifierProvider.select((value) => value.currency))}", style: STextStyles.baseXS(context).copyWith( color: Theme.of(context) .extension()! diff --git a/lib/pages_desktop_specific/my_stack_view/wallet_summary_table.dart b/lib/pages_desktop_specific/my_stack_view/wallet_summary_table.dart index c1fd12542..9741ae905 100644 --- a/lib/pages_desktop_specific/my_stack_view/wallet_summary_table.dart +++ b/lib/pages_desktop_specific/my_stack_view/wallet_summary_table.dart @@ -1,18 +1,17 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/svg.dart'; -import 'package:stackwallet/pages_desktop_specific/my_stack_view/coin_wallets_table.dart'; -import 'package:stackwallet/pages_desktop_specific/my_stack_view/wallet_view/desktop_wallet_view.dart'; +import 'package:stackwallet/pages_desktop_specific/my_stack_view/dialogs/desktop_coin_wallets_dialog.dart'; import 'package:stackwallet/providers/providers.dart'; +import 'package:stackwallet/utilities/amount/amount.dart'; import 'package:stackwallet/utilities/assets.dart'; -import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; -import 'package:stackwallet/utilities/format.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; -import 'package:stackwallet/widgets/table_view/table_view.dart'; -import 'package:stackwallet/widgets/table_view/table_view_cell.dart'; -import 'package:stackwallet/widgets/table_view/table_view_row.dart'; +import 'package:stackwallet/widgets/conditional_parent.dart'; +import 'package:stackwallet/widgets/desktop/desktop_dialog.dart'; +import 'package:stackwallet/widgets/desktop/desktop_dialog_close_button.dart'; +import 'package:stackwallet/widgets/rounded_white_container.dart'; class WalletSummaryTable extends ConsumerStatefulWidget { const WalletSummaryTable({Key? key}) : super(key: key); @@ -31,94 +30,158 @@ class _WalletTableState extends ConsumerState { ), ); - return TableView( - rows: [ - for (int i = 0; i < providersByCoin.length; i++) - Builder( - key: Key("${providersByCoin[i].item1.name}_${runtimeType}_key"), - builder: (context) { - final providers = providersByCoin[i].item2; + return ListView.separated( + itemBuilder: (_, index) { + final providers = providersByCoin[index].item2; + final coin = providersByCoin[index].item1; - VoidCallback? expandOverride; - if (providers.length == 1) { - expandOverride = () async { - final manager = ref.read(providers.first); - if (manager.coin == Coin.monero || - manager.coin == Coin.wownero) { - await manager.initializeExisting(); - } - await Navigator.of(context).pushNamed( - DesktopWalletView.routeName, - arguments: manager.walletId, - ); - }; - } + return ConditionalParent( + condition: index + 1 == providersByCoin.length, + builder: (child) => const Padding( + padding: EdgeInsets.only( + bottom: 16, + ), + ), + child: DesktopWalletSummaryRow( + key: Key("DesktopWalletSummaryRow_key_${coin.name}"), + coin: coin, + walletCount: providers.length, + ), + ); + }, + separatorBuilder: (_, __) => const SizedBox( + height: 10, + ), + itemCount: providersByCoin.length, + ); + } +} - return TableViewRow( - expandOverride: expandOverride, - padding: const EdgeInsets.symmetric( - horizontal: 20, - vertical: 16, - ), - decoration: BoxDecoration( - color: Theme.of(context).extension()!.popupBG, - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, +class DesktopWalletSummaryRow extends StatefulWidget { + const DesktopWalletSummaryRow({ + Key? key, + required this.coin, + required this.walletCount, + }) : super(key: key); + + final Coin coin; + final int walletCount; + + @override + State createState() => + _DesktopWalletSummaryRowState(); +} + +class _DesktopWalletSummaryRowState extends State { + bool _hovering = false; + + void _onPressed() { + showDialog( + context: context, + builder: (_) => DesktopDialog( + maxHeight: 600, + maxWidth: 700, + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Padding( + padding: const EdgeInsets.only(left: 32), + child: Text( + "${widget.coin.prettyName} (${widget.coin.ticker}) wallets", + style: STextStyles.desktopH3(context), ), ), - cells: [ - TableViewCell( - flex: 4, - child: Row( - children: [ - SvgPicture.asset( - Assets.svg.iconFor(coin: providersByCoin[i].item1), - width: 28, - height: 28, - ), - const SizedBox( - width: 10, - ), - Text( - providersByCoin[i].item1.prettyName, - style: STextStyles.desktopTextExtraSmall(context) - .copyWith( - color: Theme.of(context) - .extension()! - .textDark, - ), - ) - ], + const DesktopDialogCloseButton(), + ], + ), + Expanded( + child: Padding( + padding: const EdgeInsets.only( + left: 32, + right: 32, + bottom: 32, + ), + child: DesktopCoinWalletsDialog( + coin: widget.coin, + navigatorState: Navigator.of(context), + ), + ), + ), + ], + ), + ), + ); + } + + @override + Widget build(BuildContext context) { + return MouseRegion( + onEnter: (_) => setState( + () => _hovering = true, + ), + onExit: (_) => setState( + () => _hovering = false, + ), + child: AnimatedScale( + scale: _hovering ? 1.00 : 0.98, + duration: const Duration( + milliseconds: 200, + ), + child: RoundedWhiteContainer( + padding: const EdgeInsets.all(20), + hoverColor: Colors.transparent, + onPressed: _onPressed, + child: Row( + children: [ + Expanded( + flex: 4, + child: Row( + children: [ + SvgPicture.asset( + Assets.svg.iconFor(coin: widget.coin), + width: 28, + height: 28, ), - ), - TableViewCell( - flex: 4, - child: Text( - providers.length == 1 - ? "${providers.length} wallet" - : "${providers.length} wallets", + const SizedBox( + width: 10, + ), + Text( + widget.coin.prettyName, style: STextStyles.desktopTextExtraSmall(context).copyWith( color: Theme.of(context) .extension()! - .textSubtitle1, + .textDark, ), - ), - ), - TableViewCell( - flex: 6, - child: TablePriceInfo( - coin: providersByCoin[i].item1, - ), - ), - ], - expandingChild: CoinWalletsTable( - coin: providersByCoin[i].item1, + ) + ], ), - ); - }, + ), + Expanded( + flex: 4, + child: Text( + widget.walletCount == 1 + ? "${widget.walletCount} wallet" + : "${widget.walletCount} wallets", + style: STextStyles.desktopTextExtraSmall(context).copyWith( + color: Theme.of(context) + .extension()! + .textSubtitle1, + ), + ), + ), + Expanded( + flex: 6, + child: TablePriceInfo( + coin: widget.coin, + ), + ), + ], ), - ], + ), + ), ); } } @@ -142,8 +205,10 @@ class TablePriceInfo extends ConsumerWidget { ), ); - final priceString = Format.localizedStringAsFixed( - value: tuple.item1, + final priceString = Amount.fromDecimal( + tuple.item1, + fractionDigits: 2, + ).localizedStringAsFixed( locale: ref .watch( localeServiceChangeNotifierProvider.notifier, diff --git a/lib/pages_desktop_specific/my_stack_view/wallet_view/desktop_token_view.dart b/lib/pages_desktop_specific/my_stack_view/wallet_view/desktop_token_view.dart new file mode 100644 index 000000000..469aeed71 --- /dev/null +++ b/lib/pages_desktop_specific/my_stack_view/wallet_view/desktop_token_view.dart @@ -0,0 +1,250 @@ +import 'package:event_bus/event_bus.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_svg/svg.dart'; +import 'package:stackwallet/pages/token_view/sub_widgets/token_summary.dart'; +import 'package:stackwallet/pages/token_view/sub_widgets/token_transaction_list_widget.dart'; +import 'package:stackwallet/pages/token_view/token_view.dart'; +import 'package:stackwallet/pages/wallet_view/transaction_views/all_transactions_view.dart'; +import 'package:stackwallet/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_wallet_features.dart'; +import 'package:stackwallet/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_wallet_summary.dart'; +import 'package:stackwallet/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/my_wallet.dart'; +import 'package:stackwallet/providers/providers.dart'; +import 'package:stackwallet/services/event_bus/events/global/wallet_sync_status_changed_event.dart'; +import 'package:stackwallet/utilities/assets.dart'; +import 'package:stackwallet/utilities/text_styles.dart'; +import 'package:stackwallet/utilities/theme/stack_colors.dart'; +import 'package:stackwallet/widgets/custom_buttons/blue_text_button.dart'; +import 'package:stackwallet/widgets/desktop/desktop_app_bar.dart'; +import 'package:stackwallet/widgets/desktop/desktop_scaffold.dart'; +import 'package:stackwallet/widgets/desktop/secondary_button.dart'; +import 'package:stackwallet/widgets/icon_widgets/eth_token_icon.dart'; +import 'package:stackwallet/widgets/rounded_white_container.dart'; + +/// [eventBus] should only be set during testing +class DesktopTokenView extends ConsumerStatefulWidget { + const DesktopTokenView({ + Key? key, + required this.walletId, + this.eventBus, + }) : super(key: key); + + static const String routeName = "/desktopTokenView"; + + final String walletId; + final EventBus? eventBus; + + @override + ConsumerState createState() => _DesktopTokenViewState(); +} + +class _DesktopTokenViewState extends ConsumerState { + static const double sendReceiveColumnWidth = 460; + + late final WalletSyncStatus initialSyncStatus; + + @override + void initState() { + initialSyncStatus = ref.read(tokenServiceProvider)!.isRefreshing + ? WalletSyncStatus.syncing + : WalletSyncStatus.synced; + super.initState(); + } + + @override + void dispose() { + super.dispose(); + } + + @override + Widget build(BuildContext context) { + debugPrint("BUILD: $runtimeType"); + + return DesktopScaffold( + appBar: DesktopAppBar( + background: Theme.of(context).extension()!.popupBG, + leading: Expanded( + flex: 3, + child: Row( + children: [ + const SizedBox( + width: 32, + ), + SecondaryButton( + padding: const EdgeInsets.only( + left: 12, + right: 18, + ), + buttonHeight: ButtonHeight.s, + label: ref.watch( + walletsChangeNotifierProvider.select( + (value) => value.getManager(widget.walletId).walletName, + ), + ), + icon: SvgPicture.asset( + Assets.svg.arrowLeft, + width: 18, + height: 18, + color: Theme.of(context) + .extension()! + .topNavIconPrimary, + ), + onPressed: Navigator.of(context).pop, + ), + const SizedBox( + width: 15, + ), + ], + ), + ), + center: Expanded( + flex: 4, + child: Row( + children: [ + EthTokenIcon( + contractAddress: ref.watch( + tokenServiceProvider.select( + (value) => value!.tokenContract.address, + ), + ), + size: 32, + ), + const SizedBox( + width: 12, + ), + Text( + ref.watch( + tokenServiceProvider.select( + (value) => value!.tokenContract.name, + ), + ), + style: STextStyles.desktopH3(context), + ), + const SizedBox( + width: 12, + ), + CoinTickerTag( + walletId: widget.walletId, + ), + ], + ), + ), + useSpacers: false, + isCompactHeight: true, + ), + body: Padding( + padding: const EdgeInsets.all(24), + child: Column( + children: [ + RoundedWhiteContainer( + padding: const EdgeInsets.all(20), + child: Row( + children: [ + EthTokenIcon( + contractAddress: ref.watch( + tokenServiceProvider.select( + (value) => value!.tokenContract.address, + ), + ), + size: 40, + ), + const SizedBox( + width: 10, + ), + DesktopWalletSummary( + walletId: widget.walletId, + isToken: true, + initialSyncStatus: ref.watch( + walletsChangeNotifierProvider.select((value) => + value.getManager(widget.walletId).isRefreshing)) + ? WalletSyncStatus.syncing + : WalletSyncStatus.synced, + ), + const Spacer(), + DesktopWalletFeatures( + walletId: widget.walletId, + ), + ], + ), + ), + const SizedBox( + height: 24, + ), + Row( + children: [ + SizedBox( + width: sendReceiveColumnWidth, + child: Text( + "My wallet", + style: STextStyles.desktopTextExtraSmall(context).copyWith( + color: Theme.of(context) + .extension()! + .textFieldActiveSearchIconLeft, + ), + ), + ), + const SizedBox( + width: 16, + ), + Expanded( + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Recent transactions", + style: + STextStyles.desktopTextExtraSmall(context).copyWith( + color: Theme.of(context) + .extension()! + .textFieldActiveSearchIconLeft, + ), + ), + CustomTextButton( + text: "See all", + onTap: () { + Navigator.of(context).pushNamed( + AllTransactionsView.routeName, + arguments: widget.walletId, + ); + }, + ), + ], + ), + ), + ], + ), + const SizedBox( + height: 14, + ), + Expanded( + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox( + width: sendReceiveColumnWidth, + child: MyWallet( + walletId: widget.walletId, + contractAddress: ref.watch( + tokenServiceProvider.select( + (value) => value!.tokenContract.address, + ), + ), + ), + ), + const SizedBox( + width: 16, + ), + Expanded( + child: TokenTransactionsList( + walletId: widget.walletId, + ), + ), + ], + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/pages_desktop_specific/my_stack_view/wallet_view/desktop_wallet_view.dart b/lib/pages_desktop_specific/my_stack_view/wallet_view/desktop_wallet_view.dart index 49d135c4f..7e786ace6 100644 --- a/lib/pages_desktop_specific/my_stack_view/wallet_view/desktop_wallet_view.dart +++ b/lib/pages_desktop_specific/my_stack_view/wallet_view/desktop_wallet_view.dart @@ -4,11 +4,13 @@ import 'package:event_bus/event_bus.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/svg.dart'; +import 'package:stackwallet/pages/add_wallet_views/add_token_view/edit_wallet_tokens_view.dart'; +import 'package:stackwallet/pages/token_view/my_tokens_view.dart'; +import 'package:stackwallet/pages/wallet_view/sub_widgets/transactions_list.dart'; import 'package:stackwallet/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_wallet_features.dart'; import 'package:stackwallet/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_wallet_summary.dart'; import 'package:stackwallet/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/my_wallet.dart'; import 'package:stackwallet/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/network_info_button.dart'; -import 'package:stackwallet/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/recent_desktop_transactions.dart'; import 'package:stackwallet/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/wallet_keys_button.dart'; import 'package:stackwallet/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/wallet_options_button.dart'; import 'package:stackwallet/providers/global/auto_swb_service_provider.dart'; @@ -19,10 +21,12 @@ import 'package:stackwallet/services/event_bus/global_event_bus.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/enums/backup_frequency_type.dart'; +import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; import 'package:stackwallet/widgets/background.dart'; import 'package:stackwallet/widgets/conditional_parent.dart'; import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; +import 'package:stackwallet/widgets/custom_buttons/blue_text_button.dart'; import 'package:stackwallet/widgets/custom_loading_overlay.dart'; import 'package:stackwallet/widgets/desktop/desktop_app_bar.dart'; import 'package:stackwallet/widgets/desktop/desktop_scaffold.dart'; @@ -47,6 +51,8 @@ class DesktopWalletView extends ConsumerStatefulWidget { } class _DesktopWalletViewState extends ConsumerState { + static const double sendReceiveColumnWidth = 460; + late final TextEditingController controller; late final EventBus eventBus; @@ -255,11 +261,67 @@ class _DesktopWalletViewState extends ConsumerState { const SizedBox( height: 24, ), + Row( + children: [ + SizedBox( + width: sendReceiveColumnWidth, + child: Text( + "My wallet", + style: + STextStyles.desktopTextExtraSmall(context).copyWith( + color: Theme.of(context) + .extension()! + .textFieldActiveSearchIconLeft, + ), + ), + ), + const SizedBox( + width: 16, + ), + Expanded( + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Tokens", + style: STextStyles.desktopTextExtraSmall(context) + .copyWith( + color: Theme.of(context) + .extension()! + .textFieldActiveSearchIconLeft, + ), + ), + CustomTextButton( + text: "Edit", + onTap: () async { + final result = await showDialog( + context: context, + builder: (context) => EditWalletTokensView( + walletId: widget.walletId, + isDesktopPopup: true, + ), + ); + + if (result == 42) { + // wallet tokens were edited so update ui + setState(() {}); + } + }, + ), + ], + ), + ), + ], + ), + const SizedBox( + height: 14, + ), Expanded( child: Row( + crossAxisAlignment: CrossAxisAlignment.start, children: [ SizedBox( - width: 450, + width: sendReceiveColumnWidth, child: MyWallet( walletId: widget.walletId, ), @@ -268,9 +330,20 @@ class _DesktopWalletViewState extends ConsumerState { width: 16, ), Expanded( - child: RecentDesktopTransactions( - walletId: widget.walletId, - ), + child: ref.watch(walletsChangeNotifierProvider.select( + (value) => value + .getManager(widget.walletId) + .hasTokenSupport)) + ? MyTokensView( + walletId: widget.walletId, + ) + : TransactionsList( + managerProvider: ref.watch( + walletsChangeNotifierProvider.select( + (value) => value.getManagerProvider( + widget.walletId))), + walletId: widget.walletId, + ), ), ], ), diff --git a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_fee_dropdown.dart b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_fee_dropdown.dart index 25e1f47ab..9bd260a34 100644 --- a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_fee_dropdown.dart +++ b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_fee_dropdown.dart @@ -1,5 +1,4 @@ import 'package:cw_core/monero_transaction_priority.dart'; -import 'package:decimal/decimal.dart'; import 'package:dropdown_button2/dropdown_button2.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; @@ -10,11 +9,11 @@ import 'package:stackwallet/providers/global/wallets_provider.dart'; import 'package:stackwallet/providers/ui/fee_rate_type_state_provider.dart'; import 'package:stackwallet/providers/wallet/public_private_balance_state_provider.dart'; import 'package:stackwallet/services/coins/firo/firo_wallet.dart'; +import 'package:stackwallet/utilities/amount/amount.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:stackwallet/utilities/enums/fee_rate_type_enum.dart'; -import 'package:stackwallet/utilities/format.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; import 'package:stackwallet/widgets/animated_text.dart'; @@ -44,8 +43,8 @@ class _DesktopFeeDropDownState extends ConsumerState { "Calculating...", ]; - Future feeFor({ - required int amount, + Future feeFor({ + required Amount amount, required FeeRateType feeRateType, required int feeRate, required Coin coin, @@ -59,24 +58,16 @@ class _DesktopFeeDropDownState extends ConsumerState { if (coin == Coin.monero || coin == Coin.wownero) { final fee = await manager.estimateFeeFor( amount, MoneroTransactionPriority.fast.raw!); - ref.read(feeSheetSessionCacheProvider).fast[amount] = - Format.satoshisToAmount( - fee, - coin: coin, - ); + ref.read(feeSheetSessionCacheProvider).fast[amount] = fee; } else if ((coin == Coin.firo || coin == Coin.firoTestNet) && ref.read(publicPrivateBalanceStateProvider.state).state != "Private") { ref.read(feeSheetSessionCacheProvider).fast[amount] = - Format.satoshisToAmount( - await (manager.wallet as FiroWallet) - .estimateFeeForPublic(amount, feeRate), - coin: coin); + await (manager.wallet as FiroWallet) + .estimateFeeForPublic(amount, feeRate); } else { ref.read(feeSheetSessionCacheProvider).fast[amount] = - Format.satoshisToAmount( - await manager.estimateFeeFor(amount, feeRate), - coin: coin); + await manager.estimateFeeFor(amount, feeRate); } } return ref.read(feeSheetSessionCacheProvider).fast[amount]!; @@ -89,24 +80,16 @@ class _DesktopFeeDropDownState extends ConsumerState { if (coin == Coin.monero || coin == Coin.wownero) { final fee = await manager.estimateFeeFor( amount, MoneroTransactionPriority.regular.raw!); - ref.read(feeSheetSessionCacheProvider).average[amount] = - Format.satoshisToAmount( - fee, - coin: coin, - ); + ref.read(feeSheetSessionCacheProvider).average[amount] = fee; } else if ((coin == Coin.firo || coin == Coin.firoTestNet) && ref.read(publicPrivateBalanceStateProvider.state).state != "Private") { ref.read(feeSheetSessionCacheProvider).average[amount] = - Format.satoshisToAmount( - await (manager.wallet as FiroWallet) - .estimateFeeForPublic(amount, feeRate), - coin: coin); + await (manager.wallet as FiroWallet) + .estimateFeeForPublic(amount, feeRate); } else { ref.read(feeSheetSessionCacheProvider).average[amount] = - Format.satoshisToAmount( - await manager.estimateFeeFor(amount, feeRate), - coin: coin); + await manager.estimateFeeFor(amount, feeRate); } } return ref.read(feeSheetSessionCacheProvider).average[amount]!; @@ -119,24 +102,16 @@ class _DesktopFeeDropDownState extends ConsumerState { if (coin == Coin.monero || coin == Coin.wownero) { final fee = await manager.estimateFeeFor( amount, MoneroTransactionPriority.slow.raw!); - ref.read(feeSheetSessionCacheProvider).slow[amount] = - Format.satoshisToAmount( - fee, - coin: coin, - ); + ref.read(feeSheetSessionCacheProvider).slow[amount] = fee; } else if ((coin == Coin.firo || coin == Coin.firoTestNet) && ref.read(publicPrivateBalanceStateProvider.state).state != "Private") { ref.read(feeSheetSessionCacheProvider).slow[amount] = - Format.satoshisToAmount( - await (manager.wallet as FiroWallet) - .estimateFeeForPublic(amount, feeRate), - coin: coin); + await (manager.wallet as FiroWallet) + .estimateFeeForPublic(amount, feeRate); } else { ref.read(feeSheetSessionCacheProvider).slow[amount] = - Format.satoshisToAmount( - await manager.estimateFeeFor(amount, feeRate), - coin: coin); + await manager.estimateFeeFor(amount, feeRate); } } return ref.read(feeSheetSessionCacheProvider).slow[amount]!; @@ -242,7 +217,7 @@ class _DesktopFeeDropDownState extends ConsumerState { } final sendAmountProvider = - StateProvider.autoDispose((_) => Decimal.zero); + StateProvider.autoDispose((_) => Amount.zero); class FeeDropDownChild extends ConsumerWidget { const FeeDropDownChild({ @@ -257,8 +232,8 @@ class FeeDropDownChild extends ConsumerWidget { final FeeObject? feeObject; final FeeRateType feeRateType; final String walletId; - final Future Function({ - required int amount, + final Future Function({ + required Amount amount, required FeeRateType feeRateType, required int feeRate, required Coin coin, @@ -322,19 +297,20 @@ class FeeDropDownChild extends ConsumerWidget { : feeRateType == FeeRateType.slow ? feeObject!.slow : feeObject!.medium, - amount: Format.decimalAmountToSatoshis( - ref.watch(sendAmountProvider.state).state, - manager.coin, - ), + amount: ref.watch(sendAmountProvider.state).state, ), - builder: (_, AsyncSnapshot snapshot) { + builder: (_, AsyncSnapshot snapshot) { if (snapshot.connectionState == ConnectionState.done && snapshot.hasData) { return Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( - "${feeRateType.prettyName} (~${snapshot.data!} ${manager.coin.ticker})", + "${feeRateType.prettyName} " + "(~${snapshot.data!.decimal.toStringAsFixed( + manager.coin.decimals, + )} " + "${manager.coin.ticker})", style: STextStyles.desktopTextExtraExtraSmall(context).copyWith( color: Theme.of(context) diff --git a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_receive.dart b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_receive.dart index 064c31a08..5ac5bafce 100644 --- a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_receive.dart +++ b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_receive.dart @@ -7,13 +7,13 @@ import 'package:flutter_svg/flutter_svg.dart'; import 'package:qr_flutter/qr_flutter.dart'; import 'package:stackwallet/notifications/show_flush_bar.dart'; import 'package:stackwallet/pages/receive_view/generate_receiving_uri_qr_code_view.dart'; +import 'package:stackwallet/pages/token_view/token_view.dart'; import 'package:stackwallet/providers/providers.dart'; import 'package:stackwallet/route_generator.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/clipboard_interface.dart'; import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; -import 'package:stackwallet/utilities/enums/flush_bar_type.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; import 'package:stackwallet/utilities/util.dart'; @@ -28,10 +28,12 @@ class DesktopReceive extends ConsumerStatefulWidget { const DesktopReceive({ Key? key, required this.walletId, + this.contractAddress, this.clipboard = const ClipboardWrapper(), }) : super(key: key); final String walletId; + final String? contractAddress; final ClipboardInterface clipboard; @override @@ -149,7 +151,11 @@ class _DesktopReceiveState extends ConsumerState { Row( children: [ Text( - "Your ${coin.ticker} address", + "Your ${widget.contractAddress == null ? coin.ticker : ref.watch( + tokenServiceProvider.select( + (value) => value!.tokenContract.symbol, + ), + )} address", style: STextStyles.itemSubtitle(context), ), const Spacer(), @@ -199,11 +205,11 @@ class _DesktopReceiveState extends ConsumerState { ), ), ), - if (coin != Coin.epicCash) + if (coin != Coin.epicCash && coin != Coin.ethereum) const SizedBox( height: 20, ), - if (coin != Coin.epicCash) + if (coin != Coin.epicCash && coin != Coin.ethereum) SecondaryButton( buttonHeight: ButtonHeight.l, onPressed: generateNewAddress, diff --git a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_send.dart b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_send.dart index 97375f2e7..5710be78b 100644 --- a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_send.dart +++ b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_send.dart @@ -25,12 +25,12 @@ import 'package:stackwallet/services/coins/firo/firo_wallet.dart'; import 'package:stackwallet/services/coins/manager.dart'; import 'package:stackwallet/services/mixins/paynym_wallet_interface.dart'; import 'package:stackwallet/utilities/address_utils.dart'; +import 'package:stackwallet/utilities/amount/amount.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/barcode_scanner_interface.dart'; import 'package:stackwallet/utilities/clipboard_interface.dart'; import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; -import 'package:stackwallet/utilities/format.dart'; import 'package:stackwallet/utilities/logger.dart'; import 'package:stackwallet/utilities/prefs.dart'; import 'package:stackwallet/utilities/text_styles.dart'; @@ -88,8 +88,8 @@ class _DesktopSendState extends ConsumerState { String? _note; - Decimal? _amountToSend; - Decimal? _cachedAmountToSend; + Amount? _amountToSend; + Amount? _cachedAmountToSend; String? _address; String? _privateBalanceString; @@ -106,20 +106,19 @@ class _DesktopSendState extends ConsumerState { final manager = ref.read(walletsChangeNotifierProvider).getManager(walletId); - final amount = Format.decimalAmountToSatoshis(_amountToSend!, coin); - int availableBalance; + final Amount amount = _amountToSend!; + final Amount availableBalance; if ((coin == Coin.firo || coin == Coin.firoTestNet)) { if (ref.read(publicPrivateBalanceStateProvider.state).state == "Private") { - availableBalance = Format.decimalAmountToSatoshis( - (manager.wallet as FiroWallet).availablePrivateBalance(), coin); + availableBalance = + (manager.wallet as FiroWallet).availablePrivateBalance(); } else { - availableBalance = Format.decimalAmountToSatoshis( - (manager.wallet as FiroWallet).availablePublicBalance(), coin); + availableBalance = + (manager.wallet as FiroWallet).availablePublicBalance(); } } else { - availableBalance = - Format.decimalAmountToSatoshis(manager.balance.getSpendable(), coin); + availableBalance = manager.balance.spendable; } final coinControlEnabled = @@ -268,7 +267,7 @@ class _DesktopSendState extends ConsumerState { final feeRate = ref.read(feeRateTypeStateProvider); txDataFuture = wallet.preparePaymentCodeSend( paymentCode: paymentCode, - satoshiAmount: amount, + amount: amount, args: { "feeRate": feeRate, "UTXOs": (manager.hasCoinControlSupport && @@ -283,7 +282,7 @@ class _DesktopSendState extends ConsumerState { "Private") { txDataFuture = (manager.wallet as FiroWallet).prepareSendPublic( address: _address!, - satoshiAmount: amount, + amount: amount, args: { "feeRate": ref.read(feeRateTypeStateProvider), "UTXOs": (manager.hasCoinControlSupport && @@ -296,7 +295,7 @@ class _DesktopSendState extends ConsumerState { } else { txDataFuture = manager.prepareSend( address: _address!, - satoshiAmount: amount, + amount: amount, args: { "feeRate": ref.read(feeRateTypeStateProvider), "UTXOs": (manager.hasCoinControlSupport && @@ -435,7 +434,9 @@ class _DesktopSendState extends ConsumerState { cryptoAmount != ",") { _amountToSend = cryptoAmount.contains(",") ? Decimal.parse(cryptoAmount.replaceFirst(",", ".")) - : Decimal.parse(cryptoAmount); + .toAmount(fractionDigits: coin.decimals) + : Decimal.parse(cryptoAmount) + .toAmount(fractionDigits: coin.decimals); if (_cachedAmountToSend != null && _cachedAmountToSend == _amountToSend) { return; @@ -448,11 +449,11 @@ class _DesktopSendState extends ConsumerState { ref.read(priceAnd24hChangeNotifierProvider).getPrice(coin).item1; if (price > Decimal.zero) { - final String fiatAmountString = Format.localizedStringAsFixed( - value: _amountToSend! * price, - locale: ref.read(localeServiceChangeNotifierProvider).locale, - decimalPlaces: 2, - ); + final String fiatAmountString = (_amountToSend!.decimal * price) + .toAmount(fractionDigits: 2) + .localizedStringAsFixed( + locale: ref.read(localeServiceChangeNotifierProvider).locale, + ); baseAmountController.text = fiatAmountString; } @@ -476,17 +477,17 @@ class _DesktopSendState extends ConsumerState { return null; } - void _updatePreviewButtonState(String? address, Decimal? amount) { + void _updatePreviewButtonState(String? address, Amount? amount) { if (isPaynymSend) { ref.read(previewTxButtonStateProvider.state).state = - (amount != null && amount > Decimal.zero); + (amount != null && amount > Amount.zero); } else { final isValidAddress = ref .read(walletsChangeNotifierProvider) .getManager(walletId) .validateAddress(address ?? ""); ref.read(previewTxButtonStateProvider.state).state = - (isValidAddress && amount != null && amount > Decimal.zero); + (isValidAddress && amount != null && amount > Amount.zero); } } @@ -498,15 +499,16 @@ class _DesktopSendState extends ConsumerState { final wallet = ref.read(provider).wallet as FiroWallet?; if (wallet != null) { - Decimal? balance; + Amount? balance; if (private) { balance = wallet.availablePrivateBalance(); } else { balance = wallet.availablePublicBalance(); } - - return Format.localizedStringAsFixed( - value: balance, locale: locale, decimalPlaces: 8); + return balance.localizedStringAsFixed( + locale: locale, + decimalPlaces: coin.decimals, + ); } return null; @@ -577,9 +579,10 @@ class _DesktopSendState extends ConsumerState { // autofill amount field if (results["amount"] != null) { - final amount = Decimal.parse(results["amount"]!); - cryptoAmountController.text = Format.localizedStringAsFixed( - value: amount, + final amount = Decimal.parse(results["amount"]!).toAmount( + fractionDigits: coin.decimals, + ); + cryptoAmountController.text = amount.localizedStringAsFixed( locale: ref.read(localeServiceChangeNotifierProvider).locale, decimalPlaces: Constants.decimalPlacesForCoin(coin), ); @@ -643,18 +646,20 @@ class _DesktopSendState extends ConsumerState { baseAmountString != ",") { final baseAmount = baseAmountString.contains(",") ? Decimal.parse(baseAmountString.replaceFirst(",", ".")) - : Decimal.parse(baseAmountString); + .toAmount(fractionDigits: 2) + : Decimal.parse(baseAmountString).toAmount(fractionDigits: 2); var _price = ref.read(priceAnd24hChangeNotifierProvider).getPrice(coin).item1; if (_price == Decimal.zero) { - _amountToSend = Decimal.zero; + _amountToSend = Decimal.zero.toAmount(fractionDigits: coin.decimals); } else { - _amountToSend = baseAmount <= Decimal.zero - ? Decimal.zero - : (baseAmount / _price).toDecimal( - scaleOnInfinitePrecision: Constants.decimalPlacesForCoin(coin)); + _amountToSend = baseAmount <= Amount.zero + ? Decimal.zero.toAmount(fractionDigits: coin.decimals) + : (baseAmount.decimal / _price) + .toDecimal(scaleOnInfinitePrecision: coin.decimals) + .toAmount(fractionDigits: coin.decimals); } if (_cachedAmountToSend != null && _cachedAmountToSend == _amountToSend) { return; @@ -663,17 +668,16 @@ class _DesktopSendState extends ConsumerState { Logging.instance.log("it changed $_amountToSend $_cachedAmountToSend", level: LogLevel.Info); - final amountString = Format.localizedStringAsFixed( - value: _amountToSend!, + final amountString = _amountToSend!.localizedStringAsFixed( locale: ref.read(localeServiceChangeNotifierProvider).locale, - decimalPlaces: Constants.decimalPlacesForCoin(coin), + decimalPlaces: coin.decimals, ); _cryptoAmountChangeLock = true; cryptoAmountController.text = amountString; _cryptoAmountChangeLock = false; } else { - _amountToSend = Decimal.zero; + _amountToSend = Decimal.zero.toAmount(fractionDigits: coin.decimals); _cryptoAmountChangeLock = true; cryptoAmountController.text = ""; _cryptoAmountChangeLock = false; @@ -694,19 +698,24 @@ class _DesktopSendState extends ConsumerState { .wallet as FiroWallet; if (ref.read(publicPrivateBalanceStateProvider.state).state == "Private") { - cryptoAmountController.text = (firoWallet.availablePrivateBalance()) - .toStringAsFixed(Constants.decimalPlacesForCoin(coin)); + cryptoAmountController.text = firoWallet + .availablePrivateBalance() + .decimal + .toStringAsFixed(coin.decimals); } else { - cryptoAmountController.text = (firoWallet.availablePublicBalance()) - .toStringAsFixed(Constants.decimalPlacesForCoin(coin)); + cryptoAmountController.text = firoWallet + .availablePublicBalance() + .decimal + .toStringAsFixed(coin.decimals); } } else { - cryptoAmountController.text = (ref - .read(walletsChangeNotifierProvider) - .getManager(walletId) - .balance - .getSpendable()) - .toStringAsFixed(Constants.decimalPlacesForCoin(coin)); + cryptoAmountController.text = ref + .read(walletsChangeNotifierProvider) + .getManager(walletId) + .balance + .spendable + .decimal + .toStringAsFixed(coin.decimals); } } diff --git a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_token_send.dart b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_token_send.dart new file mode 100644 index 000000000..97be1da9a --- /dev/null +++ b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_token_send.dart @@ -0,0 +1,1011 @@ +import 'dart:async'; + +import 'package:decimal/decimal.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:stackwallet/models/contact_address_entry.dart'; +import 'package:stackwallet/models/paynym/paynym_account_lite.dart'; +import 'package:stackwallet/models/send_view_auto_fill_data.dart'; +import 'package:stackwallet/pages/send_view/confirm_transaction_view.dart'; +import 'package:stackwallet/pages/send_view/sub_widgets/building_transaction_dialog.dart'; +import 'package:stackwallet/pages/send_view/sub_widgets/transaction_fee_selection_sheet.dart'; +import 'package:stackwallet/pages/token_view/token_view.dart'; +import 'package:stackwallet/pages_desktop_specific/desktop_home_view.dart'; +import 'package:stackwallet/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/address_book_address_chooser/address_book_address_chooser.dart'; +import 'package:stackwallet/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_fee_dropdown.dart'; +import 'package:stackwallet/providers/providers.dart'; +import 'package:stackwallet/providers/ui/fee_rate_type_state_provider.dart'; +import 'package:stackwallet/providers/ui/preview_tx_button_state_provider.dart'; +import 'package:stackwallet/services/coins/manager.dart'; +import 'package:stackwallet/utilities/address_utils.dart'; +import 'package:stackwallet/utilities/amount/amount.dart'; +import 'package:stackwallet/utilities/barcode_scanner_interface.dart'; +import 'package:stackwallet/utilities/clipboard_interface.dart'; +import 'package:stackwallet/utilities/constants.dart'; +import 'package:stackwallet/utilities/enums/coin_enum.dart'; +import 'package:stackwallet/utilities/logger.dart'; +import 'package:stackwallet/utilities/prefs.dart'; +import 'package:stackwallet/utilities/text_styles.dart'; +import 'package:stackwallet/utilities/theme/stack_colors.dart'; +import 'package:stackwallet/utilities/util.dart'; +import 'package:stackwallet/widgets/custom_buttons/blue_text_button.dart'; +import 'package:stackwallet/widgets/desktop/desktop_dialog.dart'; +import 'package:stackwallet/widgets/desktop/desktop_dialog_close_button.dart'; +import 'package:stackwallet/widgets/desktop/primary_button.dart'; +import 'package:stackwallet/widgets/desktop/secondary_button.dart'; +import 'package:stackwallet/widgets/icon_widgets/addressbook_icon.dart'; +import 'package:stackwallet/widgets/icon_widgets/clipboard_icon.dart'; +import 'package:stackwallet/widgets/icon_widgets/x_icon.dart'; +import 'package:stackwallet/widgets/stack_text_field.dart'; +import 'package:stackwallet/widgets/textfield_icon_button.dart'; + +const _kCryptoAmountRegex = r'^([0-9]*[,.]?[0-9]{0,8}|[,.][0-9]{0,8})$'; + +class DesktopTokenSend extends ConsumerStatefulWidget { + const DesktopTokenSend({ + Key? key, + required this.walletId, + this.autoFillData, + this.clipboard = const ClipboardWrapper(), + this.barcodeScanner = const BarcodeScannerWrapper(), + this.accountLite, + }) : super(key: key); + + final String walletId; + final SendViewAutoFillData? autoFillData; + final ClipboardInterface clipboard; + final BarcodeScannerInterface barcodeScanner; + final PaynymAccountLite? accountLite; + + @override + ConsumerState createState() => _DesktopTokenSendState(); +} + +class _DesktopTokenSendState extends ConsumerState { + late final String walletId; + late final Coin coin; + late final ClipboardInterface clipboard; + late final BarcodeScannerInterface scanner; + + late TextEditingController sendToController; + late TextEditingController cryptoAmountController; + late TextEditingController baseAmountController; + + late final SendViewAutoFillData? _data; + + final _addressFocusNode = FocusNode(); + final _cryptoFocus = FocusNode(); + final _baseFocus = FocusNode(); + + String? _note; + + Amount? _amountToSend; + Amount? _cachedAmountToSend; + String? _address; + + bool _addressToggleFlag = false; + + bool _cryptoAmountChangeLock = false; + late VoidCallback onCryptoAmountChanged; + + Future previewSend() async { + final tokenWallet = ref.read(tokenServiceProvider)!; + + final Amount amount = _amountToSend!; + final Amount availableBalance = tokenWallet.balance.spendable; + + // confirm send all + if (amount == availableBalance) { + final bool? shouldSendAll = await showDialog( + context: context, + useSafeArea: false, + barrierDismissible: true, + builder: (context) { + return DesktopDialog( + maxWidth: 450, + maxHeight: double.infinity, + child: Padding( + padding: const EdgeInsets.only( + left: 32, + bottom: 32, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Confirm send all", + style: STextStyles.desktopH3(context), + ), + const DesktopDialogCloseButton(), + ], + ), + const SizedBox( + height: 12, + ), + Padding( + padding: const EdgeInsets.only( + right: 32, + ), + child: Text( + "You are about to send your entire balance. Would you like to continue?", + textAlign: TextAlign.left, + style: STextStyles.desktopTextExtraExtraSmall(context) + .copyWith( + fontSize: 18, + ), + ), + ), + const SizedBox( + height: 40, + ), + Padding( + padding: const EdgeInsets.only( + right: 32, + ), + child: Row( + children: [ + Expanded( + child: SecondaryButton( + buttonHeight: ButtonHeight.l, + label: "Cancel", + onPressed: () { + Navigator.of(context).pop(false); + }, + ), + ), + const SizedBox( + width: 16, + ), + Expanded( + child: PrimaryButton( + buttonHeight: ButtonHeight.l, + label: "Yes", + onPressed: () { + Navigator.of(context).pop(true); + }, + ), + ), + ], + ), + ), + ], + ), + ), + ); + }, + ); + + if (shouldSendAll == null || shouldSendAll == false) { + // cancel preview + return; + } + } + + try { + bool wasCancelled = false; + + if (mounted) { + unawaited( + showDialog( + context: context, + useSafeArea: false, + barrierDismissible: false, + builder: (context) { + return DesktopDialog( + maxWidth: 400, + maxHeight: double.infinity, + child: Padding( + padding: const EdgeInsets.all(32), + child: BuildingTransactionDialog( + coin: tokenWallet.coin, + onCancel: () { + wasCancelled = true; + + Navigator.of(context).pop(); + }, + ), + ), + ); + }, + ), + ); + } + + final time = Future.delayed( + const Duration( + milliseconds: 2500, + ), + ); + + Map txData; + Future> txDataFuture; + + txDataFuture = tokenWallet.prepareSend( + address: _address!, + amount: amount, + args: { + "feeRate": ref.read(feeRateTypeStateProvider), + }, + ); + + final results = await Future.wait([ + txDataFuture, + time, + ]); + + txData = results.first as Map; + + if (!wasCancelled && mounted) { + txData["address"] = _address; + txData["note"] = _note ?? ""; + + // pop building dialog + Navigator.of( + context, + rootNavigator: true, + ).pop(); + + unawaited( + showDialog( + context: context, + builder: (context) => DesktopDialog( + maxHeight: double.infinity, + maxWidth: 580, + child: ConfirmTransactionView( + transactionInfo: txData, + walletId: walletId, + isTokenTx: true, + routeOnSuccessName: DesktopHomeView.routeName, + ), + ), + ), + ); + } + } catch (e) { + if (mounted) { + // pop building dialog + Navigator.of( + context, + rootNavigator: true, + ).pop(); + + unawaited( + showDialog( + context: context, + builder: (context) { + return DesktopDialog( + maxWidth: 450, + maxHeight: double.infinity, + child: Padding( + padding: const EdgeInsets.only( + left: 32, + bottom: 32, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Transaction failed", + style: STextStyles.desktopH3(context), + ), + const DesktopDialogCloseButton(), + ], + ), + const SizedBox( + height: 12, + ), + Padding( + padding: const EdgeInsets.only( + right: 32, + ), + child: Text( + e.toString(), + textAlign: TextAlign.left, + style: STextStyles.desktopTextExtraExtraSmall(context) + .copyWith( + fontSize: 18, + ), + ), + ), + const SizedBox( + height: 40, + ), + Row( + children: [ + Expanded( + child: SecondaryButton( + buttonHeight: ButtonHeight.l, + label: "Ok", + onPressed: () { + Navigator.of( + context, + rootNavigator: true, + ).pop(); + }, + ), + ), + const SizedBox( + width: 32, + ), + ], + ), + ], + ), + ), + ); + }, + ), + ); + } + } + } + + void _cryptoAmountChanged() async { + if (!_cryptoAmountChangeLock) { + final String cryptoAmount = cryptoAmountController.text; + if (cryptoAmount.isNotEmpty && + cryptoAmount != "." && + cryptoAmount != ",") { + _amountToSend = cryptoAmount.contains(",") + ? Decimal.parse(cryptoAmount.replaceFirst(",", ".")).toAmount( + fractionDigits: + ref.read(tokenServiceProvider)!.tokenContract.decimals, + ) + : Decimal.parse(cryptoAmount).toAmount( + fractionDigits: + ref.read(tokenServiceProvider)!.tokenContract.decimals, + ); + if (_cachedAmountToSend != null && + _cachedAmountToSend == _amountToSend) { + return; + } + Logging.instance.log("it changed $_amountToSend $_cachedAmountToSend", + level: LogLevel.Info); + _cachedAmountToSend = _amountToSend; + + final price = + ref.read(priceAnd24hChangeNotifierProvider).getPrice(coin).item1; + + if (price > Decimal.zero) { + final String fiatAmountString = Amount.fromDecimal( + _amountToSend!.decimal * price, + fractionDigits: 2, + ).localizedStringAsFixed( + locale: ref.read(localeServiceChangeNotifierProvider).locale, + decimalPlaces: 2, + ); + + baseAmountController.text = fiatAmountString; + } + } else { + _amountToSend = null; + _cachedAmountToSend = null; + baseAmountController.text = ""; + } + + _updatePreviewButtonState(_address, _amountToSend); + } + } + + String? _updateInvalidAddressText(String address, Manager manager) { + if (_data != null && _data!.contactLabel == address) { + return null; + } + if (address.isNotEmpty && !manager.validateAddress(address)) { + return "Invalid address"; + } + return null; + } + + void _updatePreviewButtonState(String? address, Amount? amount) { + final isValidAddress = ref + .read(walletsChangeNotifierProvider) + .getManager(walletId) + .validateAddress(address ?? ""); + ref.read(previewTxButtonStateProvider.state).state = + (isValidAddress && amount != null && amount > Amount.zero); + } + + Future scanQr() async { + try { + if (FocusScope.of(context).hasFocus) { + FocusScope.of(context).unfocus(); + await Future.delayed(const Duration(milliseconds: 75)); + } + + final qrResult = await scanner.scan(); + + Logging.instance.log("qrResult content: ${qrResult.rawContent}", + level: LogLevel.Info); + + final results = AddressUtils.parseUri(qrResult.rawContent); + + Logging.instance.log("qrResult parsed: $results", level: LogLevel.Info); + + if (results.isNotEmpty && results["scheme"] == coin.uriScheme) { + // auto fill address + _address = results["address"] ?? ""; + sendToController.text = _address!; + + // autofill notes field + if (results["message"] != null) { + _note = results["message"]!; + } else if (results["label"] != null) { + _note = results["label"]!; + } + + // autofill amount field + if (results["amount"] != null) { + final amount = Decimal.parse(results["amount"]!).toAmount( + fractionDigits: + ref.read(tokenServiceProvider)!.tokenContract.decimals, + ); + cryptoAmountController.text = amount.localizedStringAsFixed( + locale: ref.read(localeServiceChangeNotifierProvider).locale, + ); + + amount.toString(); + _amountToSend = amount; + } + + _updatePreviewButtonState(_address, _amountToSend); + setState(() { + _addressToggleFlag = sendToController.text.isNotEmpty; + }); + + // now check for non standard encoded basic address + } else if (ref + .read(walletsChangeNotifierProvider) + .getManager(walletId) + .validateAddress(qrResult.rawContent)) { + _address = qrResult.rawContent; + sendToController.text = _address ?? ""; + + _updatePreviewButtonState(_address, _amountToSend); + setState(() { + _addressToggleFlag = sendToController.text.isNotEmpty; + }); + } + } on PlatformException catch (e, s) { + // here we ignore the exception caused by not giving permission + // to use the camera to scan a qr code + Logging.instance.log( + "Failed to get camera permissions while trying to scan qr code in SendView: $e\n$s", + level: LogLevel.Warning); + } + } + + Future pasteAddress() async { + final ClipboardData? data = await clipboard.getData(Clipboard.kTextPlain); + if (data?.text != null && data!.text!.isNotEmpty) { + String content = data.text!.trim(); + if (content.contains("\n")) { + content = content.substring(0, content.indexOf("\n")); + } + + sendToController.text = content; + _address = content; + + _updatePreviewButtonState(_address, _amountToSend); + setState(() { + _addressToggleFlag = sendToController.text.isNotEmpty; + }); + } + } + + void fiatTextFieldOnChanged(String baseAmountString) { + final int tokenDecimals = + ref.read(tokenServiceProvider)!.tokenContract.decimals; + + if (baseAmountString.isNotEmpty && + baseAmountString != "." && + baseAmountString != ",") { + final baseAmount = baseAmountString.contains(",") + ? Decimal.parse(baseAmountString.replaceFirst(",", ".")) + .toAmount(fractionDigits: 2) + : Decimal.parse(baseAmountString).toAmount(fractionDigits: 2); + + final Decimal _price = + ref.read(priceAnd24hChangeNotifierProvider).getPrice(coin).item1; + + if (_price == Decimal.zero) { + _amountToSend = Decimal.zero.toAmount(fractionDigits: tokenDecimals); + } else { + _amountToSend = baseAmount <= Amount.zero + ? Decimal.zero.toAmount(fractionDigits: tokenDecimals) + : (baseAmount.decimal / _price) + .toDecimal(scaleOnInfinitePrecision: tokenDecimals) + .toAmount(fractionDigits: tokenDecimals); + } + if (_cachedAmountToSend != null && _cachedAmountToSend == _amountToSend) { + return; + } + _cachedAmountToSend = _amountToSend; + Logging.instance.log("it changed $_amountToSend $_cachedAmountToSend", + level: LogLevel.Info); + + final amountString = _amountToSend!.localizedStringAsFixed( + locale: ref.read(localeServiceChangeNotifierProvider).locale, + decimalPlaces: tokenDecimals, + ); + + _cryptoAmountChangeLock = true; + cryptoAmountController.text = amountString; + _cryptoAmountChangeLock = false; + } else { + _amountToSend = Decimal.zero.toAmount(fractionDigits: tokenDecimals); + _cryptoAmountChangeLock = true; + cryptoAmountController.text = ""; + _cryptoAmountChangeLock = false; + } + + _updatePreviewButtonState(_address, _amountToSend); + } + + Future sendAllTapped() async { + cryptoAmountController.text = ref + .read(tokenServiceProvider)! + .balance + .spendable + .decimal + .toStringAsFixed( + ref.read(tokenServiceProvider)!.tokenContract.decimals, + ); + } + + @override + void initState() { + WidgetsBinding.instance.addPostFrameCallback((_) { + ref.refresh(feeSheetSessionCacheProvider); + ref.read(previewTxButtonStateProvider.state).state = false; + }); + + // _calculateFeesFuture = calculateFees(0); + _data = widget.autoFillData; + walletId = widget.walletId; + coin = ref.read(walletsChangeNotifierProvider).getManager(walletId).coin; + clipboard = widget.clipboard; + scanner = widget.barcodeScanner; + + sendToController = TextEditingController(); + cryptoAmountController = TextEditingController(); + baseAmountController = TextEditingController(); + // feeController = TextEditingController(); + + onCryptoAmountChanged = _cryptoAmountChanged; + cryptoAmountController.addListener(onCryptoAmountChanged); + + if (_data != null) { + if (_data!.amount != null) { + cryptoAmountController.text = _data!.amount!.toString(); + } + sendToController.text = _data!.contactLabel; + _address = _data!.address; + _addressToggleFlag = true; + } + + _cryptoFocus.addListener(() { + if (!_cryptoFocus.hasFocus && !_baseFocus.hasFocus) { + if (_amountToSend == null) { + ref.refresh(sendAmountProvider); + } else { + ref.read(sendAmountProvider.state).state = _amountToSend!; + } + } + }); + + _baseFocus.addListener(() { + if (!_cryptoFocus.hasFocus && !_baseFocus.hasFocus) { + if (_amountToSend == null) { + ref.refresh(sendAmountProvider); + } else { + ref.read(sendAmountProvider.state).state = _amountToSend!; + } + } + }); + + super.initState(); + } + + @override + void dispose() { + cryptoAmountController.removeListener(onCryptoAmountChanged); + + sendToController.dispose(); + cryptoAmountController.dispose(); + baseAmountController.dispose(); + // feeController.dispose(); + + _addressFocusNode.dispose(); + _cryptoFocus.dispose(); + _baseFocus.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + debugPrint("BUILD: $runtimeType"); + + final tokenContract = ref.watch(tokenServiceProvider)!.tokenContract; + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox( + height: 4, + ), + if (coin == Coin.firo) + Text( + "Send from", + style: STextStyles.desktopTextExtraSmall(context).copyWith( + color: Theme.of(context) + .extension()! + .textFieldActiveSearchIconRight, + ), + textAlign: TextAlign.left, + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Amount", + style: STextStyles.desktopTextExtraSmall(context).copyWith( + color: Theme.of(context) + .extension()! + .textFieldActiveSearchIconRight, + ), + textAlign: TextAlign.left, + ), + CustomTextButton( + text: "Send all ${tokenContract.symbol}", + onTap: sendAllTapped, + ), + ], + ), + const SizedBox( + height: 10, + ), + TextField( + autocorrect: Util.isDesktop ? false : true, + enableSuggestions: Util.isDesktop ? false : true, + style: STextStyles.smallMed14(context).copyWith( + color: Theme.of(context).extension()!.textDark, + ), + key: const Key("amountInputFieldCryptoTextFieldKey"), + controller: cryptoAmountController, + focusNode: _cryptoFocus, + keyboardType: Util.isDesktop + ? null + : const TextInputType.numberWithOptions( + signed: false, + decimal: true, + ), + textAlign: TextAlign.right, + inputFormatters: [ + // regex to validate a crypto amount with 8 decimal places + TextInputFormatter.withFunction((oldValue, newValue) => RegExp( + _kCryptoAmountRegex.replaceAll( + "0,8", + "0,${tokenContract.decimals}", + ), + ).hasMatch(newValue.text) + ? newValue + : oldValue), + ], + onChanged: (newValue) {}, + decoration: InputDecoration( + contentPadding: const EdgeInsets.only( + top: 22, + right: 12, + bottom: 22, + ), + hintText: "0", + hintStyle: STextStyles.desktopTextExtraSmall(context).copyWith( + color: Theme.of(context) + .extension()! + .textFieldDefaultText, + ), + prefixIcon: FittedBox( + fit: BoxFit.scaleDown, + child: Padding( + padding: const EdgeInsets.all(12), + child: Text( + tokenContract.symbol, + style: STextStyles.smallMed14(context).copyWith( + color: Theme.of(context) + .extension()! + .accentColorDark), + ), + ), + ), + ), + ), + if (Prefs.instance.externalCalls) + const SizedBox( + height: 10, + ), + if (Prefs.instance.externalCalls) + TextField( + autocorrect: Util.isDesktop ? false : true, + enableSuggestions: Util.isDesktop ? false : true, + style: STextStyles.smallMed14(context).copyWith( + color: Theme.of(context).extension()!.textDark, + ), + key: const Key("amountInputFieldFiatTextFieldKey"), + controller: baseAmountController, + focusNode: _baseFocus, + keyboardType: Util.isDesktop + ? null + : const TextInputType.numberWithOptions( + signed: false, + decimal: true, + ), + textAlign: TextAlign.right, + inputFormatters: [ + // regex to validate a fiat amount with 2 decimal places + TextInputFormatter.withFunction((oldValue, newValue) => + RegExp(r'^([0-9]*[,.]?[0-9]{0,2}|[,.][0-9]{0,2})$') + .hasMatch(newValue.text) + ? newValue + : oldValue), + ], + onChanged: fiatTextFieldOnChanged, + decoration: InputDecoration( + contentPadding: const EdgeInsets.only( + top: 22, + right: 12, + bottom: 22, + ), + hintText: "0", + hintStyle: STextStyles.desktopTextExtraSmall(context).copyWith( + color: Theme.of(context) + .extension()! + .textFieldDefaultText, + ), + prefixIcon: FittedBox( + fit: BoxFit.scaleDown, + child: Padding( + padding: const EdgeInsets.all(12), + child: Text( + ref.watch(prefsChangeNotifierProvider + .select((value) => value.currency)), + style: STextStyles.smallMed14(context).copyWith( + color: Theme.of(context) + .extension()! + .accentColorDark), + ), + ), + ), + ), + ), + const SizedBox( + height: 20, + ), + Text( + "Send to", + style: STextStyles.desktopTextExtraSmall(context).copyWith( + color: Theme.of(context) + .extension()! + .textFieldActiveSearchIconRight, + ), + textAlign: TextAlign.left, + ), + const SizedBox( + height: 10, + ), + ClipRRect( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + child: TextField( + minLines: 1, + maxLines: 5, + key: const Key("sendViewAddressFieldKey"), + controller: sendToController, + readOnly: false, + autocorrect: false, + enableSuggestions: false, + // inputFormatters: [ + // FilteringTextInputFormatter.allow( + // RegExp("[a-zA-Z0-9]{34}")), + // ], + toolbarOptions: const ToolbarOptions( + copy: false, + cut: false, + paste: true, + selectAll: false, + ), + onChanged: (newValue) { + _address = newValue; + _updatePreviewButtonState(_address, _amountToSend); + + setState(() { + _addressToggleFlag = newValue.isNotEmpty; + }); + }, + focusNode: _addressFocusNode, + style: STextStyles.desktopTextExtraSmall(context).copyWith( + color: Theme.of(context) + .extension()! + .textFieldActiveText, + height: 1.8, + ), + decoration: standardInputDecoration( + "Enter ${tokenContract.symbol} address", + _addressFocusNode, + context, + desktopMed: true, + ).copyWith( + contentPadding: const EdgeInsets.only( + left: 16, + top: 11, + bottom: 12, + right: 5, + ), + suffixIcon: Padding( + padding: sendToController.text.isEmpty + ? const EdgeInsets.only(right: 8) + : const EdgeInsets.only(right: 0), + child: UnconstrainedBox( + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + _addressToggleFlag + ? TextFieldIconButton( + key: const Key( + "sendTokenViewClearAddressFieldButtonKey"), + onTap: () { + sendToController.text = ""; + _address = ""; + _updatePreviewButtonState( + _address, _amountToSend); + setState(() { + _addressToggleFlag = false; + }); + }, + child: const XIcon(), + ) + : TextFieldIconButton( + key: const Key( + "sendTokenViewPasteAddressFieldButtonKey"), + onTap: pasteAddress, + child: sendToController.text.isEmpty + ? const ClipboardIcon() + : const XIcon(), + ), + if (sendToController.text.isEmpty) + TextFieldIconButton( + key: const Key("sendTokenViewAddressBookButtonKey"), + onTap: () async { + final entry = + await showDialog( + context: context, + builder: (context) => DesktopDialog( + maxWidth: 696, + maxHeight: 600, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + Padding( + padding: const EdgeInsets.only( + left: 32, + ), + child: Text( + "Address book", + style: + STextStyles.desktopH3(context), + ), + ), + const DesktopDialogCloseButton(), + ], + ), + Expanded( + child: AddressBookAddressChooser( + coin: coin, + ), + ), + ], + ), + ), + ); + + if (entry != null) { + sendToController.text = + entry.other ?? entry.label; + + _address = entry.address; + + _updatePreviewButtonState( + _address, + _amountToSend, + ); + + setState(() { + _addressToggleFlag = true; + }); + } + }, + child: const AddressBookIcon(), + ), + ], + ), + ), + ), + ), + ), + ), + Builder( + builder: (_) { + final error = _updateInvalidAddressText( + _address ?? "", + ref.read(walletsChangeNotifierProvider).getManager(walletId), + ); + + if (error == null || error.isEmpty) { + return Container(); + } else { + return Align( + alignment: Alignment.topLeft, + child: Padding( + padding: const EdgeInsets.only( + left: 12.0, + top: 4.0, + ), + child: Text( + error, + textAlign: TextAlign.left, + style: STextStyles.label(context).copyWith( + color: + Theme.of(context).extension()!.textError, + ), + ), + ), + ); + } + }, + ), + const SizedBox( + height: 20, + ), + Text( + "Transaction fee (estimated)", + style: STextStyles.desktopTextExtraSmall(context).copyWith( + color: Theme.of(context) + .extension()! + .textFieldActiveSearchIconRight, + ), + textAlign: TextAlign.left, + ), + const SizedBox( + height: 10, + ), + // TODO mod this for token fees + DesktopFeeDropDown( + walletId: walletId, + ), + const SizedBox( + height: 36, + ), + PrimaryButton( + buttonHeight: ButtonHeight.l, + label: "Preview send", + enabled: ref.watch(previewTxButtonStateProvider.state).state, + onPressed: ref.watch(previewTxButtonStateProvider.state).state + ? previewSend + : null, + ) + ], + ); + } +} diff --git a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_wallet_features.dart b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_wallet_features.dart index da5fd5745..d982956f2 100644 --- a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_wallet_features.dart +++ b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_wallet_features.dart @@ -1,6 +1,5 @@ import 'dart:async'; -import 'package:decimal/decimal.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/flutter_svg.dart'; @@ -18,6 +17,7 @@ import 'package:stackwallet/providers/providers.dart'; import 'package:stackwallet/providers/wallet/my_paynym_account_state_provider.dart'; import 'package:stackwallet/services/coins/firo/firo_wallet.dart'; import 'package:stackwallet/services/mixins/paynym_wallet_interface.dart'; +import 'package:stackwallet/utilities/amount/amount.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; @@ -162,7 +162,7 @@ class _DesktopWalletFeaturesState extends ConsumerState { final firoWallet = ref.read(managerProvider).wallet as FiroWallet; final publicBalance = firoWallet.availablePublicBalance(); - if (publicBalance <= Decimal.zero) { + if (publicBalance <= Amount.zero) { shouldPop = true; if (context.mounted) { Navigator.of(context, rootNavigator: true).pop(); diff --git a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_wallet_summary.dart b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_wallet_summary.dart index 4d30ebc60..921595f43 100644 --- a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_wallet_summary.dart +++ b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_wallet_summary.dart @@ -1,16 +1,16 @@ -import 'package:decimal/decimal.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:stackwallet/models/balance.dart'; +import 'package:stackwallet/pages/token_view/token_view.dart'; import 'package:stackwallet/pages/wallet_view/sub_widgets/wallet_refresh_button.dart'; import 'package:stackwallet/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_balance_toggle_button.dart'; import 'package:stackwallet/providers/providers.dart'; import 'package:stackwallet/providers/wallet/wallet_balance_toggle_state_provider.dart'; import 'package:stackwallet/services/coins/firo/firo_wallet.dart'; import 'package:stackwallet/services/event_bus/events/global/wallet_sync_status_changed_event.dart'; +import 'package:stackwallet/utilities/amount/amount.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:stackwallet/utilities/enums/wallet_balance_toggle_state.dart'; -import 'package:stackwallet/utilities/format.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; @@ -19,10 +19,12 @@ class DesktopWalletSummary extends ConsumerStatefulWidget { Key? key, required this.walletId, required this.initialSyncStatus, + this.isToken = false, }) : super(key: key); final String walletId; final WalletSyncStatus initialSyncStatus; + final bool isToken; @override ConsumerState createState() => @@ -58,17 +60,32 @@ class _WDesktopWalletSummaryState extends ConsumerState { final baseCurrency = ref .watch(prefsChangeNotifierProvider.select((value) => value.currency)); - final priceTuple = ref.watch(priceAnd24hChangeNotifierProvider - .select((value) => value.getPrice(coin))); + final priceTuple = widget.isToken + ? ref.watch(priceAnd24hChangeNotifierProvider.select((value) => + value.getTokenPrice(ref.watch(tokenServiceProvider + .select((value) => value!.tokenContract.address))))) + : ref.watch(priceAnd24hChangeNotifierProvider + .select((value) => value.getPrice(coin))); final _showAvailable = ref.watch(walletBalanceToggleStateProvider.state).state == WalletBalanceToggleState.available; - Balance balance = ref.watch(walletsChangeNotifierProvider - .select((value) => value.getManager(walletId).balance)); + final unit = widget.isToken + ? ref.watch( + tokenServiceProvider.select((value) => value!.tokenContract.symbol)) + : coin.ticker; + final decimalPlaces = widget.isToken + ? ref.watch(tokenServiceProvider + .select((value) => value!.tokenContract.decimals)) + : coin.decimals; - Decimal balanceToShow; + Balance balance = widget.isToken + ? ref.watch(tokenServiceProvider.select((value) => value!.balance)) + : ref.watch(walletsChangeNotifierProvider + .select((value) => value.getManager(walletId).balance)); + + Amount balanceToShow; if (coin == Coin.firo || coin == Coin.firoTestNet) { Balance? balanceSecondary = ref .watch( @@ -83,18 +100,16 @@ class _WDesktopWalletSummaryState extends ConsumerState { WalletBalanceToggleState.available; if (_showAvailable) { - balanceToShow = showPrivate - ? balanceSecondary!.getSpendable() - : balance.getSpendable(); - } else { balanceToShow = - showPrivate ? balanceSecondary!.getTotal() : balance.getTotal(); + showPrivate ? balanceSecondary!.spendable : balance.spendable; + } else { + balanceToShow = showPrivate ? balanceSecondary!.total : balance.total; } } else { if (_showAvailable) { - balanceToShow = balance.getSpendable(); + balanceToShow = balance.spendable; } else { - balanceToShow = balance.getTotal(); + balanceToShow = balance.total; } } @@ -109,18 +124,19 @@ class _WDesktopWalletSummaryState extends ConsumerState { FittedBox( fit: BoxFit.scaleDown, child: Text( - "${Format.localizedStringAsFixed( - value: balanceToShow, + "${balanceToShow.localizedStringAsFixed( locale: locale, - decimalPlaces: 8, - )} ${coin.ticker}", + decimalPlaces: decimalPlaces, + )} $unit", style: STextStyles.desktopH3(context), ), ), if (externalCalls) Text( - "${Format.localizedStringAsFixed( - value: priceTuple.item1 * balanceToShow, + "${Amount.fromDecimal( + priceTuple.item1 * balanceToShow.decimal, + fractionDigits: 2, + ).localizedStringAsFixed( locale: locale, decimalPlaces: 2, )} $baseCurrency", diff --git a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/my_wallet.dart b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/my_wallet.dart index 6813d42cc..f57ad8c2b 100644 --- a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/my_wallet.dart +++ b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/my_wallet.dart @@ -1,87 +1,101 @@ import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:stackwallet/pages/wallet_view/sub_widgets/transactions_list.dart'; import 'package:stackwallet/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_receive.dart'; import 'package:stackwallet/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_send.dart'; -import 'package:stackwallet/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/send_receive_tab_menu.dart'; -import 'package:stackwallet/utilities/constants.dart'; -import 'package:stackwallet/utilities/text_styles.dart'; -import 'package:stackwallet/utilities/theme/stack_colors.dart'; +import 'package:stackwallet/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_token_send.dart'; +import 'package:stackwallet/providers/global/wallets_provider.dart'; +import 'package:stackwallet/utilities/enums/coin_enum.dart'; +import 'package:stackwallet/widgets/custom_tab_view.dart'; +import 'package:stackwallet/widgets/rounded_white_container.dart'; -class MyWallet extends StatefulWidget { +class MyWallet extends ConsumerStatefulWidget { const MyWallet({ Key? key, required this.walletId, + this.contractAddress, }) : super(key: key); final String walletId; + final String? contractAddress; @override - State createState() => _MyWalletState(); + ConsumerState createState() => _MyWalletState(); } -class _MyWalletState extends State { - int _selectedIndex = 0; +class _MyWalletState extends ConsumerState { + final titles = [ + "Send", + "Receive", + ]; + + late final bool isEth; + + @override + void initState() { + isEth = ref + .read(walletsChangeNotifierProvider) + .getManager(widget.walletId) + .coin == + Coin.ethereum; + + if (isEth && widget.contractAddress == null) { + titles.add("Transactions"); + } + + super.initState(); + } @override Widget build(BuildContext context) { return ListView( primary: false, children: [ - Text( - "My wallet", - style: STextStyles.desktopTextExtraSmall(context).copyWith( - color: Theme.of(context) - .extension()! - .textFieldActiveSearchIconLeft, - ), - ), - const SizedBox( - height: 16, - ), - Container( - decoration: BoxDecoration( - color: Theme.of(context).extension()!.popupBG, - borderRadius: BorderRadius.vertical( - top: Radius.circular( - Constants.size.circularBorderRadius, + RoundedWhiteContainer( + padding: EdgeInsets.zero, + child: CustomTabView( + titles: titles, + children: [ + widget.contractAddress == null + ? Padding( + padding: const EdgeInsets.all(20), + child: DesktopSend( + walletId: widget.walletId, + ), + ) + : Padding( + padding: const EdgeInsets.all(20), + child: DesktopTokenSend( + walletId: widget.walletId, + ), + ), + Padding( + padding: const EdgeInsets.all(20), + child: DesktopReceive( + walletId: widget.walletId, + contractAddress: widget.contractAddress, + ), ), - ), - ), - child: SendReceiveTabMenu( - onChanged: (index) { - setState(() { - _selectedIndex = index; - }); - }, - ), - ), - Container( - decoration: BoxDecoration( - color: Theme.of(context).extension()!.popupBG, - borderRadius: BorderRadius.vertical( - bottom: Radius.circular( - Constants.size.circularBorderRadius, - ), - ), - ), - child: AnimatedCrossFade( - firstChild: Padding( - key: const Key("desktopSendViewPortKey"), - padding: const EdgeInsets.all(20), - child: DesktopSend( - walletId: widget.walletId, - ), - ), - secondChild: Padding( - key: const Key("desktopReceiveViewPortKey"), - padding: const EdgeInsets.all(20), - child: DesktopReceive( - walletId: widget.walletId, - ), - ), - crossFadeState: _selectedIndex == 0 - ? CrossFadeState.showFirst - : CrossFadeState.showSecond, - duration: const Duration(milliseconds: 250), + if (isEth && widget.contractAddress == null) + Padding( + padding: const EdgeInsets.only(top: 8.0), + child: ConstrainedBox( + constraints: BoxConstraints( + maxHeight: MediaQuery.of(context).size.height - 362, + ), + child: TransactionsList( + walletId: widget.walletId, + managerProvider: ref.watch( + walletsChangeNotifierProvider.select( + (value) => value.getManagerProvider( + widget.walletId, + ), + ), + ), + ), + ), + ), + ], ), ), ], diff --git a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/recent_desktop_transactions.dart b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/recent_desktop_transactions.dart deleted file mode 100644 index dca501e25..000000000 --- a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/recent_desktop_transactions.dart +++ /dev/null @@ -1,64 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:stackwallet/pages/wallet_view/sub_widgets/transactions_list.dart'; -import 'package:stackwallet/pages/wallet_view/transaction_views/all_transactions_view.dart'; -import 'package:stackwallet/providers/providers.dart'; -import 'package:stackwallet/utilities/text_styles.dart'; -import 'package:stackwallet/utilities/theme/stack_colors.dart'; -import 'package:stackwallet/widgets/custom_buttons/blue_text_button.dart'; - -class RecentDesktopTransactions extends ConsumerStatefulWidget { - const RecentDesktopTransactions({ - Key? key, - required this.walletId, - }) : super(key: key); - - final String walletId; - - @override - ConsumerState createState() => - _RecentDesktopTransactionsState(); -} - -class _RecentDesktopTransactionsState - extends ConsumerState { - @override - Widget build(BuildContext context) { - return Column( - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - "Recent transactions", - style: STextStyles.desktopTextExtraSmall(context).copyWith( - color: Theme.of(context) - .extension()! - .textFieldActiveSearchIconLeft, - ), - ), - CustomTextButton( - text: "See all", - onTap: () { - Navigator.of(context).pushNamed( - AllTransactionsView.routeName, - arguments: widget.walletId, - ); - }, - ), - ], - ), - const SizedBox( - height: 16, - ), - Expanded( - child: TransactionsList( - managerProvider: ref.watch(walletsChangeNotifierProvider - .select((value) => value.getManagerProvider(widget.walletId))), - walletId: widget.walletId, - ), - ), - ], - ); - } -} diff --git a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/send_receive_tab_menu.dart b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/send_receive_tab_menu.dart deleted file mode 100644 index f42ed297d..000000000 --- a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/send_receive_tab_menu.dart +++ /dev/null @@ -1,165 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:stackwallet/utilities/text_styles.dart'; -import 'package:stackwallet/utilities/theme/stack_colors.dart'; - -class SendReceiveTabMenu extends StatefulWidget { - const SendReceiveTabMenu({ - Key? key, - this.initialIndex = 0, - this.onChanged, - }) : super(key: key); - - final int initialIndex; - final void Function(int)? onChanged; - - @override - State createState() => _SendReceiveTabMenuState(); -} - -class _SendReceiveTabMenuState extends State { - late int _selectedIndex; - - void _onChanged(int newIndex) { - if (_selectedIndex != newIndex) { - setState(() { - _selectedIndex = newIndex; - }); - widget.onChanged?.call(_selectedIndex); - } - } - - @override - void initState() { - _selectedIndex = widget.initialIndex; - super.initState(); - } - - @override - Widget build(BuildContext context) { - return Row( - children: [ - Expanded( - child: MouseRegion( - cursor: SystemMouseCursors.click, - child: GestureDetector( - onTap: () => _onChanged(0), - child: Container( - color: Colors.transparent, - child: Column( - children: [ - const SizedBox( - height: 16, - ), - AnimatedCrossFade( - firstChild: Text( - "Send", - style: - STextStyles.desktopTextExtraSmall(context).copyWith( - color: Theme.of(context) - .extension()! - .accentColorBlue, - ), - ), - secondChild: Text( - "Send", - style: - STextStyles.desktopTextExtraSmall(context).copyWith( - color: Theme.of(context) - .extension()! - .textSubtitle1, - ), - ), - crossFadeState: _selectedIndex == 0 - ? CrossFadeState.showFirst - : CrossFadeState.showSecond, - duration: const Duration(milliseconds: 250), - ), - const SizedBox( - height: 19, - ), - Container( - height: 2, - decoration: BoxDecoration( - color: Theme.of(context) - .extension()! - .backgroundAppBar, - ), - ), - ], - ), - ), - ), - ), - ), - Expanded( - child: MouseRegion( - cursor: SystemMouseCursors.click, - child: GestureDetector( - onTap: () => _onChanged(1), - child: Container( - color: Colors.transparent, - child: Column( - children: [ - const SizedBox( - height: 16, - ), - AnimatedCrossFade( - firstChild: Text( - "Receive", - style: - STextStyles.desktopTextExtraSmall(context).copyWith( - color: Theme.of(context) - .extension()! - .accentColorBlue, - ), - ), - secondChild: Text( - "Receive", - style: - STextStyles.desktopTextExtraSmall(context).copyWith( - color: Theme.of(context) - .extension()! - .textSubtitle1, - ), - ), - crossFadeState: _selectedIndex == 1 - ? CrossFadeState.showFirst - : CrossFadeState.showSecond, - duration: const Duration(milliseconds: 250), - ), - const SizedBox( - height: 19, - ), - Stack( - children: [ - Container( - height: 2, - decoration: BoxDecoration( - color: Theme.of(context) - .extension()! - .backgroundAppBar, - ), - ), - AnimatedSlide( - offset: Offset(_selectedIndex == 0 ? -1 : 0, 0), - duration: const Duration(milliseconds: 250), - child: Container( - height: 2, - decoration: BoxDecoration( - color: Theme.of(context) - .extension()! - .accentColorBlue), - ), - ), - ], - ), - ], - ), - ), - ), - ), - ), - ], - ); - } -} diff --git a/lib/pages_desktop_specific/password/delete_password_warning_view.dart b/lib/pages_desktop_specific/password/delete_password_warning_view.dart index 54db989fd..700754efd 100644 --- a/lib/pages_desktop_specific/password/delete_password_warning_view.dart +++ b/lib/pages_desktop_specific/password/delete_password_warning_view.dart @@ -5,7 +5,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/svg.dart'; import 'package:hive/hive.dart'; -import 'package:stackwallet/hive/db.dart'; +import 'package:stackwallet/db/hive/db.dart'; import 'package:stackwallet/notifications/show_flush_bar.dart'; import 'package:stackwallet/pages/intro_view.dart'; import 'package:stackwallet/utilities/assets.dart'; diff --git a/lib/pages_desktop_specific/password/desktop_login_view.dart b/lib/pages_desktop_specific/password/desktop_login_view.dart index 65632144a..bb2390bfe 100644 --- a/lib/pages_desktop_specific/password/desktop_login_view.dart +++ b/lib/pages_desktop_specific/password/desktop_login_view.dart @@ -20,7 +20,7 @@ import 'package:stackwallet/widgets/desktop/primary_button.dart'; import 'package:stackwallet/widgets/loading_indicator.dart'; import 'package:stackwallet/widgets/stack_text_field.dart'; -import '../../hive/db.dart'; +import '../../db/hive/db.dart'; import '../../utilities/db_version_migration.dart'; import '../../utilities/logger.dart'; diff --git a/lib/pages_desktop_specific/password/forgotten_passphrase_restore_from_swb.dart b/lib/pages_desktop_specific/password/forgotten_passphrase_restore_from_swb.dart index 60dad82f1..735253ada 100644 --- a/lib/pages_desktop_specific/password/forgotten_passphrase_restore_from_swb.dart +++ b/lib/pages_desktop_specific/password/forgotten_passphrase_restore_from_swb.dart @@ -6,7 +6,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/svg.dart'; import 'package:hive_flutter/hive_flutter.dart'; -import 'package:stackwallet/hive/db.dart'; +import 'package:stackwallet/db/hive/db.dart'; import 'package:stackwallet/notifications/show_flush_bar.dart'; import 'package:stackwallet/pages/settings_views/global_settings_view/stack_backup_views/helpers/restore_create_backup.dart'; import 'package:stackwallet/pages/settings_views/global_settings_view/stack_backup_views/helpers/swb_file_system.dart'; diff --git a/lib/pages_desktop_specific/settings/settings_menu/advanced_settings/stack_privacy_dialog.dart b/lib/pages_desktop_specific/settings/settings_menu/advanced_settings/stack_privacy_dialog.dart index ecd22237c..9d61b4d3c 100644 --- a/lib/pages_desktop_specific/settings/settings_menu/advanced_settings/stack_privacy_dialog.dart +++ b/lib/pages_desktop_specific/settings/settings_menu/advanced_settings/stack_privacy_dialog.dart @@ -3,7 +3,7 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/svg.dart'; -import 'package:stackwallet/hive/db.dart'; +import 'package:stackwallet/db/hive/db.dart'; import 'package:stackwallet/providers/global/prefs_provider.dart'; import 'package:stackwallet/providers/global/price_provider.dart'; import 'package:stackwallet/services/exchange/exchange_data_loading_service.dart'; diff --git a/lib/providers/db/main_db_provider.dart b/lib/providers/db/main_db_provider.dart new file mode 100644 index 000000000..2f3b6479c --- /dev/null +++ b/lib/providers/db/main_db_provider.dart @@ -0,0 +1,4 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:stackwallet/db/isar/main_db.dart'; + +final mainDBProvider = Provider((ref) => MainDB.instance); diff --git a/lib/providers/ui/add_wallet_selected_coin_provider.dart b/lib/providers/ui/add_wallet_selected_coin_provider.dart index 4e2f77cdd..6acf51db8 100644 --- a/lib/providers/ui/add_wallet_selected_coin_provider.dart +++ b/lib/providers/ui/add_wallet_selected_coin_provider.dart @@ -1,14 +1,5 @@ -import 'package:flutter/foundation.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:stackwallet/utilities/enums/coin_enum.dart'; +import 'package:stackwallet/models/add_wallet_list_entity/add_wallet_list_entity.dart'; -int _count = 0; - -final addWalletSelectedCoinStateProvider = - StateProvider.autoDispose((_) { - if (kDebugMode) { - _count++; - } - - return null; -}); +final addWalletSelectedEntityStateProvider = + StateProvider.autoDispose((_) => null); diff --git a/lib/route_generator.dart b/lib/route_generator.dart index d94fbe6c2..0609c3839 100644 --- a/lib/route_generator.dart +++ b/lib/route_generator.dart @@ -1,8 +1,9 @@ -import 'package:decimal/decimal.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:isar/isar.dart'; +import 'package:stackwallet/models/add_wallet_list_entity/add_wallet_list_entity.dart'; +import 'package:stackwallet/models/add_wallet_list_entity/sub_classes/eth_token_entity.dart'; import 'package:stackwallet/models/buy/response_objects/quote.dart'; import 'package:stackwallet/models/contact_address_entry.dart'; import 'package:stackwallet/models/exchange/incomplete_exchange.dart'; @@ -10,6 +11,8 @@ import 'package:stackwallet/models/exchange/response_objects/trade.dart'; import 'package:stackwallet/models/isar/models/isar_models.dart'; import 'package:stackwallet/models/paynym/paynym_account_lite.dart'; import 'package:stackwallet/models/send_view_auto_fill_data.dart'; +import 'package:stackwallet/pages/add_wallet_views/add_token_view/add_custom_token_view.dart'; +import 'package:stackwallet/pages/add_wallet_views/add_token_view/edit_wallet_tokens_view.dart'; import 'package:stackwallet/pages/add_wallet_views/add_wallet_view/add_wallet_view.dart'; import 'package:stackwallet/pages/add_wallet_views/create_or_restore_wallet_view/create_or_restore_wallet_view.dart'; import 'package:stackwallet/pages/add_wallet_views/name_your_wallet_view/name_your_wallet_view.dart'; @@ -17,6 +20,7 @@ import 'package:stackwallet/pages/add_wallet_views/new_wallet_recovery_phrase_vi import 'package:stackwallet/pages/add_wallet_views/new_wallet_recovery_phrase_warning_view/new_wallet_recovery_phrase_warning_view.dart'; import 'package:stackwallet/pages/add_wallet_views/restore_wallet_view/restore_options_view/restore_options_view.dart'; import 'package:stackwallet/pages/add_wallet_views/restore_wallet_view/restore_wallet_view.dart'; +import 'package:stackwallet/pages/add_wallet_views/select_wallet_for_token_view.dart'; import 'package:stackwallet/pages/add_wallet_views/verify_recovery_phrase_view/verify_recovery_phrase_view.dart'; import 'package:stackwallet/pages/address_book_views/address_book_view.dart'; import 'package:stackwallet/pages/address_book_views/subviews/add_address_book_entry_view.dart'; @@ -55,6 +59,7 @@ import 'package:stackwallet/pages/receive_view/generate_receiving_uri_qr_code_vi import 'package:stackwallet/pages/receive_view/receive_view.dart'; import 'package:stackwallet/pages/send_view/confirm_transaction_view.dart'; import 'package:stackwallet/pages/send_view/send_view.dart'; +import 'package:stackwallet/pages/send_view/token_send_view.dart'; import 'package:stackwallet/pages/settings_views/global_settings_view/about_view.dart'; import 'package:stackwallet/pages/settings_views/global_settings_view/advanced_views/advanced_settings_view.dart'; import 'package:stackwallet/pages/settings_views/global_settings_view/advanced_views/debug_view.dart'; @@ -93,11 +98,15 @@ import 'package:stackwallet/pages/settings_views/wallet_settings_view/wallet_set import 'package:stackwallet/pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/rename_wallet_view.dart'; import 'package:stackwallet/pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/wallet_settings_wallet_settings_view.dart'; import 'package:stackwallet/pages/stack_privacy_calls.dart'; +import 'package:stackwallet/pages/token_view/my_tokens_view.dart'; +import 'package:stackwallet/pages/token_view/token_contract_details_view.dart'; +import 'package:stackwallet/pages/token_view/token_view.dart'; import 'package:stackwallet/pages/wallet_view/transaction_views/all_transactions_view.dart'; import 'package:stackwallet/pages/wallet_view/transaction_views/edit_note_view.dart'; import 'package:stackwallet/pages/wallet_view/transaction_views/transaction_details_view.dart'; import 'package:stackwallet/pages/wallet_view/transaction_views/transaction_search_filter_view.dart'; import 'package:stackwallet/pages/wallet_view/wallet_view.dart'; +import 'package:stackwallet/pages/wallets_view/eth_wallets_overview.dart'; import 'package:stackwallet/pages/wallets_view/wallets_view.dart'; import 'package:stackwallet/pages_desktop_specific/address_book_view/desktop_address_book.dart'; import 'package:stackwallet/pages_desktop_specific/addresses/desktop_wallet_addresses_view.dart'; @@ -108,6 +117,7 @@ import 'package:stackwallet/pages_desktop_specific/desktop_exchange/desktop_all_ import 'package:stackwallet/pages_desktop_specific/desktop_exchange/desktop_exchange_view.dart'; import 'package:stackwallet/pages_desktop_specific/desktop_home_view.dart'; import 'package:stackwallet/pages_desktop_specific/my_stack_view/my_stack_view.dart'; +import 'package:stackwallet/pages_desktop_specific/my_stack_view/wallet_view/desktop_token_view.dart'; import 'package:stackwallet/pages_desktop_specific/my_stack_view/wallet_view/desktop_wallet_view.dart'; import 'package:stackwallet/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/delete_wallet_keys_popup.dart'; import 'package:stackwallet/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_attention_delete_wallet.dart'; @@ -135,6 +145,7 @@ import 'package:stackwallet/pages_desktop_specific/settings/settings_menu/syncin import 'package:stackwallet/services/coins/manager.dart'; import 'package:stackwallet/services/event_bus/events/global/node_connection_status_changed_event.dart'; import 'package:stackwallet/services/event_bus/events/global/wallet_sync_status_changed_event.dart'; +import 'package:stackwallet/utilities/amount/amount.dart'; import 'package:stackwallet/utilities/enums/add_wallet_type_enum.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:stackwallet/utilities/theme/color_theme.dart'; @@ -208,6 +219,92 @@ class RouteGenerator { builder: (_) => const AddWalletView(), settings: RouteSettings(name: settings.name)); + case EditWalletTokensView.routeName: + if (args is String) { + return getRoute( + shouldUseMaterialRoute: useMaterialPageRoute, + builder: (_) => EditWalletTokensView( + walletId: args, + ), + settings: RouteSettings( + name: settings.name, + ), + ); + } else if (args is Tuple2>) { + return getRoute( + shouldUseMaterialRoute: useMaterialPageRoute, + builder: (_) => EditWalletTokensView( + walletId: args.item1, + contractsToMarkSelected: args.item2, + ), + settings: RouteSettings( + name: settings.name, + ), + ); + } + return _routeError("${settings.name} invalid args: ${args.toString()}"); + + case DesktopTokenView.routeName: + if (args is String) { + return getRoute( + shouldUseMaterialRoute: useMaterialPageRoute, + builder: (_) => DesktopTokenView( + walletId: args, + ), + settings: RouteSettings( + name: settings.name, + ), + ); + } + return _routeError("${settings.name} invalid args: ${args.toString()}"); + + case SelectWalletForTokenView.routeName: + if (args is EthTokenEntity) { + return getRoute( + shouldUseMaterialRoute: useMaterialPageRoute, + builder: (_) => SelectWalletForTokenView( + entity: args, + ), + settings: RouteSettings( + name: settings.name, + ), + ); + } + return _routeError("${settings.name} invalid args: ${args.toString()}"); + + case AddCustomTokenView.routeName: + return getRoute( + shouldUseMaterialRoute: useMaterialPageRoute, + builder: (_) => const AddCustomTokenView(), + settings: RouteSettings( + name: settings.name, + ), + ); + + case EthWalletsOverview.routeName: + return getRoute( + shouldUseMaterialRoute: useMaterialPageRoute, + builder: (_) => const EthWalletsOverview(), + settings: RouteSettings( + name: settings.name, + ), + ); + + case TokenContractDetailsView.routeName: + if (args is Tuple2) { + return getRoute( + shouldUseMaterialRoute: useMaterialPageRoute, + builder: (_) => TokenContractDetailsView( + contractAddress: args.item1, + walletId: args.item2, + ), + settings: RouteSettings( + name: settings.name, + ), + ); + } + return _routeError("${settings.name} invalid args: ${args.toString()}"); + case SingleFieldEditView.routeName: if (args is Tuple2) { return getRoute( @@ -748,11 +845,11 @@ class RouteGenerator { return _routeError("${settings.name} invalid args: ${args.toString()}"); case CreateOrRestoreWalletView.routeName: - if (args is Coin) { + if (args is AddWalletListEntity) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, builder: (_) => CreateOrRestoreWalletView( - coin: args, + entity: args, ), settings: RouteSettings( name: settings.name, @@ -1000,6 +1097,22 @@ class RouteGenerator { } return _routeError("${settings.name} invalid args: ${args.toString()}"); + case TokenSendView.routeName: + if (args is Tuple3) { + return getRoute( + shouldUseMaterialRoute: useMaterialPageRoute, + builder: (_) => TokenSendView( + walletId: args.item1, + coin: args.item2, + tokenContract: args.item3, + ), + settings: RouteSettings( + name: settings.name, + ), + ); + } + return _routeError("${settings.name} invalid args: ${args.toString()}"); + case ConfirmTransactionView.routeName: if (args is Tuple2, String>) { return getRoute( @@ -1035,6 +1148,26 @@ class RouteGenerator { ), ); } + if (args is Tuple3) { + return getRoute( + shouldUseMaterialRoute: useMaterialPageRoute, + builder: (_) => Stack( + children: [ + WalletInitiatedExchangeView( + walletId: args.item1, + coin: args.item2, + contract: args.item3, + ), + // ExchangeLoadingOverlayView( + // unawaitedLoad: args.item3, + // ), + ], + ), + settings: RouteSettings( + name: settings.name, + ), + ); + } return _routeError("${settings.name} invalid args: ${args.toString()}"); case NotificationsView.routeName: @@ -1174,7 +1307,7 @@ class RouteGenerator { return _routeError("${settings.name} invalid args: ${args.toString()}"); case SendFromView.routeName: - if (args is Tuple4) { + if (args is Tuple4) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, builder: (_) => SendFromView( @@ -1297,6 +1430,18 @@ class RouteGenerator { ), ); } + if (args is Tuple2) { + return getRoute( + shouldUseMaterialRoute: useMaterialPageRoute, + builder: (_) => BuyInWalletView( + coin: args.item1, + contract: args.item2, + ), + settings: RouteSettings( + name: settings.name, + ), + ); + } return _routeError("${settings.name} invalid args: ${args.toString()}"); case DesktopBuyView.routeName: @@ -1577,6 +1722,48 @@ class RouteGenerator { } return _routeError("${settings.name} invalid args: ${args.toString()}"); + case MyTokensView.routeName: + if (args is String) { + return getRoute( + shouldUseMaterialRoute: useMaterialPageRoute, + builder: (_) => MyTokensView( + walletId: args, + ), + settings: RouteSettings( + name: settings.name, + ), + ); + } + return _routeError("${settings.name} invalid args: ${args.toString()}"); + + // case WalletView.routeName: + // if (args is Tuple2>) { + // return getRoute( + // shouldUseMaterialRoute: useMaterialPageRoute, + // builder: (_) => WalletView( + // walletId: args.item1, + // managerProvider: args.item2, + // ), + // settings: RouteSettings( + // name: settings.name, + // ), + // ); + // } + + case TokenView.routeName: + if (args is String) { + return getRoute( + shouldUseMaterialRoute: useMaterialPageRoute, + builder: (_) => TokenView( + walletId: args, + ), + settings: RouteSettings( + name: settings.name, + ), + ); + } + return _routeError("${settings.name} invalid args: ${args.toString()}"); + // == End of desktop specific routes ===================================== default: diff --git a/lib/services/address_book_service.dart b/lib/services/address_book_service.dart index 80c60e4b8..e92c0b00b 100644 --- a/lib/services/address_book_service.dart +++ b/lib/services/address_book_service.dart @@ -1,6 +1,6 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/foundation.dart'; -import 'package:stackwallet/hive/db.dart'; +import 'package:stackwallet/db/hive/db.dart'; import 'package:stackwallet/models/contact.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:stackwallet/utilities/logger.dart'; diff --git a/lib/services/coins/bitcoin/bitcoin_wallet.dart b/lib/services/coins/bitcoin/bitcoin_wallet.dart index f2f34712e..4184fd282 100644 --- a/lib/services/coins/bitcoin/bitcoin_wallet.dart +++ b/lib/services/coins/bitcoin/bitcoin_wallet.dart @@ -11,7 +11,7 @@ import 'package:bs58check/bs58check.dart' as bs58check; import 'package:decimal/decimal.dart'; import 'package:flutter/foundation.dart'; import 'package:isar/isar.dart'; -import 'package:stackwallet/db/main_db.dart'; +import 'package:stackwallet/db/isar/main_db.dart'; import 'package:stackwallet/electrumx_rpc/cached_electrumx.dart'; import 'package:stackwallet/electrumx_rpc/electrumx.dart'; import 'package:stackwallet/exceptions/electrumx/no_such_transaction.dart'; @@ -35,6 +35,7 @@ import 'package:stackwallet/services/node_service.dart'; import 'package:stackwallet/services/notifications_api.dart'; import 'package:stackwallet/services/transaction_notification_tracker.dart'; import 'package:stackwallet/utilities/address_utils.dart'; +import 'package:stackwallet/utilities/amount/amount.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/bip32_utils.dart'; import 'package:stackwallet/utilities/constants.dart'; @@ -51,8 +52,14 @@ import 'package:tuple/tuple.dart'; import 'package:uuid/uuid.dart'; const int MINIMUM_CONFIRMATIONS = 1; -const int DUST_LIMIT = 294; -const int DUST_LIMIT_P2PKH = 546; +final Amount DUST_LIMIT = Amount( + rawValue: BigInt.from(294), + fractionDigits: Coin.particl.decimals, +); +final Amount DUST_LIMIT_P2PKH = Amount( + rawValue: BigInt.from(546), + fractionDigits: Coin.particl.decimals, +); const String GENESIS_HASH_MAINNET = "000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f"; @@ -156,7 +163,7 @@ class BitcoinWallet extends CoinServiceAPI // checkChangeAddressForTransactions: // _checkP2PKHChangeAddressForTransactions, addDerivation: addDerivation, - dustLimitP2PKH: DUST_LIMIT_P2PKH, + dustLimitP2PKH: DUST_LIMIT_P2PKH.raw.toInt(), minConfirms: MINIMUM_CONFIRMATIONS, ); } @@ -1120,7 +1127,7 @@ class BitcoinWallet extends CoinServiceAPI @override Future> prepareSend({ required String address, - required int satoshiAmount, + required Amount amount, Map? args, }) async { try { @@ -1150,14 +1157,14 @@ class BitcoinWallet extends CoinServiceAPI // check for send all bool isSendAll = false; - if (satoshiAmount == balance.spendable) { + if (amount == balance.spendable) { isSendAll = true; } final bool coinControl = utxos != null; final txData = await coinSelection( - satoshiAmountToSend: satoshiAmount, + satoshiAmountToSend: amount.raw.toInt(), selectedTxFeeRate: rate, recipientAddress: address, isSendAll: isSendAll, @@ -1336,13 +1343,16 @@ class BitcoinWallet extends CoinServiceAPI timestamp: DateTime.now().millisecondsSinceEpoch ~/ 1000, type: isar_models.TransactionType.outgoing, subType: isar_models.TransactionSubType.none, - amount: txData["recipientAmt"] as int, + // precision may be lost here hence the following amountString + amount: (txData["recipientAmt"] as Amount).raw.toInt(), + amountString: (txData["recipientAmt"] as Amount).toJsonString(), fee: txData["fee"] as int, height: null, isCancelled: false, isLelantus: false, otherData: null, slateId: null, + nonce: null, inputs: [], outputs: [], ); @@ -1464,9 +1474,18 @@ class BitcoinWallet extends CoinServiceAPI numberOfBlocksFast: f, numberOfBlocksAverage: m, numberOfBlocksSlow: s, - fast: Format.decimalAmountToSatoshis(fast, coin), - medium: Format.decimalAmountToSatoshis(medium, coin), - slow: Format.decimalAmountToSatoshis(slow, coin), + fast: Amount.fromDecimal( + fast, + fractionDigits: coin.decimals, + ).raw.toInt(), + medium: Amount.fromDecimal( + medium, + fractionDigits: coin.decimals, + ).raw.toInt(), + slow: Amount.fromDecimal( + slow, + fractionDigits: coin.decimals, + ).raw.toInt(), ); Logging.instance.log("fetched fees: $feeObject", level: LogLevel.Info); @@ -2400,8 +2419,11 @@ class BitcoinWallet extends CoinServiceAPI feeRatePerKB: selectedTxFeeRate, ); - final int roughEstimate = - roughFeeEstimate(spendableOutputs.length, 1, selectedTxFeeRate); + final int roughEstimate = roughFeeEstimate( + spendableOutputs.length, + 1, + selectedTxFeeRate, + ).raw.toInt(); if (feeForOneOutput < roughEstimate) { feeForOneOutput = roughEstimate; } @@ -2472,7 +2494,7 @@ class BitcoinWallet extends CoinServiceAPI if (satoshisBeingUsed - satoshiAmountToSend > feeForOneOutput) { if (satoshisBeingUsed - satoshiAmountToSend > - feeForOneOutput + DUST_LIMIT) { + feeForOneOutput + DUST_LIMIT.raw.toInt()) { // Here, we know that theoretically, we may be able to include another output(change) but we first need to // factor in the value of this output in satoshis. int changeOutputSize = @@ -2480,7 +2502,7 @@ class BitcoinWallet extends CoinServiceAPI // We check to see if the user can pay for the new transaction with 2 outputs instead of one. If they can and // the second output's size > DUST_LIMIT satoshis, we perform the mechanics required to properly generate and use a new // change address. - if (changeOutputSize > DUST_LIMIT && + if (changeOutputSize > DUST_LIMIT.raw.toInt() && satoshisBeingUsed - satoshiAmountToSend - changeOutputSize == feeForTwoOutputs) { // generate new change address if current change address has been used @@ -3059,22 +3081,28 @@ class BitcoinWallet extends CoinServiceAPI (isActive) => this.isActive = isActive; @override - Future estimateFeeFor(int satoshiAmount, int feeRate) async { + Future estimateFeeFor(Amount amount, int feeRate) async { final available = balance.spendable; - if (available == satoshiAmount) { - return satoshiAmount - (await sweepAllEstimate(feeRate)); - } else if (satoshiAmount <= 0 || satoshiAmount > available) { + if (available == amount) { + return amount - (await sweepAllEstimate(feeRate)); + } else if (amount <= Amount.zero || amount > available) { return roughFeeEstimate(1, 2, feeRate); } - int runningBalance = 0; + Amount runningBalance = Amount( + rawValue: BigInt.zero, + fractionDigits: coin.decimals, + ); int inputCount = 0; for (final output in (await utxos)) { if (!output.isBlocked) { - runningBalance += output.value; + runningBalance += Amount( + rawValue: BigInt.from(output.value), + fractionDigits: coin.decimals, + ); inputCount++; - if (runningBalance > satoshiAmount) { + if (runningBalance > amount) { break; } } @@ -3083,31 +3111,35 @@ class BitcoinWallet extends CoinServiceAPI final oneOutPutFee = roughFeeEstimate(inputCount, 1, feeRate); final twoOutPutFee = roughFeeEstimate(inputCount, 2, feeRate); - if (runningBalance - satoshiAmount > oneOutPutFee) { - if (runningBalance - satoshiAmount > oneOutPutFee + DUST_LIMIT) { - final change = runningBalance - satoshiAmount - twoOutPutFee; + if (runningBalance - amount > oneOutPutFee) { + if (runningBalance - amount > oneOutPutFee + DUST_LIMIT) { + final change = runningBalance - amount - twoOutPutFee; if (change > DUST_LIMIT && - runningBalance - satoshiAmount - change == twoOutPutFee) { - return runningBalance - satoshiAmount - change; + runningBalance - amount - change == twoOutPutFee) { + return runningBalance - amount - change; } else { - return runningBalance - satoshiAmount; + return runningBalance - amount; } } else { - return runningBalance - satoshiAmount; + return runningBalance - amount; } - } else if (runningBalance - satoshiAmount == oneOutPutFee) { + } else if (runningBalance - amount == oneOutPutFee) { return oneOutPutFee; } else { return twoOutPutFee; } } - int roughFeeEstimate(int inputCount, int outputCount, int feeRatePerKB) { - return ((42 + (272 * inputCount) + (128 * outputCount)) / 4).ceil() * - (feeRatePerKB / 1000).ceil(); + Amount roughFeeEstimate(int inputCount, int outputCount, int feeRatePerKB) { + return Amount( + rawValue: BigInt.from( + ((42 + (272 * inputCount) + (128 * outputCount)) / 4).ceil() * + (feeRatePerKB / 1000).ceil()), + fractionDigits: coin.decimals, + ); } - Future sweepAllEstimate(int feeRate) async { + Future sweepAllEstimate(int feeRate) async { int available = 0; int inputCount = 0; for (final output in (await utxos)) { @@ -3121,7 +3153,11 @@ class BitcoinWallet extends CoinServiceAPI // transaction will only have 1 output minus the fee final estimatedFee = roughFeeEstimate(inputCount, 1, feeRate); - return available - estimatedFee; + return Amount( + rawValue: BigInt.from(available), + fractionDigits: coin.decimals, + ) - + estimatedFee; } @override diff --git a/lib/services/coins/bitcoincash/bitcoincash_wallet.dart b/lib/services/coins/bitcoincash/bitcoincash_wallet.dart index 1ff520dd5..bbdd540f0 100644 --- a/lib/services/coins/bitcoincash/bitcoincash_wallet.dart +++ b/lib/services/coins/bitcoincash/bitcoincash_wallet.dart @@ -11,7 +11,7 @@ import 'package:bs58check/bs58check.dart' as bs58check; import 'package:decimal/decimal.dart'; import 'package:flutter/foundation.dart'; import 'package:isar/isar.dart'; -import 'package:stackwallet/db/main_db.dart'; +import 'package:stackwallet/db/isar/main_db.dart'; import 'package:stackwallet/electrumx_rpc/cached_electrumx.dart'; import 'package:stackwallet/electrumx_rpc/electrumx.dart'; import 'package:stackwallet/models/balance.dart'; @@ -32,6 +32,7 @@ import 'package:stackwallet/services/node_service.dart'; import 'package:stackwallet/services/notifications_api.dart'; import 'package:stackwallet/services/transaction_notification_tracker.dart'; import 'package:stackwallet/utilities/address_utils.dart'; +import 'package:stackwallet/utilities/amount/amount.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/bip32_utils.dart'; import 'package:stackwallet/utilities/constants.dart'; @@ -47,7 +48,10 @@ import 'package:tuple/tuple.dart'; import 'package:uuid/uuid.dart'; const int MINIMUM_CONFIRMATIONS = 0; -const int DUST_LIMIT = 546; +final Amount DUST_LIMIT = Amount( + rawValue: BigInt.from(546), + fractionDigits: Coin.particl.decimals, +); const String GENESIS_HASH_MAINNET = "000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f"; @@ -213,10 +217,7 @@ class BitcoinCashWallet extends CoinServiceAPI @override Future get maxFee async { - final fee = (await fees).fast; - final satsFee = Format.satoshisToAmount(fee, coin: coin) * - Decimal.fromInt(Constants.satsPerCoin(coin)); - return satsFee.floor().toBigInt().toInt(); + throw UnimplementedError("Not used in bch"); } @override @@ -1050,7 +1051,7 @@ class BitcoinCashWallet extends CoinServiceAPI @override Future> prepareSend({ required String address, - required int satoshiAmount, + required Amount amount, Map? args, }) async { try { @@ -1079,14 +1080,14 @@ class BitcoinCashWallet extends CoinServiceAPI } // check for send all bool isSendAll = false; - if (satoshiAmount == balance.spendable) { + if (amount == balance.spendable) { isSendAll = true; } final bool coinControl = utxos != null; final result = await coinSelection( - satoshiAmountToSend: satoshiAmount, + satoshiAmountToSend: amount.raw.toInt(), selectedTxFeeRate: rate, recipientAddress: address, isSendAll: isSendAll, @@ -1251,13 +1252,16 @@ class BitcoinCashWallet extends CoinServiceAPI timestamp: DateTime.now().millisecondsSinceEpoch ~/ 1000, type: isar_models.TransactionType.outgoing, subType: isar_models.TransactionSubType.none, - amount: txData["recipientAmt"] as int, + // precision may be lost here hence the following amountString + amount: (txData["recipientAmt"] as Amount).raw.toInt(), + amountString: (txData["recipientAmt"] as Amount).toJsonString(), fee: txData["fee"] as int, height: null, isCancelled: false, isLelantus: false, otherData: null, slateId: null, + nonce: null, inputs: [], outputs: [], ); @@ -1419,9 +1423,18 @@ class BitcoinCashWallet extends CoinServiceAPI numberOfBlocksFast: f, numberOfBlocksAverage: m, numberOfBlocksSlow: s, - fast: Format.decimalAmountToSatoshis(fast, coin), - medium: Format.decimalAmountToSatoshis(medium, coin), - slow: Format.decimalAmountToSatoshis(slow, coin), + fast: Amount.fromDecimal( + fast, + fractionDigits: coin.decimals, + ).raw.toInt(), + medium: Amount.fromDecimal( + medium, + fractionDigits: coin.decimals, + ).raw.toInt(), + slow: Amount.fromDecimal( + slow, + fractionDigits: coin.decimals, + ).raw.toInt(), ); Logging.instance.log("fetched fees: $feeObject", level: LogLevel.Info); @@ -1629,8 +1642,9 @@ class BitcoinCashWallet extends CoinServiceAPI case DerivePathType.bip49: key = "${walletId}_${chainId}DerivationsP2SH"; break; - case DerivePathType.bip84: - throw UnsupportedError("bip84 not supported by BCH"); + default: + throw UnsupportedError( + "${derivePathType.name} not supported by ${coin.prettyName}"); } return key; } @@ -2151,12 +2165,27 @@ class BitcoinCashWallet extends CoinServiceAPI Set inputAddresses = {}; Set outputAddresses = {}; - int totalInputValue = 0; - int totalOutputValue = 0; + Amount totalInputValue = Amount( + rawValue: BigInt.from(0), + fractionDigits: coin.decimals, + ); + Amount totalOutputValue = Amount( + rawValue: BigInt.from(0), + fractionDigits: coin.decimals, + ); - int amountSentFromWallet = 0; - int amountReceivedInWallet = 0; - int changeAmount = 0; + Amount amountSentFromWallet = Amount( + rawValue: BigInt.from(0), + fractionDigits: coin.decimals, + ); + Amount amountReceivedInWallet = Amount( + rawValue: BigInt.from(0), + fractionDigits: coin.decimals, + ); + Amount changeAmount = Amount( + rawValue: BigInt.from(0), + fractionDigits: coin.decimals, + ); // parse inputs for (final input in txData["vin"] as List) { @@ -2173,13 +2202,13 @@ class BitcoinCashWallet extends CoinServiceAPI // check matching output if (prevOut == output["n"]) { // get value - final value = Format.decimalAmountToSatoshis( + final value = Amount.fromDecimal( Decimal.parse(output["value"].toString()), - coin, + fractionDigits: coin.decimals, ); // add value to total - totalInputValue += value; + totalInputValue = totalInputValue + value; // get input(prevOut) address final address = @@ -2192,7 +2221,7 @@ class BitcoinCashWallet extends CoinServiceAPI // if input was from my wallet, add value to amount sent if (receivingAddresses.contains(address) || changeAddresses.contains(address)) { - amountSentFromWallet += value; + amountSentFromWallet = amountSentFromWallet + value; } } } @@ -2202,9 +2231,9 @@ class BitcoinCashWallet extends CoinServiceAPI // parse outputs for (final output in txData["vout"] as List) { // get value - final value = Format.decimalAmountToSatoshis( + final value = Amount.fromDecimal( Decimal.parse(output["value"].toString()), - coin, + fractionDigits: coin.decimals, ); // add value to total @@ -2241,7 +2270,7 @@ class BitcoinCashWallet extends CoinServiceAPI txData["address"] as isar_models.Address; isar_models.TransactionType type; - int amount; + Amount amount; if (mySentFromAddresses.isNotEmpty && myReceivedOnAddresses.isNotEmpty) { // tx is sent to self type = isar_models.TransactionType.sentToSelf; @@ -2296,10 +2325,10 @@ class BitcoinCashWallet extends CoinServiceAPI scriptPubKeyAddress: json["scriptPubKey"]?["addresses"]?[0] as String? ?? json['scriptPubKey']['type'] as String, - value: Format.decimalAmountToSatoshis( + value: Amount.fromDecimal( Decimal.parse(json["value"].toString()), - coin, - ), + fractionDigits: coin.decimals, + ).raw.toInt(), ); outputs.add(output); } @@ -2311,13 +2340,15 @@ class BitcoinCashWallet extends CoinServiceAPI (DateTime.now().millisecondsSinceEpoch ~/ 1000), type: type, subType: isar_models.TransactionSubType.none, - amount: amount, - fee: fee, + amount: amount.raw.toInt(), + amountString: amount.toJsonString(), + fee: fee.raw.toInt(), height: txData["height"] as int?, isCancelled: false, isLelantus: false, slateId: null, otherData: null, + nonce: null, inputs: inputs, outputs: outputs, ); @@ -2538,7 +2569,7 @@ class BitcoinCashWallet extends CoinServiceAPI if (satoshisBeingUsed - satoshiAmountToSend > feeForOneOutput) { if (satoshisBeingUsed - satoshiAmountToSend > - feeForOneOutput + DUST_LIMIT) { + feeForOneOutput + DUST_LIMIT.raw.toInt()) { // Here, we know that theoretically, we may be able to include another output(change) but we first need to // factor in the value of this output in satoshis. int changeOutputSize = @@ -2546,7 +2577,7 @@ class BitcoinCashWallet extends CoinServiceAPI // We check to see if the user can pay for the new transaction with 2 outputs instead of one. If they can and // the second output's size > 546 satoshis, we perform the mechanics required to properly generate and use a new // change address. - if (changeOutputSize > DUST_LIMIT && + if (changeOutputSize > DUST_LIMIT.raw.toInt() && satoshisBeingUsed - satoshiAmountToSend - changeOutputSize == feeForTwoOutputs) { // generate new change address if current change address has been used @@ -2765,7 +2796,8 @@ class BitcoinCashWallet extends CoinServiceAPI addressTxid[address] = []; } (addressTxid[address] as List).add(txid); - switch (addressType(address: address)) { + final deriveType = addressType(address: address); + switch (deriveType) { case DerivePathType.bip44: case DerivePathType.bch44: addressesP2PKH.add(address); @@ -2773,8 +2805,9 @@ class BitcoinCashWallet extends CoinServiceAPI case DerivePathType.bip49: addressesP2SH.add(address); break; - case DerivePathType.bip84: - throw UnsupportedError("bip84 not supported by BCH"); + default: + throw UnsupportedError( + "${deriveType.name} not supported by ${coin.prettyName}"); } } } @@ -3105,22 +3138,28 @@ class BitcoinCashWallet extends CoinServiceAPI (isActive) => this.isActive = isActive; @override - Future estimateFeeFor(int satoshiAmount, int feeRate) async { + Future estimateFeeFor(Amount amount, int feeRate) async { final available = balance.spendable; - if (available == satoshiAmount) { - return satoshiAmount - (await sweepAllEstimate(feeRate)); - } else if (satoshiAmount <= 0 || satoshiAmount > available) { + if (available == amount) { + return amount - (await sweepAllEstimate(feeRate)); + } else if (amount <= Amount.zero || amount > available) { return roughFeeEstimate(1, 2, feeRate); } - int runningBalance = 0; + Amount runningBalance = Amount( + rawValue: BigInt.zero, + fractionDigits: coin.decimals, + ); int inputCount = 0; for (final output in (await utxos)) { if (!output.isBlocked) { - runningBalance += output.value; + runningBalance += Amount( + rawValue: BigInt.from(output.value), + fractionDigits: coin.decimals, + ); inputCount++; - if (runningBalance > satoshiAmount) { + if (runningBalance > amount) { break; } } @@ -3129,19 +3168,19 @@ class BitcoinCashWallet extends CoinServiceAPI final oneOutPutFee = roughFeeEstimate(inputCount, 1, feeRate); final twoOutPutFee = roughFeeEstimate(inputCount, 2, feeRate); - if (runningBalance - satoshiAmount > oneOutPutFee) { - if (runningBalance - satoshiAmount > oneOutPutFee + DUST_LIMIT) { - final change = runningBalance - satoshiAmount - twoOutPutFee; + if (runningBalance - amount > oneOutPutFee) { + if (runningBalance - amount > oneOutPutFee + DUST_LIMIT) { + final change = runningBalance - amount - twoOutPutFee; if (change > DUST_LIMIT && - runningBalance - satoshiAmount - change == twoOutPutFee) { - return runningBalance - satoshiAmount - change; + runningBalance - amount - change == twoOutPutFee) { + return runningBalance - amount - change; } else { - return runningBalance - satoshiAmount; + return runningBalance - amount; } } else { - return runningBalance - satoshiAmount; + return runningBalance - amount; } - } else if (runningBalance - satoshiAmount == oneOutPutFee) { + } else if (runningBalance - amount == oneOutPutFee) { return oneOutPutFee; } else { return twoOutPutFee; @@ -3149,12 +3188,15 @@ class BitcoinCashWallet extends CoinServiceAPI } // TODO: correct formula for bch? - int roughFeeEstimate(int inputCount, int outputCount, int feeRatePerKB) { - return ((181 * inputCount) + (34 * outputCount) + 10) * - (feeRatePerKB / 1000).ceil(); + Amount roughFeeEstimate(int inputCount, int outputCount, int feeRatePerKB) { + return Amount( + rawValue: BigInt.from(((181 * inputCount) + (34 * outputCount) + 10) * + (feeRatePerKB / 1000).ceil()), + fractionDigits: coin.decimals, + ); } - Future sweepAllEstimate(int feeRate) async { + Future sweepAllEstimate(int feeRate) async { int available = 0; int inputCount = 0; for (final output in (await utxos)) { @@ -3168,7 +3210,11 @@ class BitcoinCashWallet extends CoinServiceAPI // transaction will only have 1 output minus the fee final estimatedFee = roughFeeEstimate(inputCount, 1, feeRate); - return available - estimatedFee; + return Amount( + rawValue: BigInt.from(available), + fractionDigits: coin.decimals, + ) - + estimatedFee; } @override diff --git a/lib/services/coins/coin_service.dart b/lib/services/coins/coin_service.dart index 556245b3f..ff75822fd 100644 --- a/lib/services/coins/coin_service.dart +++ b/lib/services/coins/coin_service.dart @@ -8,6 +8,7 @@ import 'package:stackwallet/services/coins/bitcoin/bitcoin_wallet.dart'; import 'package:stackwallet/services/coins/bitcoincash/bitcoincash_wallet.dart'; import 'package:stackwallet/services/coins/dogecoin/dogecoin_wallet.dart'; import 'package:stackwallet/services/coins/epiccash/epiccash_wallet.dart'; +import 'package:stackwallet/services/coins/ethereum/ethereum_wallet.dart'; import 'package:stackwallet/services/coins/firo/firo_wallet.dart'; import 'package:stackwallet/services/coins/litecoin/litecoin_wallet.dart'; import 'package:stackwallet/services/coins/monero/monero_wallet.dart'; @@ -15,6 +16,7 @@ import 'package:stackwallet/services/coins/namecoin/namecoin_wallet.dart'; import 'package:stackwallet/services/coins/particl/particl_wallet.dart'; import 'package:stackwallet/services/coins/wownero/wownero_wallet.dart'; import 'package:stackwallet/services/transaction_notification_tracker.dart'; +import 'package:stackwallet/utilities/amount/amount.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:stackwallet/utilities/flutter_secure_storage_interface.dart'; import 'package:stackwallet/utilities/prefs.dart'; @@ -173,6 +175,15 @@ abstract class CoinServiceAPI { // tracker: tracker, ); + case Coin.ethereum: + return EthereumWallet( + walletId: walletId, + walletName: walletName, + coin: coin, + secureStore: secureStorageInterface, + tracker: tracker, + ); + case Coin.monero: return MoneroWallet( walletId: walletId, @@ -234,7 +245,7 @@ abstract class CoinServiceAPI { Future> prepareSend({ required String address, - required int satoshiAmount, + required Amount amount, Map? args, }); @@ -289,7 +300,7 @@ abstract class CoinServiceAPI { bool get isConnected; - Future estimateFeeFor(int satoshiAmount, int feeRate); + Future estimateFeeFor(Amount amount, int feeRate); Future generateNewAddress(); diff --git a/lib/services/coins/dogecoin/dogecoin_wallet.dart b/lib/services/coins/dogecoin/dogecoin_wallet.dart index 517942812..1caafd791 100644 --- a/lib/services/coins/dogecoin/dogecoin_wallet.dart +++ b/lib/services/coins/dogecoin/dogecoin_wallet.dart @@ -8,10 +8,9 @@ import 'package:bip39/bip39.dart' as bip39; import 'package:bitcoindart/bitcoindart.dart'; import 'package:bitcoindart/bitcoindart.dart' as btc_dart; import 'package:bs58check/bs58check.dart' as bs58check; -import 'package:decimal/decimal.dart'; import 'package:flutter/foundation.dart'; import 'package:isar/isar.dart'; -import 'package:stackwallet/db/main_db.dart'; +import 'package:stackwallet/db/isar/main_db.dart'; import 'package:stackwallet/electrumx_rpc/cached_electrumx.dart'; import 'package:stackwallet/electrumx_rpc/electrumx.dart'; import 'package:stackwallet/exceptions/electrumx/no_such_transaction.dart'; @@ -34,6 +33,7 @@ import 'package:stackwallet/services/node_service.dart'; import 'package:stackwallet/services/notifications_api.dart'; import 'package:stackwallet/services/transaction_notification_tracker.dart'; import 'package:stackwallet/utilities/address_utils.dart'; +import 'package:stackwallet/utilities/amount/amount.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/bip32_utils.dart'; import 'package:stackwallet/utilities/constants.dart'; @@ -49,7 +49,10 @@ import 'package:tuple/tuple.dart'; import 'package:uuid/uuid.dart'; const int MINIMUM_CONFIRMATIONS = 1; -const int DUST_LIMIT = 1000000; +final Amount DUST_LIMIT = Amount( + rawValue: BigInt.from(1000000), + fractionDigits: Coin.particl.decimals, +); const String GENESIS_HASH_MAINNET = "1a91e3dace36e2be3bf030a65679fe821aa1d6ef92e7c9902eb318182c355691"; @@ -228,10 +231,7 @@ class DogecoinWallet extends CoinServiceAPI @override Future get maxFee async { - final fee = (await fees).fast; - final satsFee = Format.satoshisToAmount(fee, coin: coin) * - Decimal.fromInt(Constants.satsPerCoin(coin)); - return satsFee.floor().toBigInt().toInt(); + throw UnimplementedError("Not used in dogecoin"); } @override @@ -916,7 +916,7 @@ class DogecoinWallet extends CoinServiceAPI @override Future> prepareSend({ required String address, - required int satoshiAmount, + required Amount amount, Map? args, }) async { try { @@ -945,14 +945,14 @@ class DogecoinWallet extends CoinServiceAPI } // check for send all bool isSendAll = false; - if (satoshiAmount == balance.spendable) { + if (amount == balance.spendable) { isSendAll = true; } final bool coinControl = utxos != null; final result = await coinSelection( - satoshiAmountToSend: satoshiAmount, + satoshiAmountToSend: amount.raw.toInt(), selectedTxFeeRate: rate, recipientAddress: address, isSendAll: isSendAll, @@ -1118,13 +1118,16 @@ class DogecoinWallet extends CoinServiceAPI timestamp: DateTime.now().millisecondsSinceEpoch ~/ 1000, type: isar_models.TransactionType.outgoing, subType: isar_models.TransactionSubType.none, - amount: txData["recipientAmt"] as int, + // precision may be lost here hence the following amountString + amount: (txData["recipientAmt"] as Amount).raw.toInt(), + amountString: (txData["recipientAmt"] as Amount).toJsonString(), fee: txData["fee"] as int, height: null, isCancelled: false, isLelantus: false, otherData: null, slateId: null, + nonce: null, inputs: [], outputs: [], ); @@ -1246,9 +1249,18 @@ class DogecoinWallet extends CoinServiceAPI numberOfBlocksFast: f, numberOfBlocksAverage: m, numberOfBlocksSlow: s, - fast: Format.decimalAmountToSatoshis(fast, coin), - medium: Format.decimalAmountToSatoshis(medium, coin), - slow: Format.decimalAmountToSatoshis(slow, coin), + fast: Amount.fromDecimal( + fast, + fractionDigits: coin.decimals, + ).raw.toInt(), + medium: Amount.fromDecimal( + medium, + fractionDigits: coin.decimals, + ).raw.toInt(), + slow: Amount.fromDecimal( + slow, + fractionDigits: coin.decimals, + ).raw.toInt(), ); Logging.instance.log("fetched fees: $feeObject", level: LogLevel.Info); @@ -2237,7 +2249,7 @@ class DogecoinWallet extends CoinServiceAPI if (satoshisBeingUsed - satoshiAmountToSend > feeForOneOutput) { if (satoshisBeingUsed - satoshiAmountToSend > - feeForOneOutput + DUST_LIMIT) { + feeForOneOutput + DUST_LIMIT.raw.toInt()) { // Here, we know that theoretically, we may be able to include another output(change) but we first need to // factor in the value of this output in satoshis. int changeOutputSize = @@ -2245,7 +2257,7 @@ class DogecoinWallet extends CoinServiceAPI // We check to see if the user can pay for the new transaction with 2 outputs instead of one. If they can and // the second output's size > 546 satoshis, we perform the mechanics required to properly generate and use a new // change address. - if (changeOutputSize > DUST_LIMIT && + if (changeOutputSize > DUST_LIMIT.raw.toInt() && satoshisBeingUsed - satoshiAmountToSend - changeOutputSize == feeForTwoOutputs) { // generate new change address if current change address has been used @@ -2826,22 +2838,29 @@ class DogecoinWallet extends CoinServiceAPI (isActive) => this.isActive = isActive; @override - Future estimateFeeFor(int satoshiAmount, int feeRate) async { + Future estimateFeeFor(Amount amount, int feeRate) async { final available = balance.spendable; - if (available == satoshiAmount) { - return satoshiAmount - (await sweepAllEstimate(feeRate)); - } else if (satoshiAmount <= 0 || satoshiAmount > available) { + if (available == amount) { + return amount - (await sweepAllEstimate(feeRate)); + } else if (amount <= Amount.zero || amount > available) { return roughFeeEstimate(1, 2, feeRate); } - int runningBalance = 0; + Amount runningBalance = Amount( + rawValue: BigInt.zero, + fractionDigits: coin.decimals, + ); int inputCount = 0; for (final output in (await utxos)) { if (!output.isBlocked) { - runningBalance += output.value; + runningBalance = runningBalance + + Amount( + rawValue: BigInt.from(output.value), + fractionDigits: coin.decimals, + ); inputCount++; - if (runningBalance > satoshiAmount) { + if (runningBalance > amount) { break; } } @@ -2850,19 +2869,19 @@ class DogecoinWallet extends CoinServiceAPI final oneOutPutFee = roughFeeEstimate(inputCount, 1, feeRate); final twoOutPutFee = roughFeeEstimate(inputCount, 2, feeRate); - if (runningBalance - satoshiAmount > oneOutPutFee) { - if (runningBalance - satoshiAmount > oneOutPutFee + DUST_LIMIT) { - final change = runningBalance - satoshiAmount - twoOutPutFee; + if (runningBalance - amount > oneOutPutFee) { + if (runningBalance - amount > oneOutPutFee + DUST_LIMIT) { + final change = runningBalance - amount - twoOutPutFee; if (change > DUST_LIMIT && - runningBalance - satoshiAmount - change == twoOutPutFee) { - return runningBalance - satoshiAmount - change; + runningBalance - amount - change == twoOutPutFee) { + return runningBalance - amount - change; } else { - return runningBalance - satoshiAmount; + return runningBalance - amount; } } else { - return runningBalance - satoshiAmount; + return runningBalance - amount; } - } else if (runningBalance - satoshiAmount == oneOutPutFee) { + } else if (runningBalance - amount == oneOutPutFee) { return oneOutPutFee; } else { return twoOutPutFee; @@ -2870,12 +2889,15 @@ class DogecoinWallet extends CoinServiceAPI } // TODO: correct formula for doge? - int roughFeeEstimate(int inputCount, int outputCount, int feeRatePerKB) { - return ((181 * inputCount) + (34 * outputCount) + 10) * - (feeRatePerKB / 1000).ceil(); + Amount roughFeeEstimate(int inputCount, int outputCount, int feeRatePerKB) { + return Amount( + rawValue: BigInt.from(((181 * inputCount) + (34 * outputCount) + 10) * + (feeRatePerKB / 1000).ceil()), + fractionDigits: coin.decimals, + ); } - Future sweepAllEstimate(int feeRate) async { + Future sweepAllEstimate(int feeRate) async { int available = 0; int inputCount = 0; for (final output in (await utxos)) { @@ -2889,7 +2911,11 @@ class DogecoinWallet extends CoinServiceAPI // transaction will only have 1 output minus the fee final estimatedFee = roughFeeEstimate(inputCount, 1, feeRate); - return available - estimatedFee; + return Amount( + rawValue: BigInt.from(available), + fractionDigits: coin.decimals, + ) - + estimatedFee; } @override diff --git a/lib/services/coins/epiccash/epiccash_wallet.dart b/lib/services/coins/epiccash/epiccash_wallet.dart index a439dbb30..cbbbc76c8 100644 --- a/lib/services/coins/epiccash/epiccash_wallet.dart +++ b/lib/services/coins/epiccash/epiccash_wallet.dart @@ -9,7 +9,7 @@ import 'package:flutter_libepiccash/epic_cash.dart'; import 'package:isar/isar.dart'; import 'package:mutex/mutex.dart'; import 'package:stack_wallet_backup/generate_password.dart'; -import 'package:stackwallet/db/main_db.dart'; +import 'package:stackwallet/db/isar/main_db.dart'; import 'package:stackwallet/models/balance.dart'; import 'package:stackwallet/models/epicbox_config_model.dart'; import 'package:stackwallet/models/isar/models/isar_models.dart' as isar_models; @@ -27,12 +27,12 @@ import 'package:stackwallet/services/mixins/epic_cash_hive.dart'; import 'package:stackwallet/services/mixins/wallet_cache.dart'; import 'package:stackwallet/services/mixins/wallet_db.dart'; import 'package:stackwallet/services/node_service.dart'; +import 'package:stackwallet/utilities/amount/amount.dart'; import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/default_epicboxes.dart'; import 'package:stackwallet/utilities/default_nodes.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:stackwallet/utilities/flutter_secure_storage_interface.dart'; -import 'package:stackwallet/utilities/format.dart'; import 'package:stackwallet/utilities/logger.dart'; import 'package:stackwallet/utilities/prefs.dart'; import 'package:stackwallet/utilities/stack_file_system.dart'; @@ -856,20 +856,23 @@ class EpicCashWallet extends CoinServiceAPI ); @override - Future> prepareSend( - {required String address, - required int satoshiAmount, - Map? args}) async { + Future> prepareSend({ + required String address, + required Amount amount, + Map? args, + }) async { try { - int realfee = await nativeFee(satoshiAmount); - if (balance.spendable == satoshiAmount) { - satoshiAmount = balance.spendable - realfee; + int satAmount = amount.raw.toInt(); + int realfee = await nativeFee(satAmount); + + if (balance.spendable == amount) { + satAmount = balance.spendable.raw.toInt() - realfee; } Map txData = { "fee": realfee, "addresss": address, - "recipientAmt": satoshiAmount, + "recipientAmt": satAmount, }; Logging.instance.log("prepare send: $txData", level: LogLevel.Info); @@ -1739,12 +1742,17 @@ class EpicCashWallet extends CoinServiceAPI : isar_models.TransactionType.outgoing, subType: isar_models.TransactionSubType.none, amount: amt, + amountString: Amount( + rawValue: BigInt.from(amt), + fractionDigits: coin.decimals, + ).toJsonString(), fee: (tx["fee"] == null) ? 0 : int.parse(tx["fee"] as String), height: height, isCancelled: tx["tx_type"] == "TxSentCancelled" || tx["tx_type"] == "TxReceivedCancelled", isLelantus: false, slateId: slateId, + nonce: null, otherData: tx["id"].toString(), inputs: [], outputs: [], @@ -1933,9 +1941,13 @@ class EpicCashWallet extends CoinServiceAPI bool isActive = false; @override - Future estimateFeeFor(int satoshiAmount, int feeRate) async { - int currentFee = await nativeFee(satoshiAmount, ifErrorEstimateFee: true); - return currentFee; + Future estimateFeeFor(Amount amount, int feeRate) async { + int currentFee = + await nativeFee(amount.raw.toInt(), ifErrorEstimateFee: true); + return Amount( + rawValue: BigInt.from(currentFee), + fractionDigits: coin.decimals, + ); } // not used in epic currently @@ -1967,18 +1979,21 @@ class EpicCashWallet extends CoinServiceAPI _balance = Balance( coin: coin, - total: Format.decimalAmountToSatoshis( + total: Amount.fromDecimal( Decimal.parse(total) + Decimal.parse(awaiting), - coin, + fractionDigits: coin.decimals, ), - spendable: Format.decimalAmountToSatoshis( + spendable: Amount.fromDecimal( Decimal.parse(spendable), - coin, + fractionDigits: coin.decimals, ), - blockedTotal: 0, - pendingSpendable: Format.decimalAmountToSatoshis( + blockedTotal: Amount( + rawValue: BigInt.zero, + fractionDigits: coin.decimals, + ), + pendingSpendable: Amount.fromDecimal( Decimal.parse(pending), - coin, + fractionDigits: coin.decimals, ), ); diff --git a/lib/services/coins/ethereum/ethereum_wallet.dart b/lib/services/coins/ethereum/ethereum_wallet.dart new file mode 100644 index 000000000..b5fafeff5 --- /dev/null +++ b/lib/services/coins/ethereum/ethereum_wallet.dart @@ -0,0 +1,1107 @@ +import 'dart:async'; + +import 'package:bip39/bip39.dart' as bip39; +import 'package:ethereum_addresses/ethereum_addresses.dart'; +import 'package:http/http.dart'; +import 'package:isar/isar.dart'; +import 'package:stackwallet/db/hive/db.dart'; +import 'package:stackwallet/db/isar/main_db.dart'; +import 'package:stackwallet/models/balance.dart'; +import 'package:stackwallet/models/isar/models/isar_models.dart'; +import 'package:stackwallet/models/node_model.dart'; +import 'package:stackwallet/models/paymint/fee_object_model.dart'; +import 'package:stackwallet/models/token_balance.dart'; +import 'package:stackwallet/services/coins/coin_service.dart'; +import 'package:stackwallet/services/ethereum/ethereum_api.dart'; +import 'package:stackwallet/services/event_bus/events/global/node_connection_status_changed_event.dart'; +import 'package:stackwallet/services/event_bus/events/global/refresh_percent_changed_event.dart'; +import 'package:stackwallet/services/event_bus/events/global/updated_in_background_event.dart'; +import 'package:stackwallet/services/event_bus/events/global/wallet_sync_status_changed_event.dart'; +import 'package:stackwallet/services/event_bus/global_event_bus.dart'; +import 'package:stackwallet/services/mixins/eth_token_cache.dart'; +import 'package:stackwallet/services/mixins/wallet_cache.dart'; +import 'package:stackwallet/services/mixins/wallet_db.dart'; +import 'package:stackwallet/services/node_service.dart'; +import 'package:stackwallet/services/notifications_api.dart'; +import 'package:stackwallet/services/transaction_notification_tracker.dart'; +import 'package:stackwallet/utilities/amount/amount.dart'; +import 'package:stackwallet/utilities/assets.dart'; +import 'package:stackwallet/utilities/constants.dart'; +import 'package:stackwallet/utilities/default_nodes.dart'; +import 'package:stackwallet/utilities/enums/coin_enum.dart'; +import 'package:stackwallet/utilities/enums/fee_rate_type_enum.dart'; +import 'package:stackwallet/utilities/eth_commons.dart'; +import 'package:stackwallet/utilities/extensions/extensions.dart'; +import 'package:stackwallet/utilities/flutter_secure_storage_interface.dart'; +import 'package:stackwallet/utilities/logger.dart'; +import 'package:stackwallet/utilities/prefs.dart'; +import 'package:tuple/tuple.dart'; +import 'package:web3dart/web3dart.dart' as web3; + +const int MINIMUM_CONFIRMATIONS = 3; + +class EthereumWallet extends CoinServiceAPI with WalletCache, WalletDB { + EthereumWallet({ + required String walletId, + required String walletName, + required Coin coin, + required SecureStorageInterface secureStore, + required TransactionNotificationTracker tracker, + MainDB? mockableOverride, + }) { + txTracker = tracker; + _walletId = walletId; + _walletName = walletName; + _coin = coin; + _secureStore = secureStore; + initCache(walletId, coin); + initWalletDB(mockableOverride: mockableOverride); + } + + NodeModel? _ethNode; + + final _gasLimit = 21000; + + Timer? timer; + Timer? _networkAliveTimer; + + Future updateTokenContracts(List contractAddresses) async { + // final set = getWalletTokenContractAddresses().toSet(); + // set.addAll(contractAddresses); + await updateWalletTokenContractAddresses(contractAddresses); + + GlobalEventBus.instance.fire( + UpdatedInBackgroundEvent( + "$contractAddresses updated/added for: $walletId $walletName", + walletId, + ), + ); + } + + TokenBalance getCachedTokenBalance(EthContract contract) { + final jsonString = DB.instance.get( + boxName: _walletId, + key: TokenCacheKeys.tokenBalance(contract.address), + ) as String?; + if (jsonString == null) { + return TokenBalance( + contractAddress: contract.address, + total: Amount( + rawValue: BigInt.zero, + fractionDigits: contract.decimals, + ), + spendable: Amount( + rawValue: BigInt.zero, + fractionDigits: contract.decimals, + ), + blockedTotal: Amount( + rawValue: BigInt.zero, + fractionDigits: contract.decimals, + ), + pendingSpendable: Amount( + rawValue: BigInt.zero, + fractionDigits: contract.decimals, + ), + ); + } + return TokenBalance.fromJson( + jsonString, + contract.decimals, + ); + } + + // Future removeTokenContract(String contractAddress) async { + // final set = getWalletTokenContractAddresses().toSet(); + // set.removeWhere((e) => e == contractAddress); + // await updateWalletTokenContractAddresses(set.toList()); + // + // GlobalEventBus.instance.fire( + // UpdatedInBackgroundEvent( + // "$contractAddress removed for: $walletId $walletName", + // walletId, + // ), + // ); + // } + + @override + String get walletId => _walletId; + late String _walletId; + + @override + String get walletName => _walletName; + late String _walletName; + + @override + set walletName(String newName) => _walletName = newName; + + @override + set isFavorite(bool markFavorite) { + _isFavorite = markFavorite; + updateCachedIsFavorite(markFavorite); + } + + @override + bool get isFavorite => _isFavorite ??= getCachedIsFavorite(); + bool? _isFavorite; + + @override + Coin get coin => _coin; + late Coin _coin; + + late SecureStorageInterface _secureStore; + late final TransactionNotificationTracker txTracker; + final _prefs = Prefs.instance; + bool longMutex = false; + + NodeModel getCurrentNode() { + return _ethNode ?? + NodeService(secureStorageInterface: _secureStore) + .getPrimaryNodeFor(coin: coin) ?? + DefaultNodes.getNodeFor(coin); + } + + web3.Web3Client getEthClient() { + final node = getCurrentNode(); + return web3.Web3Client(node.host, Client()); + } + + late web3.EthPrivateKey _credentials; + + bool _shouldAutoSync = false; + + @override + bool get shouldAutoSync => _shouldAutoSync; + + @override + set shouldAutoSync(bool shouldAutoSync) { + if (_shouldAutoSync != shouldAutoSync) { + _shouldAutoSync = shouldAutoSync; + if (!shouldAutoSync) { + timer?.cancel(); + timer = null; + stopNetworkAlivePinging(); + } else { + startNetworkAlivePinging(); + refresh(); + } + } + } + + @override + Future> get utxos => db.getUTXOs(walletId).findAll(); + + @override + Future> get transactions => db + .getTransactions(walletId) + .filter() + .otherDataEqualTo( + null) // eth txns with other data where other data is the token contract address + .sortByTimestampDesc() + .findAll(); + + @override + Future get currentReceivingAddress async { + final address = await _currentReceivingAddress; + return checksumEthereumAddress( + address?.value ?? _credentials.address.toString()); + } + + Future get _currentReceivingAddress => db + .getAddresses(walletId) + .filter() + .typeEqualTo(AddressType.ethereum) + .subTypeEqualTo(AddressSubType.receiving) + .sortByDerivationIndexDesc() + .findFirst(); + + @override + Balance get balance => _balance ??= getCachedBalance(); + Balance? _balance; + + Future updateBalance() async { + web3.Web3Client client = getEthClient(); + web3.EtherAmount ethBalance = await client.getBalance(_credentials.address); + _balance = Balance( + coin: coin, + total: Amount( + rawValue: ethBalance.getInWei, + fractionDigits: coin.decimals, + ), + spendable: Amount( + rawValue: ethBalance.getInWei, + fractionDigits: coin.decimals, + ), + blockedTotal: Amount( + rawValue: BigInt.zero, + fractionDigits: coin.decimals, + ), + pendingSpendable: Amount( + rawValue: BigInt.zero, + fractionDigits: coin.decimals, + ), + ); + await updateCachedBalance(_balance!); + } + + @override + Future estimateFeeFor(Amount amount, int feeRate) async { + return estimateFee(feeRate, _gasLimit, coin.decimals); + } + + @override + Future exit() async { + _hasCalledExit = true; + timer?.cancel(); + timer = null; + stopNetworkAlivePinging(); + } + + @override + Future get fees => EthereumAPI.getFees(); + + //Full rescan is not needed for ETH since we have a balance + @override + Future fullRescan( + int maxUnusedAddressGap, int maxNumberOfIndexesToCheck) { + // TODO: implement fullRescan + throw UnimplementedError(); + } + + @override + Future generateNewAddress() { + // TODO: implement generateNewAddress - might not be needed for ETH + throw UnimplementedError(); + } + + bool _hasCalledExit = false; + + @override + bool get hasCalledExit => _hasCalledExit; + + @override + Future initializeExisting() async { + Logging.instance.log( + "initializeExisting() ${coin.prettyName} wallet", + level: LogLevel.Info, + ); + + //First get mnemonic so we can initialize credentials + String privateKey = + getPrivateKey((await mnemonicString)!, (await mnemonicPassphrase)!); + _credentials = web3.EthPrivateKey.fromHex(privateKey); + + if (getCachedId() == null) { + throw Exception( + "Attempted to initialize an existing wallet using an unknown wallet ID!"); + } + await _prefs.init(); + } + + @override + Future initializeNew() async { + Logging.instance.log( + "Generating new ${coin.prettyName} wallet.", + level: LogLevel.Info, + ); + + if (getCachedId() != null) { + throw Exception( + "Attempted to initialize a new wallet using an existing wallet ID!"); + } + + await _prefs.init(); + + try { + await _generateNewWallet(); + } catch (e, s) { + Logging.instance.log( + "Exception rethrown from initializeNew(): $e\n$s", + level: LogLevel.Fatal, + ); + rethrow; + } + await Future.wait([ + updateCachedId(walletId), + updateCachedIsFavorite(false), + ]); + } + + Future _generateNewWallet() async { + // Logging.instance + // .log("IS_INTEGRATION_TEST: $integrationTestFlag", level: LogLevel.Info); + // if (!integrationTestFlag) { + // try { + // final features = await electrumXClient.getServerFeatures(); + // Logging.instance.log("features: $features", level: LogLevel.Info); + // switch (coin) { + // case Coin.namecoin: + // if (features['genesis_hash'] != GENESIS_HASH_MAINNET) { + // throw Exception("genesis hash does not match main net!"); + // } + // break; + // default: + // throw Exception( + // "Attempted to generate a EthereumWallet using a non eth coin type: ${coin.name}"); + // } + // } catch (e, s) { + // Logging.instance.log("$e/n$s", level: LogLevel.Info); + // } + // } + + // this should never fail - sanity check + if ((await mnemonicString) != null || (await mnemonicPassphrase) != null) { + throw Exception( + "Attempted to overwrite mnemonic on generate new wallet!"); + } + + final String mnemonic = bip39.generateMnemonic(strength: 256); + await _secureStore.write(key: '${_walletId}_mnemonic', value: mnemonic); + await _secureStore.write( + key: '${_walletId}_mnemonicPassphrase', + value: "", + ); + + String privateKey = getPrivateKey(mnemonic, ""); + _credentials = web3.EthPrivateKey.fromHex(privateKey); + + final address = Address( + walletId: walletId, value: _credentials.address.toString(), + publicKey: [], // maybe store address bytes here? seems a waste of space though + derivationIndex: 0, + derivationPath: DerivationPath()..value = "$hdPathEthereum/0", + type: AddressType.ethereum, + subType: AddressSubType.receiving, + ); + + await db.putAddress(address); + + Logging.instance.log("_generateNewWalletFinished", level: LogLevel.Info); + } + + bool _isConnected = false; + + @override + bool get isConnected => _isConnected; + + @override + bool get isRefreshing => refreshMutex; + + bool refreshMutex = false; + + @override + Future get maxFee async { + throw UnimplementedError("Not used for eth"); + } + + @override + Future> get mnemonic => _getMnemonicList(); + + @override + Future get mnemonicString => + _secureStore.read(key: '${_walletId}_mnemonic'); + + @override + Future get mnemonicPassphrase => _secureStore.read( + key: '${_walletId}_mnemonicPassphrase', + ); + + Future get chainHeight async { + web3.Web3Client client = getEthClient(); + try { + final height = await client.getBlockNumber(); + await updateCachedChainHeight(height); + if (height > storedChainHeight) { + GlobalEventBus.instance.fire( + UpdatedInBackgroundEvent( + "Updated current chain height in $walletId $walletName!", + walletId, + ), + ); + } + return height; + } catch (e, s) { + Logging.instance.log("Exception caught in chainHeight: $e\n$s", + level: LogLevel.Error); + return storedChainHeight; + } + } + + @override + int get storedChainHeight => getCachedChainHeight(); + + Future> _getMnemonicList() async { + final _mnemonicString = await mnemonicString; + if (_mnemonicString == null) { + return []; + } + final List data = _mnemonicString.split(' '); + return data; + } + + @override + Future> prepareSend({ + required String address, + required Amount amount, + Map? args, + }) async { + final feeRateType = args?["feeRate"]; + int fee = 0; + final feeObject = await fees; + switch (feeRateType) { + case FeeRateType.fast: + fee = feeObject.fast; + break; + case FeeRateType.average: + fee = feeObject.medium; + break; + case FeeRateType.slow: + fee = feeObject.slow; + break; + } + + final feeEstimate = await estimateFeeFor(amount, fee); + + // bool isSendAll = false; + // final availableBalance = balance.spendable; + // if (satoshiAmount == availableBalance) { + // isSendAll = true; + // } + // + // if (isSendAll) { + // //Subtract fee amount from send amount + // satoshiAmount -= feeEstimate; + // } + + final client = getEthClient(); + + final myAddress = await currentReceivingAddress; + final myWeb3Address = web3.EthereumAddress.fromHex(myAddress); + + final est = await client.estimateGas( + sender: myWeb3Address, + to: web3.EthereumAddress.fromHex(address), + gasPrice: web3.EtherAmount.fromUnitAndValue( + web3.EtherUnit.wei, + fee, + ), + amountOfGas: BigInt.from(_gasLimit), + value: web3.EtherAmount.inWei(amount.raw), + ); + + final nonce = args?["nonce"] as int? ?? + await client.getTransactionCount(myWeb3Address, + atBlock: const web3.BlockNum.pending()); + + final nResponse = await EthereumAPI.getAddressNonce(address: myAddress); + print("=============================================================="); + print("ETH client.estimateGas: $est"); + print("ETH estimateFeeFor : $feeEstimate"); + print("ETH nonce custom response: $nResponse"); + print("ETH actual nonce : $nonce"); + print("=============================================================="); + + final tx = web3.Transaction( + to: web3.EthereumAddress.fromHex(address), + gasPrice: web3.EtherAmount.fromUnitAndValue( + web3.EtherUnit.wei, + fee, + ), + maxGas: _gasLimit, + value: web3.EtherAmount.inWei(amount.raw), + nonce: nonce, + ); + + Map txData = { + "fee": feeEstimate, + "feeInWei": fee, + "address": address, + "recipientAmt": amount, + "ethTx": tx, + "chainId": (await client.getChainId()).toInt(), + "nonce": tx.nonce, + }; + + return txData; + } + + @override + Future confirmSend({required Map txData}) async { + web3.Web3Client client = getEthClient(); + + final txid = await client.sendTransaction( + _credentials, + txData["ethTx"] as web3.Transaction, + chainId: txData["chainId"] as int, + ); + + return txid; + } + + @override + Future recoverFromMnemonic({ + required String mnemonic, + String? mnemonicPassphrase, + required int maxUnusedAddressGap, + required int maxNumberOfIndexesToCheck, + required int height, + }) async { + longMutex = true; + final start = DateTime.now(); + + try { + // check to make sure we aren't overwriting a mnemonic + // this should never fail + if ((await mnemonicString) != null || + (await this.mnemonicPassphrase) != null) { + longMutex = false; + throw Exception("Attempted to overwrite mnemonic on restore!"); + } + + await _secureStore.write( + key: '${_walletId}_mnemonic', value: mnemonic.trim()); + await _secureStore.write( + key: '${_walletId}_mnemonicPassphrase', + value: mnemonicPassphrase ?? "", + ); + + String privateKey = + getPrivateKey(mnemonic.trim(), mnemonicPassphrase ?? ""); + _credentials = web3.EthPrivateKey.fromHex(privateKey); + + final address = Address( + walletId: walletId, value: _credentials.address.toString(), + publicKey: [], // maybe store address bytes here? seems a waste of space though + derivationIndex: 0, + derivationPath: DerivationPath()..value = "$hdPathEthereum/0", + type: AddressType.ethereum, + subType: AddressSubType.receiving, + ); + + await db.putAddress(address); + + await Future.wait([ + updateCachedId(walletId), + updateCachedIsFavorite(false), + ]); + } catch (e, s) { + Logging.instance.log( + "Exception rethrown from recoverFromMnemonic(): $e\n$s", + level: LogLevel.Error); + longMutex = false; + rethrow; + } + + longMutex = false; + final end = DateTime.now(); + Logging.instance.log( + "$walletName recovery time: ${end.difference(start).inMilliseconds} millis", + level: LogLevel.Info); + } + + Future> _fetchAllOwnAddresses() => db + .getAddresses(walletId) + .filter() + .not() + .typeEqualTo(AddressType.nonWallet) + .and() + .group((q) => q + .subTypeEqualTo(AddressSubType.receiving) + .or() + .subTypeEqualTo(AddressSubType.change)) + .findAll(); + + Future refreshIfThereIsNewData() async { + if (longMutex) return false; + if (_hasCalledExit) return false; + final currentChainHeight = await chainHeight; + + try { + bool needsRefresh = false; + Set txnsToCheck = {}; + + for (final String txid in txTracker.pendings) { + if (!txTracker.wasNotifiedConfirmed(txid)) { + txnsToCheck.add(txid); + } + } + + for (String txid in txnsToCheck) { + final response = await EthereumAPI.getEthTransactionByHash(txid); + final txBlockNumber = response.value?.blockNumber; + + if (txBlockNumber != null) { + final int txConfirmations = currentChainHeight - txBlockNumber; + bool isUnconfirmed = txConfirmations < MINIMUM_CONFIRMATIONS; + if (!isUnconfirmed) { + needsRefresh = true; + break; + } + } + } + if (!needsRefresh) { + var allOwnAddresses = await _fetchAllOwnAddresses(); + final response = await EthereumAPI.getEthTransactions( + allOwnAddresses.elementAt(0).value, + ); + if (response.value != null) { + final allTxs = response.value!; + for (final element in allTxs) { + final txid = element.hash; + if ((await db + .getTransactions(walletId) + .filter() + .txidMatches(txid) + .findFirst()) == + null) { + Logging.instance.log( + " txid not found in address history already $txid", + level: LogLevel.Info); + needsRefresh = true; + break; + } + } + } else { + Logging.instance.log( + " refreshIfThereIsNewData get eth transactions failed: ${response.exception}", + level: LogLevel.Error, + ); + } + } + return needsRefresh; + } catch (e, s) { + Logging.instance.log( + "Exception caught in refreshIfThereIsNewData: $e\n$s", + level: LogLevel.Error); + rethrow; + } + } + + Future getAllTxsToWatch() async { + if (_hasCalledExit) return; + List unconfirmedTxnsToNotifyPending = []; + List unconfirmedTxnsToNotifyConfirmed = []; + + final currentChainHeight = await chainHeight; + + final txCount = await db.getTransactions(walletId).count(); + + const paginateLimit = 50; + + for (int i = 0; i < txCount; i += paginateLimit) { + final transactions = await db + .getTransactions(walletId) + .offset(i) + .limit(paginateLimit) + .findAll(); + for (final tx in transactions) { + if (tx.isConfirmed(currentChainHeight, MINIMUM_CONFIRMATIONS)) { + // get all transactions that were notified as pending but not as confirmed + if (txTracker.wasNotifiedPending(tx.txid) && + !txTracker.wasNotifiedConfirmed(tx.txid)) { + unconfirmedTxnsToNotifyConfirmed.add(tx); + } + } else { + // get all transactions that were not notified as pending yet + if (!txTracker.wasNotifiedPending(tx.txid)) { + unconfirmedTxnsToNotifyPending.add(tx); + } + } + } + } + + // notify on unconfirmed transactions + for (final tx in unconfirmedTxnsToNotifyPending) { + final confirmations = tx.getConfirmations(currentChainHeight); + + if (tx.type == TransactionType.incoming) { + unawaited(NotificationApi.showNotification( + title: "Incoming transaction", + body: walletName, + walletId: walletId, + iconAssetName: Assets.svg.iconFor(coin: coin), + date: DateTime.fromMillisecondsSinceEpoch(tx.timestamp * 1000), + shouldWatchForUpdates: confirmations < MINIMUM_CONFIRMATIONS, + coinName: coin.name, + txid: tx.txid, + confirmations: confirmations, + requiredConfirmations: MINIMUM_CONFIRMATIONS, + )); + await txTracker.addNotifiedPending(tx.txid); + } else if (tx.type == TransactionType.outgoing) { + unawaited(NotificationApi.showNotification( + title: "Sending transaction", + body: walletName, + walletId: walletId, + iconAssetName: Assets.svg.iconFor(coin: coin), + date: DateTime.fromMillisecondsSinceEpoch(tx.timestamp * 1000), + shouldWatchForUpdates: confirmations < MINIMUM_CONFIRMATIONS, + coinName: coin.name, + txid: tx.txid, + confirmations: confirmations, + requiredConfirmations: MINIMUM_CONFIRMATIONS, + )); + await txTracker.addNotifiedPending(tx.txid); + } + } + + // notify on confirmed + for (final tx in unconfirmedTxnsToNotifyConfirmed) { + if (tx.type == TransactionType.incoming) { + unawaited(NotificationApi.showNotification( + title: "Incoming transaction confirmed", + body: walletName, + walletId: walletId, + iconAssetName: Assets.svg.iconFor(coin: coin), + date: DateTime.fromMillisecondsSinceEpoch(tx.timestamp * 1000), + shouldWatchForUpdates: false, + coinName: coin.name, + )); + await txTracker.addNotifiedConfirmed(tx.txid); + } else if (tx.type == TransactionType.outgoing) { + unawaited(NotificationApi.showNotification( + title: "Outgoing transaction confirmed", + body: walletName, + walletId: walletId, + iconAssetName: Assets.svg.iconFor(coin: coin), + date: DateTime.fromMillisecondsSinceEpoch(tx.timestamp * 1000), + shouldWatchForUpdates: false, + coinName: coin.name, + )); + await txTracker.addNotifiedConfirmed(tx.txid); + } + } + } + + @override + Future refresh() async { + if (refreshMutex) { + Logging.instance.log("$walletId $walletName refreshMutex denied", + level: LogLevel.Info); + return; + } else { + refreshMutex = true; + } + + try { + GlobalEventBus.instance.fire( + WalletSyncStatusChangedEvent( + WalletSyncStatus.syncing, + walletId, + coin, + ), + ); + + GlobalEventBus.instance.fire(RefreshPercentChangedEvent(0.0, walletId)); + GlobalEventBus.instance.fire(RefreshPercentChangedEvent(0.1, walletId)); + + final currentHeight = await chainHeight; + const storedHeight = 1; //await storedChainHeight; + + Logging.instance + .log("chain height: $currentHeight", level: LogLevel.Info); + Logging.instance + .log("cached height: $storedHeight", level: LogLevel.Info); + + if (currentHeight != storedHeight) { + GlobalEventBus.instance.fire(RefreshPercentChangedEvent(0.2, walletId)); + + final newTxDataFuture = _refreshTransactions(); + GlobalEventBus.instance + .fire(RefreshPercentChangedEvent(0.50, walletId)); + + // final feeObj = _getFees(); + GlobalEventBus.instance + .fire(RefreshPercentChangedEvent(0.60, walletId)); + + GlobalEventBus.instance + .fire(RefreshPercentChangedEvent(0.70, walletId)); + // _feeObject = Future(() => feeObj); + GlobalEventBus.instance + .fire(RefreshPercentChangedEvent(0.80, walletId)); + + final allTxsToWatch = getAllTxsToWatch(); + await Future.wait([ + updateBalance(), + newTxDataFuture, + // feeObj, + allTxsToWatch, + ]); + GlobalEventBus.instance + .fire(RefreshPercentChangedEvent(0.90, walletId)); + } + refreshMutex = false; + GlobalEventBus.instance.fire(RefreshPercentChangedEvent(1.0, walletId)); + GlobalEventBus.instance.fire( + WalletSyncStatusChangedEvent( + WalletSyncStatus.synced, + walletId, + coin, + ), + ); + + if (shouldAutoSync) { + timer ??= Timer.periodic(const Duration(seconds: 30), (timer) async { + Logging.instance.log( + "Periodic refresh check for $walletId $walletName in object instance: $hashCode", + level: LogLevel.Info); + if (await refreshIfThereIsNewData()) { + await refresh(); + GlobalEventBus.instance.fire(UpdatedInBackgroundEvent( + "New data found in $walletId $walletName in background!", + walletId)); + } + }); + } + } catch (error, strace) { + refreshMutex = false; + GlobalEventBus.instance.fire( + NodeConnectionStatusChangedEvent( + NodeConnectionStatus.disconnected, + walletId, + coin, + ), + ); + GlobalEventBus.instance.fire( + WalletSyncStatusChangedEvent( + WalletSyncStatus.unableToSync, + walletId, + coin, + ), + ); + Logging.instance.log( + "Caught exception in $walletName $walletId refresh(): $error\n$strace", + level: LogLevel.Warning, + ); + } + } + + @override + Future testNetworkConnection() async { + web3.Web3Client client = getEthClient(); + try { + await client.getBlockNumber(); + return true; + } catch (_) { + return false; + } + } + + void _periodicPingCheck() async { + bool hasNetwork = await testNetworkConnection(); + _isConnected = hasNetwork; + if (_isConnected != hasNetwork) { + NodeConnectionStatus status = hasNetwork + ? NodeConnectionStatus.connected + : NodeConnectionStatus.disconnected; + GlobalEventBus.instance + .fire(NodeConnectionStatusChangedEvent(status, walletId, coin)); + } + } + + @override + Future updateNode(bool shouldRefresh) async { + _ethNode = NodeService(secureStorageInterface: _secureStore) + .getPrimaryNodeFor(coin: coin) ?? + DefaultNodes.getNodeFor(coin); + + if (shouldRefresh) { + unawaited(refresh()); + } + } + + @override + Future updateSentCachedTxData(Map txData) async { + final txid = txData["txid"] as String; + final addressString = txData["address"] as String; + final response = await EthereumAPI.getEthTransactionByHash(txid); + + final transaction = Transaction( + walletId: walletId, + txid: txid, + timestamp: DateTime.now().millisecondsSinceEpoch ~/ 1000, + type: TransactionType.outgoing, + subType: TransactionSubType.none, + // precision may be lost here hence the following amountString + amount: (txData["recipientAmt"] as Amount).raw.toInt(), + amountString: (txData["recipientAmt"] as Amount).toJsonString(), + fee: txData["fee"] as int, + height: null, + isCancelled: false, + isLelantus: false, + otherData: null, + slateId: null, + nonce: (txData["nonce"] as int?) ?? + response.value?.nonce.toBigIntFromHex.toInt(), + inputs: [], + outputs: [], + ); + + Address? address = await db.getAddress( + walletId, + addressString, + ); + + address ??= Address( + walletId: walletId, + value: addressString, + publicKey: [], + derivationIndex: -1, + derivationPath: null, + type: AddressType.ethereum, + subType: AddressSubType.nonWallet, + ); + + await db.addNewTransactionData( + [ + Tuple2(transaction, address), + ], + walletId, + ); + } + + @override + bool validateAddress(String address) { + return isValidEthereumAddress(address); + } + + Future _refreshTransactions() async { + String thisAddress = await currentReceivingAddress; + + final response = await EthereumAPI.getEthTransactions(thisAddress); + + if (response.value == null) { + Logging.instance.log( + "Failed to refresh transactions for ${coin.prettyName} $walletName " + "$walletId: ${response.exception}", + level: LogLevel.Warning, + ); + return; + } + + final txsResponse = + await EthereumAPI.getEthTransactionNonces(response.value!); + + if (txsResponse.value != null) { + final allTxs = txsResponse.value!; + final List> txnsData = []; + for (final tuple in allTxs) { + final element = tuple.item1; + + Amount transactionAmount = element.value; + + bool isIncoming; + bool txFailed = false; + if (checksumEthereumAddress(element.from) == thisAddress) { + if (element.isError != 0) { + txFailed = true; + } + isIncoming = false; + } else { + isIncoming = true; + } + + //Calculate fees (GasLimit * gasPrice) + // int txFee = element.gasPrice * element.gasUsed; + Amount txFee = element.gasCost; + + final String addressString = checksumEthereumAddress(element.to); + final int height = element.blockNumber; + + final txn = Transaction( + walletId: walletId, + txid: element.hash, + timestamp: element.timestamp, + type: + isIncoming ? TransactionType.incoming : TransactionType.outgoing, + subType: TransactionSubType.none, + amount: transactionAmount.raw.toInt(), + amountString: transactionAmount.toJsonString(), + fee: txFee.raw.toInt(), + height: height, + isCancelled: txFailed, + isLelantus: false, + slateId: null, + otherData: null, + nonce: tuple.item2, + inputs: [], + outputs: [], + ); + + Address? transactionAddress = await db + .getAddresses(walletId) + .filter() + .valueEqualTo(addressString) + .findFirst(); + + if (transactionAddress == null) { + if (isIncoming) { + transactionAddress = Address( + walletId: walletId, + value: addressString, + publicKey: [], + derivationIndex: 0, + derivationPath: DerivationPath()..value = "$hdPathEthereum/0", + type: AddressType.ethereum, + subType: AddressSubType.receiving, + ); + } else { + final myRcvAddr = await currentReceivingAddress; + final isSentToSelf = myRcvAddr == addressString; + + transactionAddress = Address( + walletId: walletId, + value: addressString, + publicKey: [], + derivationIndex: isSentToSelf ? 0 : -1, + derivationPath: isSentToSelf + ? (DerivationPath()..value = "$hdPathEthereum/0") + : null, + type: AddressType.ethereum, + subType: isSentToSelf + ? AddressSubType.receiving + : AddressSubType.nonWallet, + ); + } + } + + txnsData.add(Tuple2(txn, transactionAddress)); + } + await db.addNewTransactionData(txnsData, walletId); + + // quick hack to notify manager to call notifyListeners if + // transactions changed + if (txnsData.isNotEmpty) { + GlobalEventBus.instance.fire( + UpdatedInBackgroundEvent( + "Transactions updated/added for: $walletId $walletName ", + walletId, + ), + ); + } + } else { + Logging.instance.log( + "Failed to refresh transactions with nonces for ${coin.prettyName} " + "$walletName $walletId: ${txsResponse.exception}", + level: LogLevel.Warning, + ); + } + } + + void stopNetworkAlivePinging() { + _networkAliveTimer?.cancel(); + _networkAliveTimer = null; + } + + void startNetworkAlivePinging() { + // call once on start right away + _periodicPingCheck(); + + // then periodically check + _networkAliveTimer = Timer.periodic( + Constants.networkAliveTimerDuration, + (_) async { + _periodicPingCheck(); + }, + ); + } +} diff --git a/lib/services/coins/firo/firo_wallet.dart b/lib/services/coins/firo/firo_wallet.dart index cadddeabf..22cdde933 100644 --- a/lib/services/coins/firo/firo_wallet.dart +++ b/lib/services/coins/firo/firo_wallet.dart @@ -10,7 +10,7 @@ import 'package:decimal/decimal.dart'; import 'package:flutter/foundation.dart'; import 'package:isar/isar.dart'; import 'package:lelantus/lelantus.dart'; -import 'package:stackwallet/db/main_db.dart'; +import 'package:stackwallet/db/isar/main_db.dart'; import 'package:stackwallet/electrumx_rpc/cached_electrumx.dart'; import 'package:stackwallet/electrumx_rpc/electrumx.dart'; import 'package:stackwallet/models/balance.dart'; @@ -33,6 +33,7 @@ import 'package:stackwallet/services/node_service.dart'; import 'package:stackwallet/services/notifications_api.dart'; import 'package:stackwallet/services/transaction_notification_tracker.dart'; import 'package:stackwallet/utilities/address_utils.dart'; +import 'package:stackwallet/utilities/amount/amount.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/bip32_utils.dart'; import 'package:stackwallet/utilities/constants.dart'; @@ -473,12 +474,17 @@ Future> staticProcessRestore( type: element.type, subType: isar_models.TransactionSubType.mint, amount: element.amount, + amountString: Amount( + rawValue: BigInt.from(element.amount), + fractionDigits: Coin.firo.decimals, + ).toJsonString(), fee: sharedFee, height: element.height, isCancelled: false, isLelantus: true, slateId: null, otherData: txid, + nonce: null, inputs: element.inputs, outputs: element.outputs, )..address.value = element.address.value; @@ -702,7 +708,10 @@ Future isolateCreateJoinSplitTransaction( "txid": txId, "txHex": txHex, "value": amount, - "fees": Format.satoshisToAmount(fee, coin: coin).toDouble(), + "fees": Amount( + rawValue: BigInt.from(fee), + fractionDigits: coin.decimals, + ).decimal.toDouble(), "fee": fee, "vSize": extTx.virtualSize(), "jmintValue": changeToMint, @@ -711,7 +720,10 @@ Future isolateCreateJoinSplitTransaction( "height": locktime, "txType": "Sent", "confirmed_status": false, - "amount": Format.satoshisToAmount(amount, coin: coin).toDouble(), + "amount": Amount( + rawValue: BigInt.from(amount), + fractionDigits: coin.decimals, + ).decimal.toDouble(), "recipientAmt": amount, "address": address, "timestamp": DateTime.now().millisecondsSinceEpoch ~/ 1000, @@ -901,13 +913,16 @@ class FiroWallet extends CoinServiceAPI timestamp: DateTime.now().millisecondsSinceEpoch ~/ 1000, type: isar_models.TransactionType.outgoing, subType: isar_models.TransactionSubType.none, - amount: txData["recipientAmt"] as int, + // precision may be lost here hence the following amountString + amount: (txData["recipientAmt"] as Amount).raw.toInt(), + amountString: (txData["recipientAmt"] as Amount).toJsonString(), fee: txData["fee"] as int, height: null, isCancelled: false, isLelantus: false, otherData: null, slateId: null, + nonce: null, inputs: [], outputs: [], ); @@ -1022,7 +1037,7 @@ class FiroWallet extends CoinServiceAPI Future> prepareSendPublic({ required String address, - required int satoshiAmount, + required Amount amount, Map? args, }) async { try { @@ -1051,14 +1066,17 @@ class FiroWallet extends CoinServiceAPI // check for send all bool isSendAll = false; - final balance = - Format.decimalAmountToSatoshis(availablePublicBalance(), coin); - if (satoshiAmount == balance) { + final balance = availablePublicBalance(); + if (amount == balance) { isSendAll = true; } - final txData = - await coinSelection(satoshiAmount, rate, address, isSendAll); + final txData = await coinSelection( + amount.raw.toInt(), + rate, + address, + isSendAll, + ); Logging.instance.log("prepare send: $txData", level: LogLevel.Info); try { @@ -1131,20 +1149,22 @@ class FiroWallet extends CoinServiceAPI @override Future> prepareSend({ required String address, - required int satoshiAmount, + required Amount amount, Map? args, }) async { try { // check for send all bool isSendAll = false; - final balance = - Format.decimalAmountToSatoshis(availablePrivateBalance(), coin); - if (satoshiAmount == balance) { + final balance = availablePrivateBalance(); + if (amount == balance) { // print("is send all"); isSendAll = true; } - dynamic txHexOrError = - await _createJoinSplitTransaction(satoshiAmount, address, isSendAll); + dynamic txHexOrError = await _createJoinSplitTransaction( + amount.raw.toInt(), + address, + isSendAll, + ); Logging.instance.log("txHexOrError $txHexOrError", level: LogLevel.Error); if (txHexOrError is int) { // Here, we assume that transaction crafting returned an error @@ -2297,9 +2317,10 @@ class FiroWallet extends CoinServiceAPI Future _fetchMaxFee() async { final balance = availablePrivateBalance(); - int spendAmount = (balance * Decimal.fromInt(Constants.satsPerCoin(coin))) - .toBigInt() - .toInt(); + int spendAmount = + (balance.decimal * Decimal.fromInt(Constants.satsPerCoin(coin))) + .toBigInt() + .toInt(); int fee = await estimateJoinSplitFee(spendAmount); return fee; } @@ -2495,10 +2516,23 @@ class FiroWallet extends CoinServiceAPI _balancePrivate = Balance( coin: coin, - total: intLelantusBalance + unconfirmedLelantusBalance, - spendable: intLelantusBalance, - blockedTotal: 0, - pendingSpendable: unconfirmedLelantusBalance, + total: Amount( + rawValue: + BigInt.from(intLelantusBalance + unconfirmedLelantusBalance), + fractionDigits: coin.decimals, + ), + spendable: Amount( + rawValue: BigInt.from(intLelantusBalance), + fractionDigits: coin.decimals, + ), + blockedTotal: Amount( + rawValue: BigInt.zero, + fractionDigits: coin.decimals, + ), + pendingSpendable: Amount( + rawValue: BigInt.from(unconfirmedLelantusBalance), + fractionDigits: coin.decimals, + ), ); await updateCachedBalanceSecondary(_balancePrivate!); @@ -2597,8 +2631,10 @@ class FiroWallet extends CoinServiceAPI final feesObject = await fees; - final Decimal fastFee = - Format.satoshisToAmount(feesObject.fast, coin: coin); + final Decimal fastFee = Amount( + rawValue: BigInt.from(feesObject.fast), + fractionDigits: coin.decimals, + ).decimal; int firoFee = (dvSize * fastFee * Decimal.fromInt(100000)).toDouble().ceil(); // int firoFee = (vSize * feesObject.fast * (1 / 1000.0) * 100000000).ceil(); @@ -2787,12 +2823,18 @@ class FiroWallet extends CoinServiceAPI "txid": txId, "txHex": txHex, "value": amount - fee, - "fees": Format.satoshisToAmount(fee, coin: coin).toDouble(), + "fees": Amount( + rawValue: BigInt.from(fee), + fractionDigits: coin.decimals, + ).decimal.toDouble(), "publicCoin": "", "height": height, "txType": "Sent", "confirmed_status": false, - "amount": Format.satoshisToAmount(amount, coin: coin).toDouble(), + "amount": Amount( + rawValue: BigInt.from(amount), + fractionDigits: coin.decimals, + ).decimal.toDouble(), "timestamp": DateTime.now().millisecondsSinceEpoch ~/ 1000, "subType": "mint", "mintsMap": mintsMap, @@ -3032,6 +3074,11 @@ class FiroWallet extends CoinServiceAPI } await firoUpdateLelantusCoins(coins); + final amount = Amount.fromDecimal( + Decimal.parse(transactionInfo["amount"].toString()), + fractionDigits: coin.decimals, + ); + // add the send transaction final transaction = isar_models.Transaction( walletId: walletId, @@ -3046,18 +3093,17 @@ class FiroWallet extends CoinServiceAPI : transactionInfo["subType"] == "join" ? isar_models.TransactionSubType.join : isar_models.TransactionSubType.none, - amount: Format.decimalAmountToSatoshis( - Decimal.parse(transactionInfo["amount"].toString()), - coin, - ), - fee: Format.decimalAmountToSatoshis( + amount: amount.raw.toInt(), + amountString: amount.toJsonString(), + fee: Amount.fromDecimal( Decimal.parse(transactionInfo["fees"].toString()), - coin, - ), + fractionDigits: coin.decimals, + ).raw.toInt(), height: transactionInfo["height"] as int?, isCancelled: false, isLelantus: true, slateId: null, + nonce: null, otherData: transactionInfo["otherData"] as String?, inputs: [], outputs: [], @@ -3139,9 +3185,18 @@ class FiroWallet extends CoinServiceAPI numberOfBlocksFast: f, numberOfBlocksAverage: m, numberOfBlocksSlow: s, - fast: Format.decimalAmountToSatoshis(fast, coin), - medium: Format.decimalAmountToSatoshis(medium, coin), - slow: Format.decimalAmountToSatoshis(slow, coin), + fast: Amount.fromDecimal( + fast, + fractionDigits: coin.decimals, + ).raw.toInt(), + medium: Amount.fromDecimal( + medium, + fractionDigits: coin.decimals, + ).raw.toInt(), + slow: Amount.fromDecimal( + slow, + fractionDigits: coin.decimals, + ).raw.toInt(), ); Logging.instance.log("fetched fees: $feeObject", level: LogLevel.Info); @@ -3593,10 +3648,10 @@ class FiroWallet extends CoinServiceAPI scriptPubKeyAddress: json["scriptPubKey"]?["addresses"]?[0] as String? ?? json['scriptPubKey']['type'] as String, - value: Format.decimalAmountToSatoshis( + value: Amount.fromDecimal( Decimal.parse(json["value"].toString()), - coin, - ), + fractionDigits: coin.decimals, + ).raw.toInt(), ); outs.add(output); } @@ -3609,12 +3664,17 @@ class FiroWallet extends CoinServiceAPI type: type, subType: subType, amount: amount, + amountString: Amount( + rawValue: BigInt.from(amount), + fractionDigits: Coin.firo.decimals, + ).toJsonString(), fee: fees, height: txObject["height"] as int? ?? 0, isCancelled: false, isLelantus: false, slateId: null, otherData: null, + nonce: null, inputs: ins, outputs: outs, ); @@ -3672,10 +3732,22 @@ class FiroWallet extends CoinServiceAPI final currentChainHeight = await chainHeight; final List outputArray = []; - int satoshiBalanceTotal = 0; - int satoshiBalancePending = 0; - int satoshiBalanceSpendable = 0; - int satoshiBalanceBlocked = 0; + Amount satoshiBalanceTotal = Amount( + rawValue: BigInt.zero, + fractionDigits: coin.decimals, + ); + Amount satoshiBalancePending = Amount( + rawValue: BigInt.zero, + fractionDigits: coin.decimals, + ); + Amount satoshiBalanceSpendable = Amount( + rawValue: BigInt.zero, + fractionDigits: coin.decimals, + ); + Amount satoshiBalanceBlocked = Amount( + rawValue: BigInt.zero, + fractionDigits: coin.decimals, + ); for (int i = 0; i < fetchedUtxoList.length; i++) { for (int j = 0; j < fetchedUtxoList[i].length; j++) { @@ -3700,15 +3772,19 @@ class FiroWallet extends CoinServiceAPI blockTime: txn["blocktime"] as int?, ); - satoshiBalanceTotal += utxo.value; + final utxoAmount = Amount( + rawValue: BigInt.from(utxo.value), + fractionDigits: coin.decimals, + ); + satoshiBalanceTotal = satoshiBalanceTotal + utxoAmount; if (utxo.isBlocked) { - satoshiBalanceBlocked += utxo.value; + satoshiBalanceBlocked = satoshiBalanceBlocked + utxoAmount; } else { if (utxo.isConfirmed(currentChainHeight, MINIMUM_CONFIRMATIONS)) { - satoshiBalanceSpendable += utxo.value; + satoshiBalanceSpendable = satoshiBalanceSpendable + utxoAmount; } else { - satoshiBalancePending += utxo.value; + satoshiBalancePending = satoshiBalancePending + utxoAmount; } } @@ -4731,7 +4807,7 @@ class FiroWallet extends CoinServiceAPI int spendAmount, ) async { var lelantusEntry = await _getLelantusEntry(); - final balance = availablePrivateBalance(); + final balance = availablePrivateBalance().decimal; int spendAmount = (balance * Decimal.fromInt(Constants.satsPerCoin(coin))) .toBigInt() .toInt(); @@ -4788,27 +4864,34 @@ class FiroWallet extends CoinServiceAPI // return fee; @override - Future estimateFeeFor(int satoshiAmount, int feeRate) async { - int fee = await estimateJoinSplitFee(satoshiAmount); - return fee; + Future estimateFeeFor(Amount amount, int feeRate) async { + int fee = await estimateJoinSplitFee(amount.raw.toInt()); + return Amount(rawValue: BigInt.from(fee), fractionDigits: coin.decimals); } - Future estimateFeeForPublic(int satoshiAmount, int feeRate) async { + Future estimateFeeForPublic(Amount amount, int feeRate) async { final available = balance.spendable; - if (available == satoshiAmount) { - return satoshiAmount - (await sweepAllEstimate(feeRate)); - } else if (satoshiAmount <= 0 || satoshiAmount > available) { + if (available == amount) { + return amount - (await sweepAllEstimate(feeRate)); + } else if (amount <= Amount.zero || amount > available) { return roughFeeEstimate(1, 2, feeRate); } - int runningBalance = 0; + Amount runningBalance = Amount( + rawValue: BigInt.zero, + fractionDigits: coin.decimals, + ); int inputCount = 0; for (final output in (await utxos)) { if (!output.isBlocked) { - runningBalance += output.value; + runningBalance = runningBalance + + Amount( + rawValue: BigInt.from(output.value), + fractionDigits: coin.decimals, + ); inputCount++; - if (runningBalance > satoshiAmount) { + if (runningBalance > amount) { break; } } @@ -4817,19 +4900,24 @@ class FiroWallet extends CoinServiceAPI final oneOutPutFee = roughFeeEstimate(inputCount, 1, feeRate); final twoOutPutFee = roughFeeEstimate(inputCount, 2, feeRate); - if (runningBalance - satoshiAmount > oneOutPutFee) { - if (runningBalance - satoshiAmount > oneOutPutFee + DUST_LIMIT) { - final change = runningBalance - satoshiAmount - twoOutPutFee; - if (change > DUST_LIMIT && - runningBalance - satoshiAmount - change == twoOutPutFee) { - return runningBalance - satoshiAmount - change; + final dustLimitAmount = Amount( + rawValue: BigInt.from(DUST_LIMIT), + fractionDigits: coin.decimals, + ); + + if (runningBalance - amount > oneOutPutFee) { + if (runningBalance - amount > oneOutPutFee + dustLimitAmount) { + final change = runningBalance - amount - twoOutPutFee; + if (change > dustLimitAmount && + runningBalance - amount - change == twoOutPutFee) { + return runningBalance - amount - change; } else { - return runningBalance - satoshiAmount; + return runningBalance - amount; } } else { - return runningBalance - satoshiAmount; + return runningBalance - amount; } - } else if (runningBalance - satoshiAmount == oneOutPutFee) { + } else if (runningBalance - amount == oneOutPutFee) { return oneOutPutFee; } else { return twoOutPutFee; @@ -4837,12 +4925,15 @@ class FiroWallet extends CoinServiceAPI } // TODO: correct formula for firo? - int roughFeeEstimate(int inputCount, int outputCount, int feeRatePerKB) { - return ((181 * inputCount) + (34 * outputCount) + 10) * - (feeRatePerKB / 1000).ceil(); + Amount roughFeeEstimate(int inputCount, int outputCount, int feeRatePerKB) { + return Amount( + rawValue: BigInt.from(((181 * inputCount) + (34 * outputCount) + 10) * + (feeRatePerKB / 1000).ceil()), + fractionDigits: coin.decimals, + ); } - Future sweepAllEstimate(int feeRate) async { + Future sweepAllEstimate(int feeRate) async { int available = 0; int inputCount = 0; for (final output in (await utxos)) { @@ -4856,7 +4947,11 @@ class FiroWallet extends CoinServiceAPI // transaction will only have 1 output minus the fee final estimatedFee = roughFeeEstimate(inputCount, 1, feeRate); - return available - estimatedFee; + return Amount( + rawValue: BigInt.from(available), + fractionDigits: coin.decimals, + ) - + estimatedFee; } Future>> fastFetch(List allTxHashes) async { @@ -4929,6 +5024,11 @@ class FiroWallet extends CoinServiceAPI tx["address"] = tx["vout"][sendIndex]["scriptPubKey"]["addresses"][0]; tx["fees"] = tx["vin"][0]["nFees"]; + final Amount amount = Amount.fromDecimal( + Decimal.parse(tx["amount"].toString()), + fractionDigits: coin.decimals, + ); + final txn = isar_models.Transaction( walletId: walletId, txid: tx["txid"] as String, @@ -4936,19 +5036,18 @@ class FiroWallet extends CoinServiceAPI (DateTime.now().millisecondsSinceEpoch ~/ 1000), type: isar_models.TransactionType.outgoing, subType: isar_models.TransactionSubType.join, - amount: Format.decimalAmountToSatoshis( - Decimal.parse(tx["amount"].toString()), - coin, - ), - fee: Format.decimalAmountToSatoshis( + amount: amount.raw.toInt(), + amountString: amount.toJsonString(), + fee: Amount.fromDecimal( Decimal.parse(tx["fees"].toString()), - coin, - ), + fractionDigits: coin.decimals, + ).raw.toInt(), height: tx["height"] as int?, isCancelled: false, isLelantus: true, slateId: null, otherData: null, + nonce: null, inputs: [], outputs: [], ); @@ -5010,12 +5109,12 @@ class FiroWallet extends CoinServiceAPI } } - Decimal availablePrivateBalance() { - return balancePrivate.getSpendable(); + Amount availablePrivateBalance() { + return balancePrivate.spendable; } - Decimal availablePublicBalance() { - return balance.getSpendable(); + Amount availablePublicBalance() { + return balance.spendable; } Future get chainHeight async { diff --git a/lib/services/coins/litecoin/litecoin_wallet.dart b/lib/services/coins/litecoin/litecoin_wallet.dart index d010da0d1..b3876d3c3 100644 --- a/lib/services/coins/litecoin/litecoin_wallet.dart +++ b/lib/services/coins/litecoin/litecoin_wallet.dart @@ -11,7 +11,7 @@ import 'package:crypto/crypto.dart'; import 'package:decimal/decimal.dart'; import 'package:flutter/foundation.dart'; import 'package:isar/isar.dart'; -import 'package:stackwallet/db/main_db.dart'; +import 'package:stackwallet/db/isar/main_db.dart'; import 'package:stackwallet/electrumx_rpc/cached_electrumx.dart'; import 'package:stackwallet/electrumx_rpc/electrumx.dart'; import 'package:stackwallet/models/balance.dart'; @@ -32,6 +32,7 @@ import 'package:stackwallet/services/mixins/xpubable.dart'; import 'package:stackwallet/services/node_service.dart'; import 'package:stackwallet/services/notifications_api.dart'; import 'package:stackwallet/services/transaction_notification_tracker.dart'; +import 'package:stackwallet/utilities/amount/amount.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/bip32_utils.dart'; import 'package:stackwallet/utilities/constants.dart'; @@ -47,8 +48,14 @@ import 'package:tuple/tuple.dart'; import 'package:uuid/uuid.dart'; const int MINIMUM_CONFIRMATIONS = 1; -const int DUST_LIMIT = 294; -const int DUST_LIMIT_P2PKH = 546; +final Amount DUST_LIMIT = Amount( + rawValue: BigInt.from(294), + fractionDigits: Coin.particl.decimals, +); +final Amount DUST_LIMIT_P2PKH = Amount( + rawValue: BigInt.from(546), + fractionDigits: Coin.particl.decimals, +); const String GENESIS_HASH_MAINNET = "12a765e31ffd4059bada1e25190f6e98c99d9714d334efa41a195a7e7e04bfe2"; @@ -1027,7 +1034,7 @@ class LitecoinWallet extends CoinServiceAPI @override Future> prepareSend({ required String address, - required int satoshiAmount, + required Amount amount, Map? args, }) async { try { @@ -1057,14 +1064,14 @@ class LitecoinWallet extends CoinServiceAPI // check for send all bool isSendAll = false; - if (satoshiAmount == balance.spendable) { + if (amount == balance.spendable) { isSendAll = true; } final bool coinControl = utxos != null; final txData = await coinSelection( - satoshiAmountToSend: satoshiAmount, + satoshiAmountToSend: amount.raw.toInt(), selectedTxFeeRate: rate, recipientAddress: address, isSendAll: isSendAll, @@ -1243,13 +1250,16 @@ class LitecoinWallet extends CoinServiceAPI timestamp: DateTime.now().millisecondsSinceEpoch ~/ 1000, type: isar_models.TransactionType.outgoing, subType: isar_models.TransactionSubType.none, - amount: txData["recipientAmt"] as int, + // precision may be lost here hence the following amountString + amount: (txData["recipientAmt"] as Amount).raw.toInt(), + amountString: (txData["recipientAmt"] as Amount).toJsonString(), fee: txData["fee"] as int, height: null, isCancelled: false, isLelantus: false, otherData: null, slateId: null, + nonce: null, inputs: [], outputs: [], ); @@ -1419,9 +1429,18 @@ class LitecoinWallet extends CoinServiceAPI numberOfBlocksFast: f, numberOfBlocksAverage: m, numberOfBlocksSlow: s, - fast: Format.decimalAmountToSatoshis(fast, coin), - medium: Format.decimalAmountToSatoshis(medium, coin), - slow: Format.decimalAmountToSatoshis(slow, coin), + fast: Amount.fromDecimal( + fast, + fractionDigits: coin.decimals, + ).raw.toInt(), + medium: Amount.fromDecimal( + medium, + fractionDigits: coin.decimals, + ).raw.toInt(), + slow: Amount.fromDecimal( + slow, + fractionDigits: coin.decimals, + ).raw.toInt(), ); Logging.instance.log("fetched fees: $feeObject", level: LogLevel.Info); @@ -2350,8 +2369,11 @@ class LitecoinWallet extends CoinServiceAPI feeRatePerKB: selectedTxFeeRate, ); - final int roughEstimate = - roughFeeEstimate(spendableOutputs.length, 1, selectedTxFeeRate); + final int roughEstimate = roughFeeEstimate( + spendableOutputs.length, + 1, + selectedTxFeeRate, + ).raw.toInt(); if (feeForOneOutput < roughEstimate) { feeForOneOutput = roughEstimate; } @@ -2408,7 +2430,7 @@ class LitecoinWallet extends CoinServiceAPI if (satoshisBeingUsed - satoshiAmountToSend > feeForOneOutput) { if (satoshisBeingUsed - satoshiAmountToSend > - feeForOneOutput + DUST_LIMIT) { + feeForOneOutput + DUST_LIMIT.raw.toInt()) { // Here, we know that theoretically, we may be able to include another output(change) but we first need to // factor in the value of this output in satoshis. int changeOutputSize = @@ -2416,7 +2438,7 @@ class LitecoinWallet extends CoinServiceAPI // We check to see if the user can pay for the new transaction with 2 outputs instead of one. If they can and // the second output's size > DUST_LIMIT satoshis, we perform the mechanics required to properly generate and use a new // change address. - if (changeOutputSize > DUST_LIMIT && + if (changeOutputSize > DUST_LIMIT.raw.toInt() && satoshisBeingUsed - satoshiAmountToSend - changeOutputSize == feeForTwoOutputs) { // generate new change address if current change address has been used @@ -3225,22 +3247,29 @@ class LitecoinWallet extends CoinServiceAPI (isActive) => this.isActive = isActive; @override - Future estimateFeeFor(int satoshiAmount, int feeRate) async { + Future estimateFeeFor(Amount amount, int feeRate) async { final available = balance.spendable; - if (available == satoshiAmount) { - return satoshiAmount - (await sweepAllEstimate(feeRate)); - } else if (satoshiAmount <= 0 || satoshiAmount > available) { + if (available == amount) { + return amount - (await sweepAllEstimate(feeRate)); + } else if (amount <= Amount.zero || amount > available) { return roughFeeEstimate(1, 2, feeRate); } - int runningBalance = 0; + Amount runningBalance = Amount( + rawValue: BigInt.zero, + fractionDigits: coin.decimals, + ); int inputCount = 0; for (final output in (await utxos)) { if (!output.isBlocked) { - runningBalance += output.value; + runningBalance = runningBalance + + Amount( + rawValue: BigInt.from(output.value), + fractionDigits: coin.decimals, + ); inputCount++; - if (runningBalance > satoshiAmount) { + if (runningBalance > amount) { break; } } @@ -3249,31 +3278,35 @@ class LitecoinWallet extends CoinServiceAPI final oneOutPutFee = roughFeeEstimate(inputCount, 1, feeRate); final twoOutPutFee = roughFeeEstimate(inputCount, 2, feeRate); - if (runningBalance - satoshiAmount > oneOutPutFee) { - if (runningBalance - satoshiAmount > oneOutPutFee + DUST_LIMIT) { - final change = runningBalance - satoshiAmount - twoOutPutFee; + if (runningBalance - amount > oneOutPutFee) { + if (runningBalance - amount > oneOutPutFee + DUST_LIMIT) { + final change = runningBalance - amount - twoOutPutFee; if (change > DUST_LIMIT && - runningBalance - satoshiAmount - change == twoOutPutFee) { - return runningBalance - satoshiAmount - change; + runningBalance - amount - change == twoOutPutFee) { + return runningBalance - amount - change; } else { - return runningBalance - satoshiAmount; + return runningBalance - amount; } } else { - return runningBalance - satoshiAmount; + return runningBalance - amount; } - } else if (runningBalance - satoshiAmount == oneOutPutFee) { + } else if (runningBalance - amount == oneOutPutFee) { return oneOutPutFee; } else { return twoOutPutFee; } } - int roughFeeEstimate(int inputCount, int outputCount, int feeRatePerKB) { - return ((42 + (272 * inputCount) + (128 * outputCount)) / 4).ceil() * - (feeRatePerKB / 1000).ceil(); + Amount roughFeeEstimate(int inputCount, int outputCount, int feeRatePerKB) { + return Amount( + rawValue: BigInt.from( + ((42 + (272 * inputCount) + (128 * outputCount)) / 4).ceil() * + (feeRatePerKB / 1000).ceil()), + fractionDigits: coin.decimals, + ); } - Future sweepAllEstimate(int feeRate) async { + Future sweepAllEstimate(int feeRate) async { int available = 0; int inputCount = 0; for (final output in (await utxos)) { @@ -3287,7 +3320,11 @@ class LitecoinWallet extends CoinServiceAPI // transaction will only have 1 output minus the fee final estimatedFee = roughFeeEstimate(inputCount, 1, feeRate); - return available - estimatedFee; + return Amount( + rawValue: BigInt.from(available), + fractionDigits: coin.decimals, + ) - + estimatedFee; } @override diff --git a/lib/services/coins/manager.dart b/lib/services/coins/manager.dart index 375874265..a44cf9d8f 100644 --- a/lib/services/coins/manager.dart +++ b/lib/services/coins/manager.dart @@ -2,7 +2,7 @@ import 'dart:async'; import 'package:event_bus/event_bus.dart'; import 'package:flutter/material.dart'; -import 'package:stackwallet/hive/db.dart'; +import 'package:stackwallet/db/hive/db.dart'; import 'package:stackwallet/models/balance.dart'; import 'package:stackwallet/models/isar/models/isar_models.dart' as isar_models; import 'package:stackwallet/models/models.dart'; @@ -12,6 +12,7 @@ import 'package:stackwallet/services/event_bus/events/global/updated_in_backgrou import 'package:stackwallet/services/event_bus/global_event_bus.dart'; import 'package:stackwallet/services/mixins/coin_control_interface.dart'; import 'package:stackwallet/services/mixins/paynym_wallet_interface.dart'; +import 'package:stackwallet/utilities/amount/amount.dart'; import 'package:stackwallet/services/mixins/xpubable.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:stackwallet/utilities/logger.dart'; @@ -92,13 +93,13 @@ class Manager with ChangeNotifier { Future> prepareSend({ required String address, - required int satoshiAmount, + required Amount amount, Map? args, }) async { try { final txInfo = await _currentWallet.prepareSend( address: address, - satoshiAmount: satoshiAmount, + amount: amount, args: args, ); // notifyListeners(); @@ -215,8 +216,8 @@ class Manager with ChangeNotifier { bool get isConnected => _currentWallet.isConnected; - Future estimateFeeFor(int satoshiAmount, int feeRate) async { - return _currentWallet.estimateFeeFor(satoshiAmount, feeRate); + Future estimateFeeFor(Amount amount, int feeRate) async { + return _currentWallet.estimateFeeFor(amount, feeRate); } Future generateNewAddress() async { @@ -233,6 +234,8 @@ class Manager with ChangeNotifier { bool get hasCoinControlSupport => _currentWallet is CoinControlInterface; + bool get hasTokenSupport => _currentWallet.coin == Coin.ethereum; + bool get hasWhirlpoolSupport => false; int get rescanOnOpenVersion => diff --git a/lib/services/coins/monero/monero_wallet.dart b/lib/services/coins/monero/monero_wallet.dart index d8aaee0c1..87b803255 100644 --- a/lib/services/coins/monero/monero_wallet.dart +++ b/lib/services/coins/monero/monero_wallet.dart @@ -23,8 +23,8 @@ import 'package:flutter_libmonero/monero/monero.dart'; import 'package:flutter_libmonero/view_model/send/output.dart' as monero_output; import 'package:isar/isar.dart'; import 'package:mutex/mutex.dart'; -import 'package:stackwallet/db/main_db.dart'; -import 'package:stackwallet/hive/db.dart'; +import 'package:stackwallet/db/hive/db.dart'; +import 'package:stackwallet/db/isar/main_db.dart'; import 'package:stackwallet/models/balance.dart'; import 'package:stackwallet/models/isar/models/isar_models.dart' as isar_models; import 'package:stackwallet/models/node_model.dart'; @@ -38,12 +38,11 @@ import 'package:stackwallet/services/event_bus/global_event_bus.dart'; import 'package:stackwallet/services/mixins/wallet_cache.dart'; import 'package:stackwallet/services/mixins/wallet_db.dart'; import 'package:stackwallet/services/node_service.dart'; -import 'package:stackwallet/utilities/constants.dart'; +import 'package:stackwallet/utilities/amount/amount.dart'; import 'package:stackwallet/utilities/default_nodes.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:stackwallet/utilities/enums/fee_rate_type_enum.dart'; import 'package:stackwallet/utilities/flutter_secure_storage_interface.dart'; -import 'package:stackwallet/utilities/format.dart'; import 'package:stackwallet/utilities/logger.dart'; import 'package:stackwallet/utilities/prefs.dart'; import 'package:stackwallet/utilities/stack_file_system.dart'; @@ -170,7 +169,7 @@ class MoneroWallet extends CoinServiceAPI with WalletCache, WalletDB { (await _generateAddressForChain(0, 0)).value; @override - Future estimateFeeFor(int satoshiAmount, int feeRate) async { + Future estimateFeeFor(Amount amount, int feeRate) async { MoneroTransactionPriority priority; switch (feeRate) { @@ -192,9 +191,9 @@ class MoneroWallet extends CoinServiceAPI with WalletCache, WalletDB { break; } - final fee = walletBase!.calculateEstimatedFee(priority, satoshiAmount); + final fee = walletBase!.calculateEstimatedFee(priority, amount.raw.toInt()); - return fee; + return Amount(rawValue: BigInt.from(fee), fractionDigits: coin.decimals); } @override @@ -431,7 +430,7 @@ class MoneroWallet extends CoinServiceAPI with WalletCache, WalletDB { @override Future> prepareSend({ required String address, - required int satoshiAmount, + required Amount amount, Map? args, }) async { String toAddress = address; @@ -456,16 +455,13 @@ class MoneroWallet extends CoinServiceAPI with WalletCache, WalletDB { // check for send all bool isSendAll = false; final balance = await _availableBalance; - if (satoshiAmount == balance) { + if (amount == balance) { isSendAll = true; } Logging.instance - .log("$toAddress $satoshiAmount $args", level: LogLevel.Info); - String amountToSend = - Format.satoshisToAmount(satoshiAmount, coin: coin) - .toStringAsFixed(Constants.decimalPlacesForCoin(coin)); - Logging.instance - .log("$satoshiAmount $amountToSend", level: LogLevel.Info); + .log("$toAddress $amount $args", level: LogLevel.Info); + String amountToSend = amount.decimal.toString(); + Logging.instance.log("$amount $amountToSend", level: LogLevel.Info); monero_output.Output output = monero_output.Output(walletBase!); output.address = toAddress; @@ -487,14 +483,16 @@ class MoneroWallet extends CoinServiceAPI with WalletCache, WalletDB { PendingMoneroTransaction pendingMoneroTransaction = await (awaitPendingTransaction!) as PendingMoneroTransaction; - int realfee = Format.decimalAmountToSatoshis( - Decimal.parse(pendingMoneroTransaction.feeFormatted), coin); - debugPrint("fee? $realfee"); + final int realFee = Amount.fromDecimal( + Decimal.parse(pendingMoneroTransaction.feeFormatted), + fractionDigits: coin.decimals, + ).raw.toInt(); + Map txData = { "pendingMoneroTransaction": pendingMoneroTransaction, - "fee": realfee, + "fee": realFee, "addresss": toAddress, - "recipientAmt": satoshiAmount, + "recipientAmt": amount, }; Logging.instance.log("prepare send: $txData", level: LogLevel.Info); @@ -752,25 +750,34 @@ class MoneroWallet extends CoinServiceAPI with WalletCache, WalletDB { coin: coin, total: total, spendable: available, - blockedTotal: 0, + blockedTotal: Amount( + rawValue: BigInt.zero, + fractionDigits: coin.decimals, + ), pendingSpendable: total - available, ); await updateCachedBalance(_balance!); } - Future get _availableBalance async { + Future get _availableBalance async { try { int runningBalance = 0; for (final entry in walletBase!.balance!.entries) { runningBalance += entry.value.unlockedBalance; } - return runningBalance; + return Amount( + rawValue: BigInt.from(runningBalance), + fractionDigits: coin.decimals, + ); } catch (_) { - return 0; + return Amount( + rawValue: BigInt.zero, + fractionDigits: coin.decimals, + ); } } - Future get _totalBalance async { + Future get _totalBalance async { try { final balanceEntries = walletBase?.balance?.entries; if (balanceEntries != null) { @@ -779,7 +786,10 @@ class MoneroWallet extends CoinServiceAPI with WalletCache, WalletDB { bal = bal + element.value.fullBalance; } await _updateCachedBalance(bal); - return bal; + return Amount( + rawValue: BigInt.from(bal), + fractionDigits: coin.decimals, + ); } else { final transactions = walletBase!.transactionHistory!.transactions; int transactionBalance = 0; @@ -792,10 +802,16 @@ class MoneroWallet extends CoinServiceAPI with WalletCache, WalletDB { } await _updateCachedBalance(transactionBalance); - return transactionBalance; + return Amount( + rawValue: BigInt.from(transactionBalance), + fractionDigits: coin.decimals, + ); } } catch (_) { - return _getCachedBalance(); + return Amount( + rawValue: BigInt.from(_getCachedBalance()), + fractionDigits: coin.decimals, + ); } } @@ -926,12 +942,17 @@ class MoneroWallet extends CoinServiceAPI with WalletCache, WalletDB { type: type, subType: isar_models.TransactionSubType.none, amount: tx.value.amount ?? 0, + amountString: Amount( + rawValue: BigInt.from(tx.value.amount ?? 0), + fractionDigits: coin.decimals, + ).toJsonString(), fee: tx.value.fee ?? 0, height: tx.value.height, isCancelled: false, isLelantus: false, slateId: null, otherData: null, + nonce: null, inputs: [], outputs: [], ); diff --git a/lib/services/coins/namecoin/namecoin_wallet.dart b/lib/services/coins/namecoin/namecoin_wallet.dart index ae59d6064..5538ffde5 100644 --- a/lib/services/coins/namecoin/namecoin_wallet.dart +++ b/lib/services/coins/namecoin/namecoin_wallet.dart @@ -11,7 +11,7 @@ import 'package:crypto/crypto.dart'; import 'package:decimal/decimal.dart'; import 'package:flutter/foundation.dart'; import 'package:isar/isar.dart'; -import 'package:stackwallet/db/main_db.dart'; +import 'package:stackwallet/db/isar/main_db.dart'; import 'package:stackwallet/electrumx_rpc/cached_electrumx.dart'; import 'package:stackwallet/electrumx_rpc/electrumx.dart'; import 'package:stackwallet/models/balance.dart'; @@ -32,6 +32,7 @@ import 'package:stackwallet/services/mixins/xpubable.dart'; import 'package:stackwallet/services/node_service.dart'; import 'package:stackwallet/services/notifications_api.dart'; import 'package:stackwallet/services/transaction_notification_tracker.dart'; +import 'package:stackwallet/utilities/amount/amount.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/bip32_utils.dart'; import 'package:stackwallet/utilities/constants.dart'; @@ -48,7 +49,10 @@ import 'package:uuid/uuid.dart'; const int MINIMUM_CONFIRMATIONS = 2; // Find real dust limit -const int DUST_LIMIT = 546; +final Amount DUST_LIMIT = Amount( + rawValue: BigInt.from(546), + fractionDigits: Coin.particl.decimals, +); const String GENESIS_HASH_MAINNET = "000000000062b72c5e2ceb45fbc8587e807c155b0da735e6483dfba2f0a9c770"; @@ -1018,7 +1022,7 @@ class NamecoinWallet extends CoinServiceAPI @override Future> prepareSend({ required String address, - required int satoshiAmount, + required Amount amount, Map? args, }) async { try { @@ -1048,14 +1052,14 @@ class NamecoinWallet extends CoinServiceAPI // check for send all bool isSendAll = false; - if (satoshiAmount == balance.spendable) { + if (amount == balance.spendable) { isSendAll = true; } final bool coinControl = utxos != null; final txData = await coinSelection( - satoshiAmountToSend: satoshiAmount, + satoshiAmountToSend: amount.raw.toInt(), selectedTxFeeRate: rate, recipientAddress: address, isSendAll: isSendAll, @@ -1233,13 +1237,16 @@ class NamecoinWallet extends CoinServiceAPI timestamp: DateTime.now().millisecondsSinceEpoch ~/ 1000, type: isar_models.TransactionType.outgoing, subType: isar_models.TransactionSubType.none, - amount: txData["recipientAmt"] as int, + // precision may be lost here hence the following amountString + amount: (txData["recipientAmt"] as Amount).raw.toInt(), + amountString: (txData["recipientAmt"] as Amount).toJsonString(), fee: txData["fee"] as int, height: null, isCancelled: false, isLelantus: false, otherData: null, slateId: null, + nonce: null, inputs: [], outputs: [], ); @@ -1409,9 +1416,18 @@ class NamecoinWallet extends CoinServiceAPI numberOfBlocksFast: f, numberOfBlocksAverage: m, numberOfBlocksSlow: s, - fast: Format.decimalAmountToSatoshis(fast, coin), - medium: Format.decimalAmountToSatoshis(medium, coin), - slow: Format.decimalAmountToSatoshis(slow, coin), + fast: Amount.fromDecimal( + fast, + fractionDigits: coin.decimals, + ).raw.toInt(), + medium: Amount.fromDecimal( + medium, + fractionDigits: coin.decimals, + ).raw.toInt(), + slow: Amount.fromDecimal( + slow, + fractionDigits: coin.decimals, + ).raw.toInt(), ); Logging.instance.log("fetched fees: $feeObject", level: LogLevel.Info); @@ -2343,8 +2359,11 @@ class NamecoinWallet extends CoinServiceAPI feeRatePerKB: selectedTxFeeRate, ); - final int roughEstimate = - roughFeeEstimate(spendableOutputs.length, 1, selectedTxFeeRate); + final int roughEstimate = roughFeeEstimate( + spendableOutputs.length, + 1, + selectedTxFeeRate, + ).raw.toInt(); if (feeForOneOutput < roughEstimate) { feeForOneOutput = roughEstimate; } @@ -2401,7 +2420,7 @@ class NamecoinWallet extends CoinServiceAPI if (satoshisBeingUsed - satoshiAmountToSend > feeForOneOutput) { if (satoshisBeingUsed - satoshiAmountToSend > - feeForOneOutput + DUST_LIMIT) { + feeForOneOutput + DUST_LIMIT.raw.toInt()) { // Here, we know that theoretically, we may be able to include another output(change) but we first need to // factor in the value of this output in satoshis. int changeOutputSize = @@ -2409,7 +2428,7 @@ class NamecoinWallet extends CoinServiceAPI // We check to see if the user can pay for the new transaction with 2 outputs instead of one. If they can and // the second output's size > DUST_LIMIT satoshis, we perform the mechanics required to properly generate and use a new // change address. - if (changeOutputSize > DUST_LIMIT && + if (changeOutputSize > DUST_LIMIT.raw.toInt() && satoshisBeingUsed - satoshiAmountToSend - changeOutputSize == feeForTwoOutputs) { // generate new change address if current change address has been used @@ -3217,22 +3236,29 @@ class NamecoinWallet extends CoinServiceAPI (isActive) => this.isActive = isActive; @override - Future estimateFeeFor(int satoshiAmount, int feeRate) async { + Future estimateFeeFor(Amount amount, int feeRate) async { final available = balance.spendable; - if (available == satoshiAmount) { - return satoshiAmount - (await sweepAllEstimate(feeRate)); - } else if (satoshiAmount <= 0 || satoshiAmount > available) { + if (available == amount) { + return amount - (await sweepAllEstimate(feeRate)); + } else if (amount <= Amount.zero || amount > available) { return roughFeeEstimate(1, 2, feeRate); } - int runningBalance = 0; + Amount runningBalance = Amount( + rawValue: BigInt.zero, + fractionDigits: coin.decimals, + ); int inputCount = 0; for (final output in (await utxos)) { if (!output.isBlocked) { - runningBalance += output.value; + runningBalance = runningBalance + + Amount( + rawValue: BigInt.from(output.value), + fractionDigits: coin.decimals, + ); inputCount++; - if (runningBalance > satoshiAmount) { + if (runningBalance > amount) { break; } } @@ -3241,19 +3267,19 @@ class NamecoinWallet extends CoinServiceAPI final oneOutPutFee = roughFeeEstimate(inputCount, 1, feeRate); final twoOutPutFee = roughFeeEstimate(inputCount, 2, feeRate); - if (runningBalance - satoshiAmount > oneOutPutFee) { - if (runningBalance - satoshiAmount > oneOutPutFee + DUST_LIMIT) { - final change = runningBalance - satoshiAmount - twoOutPutFee; + if (runningBalance - amount > oneOutPutFee) { + if (runningBalance - amount > oneOutPutFee + DUST_LIMIT) { + final change = runningBalance - amount - twoOutPutFee; if (change > DUST_LIMIT && - runningBalance - satoshiAmount - change == twoOutPutFee) { - return runningBalance - satoshiAmount - change; + runningBalance - amount - change == twoOutPutFee) { + return runningBalance - amount - change; } else { - return runningBalance - satoshiAmount; + return runningBalance - amount; } } else { - return runningBalance - satoshiAmount; + return runningBalance - amount; } - } else if (runningBalance - satoshiAmount == oneOutPutFee) { + } else if (runningBalance - amount == oneOutPutFee) { return oneOutPutFee; } else { return twoOutPutFee; @@ -3261,12 +3287,16 @@ class NamecoinWallet extends CoinServiceAPI } // TODO: Check if this is the correct formula for namecoin - int roughFeeEstimate(int inputCount, int outputCount, int feeRatePerKB) { - return ((42 + (272 * inputCount) + (128 * outputCount)) / 4).ceil() * - (feeRatePerKB / 1000).ceil(); + Amount roughFeeEstimate(int inputCount, int outputCount, int feeRatePerKB) { + return Amount( + rawValue: BigInt.from( + ((42 + (272 * inputCount) + (128 * outputCount)) / 4).ceil() * + (feeRatePerKB / 1000).ceil()), + fractionDigits: coin.decimals, + ); } - Future sweepAllEstimate(int feeRate) async { + Future sweepAllEstimate(int feeRate) async { int available = 0; int inputCount = 0; for (final output in (await utxos)) { @@ -3280,7 +3310,11 @@ class NamecoinWallet extends CoinServiceAPI // transaction will only have 1 output minus the fee final estimatedFee = roughFeeEstimate(inputCount, 1, feeRate); - return available - estimatedFee; + return Amount( + rawValue: BigInt.from(available), + fractionDigits: coin.decimals, + ) - + estimatedFee; } @override diff --git a/lib/services/coins/particl/particl_wallet.dart b/lib/services/coins/particl/particl_wallet.dart index 739e39068..1c0a231e1 100644 --- a/lib/services/coins/particl/particl_wallet.dart +++ b/lib/services/coins/particl/particl_wallet.dart @@ -11,7 +11,7 @@ import 'package:crypto/crypto.dart'; import 'package:decimal/decimal.dart'; import 'package:flutter/foundation.dart'; import 'package:isar/isar.dart'; -import 'package:stackwallet/db/main_db.dart'; +import 'package:stackwallet/db/isar/main_db.dart'; import 'package:stackwallet/electrumx_rpc/cached_electrumx.dart'; import 'package:stackwallet/electrumx_rpc/electrumx.dart'; import 'package:stackwallet/models/balance.dart'; @@ -31,6 +31,7 @@ import 'package:stackwallet/services/mixins/xpubable.dart'; import 'package:stackwallet/services/node_service.dart'; import 'package:stackwallet/services/notifications_api.dart'; import 'package:stackwallet/services/transaction_notification_tracker.dart'; +import 'package:stackwallet/utilities/amount/amount.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/bip32_utils.dart'; import 'package:stackwallet/utilities/constants.dart'; @@ -46,7 +47,10 @@ import 'package:tuple/tuple.dart'; import 'package:uuid/uuid.dart'; const int MINIMUM_CONFIRMATIONS = 1; -const int DUST_LIMIT = 294; +final Amount DUST_LIMIT = Amount( + rawValue: BigInt.from(294), + fractionDigits: Coin.particl.decimals, +); const String GENESIS_HASH_MAINNET = "0000ee0784c195317ac95623e22fddb8c7b8825dc3998e0bb924d66866eccf4c"; @@ -945,7 +949,7 @@ class ParticlWallet extends CoinServiceAPI @override Future> prepareSend({ required String address, - required int satoshiAmount, + required Amount amount, Map? args, }) async { try { @@ -975,14 +979,14 @@ class ParticlWallet extends CoinServiceAPI // check for send all bool isSendAll = false; - if (satoshiAmount == balance.spendable) { + if (amount == balance.spendable) { isSendAll = true; } final bool coinControl = utxos != null; final txData = await coinSelection( - satoshiAmountToSend: satoshiAmount, + satoshiAmountToSend: amount.raw.toInt(), selectedTxFeeRate: rate, recipientAddress: address, isSendAll: isSendAll, @@ -1161,13 +1165,16 @@ class ParticlWallet extends CoinServiceAPI timestamp: DateTime.now().millisecondsSinceEpoch ~/ 1000, type: isar_models.TransactionType.outgoing, subType: isar_models.TransactionSubType.none, - amount: txData["recipientAmt"] as int, + // precision may be lost here hence the following amountString + amount: (txData["recipientAmt"] as Amount).raw.toInt(), + amountString: (txData["recipientAmt"] as Amount).toJsonString(), fee: txData["fee"] as int, height: null, isCancelled: false, isLelantus: false, otherData: null, slateId: null, + nonce: null, inputs: [], outputs: [], ); @@ -1324,9 +1331,18 @@ class ParticlWallet extends CoinServiceAPI numberOfBlocksFast: f, numberOfBlocksAverage: m, numberOfBlocksSlow: s, - fast: Format.decimalAmountToSatoshis(fast, coin), - medium: Format.decimalAmountToSatoshis(medium, coin), - slow: Format.decimalAmountToSatoshis(slow, coin), + fast: Amount.fromDecimal( + fast, + fractionDigits: coin.decimals, + ).raw.toInt(), + medium: Amount.fromDecimal( + medium, + fractionDigits: coin.decimals, + ).raw.toInt(), + slow: Amount.fromDecimal( + slow, + fractionDigits: coin.decimals, + ).raw.toInt(), ); Logging.instance.log("fetched fees: $feeObject", level: LogLevel.Info); @@ -2338,10 +2354,10 @@ class ParticlWallet extends CoinServiceAPI json["scriptPubKey"]?["addresses"]?[0] as String? ?? json['scriptPubKey']?['type'] as String? ?? "", - value: Format.decimalAmountToSatoshis( + value: Amount.fromDecimal( Decimal.parse((json["value"] ?? 0).toString()), - coin, - ), + fractionDigits: coin.decimals, + ).raw.toInt(), ); outputs.add(output); } @@ -2353,12 +2369,17 @@ class ParticlWallet extends CoinServiceAPI type: type, subType: isar_models.TransactionSubType.none, amount: amount, + amountString: Amount( + rawValue: BigInt.from(amount), + fractionDigits: coin.decimals, + ).toJsonString(), fee: fee, height: txObject["height"] as int, inputs: inputs, outputs: outputs, isCancelled: false, isLelantus: false, + nonce: null, slateId: null, otherData: null, ); @@ -2504,8 +2525,11 @@ class ParticlWallet extends CoinServiceAPI feeRatePerKB: selectedTxFeeRate, ); - final int roughEstimate = - roughFeeEstimate(spendableOutputs.length, 1, selectedTxFeeRate); + final int roughEstimate = roughFeeEstimate( + spendableOutputs.length, + 1, + selectedTxFeeRate, + ).raw.toInt(); if (feeForOneOutput < roughEstimate) { feeForOneOutput = roughEstimate; } @@ -2562,7 +2586,7 @@ class ParticlWallet extends CoinServiceAPI if (satoshisBeingUsed - satoshiAmountToSend > feeForOneOutput) { if (satoshisBeingUsed - satoshiAmountToSend > - feeForOneOutput + DUST_LIMIT) { + feeForOneOutput + DUST_LIMIT.raw.toInt()) { // Here, we know that theoretically, we may be able to include another output(change) but we first need to // factor in the value of this output in satoshis. int changeOutputSize = @@ -2570,7 +2594,7 @@ class ParticlWallet extends CoinServiceAPI // We check to see if the user can pay for the new transaction with 2 outputs instead of one. If they can and // the second output's size > DUST_LIMIT satoshis, we perform the mechanics required to properly generate and use a new // change address. - if (changeOutputSize > DUST_LIMIT && + if (changeOutputSize > DUST_LIMIT.raw.toInt() && satoshisBeingUsed - satoshiAmountToSend - changeOutputSize == feeForTwoOutputs) { // generate new change address if current change address has been used @@ -3271,22 +3295,28 @@ class ParticlWallet extends CoinServiceAPI (isActive) => this.isActive = isActive; @override - Future estimateFeeFor(int satoshiAmount, int feeRate) async { + Future estimateFeeFor(Amount amount, int feeRate) async { final available = balance.spendable; - if (available == satoshiAmount) { - return satoshiAmount - (await sweepAllEstimate(feeRate)); - } else if (satoshiAmount <= 0 || satoshiAmount > available) { + if (available == amount) { + return amount - (await sweepAllEstimate(feeRate)); + } else if (amount <= Amount.zero || amount > available) { return roughFeeEstimate(1, 2, feeRate); } - int runningBalance = 0; + Amount runningBalance = Amount( + rawValue: BigInt.zero, + fractionDigits: coin.decimals, + ); int inputCount = 0; for (final output in (await utxos)) { if (!output.isBlocked) { - runningBalance += output.value; + runningBalance += Amount( + rawValue: BigInt.from(output.value), + fractionDigits: coin.decimals, + ); inputCount++; - if (runningBalance > satoshiAmount) { + if (runningBalance > amount) { break; } } @@ -3295,31 +3325,35 @@ class ParticlWallet extends CoinServiceAPI final oneOutPutFee = roughFeeEstimate(inputCount, 1, feeRate); final twoOutPutFee = roughFeeEstimate(inputCount, 2, feeRate); - if (runningBalance - satoshiAmount > oneOutPutFee) { - if (runningBalance - satoshiAmount > oneOutPutFee + DUST_LIMIT) { - final change = runningBalance - satoshiAmount - twoOutPutFee; + if (runningBalance - amount > oneOutPutFee) { + if (runningBalance - amount > oneOutPutFee + DUST_LIMIT) { + final change = runningBalance - amount - twoOutPutFee; if (change > DUST_LIMIT && - runningBalance - satoshiAmount - change == twoOutPutFee) { - return runningBalance - satoshiAmount - change; + runningBalance - amount - change == twoOutPutFee) { + return runningBalance - amount - change; } else { - return runningBalance - satoshiAmount; + return runningBalance - amount; } } else { - return runningBalance - satoshiAmount; + return runningBalance - amount; } - } else if (runningBalance - satoshiAmount == oneOutPutFee) { + } else if (runningBalance - amount == oneOutPutFee) { return oneOutPutFee; } else { return twoOutPutFee; } } - int roughFeeEstimate(int inputCount, int outputCount, int feeRatePerKB) { - return ((42 + (272 * inputCount) + (128 * outputCount)) / 4).ceil() * - (feeRatePerKB / 1000).ceil(); + Amount roughFeeEstimate(int inputCount, int outputCount, int feeRatePerKB) { + return Amount( + rawValue: BigInt.from( + ((42 + (272 * inputCount) + (128 * outputCount)) / 4).ceil() * + (feeRatePerKB / 1000).ceil()), + fractionDigits: coin.decimals, + ); } - Future sweepAllEstimate(int feeRate) async { + Future sweepAllEstimate(int feeRate) async { int available = 0; int inputCount = 0; for (final output in (await utxos)) { @@ -3333,7 +3367,11 @@ class ParticlWallet extends CoinServiceAPI // transaction will only have 1 output minus the fee final estimatedFee = roughFeeEstimate(inputCount, 1, feeRate); - return available - estimatedFee; + return Amount( + rawValue: BigInt.from(available), + fractionDigits: coin.decimals, + ) - + estimatedFee; } @override diff --git a/lib/services/coins/wownero/wownero_wallet.dart b/lib/services/coins/wownero/wownero_wallet.dart index c10a37728..84f806be2 100644 --- a/lib/services/coins/wownero/wownero_wallet.dart +++ b/lib/services/coins/wownero/wownero_wallet.dart @@ -25,8 +25,8 @@ import 'package:flutter_libmonero/view_model/send/output.dart' import 'package:flutter_libmonero/wownero/wownero.dart'; import 'package:isar/isar.dart'; import 'package:mutex/mutex.dart'; -import 'package:stackwallet/db/main_db.dart'; -import 'package:stackwallet/hive/db.dart'; +import 'package:stackwallet/db/hive/db.dart'; +import 'package:stackwallet/db/isar/main_db.dart'; import 'package:stackwallet/models/balance.dart'; import 'package:stackwallet/models/isar/models/isar_models.dart' as isar_models; import 'package:stackwallet/models/node_model.dart'; @@ -40,12 +40,11 @@ import 'package:stackwallet/services/event_bus/global_event_bus.dart'; import 'package:stackwallet/services/mixins/wallet_cache.dart'; import 'package:stackwallet/services/mixins/wallet_db.dart'; import 'package:stackwallet/services/node_service.dart'; -import 'package:stackwallet/utilities/constants.dart'; +import 'package:stackwallet/utilities/amount/amount.dart'; import 'package:stackwallet/utilities/default_nodes.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:stackwallet/utilities/enums/fee_rate_type_enum.dart'; import 'package:stackwallet/utilities/flutter_secure_storage_interface.dart'; -import 'package:stackwallet/utilities/format.dart'; import 'package:stackwallet/utilities/logger.dart'; import 'package:stackwallet/utilities/prefs.dart'; import 'package:stackwallet/utilities/stack_file_system.dart'; @@ -172,7 +171,7 @@ class WowneroWallet extends CoinServiceAPI with WalletCache, WalletDB { (await _generateAddressForChain(0, 0)).value; @override - Future estimateFeeFor(int satoshiAmount, int feeRate) async { + Future estimateFeeFor(Amount amount, int feeRate) async { MoneroTransactionPriority priority; FeeRateType feeRateType = FeeRateType.slow; switch (feeRate) { @@ -204,20 +203,29 @@ class WowneroWallet extends CoinServiceAPI with WalletCache, WalletDB { try { aprox = (await prepareSend( // This address is only used for getting an approximate fee, never for sending - address: - "WW3iVcnoAY6K9zNdU4qmdvZELefx6xZz4PMpTwUifRkvMQckyadhSPYMVPJhBdYE8P9c27fg9RPmVaWNFx1cDaj61HnetqBiy", - satoshiAmount: satoshiAmount, + address: "WW3iVcnoAY6K9zNdU4qmdvZELefx6xZz4PMpTwUifRkvMQckyadhSPYMVPJhBdYE8P9c27fg9RPmVaWNFx1cDaj61HnetqBiy", + amount: amount, args: {"feeRate": feeRateType}))['fee']; - await Future.delayed(const Duration(milliseconds: 500)); + await Future.delayed(const Duration(milliseconds: 500)); } catch (e, s) { - aprox = walletBase!.calculateEstimatedFee(priority, satoshiAmount); + aprox = walletBase!.calculateEstimatedFee( + priority, + amount.raw.toInt(), + ); } } }); - print("this is the aprox fee $aprox for $satoshiAmount"); - final fee = (aprox as int); - return fee; + print("this is the aprox fee $aprox for $amount"); + + if (aprox is Amount) { + return aprox as Amount; + } else { + return Amount( + rawValue: BigInt.from(aprox as int), + fractionDigits: coin.decimals, + ); + } } @override @@ -451,7 +459,7 @@ class WowneroWallet extends CoinServiceAPI with WalletCache, WalletDB { @override Future> prepareSend({ required String address, - required int satoshiAmount, + required Amount amount, Map? args, }) async { try { @@ -475,16 +483,12 @@ class WowneroWallet extends CoinServiceAPI with WalletCache, WalletDB { // check for send all bool isSendAll = false; final balance = await _availableBalance; - if (satoshiAmount == balance) { + if (amount == balance) { isSendAll = true; } - Logging.instance - .log("$address $satoshiAmount $args", level: LogLevel.Info); - String amountToSend = - Format.satoshisToAmount(satoshiAmount, coin: coin) - .toStringAsFixed(Constants.decimalPlacesForCoin(coin)); - Logging.instance - .log("$satoshiAmount $amountToSend", level: LogLevel.Info); + Logging.instance.log("$address $amount $args", level: LogLevel.Info); + String amountToSend = amount.decimal.toString(); + Logging.instance.log("$amount $amountToSend", level: LogLevel.Info); wownero_output.Output output = wownero_output.Output(walletBase!); output.address = address; @@ -507,15 +511,16 @@ class WowneroWallet extends CoinServiceAPI with WalletCache, WalletDB { PendingWowneroTransaction pendingWowneroTransaction = await (awaitPendingTransaction!) as PendingWowneroTransaction; - int realfee = Format.decimalAmountToSatoshis( - Decimal.parse(pendingWowneroTransaction.feeFormatted), coin); - //todo: check if print needed - // debugPrint("fee? $realfee"); + final int realFee = Amount.fromDecimal( + Decimal.parse(pendingWowneroTransaction.feeFormatted), + fractionDigits: coin.decimals, + ).raw.toInt(); + Map txData = { "pendingWowneroTransaction": pendingWowneroTransaction, - "fee": realfee, + "fee": realFee, "addresss": address, - "recipientAmt": satoshiAmount, + "recipientAmt": amount, }; Logging.instance.log("prepare send: $txData", level: LogLevel.Info); @@ -772,25 +777,34 @@ class WowneroWallet extends CoinServiceAPI with WalletCache, WalletDB { coin: coin, total: total, spendable: available, - blockedTotal: 0, + blockedTotal: Amount( + rawValue: BigInt.zero, + fractionDigits: coin.decimals, + ), pendingSpendable: total - available, ); await updateCachedBalance(_balance!); } - Future get _availableBalance async { + Future get _availableBalance async { try { int runningBalance = 0; for (final entry in walletBase!.balance!.entries) { runningBalance += entry.value.unlockedBalance; } - return runningBalance; + return Amount( + rawValue: BigInt.from(runningBalance), + fractionDigits: coin.decimals, + ); } catch (_) { - return 0; + return Amount( + rawValue: BigInt.zero, + fractionDigits: coin.decimals, + ); } } - Future get _totalBalance async { + Future get _totalBalance async { try { final balanceEntries = walletBase?.balance?.entries; if (balanceEntries != null) { @@ -799,7 +813,10 @@ class WowneroWallet extends CoinServiceAPI with WalletCache, WalletDB { bal = bal + element.value.fullBalance; } await _updateCachedBalance(bal); - return bal; + return Amount( + rawValue: BigInt.from(bal), + fractionDigits: coin.decimals, + ); } else { final transactions = walletBase!.transactionHistory!.transactions; int transactionBalance = 0; @@ -812,10 +829,16 @@ class WowneroWallet extends CoinServiceAPI with WalletCache, WalletDB { } await _updateCachedBalance(transactionBalance); - return transactionBalance; + return Amount( + rawValue: BigInt.from(transactionBalance), + fractionDigits: coin.decimals, + ); } } catch (_) { - return _getCachedBalance(); + return Amount( + rawValue: BigInt.from(_getCachedBalance()), + fractionDigits: coin.decimals, + ); } } @@ -1006,12 +1029,17 @@ class WowneroWallet extends CoinServiceAPI with WalletCache, WalletDB { type: type, subType: isar_models.TransactionSubType.none, amount: tx.value.amount ?? 0, + amountString: Amount( + rawValue: BigInt.from(tx.value.amount ?? 0), + fractionDigits: coin.decimals, + ).toJsonString(), fee: tx.value.fee ?? 0, height: tx.value.height, isCancelled: false, isLelantus: false, slateId: null, otherData: null, + nonce: null, inputs: [], outputs: [], ); diff --git a/lib/services/ethereum/cached_eth_token_balance.dart b/lib/services/ethereum/cached_eth_token_balance.dart new file mode 100644 index 000000000..f36cf2ea3 --- /dev/null +++ b/lib/services/ethereum/cached_eth_token_balance.dart @@ -0,0 +1,45 @@ +import 'package:stackwallet/models/isar/models/ethereum/eth_contract.dart'; +import 'package:stackwallet/models/token_balance.dart'; +import 'package:stackwallet/services/ethereum/ethereum_api.dart'; +import 'package:stackwallet/services/mixins/eth_token_cache.dart'; +import 'package:stackwallet/utilities/amount/amount.dart'; +import 'package:stackwallet/utilities/logger.dart'; + +class CachedEthTokenBalance with EthTokenCache { + final String walletId; + final EthContract token; + + CachedEthTokenBalance(this.walletId, this.token) { + initCache(walletId, token); + } + + Future fetchAndUpdateCachedBalance(String address) async { + final response = await EthereumAPI.getWalletTokenBalance( + address: address, + contractAddress: token.address, + ); + + if (response.value != null) { + await updateCachedBalance( + TokenBalance( + contractAddress: token.address, + total: response.value!, + spendable: response.value!, + blockedTotal: Amount( + rawValue: BigInt.zero, + fractionDigits: token.decimals, + ), + pendingSpendable: Amount( + rawValue: BigInt.zero, + fractionDigits: token.decimals, + ), + ), + ); + } else { + Logging.instance.log( + "CachedEthTokenBalance.fetchAndUpdateCachedBalance failed: ${response.exception}", + level: LogLevel.Warning, + ); + } + } +} diff --git a/lib/services/ethereum/ethereum_api.dart b/lib/services/ethereum/ethereum_api.dart new file mode 100644 index 000000000..d31bcfd40 --- /dev/null +++ b/lib/services/ethereum/ethereum_api.dart @@ -0,0 +1,698 @@ +import 'dart:convert'; + +import 'package:decimal/decimal.dart'; +import 'package:http/http.dart'; +import 'package:stackwallet/dto/ethereum/eth_token_tx_dto.dart'; +import 'package:stackwallet/dto/ethereum/eth_token_tx_extra_dto.dart'; +import 'package:stackwallet/dto/ethereum/eth_tx_dto.dart'; +import 'package:stackwallet/dto/ethereum/pending_eth_tx_dto.dart'; +import 'package:stackwallet/models/isar/models/ethereum/eth_contract.dart'; +import 'package:stackwallet/models/paymint/fee_object_model.dart'; +import 'package:stackwallet/utilities/amount/amount.dart'; +import 'package:stackwallet/utilities/default_nodes.dart'; +import 'package:stackwallet/utilities/eth_commons.dart'; +import 'package:stackwallet/utilities/extensions/extensions.dart'; +import 'package:stackwallet/utilities/logger.dart'; +import 'package:tuple/tuple.dart'; + +class EthApiException with Exception { + EthApiException(this.message); + + final String message; + + @override + String toString() => "$runtimeType: $message"; +} + +class EthereumResponse { + EthereumResponse(this.value, this.exception); + + final T? value; + final EthApiException? exception; + + @override + toString() => "EthereumResponse: { value: $value, exception: $exception }"; +} + +abstract class EthereumAPI { + static String get stackBaseServer => DefaultNodes.ethereum.host; + + static Future>> getEthTransactions( + String address) async { + try { + final response = await get( + Uri.parse( + "$stackBaseServer/export?addrs=$address", + ), + ); + + if (response.statusCode == 200) { + if (response.body.isNotEmpty) { + final json = jsonDecode(response.body) as Map; + final list = json["data"] as List?; + + final List txns = []; + for (final map in list!) { + final txn = EthTxDTO.fromMap(Map.from(map as Map)); + + if (txn.hasToken == 0) { + txns.add(txn); + } + } + return EthereumResponse( + txns, + null, + ); + } else { + throw EthApiException( + "getEthTransactions($address) response is empty but status code is " + "${response.statusCode}", + ); + } + } else { + throw EthApiException( + "getEthTransactions($address) failed with status code: " + "${response.statusCode}", + ); + } + } on EthApiException catch (e) { + return EthereumResponse( + null, + e, + ); + } catch (e, s) { + Logging.instance.log( + "getEthTransactions($address): $e\n$s", + level: LogLevel.Error, + ); + return EthereumResponse( + null, + EthApiException(e.toString()), + ); + } + } + + static Future> getEthTransactionByHash( + String txid) async { + try { + final response = await post( + Uri.parse( + "$stackBaseServer/v1/mainnet", + ), + headers: {'Content-Type': 'application/json'}, + body: json.encode({ + "jsonrpc": "2.0", + "method": "eth_getTransactionByHash", + "params": [ + txid, + ], + "id": DateTime.now().millisecondsSinceEpoch, + }), + ); + + if (response.statusCode == 200) { + if (response.body.isNotEmpty) { + try { + final json = jsonDecode(response.body) as Map; + final result = json["result"] as Map; + return EthereumResponse( + PendingEthTxDto.fromMap(Map.from(result)), + null, + ); + } catch (_) { + throw EthApiException( + "getEthTransactionByHash($txid) failed with response: " + "${response.body}", + ); + } + } else { + throw EthApiException( + "getEthTransactionByHash($txid) response is empty but status code is " + "${response.statusCode}", + ); + } + } else { + throw EthApiException( + "getEthTransactionByHash($txid) failed with status code: " + "${response.statusCode}", + ); + } + } on EthApiException catch (e) { + return EthereumResponse( + null, + e, + ); + } catch (e, s) { + Logging.instance.log( + "getEthTransactionByHash($txid): $e\n$s", + level: LogLevel.Error, + ); + return EthereumResponse( + null, + EthApiException(e.toString()), + ); + } + } + + static Future>>> + getEthTransactionNonces( + List txns, + ) async { + try { + final response = await get( + Uri.parse( + "$stackBaseServer/transactions?transactions=${txns.map((e) => e.hash).join(" ")}&raw=true", + ), + ); + + if (response.statusCode == 200) { + if (response.body.isNotEmpty) { + final json = jsonDecode(response.body) as Map; + final list = List>.from(json["data"] as List); + + final List> result = []; + + for (final dto in txns) { + final data = + list.firstWhere((e) => e["hash"] == dto.hash, orElse: () => {}); + + final nonce = (data["nonce"] as String?)?.toBigIntFromHex.toInt(); + result.add(Tuple2(dto, nonce)); + } + return EthereumResponse( + result, + null, + ); + } else { + throw EthApiException( + "getEthTransactionNonces($txns) response is empty but status code is " + "${response.statusCode}", + ); + } + } else { + throw EthApiException( + "getEthTransactionNonces($txns) failed with status code: " + "${response.statusCode}", + ); + } + } on EthApiException catch (e) { + return EthereumResponse( + null, + e, + ); + } catch (e, s) { + Logging.instance.log( + "getEthTransactionNonces($txns): $e\n$s", + level: LogLevel.Error, + ); + return EthereumResponse( + null, + EthApiException(e.toString()), + ); + } + } + + static Future>> + getEthTokenTransactionsByTxids(List txids) async { + try { + final response = await get( + Uri.parse( + "$stackBaseServer/transactions?transactions=${txids.join(" ")}", + ), + ); + + if (response.statusCode == 200) { + if (response.body.isNotEmpty) { + final json = jsonDecode(response.body) as Map; + final list = json["data"] as List?; + + final List txns = []; + for (final map in list!) { + final txn = EthTokenTxExtraDTO.fromMap( + Map.from(map as Map), + ); + + txns.add(txn); + } + return EthereumResponse( + txns, + null, + ); + } else { + throw EthApiException( + "getEthTransaction($txids) response is empty but status code is " + "${response.statusCode}", + ); + } + } else { + throw EthApiException( + "getEthTransaction($txids) failed with status code: " + "${response.statusCode}", + ); + } + } on EthApiException catch (e) { + return EthereumResponse( + null, + e, + ); + } catch (e, s) { + Logging.instance.log( + "getEthTransaction($txids): $e\n$s", + level: LogLevel.Error, + ); + return EthereumResponse( + null, + EthApiException(e.toString()), + ); + } + } + + static Future>> getTokenTransactions({ + required String address, + required String tokenContractAddress, + }) async { + try { + final response = await get( + Uri.parse( + "$stackBaseServer/export?addrs=$address&emitter=$tokenContractAddress&logs=true", + ), + ); + + if (response.statusCode == 200) { + if (response.body.isNotEmpty) { + final json = jsonDecode(response.body) as Map; + final list = json["data"] as List?; + + final List txns = []; + for (final map in list!) { + final txn = + EthTokenTxDto.fromMap(Map.from(map as Map)); + + txns.add(txn); + } + return EthereumResponse( + txns, + null, + ); + } else { + throw EthApiException( + "getTokenTransactions($address, $tokenContractAddress) response is empty but status code is " + "${response.statusCode}", + ); + } + } else { + throw EthApiException( + "getTokenTransactions($address, $tokenContractAddress) failed with status code: " + "${response.statusCode}", + ); + } + } on EthApiException catch (e) { + return EthereumResponse( + null, + e, + ); + } catch (e, s) { + Logging.instance.log( + "getTokenTransactions($address, $tokenContractAddress): $e\n$s", + level: LogLevel.Error, + ); + return EthereumResponse( + null, + EthApiException(e.toString()), + ); + } + } + +// ONLY FETCHES WALLET TOKENS WITH A NON ZERO BALANCE + // static Future>> getWalletTokens({ + // required String address, + // }) async { + // try { + // final uri = Uri.parse( + // "$blockExplorer?module=account&action=tokenlist&address=$address", + // ); + // final response = await get(uri); + // + // if (response.statusCode == 200) { + // final json = jsonDecode(response.body); + // if (json["message"] == "OK") { + // final result = + // List>.from(json["result"] as List); + // final List tokens = []; + // for (final map in result) { + // if (map["type"] == "ERC-20") { + // tokens.add( + // Erc20Token( + // balance: int.parse(map["balance"] as String), + // contractAddress: map["contractAddress"] as String, + // decimals: int.parse(map["decimals"] as String), + // name: map["name"] as String, + // symbol: map["symbol"] as String, + // ), + // ); + // } else if (map["type"] == "ERC-721") { + // tokens.add( + // Erc721Token( + // balance: int.parse(map["balance"] as String), + // contractAddress: map["contractAddress"] as String, + // decimals: int.parse(map["decimals"] as String), + // name: map["name"] as String, + // symbol: map["symbol"] as String, + // ), + // ); + // } else { + // throw EthApiException( + // "Unsupported token type found: ${map["type"]}"); + // } + // } + // + // return EthereumResponse( + // tokens, + // null, + // ); + // } else { + // throw EthApiException(json["message"] as String); + // } + // } else { + // throw EthApiException( + // "getWalletTokens($address) failed with status code: " + // "${response.statusCode}", + // ); + // } + // } on EthApiException catch (e) { + // return EthereumResponse( + // null, + // e, + // ); + // } catch (e, s) { + // Logging.instance.log( + // "getWalletTokens(): $e\n$s", + // level: LogLevel.Error, + // ); + // return EthereumResponse( + // null, + // EthApiException(e.toString()), + // ); + // } + // } + + static Future> getWalletTokenBalance({ + required String address, + required String contractAddress, + }) async { + try { + final uri = Uri.parse( + "$stackBaseServer/tokens?addrs=$contractAddress $address", + ); + final response = await get(uri); + + if (response.statusCode == 200) { + final json = jsonDecode(response.body); + if (json["data"] is List) { + final map = json["data"].first as Map; + + final balance = + Decimal.tryParse(map["balance"].toString()) ?? Decimal.zero; + + return EthereumResponse( + Amount.fromDecimal(balance, fractionDigits: map["decimals"] as int), + null, + ); + } else { + throw EthApiException(json["message"] as String); + } + } else { + throw EthApiException( + "getWalletTokenBalance($address) failed with status code: " + "${response.statusCode}", + ); + } + } on EthApiException catch (e) { + return EthereumResponse( + null, + e, + ); + } catch (e, s) { + Logging.instance.log( + "getWalletTokenBalance(): $e\n$s", + level: LogLevel.Error, + ); + return EthereumResponse( + null, + EthApiException(e.toString()), + ); + } + } + + static Future> getAddressNonce({ + required String address, + }) async { + try { + final uri = Uri.parse( + "$stackBaseServer/state?addrs=$address&parts=nonce", + ); + final response = await get(uri); + + if (response.statusCode == 200) { + final json = jsonDecode(response.body); + if (json["data"] is List) { + final map = json["data"].first as Map; + + final nonce = map["nonce"] as int; + + return EthereumResponse( + nonce, + null, + ); + } else { + throw EthApiException(json["message"] as String); + } + } else { + throw EthApiException( + "getWalletTokenBalance($address) failed with status code: " + "${response.statusCode}", + ); + } + } on EthApiException catch (e) { + return EthereumResponse( + null, + e, + ); + } catch (e, s) { + Logging.instance.log( + "getWalletTokenBalance(): $e\n$s", + level: LogLevel.Error, + ); + return EthereumResponse( + null, + EthApiException(e.toString()), + ); + } + } + + static Future> getGasOracle() async { + try { + final response = await get( + Uri.parse( + "$stackBaseServer/gas-prices", + ), + ); + + if (response.statusCode == 200) { + final json = jsonDecode(response.body) as Map; + if (json["success"] == true) { + return EthereumResponse( + GasTracker.fromJson( + Map.from(json["result"] as Map), + ), + null, + ); + } else { + throw EthApiException( + "getGasOracle() failed with response: " + "${response.body}", + ); + } + } else { + throw EthApiException( + "getGasOracle() failed with status code: " + "${response.statusCode}", + ); + } + } on EthApiException catch (e) { + return EthereumResponse( + null, + e, + ); + } catch (e, s) { + Logging.instance.log( + "getGasOracle(): $e\n$s", + level: LogLevel.Error, + ); + return EthereumResponse( + null, + EthApiException(e.toString()), + ); + } + } + + static Future getFees() async { + final fees = (await getGasOracle()).value!; + final feesFast = fees.fast.shift(9).toBigInt(); + final feesStandard = fees.average.shift(9).toBigInt(); + final feesSlow = fees.slow.shift(9).toBigInt(); + + return FeeObject( + numberOfBlocksFast: fees.numberOfBlocksFast, + numberOfBlocksAverage: fees.numberOfBlocksAverage, + numberOfBlocksSlow: fees.numberOfBlocksSlow, + fast: feesFast.toInt(), + medium: feesStandard.toInt(), + slow: feesSlow.toInt()); + } + + static Future> getTokenContractInfoByAddress( + String contractAddress) async { + try { + final response = await get( + Uri.parse( + "$stackBaseServer/tokens?addrs=$contractAddress&parts=all", + ), + ); + + if (response.statusCode == 200) { + final json = jsonDecode(response.body) as Map; + if (json["data"] is List) { + final map = Map.from(json["data"].first as Map); + EthContract? token; + if (map["isErc20"] == true) { + token = EthContract( + address: map["address"] as String, + decimals: map["decimals"] as int, + name: map["name"] as String, + symbol: map["symbol"] as String, + type: EthContractType.erc20, + ); + } else if (map["isErc721"] == true) { + token = EthContract( + address: map["address"] as String, + decimals: map["decimals"] as int, + name: map["name"] as String, + symbol: map["symbol"] as String, + type: EthContractType.erc721, + ); + } else { + throw EthApiException( + "Unsupported token type found: ${map["type"]}"); + } + + return EthereumResponse( + token, + null, + ); + } else { + throw EthApiException(response.body); + } + } else { + throw EthApiException( + "getTokenByContractAddress($contractAddress) failed with status code: " + "${response.statusCode}", + ); + } + } on EthApiException catch (e) { + return EthereumResponse( + null, + e, + ); + } catch (e, s) { + Logging.instance.log( + "getTokenByContractAddress(): $e\n$s", + level: LogLevel.Error, + ); + return EthereumResponse( + null, + EthApiException(e.toString()), + ); + } + } + + static Future> getTokenAbi({ + required String name, + required String contractAddress, + }) async { + try { + final response = await get( + Uri.parse( + "$stackBaseServer/abis?addrs=$contractAddress&verbose=true", + ), + ); + + if (response.statusCode == 200) { + final json = jsonDecode(response.body)["data"] as List; + + return EthereumResponse( + jsonEncode(json), + null, + ); + } else { + throw EthApiException( + "getTokenAbi($name, $contractAddress) failed with status code: " + "${response.statusCode}", + ); + } + } on EthApiException catch (e) { + return EthereumResponse( + null, + e, + ); + } catch (e, s) { + Logging.instance.log( + "getTokenAbi($name, $contractAddress): $e\n$s", + level: LogLevel.Error, + ); + return EthereumResponse( + null, + EthApiException(e.toString()), + ); + } + } + + /// Fetch the underlying contract address that a proxy contract points to + static Future> getProxyTokenImplementationAddress( + String contractAddress, + ) async { + try { + final response = await get(Uri.parse( + "$stackBaseServer/state?addrs=$contractAddress&parts=proxy")); + if (response.statusCode == 200) { + final json = jsonDecode(response.body); + final list = json["data"] as List; + final map = Map.from(list.first as Map); + + return EthereumResponse( + map["proxy"] as String, + null, + ); + } else { + throw EthApiException( + "getProxyTokenImplementationAddress($contractAddress) failed with" + " status code: ${response.statusCode}", + ); + } + } on EthApiException catch (e) { + return EthereumResponse( + null, + e, + ); + } catch (e, s) { + Logging.instance.log( + "getProxyTokenImplementationAddress($contractAddress) : $e\n$s", + level: LogLevel.Error, + ); + return EthereumResponse( + null, + EthApiException(e.toString()), + ); + } + } +} diff --git a/lib/services/ethereum/ethereum_token_service.dart b/lib/services/ethereum/ethereum_token_service.dart new file mode 100644 index 000000000..90edb1904 --- /dev/null +++ b/lib/services/ethereum/ethereum_token_service.dart @@ -0,0 +1,593 @@ +import 'dart:async'; + +import 'package:decimal/decimal.dart'; +import 'package:ethereum_addresses/ethereum_addresses.dart'; +import 'package:flutter/widgets.dart'; +import 'package:http/http.dart'; +import 'package:isar/isar.dart'; +import 'package:stackwallet/db/isar/main_db.dart'; +import 'package:stackwallet/dto/ethereum/eth_token_tx_dto.dart'; +import 'package:stackwallet/dto/ethereum/eth_token_tx_extra_dto.dart'; +import 'package:stackwallet/models/isar/models/isar_models.dart'; +import 'package:stackwallet/models/node_model.dart'; +import 'package:stackwallet/models/paymint/fee_object_model.dart'; +import 'package:stackwallet/models/token_balance.dart'; +import 'package:stackwallet/services/coins/ethereum/ethereum_wallet.dart'; +import 'package:stackwallet/services/ethereum/ethereum_api.dart'; +import 'package:stackwallet/services/event_bus/events/global/updated_in_background_event.dart'; +import 'package:stackwallet/services/event_bus/events/global/wallet_sync_status_changed_event.dart'; +import 'package:stackwallet/services/event_bus/global_event_bus.dart'; +import 'package:stackwallet/services/mixins/eth_token_cache.dart'; +import 'package:stackwallet/services/node_service.dart'; +import 'package:stackwallet/services/transaction_notification_tracker.dart'; +import 'package:stackwallet/utilities/amount/amount.dart'; +import 'package:stackwallet/utilities/default_nodes.dart'; +import 'package:stackwallet/utilities/enums/coin_enum.dart'; +import 'package:stackwallet/utilities/enums/fee_rate_type_enum.dart'; +import 'package:stackwallet/utilities/eth_commons.dart'; +import 'package:stackwallet/utilities/extensions/extensions.dart'; +import 'package:stackwallet/utilities/extensions/impl/contract_abi.dart'; +import 'package:stackwallet/utilities/flutter_secure_storage_interface.dart'; +import 'package:stackwallet/utilities/logger.dart'; +import 'package:tuple/tuple.dart'; +import 'package:web3dart/web3dart.dart' as web3dart; + +class EthTokenWallet extends ChangeNotifier with EthTokenCache { + final EthereumWallet ethWallet; + final TransactionNotificationTracker tracker; + final SecureStorageInterface _secureStore; + + // late web3dart.EthereumAddress _contractAddress; + late web3dart.EthPrivateKey _credentials; + late web3dart.DeployedContract _deployedContract; + late web3dart.ContractFunction _balanceFunction; + late web3dart.ContractFunction _sendFunction; + late web3dart.Web3Client _client; + + static const _gasLimit = 200000; + + EthTokenWallet({ + required EthContract token, + required this.ethWallet, + required SecureStorageInterface secureStore, + required this.tracker, + }) : _secureStore = secureStore, + _tokenContract = token { + // _contractAddress = web3dart.EthereumAddress.fromHex(token.address); + initCache(ethWallet.walletId, token); + } + + EthContract get tokenContract => _tokenContract; + EthContract _tokenContract; + + TokenBalance get balance => _balance ??= getCachedBalance(); + TokenBalance? _balance; + + Coin get coin => Coin.ethereum; + + Future> prepareSend({ + required String address, + required Amount amount, + Map? args, + }) async { + final feeRateType = args?["feeRate"]; + int fee = 0; + final feeObject = await fees; + switch (feeRateType) { + case FeeRateType.fast: + fee = feeObject.fast; + break; + case FeeRateType.average: + fee = feeObject.medium; + break; + case FeeRateType.slow: + fee = feeObject.slow; + break; + } + + final feeEstimate = estimateFeeFor(fee); + + final client = await getEthClient(); + + final myAddress = await currentReceivingAddress; + final myWeb3Address = web3dart.EthereumAddress.fromHex(myAddress); + + final est = await client.estimateGas( + sender: myWeb3Address, + to: web3dart.EthereumAddress.fromHex(address), + data: _sendFunction + .encodeCall([web3dart.EthereumAddress.fromHex(address), amount.raw]), + gasPrice: web3dart.EtherAmount.fromUnitAndValue( + web3dart.EtherUnit.wei, + fee, + ), + amountOfGas: BigInt.from(_gasLimit), + ); + + final nonce = args?["nonce"] as int? ?? + await client.getTransactionCount(myWeb3Address, + atBlock: const web3dart.BlockNum.pending()); + + final nResponse = await EthereumAPI.getAddressNonce(address: myAddress); + print("=============================================================="); + print("TOKEN client.estimateGas: $est"); + print("TOKEN estimateFeeFor : $feeEstimate"); + print("TOKEN nonce custom response: $nResponse"); + print("TOKEN actual nonce : $nonce"); + print("=============================================================="); + + final tx = web3dart.Transaction.callContract( + contract: _deployedContract, + function: _sendFunction, + parameters: [web3dart.EthereumAddress.fromHex(address), amount.raw], + maxGas: _gasLimit, + gasPrice: web3dart.EtherAmount.fromUnitAndValue( + web3dart.EtherUnit.wei, + fee, + ), + nonce: nonce, + ); + + Map txData = { + "fee": feeEstimate, + "feeInWei": fee, + "address": address, + "recipientAmt": amount, + "ethTx": tx, + "chainId": (await client.getChainId()).toInt(), + "nonce": tx.nonce, + }; + + return txData; + } + + Future confirmSend({required Map txData}) async { + try { + final txid = await _client.sendTransaction( + _credentials, + txData["ethTx"] as web3dart.Transaction, + chainId: txData["chainId"] as int, + ); + + try { + txData["txid"] = txid; + await updateSentCachedTxData(txData); + } catch (e, s) { + // do not rethrow as that would get handled as a send failure further up + // also this is not critical code and transaction should show up on \ + // refresh regardless + Logging.instance.log("$e\n$s", level: LogLevel.Warning); + } + + notifyListeners(); + return txid; + } catch (e) { + // rethrow to pass error in alert + rethrow; + } + } + + Future updateSentCachedTxData(Map txData) async { + final txid = txData["txid"] as String; + final addressString = txData["address"] as String; + final response = await EthereumAPI.getEthTransactionByHash(txid); + + final transaction = Transaction( + walletId: ethWallet.walletId, + txid: txid, + timestamp: DateTime.now().millisecondsSinceEpoch ~/ 1000, + type: TransactionType.outgoing, + subType: TransactionSubType.ethToken, + // precision may be lost here hence the following amountString + amount: (txData["recipientAmt"] as Amount).raw.toInt(), + amountString: (txData["recipientAmt"] as Amount).toJsonString(), + fee: txData["fee"] as int, + height: null, + isCancelled: false, + isLelantus: false, + otherData: tokenContract.address, + slateId: null, + nonce: (txData["nonce"] as int?) ?? + response.value?.nonce.toBigIntFromHex.toInt(), + inputs: [], + outputs: [], + ); + + Address? address = await ethWallet.db.getAddress( + ethWallet.walletId, + addressString, + ); + + address ??= Address( + walletId: ethWallet.walletId, + value: addressString, + publicKey: [], + derivationIndex: -1, + derivationPath: null, + type: AddressType.ethereum, + subType: AddressSubType.nonWallet, + ); + + await ethWallet.db.addNewTransactionData( + [ + Tuple2(transaction, address), + ], + ethWallet.walletId, + ); + } + + Future get currentReceivingAddress async { + final address = await _currentReceivingAddress; + return checksumEthereumAddress( + address?.value ?? _credentials.address.toString()); + } + + Future get _currentReceivingAddress => ethWallet.db + .getAddresses(ethWallet.walletId) + .filter() + .typeEqualTo(AddressType.ethereum) + .subTypeEqualTo(AddressSubType.receiving) + .sortByDerivationIndexDesc() + .findFirst(); + + Amount estimateFeeFor(int feeRate) { + return estimateFee(feeRate, _gasLimit, coin.decimals); + } + + Future get fees => EthereumAPI.getFees(); + + Future _updateTokenABI({ + required EthContract forContract, + required String usingContractAddress, + }) async { + final abiResponse = await EthereumAPI.getTokenAbi( + name: forContract.name, + contractAddress: usingContractAddress, + ); + // Fetch token ABI so we can call token functions + if (abiResponse.value != null) { + final updatedToken = forContract.copyWith(abi: abiResponse.value!); + // Store updated contract + final id = await MainDB.instance.putEthContract(updatedToken); + return updatedToken..id = id; + } else { + throw abiResponse.exception!; + } + } + + Future initialize() async { + final contractAddress = + web3dart.EthereumAddress.fromHex(tokenContract.address); + + // if (tokenContract.abi == null) { + _tokenContract = await _updateTokenABI( + forContract: tokenContract, + usingContractAddress: contractAddress.hex, + ); + // } + + String? mnemonicString = await ethWallet.mnemonicString; + + //Get private key for given mnemonic + String privateKey = getPrivateKey( + mnemonicString!, + (await ethWallet.mnemonicPassphrase) ?? "", + ); + _credentials = web3dart.EthPrivateKey.fromHex(privateKey); + + _deployedContract = web3dart.DeployedContract( + ContractAbiExtensions.fromJsonList( + jsonList: tokenContract.abi!, + name: tokenContract.name, + ), + contractAddress, + ); + + try { + _balanceFunction = _deployedContract.function('balanceOf'); + _sendFunction = _deployedContract.function('transfer'); + } catch (_) { + //==================================================================== + // final list = List>.from( + // jsonDecode(tokenContract.abi!) as List); + // final functionNames = list.map((e) => e["name"] as String); + // + // if (!functionNames.contains("balanceOf")) { + // list.add( + // { + // "encoding": "0x70a08231", + // "inputs": [ + // {"name": "account", "type": "address"} + // ], + // "name": "balanceOf", + // "outputs": [ + // {"name": "val_0", "type": "uint256"} + // ], + // "signature": "balanceOf(address)", + // "type": "function" + // }, + // ); + // } + // + // if (!functionNames.contains("transfer")) { + // list.add( + // { + // "encoding": "0xa9059cbb", + // "inputs": [ + // {"name": "dst", "type": "address"}, + // {"name": "rawAmount", "type": "uint256"} + // ], + // "name": "transfer", + // "outputs": [ + // {"name": "val_0", "type": "bool"} + // ], + // "signature": "transfer(address,uint256)", + // "type": "function" + // }, + // ); + // } + //-------------------------------------------------------------------- + //==================================================================== + + // function not found so likely a proxy so we need to fetch the impl + //==================================================================== + // final updatedToken = tokenContract.copyWith(abi: jsonEncode(list)); + // // Store updated contract + // final id = await MainDB.instance.putEthContract(updatedToken); + // _tokenContract = updatedToken..id = id; + //-------------------------------------------------------------------- + final contractAddressResponse = + await EthereumAPI.getProxyTokenImplementationAddress( + contractAddress.hex); + + if (contractAddressResponse.value != null) { + _tokenContract = await _updateTokenABI( + forContract: tokenContract, + usingContractAddress: contractAddressResponse.value!, + ); + } else { + throw contractAddressResponse.exception!; + } + //==================================================================== + } + + _deployedContract = web3dart.DeployedContract( + ContractAbiExtensions.fromJsonList( + jsonList: tokenContract.abi!, + name: tokenContract.name, + ), + contractAddress, + ); + + _balanceFunction = _deployedContract.function('balanceOf'); + _sendFunction = _deployedContract.function('transfer'); + + _client = await getEthClient(); + + unawaited(refresh()); + } + + bool get isRefreshing => _refreshLock; + + bool _refreshLock = false; + + Future refresh() async { + if (!_refreshLock) { + _refreshLock = true; + try { + GlobalEventBus.instance.fire( + WalletSyncStatusChangedEvent( + WalletSyncStatus.syncing, + ethWallet.walletId + tokenContract.address, + coin, + ), + ); + + await refreshCachedBalance(); + await _refreshTransactions(); + } catch (e, s) { + Logging.instance.log( + "Caught exception in ${tokenContract.name} ${ethWallet.walletName} ${ethWallet.walletId} refresh(): $e\n$s", + level: LogLevel.Warning, + ); + } finally { + _refreshLock = false; + GlobalEventBus.instance.fire( + WalletSyncStatusChangedEvent( + WalletSyncStatus.synced, + ethWallet.walletId + tokenContract.address, + coin, + ), + ); + notifyListeners(); + } + } + } + + Future refreshCachedBalance() async { + final balanceRequest = await _client.call( + contract: _deployedContract, + function: _balanceFunction, + params: [_credentials.address], + ); + + String _balance = balanceRequest.first.toString(); + + final newBalance = TokenBalance( + contractAddress: tokenContract.address, + total: Amount.fromDecimal( + Decimal.parse(_balance), + fractionDigits: tokenContract.decimals, + ), + spendable: Amount.fromDecimal( + Decimal.parse(_balance), + fractionDigits: tokenContract.decimals, + ), + blockedTotal: Amount( + rawValue: BigInt.zero, + fractionDigits: tokenContract.decimals, + ), + pendingSpendable: Amount( + rawValue: BigInt.zero, + fractionDigits: tokenContract.decimals, + ), + ); + await updateCachedBalance(newBalance); + notifyListeners(); + } + + Future> get transactions => ethWallet.db + .getTransactions(ethWallet.walletId) + .filter() + .otherDataEqualTo(tokenContract.address) + .sortByTimestampDesc() + .findAll(); + + String _addressFromTopic(String topic) => + checksumEthereumAddress("0x${topic.substring(topic.length - 40)}"); + + Future _refreshTransactions() async { + String addressString = + checksumEthereumAddress(await currentReceivingAddress); + + final response = await EthereumAPI.getTokenTransactions( + address: addressString, + tokenContractAddress: tokenContract.address, + ); + + if (response.value == null) { + throw response.exception ?? + Exception("Failed to fetch token transaction data"); + } + + // no need to continue if no transactions found + if (response.value!.isEmpty) { + return; + } + + final response2 = await EthereumAPI.getEthTokenTransactionsByTxids( + response.value!.map((e) => e.transactionHash).toList(), + ); + + if (response2.value == null) { + throw response2.exception ?? + Exception("Failed to fetch token transactions"); + } + final List> data = []; + for (final tokenDto in response.value!) { + data.add( + Tuple2( + tokenDto, + response2.value!.firstWhere( + (e) => e.hash == tokenDto.transactionHash, + ), + ), + ); + } + + final List> txnsData = []; + + for (final tuple in data) { + // ignore all non Transfer events (for now) + if (tuple.item1.topics[0] == kTransferEventSignature) { + final Amount amount; + String fromAddress, toAddress; + amount = Amount( + rawValue: tuple.item1.data.toBigIntFromHex, + fractionDigits: tokenContract.decimals, + ); + + fromAddress = _addressFromTopic( + tuple.item1.topics[1], + ); + toAddress = _addressFromTopic( + tuple.item1.topics[2], + ); + + bool isIncoming; + bool isSentToSelf = false; + if (fromAddress == addressString) { + isIncoming = false; + if (toAddress == addressString) { + isSentToSelf = true; + } + } else if (toAddress == addressString) { + isIncoming = true; + } else { + throw Exception("Unknown token transaction found for " + "${ethWallet.walletName} ${ethWallet.walletId}: " + "${tuple.item1.toString()}"); + } + + final txn = Transaction( + walletId: ethWallet.walletId, + txid: tuple.item1.transactionHash, + timestamp: tuple.item2.timestamp, + type: + isIncoming ? TransactionType.incoming : TransactionType.outgoing, + subType: TransactionSubType.ethToken, + amount: amount.raw.toInt(), + amountString: amount.toJsonString(), + fee: (tuple.item2.gasUsed.raw * tuple.item2.gasPrice.raw).toInt(), + height: tuple.item1.blockNumber, + isCancelled: false, + isLelantus: false, + slateId: null, + nonce: tuple.item2.nonce, + otherData: tuple.item1.address, + inputs: [], + outputs: [], + ); + + Address? transactionAddress = await ethWallet.db + .getAddresses(ethWallet.walletId) + .filter() + .valueEqualTo(toAddress) + .findFirst(); + + transactionAddress ??= Address( + walletId: ethWallet.walletId, + value: toAddress, + publicKey: [], + derivationIndex: isSentToSelf ? 0 : -1, + derivationPath: isSentToSelf + ? (DerivationPath()..value = "$hdPathEthereum/0") + : null, + type: AddressType.ethereum, + subType: isSentToSelf + ? AddressSubType.receiving + : AddressSubType.nonWallet, + ); + + txnsData.add(Tuple2(txn, transactionAddress)); + } + } + await ethWallet.db.addNewTransactionData(txnsData, ethWallet.walletId); + + // quick hack to notify manager to call notifyListeners if + // transactions changed + if (txnsData.isNotEmpty) { + GlobalEventBus.instance.fire( + UpdatedInBackgroundEvent( + "${tokenContract.name} transactions updated/added for: ${ethWallet.walletId} ${ethWallet.walletName}", + ethWallet.walletId, + ), + ); + } + } + + bool validateAddress(String address) { + return isValidEthereumAddress(address); + } + + NodeModel getCurrentNode() { + return NodeService(secureStorageInterface: _secureStore) + .getPrimaryNodeFor(coin: coin) ?? + DefaultNodes.getNodeFor(coin); + } + + Future getEthClient() async { + final node = getCurrentNode(); + return web3dart.Web3Client(node.host, Client()); + } +} diff --git a/lib/services/exchange/change_now/change_now_api.dart b/lib/services/exchange/change_now/change_now_api.dart index cc9a14182..57823a813 100644 --- a/lib/services/exchange/change_now/change_now_api.dart +++ b/lib/services/exchange/change_now/change_now_api.dart @@ -195,6 +195,89 @@ class ChangeNowAPI { } } + Future>> getCurrenciesV2( + // { + // bool? fixedRate, + // bool? active, + // } + ) async { + Map? params; + + // if (active != null || fixedRate != null) { + // params = {}; + // if (fixedRate != null) { + // params.addAll({"fixedRate": fixedRate.toString()}); + // } + // if (active != null) { + // params.addAll({"active": active.toString()}); + // } + // } + + final uri = _buildUriV2("/exchange/currencies", params); + + try { + // json array is expected here + final jsonArray = await _makeGetRequest(uri); + + try { + final result = await compute( + _parseV2CurrenciesJson, + jsonArray as List, + ); + return result; + } catch (e, s) { + Logging.instance.log("getAvailableCurrencies exception: $e\n$s", + level: LogLevel.Error); + return ExchangeResponse( + exception: ExchangeException( + "Error: $jsonArray", + ExchangeExceptionType.serializeResponseError, + ), + ); + } + } catch (e, s) { + Logging.instance.log("getAvailableCurrencies exception: $e\n$s", + level: LogLevel.Error); + return ExchangeResponse( + exception: ExchangeException( + e.toString(), + ExchangeExceptionType.generic, + ), + ); + } + } + + ExchangeResponse> _parseV2CurrenciesJson( + List args, + ) { + try { + List currencies = []; + + for (final json in args) { + try { + final map = Map.from(json as Map); + currencies.add( + Currency.fromJson( + map, + rateType: (map["supportsFixedRate"] as bool) + ? SupportedRateType.both + : SupportedRateType.estimated, + exchangeName: ChangeNowExchange.exchangeName, + ), + ); + } catch (_) { + return ExchangeResponse( + exception: ExchangeException("Failed to serialize $json", + ExchangeExceptionType.serializeResponseError)); + } + } + + return ExchangeResponse(value: currencies); + } catch (_) { + rethrow; + } + } + /// This API endpoint returns the array of markets available for the specified currency be default. /// The availability of a particular pair is determined by the 'isAvailable' field. /// diff --git a/lib/services/exchange/change_now/change_now_exchange.dart b/lib/services/exchange/change_now/change_now_exchange.dart index dd5baec94..3189ff84e 100644 --- a/lib/services/exchange/change_now/change_now_exchange.dart +++ b/lib/services/exchange/change_now/change_now_exchange.dart @@ -82,10 +82,11 @@ class ChangeNowExchange extends Exchange { Future>> getAllCurrencies( bool fixedRate, ) async { - return await ChangeNowAPI.instance.getAvailableCurrencies( - fixedRate: fixedRate ? true : null, - active: true, - ); + return await ChangeNowAPI.instance.getCurrenciesV2(); + // return await ChangeNowAPI.instance.getAvailableCurrencies( + // fixedRate: fixedRate ? true : null, + // active: true, + // ); } @override diff --git a/lib/services/exchange/exchange_data_loading_service.dart b/lib/services/exchange/exchange_data_loading_service.dart index ad754cb56..8e73e46a9 100644 --- a/lib/services/exchange/exchange_data_loading_service.dart +++ b/lib/services/exchange/exchange_data_loading_service.dart @@ -1,6 +1,6 @@ import 'package:flutter/foundation.dart'; import 'package:isar/isar.dart'; -import 'package:stackwallet/hive/db.dart'; +import 'package:stackwallet/db/hive/db.dart'; import 'package:stackwallet/models/exchange/aggregate_currency.dart'; import 'package:stackwallet/models/exchange/exchange_form_state.dart'; import 'package:stackwallet/models/isar/exchange_cache/currency.dart'; diff --git a/lib/services/exchange/majestic_bank/majestic_bank_exchange.dart b/lib/services/exchange/majestic_bank/majestic_bank_exchange.dart index d6b9bff98..dcc63c68b 100644 --- a/lib/services/exchange/majestic_bank/majestic_bank_exchange.dart +++ b/lib/services/exchange/majestic_bank/majestic_bank_exchange.dart @@ -131,6 +131,7 @@ class MajesticBankExchange extends Exchange { rateType: SupportedRateType.both, isAvailable: true, isStackCoin: Currency.checkIsStackCoin(limit.currency), + tokenContract: null, ); currencies.add(currency); } diff --git a/lib/services/exchange/simpleswap/simpleswap_exchange.dart b/lib/services/exchange/simpleswap/simpleswap_exchange.dart index 791ccb17c..1157bd09e 100644 --- a/lib/services/exchange/simpleswap/simpleswap_exchange.dart +++ b/lib/services/exchange/simpleswap/simpleswap_exchange.dart @@ -67,6 +67,7 @@ class SimpleSwapExchange extends Exchange { : SupportedRateType.estimated, isAvailable: true, isStackCoin: Currency.checkIsStackCoin(e.symbol), + tokenContract: null, ), ) .toList(); diff --git a/lib/services/mixins/coin_control_interface.dart b/lib/services/mixins/coin_control_interface.dart index ceebb6416..f1cc15fa4 100644 --- a/lib/services/mixins/coin_control_interface.dart +++ b/lib/services/mixins/coin_control_interface.dart @@ -1,10 +1,11 @@ import 'dart:async'; import 'package:isar/isar.dart'; -import 'package:stackwallet/db/main_db.dart'; +import 'package:stackwallet/db/isar/main_db.dart'; import 'package:stackwallet/models/balance.dart'; import 'package:stackwallet/services/event_bus/events/global/balance_refreshed_event.dart'; import 'package:stackwallet/services/event_bus/global_event_bus.dart'; +import 'package:stackwallet/utilities/amount/amount.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; mixin CoinControlInterface { @@ -35,24 +36,41 @@ mixin CoinControlInterface { final utxos = await _db.getUTXOs(_walletId).findAll(); final currentChainHeight = await _getChainHeight(); - int satoshiBalanceTotal = 0; - int satoshiBalancePending = 0; - int satoshiBalanceSpendable = 0; - int satoshiBalanceBlocked = 0; + Amount satoshiBalanceTotal = Amount( + rawValue: BigInt.zero, + fractionDigits: _coin.decimals, + ); + Amount satoshiBalancePending = Amount( + rawValue: BigInt.zero, + fractionDigits: _coin.decimals, + ); + Amount satoshiBalanceSpendable = Amount( + rawValue: BigInt.zero, + fractionDigits: _coin.decimals, + ); + Amount satoshiBalanceBlocked = Amount( + rawValue: BigInt.zero, + fractionDigits: _coin.decimals, + ); for (final utxo in utxos) { - satoshiBalanceTotal += utxo.value; + final utxoAmount = Amount( + rawValue: BigInt.from(utxo.value), + fractionDigits: _coin.decimals, + ); + + satoshiBalanceTotal = satoshiBalanceTotal + utxoAmount; if (utxo.isBlocked) { - satoshiBalanceBlocked += utxo.value; + satoshiBalanceBlocked = satoshiBalanceBlocked + utxoAmount; } else { if (utxo.isConfirmed( currentChainHeight, _coin.requiredConfirmations, )) { - satoshiBalanceSpendable += utxo.value; + satoshiBalanceSpendable + satoshiBalanceSpendable + utxoAmount; } else { - satoshiBalancePending += utxo.value; + satoshiBalancePending = satoshiBalancePending + utxoAmount; } } } diff --git a/lib/services/mixins/electrum_x_parsing.dart b/lib/services/mixins/electrum_x_parsing.dart index 440a1981a..c313a91eb 100644 --- a/lib/services/mixins/electrum_x_parsing.dart +++ b/lib/services/mixins/electrum_x_parsing.dart @@ -4,8 +4,8 @@ import 'package:bip47/src/util.dart'; import 'package:decimal/decimal.dart'; import 'package:stackwallet/models/isar/models/isar_models.dart'; import 'package:stackwallet/services/mixins/paynym_wallet_interface.dart'; +import 'package:stackwallet/utilities/amount/amount.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; -import 'package:stackwallet/utilities/format.dart'; import 'package:tuple/tuple.dart'; mixin ElectrumXParsing { @@ -32,12 +32,27 @@ mixin ElectrumXParsing { Set inputAddresses = {}; Set outputAddresses = {}; - int totalInputValue = 0; - int totalOutputValue = 0; + Amount totalInputValue = Amount( + rawValue: BigInt.zero, + fractionDigits: coin.decimals, + ); + Amount totalOutputValue = Amount( + rawValue: BigInt.zero, + fractionDigits: coin.decimals, + ); - int amountSentFromWallet = 0; - int amountReceivedInWallet = 0; - int changeAmount = 0; + Amount amountSentFromWallet = Amount( + rawValue: BigInt.zero, + fractionDigits: coin.decimals, + ); + Amount amountReceivedInWallet = Amount( + rawValue: BigInt.zero, + fractionDigits: coin.decimals, + ); + Amount changeAmount = Amount( + rawValue: BigInt.zero, + fractionDigits: coin.decimals, + ); // parse inputs for (final input in txData["vin"] as List) { @@ -54,9 +69,9 @@ mixin ElectrumXParsing { // check matching output if (prevOut == output["n"]) { // get value - final value = Format.decimalAmountToSatoshis( + final value = Amount.fromDecimal( Decimal.parse(output["value"].toString()), - coin, + fractionDigits: coin.decimals, ); // add value to total @@ -82,9 +97,9 @@ mixin ElectrumXParsing { // parse outputs for (final output in txData["vout"] as List) { // get value - final value = Format.decimalAmountToSatoshis( + final value = Amount.fromDecimal( Decimal.parse(output["value"].toString()), - coin, + fractionDigits: coin.decimals, ); // add value to total @@ -120,7 +135,7 @@ mixin ElectrumXParsing { Address transactionAddress = txData["address"] as Address; TransactionType type; - int amount; + Amount amount; if (mySentFromAddresses.isNotEmpty && myReceivedOnAddresses.isNotEmpty) { // tx is sent to self type = TransactionType.sentToSelf; @@ -188,10 +203,10 @@ mixin ElectrumXParsing { json["scriptPubKey"]?["addresses"]?[0] as String? ?? json['scriptPubKey']?['type'] as String? ?? "", - value: Format.decimalAmountToSatoshis( + value: Amount.fromDecimal( Decimal.parse(json["value"].toString()), - coin, - ), + fractionDigits: coin.decimals, + ).raw.toInt(), ); outs.add(output); } @@ -219,13 +234,16 @@ mixin ElectrumXParsing { (DateTime.now().millisecondsSinceEpoch ~/ 1000), type: type, subType: txSubType, - amount: amount, - fee: fee, + // amount may overflow. Deprecated. Use amountString + amount: amount.raw.toInt(), + amountString: amount.toJsonString(), + fee: fee.raw.toInt(), height: txData["height"] as int?, isCancelled: false, isLelantus: false, slateId: null, otherData: null, + nonce: null, inputs: ins, outputs: outs, ); diff --git a/lib/services/mixins/epic_cash_hive.dart b/lib/services/mixins/epic_cash_hive.dart index 09a3563b8..3c5c91b07 100644 --- a/lib/services/mixins/epic_cash_hive.dart +++ b/lib/services/mixins/epic_cash_hive.dart @@ -1,4 +1,4 @@ -import 'package:stackwallet/hive/db.dart'; +import 'package:stackwallet/db/hive/db.dart'; mixin EpicCashHive { late final String _walletId; diff --git a/lib/services/mixins/eth_token_cache.dart b/lib/services/mixins/eth_token_cache.dart new file mode 100644 index 000000000..ccaa293d0 --- /dev/null +++ b/lib/services/mixins/eth_token_cache.dart @@ -0,0 +1,61 @@ +import 'package:stackwallet/db/hive/db.dart'; +import 'package:stackwallet/models/isar/models/ethereum/eth_contract.dart'; +import 'package:stackwallet/models/token_balance.dart'; +import 'package:stackwallet/utilities/amount/amount.dart'; + +abstract class TokenCacheKeys { + static String tokenBalance(String contractAddress) { + return "tokenBalanceCache_$contractAddress"; + } +} + +mixin EthTokenCache { + late final String _walletId; + late final EthContract _token; + + void initCache(String walletId, EthContract token) { + _walletId = walletId; + _token = token; + } + + // token balance cache + TokenBalance getCachedBalance() { + final jsonString = DB.instance.get( + boxName: _walletId, + key: TokenCacheKeys.tokenBalance(_token.address), + ) as String?; + if (jsonString == null) { + return TokenBalance( + contractAddress: _token.address, + total: Amount( + rawValue: BigInt.zero, + fractionDigits: _token.decimals, + ), + spendable: Amount( + rawValue: BigInt.zero, + fractionDigits: _token.decimals, + ), + blockedTotal: Amount( + rawValue: BigInt.zero, + fractionDigits: _token.decimals, + ), + pendingSpendable: Amount( + rawValue: BigInt.zero, + fractionDigits: _token.decimals, + ), + ); + } + return TokenBalance.fromJson( + jsonString, + _token.decimals, + ); + } + + Future updateCachedBalance(TokenBalance balance) async { + await DB.instance.put( + boxName: _walletId, + key: TokenCacheKeys.tokenBalance(_token.address), + value: balance.toJsonIgnoreCoin(), + ); + } +} diff --git a/lib/services/mixins/firo_hive.dart b/lib/services/mixins/firo_hive.dart index 43f0dd622..321724ad1 100644 --- a/lib/services/mixins/firo_hive.dart +++ b/lib/services/mixins/firo_hive.dart @@ -1,4 +1,4 @@ -import 'package:stackwallet/hive/db.dart'; +import 'package:stackwallet/db/hive/db.dart'; mixin FiroHive { late final String _walletId; diff --git a/lib/services/mixins/paynym_wallet_interface.dart b/lib/services/mixins/paynym_wallet_interface.dart index 0003aa50d..156407420 100644 --- a/lib/services/mixins/paynym_wallet_interface.dart +++ b/lib/services/mixins/paynym_wallet_interface.dart @@ -10,12 +10,13 @@ import 'package:bitcoindart/src/utils/constants/op.dart' as op; import 'package:bitcoindart/src/utils/script.dart' as bscript; import 'package:isar/isar.dart'; import 'package:pointycastle/digests/sha256.dart'; -import 'package:stackwallet/db/main_db.dart'; +import 'package:stackwallet/db/isar/main_db.dart'; import 'package:stackwallet/electrumx_rpc/electrumx.dart'; import 'package:stackwallet/exceptions/wallet/insufficient_balance_exception.dart'; import 'package:stackwallet/exceptions/wallet/paynym_send_exception.dart'; import 'package:stackwallet/models/isar/models/isar_models.dart'; import 'package:stackwallet/models/signing_data.dart'; +import 'package:stackwallet/utilities/amount/amount.dart'; import 'package:stackwallet/utilities/bip32_utils.dart'; import 'package:stackwallet/utilities/bip47_utils.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; @@ -52,7 +53,7 @@ mixin PaynymWalletInterface { }) _estimateTxFee; late final Future> Function({ required String address, - required int satoshiAmount, + required Amount amount, Map? args, }) _prepareSend; late final Future Function({ @@ -93,7 +94,7 @@ mixin PaynymWalletInterface { estimateTxFee, required Future> Function({ required String address, - required int satoshiAmount, + required Amount amount, Map? args, }) prepareSend, @@ -307,10 +308,11 @@ mixin PaynymWalletInterface { return Format.uint8listToString(bytes); } - Future> preparePaymentCodeSend( - {required PaymentCode paymentCode, - required int satoshiAmount, - Map? args}) async { + Future> preparePaymentCodeSend({ + required PaymentCode paymentCode, + required Amount amount, + Map? args, + }) async { if (!(await hasConnected(paymentCode.toString()))) { throw PaynymSendException( "No notification transaction sent to $paymentCode"); @@ -326,7 +328,7 @@ mixin PaynymWalletInterface { return _prepareSend( address: sendToAddress.value, - satoshiAmount: satoshiAmount, + amount: amount, args: args, ); } diff --git a/lib/services/mixins/wallet_cache.dart b/lib/services/mixins/wallet_cache.dart index ec0cbfe67..49f53381a 100644 --- a/lib/services/mixins/wallet_cache.dart +++ b/lib/services/mixins/wallet_cache.dart @@ -1,5 +1,6 @@ -import 'package:stackwallet/hive/db.dart'; +import 'package:stackwallet/db/hive/db.dart'; import 'package:stackwallet/models/balance.dart'; +import 'package:stackwallet/utilities/amount/amount.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; mixin WalletCache { @@ -70,10 +71,13 @@ mixin WalletCache { if (jsonString == null) { return Balance( coin: _coin, - total: 0, - spendable: 0, - blockedTotal: 0, - pendingSpendable: 0, + total: Amount(rawValue: BigInt.zero, fractionDigits: _coin.decimals), + spendable: + Amount(rawValue: BigInt.zero, fractionDigits: _coin.decimals), + blockedTotal: + Amount(rawValue: BigInt.zero, fractionDigits: _coin.decimals), + pendingSpendable: + Amount(rawValue: BigInt.zero, fractionDigits: _coin.decimals), ); } return Balance.fromJson(jsonString, _coin); @@ -96,10 +100,13 @@ mixin WalletCache { if (jsonString == null) { return Balance( coin: _coin, - total: 0, - spendable: 0, - blockedTotal: 0, - pendingSpendable: 0, + total: Amount(rawValue: BigInt.zero, fractionDigits: _coin.decimals), + spendable: + Amount(rawValue: BigInt.zero, fractionDigits: _coin.decimals), + blockedTotal: + Amount(rawValue: BigInt.zero, fractionDigits: _coin.decimals), + pendingSpendable: + Amount(rawValue: BigInt.zero, fractionDigits: _coin.decimals), ); } return Balance.fromJson(jsonString, _coin); @@ -112,4 +119,22 @@ mixin WalletCache { value: balance.toJsonIgnoreCoin(), ); } + + // Ethereum specific + List getWalletTokenContractAddresses() { + return DB.instance.get( + boxName: _walletId, + key: DBKeys.ethTokenContracts, + ) as List? ?? + []; + } + + Future updateWalletTokenContractAddresses( + List contractAddresses) async { + await DB.instance.put( + boxName: _walletId, + key: DBKeys.ethTokenContracts, + value: contractAddresses, + ); + } } diff --git a/lib/services/mixins/wallet_db.dart b/lib/services/mixins/wallet_db.dart index cf62cf6da..bb46be7d1 100644 --- a/lib/services/mixins/wallet_db.dart +++ b/lib/services/mixins/wallet_db.dart @@ -1,4 +1,4 @@ -import 'package:stackwallet/db/main_db.dart'; +import 'package:stackwallet/db/isar/main_db.dart'; mixin WalletDB { MainDB? _db; diff --git a/lib/services/node_service.dart b/lib/services/node_service.dart index 8a0e17ad7..fc588fa6a 100644 --- a/lib/services/node_service.dart +++ b/lib/services/node_service.dart @@ -2,7 +2,7 @@ import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:http/http.dart'; -import 'package:stackwallet/hive/db.dart'; +import 'package:stackwallet/db/hive/db.dart'; import 'package:stackwallet/models/node_model.dart'; import 'package:stackwallet/utilities/default_nodes.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; diff --git a/lib/services/notes_service.dart b/lib/services/notes_service.dart index 013600625..eac8b6c22 100644 --- a/lib/services/notes_service.dart +++ b/lib/services/notes_service.dart @@ -1,5 +1,5 @@ import 'package:flutter/material.dart'; -import 'package:stackwallet/hive/db.dart'; +import 'package:stackwallet/db/hive/db.dart'; import 'package:stackwallet/utilities/logger.dart'; class NotesService extends ChangeNotifier { diff --git a/lib/services/notifications_service.dart b/lib/services/notifications_service.dart index 5408d6126..54f6d60ae 100644 --- a/lib/services/notifications_service.dart +++ b/lib/services/notifications_service.dart @@ -1,9 +1,9 @@ import 'dart:async'; import 'package:flutter/foundation.dart'; +import 'package:stackwallet/db/hive/db.dart'; import 'package:stackwallet/electrumx_rpc/electrumx.dart'; import 'package:stackwallet/exceptions/electrumx/no_such_transaction.dart'; -import 'package:stackwallet/hive/db.dart'; import 'package:stackwallet/models/exchange/response_objects/trade.dart'; import 'package:stackwallet/models/notification_model.dart'; import 'package:stackwallet/services/exchange/exchange_response.dart'; diff --git a/lib/services/price.dart b/lib/services/price.dart index 43751c141..2a50f0cf1 100644 --- a/lib/services/price.dart +++ b/lib/services/price.dart @@ -4,7 +4,7 @@ import 'dart:convert'; import 'package:decimal/decimal.dart'; import 'package:flutter/foundation.dart'; import 'package:http/http.dart'; -import 'package:stackwallet/hive/db.dart'; +import 'package:stackwallet/db/hive/db.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:stackwallet/utilities/logger.dart'; import 'package:stackwallet/utilities/prefs.dart'; @@ -86,10 +86,12 @@ class PriceAPI { } Map> result = {}; try { - final uri = Uri.parse( - "https://api.coingecko.com/api/v3/coins/markets?vs_currency=${baseCurrency.toLowerCase()}&ids=monero,bitcoin,litecoin,epic-cash,zcoin,dogecoin,bitcoin-cash,namecoin,wownero,particl&order=market_cap_desc&per_page=10&page=1&sparkline=false"); - // final uri = Uri.parse( - // "https://api.coingecko.com/api/v3/coins/markets?vs_currency=${baseCurrency.toLowerCase()}&ids=monero%2Cbitcoin%2Cepic-cash%2Czcoin%2Cdogecoin&order=market_cap_desc&per_page=10&page=1&sparkline=false"); + final uri = + Uri.parse("https://api.coingecko.com/api/v3/coins/markets?vs_currency" + "=${baseCurrency.toLowerCase()}" + "&ids=monero,bitcoin,litecoin,epic-cash,zcoin,dogecoin," + "bitcoin-cash,namecoin,wownero,ethereum,particl" + "&order=market_cap_desc&per_page=50&page=1&sparkline=false"); final coinGeckoResponse = await client.get( uri, @@ -146,4 +148,59 @@ class PriceAPI { return null; } } + + Future>> + getPricesAnd24hChangeForEthTokens({ + required Set contractAddresses, + required String baseCurrency, + }) async { + final Map> tokenPrices = {}; + + if (contractAddresses.isEmpty) return tokenPrices; + + final externalCalls = Prefs.instance.externalCalls; + if ((!Logger.isTestEnv && !externalCalls) || + !(await Prefs.instance.isExternalCallsSet())) { + Logging.instance.log("User does not want to use external calls", + level: LogLevel.Info); + return tokenPrices; + } + + try { + final contractAddressesString = + contractAddresses.reduce((value, element) => "$value,$element"); + final uri = Uri.parse( + "https://api.coingecko.com/api/v3/simple/token_price/ethereum" + "?vs_currencies=${baseCurrency.toLowerCase()}&contract_addresses" + "=$contractAddressesString&include_24hr_change=true"); + + final coinGeckoResponse = await client.get( + uri, + headers: {'Content-Type': 'application/json'}, + ); + + final coinGeckoData = jsonDecode(coinGeckoResponse.body) as Map; + + for (final key in coinGeckoData.keys) { + final contractAddress = key as String; + + final map = coinGeckoData[contractAddress] as Map; + + final price = Decimal.parse(map[baseCurrency.toLowerCase()].toString()); + final change24h = double.parse( + map["${baseCurrency.toLowerCase()}_24h_change"].toString()); + + tokenPrices[contractAddress] = Tuple2(price, change24h); + } + + return tokenPrices; + } catch (e, s) { + Logging.instance.log( + "getPricesAnd24hChangeForEthTokens($baseCurrency,$contractAddresses): $e\n$s", + level: LogLevel.Error, + ); + // return previous cached values + return tokenPrices; + } + } } diff --git a/lib/services/price_service.dart b/lib/services/price_service.dart index eb2b1eba4..b699af943 100644 --- a/lib/services/price_service.dart +++ b/lib/services/price_service.dart @@ -9,16 +9,24 @@ import 'package:tuple/tuple.dart'; class PriceService extends ChangeNotifier { late final String baseTicker; + final Set tokenContractAddressesToCheck = {}; final Duration updateInterval = const Duration(seconds: 60); Timer? _timer; final Map> _cachedPrices = { for (final coin in Coin.values) coin: Tuple2(Decimal.zero, 0.0) }; + + final Map> _cachedTokenPrices = {}; + final _priceAPI = PriceAPI(Client()); Tuple2 getPrice(Coin coin) => _cachedPrices[coin]!; + Tuple2 getTokenPrice(String contractAddress) => + _cachedTokenPrices[contractAddress.toLowerCase()] ?? + Tuple2(Decimal.zero, 0); + PriceService(this.baseTicker); Future updatePrice() async { @@ -33,6 +41,20 @@ class PriceService extends ChangeNotifier { } } + if (tokenContractAddressesToCheck.isNotEmpty) { + final tokenPriceMap = await _priceAPI.getPricesAnd24hChangeForEthTokens( + contractAddresses: tokenContractAddressesToCheck, + baseCurrency: baseTicker, + ); + + for (final map in tokenPriceMap.entries) { + if (_cachedTokenPrices[map.key] != map.value) { + _cachedTokenPrices[map.key] = map.value; + shouldNotify = true; + } + } + } + if (shouldNotify) { notifyListeners(); } diff --git a/lib/services/tokens_service.dart b/lib/services/tokens_service.dart new file mode 100644 index 000000000..166a97cda --- /dev/null +++ b/lib/services/tokens_service.dart @@ -0,0 +1,425 @@ +import 'dart:convert'; + +import 'package:flutter/material.dart'; +import 'package:stackwallet/db/hive/db.dart'; +import 'package:stackwallet/utilities/enums/coin_enum.dart'; +import 'package:stackwallet/utilities/flutter_secure_storage_interface.dart'; + +class TokenInfo { + final Coin coin; + final String walletId; + final String contractAddress; + + const TokenInfo( + {required this.coin, + required this.walletId, + required this.contractAddress}); + + factory TokenInfo.fromJson(Map jsonObject) { + return TokenInfo( + coin: Coin.values.byName(jsonObject["coin"] as String), + walletId: jsonObject["id"] as String, + contractAddress: jsonObject["contractAddress"] as String, + ); + } + + Map toMap() { + return { + "contractAddress": contractAddress, + "walletId": walletId, + "coin": coin.name, + }; + } + + String toJsonString() { + return jsonEncode(toMap()); + } + + @override + String toString() { + return "TokenInfo: ${toJsonString()}"; + } +} + +class TokensService extends ChangeNotifier { + late final SecureStorageInterface _secureStore; + + // Future>? _walletNames; + // Future> get walletNames => + // _walletNames ??= _fetchWalletNames(); + + TokensService({ + required SecureStorageInterface secureStorageInterface, + }) { + _secureStore = secureStorageInterface; + } + + // Future getWalletCryptoCurrency({required String walletName}) async { + // final id = await getWalletId(walletName); + // final currency = DB.instance.get( + // boxName: DB.boxNameAllWalletsData, key: "${id}_cryptoCurrency"); + // return Coin.values.byName(currency as String); + // } + + // Future renameWallet({ + // required String from, + // required String to, + // required bool shouldNotifyListeners, + // }) async { + // if (from == to) { + // return true; + // } + // + // final walletInfo = DB.instance + // .get(boxName: DB.boxNameAllWalletsData, key: 'names') as Map; + // + // final info = walletInfo.values.firstWhere( + // (element) => element['name'] == from, + // orElse: () => {}) as Map; + // + // if (info.isEmpty) { + // // tried to rename a non existing wallet + // Logging.instance + // .log("Tried to rename a non existing wallet!", level: LogLevel.Error); + // return false; + // } + // + // if (from != to && + // (walletInfo.values.firstWhere((element) => element['name'] == to, + // orElse: () => {}) as Map) + // .isNotEmpty) { + // // name already exists + // Logging.instance.log("wallet with name \"$to\" already exists!", + // level: LogLevel.Error); + // return false; + // } + // + // info["name"] = to; + // walletInfo[info['id']] = info; + // + // await DB.instance.put( + // boxName: DB.boxNameAllWalletsData, key: 'names', value: walletInfo); + // await refreshWallets(shouldNotifyListeners); + // return true; + // } + + // Future> _fetchWalletNames() async { + // final names = DB.instance + // .get(boxName: DB.boxNameAllWalletsData, key: 'names') as Map?; + // if (names == null) { + // Logging.instance.log( + // "Fetched wallet 'names' returned null. Setting initializing 'names'", + // level: LogLevel.Info); + // await DB.instance.put( + // boxName: DB.boxNameAllWalletsData, + // key: 'names', + // value: {}); + // return {}; + // } + // Logging.instance.log("Fetched wallet names: $names", level: LogLevel.Info); + // final mapped = Map.from(names); + // mapped.removeWhere((name, dyn) { + // final jsonObject = Map.from(dyn as Map); + // try { + // Coin.values.byName(jsonObject["coin"] as String); + // return false; + // } catch (e, s) { + // Logging.instance.log("Error, ${jsonObject["coin"]} does not exist", + // level: LogLevel.Error); + // return true; + // } + // }); + // + // return mapped.map((name, dyn) => MapEntry( + // name, WalletInfo.fromJson(Map.from(dyn as Map)))); + // } + + // Future addExistingStackWallet({ + // required String name, + // required String walletId, + // required Coin coin, + // required bool shouldNotifyListeners, + // }) async { + // final _names = DB.instance + // .get(boxName: DB.boxNameAllWalletsData, key: 'names') as Map?; + // + // Map names; + // if (_names == null) { + // names = {}; + // } else { + // names = Map.from(_names); + // } + // + // if (names.keys.contains(walletId)) { + // throw Exception("Wallet with walletId \"$walletId\" already exists!"); + // } + // if (names.values.where((element) => element['name'] == name).isNotEmpty) { + // throw Exception("Wallet with name \"$name\" already exists!"); + // } + // + // names[walletId] = { + // "id": walletId, + // "coin": coin.name, + // "name": name, + // }; + // + // await DB.instance.put( + // boxName: DB.boxNameAllWalletsData, key: 'names', value: names); + // await DB.instance.put( + // boxName: DB.boxNameAllWalletsData, + // key: "${walletId}_cryptoCurrency", + // value: coin.name); + // await DB.instance.put( + // boxName: DB.boxNameAllWalletsData, + // key: "${walletId}_mnemonicHasBeenVerified", + // value: false); + // await DB.instance.addWalletBox(walletId: walletId); + // await refreshWallets(shouldNotifyListeners); + // } + + // /// returns the new walletId if successful, otherwise null + // Future addNewWallet({ + // required String name, + // required Coin coin, + // required bool shouldNotifyListeners, + // }) async { + // final _names = DB.instance + // .get(boxName: DB.boxNameAllWalletsData, key: 'names') as Map?; + // + // Map names; + // if (_names == null) { + // names = {}; + // } else { + // names = Map.from(_names); + // } + // + // // Prevent overwriting or storing empty names + // if (name.isEmpty || + // names.values.where((element) => element['name'] == name).isNotEmpty) { + // return null; + // } + // + // final id = const Uuid().v1(); + // names[id] = { + // "id": id, + // "coin": coin.name, + // "name": name, + // }; + // + // await DB.instance.put( + // boxName: DB.boxNameAllWalletsData, key: 'names', value: names); + // await DB.instance.put( + // boxName: DB.boxNameAllWalletsData, + // key: "${id}_cryptoCurrency", + // value: coin.name); + // await DB.instance.put( + // boxName: DB.boxNameAllWalletsData, + // key: "${id}_mnemonicHasBeenVerified", + // value: false); + // await DB.instance.addWalletBox(walletId: id); + // await refreshWallets(shouldNotifyListeners); + // return id; + // } + + // Future> getFavoriteWalletIds() async { + // return DB.instance + // .values(boxName: DB.boxNameFavoriteWallets) + // .toList(); + // } + + // Future saveFavoriteWalletIds(List walletIds) async { + // await DB.instance.deleteAll(boxName: DB.boxNameFavoriteWallets); + // await DB.instance + // .addAll(boxName: DB.boxNameFavoriteWallets, values: walletIds); + // debugPrint("saveFavoriteWalletIds list: $walletIds"); + // } + // + // Future addFavorite(String walletId) async { + // final list = await getFavoriteWalletIds(); + // if (!list.contains(walletId)) { + // list.add(walletId); + // } + // await saveFavoriteWalletIds(list); + // } + // + // Future removeFavorite(String walletId) async { + // final list = await getFavoriteWalletIds(); + // list.remove(walletId); + // await saveFavoriteWalletIds(list); + // } + // + // Future moveFavorite({ + // required int fromIndex, + // required int toIndex, + // }) async { + // final list = await getFavoriteWalletIds(); + // if (fromIndex < toIndex) { + // toIndex -= 1; + // } + // final walletId = list.removeAt(fromIndex); + // list.insert(toIndex, walletId); + // await saveFavoriteWalletIds(list); + // } + // + // Future checkForDuplicate(String name) async { + // final names = DB.instance + // .get(boxName: DB.boxNameAllWalletsData, key: 'names') as Map?; + // if (names == null) return false; + // + // return names.values.where((element) => element['name'] == name).isNotEmpty; + // } + + Future getWalletId(String walletName) async { + final names = DB.instance + .get(boxName: DB.boxNameAllWalletsData, key: 'names') as Map; + final shells = + names.values.where((element) => element['name'] == walletName); + if (shells.isEmpty) { + return null; + } + return shells.first["id"] as String; + } + + // Future isMnemonicVerified({required String walletId}) async { + // final isVerified = DB.instance.get( + // boxName: DB.boxNameAllWalletsData, + // key: "${walletId}_mnemonicHasBeenVerified") as bool?; + // + // if (isVerified == null) { + // Logging.instance.log( + // "isMnemonicVerified(walletId: $walletId) returned null which should never happen!", + // level: LogLevel.Error, + // ); + // throw Exception( + // "isMnemonicVerified(walletId: $walletId) returned null which should never happen!"); + // } else { + // return isVerified; + // } + // } + // + // Future setMnemonicVerified({required String walletId}) async { + // final isVerified = DB.instance.get( + // boxName: DB.boxNameAllWalletsData, + // key: "${walletId}_mnemonicHasBeenVerified") as bool?; + // + // if (isVerified == null) { + // Logging.instance.log( + // "setMnemonicVerified(walletId: $walletId) tried running on non existent wallet!", + // level: LogLevel.Error, + // ); + // throw Exception( + // "setMnemonicVerified(walletId: $walletId) tried running on non existent wallet!"); + // } else if (isVerified) { + // Logging.instance.log( + // "setMnemonicVerified(walletId: $walletId) tried running on already verified wallet!", + // level: LogLevel.Error, + // ); + // throw Exception( + // "setMnemonicVerified(walletId: $walletId) tried running on already verified wallet!"); + // } else { + // await DB.instance.put( + // boxName: DB.boxNameAllWalletsData, + // key: "${walletId}_mnemonicHasBeenVerified", + // value: true); + // Logging.instance.log( + // "setMnemonicVerified(walletId: $walletId) successful", + // level: LogLevel.Error, + // ); + // } + // } + // + // // pin + mnemonic as well as anything else in secureStore + // Future deleteWallet(String name, bool shouldNotifyListeners) async { + // final names = DB.instance.get( + // boxName: DB.boxNameAllWalletsData, key: 'names') as Map? ?? + // {}; + // + // final walletId = await getWalletId(name); + // if (walletId == null) { + // return 3; + // } + // + // Logging.instance.log( + // "deleteWallet called with name=$name and id=$walletId", + // level: LogLevel.Warning, + // ); + // + // final shell = names.remove(walletId); + // + // if (shell == null) { + // return 0; + // } + // + // // TODO delete derivations!!! + // await _secureStore.delete(key: "${walletId}_pin"); + // await _secureStore.delete(key: "${walletId}_mnemonic"); + // + // await DB.instance.delete( + // boxName: DB.boxNameAllWalletsData, key: "${walletId}_cryptoCurrency"); + // await DB.instance.delete( + // boxName: DB.boxNameAllWalletsData, + // key: "${walletId}_mnemonicHasBeenVerified"); + // if (coinFromPrettyName(shell['coin'] as String) == Coin.wownero) { + // final wowService = + // wownero.createWowneroWalletService(DB.instance.moneroWalletInfoBox); + // await wowService.remove(walletId); + // Logging.instance + // .log("monero wallet: $walletId deleted", level: LogLevel.Info); + // } else if (coinFromPrettyName(shell['coin'] as String) == Coin.monero) { + // final xmrService = + // monero.createMoneroWalletService(DB.instance.moneroWalletInfoBox); + // await xmrService.remove(walletId); + // Logging.instance + // .log("monero wallet: $walletId deleted", level: LogLevel.Info); + // } else if (coinFromPrettyName(shell['coin'] as String) == Coin.epicCash) { + // final deleteResult = + // await deleteEpicWallet(walletId: walletId, secureStore: _secureStore); + // Logging.instance.log( + // "epic wallet: $walletId deleted with result: $deleteResult", + // level: LogLevel.Info); + // } + // + // // box data may currently still be read/written to if wallet was refreshing + // // when delete was requested so instead of deleting now we mark the wallet + // // as needs delete by adding it's id to a list which gets checked on app start + // await DB.instance.add( + // boxName: DB.boxNameWalletsToDeleteOnStart, value: walletId); + // + // final lookupService = TradeSentFromStackService(); + // for (final lookup in lookupService.all) { + // if (lookup.walletIds.contains(walletId)) { + // // update lookup data to reflect deleted wallet + // await lookupService.save( + // tradeWalletLookup: lookup.copyWith( + // walletIds: lookup.walletIds.where((id) => id != walletId).toList(), + // ), + // ); + // } + // } + // + // // delete notifications tied to deleted wallet + // for (final notification in NotificationsService.instance.notifications) { + // if (notification.walletId == walletId) { + // await NotificationsService.instance.delete(notification, false); + // } + // } + // + // if (names.isEmpty) { + // await DB.instance.deleteAll(boxName: DB.boxNameAllWalletsData); + // _walletNames = Future(() => {}); + // notifyListeners(); + // return 2; // error code no wallets on device + // } + // + // await DB.instance.put( + // boxName: DB.boxNameAllWalletsData, key: 'names', value: names); + // await refreshWallets(shouldNotifyListeners); + // return 0; + // } + // + // Future refreshWallets(bool shouldNotifyListeners) async { + // final newNames = await _fetchWalletNames(); + // _walletNames = Future(() => newNames); + // if (shouldNotifyListeners) notifyListeners(); + // } +} diff --git a/lib/services/trade_notes_service.dart b/lib/services/trade_notes_service.dart index 763566736..ba5226001 100644 --- a/lib/services/trade_notes_service.dart +++ b/lib/services/trade_notes_service.dart @@ -1,5 +1,5 @@ import 'package:flutter/material.dart'; -import 'package:stackwallet/hive/db.dart'; +import 'package:stackwallet/db/hive/db.dart'; class TradeNotesService extends ChangeNotifier { Map get all { diff --git a/lib/services/trade_sent_from_stack_service.dart b/lib/services/trade_sent_from_stack_service.dart index 88c7d7602..20d2b0ffb 100644 --- a/lib/services/trade_sent_from_stack_service.dart +++ b/lib/services/trade_sent_from_stack_service.dart @@ -1,5 +1,5 @@ import 'package:flutter/material.dart'; -import 'package:stackwallet/hive/db.dart'; +import 'package:stackwallet/db/hive/db.dart'; import 'package:stackwallet/models/trade_wallet_lookup.dart'; class TradeSentFromStackService extends ChangeNotifier { diff --git a/lib/services/trade_service.dart b/lib/services/trade_service.dart index abdcebb4b..7d1a3e00c 100644 --- a/lib/services/trade_service.dart +++ b/lib/services/trade_service.dart @@ -1,5 +1,5 @@ import 'package:flutter/cupertino.dart'; -import 'package:stackwallet/hive/db.dart'; +import 'package:stackwallet/db/hive/db.dart'; import 'package:stackwallet/models/exchange/response_objects/trade.dart'; class TradesService extends ChangeNotifier { diff --git a/lib/services/transaction_notification_tracker.dart b/lib/services/transaction_notification_tracker.dart index 6a2cce2c7..732c4a110 100644 --- a/lib/services/transaction_notification_tracker.dart +++ b/lib/services/transaction_notification_tracker.dart @@ -1,4 +1,4 @@ -import 'package:stackwallet/hive/db.dart'; +import 'package:stackwallet/db/hive/db.dart'; class TransactionNotificationTracker { final String walletId; diff --git a/lib/services/wallets.dart b/lib/services/wallets.dart index 4d5c07fb9..ab3886015 100644 --- a/lib/services/wallets.dart +++ b/lib/services/wallets.dart @@ -1,6 +1,6 @@ import 'package:flutter/foundation.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:stackwallet/hive/db.dart'; +import 'package:stackwallet/db/hive/db.dart'; import 'package:stackwallet/models/node_model.dart'; import 'package:stackwallet/services/coins/coin_service.dart'; import 'package:stackwallet/services/coins/manager.dart'; diff --git a/lib/services/wallets_service.dart b/lib/services/wallets_service.dart index 3a9c99346..cbba6bf5a 100644 --- a/lib/services/wallets_service.dart +++ b/lib/services/wallets_service.dart @@ -3,8 +3,8 @@ import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:flutter_libmonero/monero/monero.dart'; import 'package:flutter_libmonero/wownero/wownero.dart'; -import 'package:stackwallet/db/main_db.dart'; -import 'package:stackwallet/hive/db.dart'; +import 'package:stackwallet/db/hive/db.dart'; +import 'package:stackwallet/db/isar/main_db.dart'; import 'package:stackwallet/services/coins/epiccash/epiccash_wallet.dart'; import 'package:stackwallet/services/notifications_service.dart'; import 'package:stackwallet/services/trade_sent_from_stack_service.dart'; @@ -140,6 +140,29 @@ class WalletsService extends ChangeNotifier { name, WalletInfo.fromJson(Map.from(dyn as Map)))); } + Map fetchWalletsData() { + final names = DB.instance.get( + boxName: DB.boxNameAllWalletsData, key: 'names') as Map? ?? + {}; + + Logging.instance.log("Fetched wallet names: $names", level: LogLevel.Info); + final mapped = Map.from(names); + mapped.removeWhere((name, dyn) { + final jsonObject = Map.from(dyn as Map); + try { + Coin.values.byName(jsonObject["coin"] as String); + return false; + } catch (e, s) { + Logging.instance.log("Error, ${jsonObject["coin"]} does not exist", + level: LogLevel.Error); + return true; + } + }); + + return mapped.map((name, dyn) => MapEntry( + name, WalletInfo.fromJson(Map.from(dyn as Map)))); + } + Future addExistingStackWallet({ required String name, required String walletId, diff --git a/lib/utilities/address_utils.dart b/lib/utilities/address_utils.dart index c5c4ae39b..0006110c9 100644 --- a/lib/utilities/address_utils.dart +++ b/lib/utilities/address_utils.dart @@ -57,6 +57,8 @@ class AddressUtils { return Address.validateAddress(address, dogecoin); case Coin.epicCash: return validateSendAddress(address) == "1"; + case Coin.ethereum: + return true; //TODO - validate ETH address case Coin.firo: return Address.validateAddress(address, firoNetwork); case Coin.monero: diff --git a/lib/utilities/amount/amount.dart b/lib/utilities/amount/amount.dart new file mode 100644 index 000000000..30619bb0d --- /dev/null +++ b/lib/utilities/amount/amount.dart @@ -0,0 +1,172 @@ +import 'dart:convert'; + +import 'package:decimal/decimal.dart'; +import 'package:intl/number_symbols.dart'; +import 'package:intl/number_symbols_data.dart'; + +class Amount { + Amount({ + required BigInt rawValue, + required this.fractionDigits, + }) : assert(fractionDigits >= 0), + _value = rawValue; + + /// special zero case with [fractionDigits] set to 0 + static Amount get zero => Amount( + rawValue: BigInt.zero, + fractionDigits: 0, + ); + + /// truncate decimal value to [fractionDigits] places + Amount.fromDecimal(Decimal amount, {required this.fractionDigits}) + : assert(fractionDigits >= 0), + _value = amount.shift(fractionDigits).toBigInt(); + + // =========================================================================== + // ======= Instance properties =============================================== + + final int fractionDigits; + final BigInt _value; + + // =========================================================================== + // ======= Getters =========================================================== + + /// raw base value + BigInt get raw => _value; + + /// actual decimal vale represented + Decimal get decimal => Decimal.fromBigInt(raw).shift(-1 * fractionDigits); + + /// convenience getter + @Deprecated("provided for convenience only. Use fractionDigits instead.") + int get decimals => fractionDigits; + + // =========================================================================== + // ======= Serialization ===================================================== + + Map toMap() { + return {"raw": raw.toString(), "fractionDigits": fractionDigits}; + } + + String toJsonString() { + return jsonEncode(toMap()); + } + + String localizedStringAsFixed({ + required String locale, + int? decimalPlaces, + }) { + decimalPlaces ??= fractionDigits; + assert(decimalPlaces >= 0); + + final wholeNumber = decimal.truncate(); + + if (decimalPlaces == 0) { + return wholeNumber.toStringAsFixed(0); + } + + final String separator = + (numberFormatSymbols[locale] as NumberSymbols?)?.DECIMAL_SEP ?? + (numberFormatSymbols[locale.substring(0, 2)] as NumberSymbols?) + ?.DECIMAL_SEP ?? + "."; + + final fraction = decimal - wholeNumber; + + return "${wholeNumber.toStringAsFixed(0)}$separator${fraction.toStringAsFixed(decimalPlaces).substring(2)}"; + } + + // =========================================================================== + // ======= Deserialization =================================================== + + static Amount fromSerializedJsonString(String json) { + final map = jsonDecode(json) as Map; + return Amount( + rawValue: BigInt.parse(map["raw"] as String), + fractionDigits: map["fractionDigits"] as int, + ); + } + + // =========================================================================== + // ======= operators ========================================================= + + bool operator >(Amount other) => decimal > other.decimal; + + bool operator <(Amount other) => decimal < other.decimal; + + bool operator >=(Amount other) => decimal >= other.decimal; + + bool operator <=(Amount other) => decimal <= other.decimal; + + Amount operator +(Amount other) { + if (fractionDigits != other.fractionDigits) { + throw ArgumentError( + "fractionDigits do not match: this=$this, other=$other"); + } + return Amount( + rawValue: raw + other.raw, + fractionDigits: fractionDigits, + ); + } + + Amount operator -(Amount other) { + if (fractionDigits != other.fractionDigits) { + throw ArgumentError( + "fractionDigits do not match: this=$this, other=$other"); + } + return Amount( + rawValue: raw - other.raw, + fractionDigits: fractionDigits, + ); + } + + Amount operator *(Amount other) { + if (fractionDigits != other.fractionDigits) { + throw ArgumentError( + "fractionDigits do not match: this=$this, other=$other"); + } + return Amount( + rawValue: raw * other.raw, + fractionDigits: fractionDigits, + ); + } + + // =========================================================================== + // ======= Overrides ========================================================= + + @override + String toString() => "Amount($raw, $fractionDigits)"; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is Amount && + runtimeType == other.runtimeType && + raw == other.raw && + fractionDigits == other.fractionDigits; + + @override + int get hashCode => Object.hashAll([raw, fractionDigits]); +} + +// ============================================================================= +// ============================================================================= +// ======= Extensions ========================================================== + +extension DecimalAmountExt on Decimal { + Amount toAmount({required int fractionDigits}) { + return Amount.fromDecimal( + this, + fractionDigits: fractionDigits, + ); + } +} + +extension IntAmountExtension on int { + Amount toAmountAsRaw({required int fractionDigits}) { + return Amount( + rawValue: BigInt.from(this), + fractionDigits: fractionDigits, + ); + } +} diff --git a/lib/utilities/amount/amount_unit.dart b/lib/utilities/amount/amount_unit.dart new file mode 100644 index 000000000..2dd64202c --- /dev/null +++ b/lib/utilities/amount/amount_unit.dart @@ -0,0 +1,120 @@ +import 'dart:math' as math; + +import 'package:decimal/decimal.dart'; +import 'package:intl/number_symbols.dart'; +import 'package:intl/number_symbols_data.dart'; +import 'package:stackwallet/utilities/amount/amount.dart'; +import 'package:stackwallet/utilities/enums/coin_enum.dart'; + +enum AmountUnit { + normal(0), + milli(3), + micro(6), + nano(9), + pico(12), + femto(15), + atto(18), + ; + + const AmountUnit(this.shift); + final int shift; +} + +extension AmountUnitExt on AmountUnit { + String unitForCoin(Coin coin) { + switch (this) { + case AmountUnit.normal: + return coin.ticker; + case AmountUnit.milli: + return "m${coin.ticker}"; + case AmountUnit.micro: + return "µ${coin.ticker}"; + case AmountUnit.nano: + if (coin == Coin.ethereum) { + return "gwei"; + } else if (coin == Coin.wownero || coin == Coin.monero) { + return "n${coin.ticker}"; + } else { + return "sats"; + } + case AmountUnit.pico: + if (coin == Coin.ethereum) { + return "mwei"; + } else if (coin == Coin.wownero || coin == Coin.monero) { + return "p${coin.ticker}"; + } else { + return "invalid"; + } + case AmountUnit.femto: + if (coin == Coin.ethereum) { + return "kwei"; + } else { + return "invalid"; + } + case AmountUnit.atto: + if (coin == Coin.ethereum) { + return "wei"; + } else { + return "invalid"; + } + } + } + + String displayAmount({ + required Amount amount, + required String locale, + required Coin coin, + required int maxDecimalPlaces, + }) { + assert(maxDecimalPlaces >= 0); + // ensure we don't shift past minimum atomic value + final realShift = math.min(shift, amount.fractionDigits); + + // shifted to unit + final Decimal shifted = amount.decimal.shift(realShift); + + // get shift int value without fractional value + final BigInt wholeNumber = shifted.toBigInt(); + + // get decimal places to display + final int places = math.max(0, amount.fractionDigits - realShift); + + // start building the return value with just the whole value + String returnValue = wholeNumber.toString(); + + // if any decimal places should be shown continue building the return value + if (places > 0) { + // get the fractional value + final Decimal fraction = shifted - shifted.truncate(); + + // get final decimal based on max precision wanted + final int actualDecimalPlaces = math.min(places, maxDecimalPlaces); + + // get remainder string without the prepending "0." + String remainder = fraction.toString().substring(2); + + if (remainder.length > actualDecimalPlaces) { + // trim unwanted trailing digits + remainder = remainder.substring(0, actualDecimalPlaces); + } else if (remainder.length < actualDecimalPlaces) { + // pad with zeros to achieve requested precision + for (int i = remainder.length; i < actualDecimalPlaces; i++) { + remainder += "0"; + } + } + + // get decimal separator based on locale + final String separator = + (numberFormatSymbols[locale] as NumberSymbols?)?.DECIMAL_SEP ?? + (numberFormatSymbols[locale.substring(0, 2)] as NumberSymbols?) + ?.DECIMAL_SEP ?? + "."; + + // append separator and fractional amount + returnValue += "$separator$remainder"; + } + + // return the value with the proper unit symbol + return "$returnValue ${unitForCoin(coin)}"; + } +} diff --git a/lib/utilities/assets.dart b/lib/utilities/assets.dart index 4863bc7a0..c1401b8cd 100644 --- a/lib/utilities/assets.dart +++ b/lib/utilities/assets.dart @@ -272,6 +272,10 @@ class _SVG { String get whirlPool => "assets/svg/whirlpool.svg"; String get fingerprint => "assets/svg/fingerprint.svg"; String get faceId => "assets/svg/faceid.svg"; + String get tokens => "assets/svg/tokens.svg"; + String get circlePlusDark => "assets/svg/circle-plus.svg"; + String get creditCard => "assets/svg/cc.svg"; + String get ellipse1 => "assets/svg/Ellipse-43.svg"; String get ellipse2 => "assets/svg/Ellipse-42.svg"; String get chevronRight => "assets/svg/chevron-right.svg"; @@ -295,12 +299,15 @@ class _SVG { String get bitcoincash => "assets/svg/coin_icons/Bitcoincash.svg"; String get dogecoin => "assets/svg/coin_icons/Dogecoin.svg"; String get epicCash => "assets/svg/coin_icons/EpicCash.svg"; + String get ethereum => "assets/svg/coin_icons/Ethereum.svg"; String get firo => "assets/svg/coin_icons/Firo.svg"; String get monero => "assets/svg/coin_icons/Monero.svg"; String get wownero => "assets/svg/coin_icons/Wownero.svg"; String get namecoin => "assets/svg/coin_icons/Namecoin.svg"; String get particl => "assets/svg/coin_icons/Particl.svg"; + String get bnbIcon => "assets/svg/coin_icons/bnb_icon.svg"; + String iconFor({required Coin coin}) { switch (coin) { case Coin.bitcoin: @@ -314,6 +321,8 @@ class _SVG { return dogecoin; case Coin.epicCash: return epicCash; + case Coin.ethereum: + return ethereum; case Coin.firo: return firo; case Coin.monero: @@ -342,6 +351,8 @@ class _SVG { String dogecoinImage(BuildContext context) => "${_path(context)}/doge.svg"; String epicCashImage(BuildContext context) => "${_path(context)}/epic-cash.svg"; + String ethereumImage(BuildContext context) => + "${_path(context)}/ethereum.svg"; String firoImage(BuildContext context) => "${_path(context)}/firo.svg"; String litecoinImage(BuildContext context) => "${_path(context)}/litecoin.svg"; @@ -382,6 +393,8 @@ class _SVG { return firoImage(context); case Coin.dogecoinTestNet: return dogecoinImage(context); + case Coin.ethereum: + return ethereumImage(context); } } } diff --git a/lib/utilities/block_explorers.dart b/lib/utilities/block_explorers.dart index 4b406b704..deffb5311 100644 --- a/lib/utilities/block_explorers.dart +++ b/lib/utilities/block_explorers.dart @@ -20,6 +20,8 @@ Uri getBlockExplorerTransactionUrlFor({ case Coin.epicCash: // TODO: Handle this case. throw UnimplementedError("missing block explorer for epic cash"); + case Coin.ethereum: + return Uri.parse("https://etherscan.io/tx/$txid"); case Coin.monero: return Uri.parse("https://xmrchain.net/tx/$txid"); case Coin.wownero: diff --git a/lib/utilities/constants.dart b/lib/utilities/constants.dart index 55569a6d3..92e99849d 100644 --- a/lib/utilities/constants.dart +++ b/lib/utilities/constants.dart @@ -25,13 +25,14 @@ abstract class Constants { // static bool enableBuy = enableExchange; // // true; // true for development, - //TODO: correct for monero? + static const int _satsPerCoinEthereum = 1000000000000000000; static const int _satsPerCoinMonero = 1000000000000; static const int _satsPerCoinWownero = 100000000000; static const int _satsPerCoin = 100000000; static const int _decimalPlaces = 8; static const int _decimalPlacesWownero = 11; static const int _decimalPlacesMonero = 12; + static const int _decimalPlacesEthereum = 18; static const int notificationsMax = 0xFFFFFFFF; static const Duration networkAliveTimerDuration = Duration(seconds: 10); @@ -41,7 +42,7 @@ abstract class Constants { // Enable Logger.print statements static const bool disableLogger = false; - static const int currentHiveDbVersion = 7; + static const int currentHiveDbVersion = 8; static const int rescanV1 = 1; @@ -67,6 +68,9 @@ abstract class Constants { case Coin.monero: return _satsPerCoinMonero; + + case Coin.ethereum: + return _satsPerCoinEthereum; } } @@ -92,6 +96,9 @@ abstract class Constants { case Coin.monero: return _decimalPlacesMonero; + + case Coin.ethereum: + return _decimalPlacesEthereum; } } @@ -109,6 +116,7 @@ abstract class Constants { case Coin.dogecoinTestNet: case Coin.firoTestNet: case Coin.epicCash: + case Coin.ethereum: case Coin.namecoin: case Coin.particl: values.addAll([24, 21, 18, 15, 12]); @@ -150,6 +158,9 @@ abstract class Constants { case Coin.epicCash: return 60; + case Coin.ethereum: + return 15; + case Coin.monero: return 120; diff --git a/lib/utilities/db_version_migration.dart b/lib/utilities/db_version_migration.dart index aaa40b45a..24e8b8582 100644 --- a/lib/utilities/db_version_migration.dart +++ b/lib/utilities/db_version_migration.dart @@ -1,8 +1,8 @@ import 'package:hive/hive.dart'; import 'package:isar/isar.dart'; -import 'package:stackwallet/db/main_db.dart'; +import 'package:stackwallet/db/hive/db.dart'; +import 'package:stackwallet/db/isar/main_db.dart'; import 'package:stackwallet/electrumx_rpc/electrumx.dart'; -import 'package:stackwallet/hive/db.dart'; import 'package:stackwallet/models/exchange/change_now/exchange_transaction.dart'; import 'package:stackwallet/models/exchange/response_objects/trade.dart'; import 'package:stackwallet/models/isar/models/blockchain_data/address.dart'; @@ -12,6 +12,7 @@ import 'package:stackwallet/models/node_model.dart'; import 'package:stackwallet/services/mixins/wallet_db.dart'; import 'package:stackwallet/services/node_service.dart'; import 'package:stackwallet/services/wallets_service.dart'; +import 'package:stackwallet/utilities/amount/amount.dart'; import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/default_nodes.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; @@ -278,6 +279,17 @@ class DbVersionMigrator with WalletDB { // try to continue migrating return await migrate(7, secureStore: secureStore); + case 7: + // migrate + await _v7(secureStore); + + // update version + await DB.instance.put( + boxName: DB.boxNameDBInfo, key: "hive_data_version", value: 8); + + // try to continue migrating + return await migrate(8, secureStore: secureStore); + default: // finally return return; @@ -327,12 +339,17 @@ class DbVersionMigrator with WalletDB { : isar_models.TransactionType.outgoing, subType: isar_models.TransactionSubType.none, amount: tx.amount, + amountString: Amount( + rawValue: BigInt.from(tx.amount), + fractionDigits: info.coin.decimals, + ).toJsonString(), fee: tx.fees, height: tx.height, isCancelled: tx.isCancelled, isLelantus: false, slateId: tx.slateId, otherData: tx.otherData, + nonce: null, inputs: [], outputs: [], ); @@ -391,4 +408,43 @@ class DbVersionMigrator with WalletDB { } } } + + Future _v7(SecureStorageInterface secureStore) async { + await Hive.openBox(DB.boxNameAllWalletsData); + final walletsService = WalletsService(secureStorageInterface: secureStore); + final walletInfoList = await walletsService.walletNames; + await MainDB.instance.initMainDB(); + + for (final walletId in walletInfoList.keys) { + final info = walletInfoList[walletId]!; + assert(info.walletId == walletId); + + final count = await MainDB.instance.getTransactions(walletId).count(); + + for (var i = 0; i < count; i += 50) { + final txns = await MainDB.instance + .getTransactions(walletId) + .offset(i) + .limit(50) + .findAll(); + + // migrate amount to serialized amount string + final txnsData = txns + .map( + (tx) => Tuple2( + tx + ..amountString = Amount( + rawValue: BigInt.from(tx.amount), + fractionDigits: info.coin.decimals, + ).toJsonString(), + tx.address.value, + ), + ) + .toList(); + + // update db records + await MainDB.instance.addNewTransactionData(txnsData, walletId); + } + } + } } diff --git a/lib/utilities/default_eth_tokens.dart b/lib/utilities/default_eth_tokens.dart new file mode 100644 index 000000000..a13a890f5 --- /dev/null +++ b/lib/utilities/default_eth_tokens.dart @@ -0,0 +1,55 @@ +import 'package:stackwallet/models/isar/models/ethereum/eth_contract.dart'; + +abstract class DefaultTokens { + static List list = [ + EthContract( + address: "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", + name: "USD Coin", + symbol: "USDC", + decimals: 6, + type: EthContractType.erc20, + ), + EthContract( + address: "0xdac17f958d2ee523a2206206994597c13d831ec7", + name: "Tether", + symbol: "USDT", + decimals: 6, + type: EthContractType.erc20, + ), + EthContract( + address: "0x95ad61b0a150d79219dcf64e1e6cc01f0b64c4ce", + name: "Shiba Inu", + symbol: "SHIB", + decimals: 18, + type: EthContractType.erc20, + ), + EthContract( + address: "0xB8c77482e45F1F44dE1745F52C74426C631bDD52", + name: "BNB Token", + symbol: "BNB", + decimals: 18, + type: EthContractType.erc20, + ), + EthContract( + address: "0x4Fabb145d64652a948d72533023f6E7A623C7C53", + name: "BUSD", + symbol: "BUSD", + decimals: 18, + type: EthContractType.erc20, + ), + EthContract( + address: "0x514910771af9ca656af840dff83e8264ecf986ca", + name: "Chainlink", + symbol: "LINK", + decimals: 18, + type: EthContractType.erc20, + ), + EthContract( + address: "0x1f9840a85d5af5bf1d1762f925bdaddc4201f984", + name: "Uniswap", + symbol: "UNI", + decimals: 18, + type: EthContractType.erc20, + ), + ]; +} diff --git a/lib/utilities/default_nodes.dart b/lib/utilities/default_nodes.dart index fc25fee24..785c5561b 100644 --- a/lib/utilities/default_nodes.dart +++ b/lib/utilities/default_nodes.dart @@ -1,5 +1,6 @@ import 'package:stackwallet/models/node_model.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; +// import 'package:web3dart/browser.dart'; abstract class DefaultNodes { static const String defaultNodeIdPrefix = "default_"; @@ -13,6 +14,7 @@ abstract class DefaultNodes { firo, monero, epicCash, + ethereum, bitcoincash, namecoin, wownero, @@ -134,6 +136,18 @@ abstract class DefaultNodes { isDown: false, ); + static NodeModel get ethereum => NodeModel( + host: "https://eth.stackwallet.com", + port: 443, + name: defaultName, + id: _nodeId(Coin.ethereum), + useSSL: true, + enabled: true, + coinName: Coin.ethereum.name, + isFailover: true, + isDown: false, + ); + static NodeModel get namecoin => NodeModel( host: "namecoin.stackwallet.com", port: 57002, @@ -222,6 +236,9 @@ abstract class DefaultNodes { case Coin.epicCash: return epicCash; + case Coin.ethereum: + return ethereum; + case Coin.firo: return firo; diff --git a/lib/utilities/delete_everything.dart b/lib/utilities/delete_everything.dart index f9a33dbaf..3c12fba85 100644 --- a/lib/utilities/delete_everything.dart +++ b/lib/utilities/delete_everything.dart @@ -1,4 +1,4 @@ -import 'package:stackwallet/hive/db.dart'; +import 'package:stackwallet/db/hive/db.dart'; import 'package:stackwallet/utilities/logger.dart'; Future deleteEverything() async { diff --git a/lib/utilities/desktop_password_service.dart b/lib/utilities/desktop_password_service.dart index 6263eb33b..dc32b8254 100644 --- a/lib/utilities/desktop_password_service.dart +++ b/lib/utilities/desktop_password_service.dart @@ -1,6 +1,6 @@ import 'package:hive/hive.dart'; import 'package:stack_wallet_backup/secure_storage.dart'; -import 'package:stackwallet/hive/db.dart'; +import 'package:stackwallet/db/hive/db.dart'; import 'package:stackwallet/utilities/logger.dart'; const String _kKeyBlobKey = "swbKeyBlobKeyStringID"; diff --git a/lib/utilities/enums/coin_enum.dart b/lib/utilities/enums/coin_enum.dart index 2cc7b3363..0490174f0 100644 --- a/lib/utilities/enums/coin_enum.dart +++ b/lib/utilities/enums/coin_enum.dart @@ -5,6 +5,8 @@ import 'package:stackwallet/services/coins/dogecoin/dogecoin_wallet.dart' as doge; import 'package:stackwallet/services/coins/epiccash/epiccash_wallet.dart' as epic; +import 'package:stackwallet/services/coins/ethereum/ethereum_wallet.dart' + as eth; import 'package:stackwallet/services/coins/firo/firo_wallet.dart' as firo; import 'package:stackwallet/services/coins/litecoin/litecoin_wallet.dart' as ltc; @@ -15,13 +17,13 @@ import 'package:stackwallet/services/coins/particl/particl_wallet.dart' as particl; import 'package:stackwallet/services/coins/wownero/wownero_wallet.dart' as wow; import 'package:stackwallet/utilities/constants.dart'; -import 'dart:io' show Platform; enum Coin { bitcoin, bitcoincash, dogecoin, epicCash, + ethereum, firo, litecoin, monero, @@ -57,6 +59,8 @@ extension CoinExt on Coin { return "Dogecoin"; case Coin.epicCash: return "Epic Cash"; + case Coin.ethereum: + return "Ethereum"; case Coin.firo: return "Firo"; case Coin.monero: @@ -92,6 +96,8 @@ extension CoinExt on Coin { return "DOGE"; case Coin.epicCash: return "EPIC"; + case Coin.ethereum: + return "ETH"; case Coin.firo: return "FIRO"; case Coin.monero: @@ -128,6 +134,8 @@ extension CoinExt on Coin { case Coin.epicCash: // TODO: is this actually the right one? return "epic"; + case Coin.ethereum: + return "ethereum"; case Coin.firo: return "firo"; case Coin.monero: @@ -168,6 +176,7 @@ extension CoinExt on Coin { return true; case Coin.epicCash: + case Coin.ethereum: case Coin.monero: case Coin.wownero: return false; @@ -180,6 +189,7 @@ extension CoinExt on Coin { case Coin.litecoin: case Coin.bitcoincash: case Coin.dogecoin: + case Coin.ethereum: return true; case Coin.firo: @@ -207,6 +217,7 @@ extension CoinExt on Coin { case Coin.namecoin: case Coin.particl: case Coin.epicCash: + case Coin.ethereum: case Coin.monero: case Coin.wownero: return false; @@ -230,6 +241,7 @@ extension CoinExt on Coin { case Coin.namecoin: case Coin.particl: case Coin.epicCash: + case Coin.ethereum: case Coin.monero: case Coin.wownero: return this; @@ -276,6 +288,9 @@ extension CoinExt on Coin { case Coin.epicCash: return epic.MINIMUM_CONFIRMATIONS; + case Coin.ethereum: + return eth.MINIMUM_CONFIRMATIONS; + case Coin.monero: return xmr.MINIMUM_CONFIRMATIONS; @@ -316,6 +331,10 @@ Coin coinFromPrettyName(String name) { case "epicCash": return Coin.epicCash; + case "Ethereum": + case "ethereum": + return Coin.ethereum; + case "Firo": case "firo": return Coin.firo; @@ -385,6 +404,8 @@ Coin coinFromTickerCaseInsensitive(String ticker) { return Coin.dogecoin; case "epic": return Coin.epicCash; + case "eth": + return Coin.ethereum; case "firo": return Coin.firo; case "xmr": diff --git a/lib/utilities/enums/derive_path_type_enum.dart b/lib/utilities/enums/derive_path_type_enum.dart index 53db94e96..8aa519727 100644 --- a/lib/utilities/enums/derive_path_type_enum.dart +++ b/lib/utilities/enums/derive_path_type_enum.dart @@ -5,6 +5,7 @@ enum DerivePathType { bch44, bip49, bip84, + eth, } extension DerivePathTypeExt on DerivePathType { @@ -26,6 +27,9 @@ extension DerivePathTypeExt on DerivePathType { case Coin.particl: return DerivePathType.bip84; + case Coin.ethereum: // TODO: do we need something here? + return DerivePathType.eth; + case Coin.epicCash: case Coin.monero: case Coin.wownero: diff --git a/lib/utilities/eth_commons.dart b/lib/utilities/eth_commons.dart new file mode 100644 index 000000000..9b74cdbfe --- /dev/null +++ b/lib/utilities/eth_commons.dart @@ -0,0 +1,80 @@ +import 'package:bip32/bip32.dart' as bip32; +import 'package:bip39/bip39.dart' as bip39; +import 'package:decimal/decimal.dart'; +import "package:hex/hex.dart"; +import 'package:stackwallet/utilities/amount/amount.dart'; +import 'package:stackwallet/utilities/constants.dart'; +import 'package:stackwallet/utilities/enums/coin_enum.dart'; + +class GasTracker { + final Decimal average; + final Decimal fast; + final Decimal slow; + + final int numberOfBlocksFast; + final int numberOfBlocksAverage; + final int numberOfBlocksSlow; + + final int timestamp; + + const GasTracker({ + required this.average, + required this.fast, + required this.slow, + required this.numberOfBlocksFast, + required this.numberOfBlocksAverage, + required this.numberOfBlocksSlow, + required this.timestamp, + }); + + factory GasTracker.fromJson(Map json) { + final targetTime = Constants.targetBlockTimeInSeconds(Coin.ethereum); + return GasTracker( + average: Decimal.parse(json["average"]["price"].toString()), + fast: Decimal.parse(json["fast"]["price"].toString()), + slow: Decimal.parse(json["slow"]["price"].toString()), + numberOfBlocksAverage: (json["average"]["time"] as int) ~/ targetTime, + numberOfBlocksFast: (json["fast"]["time"] as int) ~/ targetTime, + numberOfBlocksSlow: (json["slow"]["time"] as int) ~/ targetTime, + timestamp: json["timestamp"] as int, + ); + } +} + +const hdPathEthereum = "m/44'/60'/0'/0"; + +// equal to "0x${keccak256("Transfer(address,address,uint256)".toUint8ListFromUtf8).toHex}"; +const kTransferEventSignature = + "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef"; + +String getPrivateKey(String mnemonic, String mnemonicPassphrase) { + final isValidMnemonic = bip39.validateMnemonic(mnemonic); + if (!isValidMnemonic) { + throw 'Invalid mnemonic'; + } + + final seed = bip39.mnemonicToSeed(mnemonic, passphrase: mnemonicPassphrase); + final root = bip32.BIP32.fromSeed(seed); + const index = 0; + final addressAtIndex = root.derivePath("$hdPathEthereum/$index"); + + return HEX.encode(addressAtIndex.privateKey as List); +} + +Amount estimateFee(int feeRate, int gasLimit, int decimals) { + final gweiAmount = feeRate.toDecimal() / (Decimal.ten.pow(9).toDecimal()); + final fee = gasLimit.toDecimal() * + gweiAmount.toDecimal( + scaleOnInfinitePrecision: Coin.ethereum.decimals, + ); + + //Convert gwei to ETH + final feeInWei = fee * Decimal.ten.pow(9).toDecimal(); + final ethAmount = feeInWei / Decimal.ten.pow(decimals).toDecimal(); + return Amount.fromDecimal( + ethAmount.toDecimal( + scaleOnInfinitePrecision: Coin.ethereum.decimals, + ), + fractionDigits: decimals, + ); +} diff --git a/lib/utilities/extensions/extensions.dart b/lib/utilities/extensions/extensions.dart new file mode 100644 index 000000000..792ebe0b3 --- /dev/null +++ b/lib/utilities/extensions/extensions.dart @@ -0,0 +1,3 @@ +export 'impl/big_int.dart'; +export 'impl/string.dart'; +export 'impl/uint8_list.dart'; diff --git a/lib/utilities/extensions/impl/big_int.dart b/lib/utilities/extensions/impl/big_int.dart new file mode 100644 index 000000000..c9b78ab55 --- /dev/null +++ b/lib/utilities/extensions/impl/big_int.dart @@ -0,0 +1,33 @@ +import 'dart:typed_data'; + +extension BigIntExtensions on BigInt { + String get toHex { + if (this < BigInt.zero) { + throw Exception("BigInt value is negative"); + } + + final String hex = toRadixString(16); + if (hex.length % 2 == 0) { + return hex; + } else { + return "0$hex"; + } + } + + String get toHexUppercase => toHex.toUpperCase(); + + Uint8List get toBytes { + if (this < BigInt.zero) { + throw Exception("BigInt value is negative"); + } + BigInt number = this; + int bytes = (number.bitLength + 7) >> 3; + final b256 = BigInt.from(256); + final result = Uint8List(bytes); + for (int i = 0; i < bytes; i++) { + result[bytes - 1 - i] = number.remainder(b256).toInt(); + number = number >> 8; + } + return result; + } +} diff --git a/lib/utilities/extensions/impl/contract_abi.dart b/lib/utilities/extensions/impl/contract_abi.dart new file mode 100644 index 000000000..c5a2877d8 --- /dev/null +++ b/lib/utilities/extensions/impl/contract_abi.dart @@ -0,0 +1,117 @@ +import 'dart:convert'; + +import 'package:web3dart/web3dart.dart'; + +extension ContractAbiExtensions on ContractAbi { + static ContractAbi fromJsonList({ + required String name, + required String jsonList, + }) { + final List functions = []; + final List events = []; + + final list = List>.from(jsonDecode(jsonList) as List); + + for (final json in list) { + final type = json["type"] as String; + final name = json["name"] as String? ?? ""; + + if (type == "event") { + final anonymous = json["anonymous"] as bool? ?? false; + final List> components = []; + + for (final input in json["inputs"] as List) { + components.add( + EventComponent( + _parseParam(input as Map), + input['indexed'] as bool? ?? false, + ), + ); + } + + events.add(ContractEvent(anonymous, name, components)); + } else { + final mutability = _mutabilityNames[json['stateMutability']]; + final parsedType = _functionTypeNames[json['type']]; + if (parsedType != null) { + final inputs = _parseParams(json['inputs'] as List?); + final outputs = _parseParams(json['outputs'] as List?); + + functions.add( + ContractFunction( + name, + inputs, + outputs: outputs, + type: parsedType, + mutability: mutability ?? StateMutability.nonPayable, + ), + ); + } + } + } + + return ContractAbi(name, functions, events); + } + + static const Map _functionTypeNames = { + 'function': ContractFunctionType.function, + 'constructor': ContractFunctionType.constructor, + 'fallback': ContractFunctionType.fallback, + }; + + static const Map _mutabilityNames = { + 'pure': StateMutability.pure, + 'view': StateMutability.view, + 'nonpayable': StateMutability.nonPayable, + 'payable': StateMutability.payable, + }; + + static List> _parseParams(List? data) { + if (data == null || data.isEmpty) return []; + + final elements = >[]; + for (final entry in data) { + elements.add(_parseParam(entry as Map)); + } + + return elements; + } + + static FunctionParameter _parseParam(Map entry) { + final name = entry['name'] as String; + final typeName = entry['type'] as String; + + if (typeName.contains('tuple')) { + final components = entry['components'] as List; + return _parseTuple(name, typeName, _parseParams(components)); + } else { + final type = parseAbiType(entry['type'] as String); + return FunctionParameter(name, type); + } + } + + static CompositeFunctionParameter _parseTuple(String name, String typeName, + List> components) { + // The type will have the form tuple[3][]...[1], where the indices after the + // tuple indicate that the type is part of an array. + assert(RegExp(r'^tuple(?:\[\d*\])*$').hasMatch(typeName), + '$typeName is an invalid tuple type'); + + final arrayLengths = []; + var remainingName = typeName; + + while (remainingName != 'tuple') { + final arrayMatch = RegExp(r'^(.*)\[(\d*)\]$').firstMatch(remainingName)!; + remainingName = arrayMatch.group(1)!; + + final insideSquareBrackets = arrayMatch.group(2)!; + if (insideSquareBrackets.isEmpty) { + arrayLengths.insert(0, null); + } else { + arrayLengths.insert(0, int.parse(insideSquareBrackets)); + } + } + + return CompositeFunctionParameter(name, components, arrayLengths); + } +} diff --git a/lib/utilities/extensions/impl/string.dart b/lib/utilities/extensions/impl/string.dart new file mode 100644 index 000000000..e5021e3f1 --- /dev/null +++ b/lib/utilities/extensions/impl/string.dart @@ -0,0 +1,20 @@ +import 'dart:convert'; +import 'dart:typed_data'; + +import 'package:dart_bs58/dart_bs58.dart'; +import 'package:dart_bs58check/dart_bs58check.dart'; +import 'package:hex/hex.dart'; +import 'package:stackwallet/utilities/extensions/extensions.dart'; + +extension StringExtensions on String { + Uint8List get toUint8ListFromUtf8 => Uint8List.fromList(utf8.encode(this)); + + Uint8List get toUint8ListFromHex => + Uint8List.fromList(HEX.decode(startsWith("0x") ? substring(2) : this)); + + Uint8List get toUint8ListFromBase58Encoded => bs58.decode(this); + + Uint8List get toUint8ListFromBase58CheckEncoded => bs58check.decode(this); + + BigInt get toBigIntFromHex => toUint8ListFromHex.toBigInt; +} diff --git a/lib/utilities/extensions/impl/uint8_list.dart b/lib/utilities/extensions/impl/uint8_list.dart new file mode 100644 index 000000000..04980f91a --- /dev/null +++ b/lib/utilities/extensions/impl/uint8_list.dart @@ -0,0 +1,39 @@ +import 'dart:convert'; +import 'dart:typed_data'; + +import 'package:dart_bs58/dart_bs58.dart'; +import 'package:dart_bs58check/dart_bs58check.dart'; +import 'package:hex/hex.dart'; + +extension Uint8ListExtensions on Uint8List { + String get toUtf8String => utf8.decode(this); + + String get toHex { + return HEX.encode(this); + } + + String get toBase58Encoded { + return bs58.encode(this); + } + + String get toBase58CheckEncoded { + return bs58check.encode(this); + } + + /// returns copy of byte list in reverse order + Uint8List get reversed { + final reversed = Uint8List(length); + for (final byte in this) { + reversed.insert(0, byte); + } + return reversed; + } + + BigInt get toBigInt { + BigInt number = BigInt.zero; + for (final byte in this) { + number = (number << 8) | BigInt.from(byte & 0xff); + } + return number; + } +} diff --git a/lib/utilities/format.dart b/lib/utilities/format.dart index a0a6613a7..56d7059b7 100644 --- a/lib/utilities/format.dart +++ b/lib/utilities/format.dart @@ -1,41 +1,43 @@ import 'dart:typed_data'; -import 'package:decimal/decimal.dart'; -import 'package:intl/number_symbols.dart'; -import 'package:intl/number_symbols_data.dart' show numberFormatSymbols; import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/enums/backup_frequency_type.dart'; -import 'package:stackwallet/utilities/enums/coin_enum.dart'; abstract class Format { static String shorten(String value, int beginCount, int endCount) { return "${value.substring(0, beginCount)}...${value.substring(value.length - endCount)}"; } - static Decimal satoshisToAmount(int sats, {required Coin coin}) { - return (Decimal.fromInt(sats) / - Decimal.fromInt(Constants.satsPerCoin(coin))) - .toDecimal( - scaleOnInfinitePrecision: Constants.decimalPlacesForCoin(coin)); - } - - /// - static String satoshiAmountToPrettyString( - int sats, String locale, Coin coin) { - final amount = satoshisToAmount(sats, coin: coin); - return localizedStringAsFixed( - value: amount, - locale: locale, - decimalPlaces: Constants.decimalPlacesForCoin(coin), - ); - } - - static int decimalAmountToSatoshis(Decimal amount, Coin coin) { - final value = (Decimal.fromInt(Constants.satsPerCoin(coin)) * amount) - .floor() - .toBigInt(); - return value.toInt(); - } + // static Decimal satoshisToAmount(int sats, {required Coin coin}) { + // return (Decimal.fromInt(sats) / + // Decimal.fromInt(Constants.satsPerCoin(coin))) + // .toDecimal( + // scaleOnInfinitePrecision: Constants.decimalPlacesForCoin(coin)); + // } + // + // static Decimal satoshisToEthTokenAmount(int sats, int decimalPlaces) { + // return (Decimal.fromInt(sats) / + // Decimal.fromInt(pow(10, decimalPlaces).toInt())) + // .toDecimal(scaleOnInfinitePrecision: decimalPlaces); + // } + // + // /// + // static String satoshiAmountToPrettyString( + // int sats, String locale, Coin coin) { + // final amount = satoshisToAmount(sats, coin: coin); + // return localizedStringAsFixed( + // value: amount, + // locale: locale, + // decimalPlaces: Constants.decimalPlacesForCoin(coin), + // ); + // } + // + // static int decimalAmountToSatoshis(Decimal amount, Coin coin) { + // final value = (Decimal.fromInt(Constants.satsPerCoin(coin)) * amount) + // .floor() + // .toBigInt(); + // return value.toInt(); + // } // format date string from unix timestamp static String extractDateFrom(int timestamp, {bool localized = true}) { @@ -50,26 +52,26 @@ abstract class Format { return "${date.day} ${Constants.monthMapShort[date.month]} ${date.year}, ${date.hour}:$minutes"; } - static String localizedStringAsFixed({ - required Decimal value, - required String locale, - int decimalPlaces = 0, - }) { - assert(decimalPlaces >= 0); - - final String separator = - (numberFormatSymbols[locale] as NumberSymbols?)?.DECIMAL_SEP ?? - (numberFormatSymbols[locale.substring(0, 2)] as NumberSymbols?) - ?.DECIMAL_SEP ?? - "."; - - final intValue = value.truncate(); - final fraction = value - intValue; - - return intValue.toStringAsFixed(0) + - separator + - fraction.toStringAsFixed(decimalPlaces).substring(2); - } + // static String localizedStringAsFixed({ + // required Decimal value, + // required String locale, + // int decimalPlaces = 0, + // }) { + // assert(decimalPlaces >= 0); + // + // final String separator = + // (numberFormatSymbols[locale] as NumberSymbols?)?.DECIMAL_SEP ?? + // (numberFormatSymbols[locale.substring(0, 2)] as NumberSymbols?) + // ?.DECIMAL_SEP ?? + // "."; + // + // final intValue = value.truncate(); + // final fraction = value - intValue; + // + // return intValue.toStringAsFixed(0) + + // separator + + // fraction.toStringAsFixed(decimalPlaces).substring(2); + // } /// format date string as dd/mm/yy from DateTime object static String formatDate(DateTime date) { diff --git a/lib/utilities/prefs.dart b/lib/utilities/prefs.dart index 25388c6ff..7dd17e2ad 100644 --- a/lib/utilities/prefs.dart +++ b/lib/utilities/prefs.dart @@ -1,5 +1,5 @@ import 'package:flutter/cupertino.dart'; -import 'package:stackwallet/hive/db.dart'; +import 'package:stackwallet/db/hive/db.dart'; import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/enums/backup_frequency_type.dart'; import 'package:stackwallet/utilities/enums/languages_enum.dart'; diff --git a/lib/utilities/theme/color_theme.dart b/lib/utilities/theme/color_theme.dart index 9741d7326..eaa3d702b 100644 --- a/lib/utilities/theme/color_theme.dart +++ b/lib/utilities/theme/color_theme.dart @@ -299,6 +299,7 @@ class CoinThemeColor { Color get firo => const Color(0xFFFF897A); Color get dogecoin => const Color(0xFFFFE079); Color get epicCash => const Color(0xFFC5C7CB); + Color get ethereum => const Color(0xFFA7ADE9); Color get monero => const Color(0xFFFF9E6B); Color get namecoin => const Color(0xFF91B1E1); Color get wownero => const Color(0xFFED80C1); @@ -320,6 +321,8 @@ class CoinThemeColor { return dogecoin; case Coin.epicCash: return epicCash; + case Coin.ethereum: + return ethereum; case Coin.firo: case Coin.firoTestNet: return firo; diff --git a/lib/utilities/theme/forest_colors.dart b/lib/utilities/theme/forest_colors.dart index ff553f436..8d66befda 100644 --- a/lib/utilities/theme/forest_colors.dart +++ b/lib/utilities/theme/forest_colors.dart @@ -180,9 +180,9 @@ class ForestColors extends StackColorTheme { Color get bottomNavIconIcon => const Color(0xFF22867A); @override - Color get topNavIconPrimary => const Color(0xFF227386); + Color get topNavIconPrimary => accentColorDark; //const Color(0xFF227386); @override - Color get topNavIconGreen => const Color(0xFF00A591); + Color get topNavIconGreen => accentColorDark; //const Color(0xFF00A591); @override Color get topNavIconYellow => const Color(0xFFFDD33A); @override diff --git a/lib/utilities/theme/stack_colors.dart b/lib/utilities/theme/stack_colors.dart index 08614b8c5..cbba0bb36 100644 --- a/lib/utilities/theme/stack_colors.dart +++ b/lib/utilities/theme/stack_colors.dart @@ -1576,6 +1576,8 @@ class StackColors extends ThemeExtension { return _coin.dogecoin; case Coin.epicCash: return _coin.epicCash; + case Coin.ethereum: + return _coin.ethereum; case Coin.firo: case Coin.firoTestNet: return _coin.firo; diff --git a/lib/widgets/custom_tab_view.dart b/lib/widgets/custom_tab_view.dart new file mode 100644 index 000000000..49fd81d18 --- /dev/null +++ b/lib/widgets/custom_tab_view.dart @@ -0,0 +1,151 @@ +import 'package:flutter/material.dart'; +import 'package:stackwallet/utilities/text_styles.dart'; +import 'package:stackwallet/utilities/theme/stack_colors.dart'; + +class CustomTabView extends StatefulWidget { + const CustomTabView({ + Key? key, + required this.titles, + required this.children, + this.initialIndex = 0, + this.childPadding, + }) : assert(titles.length == children.length), + super(key: key); + + final List titles; + final List children; + final int initialIndex; + final EdgeInsets? childPadding; + + @override + State createState() => _CustomTabViewState(); +} + +class _CustomTabViewState extends State { + final _key = GlobalKey(); + late int _selectedIndex; + + static const duration = Duration(milliseconds: 250); + + @override + void initState() { + _selectedIndex = widget.initialIndex; + super.initState(); + } + + @override + Widget build(BuildContext context) { + return LayoutBuilder( + builder: (context, constraints) => Column( + mainAxisSize: MainAxisSize.min, + children: [ + Row( + children: [ + for (int i = 0; i < widget.titles.length; i++) + Expanded( + child: MouseRegion( + cursor: SystemMouseCursors.click, + child: GestureDetector( + onTap: () => setState(() => _selectedIndex = i), + child: Container( + color: Colors.transparent, + child: Column( + children: [ + const SizedBox( + height: 16, + ), + AnimatedCrossFade( + firstChild: Text( + widget.titles[i], + style: + STextStyles.desktopTextExtraSmall(context) + .copyWith( + color: Theme.of(context) + .extension()! + .accentColorBlue, + ), + ), + secondChild: Text( + widget.titles[i], + style: + STextStyles.desktopTextExtraSmall(context) + .copyWith( + color: Theme.of(context) + .extension()! + .textSubtitle1, + ), + ), + crossFadeState: _selectedIndex == i + ? CrossFadeState.showFirst + : CrossFadeState.showSecond, + duration: const Duration(milliseconds: 250), + ), + ], + ), + ), + ), + ), + ), + ], + ), + const SizedBox( + height: 19, + ), + Stack( + children: [ + Container( + height: 2, + decoration: BoxDecoration( + color: Theme.of(context) + .extension()! + .backgroundAppBar, + ), + ), + AnimatedSlide( + offset: Offset(_selectedIndex.toDouble(), 0), + duration: duration, + child: Container( + height: 2, + width: constraints.maxWidth / widget.titles.length, + decoration: BoxDecoration( + color: Theme.of(context) + .extension()! + .accentColorBlue, + ), + ), + ), + ], + ), + AnimatedSwitcher( + key: _key, + duration: duration, + transitionBuilder: (child, animation) { + return FadeTransition( + opacity: animation, + child: child, + ); + }, + layoutBuilder: (currentChild, prevChildren) { + return Stack( + alignment: Alignment.topCenter, + children: [ + ...prevChildren, + if (currentChild != null) currentChild, + ], + ); + }, + child: AnimatedAlign( + key: Key(widget.titles[_selectedIndex]), + alignment: Alignment.topCenter, + duration: duration, + child: Padding( + padding: widget.childPadding ?? EdgeInsets.zero, + child: widget.children[_selectedIndex], + ), + ), + ), + ], + ), + ); + } +} diff --git a/lib/widgets/desktop/desktop_app_bar.dart b/lib/widgets/desktop/desktop_app_bar.dart index a95a552e5..848d4c0a3 100644 --- a/lib/widgets/desktop/desktop_app_bar.dart +++ b/lib/widgets/desktop/desktop_app_bar.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:stackwallet/widgets/conditional_parent.dart'; const double kDesktopAppBarHeight = 96.0; const double kDesktopAppBarHeightCompact = 82.0; @@ -8,6 +9,7 @@ class DesktopAppBar extends StatelessWidget { Key? key, this.leading, this.center, + this.overlayCenter, this.trailing, this.background = Colors.transparent, required this.isCompactHeight, @@ -16,6 +18,7 @@ class DesktopAppBar extends StatelessWidget { final Widget? leading; final Widget? center; + final Widget? overlayCenter; final Widget? trailing; final Color background; final bool isCompactHeight; @@ -43,16 +46,31 @@ class DesktopAppBar extends StatelessWidget { items.add(trailing!); } - return Container( - decoration: BoxDecoration( - color: background, + return ConditionalParent( + condition: overlayCenter != null, + builder: (child) => Stack( + children: [ + child, + Positioned.fill( + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [overlayCenter!], + ), + ), + ], ), - height: - isCompactHeight ? kDesktopAppBarHeightCompact : kDesktopAppBarHeight, - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - crossAxisAlignment: CrossAxisAlignment.center, - children: items, + child: Container( + decoration: BoxDecoration( + color: background, + ), + height: isCompactHeight + ? kDesktopAppBarHeightCompact + : kDesktopAppBarHeight, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.center, + children: items, + ), ), ); } diff --git a/lib/widgets/desktop/secondary_button.dart b/lib/widgets/desktop/secondary_button.dart index 5d1ba1970..145a6446c 100644 --- a/lib/widgets/desktop/secondary_button.dart +++ b/lib/widgets/desktop/secondary_button.dart @@ -18,6 +18,7 @@ class SecondaryButton extends StatelessWidget { this.enabled = true, this.buttonHeight, this.iconSpacing = 10, + this.padding = EdgeInsets.zero, }) : super(key: key); final double? width; @@ -29,6 +30,7 @@ class SecondaryButton extends StatelessWidget { final Widget? trailingIcon; final ButtonHeight? buttonHeight; final double iconSpacing; + final EdgeInsets padding; TextStyle getStyle(bool isDesktop, BuildContext context) { if (isDesktop) { @@ -155,37 +157,40 @@ class SecondaryButton extends StatelessWidget { : Theme.of(context) .extension()! .getSecondaryDisabledButtonStyle(context), - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - if (icon != null) icon!, - if (icon != null && label != null) - SizedBox( - width: iconSpacing, - ), - if (label != null) - Column( - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Text( - label!, - style: getStyle(isDesktop, context), - ), - if (buttonHeight != null && buttonHeight == ButtonHeight.s) - const SizedBox( - height: 2, + child: Padding( + padding: padding, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + if (icon != null) icon!, + if (icon != null && label != null) + SizedBox( + width: iconSpacing, + ), + if (label != null) + Column( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text( + label!, + style: getStyle(isDesktop, context), ), - ], - ), - if (trailingIcon != null) - SizedBox( - width: iconSpacing, - ), - if (trailingIcon != null) trailingIcon!, - ], + if (buttonHeight != null && buttonHeight == ButtonHeight.s) + const SizedBox( + height: 2, + ), + ], + ), + if (trailingIcon != null) + SizedBox( + width: iconSpacing, + ), + if (trailingIcon != null) trailingIcon!, + ], + ), ), ), ); diff --git a/lib/widgets/eth_wallet_radio.dart b/lib/widgets/eth_wallet_radio.dart new file mode 100644 index 000000000..c20a4cbb9 --- /dev/null +++ b/lib/widgets/eth_wallet_radio.dart @@ -0,0 +1,77 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:stackwallet/providers/providers.dart'; +import 'package:stackwallet/utilities/text_styles.dart'; +import 'package:stackwallet/utilities/theme/stack_colors.dart'; +import 'package:stackwallet/widgets/wallet_info_row/sub_widgets/wallet_info_row_balance_future.dart'; +import 'package:stackwallet/widgets/wallet_info_row/sub_widgets/wallet_info_row_coin_icon.dart'; + +class EthWalletRadio extends ConsumerStatefulWidget { + const EthWalletRadio({ + Key? key, + required this.walletId, + this.selectedWalletId, + }) : super(key: key); + + final String walletId; + final String? selectedWalletId; + + @override + ConsumerState createState() => _EthWalletRadioState(); +} + +class _EthWalletRadioState extends ConsumerState { + @override + Widget build(BuildContext context) { + final manager = ref.watch(walletsChangeNotifierProvider + .select((value) => value.getManager(widget.walletId))); + + return Padding( + padding: EdgeInsets.zero, + child: Container( + color: Colors.transparent, + child: Row( + children: [ + IgnorePointer( + child: Radio( + value: widget.walletId, + groupValue: widget.selectedWalletId, + onChanged: (_) { + // do nothing since changing updating the ui is already + // done elsewhere + }, + ), + ), + const SizedBox( + width: 12, + ), + WalletInfoCoinIcon( + coin: manager.coin, + size: 40, + ), + const SizedBox( + width: 12, + ), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Text( + manager.walletName, + style: STextStyles.desktopTextExtraSmall(context).copyWith( + color: + Theme.of(context).extension()!.textDark, + ), + ), + WalletInfoRowBalance( + walletId: widget.walletId, + ), + ], + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/widgets/expandable.dart b/lib/widgets/expandable.dart index b60226a50..dbd5c67f6 100644 --- a/lib/widgets/expandable.dart +++ b/lib/widgets/expandable.dart @@ -19,8 +19,11 @@ class Expandable extends StatefulWidget { this.animation, this.animationDurationMultiplier = 1.0, this.onExpandChanged, + this.onExpandWillChange, this.controller, this.expandOverride, + this.curve = Curves.easeInOut, + this.initialState = ExpandableState.collapsed, }) : super(key: key); final Widget header; @@ -29,8 +32,11 @@ class Expandable extends StatefulWidget { final Animation? animation; final double animationDurationMultiplier; final void Function(ExpandableState)? onExpandChanged; + final void Function(ExpandableState)? onExpandWillChange; final ExpandableController? controller; final VoidCallback? expandOverride; + final Curve curve; + final ExpandableState initialState; @override State createState() => _ExpandableState(); @@ -42,14 +48,16 @@ class _ExpandableState extends State with TickerProviderStateMixin { late final Duration duration; late final ExpandableController? controller; - ExpandableState _toggleState = ExpandableState.collapsed; + late ExpandableState _toggleState; Future toggle() async { if (animation.isDismissed) { + widget.onExpandWillChange?.call(ExpandableState.expanded); await animationController.forward(); _toggleState = ExpandableState.expanded; widget.onExpandChanged?.call(_toggleState); } else if (animation.isCompleted) { + widget.onExpandWillChange?.call(ExpandableState.collapsed); await animationController.reverse(); _toggleState = ExpandableState.collapsed; widget.onExpandChanged?.call(_toggleState); @@ -59,6 +67,7 @@ class _ExpandableState extends State with TickerProviderStateMixin { @override void initState() { + _toggleState = widget.initialState; controller = widget.controller; controller?.toggle = toggle; @@ -70,10 +79,15 @@ class _ExpandableState extends State with TickerProviderStateMixin { vsync: this, duration: duration, ); + + final tween = _toggleState == ExpandableState.collapsed + ? Tween(begin: 0.0, end: 1.0) + : Tween(begin: 1.0, end: 0.0); + animation = widget.animation ?? - Tween(begin: 0.0, end: 1.0).animate( + tween.animate( CurvedAnimation( - curve: Curves.easeInOut, + curve: widget.curve, parent: animationController, ), ); diff --git a/lib/widgets/icon_widgets/eth_token_icon.dart b/lib/widgets/icon_widgets/eth_token_icon.dart new file mode 100644 index 000000000..0b47d526d --- /dev/null +++ b/lib/widgets/icon_widgets/eth_token_icon.dart @@ -0,0 +1,53 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_svg/svg.dart'; +import 'package:isar/isar.dart'; +import 'package:stackwallet/models/isar/exchange_cache/currency.dart'; +import 'package:stackwallet/services/exchange/exchange_data_loading_service.dart'; +import 'package:stackwallet/utilities/assets.dart'; +import 'package:stackwallet/utilities/enums/coin_enum.dart'; + +class EthTokenIcon extends StatefulWidget { + const EthTokenIcon({ + Key? key, + required this.contractAddress, + this.size = 22, + }) : super(key: key); + + final String contractAddress; + final double size; + + @override + State createState() => _EthTokenIconState(); +} + +class _EthTokenIconState extends State { + late final String? imageUrl; + + @override + void initState() { + imageUrl = ExchangeDataLoadingService.instance.isar.currencies + .where() + .filter() + .tokenContractEqualTo(widget.contractAddress, caseSensitive: false) + .findFirstSync() + ?.image; + super.initState(); + } + + @override + Widget build(BuildContext context) { + if (imageUrl == null || imageUrl!.isEmpty) { + return SvgPicture.asset( + Assets.svg.iconFor(coin: Coin.ethereum), + width: widget.size, + height: widget.size, + ); + } else { + return SvgPicture.network( + imageUrl!, + width: widget.size, + height: widget.size, + ); + } + } +} diff --git a/lib/widgets/managed_favorite.dart b/lib/widgets/managed_favorite.dart index a9d5fcd90..ccfcdf38a 100644 --- a/lib/widgets/managed_favorite.dart +++ b/lib/widgets/managed_favorite.dart @@ -5,7 +5,6 @@ import 'package:stackwallet/providers/providers.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; -import 'package:stackwallet/utilities/format.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; import 'package:stackwallet/utilities/util.dart'; @@ -104,12 +103,13 @@ class _ManagedFavoriteCardState extends ConsumerState { ), Expanded( child: Text( - "${Format.localizedStringAsFixed( - value: manager.balance.getTotal(), + "${manager.balance.total.localizedStringAsFixed( locale: ref.watch( - localeServiceChangeNotifierProvider - .select((value) => value.locale)), - decimalPlaces: 8, + localeServiceChangeNotifierProvider.select( + (value) => value.locale, + ), + ), + decimalPlaces: manager.coin.decimals, )} ${manager.coin.ticker}", style: STextStyles.itemSubtitle(context), ), @@ -146,11 +146,13 @@ class _ManagedFavoriteCardState extends ConsumerState { height: 2, ), Text( - "${Format.localizedStringAsFixed( - value: manager.balance.getTotal(), - locale: ref.watch(localeServiceChangeNotifierProvider - .select((value) => value.locale)), - decimalPlaces: 8, + "${manager.balance.total.localizedStringAsFixed( + locale: ref.watch( + localeServiceChangeNotifierProvider.select( + (value) => value.locale, + ), + ), + decimalPlaces: manager.coin.decimals, )} ${manager.coin.ticker}", style: STextStyles.itemSubtitle(context), ), diff --git a/lib/widgets/master_wallet_card.dart b/lib/widgets/master_wallet_card.dart new file mode 100644 index 000000000..65f65c3a1 --- /dev/null +++ b/lib/widgets/master_wallet_card.dart @@ -0,0 +1,137 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:stackwallet/providers/providers.dart'; +import 'package:stackwallet/services/coins/ethereum/ethereum_wallet.dart'; +import 'package:stackwallet/utilities/assets.dart'; +import 'package:stackwallet/utilities/constants.dart'; +import 'package:stackwallet/utilities/theme/stack_colors.dart'; +import 'package:stackwallet/widgets/animated_widgets/rotate_icon.dart'; +import 'package:stackwallet/widgets/expandable.dart'; +import 'package:stackwallet/widgets/rounded_white_container.dart'; +import 'package:stackwallet/widgets/wallet_card.dart'; +import 'package:stackwallet/widgets/wallet_info_row/wallet_info_row.dart'; + +class MasterWalletCard extends ConsumerStatefulWidget { + const MasterWalletCard({ + Key? key, + required this.walletId, + this.popPrevious = false, + }) : super(key: key); + + final String walletId; + final bool popPrevious; + + @override + ConsumerState createState() => _MasterWalletCardState(); +} + +class _MasterWalletCardState extends ConsumerState { + final expandableController = ExpandableController(); + final rotateIconController = RotateIconController(); + late final List tokenContractAddresses; + + @override + void initState() { + final ethWallet = ref + .read(walletsChangeNotifierProvider) + .getManager(widget.walletId) + .wallet as EthereumWallet; + + tokenContractAddresses = ethWallet.getWalletTokenContractAddresses(); + + super.initState(); + } + + @override + Widget build(BuildContext context) { + return RoundedWhiteContainer( + padding: EdgeInsets.zero, + child: Expandable( + controller: expandableController, + expandOverride: () {}, + header: Padding( + padding: const EdgeInsets.all(12), + child: Row( + children: [ + Expanded( + child: WalletInfoRow( + walletId: widget.walletId, + ), + ), + MaterialButton( + padding: const EdgeInsets.all(5), + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + minWidth: 32, + height: 32, + color: Theme.of(context) + .extension()! + .textFieldDefaultBG, + elevation: 0, + hoverElevation: 0, + disabledElevation: 0, + highlightElevation: 0, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + ), + onPressed: () { + if (expandableController.state == ExpandableState.collapsed) { + rotateIconController.forward?.call(); + } else { + rotateIconController.reverse?.call(); + } + expandableController.toggle?.call(); + }, + child: RotateIcon( + controller: rotateIconController, + icon: SvgPicture.asset( + Assets.svg.chevronDown, + width: 14, + ), + curve: Curves.easeInOut, + ), + ), + ], + ), + ), + body: ListView( + shrinkWrap: true, + primary: false, + children: [ + Container( + width: double.infinity, + height: 1.5, + color: + Theme.of(context).extension()!.backgroundAppBar, + ), + Padding( + padding: const EdgeInsets.all( + 7, + ), + child: WalletSheetCard( + walletId: widget.walletId, + popPrevious: true, + ), + ), + ...tokenContractAddresses.map( + (e) => Padding( + padding: const EdgeInsets.only( + left: 7, + right: 7, + bottom: 7, + ), + child: WalletSheetCard( + walletId: widget.walletId, + contractAddress: e, + popPrevious: true, + ), + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/widgets/rounded_container.dart b/lib/widgets/rounded_container.dart index 5b64c7d50..c30f7a186 100644 --- a/lib/widgets/rounded_container.dart +++ b/lib/widgets/rounded_container.dart @@ -12,6 +12,7 @@ class RoundedContainer extends StatelessWidget { this.width, this.height, this.borderColor, + this.hoverColor, this.boxShadow, this.onPressed, }) : super(key: key); @@ -23,6 +24,7 @@ class RoundedContainer extends StatelessWidget { final double? width; final double? height; final Color? borderColor; + final Color? hoverColor; final List? boxShadow; final VoidCallback? onPressed; @@ -32,6 +34,7 @@ class RoundedContainer extends StatelessWidget { condition: onPressed != null, builder: (child) => RawMaterialButton( fillColor: color, + hoverColor: hoverColor, elevation: 0, highlightElevation: 0, disabledElevation: 0, diff --git a/lib/widgets/rounded_white_container.dart b/lib/widgets/rounded_white_container.dart index 8208d69ca..a72d7af5a 100644 --- a/lib/widgets/rounded_white_container.dart +++ b/lib/widgets/rounded_white_container.dart @@ -11,6 +11,7 @@ class RoundedWhiteContainer extends StatelessWidget { this.width, this.height, this.borderColor, + this.hoverColor, this.boxShadow, this.onPressed, }) : super(key: key); @@ -21,6 +22,7 @@ class RoundedWhiteContainer extends StatelessWidget { final double? width; final double? height; final Color? borderColor; + final Color? hoverColor; final List? boxShadow; final VoidCallback? onPressed; @@ -34,6 +36,7 @@ class RoundedWhiteContainer extends StatelessWidget { height: height, borderColor: borderColor, boxShadow: boxShadow, + hoverColor: hoverColor, onPressed: onPressed, child: child, ); diff --git a/lib/widgets/transaction_card.dart b/lib/widgets/transaction_card.dart index e269ace4d..a2b9ba866 100644 --- a/lib/widgets/transaction_card.dart +++ b/lib/widgets/transaction_card.dart @@ -6,7 +6,9 @@ import 'package:stackwallet/models/isar/models/isar_models.dart'; import 'package:stackwallet/notifications/show_flush_bar.dart'; import 'package:stackwallet/pages/wallet_view/sub_widgets/tx_icon.dart'; import 'package:stackwallet/pages/wallet_view/transaction_views/transaction_details_view.dart'; +import 'package:stackwallet/providers/db/main_db_provider.dart'; import 'package:stackwallet/providers/providers.dart'; +import 'package:stackwallet/utilities/amount/amount.dart'; import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:stackwallet/utilities/format.dart'; @@ -33,6 +35,10 @@ class TransactionCard extends ConsumerStatefulWidget { class _TransactionCardState extends ConsumerState { late final Transaction _transaction; late final String walletId; + late final bool isTokenTx; + late final String prefix; + late final String unit; + late final Coin coin; String whatIsIt( TransactionType type, @@ -93,6 +99,29 @@ class _TransactionCardState extends ConsumerState { void initState() { walletId = widget.walletId; _transaction = widget.transaction; + isTokenTx = _transaction.subType == TransactionSubType.ethToken; + if (Util.isDesktop) { + if (_transaction.type == TransactionType.outgoing) { + prefix = "-"; + } else if (_transaction.type == TransactionType.incoming) { + prefix = "+"; + } else { + prefix = ""; + } + } else { + prefix = ""; + } + coin = ref + .read(walletsChangeNotifierProvider) + .getManager(widget.walletId) + .coin; + + unit = isTokenTx + ? ref + .read(mainDBProvider) + .getEthContractSync(_transaction.otherData!)! + .symbol + : coin.ticker; super.initState(); } @@ -100,28 +129,16 @@ class _TransactionCardState extends ConsumerState { Widget build(BuildContext context) { final locale = ref.watch( localeServiceChangeNotifierProvider.select((value) => value.locale)); - final manager = ref.watch(walletsChangeNotifierProvider - .select((value) => value.getManager(walletId))); final baseCurrency = ref .watch(prefsChangeNotifierProvider.select((value) => value.currency)); - final coin = manager.coin; - final price = ref - .watch(priceAnd24hChangeNotifierProvider - .select((value) => value.getPrice(coin))) + .watch(priceAnd24hChangeNotifierProvider.select((value) => isTokenTx + ? value.getTokenPrice(_transaction.otherData!) + : value.getPrice(coin))) .item1; - String prefix = ""; - if (Util.isDesktop) { - if (_transaction.type == TransactionType.outgoing) { - prefix = "-"; - } else if (_transaction.type == TransactionType.incoming) { - prefix = "+"; - } - } - final currentHeight = ref.watch(walletsChangeNotifierProvider .select((value) => value.getManager(walletId).currentHeight)); @@ -183,8 +200,7 @@ class _TransactionCardState extends ConsumerState { children: [ TxIcon( transaction: _transaction, - coin: ref.watch(walletsChangeNotifierProvider.select( - (value) => value.getManager(widget.walletId).coin)), + coin: coin, currentHeight: currentHeight, ), const SizedBox( @@ -221,9 +237,12 @@ class _TransactionCardState extends ConsumerState { fit: BoxFit.scaleDown, child: Builder( builder: (_) { - final amount = _transaction.amount; + final amount = _transaction.realAmount; + return Text( - "$prefix${Format.satoshiAmountToPrettyString(amount, locale, coin)} ${coin.ticker}", + "$prefix${amount.localizedStringAsFixed( + locale: locale, + )} $unit", style: STextStyles.itemSubtitle12(context), ); }, @@ -260,13 +279,13 @@ class _TransactionCardState extends ConsumerState { fit: BoxFit.scaleDown, child: Builder( builder: (_) { - int value = _transaction.amount; + final amount = _transaction.realAmount; return Text( - "$prefix${Format.localizedStringAsFixed( - value: Format.satoshisToAmount(value, - coin: coin) * - price, + "$prefix${Amount.fromDecimal( + amount.decimal * price, + fractionDigits: 2, + ).localizedStringAsFixed( locale: locale, decimalPlaces: 2, )} $baseCurrency", diff --git a/lib/widgets/wallet_card.dart b/lib/widgets/wallet_card.dart index 5812a86e4..9e4fbc034 100644 --- a/lib/widgets/wallet_card.dart +++ b/lib/widgets/wallet_card.dart @@ -1,9 +1,22 @@ +import 'dart:async'; + import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:stackwallet/pages/token_view/token_view.dart'; import 'package:stackwallet/pages/wallet_view/wallet_view.dart'; +import 'package:stackwallet/pages_desktop_specific/my_stack_view/wallet_view/desktop_token_view.dart'; +import 'package:stackwallet/pages_desktop_specific/my_stack_view/wallet_view/desktop_wallet_view.dart'; +import 'package:stackwallet/providers/db/main_db_provider.dart'; +import 'package:stackwallet/providers/global/secure_store_provider.dart'; import 'package:stackwallet/providers/providers.dart'; +import 'package:stackwallet/services/coins/ethereum/ethereum_wallet.dart'; +import 'package:stackwallet/services/ethereum/ethereum_token_service.dart'; +import 'package:stackwallet/services/transaction_notification_tracker.dart'; import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; +import 'package:stackwallet/utilities/show_loading.dart'; +import 'package:stackwallet/utilities/util.dart'; +import 'package:stackwallet/widgets/conditional_parent.dart'; import 'package:stackwallet/widgets/rounded_white_container.dart'; import 'package:stackwallet/widgets/wallet_info_row/wallet_info_row.dart'; import 'package:tuple/tuple.dart'; @@ -12,49 +25,110 @@ class WalletSheetCard extends ConsumerWidget { const WalletSheetCard({ Key? key, required this.walletId, + this.contractAddress, this.popPrevious = false, + this.desktopNavigatorState, }) : super(key: key); final String walletId; + final String? contractAddress; final bool popPrevious; + final NavigatorState? desktopNavigatorState; + + void _openWallet(BuildContext context, WidgetRef ref) async { + final nav = Navigator.of(context); + + final manager = + ref.read(walletsChangeNotifierProvider).getManager(walletId); + if (manager.coin == Coin.monero || manager.coin == Coin.wownero) { + await manager.initializeExisting(); + } + if (context.mounted) { + if (popPrevious) nav.pop(); + + if (desktopNavigatorState != null) { + unawaited( + desktopNavigatorState!.pushNamed( + DesktopWalletView.routeName, + arguments: walletId, + ), + ); + } else { + unawaited( + nav.pushNamed( + WalletView.routeName, + arguments: Tuple2( + walletId, + ref + .read(walletsChangeNotifierProvider) + .getManagerProvider(walletId), + ), + ), + ); + } + + if (contractAddress != null) { + final contract = + ref.read(mainDBProvider).getEthContractSync(contractAddress!)!; + ref.read(tokenServiceStateProvider.state).state = EthTokenWallet( + token: contract, + secureStore: ref.read(secureStoreProvider), + ethWallet: manager.wallet as EthereumWallet, + tracker: TransactionNotificationTracker( + walletId: walletId, + ), + ); + + await showLoading( + whileFuture: ref.read(tokenServiceProvider)!.initialize(), + context: context, + opaqueBG: true, + message: "Loading ${contract.name}", + ); + + // pop loading + nav.pop(); + + if (desktopNavigatorState != null) { + await desktopNavigatorState!.pushNamed( + DesktopTokenView.routeName, + arguments: walletId, + ); + } else { + await nav.pushNamed( + TokenView.routeName, + arguments: walletId, + ); + } + } + } + } @override Widget build(BuildContext context, WidgetRef ref) { - return RoundedWhiteContainer( - padding: const EdgeInsets.all(0), - child: MaterialButton( - // splashColor: Theme.of(context).extension()!.highlight, - key: Key("walletsSheetItemButtonKey_$walletId"), - padding: const EdgeInsets.all(5), - materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, + return ConditionalParent( + condition: !Util.isDesktop, + builder: (child) => RoundedWhiteContainer( + padding: const EdgeInsets.all(0), + child: MaterialButton( + // splashColor: Theme.of(context).extension()!.highlight, + key: Key("walletsSheetItemButtonKey_$walletId"), + padding: const EdgeInsets.all(5), + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), ), + onPressed: () => _openWallet(context, ref), + child: child, ), - onPressed: () async { - final manager = - ref.read(walletsChangeNotifierProvider).getManager(walletId); - if (manager.coin == Coin.monero || manager.coin == Coin.wownero) { - await manager.initializeExisting(); - } - if (context.mounted) { - if (popPrevious && context.mounted) Navigator.of(context).pop(); - - await Navigator.of(context).pushNamed( - WalletView.routeName, - arguments: Tuple2( - walletId, - ref - .read(walletsChangeNotifierProvider) - .getManagerProvider(walletId), - ), - ); - } - }, - child: WalletInfoRow( - walletId: walletId, - ), + ), + child: WalletInfoRow( + walletId: walletId, + contractAddress: contractAddress, + onPressedDesktop: + Util.isDesktop ? () => _openWallet(context, ref) : null, ), ); } diff --git a/lib/widgets/wallet_info_row/sub_widgets/wallet_info_row_balance_future.dart b/lib/widgets/wallet_info_row/sub_widgets/wallet_info_row_balance_future.dart index 542cb545a..1bbaf8a94 100644 --- a/lib/widgets/wallet_info_row/sub_widgets/wallet_info_row_balance_future.dart +++ b/lib/widgets/wallet_info_row/sub_widgets/wallet_info_row_balance_future.dart @@ -1,20 +1,24 @@ -import 'package:decimal/decimal.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:stackwallet/db/isar/main_db.dart'; import 'package:stackwallet/providers/providers.dart'; +import 'package:stackwallet/services/coins/ethereum/ethereum_wallet.dart'; import 'package:stackwallet/services/coins/firo/firo_wallet.dart'; -import 'package:stackwallet/utilities/constants.dart'; +import 'package:stackwallet/utilities/amount/amount.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; -import 'package:stackwallet/utilities/format.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; import 'package:stackwallet/utilities/util.dart'; -class WalletInfoRowBalanceFuture extends ConsumerWidget { - const WalletInfoRowBalanceFuture({Key? key, required this.walletId}) - : super(key: key); +class WalletInfoRowBalance extends ConsumerWidget { + const WalletInfoRowBalance({ + Key? key, + required this.walletId, + this.contractAddress, + }) : super(key: key); final String walletId; + final String? contractAddress; @override Widget build(BuildContext context, WidgetRef ref) { @@ -28,18 +32,30 @@ class WalletInfoRowBalanceFuture extends ConsumerWidget { ), ); - Decimal balance = manager.balance.getTotal(); - - if (manager.coin == Coin.firo || manager.coin == Coin.firoTestNet) { - balance += (manager.wallet as FiroWallet).balancePrivate.getTotal(); + Amount totalBalance; + int decimals; + String unit; + if (contractAddress == null) { + totalBalance = manager.balance.total; + if (manager.coin == Coin.firo || manager.coin == Coin.firoTestNet) { + totalBalance = + totalBalance + (manager.wallet as FiroWallet).balancePrivate.total; + } + unit = manager.coin.ticker; + decimals = manager.coin.decimals; + } else { + final ethWallet = manager.wallet as EthereumWallet; + final contract = MainDB.instance.getEthContractSync(contractAddress!)!; + totalBalance = ethWallet.getCachedTokenBalance(contract).total; + unit = contract.symbol; + decimals = contract.decimals; } return Text( - "${Format.localizedStringAsFixed( - value: balance, + "${totalBalance.localizedStringAsFixed( locale: locale, - decimalPlaces: Constants.decimalPlacesForCoin(manager.coin), - )} ${manager.coin.ticker}", + decimalPlaces: decimals, + )} $unit", style: Util.isDesktop ? STextStyles.desktopTextExtraSmall(context).copyWith( color: Theme.of(context).extension()!.textSubtitle1, diff --git a/lib/widgets/wallet_info_row/sub_widgets/wallet_info_row_coin_icon.dart b/lib/widgets/wallet_info_row/sub_widgets/wallet_info_row_coin_icon.dart index ec5924de6..afe47e250 100644 --- a/lib/widgets/wallet_info_row/sub_widgets/wallet_info_row_coin_icon.dart +++ b/lib/widgets/wallet_info_row/sub_widgets/wallet_info_row_coin_icon.dart @@ -1,34 +1,68 @@ import 'package:flutter/material.dart'; import 'package:flutter_svg/svg.dart'; +import 'package:isar/isar.dart'; +import 'package:stackwallet/models/isar/exchange_cache/currency.dart'; +import 'package:stackwallet/services/exchange/change_now/change_now_exchange.dart'; +import 'package:stackwallet/services/exchange/exchange_data_loading_service.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; class WalletInfoCoinIcon extends StatelessWidget { - const WalletInfoCoinIcon({Key? key, required this.coin}) : super(key: key); + const WalletInfoCoinIcon({ + Key? key, + required this.coin, + this.size = 32, + this.contractAddress, + }) : super(key: key); final Coin coin; + final String? contractAddress; + final double size; @override Widget build(BuildContext context) { + Currency? currency; + if (contractAddress != null) { + currency = ExchangeDataLoadingService.instance.isar.currencies + .where() + .exchangeNameEqualTo(ChangeNowExchange.exchangeName) + .filter() + .tokenContractEqualTo( + contractAddress!, + caseSensitive: false, + ) + .and() + .imageIsNotEmpty() + .findFirstSync(); + } + return Container( + width: size, + height: size, decoration: BoxDecoration( color: Theme.of(context) .extension()! .colorForCoin(coin) - .withOpacity(0.5), + .withOpacity(0.4), borderRadius: BorderRadius.circular( Constants.size.circularBorderRadius, ), ), child: Padding( - padding: const EdgeInsets.all(4), - child: SvgPicture.asset( - Assets.svg.iconFor(coin: coin), - width: 20, - height: 20, - ), + padding: EdgeInsets.all(size / 5), + child: currency != null && currency.image.isNotEmpty + ? SvgPicture.network( + currency.image, + width: 20, + height: 20, + ) + : SvgPicture.asset( + Assets.svg.iconFor(coin: coin), + width: 20, + height: 20, + ), ), ); } diff --git a/lib/widgets/wallet_info_row/wallet_info_row.dart b/lib/widgets/wallet_info_row/wallet_info_row.dart index 5bb51e2e6..26879aa7e 100644 --- a/lib/widgets/wallet_info_row/wallet_info_row.dart +++ b/lib/widgets/wallet_info_row/wallet_info_row.dart @@ -1,11 +1,13 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:flutter_svg/svg.dart'; +import 'package:stackwallet/models/isar/models/ethereum/eth_contract.dart'; +import 'package:stackwallet/pages/token_view/sub_widgets/token_summary.dart'; +import 'package:stackwallet/providers/db/main_db_provider.dart'; import 'package:stackwallet/providers/providers.dart'; -import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/theme/stack_colors.dart'; import 'package:stackwallet/utilities/util.dart'; +import 'package:stackwallet/widgets/custom_buttons/blue_text_button.dart'; import 'package:stackwallet/widgets/wallet_info_row/sub_widgets/wallet_info_row_balance_future.dart'; import 'package:stackwallet/widgets/wallet_info_row/sub_widgets/wallet_info_row_coin_icon.dart'; @@ -13,12 +15,14 @@ class WalletInfoRow extends ConsumerWidget { const WalletInfoRow({ Key? key, required this.walletId, - this.onPressed, + this.onPressedDesktop, + this.contractAddress, this.padding = const EdgeInsets.all(0), }) : super(key: key); final String walletId; - final VoidCallback? onPressed; + final String? contractAddress; + final VoidCallback? onPressedDesktop; final EdgeInsets padding; @override @@ -27,69 +31,93 @@ class WalletInfoRow extends ConsumerWidget { .watch(walletsChangeNotifierProvider.notifier) .getManagerProvider(walletId)); + EthContract? contract; + if (contractAddress != null) { + contract = ref.watch(mainDBProvider + .select((value) => value.getEthContractSync(contractAddress!))); + } + if (Util.isDesktop) { - return MouseRegion( - cursor: SystemMouseCursors.click, - child: GestureDetector( - onTap: onPressed, - child: Padding( - padding: padding, - child: Container( - color: Colors.transparent, - child: Row( - children: [ - Expanded( - flex: 4, - child: Row( - children: [ - WalletInfoCoinIcon(coin: manager.coin), - const SizedBox( - width: 12, - ), - Text( - manager.walletName, - style: STextStyles.desktopTextExtraSmall(context) - .copyWith( - color: Theme.of(context) - .extension()! - .textDark, + return Padding( + padding: padding, + child: Container( + color: Colors.transparent, + child: Row( + children: [ + Expanded( + flex: 3, + child: Row( + children: [ + WalletInfoCoinIcon( + coin: manager.coin, + contractAddress: contractAddress, + ), + const SizedBox( + width: 12, + ), + contract != null + ? Row( + children: [ + Text( + contract.name, + style: + STextStyles.desktopTextExtraSmall(context) + .copyWith( + color: Theme.of(context) + .extension()! + .textDark, + ), + ), + const SizedBox( + width: 4, + ), + CoinTickerTag( + walletId: walletId, + ), + ], + ) + : Text( + manager.walletName, + style: STextStyles.desktopTextExtraSmall(context) + .copyWith( + color: Theme.of(context) + .extension()! + .textDark, + ), ), - ), - ], - ), - ), - Expanded( - flex: 4, - child: WalletInfoRowBalanceFuture( - walletId: walletId, - ), - ), - Expanded( - flex: 6, - child: Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - SvgPicture.asset( - Assets.svg.chevronRight, - width: 20, - height: 20, - color: Theme.of(context) - .extension()! - .textSubtitle1, - ) - ], - ), - ) - ], + ], + ), ), - ), + Expanded( + flex: 4, + child: WalletInfoRowBalance( + walletId: walletId, + contractAddress: contractAddress, + ), + ), + Expanded( + flex: 2, + child: Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + CustomTextButton( + text: "Open wallet", + onTap: onPressedDesktop, + ), + ], + ), + ) + ], ), ), ); } else { return Row( children: [ - WalletInfoCoinIcon(coin: manager.coin), + WalletInfoCoinIcon( + coin: manager.coin, + contractAddress: contractAddress, + ), const SizedBox( width: 12, ), @@ -98,14 +126,32 @@ class WalletInfoRow extends ConsumerWidget { mainAxisAlignment: MainAxisAlignment.spaceBetween, crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( - manager.walletName, - style: STextStyles.titleBold12(context), - ), + contract != null + ? Row( + children: [ + Text( + contract.name, + style: STextStyles.titleBold12(context), + ), + const SizedBox( + width: 4, + ), + CoinTickerTag( + walletId: walletId, + ), + ], + ) + : Text( + manager.walletName, + style: STextStyles.titleBold12(context), + ), const SizedBox( height: 2, ), - WalletInfoRowBalanceFuture(walletId: walletId), + WalletInfoRowBalance( + walletId: walletId, + contractAddress: contractAddress, + ), ], ), ), diff --git a/pubspec.lock b/pubspec.lock index fdf6de547..704bbe10e 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -395,7 +395,7 @@ packages: source: hosted version: "1.0.0" dart_bs58: - dependency: transitive + dependency: "direct main" description: name: dart_bs58 sha256: e2fff08fca810d5215f6fca3ea713d8a4a9728aaf1b1658472863b2de7377234 @@ -403,7 +403,7 @@ packages: source: hosted version: "1.0.1" dart_bs58check: - dependency: transitive + dependency: "direct main" description: name: dart_bs58check sha256: "4284e606795a18c1df5a955928bdc4e1b6f908da7ab0e87f49db51b3774e9e6c" @@ -514,6 +514,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.5" + ethereum_addresses: + dependency: "direct main" + description: + name: ethereum_addresses + sha256: e6ba01d44ecb9c5634367b017d6e94598fc937be8b28fc406d0e51ed6e9513dd + url: "https://pub.dev" + source: hosted + version: "1.0.2" event_bus: dependency: "direct main" description: @@ -794,7 +802,7 @@ packages: source: hosted version: "2.2.0" hex: - dependency: transitive + dependency: "direct main" description: name: hex sha256: "4e7cd54e4b59ba026432a6be2dd9d96e4c5205725194997193bf871703b82c4a" @@ -942,6 +950,14 @@ packages: url: "https://pub.dev" source: hosted version: "4.8.0" + json_rpc_2: + dependency: transitive + description: + name: json_rpc_2 + sha256: "5e469bffa23899edacb7b22787780068d650b106a21c76db3c49218ab7ca447e" + url: "https://pub.dev" + source: hosted + version: "3.0.2" jsonrpc2: dependency: "direct main" description: @@ -1342,7 +1358,7 @@ packages: source: hosted version: "4.0.0" rational: - dependency: transitive + dependency: "direct main" description: name: rational sha256: ba58e9e18df9abde280e8b10051e4bce85091e41e8e7e411b6cde2e738d357cf @@ -1579,6 +1595,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.2.0" + string_to_hex: + dependency: "direct main" + description: + name: string_to_hex + sha256: "63e5dc1f4821a2449d505033fbd4569f7020ebf30ddffb54d00ebaba8e144a49" + url: "https://pub.dev" + source: hosted + version: "0.2.2" string_validator: dependency: "direct main" description: @@ -1827,6 +1851,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.2" + web3dart: + dependency: "direct main" + description: + name: web3dart + sha256: "48b89a5fac0029770a18d1a8bd05ce8431722bacf76184e4301dae05781565e5" + url: "https://pub.dev" + source: hosted + version: "2.3.5" web_socket_channel: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 258c636c5..e20b3e5ad 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -11,7 +11,7 @@ description: Stack Wallet # In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion. # Read more about iOS versioning at # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html -version: 1.6.9+152 +version: 1.7.0+153 environment: sdk: ">=2.17.0 <3.0.0" @@ -94,6 +94,11 @@ dependencies: ref: 22279d4bb24ed541b431acd269a1bc50af0f36a0 bs58check: ^1.0.2 + # Eth Plugins + web3dart: 2.3.5 + string_to_hex: 0.2.2 + ethereum_addresses: 1.0.2 + # Storage plugins flutter_secure_storage: ^5.0.2 hive: ^2.0.5 @@ -142,6 +147,10 @@ dependencies: string_validator: ^0.3.0 equatable: ^2.0.5 async: ^2.10.0 + dart_bs58: ^1.0.1 + dart_bs58check: ^3.0.2 + hex: ^0.2.0 + rational: ^2.2.2 dev_dependencies: flutter_test: @@ -300,6 +309,8 @@ flutter: - assets/svg/plus-circle.svg - assets/svg/circle-plus-filled.svg - assets/svg/configuration.svg + - assets/svg/tokens.svg + - assets/svg/circle-plus.svg - assets/svg/robot-head.svg - assets/svg/whirlpool.svg - assets/svg/fingerprint.svg @@ -314,6 +325,7 @@ flutter: - assets/svg/framed-address-book.svg - assets/svg/framed-gear.svg - assets/svg/list-ul.svg + - assets/svg/cc.svg # coin icons diff --git a/test/address_book_service_test.dart b/test/address_book_service_test.dart index c5effd223..adc036d5a 100644 --- a/test/address_book_service_test.dart +++ b/test/address_book_service_test.dart @@ -1,7 +1,7 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:hive/hive.dart'; import 'package:hive_test/hive_test.dart'; -import 'package:stackwallet/hive/db.dart'; +import 'package:stackwallet/db/hive/db.dart'; import 'package:stackwallet/models/contact.dart'; import 'package:stackwallet/models/contact_address_entry.dart'; import 'package:stackwallet/services/address_book_service.dart'; diff --git a/test/cached_electrumx_test.dart b/test/cached_electrumx_test.dart index e0f7fd6ca..329de9daf 100644 --- a/test/cached_electrumx_test.dart +++ b/test/cached_electrumx_test.dart @@ -3,9 +3,9 @@ import 'package:hive/hive.dart'; import 'package:hive_test/hive_test.dart'; import 'package:mockito/annotations.dart'; import 'package:mockito/mockito.dart'; +import 'package:stackwallet/db/hive/db.dart'; import 'package:stackwallet/electrumx_rpc/cached_electrumx.dart'; import 'package:stackwallet/electrumx_rpc/electrumx.dart'; -import 'package:stackwallet/hive/db.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:stackwallet/utilities/prefs.dart'; diff --git a/test/hive/db_test.dart b/test/hive/db_test.dart index a87f568bd..2ea60bd52 100644 --- a/test/hive/db_test.dart +++ b/test/hive/db_test.dart @@ -1,6 +1,6 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:hive_test/hive_test.dart'; -import 'package:stackwallet/hive/db.dart'; +import 'package:stackwallet/db/hive/db.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; void main() { diff --git a/test/pages/send_view/send_view_test.mocks.dart b/test/pages/send_view/send_view_test.mocks.dart index 149d690b1..db7df0b52 100644 --- a/test/pages/send_view/send_view_test.mocks.dart +++ b/test/pages/send_view/send_view_test.mocks.dart @@ -13,7 +13,7 @@ import 'package:bitcoindart/bitcoindart.dart' as _i14; import 'package:flutter/foundation.dart' as _i4; import 'package:flutter_riverpod/flutter_riverpod.dart' as _i5; import 'package:mockito/mockito.dart' as _i1; -import 'package:stackwallet/db/main_db.dart' as _i13; +import 'package:stackwallet/db/isar/main_db.dart' as _i13; import 'package:stackwallet/electrumx_rpc/cached_electrumx.dart' as _i11; import 'package:stackwallet/electrumx_rpc/electrumx.dart' as _i10; import 'package:stackwallet/models/balance.dart' as _i12; @@ -486,6 +486,14 @@ class MockWalletsService extends _i1.Mock implements _i2.WalletsService { returnValue: _i22.Future.value(false), ) as _i22.Future); @override + Map fetchWalletsData() => (super.noSuchMethod( + Invocation.method( + #fetchWalletsData, + [], + ), + returnValue: {}, + ) as Map); + @override _i22.Future addExistingStackWallet({ required String? name, required String? walletId, @@ -1207,7 +1215,7 @@ class MockBitcoinWallet extends _i1.Mock implements _i26.BitcoinWallet { @override _i22.Future> prepareSend({ required String? address, - required int? satoshiAmount, + required int? amount, Map? args, }) => (super.noSuchMethod( @@ -1216,7 +1224,7 @@ class MockBitcoinWallet extends _i1.Mock implements _i26.BitcoinWallet { [], { #address: address, - #satoshiAmount: satoshiAmount, + #satoshiAmount: amount, #args: args, }, ), @@ -1641,6 +1649,25 @@ class MockBitcoinWallet extends _i1.Mock implements _i26.BitcoinWallet { returnValueForMissingStub: _i22.Future.value(), ) as _i22.Future); @override + List getWalletTokenContractAddresses() => (super.noSuchMethod( + Invocation.method( + #getWalletTokenContractAddresses, + [], + ), + returnValue: [], + ) as List); + @override + _i22.Future updateWalletTokenContractAddresses( + List? contractAddresses) => + (super.noSuchMethod( + Invocation.method( + #updateWalletTokenContractAddresses, + [contractAddresses], + ), + returnValue: _i22.Future.value(), + returnValueForMissingStub: _i22.Future.value(), + ) as _i22.Future); + @override void initWalletDB({_i13.MainDB? mockableOverride}) => super.noSuchMethod( Invocation.method( #initWalletDB, @@ -1886,7 +1913,7 @@ class MockBitcoinWallet extends _i1.Mock implements _i26.BitcoinWallet { @override _i22.Future> preparePaymentCodeSend({ required _i18.PaymentCode? paymentCode, - required int? satoshiAmount, + required int? amount, Map? args, }) => (super.noSuchMethod( @@ -1895,7 +1922,7 @@ class MockBitcoinWallet extends _i1.Mock implements _i26.BitcoinWallet { [], { #paymentCode: paymentCode, - #satoshiAmount: satoshiAmount, + #satoshiAmount: amount, #args: args, }, ), @@ -2837,6 +2864,11 @@ class MockManager extends _i1.Mock implements _i6.Manager { returnValue: false, ) as bool); @override + bool get hasTokenSupport => (super.noSuchMethod( + Invocation.getter(#hasTokenSupport), + returnValue: false, + ) as bool); + @override bool get hasWhirlpoolSupport => (super.noSuchMethod( Invocation.getter(#hasWhirlpoolSupport), returnValue: false, @@ -2871,7 +2903,7 @@ class MockManager extends _i1.Mock implements _i6.Manager { @override _i22.Future> prepareSend({ required String? address, - required int? satoshiAmount, + required int? amount, Map? args, }) => (super.noSuchMethod( @@ -2880,7 +2912,7 @@ class MockManager extends _i1.Mock implements _i6.Manager { [], { #address: address, - #satoshiAmount: satoshiAmount, + #satoshiAmount: amount, #args: args, }, ), @@ -3183,7 +3215,7 @@ class MockCoinServiceAPI extends _i1.Mock implements _i19.CoinServiceAPI { @override _i22.Future> prepareSend({ required String? address, - required int? satoshiAmount, + required int? amount, Map? args, }) => (super.noSuchMethod( @@ -3192,7 +3224,7 @@ class MockCoinServiceAPI extends _i1.Mock implements _i19.CoinServiceAPI { [], { #address: address, - #satoshiAmount: satoshiAmount, + #satoshiAmount: amount, #args: args, }, ), diff --git a/test/price_test.dart b/test/price_test.dart index 6b98b67d1..b93255976 100644 --- a/test/price_test.dart +++ b/test/price_test.dart @@ -6,7 +6,7 @@ import 'package:hive_test/hive_test.dart'; import 'package:http/http.dart'; import 'package:mockito/annotations.dart'; import 'package:mockito/mockito.dart'; -import 'package:stackwallet/hive/db.dart'; +import 'package:stackwallet/db/hive/db.dart'; import 'package:stackwallet/services/price.dart'; import 'price_test.mocks.dart'; @@ -26,7 +26,10 @@ void main() { when(client.get( Uri.parse( - "https://api.coingecko.com/api/v3/coins/markets?vs_currency=btc&ids=monero,bitcoin,litecoin,epic-cash,zcoin,dogecoin,bitcoin-cash,namecoin,wownero,particl&order=market_cap_desc&per_page=10&page=1&sparkline=false"), + "https://api.coingecko.com/api/v3/coins/markets?vs_currency=btc&ids" + "=monero,bitcoin,litecoin,epic-cash,zcoin,dogecoin,bitcoin-cash" + ",namecoin,wownero,ethereum,particl&order=market_cap_desc&per_page=50" + "&page=1&sparkline=false"), headers: { 'Content-Type': 'application/json' })).thenAnswer((_) async => Response( @@ -38,11 +41,26 @@ void main() { final price = await priceAPI.getPricesAnd24hChange(baseCurrency: "btc"); - expect(price.toString(), - '{Coin.bitcoin: [1, 0.0], Coin.bitcoincash: [0, 0.0], Coin.dogecoin: [0.00000315, -2.68533], Coin.epicCash: [0.00002803, 7.27524], Coin.firo: [0.0001096, -0.89304], Coin.litecoin: [0, 0.0], Coin.monero: [0.00717236, -0.77656], Coin.namecoin: [0, 0.0], Coin.particl: [0, 0.0], Coin.wownero: [0, 0.0], Coin.bitcoinTestNet: [0, 0.0], Coin.litecoinTestNet: [0, 0.0], Coin.bitcoincashTestnet: [0, 0.0], Coin.dogecoinTestNet: [0, 0.0], Coin.firoTestNet: [0, 0.0]}'); + expect( + price.toString(), + '{Coin.bitcoin: [1, 0.0], Coin.bitcoincash: [0, 0.0], ' + 'Coin.dogecoin: [0.00000315, -2.68533], ' + 'Coin.epicCash: [0.00002803, 7.27524], ' + 'Coin.ethereum: [0, 0.0], ' + 'Coin.firo: [0.0001096, -0.89304], Coin.litecoin: [0, 0.0], ' + 'Coin.monero: [0.00717236, -0.77656], Coin.namecoin: [0, 0.0], ' + 'Coin.particl: [0, 0.0], Coin.wownero: [0, 0.0], ' + 'Coin.bitcoinTestNet: [0, 0.0],' + ' Coin.litecoinTestNet: [0, 0.0], Coin.bitcoincashTestnet: [0, 0.0], ' + 'Coin.dogecoinTestNet: [0, 0.0], Coin.firoTestNet: [0, 0.0]}', + ); verify(client.get( Uri.parse( - "https://api.coingecko.com/api/v3/coins/markets?vs_currency=btc&ids=monero,bitcoin,litecoin,epic-cash,zcoin,dogecoin,bitcoin-cash,namecoin,wownero,particl&order=market_cap_desc&per_page=10&page=1&sparkline=false"), + "https://api.coingecko.com/api/v3/coins/markets?vs_currency=btc" + "&ids=monero,bitcoin,litecoin,epic-cash,zcoin,dogecoin," + "bitcoin-cash,namecoin,wownero,ethereum,particl" + "&order=market_cap_desc&per_page=50&page=1&sparkline=false", + ), headers: {'Content-Type': 'application/json'})).called(1); verifyNoMoreInteractions(client); @@ -53,7 +71,10 @@ void main() { when(client.get( Uri.parse( - "https://api.coingecko.com/api/v3/coins/markets?vs_currency=btc&ids=monero,bitcoin,litecoin,epic-cash,zcoin,dogecoin,bitcoin-cash,namecoin,wownero,particl&order=market_cap_desc&per_page=10&page=1&sparkline=false"), + "https://api.coingecko.com/api/v3/coins/markets?vs_currency=btc&" + "ids=monero,bitcoin,litecoin,epic-cash,zcoin,dogecoin," + "bitcoin-cash,namecoin,wownero,ethereum,particl" + "&order=market_cap_desc&per_page=50&page=1&sparkline=false"), headers: { 'Content-Type': 'application/json' })).thenAnswer((_) async => Response( @@ -70,13 +91,21 @@ void main() { final cachedPrice = await priceAPI.getPricesAnd24hChange(baseCurrency: "btc"); - expect(cachedPrice.toString(), - '{Coin.bitcoin: [1, 0.0], Coin.bitcoincash: [0, 0.0], Coin.dogecoin: [0.00000315, -2.68533], Coin.epicCash: [0.00002803, 7.27524], Coin.firo: [0.0001096, -0.89304], Coin.litecoin: [0, 0.0], Coin.monero: [0.00717236, -0.77656], Coin.namecoin: [0, 0.0], Coin.particl: [0, 0.0], Coin.wownero: [0, 0.0], Coin.bitcoinTestNet: [0, 0.0], Coin.litecoinTestNet: [0, 0.0], Coin.bitcoincashTestnet: [0, 0.0], Coin.dogecoinTestNet: [0, 0.0], Coin.firoTestNet: [0, 0.0]}'); + expect( + cachedPrice.toString(), + '{Coin.bitcoin: [1, 0.0], Coin.bitcoincash: [0, 0.0],' + ' Coin.dogecoin: [0.00000315, -2.68533], Coin.epicCash: [0.00002803, 7.27524],' + ' Coin.ethereum: [0, 0.0], Coin.firo: [0.0001096, -0.89304], ' + 'Coin.litecoin: [0, 0.0], Coin.monero: [0.00717236, -0.77656], ' + 'Coin.namecoin: [0, 0.0], Coin.particl: [0, 0.0], Coin.wownero: [0, 0.0], ' + 'Coin.bitcoinTestNet: [0, 0.0], Coin.litecoinTestNet: [0, 0.0], ' + 'Coin.bitcoincashTestnet: [0, 0.0], Coin.dogecoinTestNet: [0, 0.0], ' + 'Coin.firoTestNet: [0, 0.0]}'); // verify only called once during filling of cache verify(client.get( Uri.parse( - "https://api.coingecko.com/api/v3/coins/markets?vs_currency=btc&ids=monero,bitcoin,litecoin,epic-cash,zcoin,dogecoin,bitcoin-cash,namecoin,wownero,particl&order=market_cap_desc&per_page=10&page=1&sparkline=false"), + "https://api.coingecko.com/api/v3/coins/markets?vs_currency=btc&ids=monero,bitcoin,litecoin,epic-cash,zcoin,dogecoin,bitcoin-cash,namecoin,wownero,ethereum,particl&order=market_cap_desc&per_page=50&page=1&sparkline=false"), headers: {'Content-Type': 'application/json'})).called(1); verifyNoMoreInteractions(client); @@ -87,7 +116,10 @@ void main() { when(client.get( Uri.parse( - "https://api.coingecko.com/api/v3/coins/markets?vs_currency=btc&ids=monero,bitcoin,litecoin,epic-cash,zcoin,dogecoin,bitcoin-cash,namecoin,wownero,particl&order=market_cap_desc&per_page=10&page=1&sparkline=false"), + "https://api.coingecko.com/api/v3/coins/markets?vs_currency=btc" + "&ids=monero,bitcoin,litecoin,epic-cash,zcoin,dogecoin," + "bitcoin-cash,namecoin,wownero,ethereum,particl" + "&order=market_cap_desc&per_page=50&page=1&sparkline=false"), headers: { 'Content-Type': 'application/json' })).thenAnswer((_) async => Response( @@ -100,7 +132,7 @@ void main() { final price = await priceAPI.getPricesAnd24hChange(baseCurrency: "btc"); expect(price.toString(), - '{Coin.bitcoin: [0, 0.0], Coin.bitcoincash: [0, 0.0], Coin.dogecoin: [0, 0.0], Coin.epicCash: [0, 0.0], Coin.firo: [0, 0.0], Coin.litecoin: [0, 0.0], Coin.monero: [0, 0.0], Coin.namecoin: [0, 0.0], Coin.particl: [0, 0.0], Coin.wownero: [0, 0.0], Coin.bitcoinTestNet: [0, 0.0], Coin.litecoinTestNet: [0, 0.0], Coin.bitcoincashTestnet: [0, 0.0], Coin.dogecoinTestNet: [0, 0.0], Coin.firoTestNet: [0, 0.0]}'); + '{Coin.bitcoin: [0, 0.0], Coin.bitcoincash: [0, 0.0], Coin.dogecoin: [0, 0.0], Coin.epicCash: [0, 0.0], Coin.ethereum: [0, 0.0], Coin.firo: [0, 0.0], Coin.litecoin: [0, 0.0], Coin.monero: [0, 0.0], Coin.namecoin: [0, 0.0], Coin.particl: [0, 0.0], Coin.wownero: [0, 0.0], Coin.bitcoinTestNet: [0, 0.0], Coin.litecoinTestNet: [0, 0.0], Coin.bitcoincashTestnet: [0, 0.0], Coin.dogecoinTestNet: [0, 0.0], Coin.firoTestNet: [0, 0.0]}'); }); test("no internet available", () async { @@ -108,7 +140,10 @@ void main() { when(client.get( Uri.parse( - "https://api.coingecko.com/api/v3/coins/markets?vs_currency=btc&ids=monero,bitcoin,litecoin,epic-cash,zcoin,dogecoin,bitcoin-cash,namecoin,wownero,particl&order=market_cap_desc&per_page=10&page=1&sparkline=false"), + "https://api.coingecko.com/api/v3/coins/markets?vs_currency=btc" + "&ids=monero,bitcoin,litecoin,epic-cash,zcoin,dogecoin," + "bitcoin-cash,namecoin,wownero,ethereum,particl" + "&order=market_cap_desc&per_page=50&page=1&sparkline=false"), headers: { 'Content-Type': 'application/json' })).thenThrow(const SocketException( @@ -119,8 +154,15 @@ void main() { final price = await priceAPI.getPricesAnd24hChange(baseCurrency: "btc"); - expect(price.toString(), - '{Coin.bitcoin: [0, 0.0], Coin.bitcoincash: [0, 0.0], Coin.dogecoin: [0, 0.0], Coin.epicCash: [0, 0.0], Coin.firo: [0, 0.0], Coin.litecoin: [0, 0.0], Coin.monero: [0, 0.0], Coin.namecoin: [0, 0.0], Coin.particl: [0, 0.0], Coin.wownero: [0, 0.0], Coin.bitcoinTestNet: [0, 0.0], Coin.litecoinTestNet: [0, 0.0], Coin.bitcoincashTestnet: [0, 0.0], Coin.dogecoinTestNet: [0, 0.0], Coin.firoTestNet: [0, 0.0]}'); + expect( + price.toString(), + '{Coin.bitcoin: [0, 0.0], Coin.bitcoincash: [0, 0.0], ' + 'Coin.dogecoin: [0, 0.0], Coin.epicCash: [0, 0.0], Coin.ethereum: [0, 0.0],' + ' Coin.firo: [0, 0.0], Coin.litecoin: [0, 0.0], Coin.monero: [0, 0.0],' + ' Coin.namecoin: [0, 0.0], Coin.particl: [0, 0.0], Coin.wownero: [0, 0.0],' + ' Coin.bitcoinTestNet: [0, 0.0], Coin.litecoinTestNet: [0, 0.0], ' + 'Coin.bitcoincashTestnet: [0, 0.0], Coin.dogecoinTestNet: [0, 0.0], ' + 'Coin.firoTestNet: [0, 0.0]}'); }); tearDown(() async { diff --git a/test/screen_tests/address_book_view/subviews/add_address_book_view_screen_test.mocks.dart b/test/screen_tests/address_book_view/subviews/add_address_book_view_screen_test.mocks.dart index a36548d2d..72ac2d172 100644 --- a/test/screen_tests/address_book_view/subviews/add_address_book_view_screen_test.mocks.dart +++ b/test/screen_tests/address_book_view/subviews/add_address_book_view_screen_test.mocks.dart @@ -378,6 +378,11 @@ class MockManager extends _i1.Mock implements _i11.Manager { returnValue: false, ) as bool); @override + bool get hasTokenSupport => (super.noSuchMethod( + Invocation.getter(#hasTokenSupport), + returnValue: false, + ) as bool); + @override bool get hasWhirlpoolSupport => (super.noSuchMethod( Invocation.getter(#hasWhirlpoolSupport), returnValue: false, @@ -412,7 +417,7 @@ class MockManager extends _i1.Mock implements _i11.Manager { @override _i8.Future> prepareSend({ required String? address, - required int? satoshiAmount, + required int? amount, Map? args, }) => (super.noSuchMethod( @@ -421,7 +426,7 @@ class MockManager extends _i1.Mock implements _i11.Manager { [], { #address: address, - #satoshiAmount: satoshiAmount, + #satoshiAmount: amount, #args: args, }, ), diff --git a/test/screen_tests/address_book_view/subviews/address_book_entry_details_view_screen_test.mocks.dart b/test/screen_tests/address_book_view/subviews/address_book_entry_details_view_screen_test.mocks.dart index b78f3f23e..bcf9e5386 100644 --- a/test/screen_tests/address_book_view/subviews/address_book_entry_details_view_screen_test.mocks.dart +++ b/test/screen_tests/address_book_view/subviews/address_book_entry_details_view_screen_test.mocks.dart @@ -339,6 +339,11 @@ class MockManager extends _i1.Mock implements _i9.Manager { returnValue: false, ) as bool); @override + bool get hasTokenSupport => (super.noSuchMethod( + Invocation.getter(#hasTokenSupport), + returnValue: false, + ) as bool); + @override bool get hasWhirlpoolSupport => (super.noSuchMethod( Invocation.getter(#hasWhirlpoolSupport), returnValue: false, @@ -373,7 +378,7 @@ class MockManager extends _i1.Mock implements _i9.Manager { @override _i7.Future> prepareSend({ required String? address, - required int? satoshiAmount, + required int? amount, Map? args, }) => (super.noSuchMethod( @@ -382,7 +387,7 @@ class MockManager extends _i1.Mock implements _i9.Manager { [], { #address: address, - #satoshiAmount: satoshiAmount, + #satoshiAmount: amount, #args: args, }, ), diff --git a/test/screen_tests/address_book_view/subviews/edit_address_book_entry_view_screen_test.mocks.dart b/test/screen_tests/address_book_view/subviews/edit_address_book_entry_view_screen_test.mocks.dart index 68697c039..3cc4c9852 100644 --- a/test/screen_tests/address_book_view/subviews/edit_address_book_entry_view_screen_test.mocks.dart +++ b/test/screen_tests/address_book_view/subviews/edit_address_book_entry_view_screen_test.mocks.dart @@ -337,6 +337,11 @@ class MockManager extends _i1.Mock implements _i9.Manager { returnValue: false, ) as bool); @override + bool get hasTokenSupport => (super.noSuchMethod( + Invocation.getter(#hasTokenSupport), + returnValue: false, + ) as bool); + @override bool get hasWhirlpoolSupport => (super.noSuchMethod( Invocation.getter(#hasWhirlpoolSupport), returnValue: false, @@ -371,7 +376,7 @@ class MockManager extends _i1.Mock implements _i9.Manager { @override _i7.Future> prepareSend({ required String? address, - required int? satoshiAmount, + required int? amount, Map? args, }) => (super.noSuchMethod( @@ -380,7 +385,7 @@ class MockManager extends _i1.Mock implements _i9.Manager { [], { #address: address, - #satoshiAmount: satoshiAmount, + #satoshiAmount: amount, #args: args, }, ), diff --git a/test/screen_tests/exchange/exchange_view_test.mocks.dart b/test/screen_tests/exchange/exchange_view_test.mocks.dart index d926e9e5b..7a0074677 100644 --- a/test/screen_tests/exchange/exchange_view_test.mocks.dart +++ b/test/screen_tests/exchange/exchange_view_test.mocks.dart @@ -732,6 +732,23 @@ class MockChangeNowAPI extends _i1.Mock implements _i12.ChangeNowAPI { )), ) as _i7.Future<_i2.ExchangeResponse>>); @override + _i7.Future<_i2.ExchangeResponse>> getCurrenciesV2() => + (super.noSuchMethod( + Invocation.method( + #getCurrenciesV2, + [], + ), + returnValue: + _i7.Future<_i2.ExchangeResponse>>.value( + _FakeExchangeResponse_0>( + this, + Invocation.method( + #getCurrenciesV2, + [], + ), + )), + ) as _i7.Future<_i2.ExchangeResponse>>); + @override _i7.Future<_i2.ExchangeResponse>> getPairedCurrencies({ required String? ticker, bool? fixedRate, diff --git a/test/screen_tests/lockscreen_view_screen_test.mocks.dart b/test/screen_tests/lockscreen_view_screen_test.mocks.dart index 439ecd5c9..51a9b0558 100644 --- a/test/screen_tests/lockscreen_view_screen_test.mocks.dart +++ b/test/screen_tests/lockscreen_view_screen_test.mocks.dart @@ -107,6 +107,14 @@ class MockWalletsService extends _i1.Mock implements _i6.WalletsService { returnValue: _i7.Future.value(false), ) as _i7.Future); @override + Map fetchWalletsData() => (super.noSuchMethod( + Invocation.method( + #fetchWalletsData, + [], + ), + returnValue: {}, + ) as Map); + @override _i7.Future addExistingStackWallet({ required String? name, required String? walletId, @@ -646,6 +654,11 @@ class MockManager extends _i1.Mock implements _i12.Manager { returnValue: false, ) as bool); @override + bool get hasTokenSupport => (super.noSuchMethod( + Invocation.getter(#hasTokenSupport), + returnValue: false, + ) as bool); + @override bool get hasWhirlpoolSupport => (super.noSuchMethod( Invocation.getter(#hasWhirlpoolSupport), returnValue: false, @@ -680,7 +693,7 @@ class MockManager extends _i1.Mock implements _i12.Manager { @override _i7.Future> prepareSend({ required String? address, - required int? satoshiAmount, + required int? amount, Map? args, }) => (super.noSuchMethod( @@ -689,7 +702,7 @@ class MockManager extends _i1.Mock implements _i12.Manager { [], { #address: address, - #satoshiAmount: satoshiAmount, + #satoshiAmount: amount, #args: args, }, ), diff --git a/test/screen_tests/main_view_tests/main_view_screen_testA_test.mocks.dart b/test/screen_tests/main_view_tests/main_view_screen_testA_test.mocks.dart index d624d4171..6cc5c84a9 100644 --- a/test/screen_tests/main_view_tests/main_view_screen_testA_test.mocks.dart +++ b/test/screen_tests/main_view_tests/main_view_screen_testA_test.mocks.dart @@ -94,6 +94,14 @@ class MockWalletsService extends _i1.Mock implements _i5.WalletsService { returnValue: _i6.Future.value(false), ) as _i6.Future); @override + Map fetchWalletsData() => (super.noSuchMethod( + Invocation.method( + #fetchWalletsData, + [], + ), + returnValue: {}, + ) as Map); + @override _i6.Future addExistingStackWallet({ required String? name, required String? walletId, @@ -433,6 +441,11 @@ class MockManager extends _i1.Mock implements _i9.Manager { returnValue: false, ) as bool); @override + bool get hasTokenSupport => (super.noSuchMethod( + Invocation.getter(#hasTokenSupport), + returnValue: false, + ) as bool); + @override bool get hasWhirlpoolSupport => (super.noSuchMethod( Invocation.getter(#hasWhirlpoolSupport), returnValue: false, @@ -467,7 +480,7 @@ class MockManager extends _i1.Mock implements _i9.Manager { @override _i6.Future> prepareSend({ required String? address, - required int? satoshiAmount, + required int? amount, Map? args, }) => (super.noSuchMethod( @@ -476,7 +489,7 @@ class MockManager extends _i1.Mock implements _i9.Manager { [], { #address: address, - #satoshiAmount: satoshiAmount, + #satoshiAmount: amount, #args: args, }, ), diff --git a/test/screen_tests/main_view_tests/main_view_screen_testB_test.mocks.dart b/test/screen_tests/main_view_tests/main_view_screen_testB_test.mocks.dart index 70c08470b..70296abd2 100644 --- a/test/screen_tests/main_view_tests/main_view_screen_testB_test.mocks.dart +++ b/test/screen_tests/main_view_tests/main_view_screen_testB_test.mocks.dart @@ -94,6 +94,14 @@ class MockWalletsService extends _i1.Mock implements _i5.WalletsService { returnValue: _i6.Future.value(false), ) as _i6.Future); @override + Map fetchWalletsData() => (super.noSuchMethod( + Invocation.method( + #fetchWalletsData, + [], + ), + returnValue: {}, + ) as Map); + @override _i6.Future addExistingStackWallet({ required String? name, required String? walletId, @@ -433,6 +441,11 @@ class MockManager extends _i1.Mock implements _i9.Manager { returnValue: false, ) as bool); @override + bool get hasTokenSupport => (super.noSuchMethod( + Invocation.getter(#hasTokenSupport), + returnValue: false, + ) as bool); + @override bool get hasWhirlpoolSupport => (super.noSuchMethod( Invocation.getter(#hasWhirlpoolSupport), returnValue: false, @@ -467,7 +480,7 @@ class MockManager extends _i1.Mock implements _i9.Manager { @override _i6.Future> prepareSend({ required String? address, - required int? satoshiAmount, + required int? amount, Map? args, }) => (super.noSuchMethod( @@ -476,7 +489,7 @@ class MockManager extends _i1.Mock implements _i9.Manager { [], { #address: address, - #satoshiAmount: satoshiAmount, + #satoshiAmount: amount, #args: args, }, ), diff --git a/test/screen_tests/main_view_tests/main_view_screen_testC_test.mocks.dart b/test/screen_tests/main_view_tests/main_view_screen_testC_test.mocks.dart index 1f2176350..b5860e0cd 100644 --- a/test/screen_tests/main_view_tests/main_view_screen_testC_test.mocks.dart +++ b/test/screen_tests/main_view_tests/main_view_screen_testC_test.mocks.dart @@ -94,6 +94,14 @@ class MockWalletsService extends _i1.Mock implements _i5.WalletsService { returnValue: _i6.Future.value(false), ) as _i6.Future); @override + Map fetchWalletsData() => (super.noSuchMethod( + Invocation.method( + #fetchWalletsData, + [], + ), + returnValue: {}, + ) as Map); + @override _i6.Future addExistingStackWallet({ required String? name, required String? walletId, @@ -433,6 +441,11 @@ class MockManager extends _i1.Mock implements _i9.Manager { returnValue: false, ) as bool); @override + bool get hasTokenSupport => (super.noSuchMethod( + Invocation.getter(#hasTokenSupport), + returnValue: false, + ) as bool); + @override bool get hasWhirlpoolSupport => (super.noSuchMethod( Invocation.getter(#hasWhirlpoolSupport), returnValue: false, @@ -467,7 +480,7 @@ class MockManager extends _i1.Mock implements _i9.Manager { @override _i6.Future> prepareSend({ required String? address, - required int? satoshiAmount, + required int? amount, Map? args, }) => (super.noSuchMethod( @@ -476,7 +489,7 @@ class MockManager extends _i1.Mock implements _i9.Manager { [], { #address: address, - #satoshiAmount: satoshiAmount, + #satoshiAmount: amount, #args: args, }, ), diff --git a/test/screen_tests/onboarding/backup_key_view_screen_test.mocks.dart b/test/screen_tests/onboarding/backup_key_view_screen_test.mocks.dart index 7377d155e..ce6a5fa73 100644 --- a/test/screen_tests/onboarding/backup_key_view_screen_test.mocks.dart +++ b/test/screen_tests/onboarding/backup_key_view_screen_test.mocks.dart @@ -208,6 +208,11 @@ class MockManager extends _i1.Mock implements _i5.Manager { returnValue: false, ) as bool); @override + bool get hasTokenSupport => (super.noSuchMethod( + Invocation.getter(#hasTokenSupport), + returnValue: false, + ) as bool); + @override bool get hasWhirlpoolSupport => (super.noSuchMethod( Invocation.getter(#hasWhirlpoolSupport), returnValue: false, @@ -242,7 +247,7 @@ class MockManager extends _i1.Mock implements _i5.Manager { @override _i7.Future> prepareSend({ required String? address, - required int? satoshiAmount, + required int? amount, Map? args, }) => (super.noSuchMethod( @@ -251,7 +256,7 @@ class MockManager extends _i1.Mock implements _i5.Manager { [], { #address: address, - #satoshiAmount: satoshiAmount, + #satoshiAmount: amount, #args: args, }, ), diff --git a/test/screen_tests/onboarding/backup_key_warning_view_screen_test.mocks.dart b/test/screen_tests/onboarding/backup_key_warning_view_screen_test.mocks.dart index 2b31cb96c..dd1a445be 100644 --- a/test/screen_tests/onboarding/backup_key_warning_view_screen_test.mocks.dart +++ b/test/screen_tests/onboarding/backup_key_warning_view_screen_test.mocks.dart @@ -92,6 +92,14 @@ class MockWalletsService extends _i1.Mock implements _i5.WalletsService { returnValue: _i6.Future.value(false), ) as _i6.Future); @override + Map fetchWalletsData() => (super.noSuchMethod( + Invocation.method( + #fetchWalletsData, + [], + ), + returnValue: {}, + ) as Map); + @override _i6.Future addExistingStackWallet({ required String? name, required String? walletId, @@ -431,6 +439,11 @@ class MockManager extends _i1.Mock implements _i9.Manager { returnValue: false, ) as bool); @override + bool get hasTokenSupport => (super.noSuchMethod( + Invocation.getter(#hasTokenSupport), + returnValue: false, + ) as bool); + @override bool get hasWhirlpoolSupport => (super.noSuchMethod( Invocation.getter(#hasWhirlpoolSupport), returnValue: false, @@ -465,7 +478,7 @@ class MockManager extends _i1.Mock implements _i9.Manager { @override _i6.Future> prepareSend({ required String? address, - required int? satoshiAmount, + required int? amount, Map? args, }) => (super.noSuchMethod( @@ -474,7 +487,7 @@ class MockManager extends _i1.Mock implements _i9.Manager { [], { #address: address, - #satoshiAmount: satoshiAmount, + #satoshiAmount: amount, #args: args, }, ), diff --git a/test/screen_tests/onboarding/create_pin_view_screen_test.mocks.dart b/test/screen_tests/onboarding/create_pin_view_screen_test.mocks.dart index 54eec30cf..0be6762a5 100644 --- a/test/screen_tests/onboarding/create_pin_view_screen_test.mocks.dart +++ b/test/screen_tests/onboarding/create_pin_view_screen_test.mocks.dart @@ -107,6 +107,14 @@ class MockWalletsService extends _i1.Mock implements _i6.WalletsService { returnValue: _i7.Future.value(false), ) as _i7.Future); @override + Map fetchWalletsData() => (super.noSuchMethod( + Invocation.method( + #fetchWalletsData, + [], + ), + returnValue: {}, + ) as Map); + @override _i7.Future addExistingStackWallet({ required String? name, required String? walletId, @@ -646,6 +654,11 @@ class MockManager extends _i1.Mock implements _i12.Manager { returnValue: false, ) as bool); @override + bool get hasTokenSupport => (super.noSuchMethod( + Invocation.getter(#hasTokenSupport), + returnValue: false, + ) as bool); + @override bool get hasWhirlpoolSupport => (super.noSuchMethod( Invocation.getter(#hasWhirlpoolSupport), returnValue: false, @@ -680,7 +693,7 @@ class MockManager extends _i1.Mock implements _i12.Manager { @override _i7.Future> prepareSend({ required String? address, - required int? satoshiAmount, + required int? amount, Map? args, }) => (super.noSuchMethod( @@ -689,7 +702,7 @@ class MockManager extends _i1.Mock implements _i12.Manager { [], { #address: address, - #satoshiAmount: satoshiAmount, + #satoshiAmount: amount, #args: args, }, ), diff --git a/test/screen_tests/onboarding/name_your_wallet_view_screen_test.mocks.dart b/test/screen_tests/onboarding/name_your_wallet_view_screen_test.mocks.dart index 29cdf0642..65cd8fea1 100644 --- a/test/screen_tests/onboarding/name_your_wallet_view_screen_test.mocks.dart +++ b/test/screen_tests/onboarding/name_your_wallet_view_screen_test.mocks.dart @@ -56,6 +56,14 @@ class MockWalletsService extends _i1.Mock implements _i2.WalletsService { returnValue: _i3.Future.value(false), ) as _i3.Future); @override + Map fetchWalletsData() => (super.noSuchMethod( + Invocation.method( + #fetchWalletsData, + [], + ), + returnValue: {}, + ) as Map); + @override _i3.Future addExistingStackWallet({ required String? name, required String? walletId, diff --git a/test/screen_tests/onboarding/restore_wallet_view_screen_test.mocks.dart b/test/screen_tests/onboarding/restore_wallet_view_screen_test.mocks.dart index e92f4d289..ce7c6a728 100644 --- a/test/screen_tests/onboarding/restore_wallet_view_screen_test.mocks.dart +++ b/test/screen_tests/onboarding/restore_wallet_view_screen_test.mocks.dart @@ -148,6 +148,14 @@ class MockWalletsService extends _i1.Mock implements _i9.WalletsService { returnValue: _i8.Future.value(false), ) as _i8.Future); @override + Map fetchWalletsData() => (super.noSuchMethod( + Invocation.method( + #fetchWalletsData, + [], + ), + returnValue: {}, + ) as Map); + @override _i8.Future addExistingStackWallet({ required String? name, required String? walletId, @@ -487,6 +495,11 @@ class MockManager extends _i1.Mock implements _i12.Manager { returnValue: false, ) as bool); @override + bool get hasTokenSupport => (super.noSuchMethod( + Invocation.getter(#hasTokenSupport), + returnValue: false, + ) as bool); + @override bool get hasWhirlpoolSupport => (super.noSuchMethod( Invocation.getter(#hasWhirlpoolSupport), returnValue: false, @@ -521,7 +534,7 @@ class MockManager extends _i1.Mock implements _i12.Manager { @override _i8.Future> prepareSend({ required String? address, - required int? satoshiAmount, + required int? amount, Map? args, }) => (super.noSuchMethod( @@ -530,7 +543,7 @@ class MockManager extends _i1.Mock implements _i12.Manager { [], { #address: address, - #satoshiAmount: satoshiAmount, + #satoshiAmount: amount, #args: args, }, ), diff --git a/test/screen_tests/onboarding/verify_backup_key_view_screen_test.mocks.dart b/test/screen_tests/onboarding/verify_backup_key_view_screen_test.mocks.dart index 13a6568c8..2df1a8d62 100644 --- a/test/screen_tests/onboarding/verify_backup_key_view_screen_test.mocks.dart +++ b/test/screen_tests/onboarding/verify_backup_key_view_screen_test.mocks.dart @@ -208,6 +208,11 @@ class MockManager extends _i1.Mock implements _i5.Manager { returnValue: false, ) as bool); @override + bool get hasTokenSupport => (super.noSuchMethod( + Invocation.getter(#hasTokenSupport), + returnValue: false, + ) as bool); + @override bool get hasWhirlpoolSupport => (super.noSuchMethod( Invocation.getter(#hasWhirlpoolSupport), returnValue: false, @@ -242,7 +247,7 @@ class MockManager extends _i1.Mock implements _i5.Manager { @override _i7.Future> prepareSend({ required String? address, - required int? satoshiAmount, + required int? amount, Map? args, }) => (super.noSuchMethod( @@ -251,7 +256,7 @@ class MockManager extends _i1.Mock implements _i5.Manager { [], { #address: address, - #satoshiAmount: satoshiAmount, + #satoshiAmount: amount, #args: args, }, ), diff --git a/test/screen_tests/settings_view/settings_subviews/currency_view_screen_test.mocks.dart b/test/screen_tests/settings_view/settings_subviews/currency_view_screen_test.mocks.dart index 82f4bcd3e..5c48821f8 100644 --- a/test/screen_tests/settings_view/settings_subviews/currency_view_screen_test.mocks.dart +++ b/test/screen_tests/settings_view/settings_subviews/currency_view_screen_test.mocks.dart @@ -208,6 +208,11 @@ class MockManager extends _i1.Mock implements _i5.Manager { returnValue: false, ) as bool); @override + bool get hasTokenSupport => (super.noSuchMethod( + Invocation.getter(#hasTokenSupport), + returnValue: false, + ) as bool); + @override bool get hasWhirlpoolSupport => (super.noSuchMethod( Invocation.getter(#hasWhirlpoolSupport), returnValue: false, @@ -242,7 +247,7 @@ class MockManager extends _i1.Mock implements _i5.Manager { @override _i7.Future> prepareSend({ required String? address, - required int? satoshiAmount, + required int? amount, Map? args, }) => (super.noSuchMethod( @@ -251,7 +256,7 @@ class MockManager extends _i1.Mock implements _i5.Manager { [], { #address: address, - #satoshiAmount: satoshiAmount, + #satoshiAmount: amount, #args: args, }, ), diff --git a/test/screen_tests/settings_view/settings_subviews/network_settings_subviews/add_custom_node_view_screen_test.mocks.dart b/test/screen_tests/settings_view/settings_subviews/network_settings_subviews/add_custom_node_view_screen_test.mocks.dart index fc4171b63..09a6f027e 100644 --- a/test/screen_tests/settings_view/settings_subviews/network_settings_subviews/add_custom_node_view_screen_test.mocks.dart +++ b/test/screen_tests/settings_view/settings_subviews/network_settings_subviews/add_custom_node_view_screen_test.mocks.dart @@ -423,6 +423,11 @@ class MockManager extends _i1.Mock implements _i11.Manager { returnValue: false, ) as bool); @override + bool get hasTokenSupport => (super.noSuchMethod( + Invocation.getter(#hasTokenSupport), + returnValue: false, + ) as bool); + @override bool get hasWhirlpoolSupport => (super.noSuchMethod( Invocation.getter(#hasWhirlpoolSupport), returnValue: false, @@ -457,7 +462,7 @@ class MockManager extends _i1.Mock implements _i11.Manager { @override _i8.Future> prepareSend({ required String? address, - required int? satoshiAmount, + required int? amount, Map? args, }) => (super.noSuchMethod( @@ -466,7 +471,7 @@ class MockManager extends _i1.Mock implements _i11.Manager { [], { #address: address, - #satoshiAmount: satoshiAmount, + #satoshiAmount: amount, #args: args, }, ), diff --git a/test/screen_tests/settings_view/settings_subviews/network_settings_subviews/node_details_view_screen_test.mocks.dart b/test/screen_tests/settings_view/settings_subviews/network_settings_subviews/node_details_view_screen_test.mocks.dart index f990ec83a..689918939 100644 --- a/test/screen_tests/settings_view/settings_subviews/network_settings_subviews/node_details_view_screen_test.mocks.dart +++ b/test/screen_tests/settings_view/settings_subviews/network_settings_subviews/node_details_view_screen_test.mocks.dart @@ -423,6 +423,11 @@ class MockManager extends _i1.Mock implements _i11.Manager { returnValue: false, ) as bool); @override + bool get hasTokenSupport => (super.noSuchMethod( + Invocation.getter(#hasTokenSupport), + returnValue: false, + ) as bool); + @override bool get hasWhirlpoolSupport => (super.noSuchMethod( Invocation.getter(#hasWhirlpoolSupport), returnValue: false, @@ -457,7 +462,7 @@ class MockManager extends _i1.Mock implements _i11.Manager { @override _i8.Future> prepareSend({ required String? address, - required int? satoshiAmount, + required int? amount, Map? args, }) => (super.noSuchMethod( @@ -466,7 +471,7 @@ class MockManager extends _i1.Mock implements _i11.Manager { [], { #address: address, - #satoshiAmount: satoshiAmount, + #satoshiAmount: amount, #args: args, }, ), diff --git a/test/screen_tests/settings_view/settings_subviews/wallet_backup_view_screen_test.mocks.dart b/test/screen_tests/settings_view/settings_subviews/wallet_backup_view_screen_test.mocks.dart index 44dd68cee..8a88c264c 100644 --- a/test/screen_tests/settings_view/settings_subviews/wallet_backup_view_screen_test.mocks.dart +++ b/test/screen_tests/settings_view/settings_subviews/wallet_backup_view_screen_test.mocks.dart @@ -208,6 +208,11 @@ class MockManager extends _i1.Mock implements _i5.Manager { returnValue: false, ) as bool); @override + bool get hasTokenSupport => (super.noSuchMethod( + Invocation.getter(#hasTokenSupport), + returnValue: false, + ) as bool); + @override bool get hasWhirlpoolSupport => (super.noSuchMethod( Invocation.getter(#hasWhirlpoolSupport), returnValue: false, @@ -242,7 +247,7 @@ class MockManager extends _i1.Mock implements _i5.Manager { @override _i7.Future> prepareSend({ required String? address, - required int? satoshiAmount, + required int? amount, Map? args, }) => (super.noSuchMethod( @@ -251,7 +256,7 @@ class MockManager extends _i1.Mock implements _i5.Manager { [], { #address: address, - #satoshiAmount: satoshiAmount, + #satoshiAmount: amount, #args: args, }, ), diff --git a/test/screen_tests/settings_view/settings_subviews/wallet_settings_subviews/change_pin_view_screen_test.mocks.dart b/test/screen_tests/settings_view/settings_subviews/wallet_settings_subviews/change_pin_view_screen_test.mocks.dart index 280d08e09..799feb0cb 100644 --- a/test/screen_tests/settings_view/settings_subviews/wallet_settings_subviews/change_pin_view_screen_test.mocks.dart +++ b/test/screen_tests/settings_view/settings_subviews/wallet_settings_subviews/change_pin_view_screen_test.mocks.dart @@ -56,6 +56,14 @@ class MockWalletsService extends _i1.Mock implements _i2.WalletsService { returnValue: _i3.Future.value(false), ) as _i3.Future); @override + Map fetchWalletsData() => (super.noSuchMethod( + Invocation.method( + #fetchWalletsData, + [], + ), + returnValue: {}, + ) as Map); + @override _i3.Future addExistingStackWallet({ required String? name, required String? walletId, diff --git a/test/screen_tests/settings_view/settings_subviews/wallet_settings_subviews/rename_wallet_view_screen_test.mocks.dart b/test/screen_tests/settings_view/settings_subviews/wallet_settings_subviews/rename_wallet_view_screen_test.mocks.dart index 12e07fd2f..b11c9ad89 100644 --- a/test/screen_tests/settings_view/settings_subviews/wallet_settings_subviews/rename_wallet_view_screen_test.mocks.dart +++ b/test/screen_tests/settings_view/settings_subviews/wallet_settings_subviews/rename_wallet_view_screen_test.mocks.dart @@ -56,6 +56,14 @@ class MockWalletsService extends _i1.Mock implements _i2.WalletsService { returnValue: _i3.Future.value(false), ) as _i3.Future); @override + Map fetchWalletsData() => (super.noSuchMethod( + Invocation.method( + #fetchWalletsData, + [], + ), + returnValue: {}, + ) as Map); + @override _i3.Future addExistingStackWallet({ required String? name, required String? walletId, diff --git a/test/screen_tests/settings_view/settings_subviews/wallet_settings_subviews/rescan_warning_view_screen_test.mocks.dart b/test/screen_tests/settings_view/settings_subviews/wallet_settings_subviews/rescan_warning_view_screen_test.mocks.dart index a7764e149..dd74d3134 100644 --- a/test/screen_tests/settings_view/settings_subviews/wallet_settings_subviews/rescan_warning_view_screen_test.mocks.dart +++ b/test/screen_tests/settings_view/settings_subviews/wallet_settings_subviews/rescan_warning_view_screen_test.mocks.dart @@ -208,6 +208,11 @@ class MockManager extends _i1.Mock implements _i5.Manager { returnValue: false, ) as bool); @override + bool get hasTokenSupport => (super.noSuchMethod( + Invocation.getter(#hasTokenSupport), + returnValue: false, + ) as bool); + @override bool get hasWhirlpoolSupport => (super.noSuchMethod( Invocation.getter(#hasWhirlpoolSupport), returnValue: false, @@ -242,7 +247,7 @@ class MockManager extends _i1.Mock implements _i5.Manager { @override _i7.Future> prepareSend({ required String? address, - required int? satoshiAmount, + required int? amount, Map? args, }) => (super.noSuchMethod( @@ -251,7 +256,7 @@ class MockManager extends _i1.Mock implements _i5.Manager { [], { #address: address, - #satoshiAmount: satoshiAmount, + #satoshiAmount: amount, #args: args, }, ), diff --git a/test/screen_tests/settings_view/settings_subviews/wallet_settings_subviews/wallet_delete_mnemonic_view_screen_test.mocks.dart b/test/screen_tests/settings_view/settings_subviews/wallet_settings_subviews/wallet_delete_mnemonic_view_screen_test.mocks.dart index 4c7dbfd7a..71ab847d7 100644 --- a/test/screen_tests/settings_view/settings_subviews/wallet_settings_subviews/wallet_delete_mnemonic_view_screen_test.mocks.dart +++ b/test/screen_tests/settings_view/settings_subviews/wallet_settings_subviews/wallet_delete_mnemonic_view_screen_test.mocks.dart @@ -92,6 +92,14 @@ class MockWalletsService extends _i1.Mock implements _i5.WalletsService { returnValue: _i6.Future.value(false), ) as _i6.Future); @override + Map fetchWalletsData() => (super.noSuchMethod( + Invocation.method( + #fetchWalletsData, + [], + ), + returnValue: {}, + ) as Map); + @override _i6.Future addExistingStackWallet({ required String? name, required String? walletId, @@ -431,6 +439,11 @@ class MockManager extends _i1.Mock implements _i9.Manager { returnValue: false, ) as bool); @override + bool get hasTokenSupport => (super.noSuchMethod( + Invocation.getter(#hasTokenSupport), + returnValue: false, + ) as bool); + @override bool get hasWhirlpoolSupport => (super.noSuchMethod( Invocation.getter(#hasWhirlpoolSupport), returnValue: false, @@ -465,7 +478,7 @@ class MockManager extends _i1.Mock implements _i9.Manager { @override _i6.Future> prepareSend({ required String? address, - required int? satoshiAmount, + required int? amount, Map? args, }) => (super.noSuchMethod( @@ -474,7 +487,7 @@ class MockManager extends _i1.Mock implements _i9.Manager { [], { #address: address, - #satoshiAmount: satoshiAmount, + #satoshiAmount: amount, #args: args, }, ), diff --git a/test/screen_tests/settings_view/settings_subviews/wallet_settings_view_screen_test.mocks.dart b/test/screen_tests/settings_view/settings_subviews/wallet_settings_view_screen_test.mocks.dart index e736cfdd8..94659b07c 100644 --- a/test/screen_tests/settings_view/settings_subviews/wallet_settings_view_screen_test.mocks.dart +++ b/test/screen_tests/settings_view/settings_subviews/wallet_settings_view_screen_test.mocks.dart @@ -349,6 +349,14 @@ class MockWalletsService extends _i1.Mock implements _i13.WalletsService { returnValue: _i8.Future.value(false), ) as _i8.Future); @override + Map fetchWalletsData() => (super.noSuchMethod( + Invocation.method( + #fetchWalletsData, + [], + ), + returnValue: {}, + ) as Map); + @override _i8.Future addExistingStackWallet({ required String? name, required String? walletId, @@ -688,6 +696,11 @@ class MockManager extends _i1.Mock implements _i15.Manager { returnValue: false, ) as bool); @override + bool get hasTokenSupport => (super.noSuchMethod( + Invocation.getter(#hasTokenSupport), + returnValue: false, + ) as bool); + @override bool get hasWhirlpoolSupport => (super.noSuchMethod( Invocation.getter(#hasWhirlpoolSupport), returnValue: false, @@ -722,7 +735,7 @@ class MockManager extends _i1.Mock implements _i15.Manager { @override _i8.Future> prepareSend({ required String? address, - required int? satoshiAmount, + required int? amount, Map? args, }) => (super.noSuchMethod( @@ -731,7 +744,7 @@ class MockManager extends _i1.Mock implements _i15.Manager { [], { #address: address, - #satoshiAmount: satoshiAmount, + #satoshiAmount: amount, #args: args, }, ), diff --git a/test/screen_tests/settings_view/settings_view_screen_test.mocks.dart b/test/screen_tests/settings_view/settings_view_screen_test.mocks.dart index be1b24bcb..d42f9998a 100644 --- a/test/screen_tests/settings_view/settings_view_screen_test.mocks.dart +++ b/test/screen_tests/settings_view/settings_view_screen_test.mocks.dart @@ -92,6 +92,14 @@ class MockWalletsService extends _i1.Mock implements _i5.WalletsService { returnValue: _i6.Future.value(false), ) as _i6.Future); @override + Map fetchWalletsData() => (super.noSuchMethod( + Invocation.method( + #fetchWalletsData, + [], + ), + returnValue: {}, + ) as Map); + @override _i6.Future addExistingStackWallet({ required String? name, required String? walletId, @@ -431,6 +439,11 @@ class MockManager extends _i1.Mock implements _i9.Manager { returnValue: false, ) as bool); @override + bool get hasTokenSupport => (super.noSuchMethod( + Invocation.getter(#hasTokenSupport), + returnValue: false, + ) as bool); + @override bool get hasWhirlpoolSupport => (super.noSuchMethod( Invocation.getter(#hasWhirlpoolSupport), returnValue: false, @@ -465,7 +478,7 @@ class MockManager extends _i1.Mock implements _i9.Manager { @override _i6.Future> prepareSend({ required String? address, - required int? satoshiAmount, + required int? amount, Map? args, }) => (super.noSuchMethod( @@ -474,7 +487,7 @@ class MockManager extends _i1.Mock implements _i9.Manager { [], { #address: address, - #satoshiAmount: satoshiAmount, + #satoshiAmount: amount, #args: args, }, ), diff --git a/test/screen_tests/transaction_subviews/transaction_search_results_view_screen_test.mocks.dart b/test/screen_tests/transaction_subviews/transaction_search_results_view_screen_test.mocks.dart index 7cf8968b1..665b0333b 100644 --- a/test/screen_tests/transaction_subviews/transaction_search_results_view_screen_test.mocks.dart +++ b/test/screen_tests/transaction_subviews/transaction_search_results_view_screen_test.mocks.dart @@ -210,6 +210,11 @@ class MockManager extends _i1.Mock implements _i5.Manager { returnValue: false, ) as bool); @override + bool get hasTokenSupport => (super.noSuchMethod( + Invocation.getter(#hasTokenSupport), + returnValue: false, + ) as bool); + @override bool get hasWhirlpoolSupport => (super.noSuchMethod( Invocation.getter(#hasWhirlpoolSupport), returnValue: false, @@ -244,7 +249,7 @@ class MockManager extends _i1.Mock implements _i5.Manager { @override _i7.Future> prepareSend({ required String? address, - required int? satoshiAmount, + required int? amount, Map? args, }) => (super.noSuchMethod( @@ -253,7 +258,7 @@ class MockManager extends _i1.Mock implements _i5.Manager { [], { #address: address, - #satoshiAmount: satoshiAmount, + #satoshiAmount: amount, #args: args, }, ), diff --git a/test/screen_tests/wallet_view/confirm_send_view_screen_test.mocks.dart b/test/screen_tests/wallet_view/confirm_send_view_screen_test.mocks.dart index 81980dca0..d1190a181 100644 --- a/test/screen_tests/wallet_view/confirm_send_view_screen_test.mocks.dart +++ b/test/screen_tests/wallet_view/confirm_send_view_screen_test.mocks.dart @@ -209,6 +209,11 @@ class MockManager extends _i1.Mock implements _i5.Manager { returnValue: false, ) as bool); @override + bool get hasTokenSupport => (super.noSuchMethod( + Invocation.getter(#hasTokenSupport), + returnValue: false, + ) as bool); + @override bool get hasWhirlpoolSupport => (super.noSuchMethod( Invocation.getter(#hasWhirlpoolSupport), returnValue: false, @@ -243,7 +248,7 @@ class MockManager extends _i1.Mock implements _i5.Manager { @override _i7.Future> prepareSend({ required String? address, - required int? satoshiAmount, + required int? amount, Map? args, }) => (super.noSuchMethod( @@ -252,7 +257,7 @@ class MockManager extends _i1.Mock implements _i5.Manager { [], { #address: address, - #satoshiAmount: satoshiAmount, + #satoshiAmount: amount, #args: args, }, ), diff --git a/test/screen_tests/wallet_view/receive_view_screen_test.mocks.dart b/test/screen_tests/wallet_view/receive_view_screen_test.mocks.dart index 3055cf6c6..b542dc860 100644 --- a/test/screen_tests/wallet_view/receive_view_screen_test.mocks.dart +++ b/test/screen_tests/wallet_view/receive_view_screen_test.mocks.dart @@ -208,6 +208,11 @@ class MockManager extends _i1.Mock implements _i5.Manager { returnValue: false, ) as bool); @override + bool get hasTokenSupport => (super.noSuchMethod( + Invocation.getter(#hasTokenSupport), + returnValue: false, + ) as bool); + @override bool get hasWhirlpoolSupport => (super.noSuchMethod( Invocation.getter(#hasWhirlpoolSupport), returnValue: false, @@ -242,7 +247,7 @@ class MockManager extends _i1.Mock implements _i5.Manager { @override _i7.Future> prepareSend({ required String? address, - required int? satoshiAmount, + required int? amount, Map? args, }) => (super.noSuchMethod( @@ -251,7 +256,7 @@ class MockManager extends _i1.Mock implements _i5.Manager { [], { #address: address, - #satoshiAmount: satoshiAmount, + #satoshiAmount: amount, #args: args, }, ), diff --git a/test/screen_tests/wallet_view/send_view_screen_test.mocks.dart b/test/screen_tests/wallet_view/send_view_screen_test.mocks.dart index 533578e6c..d0a586e2e 100644 --- a/test/screen_tests/wallet_view/send_view_screen_test.mocks.dart +++ b/test/screen_tests/wallet_view/send_view_screen_test.mocks.dart @@ -250,6 +250,11 @@ class MockManager extends _i1.Mock implements _i8.Manager { returnValue: false, ) as bool); @override + bool get hasTokenSupport => (super.noSuchMethod( + Invocation.getter(#hasTokenSupport), + returnValue: false, + ) as bool); + @override bool get hasWhirlpoolSupport => (super.noSuchMethod( Invocation.getter(#hasWhirlpoolSupport), returnValue: false, @@ -284,7 +289,7 @@ class MockManager extends _i1.Mock implements _i8.Manager { @override _i7.Future> prepareSend({ required String? address, - required int? satoshiAmount, + required int? amount, Map? args, }) => (super.noSuchMethod( @@ -293,7 +298,7 @@ class MockManager extends _i1.Mock implements _i8.Manager { [], { #address: address, - #satoshiAmount: satoshiAmount, + #satoshiAmount: amount, #args: args, }, ), diff --git a/test/screen_tests/wallet_view/wallet_view_screen_test.mocks.dart b/test/screen_tests/wallet_view/wallet_view_screen_test.mocks.dart index f71a62d92..71a400d4c 100644 --- a/test/screen_tests/wallet_view/wallet_view_screen_test.mocks.dart +++ b/test/screen_tests/wallet_view/wallet_view_screen_test.mocks.dart @@ -210,6 +210,11 @@ class MockManager extends _i1.Mock implements _i5.Manager { returnValue: false, ) as bool); @override + bool get hasTokenSupport => (super.noSuchMethod( + Invocation.getter(#hasTokenSupport), + returnValue: false, + ) as bool); + @override bool get hasWhirlpoolSupport => (super.noSuchMethod( Invocation.getter(#hasWhirlpoolSupport), returnValue: false, @@ -244,7 +249,7 @@ class MockManager extends _i1.Mock implements _i5.Manager { @override _i7.Future> prepareSend({ required String? address, - required int? satoshiAmount, + required int? amount, Map? args, }) => (super.noSuchMethod( @@ -253,7 +258,7 @@ class MockManager extends _i1.Mock implements _i5.Manager { [], { #address: address, - #satoshiAmount: satoshiAmount, + #satoshiAmount: amount, #args: args, }, ), diff --git a/test/services/coins/bitcoincash/bitcoincash_wallet_test.dart b/test/services/coins/bitcoincash/bitcoincash_wallet_test.dart index bdc3c71e9..604bb741e 100644 --- a/test/services/coins/bitcoincash/bitcoincash_wallet_test.dart +++ b/test/services/coins/bitcoincash/bitcoincash_wallet_test.dart @@ -4,9 +4,9 @@ import 'package:hive/hive.dart'; import 'package:hive_test/hive_test.dart'; import 'package:mockito/annotations.dart'; import 'package:mockito/mockito.dart'; +import 'package:stackwallet/db/hive/db.dart'; import 'package:stackwallet/electrumx_rpc/cached_electrumx.dart'; import 'package:stackwallet/electrumx_rpc/electrumx.dart'; -import 'package:stackwallet/hive/db.dart'; import 'package:stackwallet/models/paymint/fee_object_model.dart'; import 'package:stackwallet/services/coins/bitcoincash/bitcoincash_wallet.dart'; import 'package:stackwallet/services/transaction_notification_tracker.dart'; diff --git a/test/services/coins/dogecoin/dogecoin_wallet_test.dart b/test/services/coins/dogecoin/dogecoin_wallet_test.dart index 697d1c98c..9e117d2ca 100644 --- a/test/services/coins/dogecoin/dogecoin_wallet_test.dart +++ b/test/services/coins/dogecoin/dogecoin_wallet_test.dart @@ -4,9 +4,9 @@ import 'package:hive/hive.dart'; import 'package:hive_test/hive_test.dart'; import 'package:mockito/annotations.dart'; import 'package:mockito/mockito.dart'; +import 'package:stackwallet/db/hive/db.dart'; import 'package:stackwallet/electrumx_rpc/cached_electrumx.dart'; import 'package:stackwallet/electrumx_rpc/electrumx.dart'; -import 'package:stackwallet/hive/db.dart'; import 'package:stackwallet/models/paymint/fee_object_model.dart'; import 'package:stackwallet/services/coins/dogecoin/dogecoin_wallet.dart'; import 'package:stackwallet/services/transaction_notification_tracker.dart'; @@ -40,7 +40,6 @@ void main() { }); }); - group("validate mainnet dogecoin addresses", () { MockElectrumX? client; MockCachedElectrumX? cachedClient; diff --git a/test/services/coins/fake_coin_service_api.dart b/test/services/coins/fake_coin_service_api.dart index 834d676a2..9f522d822 100644 --- a/test/services/coins/fake_coin_service_api.dart +++ b/test/services/coins/fake_coin_service_api.dart @@ -103,7 +103,7 @@ class FakeCoinServiceAPI extends CoinServiceAPI { @override Future> prepareSend( {required String address, - required int satoshiAmount, + required int amount, Map? args}) { // TODO: implement prepareSend throw UnimplementedError(); diff --git a/test/services/coins/firo/firo_wallet_test.dart b/test/services/coins/firo/firo_wallet_test.dart index 5cdf9ce50..d7b23e51e 100644 --- a/test/services/coins/firo/firo_wallet_test.dart +++ b/test/services/coins/firo/firo_wallet_test.dart @@ -18,6 +18,7 @@ import 'package:stackwallet/models/paymint/transactions_model.dart' as old; import 'package:stackwallet/services/coins/firo/firo_wallet.dart'; import 'package:stackwallet/services/transaction_notification_tracker.dart'; import 'package:stackwallet/utilities/address_utils.dart'; +import 'package:stackwallet/utilities/amount/amount.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:stackwallet/utilities/flutter_secure_storage_interface.dart'; @@ -97,12 +98,17 @@ void main() { ? TransactionSubType.join : TransactionSubType.none, amount: t.amount, + amountString: Amount( + rawValue: BigInt.from(t.amount), + fractionDigits: Coin.firo.decimals, + ).toJsonString(), fee: t.fees, height: t.height, isCancelled: t.isCancelled, isLelantus: null, slateId: t.slateId, otherData: t.otherData, + nonce: null, inputs: [], outputs: [], ), diff --git a/test/services/coins/manager_test.dart b/test/services/coins/manager_test.dart index 2a5661ab2..d4a8f9969 100644 --- a/test/services/coins/manager_test.dart +++ b/test/services/coins/manager_test.dart @@ -8,6 +8,7 @@ import 'package:stackwallet/models/paymint/fee_object_model.dart'; import 'package:stackwallet/services/coins/coin_service.dart'; import 'package:stackwallet/services/coins/firo/firo_wallet.dart'; import 'package:stackwallet/services/coins/manager.dart'; +import 'package:stackwallet/utilities/amount/amount.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'manager_test.mocks.dart'; @@ -74,6 +75,7 @@ void main() { group("get balances", () { test("balance", () async { final CoinServiceAPI wallet = MockFiroWallet(); + when(wallet.coin).thenAnswer((_) => Coin.firo); when(wallet.balance).thenAnswer( (_) => Balance( coin: Coin.firo, @@ -96,6 +98,9 @@ void main() { test("transactions", () async { final CoinServiceAPI wallet = MockFiroWallet(); + + when(wallet.coin).thenAnswer((realInvocation) => Coin.firo); + final tx = Transaction( walletId: "walletId", txid: "txid", @@ -103,12 +108,17 @@ void main() { type: TransactionType.incoming, subType: TransactionSubType.mint, amount: 123, + amountString: Amount( + rawValue: BigInt.from(123), + fractionDigits: wallet.coin.decimals, + ).toJsonString(), fee: 3, height: 123, isCancelled: false, isLelantus: true, slateId: null, otherData: null, + nonce: null, inputs: [], outputs: [], ); diff --git a/test/services/coins/manager_test.mocks.dart b/test/services/coins/manager_test.mocks.dart index bc1f2c2e4..e85f304ce 100644 --- a/test/services/coins/manager_test.mocks.dart +++ b/test/services/coins/manager_test.mocks.dart @@ -7,7 +7,7 @@ import 'dart:async' as _i10; import 'package:decimal/decimal.dart' as _i8; import 'package:mockito/mockito.dart' as _i1; -import 'package:stackwallet/db/main_db.dart' as _i7; +import 'package:stackwallet/db/isar/main_db.dart' as _i7; import 'package:stackwallet/electrumx_rpc/cached_electrumx.dart' as _i5; import 'package:stackwallet/electrumx_rpc/electrumx.dart' as _i4; import 'package:stackwallet/models/balance.dart' as _i6; @@ -392,7 +392,7 @@ class MockFiroWallet extends _i1.Mock implements _i9.FiroWallet { @override _i10.Future> prepareSendPublic({ required String? address, - required int? satoshiAmount, + required int? amount, Map? args, }) => (super.noSuchMethod( @@ -401,7 +401,7 @@ class MockFiroWallet extends _i1.Mock implements _i9.FiroWallet { [], { #address: address, - #satoshiAmount: satoshiAmount, + #satoshiAmount: amount, #args: args, }, ), @@ -421,7 +421,7 @@ class MockFiroWallet extends _i1.Mock implements _i9.FiroWallet { @override _i10.Future> prepareSend({ required String? address, - required int? satoshiAmount, + required int? amount, Map? args, }) => (super.noSuchMethod( @@ -430,7 +430,7 @@ class MockFiroWallet extends _i1.Mock implements _i9.FiroWallet { [], { #address: address, - #satoshiAmount: satoshiAmount, + #satoshiAmount: amount, #args: args, }, ), @@ -992,6 +992,25 @@ class MockFiroWallet extends _i1.Mock implements _i9.FiroWallet { returnValueForMissingStub: _i10.Future.value(), ) as _i10.Future); @override + List getWalletTokenContractAddresses() => (super.noSuchMethod( + Invocation.method( + #getWalletTokenContractAddresses, + [], + ), + returnValue: [], + ) as List); + @override + _i10.Future updateWalletTokenContractAddresses( + List? contractAddresses) => + (super.noSuchMethod( + Invocation.method( + #updateWalletTokenContractAddresses, + [contractAddresses], + ), + returnValue: _i10.Future.value(), + returnValueForMissingStub: _i10.Future.value(), + ) as _i10.Future); + @override void initWalletDB({_i7.MainDB? mockableOverride}) => super.noSuchMethod( Invocation.method( #initWalletDB, diff --git a/test/services/node_service_test.dart b/test/services/node_service_test.dart index cea30be2d..2bd889b6e 100644 --- a/test/services/node_service_test.dart +++ b/test/services/node_service_test.dart @@ -1,7 +1,7 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:hive/hive.dart'; import 'package:hive_test/hive_test.dart'; -import 'package:stackwallet/hive/db.dart'; +import 'package:stackwallet/db/hive/db.dart'; import 'package:stackwallet/models/node_model.dart'; import 'package:stackwallet/services/node_service.dart'; import 'package:stackwallet/utilities/default_nodes.dart'; diff --git a/test/services/wallets_service_test.dart b/test/services/wallets_service_test.dart index fc1984923..86659206a 100644 --- a/test/services/wallets_service_test.dart +++ b/test/services/wallets_service_test.dart @@ -2,7 +2,7 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:hive/hive.dart'; import 'package:hive_test/hive_test.dart'; import 'package:mockito/annotations.dart'; -import 'package:stackwallet/hive/db.dart'; +import 'package:stackwallet/db/hive/db.dart'; import 'package:stackwallet/services/wallets_service.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:stackwallet/utilities/flutter_secure_storage_interface.dart'; diff --git a/test/utilities/amount/amount_test.dart b/test/utilities/amount/amount_test.dart new file mode 100644 index 000000000..c909ff122 --- /dev/null +++ b/test/utilities/amount/amount_test.dart @@ -0,0 +1,219 @@ +import 'package:decimal/decimal.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:stackwallet/utilities/amount/amount.dart'; + +void main() { + test("Basic Amount Constructor tests", () { + Amount amount = Amount(rawValue: BigInt.two, fractionDigits: 0); + expect(amount.fractionDigits, 0); + expect(amount.raw, BigInt.two); + expect(amount.decimal, Decimal.fromInt(2)); + + amount = Amount(rawValue: BigInt.two, fractionDigits: 2); + expect(amount.fractionDigits, 2); + expect(amount.raw, BigInt.two); + expect(amount.decimal, Decimal.parse("0.02")); + + amount = Amount(rawValue: BigInt.from(123456789), fractionDigits: 7); + expect(amount.fractionDigits, 7); + expect(amount.raw, BigInt.from(123456789)); + expect(amount.decimal, Decimal.parse("12.3456789")); + + bool didThrow = false; + try { + amount = Amount(rawValue: BigInt.one, fractionDigits: -1); + } catch (_) { + didThrow = true; + } + expect(didThrow, true); + }); + + test("Named fromDecimal Amount Constructor tests", () { + Amount amount = Amount.fromDecimal(Decimal.fromInt(2), fractionDigits: 0); + expect(amount.fractionDigits, 0); + expect(amount.raw, BigInt.two); + expect(amount.decimal, Decimal.fromInt(2)); + + amount = Amount.fromDecimal(Decimal.fromInt(2), fractionDigits: 2); + expect(amount.fractionDigits, 2); + expect(amount.raw, BigInt.from(200)); + expect(amount.decimal, Decimal.fromInt(2)); + + amount = + Amount.fromDecimal(Decimal.parse("0.0123456789"), fractionDigits: 7); + expect(amount.fractionDigits, 7); + expect(amount.raw, BigInt.from(123456)); + expect(amount.decimal, Decimal.parse("0.0123456")); + + bool didThrow = false; + try { + amount = Amount.fromDecimal(Decimal.fromInt(2), fractionDigits: -1); + } catch (_) { + didThrow = true; + } + expect(didThrow, true); + }); + + group("serialization", () { + test("toMap", () { + expect( + Amount(rawValue: BigInt.two, fractionDigits: 8).toMap(), + {"raw": "2", "fractionDigits": 8}, + ); + expect( + Amount.fromDecimal(Decimal.fromInt(2), fractionDigits: 8).toMap(), + {"raw": "200000000", "fractionDigits": 8}, + ); + }); + + test("toJsonString", () { + expect( + Amount(rawValue: BigInt.two, fractionDigits: 8).toJsonString(), + '{"raw":"2","fractionDigits":8}', + ); + expect( + Amount.fromDecimal(Decimal.fromInt(2), fractionDigits: 8) + .toJsonString(), + '{"raw":"200000000","fractionDigits":8}', + ); + }); + + test("localizedStringAsFixed", () { + expect( + Amount(rawValue: BigInt.two, fractionDigits: 8) + .localizedStringAsFixed(locale: "en_US"), + "0.00000002", + ); + expect( + Amount(rawValue: BigInt.two, fractionDigits: 8) + .localizedStringAsFixed(locale: "en_US", decimalPlaces: 2), + "0.00", + ); + expect( + Amount.fromDecimal(Decimal.fromInt(2), fractionDigits: 8) + .localizedStringAsFixed(locale: "en_US"), + "2.00000000", + ); + expect( + Amount.fromDecimal(Decimal.fromInt(2), fractionDigits: 8) + .localizedStringAsFixed(locale: "en_US", decimalPlaces: 4), + "2.0000", + ); + expect( + Amount.fromDecimal(Decimal.fromInt(2), fractionDigits: 8) + .localizedStringAsFixed(locale: "en_US", decimalPlaces: 0), + "2", + ); + }); + }); + + group("deserialization", () { + test("fromSerializedJsonString", () { + expect( + Amount.fromSerializedJsonString( + '{"raw":"200000000","fractionDigits":8}'), + Amount.fromDecimal(Decimal.parse("2"), fractionDigits: 8), + ); + }); + }); + + group("operators", () { + final one = Amount(rawValue: BigInt.one, fractionDigits: 0); + final two = Amount(rawValue: BigInt.two, fractionDigits: 0); + final four4 = Amount(rawValue: BigInt.from(4), fractionDigits: 4); + final four4_2 = Amount(rawValue: BigInt.from(4), fractionDigits: 4); + final four5 = Amount(rawValue: BigInt.from(4), fractionDigits: 5); + + test(">", () { + expect(one > two, false); + expect(one > one, false); + + expect(two > two, false); + expect(two > one, true); + }); + + test("<", () { + expect(one < two, true); + expect(one < one, false); + + expect(two < two, false); + expect(two < one, false); + }); + + test(">=", () { + expect(one >= two, false); + expect(one >= one, true); + + expect(two >= two, true); + expect(two >= one, true); + }); + + test("<=", () { + expect(one <= two, true); + expect(one <= one, true); + + expect(two <= two, true); + expect(two <= one, false); + }); + + test("<=", () { + expect(one <= two, true); + expect(one <= one, true); + + expect(two <= two, true); + expect(two <= one, false); + }); + + test("==", () { + expect(one == two, false); + expect(one == one, true); + + expect(BigInt.from(2) == BigInt.from(2), true); + + expect(four4 == four4_2, true); + expect(four4 == four5, false); + }); + + test("+", () { + expect(one + two, Amount(rawValue: BigInt.from(3), fractionDigits: 0)); + expect(one + one, Amount(rawValue: BigInt.from(2), fractionDigits: 0)); + + expect( + Amount(rawValue: BigInt.from(3), fractionDigits: 0) + + Amount(rawValue: BigInt.from(-5), fractionDigits: 0), + Amount(rawValue: BigInt.from(-2), fractionDigits: 0)); + expect( + Amount(rawValue: BigInt.from(-3), fractionDigits: 0) + + Amount(rawValue: BigInt.from(6), fractionDigits: 0), + Amount(rawValue: BigInt.from(3), fractionDigits: 0)); + }); + + test("-", () { + expect(one - two, Amount(rawValue: BigInt.from(-1), fractionDigits: 0)); + expect(one - one, Amount(rawValue: BigInt.from(0), fractionDigits: 0)); + + expect( + Amount(rawValue: BigInt.from(3), fractionDigits: 0) - + Amount(rawValue: BigInt.from(-5), fractionDigits: 0), + Amount(rawValue: BigInt.from(8), fractionDigits: 0)); + expect( + Amount(rawValue: BigInt.from(-3), fractionDigits: 0) - + Amount(rawValue: BigInt.from(6), fractionDigits: 0), + Amount(rawValue: BigInt.from(-9), fractionDigits: 0)); + }); + + test("*", () { + expect(one * two, Amount(rawValue: BigInt.from(2), fractionDigits: 0)); + expect(one * one, Amount(rawValue: BigInt.from(1), fractionDigits: 0)); + + expect( + Amount(rawValue: BigInt.from(3), fractionDigits: 0) * + Amount(rawValue: BigInt.from(-5), fractionDigits: 0), + Amount(rawValue: BigInt.from(-15), fractionDigits: 0)); + expect( + Amount(rawValue: BigInt.from(-3), fractionDigits: 0) * + Amount(rawValue: BigInt.from(-6), fractionDigits: 0), + Amount(rawValue: BigInt.from(18), fractionDigits: 0)); + }); + }); +} diff --git a/test/utilities/amount/amount_unit_test.dart b/test/utilities/amount/amount_unit_test.dart new file mode 100644 index 000000000..2dcd5125c --- /dev/null +++ b/test/utilities/amount/amount_unit_test.dart @@ -0,0 +1,144 @@ +import 'package:decimal/decimal.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:stackwallet/utilities/amount/amount.dart'; +import 'package:stackwallet/utilities/amount/amount_unit.dart'; +import 'package:stackwallet/utilities/enums/coin_enum.dart'; + +void main() { + test("displayAmount BTC", () { + final Amount amount = Amount( + rawValue: BigInt.from(1012345678), + fractionDigits: 8, + ); + + expect( + AmountUnit.normal.displayAmount( + amount: amount, + locale: "en_US", + coin: Coin.bitcoin, + maxDecimalPlaces: 8, + ), + "10.12345678 BTC", + ); + + expect( + AmountUnit.milli.displayAmount( + amount: amount, + locale: "en_US", + coin: Coin.bitcoin, + maxDecimalPlaces: 8, + ), + "10123.45678 mBTC", + ); + + expect( + AmountUnit.micro.displayAmount( + amount: amount, + locale: "en_US", + coin: Coin.bitcoin, + maxDecimalPlaces: 8, + ), + "10123456.78 µBTC", + ); + + expect( + AmountUnit.nano.displayAmount( + amount: amount, + locale: "en_US", + coin: Coin.bitcoin, + maxDecimalPlaces: 8, + ), + "1012345678 sats", + ); + final dec = Decimal.parse("10.123456789123456789"); + + expect(dec.toString(), "10.123456789123456789"); + }); + + test("displayAmount ETH", () { + final Amount amount = Amount.fromDecimal( + Decimal.parse("10.123456789123456789"), + fractionDigits: Coin.ethereum.decimals, + ); + + expect( + AmountUnit.normal.displayAmount( + amount: amount, + locale: "en_US", + coin: Coin.ethereum, + maxDecimalPlaces: 8, + ), + "10.12345678 ETH", + ); + + expect( + AmountUnit.normal.displayAmount( + amount: amount, + locale: "en_US", + coin: Coin.ethereum, + maxDecimalPlaces: 18, + ), + "10.123456789123456789 ETH", + ); + + expect( + AmountUnit.milli.displayAmount( + amount: amount, + locale: "en_US", + coin: Coin.ethereum, + maxDecimalPlaces: 9, + ), + "10123.456789123 mETH", + ); + + expect( + AmountUnit.micro.displayAmount( + amount: amount, + locale: "en_US", + coin: Coin.ethereum, + maxDecimalPlaces: 8, + ), + "10123456.78912345 µETH", + ); + + expect( + AmountUnit.nano.displayAmount( + amount: amount, + locale: "en_US", + coin: Coin.ethereum, + maxDecimalPlaces: 1, + ), + "10123456789.1 gwei", + ); + + expect( + AmountUnit.pico.displayAmount( + amount: amount, + locale: "en_US", + coin: Coin.ethereum, + maxDecimalPlaces: 18, + ), + "10123456789123.456789 mwei", + ); + + expect( + AmountUnit.femto.displayAmount( + amount: amount, + locale: "en_US", + coin: Coin.ethereum, + maxDecimalPlaces: 4, + ), + "10123456789123456.789 kwei", + ); + + expect( + AmountUnit.atto.displayAmount( + amount: amount, + locale: "en_US", + coin: Coin.ethereum, + maxDecimalPlaces: 1, + ), + "10123456789123456789 wei", + ); + }); +} diff --git a/test/widget_tests/managed_favorite_test.mocks.dart b/test/widget_tests/managed_favorite_test.mocks.dart index f8bda0845..ebe7b3724 100644 --- a/test/widget_tests/managed_favorite_test.mocks.dart +++ b/test/widget_tests/managed_favorite_test.mocks.dart @@ -13,7 +13,7 @@ import 'package:bitcoindart/bitcoindart.dart' as _i13; import 'package:flutter/foundation.dart' as _i4; import 'package:flutter_riverpod/flutter_riverpod.dart' as _i5; import 'package:mockito/mockito.dart' as _i1; -import 'package:stackwallet/db/main_db.dart' as _i12; +import 'package:stackwallet/db/isar/main_db.dart' as _i12; import 'package:stackwallet/electrumx_rpc/cached_electrumx.dart' as _i10; import 'package:stackwallet/electrumx_rpc/electrumx.dart' as _i9; import 'package:stackwallet/models/balance.dart' as _i11; @@ -482,6 +482,14 @@ class MockWalletsService extends _i1.Mock implements _i2.WalletsService { returnValue: _i22.Future.value(false), ) as _i22.Future); @override + Map fetchWalletsData() => (super.noSuchMethod( + Invocation.method( + #fetchWalletsData, + [], + ), + returnValue: {}, + ) as Map); + @override _i22.Future addExistingStackWallet({ required String? name, required String? walletId, @@ -999,7 +1007,7 @@ class MockBitcoinWallet extends _i1.Mock implements _i25.BitcoinWallet { @override _i22.Future> prepareSend({ required String? address, - required int? satoshiAmount, + required int? amount, Map? args, }) => (super.noSuchMethod( @@ -1008,7 +1016,7 @@ class MockBitcoinWallet extends _i1.Mock implements _i25.BitcoinWallet { [], { #address: address, - #satoshiAmount: satoshiAmount, + #satoshiAmount: amount, #args: args, }, ), @@ -1432,6 +1440,25 @@ class MockBitcoinWallet extends _i1.Mock implements _i25.BitcoinWallet { returnValueForMissingStub: _i22.Future.value(), ) as _i22.Future); @override + List getWalletTokenContractAddresses() => (super.noSuchMethod( + Invocation.method( + #getWalletTokenContractAddresses, + [], + ), + returnValue: [], + ) as List); + @override + _i22.Future updateWalletTokenContractAddresses( + List? contractAddresses) => + (super.noSuchMethod( + Invocation.method( + #updateWalletTokenContractAddresses, + [contractAddresses], + ), + returnValue: _i22.Future.value(), + returnValueForMissingStub: _i22.Future.value(), + ) as _i22.Future); + @override void initWalletDB({_i12.MainDB? mockableOverride}) => super.noSuchMethod( Invocation.method( #initWalletDB, @@ -1677,7 +1704,7 @@ class MockBitcoinWallet extends _i1.Mock implements _i25.BitcoinWallet { @override _i22.Future> preparePaymentCodeSend({ required _i17.PaymentCode? paymentCode, - required int? satoshiAmount, + required int? amount, Map? args, }) => (super.noSuchMethod( @@ -1686,7 +1713,7 @@ class MockBitcoinWallet extends _i1.Mock implements _i25.BitcoinWallet { [], { #paymentCode: paymentCode, - #satoshiAmount: satoshiAmount, + #satoshiAmount: amount, #args: args, }, ), @@ -2411,6 +2438,11 @@ class MockManager extends _i1.Mock implements _i6.Manager { returnValue: false, ) as bool); @override + bool get hasTokenSupport => (super.noSuchMethod( + Invocation.getter(#hasTokenSupport), + returnValue: false, + ) as bool); + @override bool get hasWhirlpoolSupport => (super.noSuchMethod( Invocation.getter(#hasWhirlpoolSupport), returnValue: false, @@ -2445,7 +2477,7 @@ class MockManager extends _i1.Mock implements _i6.Manager { @override _i22.Future> prepareSend({ required String? address, - required int? satoshiAmount, + required int? amount, Map? args, }) => (super.noSuchMethod( @@ -2454,7 +2486,7 @@ class MockManager extends _i1.Mock implements _i6.Manager { [], { #address: address, - #satoshiAmount: satoshiAmount, + #satoshiAmount: amount, #args: args, }, ), @@ -2757,7 +2789,7 @@ class MockCoinServiceAPI extends _i1.Mock implements _i19.CoinServiceAPI { @override _i22.Future> prepareSend({ required String? address, - required int? satoshiAmount, + required int? amount, Map? args, }) => (super.noSuchMethod( @@ -2766,7 +2798,7 @@ class MockCoinServiceAPI extends _i1.Mock implements _i19.CoinServiceAPI { [], { #address: address, - #satoshiAmount: satoshiAmount, + #satoshiAmount: amount, #args: args, }, ), diff --git a/test/widget_tests/table_view/table_view_row_test.dart b/test/widget_tests/table_view/table_view_row_test.dart index 26292b7e5..9f732f51e 100644 --- a/test/widget_tests/table_view/table_view_row_test.dart +++ b/test/widget_tests/table_view/table_view_row_test.dart @@ -29,6 +29,8 @@ import 'table_view_row_test.mocks.dart'; ]) void main() { testWidgets('Test table view row', (widgetTester) async { + widgetTester.binding.window.physicalSizeTestValue = const Size(2500, 1800); + final mockWallet = MockWallets(); final CoinServiceAPI wallet = MockBitcoinWallet(); when(wallet.coin).thenAnswer((_) => Coin.bitcoin); @@ -56,8 +58,6 @@ void main() { when(mockWallet.getManagerProvider("wallet id 2")).thenAnswer( (realInvocation) => ChangeNotifierProvider((ref) => manager)); - final walletIds = mockWallet.getWalletIdsFor(coin: Coin.bitcoin); - await widgetTester.pumpWidget( ProviderScope( overrides: [ @@ -73,13 +73,14 @@ void main() { ), home: Material( child: TableViewRow( - cells: [ - for (int j = 1; j <= 5; j++) - TableViewCell(flex: 16, child: Text("Some Text ${j}")) - ], - expandingChild: const CoinWalletsTable( - coin: Coin.bitcoin, - )), + cells: [ + for (int j = 1; j <= 5; j++) + TableViewCell(flex: 16, child: Text("Some ${j}")) + ], + expandingChild: const CoinWalletsTable( + coin: Coin.bitcoin, + ), + ), ), ), ), @@ -87,7 +88,7 @@ void main() { await widgetTester.pumpAndSettle(); - expect(find.text("Some Text 1"), findsOneWidget); + expect(find.text("Some 1"), findsOneWidget); expect(find.byType(TableViewRow), findsWidgets); expect(find.byType(TableViewCell), findsWidgets); expect(find.byType(CoinWalletsTable), findsWidgets); diff --git a/test/widget_tests/table_view/table_view_row_test.mocks.dart b/test/widget_tests/table_view/table_view_row_test.mocks.dart index 84cfac4c8..a3182a21f 100644 --- a/test/widget_tests/table_view/table_view_row_test.mocks.dart +++ b/test/widget_tests/table_view/table_view_row_test.mocks.dart @@ -13,7 +13,7 @@ import 'package:bitcoindart/bitcoindart.dart' as _i13; import 'package:flutter/foundation.dart' as _i4; import 'package:flutter_riverpod/flutter_riverpod.dart' as _i5; import 'package:mockito/mockito.dart' as _i1; -import 'package:stackwallet/db/main_db.dart' as _i12; +import 'package:stackwallet/db/isar/main_db.dart' as _i12; import 'package:stackwallet/electrumx_rpc/cached_electrumx.dart' as _i10; import 'package:stackwallet/electrumx_rpc/electrumx.dart' as _i9; import 'package:stackwallet/models/balance.dart' as _i11; @@ -469,6 +469,14 @@ class MockWalletsService extends _i1.Mock implements _i2.WalletsService { returnValue: _i21.Future.value(false), ) as _i21.Future); @override + Map fetchWalletsData() => (super.noSuchMethod( + Invocation.method( + #fetchWalletsData, + [], + ), + returnValue: {}, + ) as Map); + @override _i21.Future addExistingStackWallet({ required String? name, required String? walletId, @@ -986,7 +994,7 @@ class MockBitcoinWallet extends _i1.Mock implements _i24.BitcoinWallet { @override _i21.Future> prepareSend({ required String? address, - required int? satoshiAmount, + required int? amount, Map? args, }) => (super.noSuchMethod( @@ -995,7 +1003,7 @@ class MockBitcoinWallet extends _i1.Mock implements _i24.BitcoinWallet { [], { #address: address, - #satoshiAmount: satoshiAmount, + #satoshiAmount: amount, #args: args, }, ), @@ -1419,6 +1427,25 @@ class MockBitcoinWallet extends _i1.Mock implements _i24.BitcoinWallet { returnValueForMissingStub: _i21.Future.value(), ) as _i21.Future); @override + List getWalletTokenContractAddresses() => (super.noSuchMethod( + Invocation.method( + #getWalletTokenContractAddresses, + [], + ), + returnValue: [], + ) as List); + @override + _i21.Future updateWalletTokenContractAddresses( + List? contractAddresses) => + (super.noSuchMethod( + Invocation.method( + #updateWalletTokenContractAddresses, + [contractAddresses], + ), + returnValue: _i21.Future.value(), + returnValueForMissingStub: _i21.Future.value(), + ) as _i21.Future); + @override void initWalletDB({_i12.MainDB? mockableOverride}) => super.noSuchMethod( Invocation.method( #initWalletDB, @@ -1664,7 +1691,7 @@ class MockBitcoinWallet extends _i1.Mock implements _i24.BitcoinWallet { @override _i21.Future> preparePaymentCodeSend({ required _i17.PaymentCode? paymentCode, - required int? satoshiAmount, + required int? amount, Map? args, }) => (super.noSuchMethod( @@ -1673,7 +1700,7 @@ class MockBitcoinWallet extends _i1.Mock implements _i24.BitcoinWallet { [], { #paymentCode: paymentCode, - #satoshiAmount: satoshiAmount, + #satoshiAmount: amount, #args: args, }, ), @@ -2136,6 +2163,11 @@ class MockManager extends _i1.Mock implements _i6.Manager { returnValue: false, ) as bool); @override + bool get hasTokenSupport => (super.noSuchMethod( + Invocation.getter(#hasTokenSupport), + returnValue: false, + ) as bool); + @override bool get hasWhirlpoolSupport => (super.noSuchMethod( Invocation.getter(#hasWhirlpoolSupport), returnValue: false, @@ -2170,7 +2202,7 @@ class MockManager extends _i1.Mock implements _i6.Manager { @override _i21.Future> prepareSend({ required String? address, - required int? satoshiAmount, + required int? amount, Map? args, }) => (super.noSuchMethod( @@ -2179,7 +2211,7 @@ class MockManager extends _i1.Mock implements _i6.Manager { [], { #address: address, - #satoshiAmount: satoshiAmount, + #satoshiAmount: amount, #args: args, }, ), @@ -2482,7 +2514,7 @@ class MockCoinServiceAPI extends _i1.Mock implements _i18.CoinServiceAPI { @override _i21.Future> prepareSend({ required String? address, - required int? satoshiAmount, + required int? amount, Map? args, }) => (super.noSuchMethod( @@ -2491,7 +2523,7 @@ class MockCoinServiceAPI extends _i1.Mock implements _i18.CoinServiceAPI { [], { #address: address, - #satoshiAmount: satoshiAmount, + #satoshiAmount: amount, #args: args, }, ), diff --git a/test/widget_tests/transaction_card_test.dart b/test/widget_tests/transaction_card_test.dart index d8cb9d390..02fa7bdfc 100644 --- a/test/widget_tests/transaction_card_test.dart +++ b/test/widget_tests/transaction_card_test.dart @@ -17,6 +17,7 @@ import 'package:stackwallet/services/locale_service.dart'; import 'package:stackwallet/services/notes_service.dart'; import 'package:stackwallet/services/price_service.dart'; import 'package:stackwallet/services/wallets.dart'; +import 'package:stackwallet/utilities/amount/amount.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:stackwallet/utilities/prefs.dart'; import 'package:stackwallet/utilities/theme/light_colors.dart'; @@ -51,6 +52,10 @@ void main() { timestamp: 1648595998, type: TransactionType.outgoing, amount: 100000000, + amountString: Amount( + rawValue: BigInt.from(100000000), + fractionDigits: Coin.firo.decimals, + ).toJsonString(), fee: 3794, height: 450123, subType: TransactionSubType.none, @@ -59,6 +64,7 @@ void main() { isLelantus: null, slateId: '', otherData: '', + nonce: null, inputs: [], outputs: [], )..address.value = Address( @@ -132,7 +138,7 @@ void main() { verify(mockPrefs.currency).called(1); verify(mockPriceService.getPrice(Coin.firo)).called(1); - verify(wallet.coin.ticker).called(2); + verify(wallet.coin.ticker).called(1); verify(mockLocaleService.locale).called(1); @@ -152,6 +158,10 @@ void main() { timestamp: 1648595998, type: TransactionType.outgoing, amount: 9659, + amountString: Amount( + rawValue: BigInt.from(9659), + fractionDigits: Coin.firo.decimals, + ).toJsonString(), fee: 3794, height: 450123, subType: TransactionSubType.mint, @@ -160,6 +170,7 @@ void main() { isLelantus: null, slateId: '', otherData: '', + nonce: null, inputs: [], outputs: [], )..address.value = Address( @@ -230,7 +241,7 @@ void main() { verify(mockPrefs.currency).called(1); verify(mockPriceService.getPrice(Coin.firo)).called(1); - verify(wallet.coin.ticker).called(2); + verify(wallet.coin.ticker).called(1); verify(mockLocaleService.locale).called(1); @@ -250,6 +261,10 @@ void main() { timestamp: 1648595998, type: TransactionType.incoming, amount: 100000000, + amountString: Amount( + rawValue: BigInt.from(100000000), + fractionDigits: Coin.firo.decimals, + ).toJsonString(), fee: 3794, height: 450123, subType: TransactionSubType.none, @@ -258,6 +273,7 @@ void main() { isLelantus: null, slateId: '', otherData: '', + nonce: null, inputs: [], outputs: [], )..address.value = Address( @@ -321,7 +337,7 @@ void main() { verify(mockPrefs.currency).called(1); verify(mockPriceService.getPrice(Coin.firo)).called(1); - verify(wallet.coin.ticker).called(2); + verify(wallet.coin.ticker).called(1); verify(mockLocaleService.locale).called(1); @@ -342,6 +358,10 @@ void main() { timestamp: 1648595998, type: TransactionType.outgoing, amount: 100000000, + amountString: Amount( + rawValue: BigInt.from(100000000), + fractionDigits: Coin.firo.decimals, + ).toJsonString(), fee: 3794, height: 450123, subType: TransactionSubType.none, @@ -350,6 +370,7 @@ void main() { isLelantus: null, slateId: '', otherData: '', + nonce: null, inputs: [], outputs: [], )..address.value = Address( @@ -413,7 +434,7 @@ void main() { verify(mockPrefs.currency).called(2); verify(mockLocaleService.locale).called(4); - verify(wallet.coin.ticker).called(2); + verify(wallet.coin.ticker).called(1); verify(wallet.storedChainHeight).called(2); verifyNoMoreInteractions(wallet); diff --git a/test/widget_tests/transaction_card_test.mocks.dart b/test/widget_tests/transaction_card_test.mocks.dart index 51afbec22..686f360b1 100644 --- a/test/widget_tests/transaction_card_test.mocks.dart +++ b/test/widget_tests/transaction_card_test.mocks.dart @@ -10,7 +10,7 @@ import 'package:decimal/decimal.dart' as _i14; import 'package:flutter/foundation.dart' as _i4; import 'package:flutter_riverpod/flutter_riverpod.dart' as _i5; import 'package:mockito/mockito.dart' as _i1; -import 'package:stackwallet/db/main_db.dart' as _i13; +import 'package:stackwallet/db/isar/main_db.dart' as _i13; import 'package:stackwallet/electrumx_rpc/cached_electrumx.dart' as _i12; import 'package:stackwallet/electrumx_rpc/electrumx.dart' as _i11; import 'package:stackwallet/models/balance.dart' as _i9; @@ -556,6 +556,11 @@ class MockManager extends _i1.Mock implements _i6.Manager { returnValue: false, ) as bool); @override + bool get hasTokenSupport => (super.noSuchMethod( + Invocation.getter(#hasTokenSupport), + returnValue: false, + ) as bool); + @override bool get hasWhirlpoolSupport => (super.noSuchMethod( Invocation.getter(#hasWhirlpoolSupport), returnValue: false, @@ -590,7 +595,7 @@ class MockManager extends _i1.Mock implements _i6.Manager { @override _i18.Future> prepareSend({ required String? address, - required int? satoshiAmount, + required int? amount, Map? args, }) => (super.noSuchMethod( @@ -599,7 +604,7 @@ class MockManager extends _i1.Mock implements _i6.Manager { [], { #address: address, - #satoshiAmount: satoshiAmount, + #satoshiAmount: amount, #args: args, }, ), @@ -906,7 +911,7 @@ class MockCoinServiceAPI extends _i1.Mock implements _i7.CoinServiceAPI { @override _i18.Future> prepareSend({ required String? address, - required int? satoshiAmount, + required int? amount, Map? args, }) => (super.noSuchMethod( @@ -915,7 +920,7 @@ class MockCoinServiceAPI extends _i1.Mock implements _i7.CoinServiceAPI { [], { #address: address, - #satoshiAmount: satoshiAmount, + #satoshiAmount: amount, #args: args, }, ), @@ -1356,7 +1361,7 @@ class MockFiroWallet extends _i1.Mock implements _i22.FiroWallet { @override _i18.Future> prepareSendPublic({ required String? address, - required int? satoshiAmount, + required int? amount, Map? args, }) => (super.noSuchMethod( @@ -1365,7 +1370,7 @@ class MockFiroWallet extends _i1.Mock implements _i22.FiroWallet { [], { #address: address, - #satoshiAmount: satoshiAmount, + #satoshiAmount: amount, #args: args, }, ), @@ -1385,7 +1390,7 @@ class MockFiroWallet extends _i1.Mock implements _i22.FiroWallet { @override _i18.Future> prepareSend({ required String? address, - required int? satoshiAmount, + required int? amount, Map? args, }) => (super.noSuchMethod( @@ -1394,7 +1399,7 @@ class MockFiroWallet extends _i1.Mock implements _i22.FiroWallet { [], { #address: address, - #satoshiAmount: satoshiAmount, + #satoshiAmount: amount, #args: args, }, ), @@ -1956,6 +1961,25 @@ class MockFiroWallet extends _i1.Mock implements _i22.FiroWallet { returnValueForMissingStub: _i18.Future.value(), ) as _i18.Future); @override + List getWalletTokenContractAddresses() => (super.noSuchMethod( + Invocation.method( + #getWalletTokenContractAddresses, + [], + ), + returnValue: [], + ) as List); + @override + _i18.Future updateWalletTokenContractAddresses( + List? contractAddresses) => + (super.noSuchMethod( + Invocation.method( + #updateWalletTokenContractAddresses, + [contractAddresses], + ), + returnValue: _i18.Future.value(), + returnValueForMissingStub: _i18.Future.value(), + ) as _i18.Future); + @override void initWalletDB({_i13.MainDB? mockableOverride}) => super.noSuchMethod( Invocation.method( #initWalletDB, @@ -2504,6 +2528,11 @@ class MockPriceService extends _i1.Mock implements _i28.PriceService { returnValueForMissingStub: null, ); @override + Set get tokenContractAddressesToCheck => (super.noSuchMethod( + Invocation.getter(#tokenContractAddressesToCheck), + returnValue: {}, + ) as Set); + @override Duration get updateInterval => (super.noSuchMethod( Invocation.getter(#updateInterval), returnValue: _FakeDuration_12( @@ -2532,6 +2561,21 @@ class MockPriceService extends _i1.Mock implements _i28.PriceService { ), ) as _i15.Tuple2<_i14.Decimal, double>); @override + _i15.Tuple2<_i14.Decimal, double> getTokenPrice(String? contractAddress) => + (super.noSuchMethod( + Invocation.method( + #getTokenPrice, + [contractAddress], + ), + returnValue: _FakeTuple2_13<_i14.Decimal, double>( + this, + Invocation.method( + #getTokenPrice, + [contractAddress], + ), + ), + ) as _i15.Tuple2<_i14.Decimal, double>); + @override _i18.Future updatePrice() => (super.noSuchMethod( Invocation.method( #updatePrice, diff --git a/test/widget_tests/wallet_card_test.mocks.dart b/test/widget_tests/wallet_card_test.mocks.dart index defea0da2..5e55a5020 100644 --- a/test/widget_tests/wallet_card_test.mocks.dart +++ b/test/widget_tests/wallet_card_test.mocks.dart @@ -13,7 +13,7 @@ import 'package:bitcoindart/bitcoindart.dart' as _i13; import 'package:flutter/foundation.dart' as _i4; import 'package:flutter_riverpod/flutter_riverpod.dart' as _i5; import 'package:mockito/mockito.dart' as _i1; -import 'package:stackwallet/db/main_db.dart' as _i12; +import 'package:stackwallet/db/isar/main_db.dart' as _i12; import 'package:stackwallet/electrumx_rpc/cached_electrumx.dart' as _i10; import 'package:stackwallet/electrumx_rpc/electrumx.dart' as _i9; import 'package:stackwallet/models/balance.dart' as _i11; @@ -749,7 +749,7 @@ class MockBitcoinWallet extends _i1.Mock implements _i23.BitcoinWallet { @override _i20.Future> prepareSend({ required String? address, - required int? satoshiAmount, + required int? amount, Map? args, }) => (super.noSuchMethod( @@ -758,7 +758,7 @@ class MockBitcoinWallet extends _i1.Mock implements _i23.BitcoinWallet { [], { #address: address, - #satoshiAmount: satoshiAmount, + #satoshiAmount: amount, #args: args, }, ), @@ -1182,6 +1182,25 @@ class MockBitcoinWallet extends _i1.Mock implements _i23.BitcoinWallet { returnValueForMissingStub: _i20.Future.value(), ) as _i20.Future); @override + List getWalletTokenContractAddresses() => (super.noSuchMethod( + Invocation.method( + #getWalletTokenContractAddresses, + [], + ), + returnValue: [], + ) as List); + @override + _i20.Future updateWalletTokenContractAddresses( + List? contractAddresses) => + (super.noSuchMethod( + Invocation.method( + #updateWalletTokenContractAddresses, + [contractAddresses], + ), + returnValue: _i20.Future.value(), + returnValueForMissingStub: _i20.Future.value(), + ) as _i20.Future); + @override void initWalletDB({_i12.MainDB? mockableOverride}) => super.noSuchMethod( Invocation.method( #initWalletDB, @@ -1427,7 +1446,7 @@ class MockBitcoinWallet extends _i1.Mock implements _i23.BitcoinWallet { @override _i20.Future> preparePaymentCodeSend({ required _i17.PaymentCode? paymentCode, - required int? satoshiAmount, + required int? amount, Map? args, }) => (super.noSuchMethod( @@ -1436,7 +1455,7 @@ class MockBitcoinWallet extends _i1.Mock implements _i23.BitcoinWallet { [], { #paymentCode: paymentCode, - #satoshiAmount: satoshiAmount, + #satoshiAmount: amount, #args: args, }, ), diff --git a/test/widget_tests/wallet_info_row/sub_widgets/wallet_info_row_balance_future_test.dart b/test/widget_tests/wallet_info_row/sub_widgets/wallet_info_row_balance_future_test.dart index 5747020ce..2d4800a36 100644 --- a/test/widget_tests/wallet_info_row/sub_widgets/wallet_info_row_balance_future_test.dart +++ b/test/widget_tests/wallet_info_row/sub_widgets/wallet_info_row_balance_future_test.dart @@ -51,7 +51,7 @@ void main() { (realInvocation) => ChangeNotifierProvider((ref) => manager)); const walletInfoRowBalance = - WalletInfoRowBalanceFuture(walletId: "some-wallet-id"); + WalletInfoRowBalance(walletId: "some-wallet-id"); await widgetTester.pumpWidget( ProviderScope( overrides: [ @@ -76,6 +76,6 @@ void main() { await widgetTester.pumpAndSettle(); - expect(find.byType(WalletInfoRowBalanceFuture), findsOneWidget); + expect(find.byType(WalletInfoRowBalance), findsOneWidget); }); } diff --git a/test/widget_tests/wallet_info_row/sub_widgets/wallet_info_row_balance_future_test.mocks.dart b/test/widget_tests/wallet_info_row/sub_widgets/wallet_info_row_balance_future_test.mocks.dart index 028cd047e..9d99f22ed 100644 --- a/test/widget_tests/wallet_info_row/sub_widgets/wallet_info_row_balance_future_test.mocks.dart +++ b/test/widget_tests/wallet_info_row/sub_widgets/wallet_info_row_balance_future_test.mocks.dart @@ -13,7 +13,7 @@ import 'package:bitcoindart/bitcoindart.dart' as _i13; import 'package:flutter/foundation.dart' as _i4; import 'package:flutter_riverpod/flutter_riverpod.dart' as _i5; import 'package:mockito/mockito.dart' as _i1; -import 'package:stackwallet/db/main_db.dart' as _i12; +import 'package:stackwallet/db/isar/main_db.dart' as _i12; import 'package:stackwallet/electrumx_rpc/cached_electrumx.dart' as _i10; import 'package:stackwallet/electrumx_rpc/electrumx.dart' as _i9; import 'package:stackwallet/models/balance.dart' as _i11; @@ -481,6 +481,14 @@ class MockWalletsService extends _i1.Mock implements _i2.WalletsService { returnValue: _i22.Future.value(false), ) as _i22.Future); @override + Map fetchWalletsData() => (super.noSuchMethod( + Invocation.method( + #fetchWalletsData, + [], + ), + returnValue: {}, + ) as Map); + @override _i22.Future addExistingStackWallet({ required String? name, required String? walletId, @@ -998,7 +1006,7 @@ class MockBitcoinWallet extends _i1.Mock implements _i25.BitcoinWallet { @override _i22.Future> prepareSend({ required String? address, - required int? satoshiAmount, + required int? amount, Map? args, }) => (super.noSuchMethod( @@ -1007,7 +1015,7 @@ class MockBitcoinWallet extends _i1.Mock implements _i25.BitcoinWallet { [], { #address: address, - #satoshiAmount: satoshiAmount, + #satoshiAmount: amount, #args: args, }, ), @@ -1431,6 +1439,25 @@ class MockBitcoinWallet extends _i1.Mock implements _i25.BitcoinWallet { returnValueForMissingStub: _i22.Future.value(), ) as _i22.Future); @override + List getWalletTokenContractAddresses() => (super.noSuchMethod( + Invocation.method( + #getWalletTokenContractAddresses, + [], + ), + returnValue: [], + ) as List); + @override + _i22.Future updateWalletTokenContractAddresses( + List? contractAddresses) => + (super.noSuchMethod( + Invocation.method( + #updateWalletTokenContractAddresses, + [contractAddresses], + ), + returnValue: _i22.Future.value(), + returnValueForMissingStub: _i22.Future.value(), + ) as _i22.Future); + @override void initWalletDB({_i12.MainDB? mockableOverride}) => super.noSuchMethod( Invocation.method( #initWalletDB, @@ -1676,7 +1703,7 @@ class MockBitcoinWallet extends _i1.Mock implements _i25.BitcoinWallet { @override _i22.Future> preparePaymentCodeSend({ required _i17.PaymentCode? paymentCode, - required int? satoshiAmount, + required int? amount, Map? args, }) => (super.noSuchMethod( @@ -1685,7 +1712,7 @@ class MockBitcoinWallet extends _i1.Mock implements _i25.BitcoinWallet { [], { #paymentCode: paymentCode, - #satoshiAmount: satoshiAmount, + #satoshiAmount: amount, #args: args, }, ), @@ -2348,6 +2375,11 @@ class MockManager extends _i1.Mock implements _i6.Manager { returnValue: false, ) as bool); @override + bool get hasTokenSupport => (super.noSuchMethod( + Invocation.getter(#hasTokenSupport), + returnValue: false, + ) as bool); + @override bool get hasWhirlpoolSupport => (super.noSuchMethod( Invocation.getter(#hasWhirlpoolSupport), returnValue: false, @@ -2382,7 +2414,7 @@ class MockManager extends _i1.Mock implements _i6.Manager { @override _i22.Future> prepareSend({ required String? address, - required int? satoshiAmount, + required int? amount, Map? args, }) => (super.noSuchMethod( @@ -2391,7 +2423,7 @@ class MockManager extends _i1.Mock implements _i6.Manager { [], { #address: address, - #satoshiAmount: satoshiAmount, + #satoshiAmount: amount, #args: args, }, ), @@ -2694,7 +2726,7 @@ class MockCoinServiceAPI extends _i1.Mock implements _i19.CoinServiceAPI { @override _i22.Future> prepareSend({ required String? address, - required int? satoshiAmount, + required int? amount, Map? args, }) => (super.noSuchMethod( @@ -2703,7 +2735,7 @@ class MockCoinServiceAPI extends _i1.Mock implements _i19.CoinServiceAPI { [], { #address: address, - #satoshiAmount: satoshiAmount, + #satoshiAmount: amount, #args: args, }, ), diff --git a/test/widget_tests/wallet_info_row/wallet_info_row_test.dart b/test/widget_tests/wallet_info_row/wallet_info_row_test.dart index 67f7527be..c14cbb896 100644 --- a/test/widget_tests/wallet_info_row/wallet_info_row_test.dart +++ b/test/widget_tests/wallet_info_row/wallet_info_row_test.dart @@ -74,6 +74,6 @@ void main() { await widgetTester.pumpAndSettle(); expect(find.text("some wallet"), findsOneWidget); - expect(find.byType(WalletInfoRowBalanceFuture), findsOneWidget); + expect(find.byType(WalletInfoRowBalance), findsOneWidget); }); } diff --git a/test/widget_tests/wallet_info_row/wallet_info_row_test.mocks.dart b/test/widget_tests/wallet_info_row/wallet_info_row_test.mocks.dart index 8b75cb42e..e4daea467 100644 --- a/test/widget_tests/wallet_info_row/wallet_info_row_test.mocks.dart +++ b/test/widget_tests/wallet_info_row/wallet_info_row_test.mocks.dart @@ -13,7 +13,7 @@ import 'package:bitcoindart/bitcoindart.dart' as _i13; import 'package:flutter/foundation.dart' as _i4; import 'package:flutter_riverpod/flutter_riverpod.dart' as _i5; import 'package:mockito/mockito.dart' as _i1; -import 'package:stackwallet/db/main_db.dart' as _i12; +import 'package:stackwallet/db/isar/main_db.dart' as _i12; import 'package:stackwallet/electrumx_rpc/cached_electrumx.dart' as _i10; import 'package:stackwallet/electrumx_rpc/electrumx.dart' as _i9; import 'package:stackwallet/models/balance.dart' as _i11; @@ -481,6 +481,14 @@ class MockWalletsService extends _i1.Mock implements _i2.WalletsService { returnValue: _i22.Future.value(false), ) as _i22.Future); @override + Map fetchWalletsData() => (super.noSuchMethod( + Invocation.method( + #fetchWalletsData, + [], + ), + returnValue: {}, + ) as Map); + @override _i22.Future addExistingStackWallet({ required String? name, required String? walletId, @@ -998,7 +1006,7 @@ class MockBitcoinWallet extends _i1.Mock implements _i25.BitcoinWallet { @override _i22.Future> prepareSend({ required String? address, - required int? satoshiAmount, + required int? amount, Map? args, }) => (super.noSuchMethod( @@ -1007,7 +1015,7 @@ class MockBitcoinWallet extends _i1.Mock implements _i25.BitcoinWallet { [], { #address: address, - #satoshiAmount: satoshiAmount, + #satoshiAmount: amount, #args: args, }, ), @@ -1431,6 +1439,25 @@ class MockBitcoinWallet extends _i1.Mock implements _i25.BitcoinWallet { returnValueForMissingStub: _i22.Future.value(), ) as _i22.Future); @override + List getWalletTokenContractAddresses() => (super.noSuchMethod( + Invocation.method( + #getWalletTokenContractAddresses, + [], + ), + returnValue: [], + ) as List); + @override + _i22.Future updateWalletTokenContractAddresses( + List? contractAddresses) => + (super.noSuchMethod( + Invocation.method( + #updateWalletTokenContractAddresses, + [contractAddresses], + ), + returnValue: _i22.Future.value(), + returnValueForMissingStub: _i22.Future.value(), + ) as _i22.Future); + @override void initWalletDB({_i12.MainDB? mockableOverride}) => super.noSuchMethod( Invocation.method( #initWalletDB, @@ -1676,7 +1703,7 @@ class MockBitcoinWallet extends _i1.Mock implements _i25.BitcoinWallet { @override _i22.Future> preparePaymentCodeSend({ required _i17.PaymentCode? paymentCode, - required int? satoshiAmount, + required int? amount, Map? args, }) => (super.noSuchMethod( @@ -1685,7 +1712,7 @@ class MockBitcoinWallet extends _i1.Mock implements _i25.BitcoinWallet { [], { #paymentCode: paymentCode, - #satoshiAmount: satoshiAmount, + #satoshiAmount: amount, #args: args, }, ), @@ -2348,6 +2375,11 @@ class MockManager extends _i1.Mock implements _i6.Manager { returnValue: false, ) as bool); @override + bool get hasTokenSupport => (super.noSuchMethod( + Invocation.getter(#hasTokenSupport), + returnValue: false, + ) as bool); + @override bool get hasWhirlpoolSupport => (super.noSuchMethod( Invocation.getter(#hasWhirlpoolSupport), returnValue: false, @@ -2382,7 +2414,7 @@ class MockManager extends _i1.Mock implements _i6.Manager { @override _i22.Future> prepareSend({ required String? address, - required int? satoshiAmount, + required int? amount, Map? args, }) => (super.noSuchMethod( @@ -2391,7 +2423,7 @@ class MockManager extends _i1.Mock implements _i6.Manager { [], { #address: address, - #satoshiAmount: satoshiAmount, + #satoshiAmount: amount, #args: args, }, ), @@ -2694,7 +2726,7 @@ class MockCoinServiceAPI extends _i1.Mock implements _i19.CoinServiceAPI { @override _i22.Future> prepareSend({ required String? address, - required int? satoshiAmount, + required int? amount, Map? args, }) => (super.noSuchMethod( @@ -2703,7 +2735,7 @@ class MockCoinServiceAPI extends _i1.Mock implements _i19.CoinServiceAPI { [], { #address: address, - #satoshiAmount: satoshiAmount, + #satoshiAmount: amount, #args: args, }, ),