From 3273cf0e23174252b54cc2a2c7df36a3f6141493 Mon Sep 17 00:00:00 2001 From: julian-CStack Date: Thu, 13 Jul 2023 16:26:29 -0600 Subject: [PATCH 001/169] disable new wownero wallet unless platform is android --- .../create_wallet_button_group.dart | 58 ++++++++++--------- 1 file changed, 31 insertions(+), 27 deletions(-) diff --git a/lib/pages/add_wallet_views/create_or_restore_wallet_view/sub_widgets/create_wallet_button_group.dart b/lib/pages/add_wallet_views/create_or_restore_wallet_view/sub_widgets/create_wallet_button_group.dart index e8c7c711c..36c914fd8 100644 --- a/lib/pages/add_wallet_views/create_or_restore_wallet_view/sub_widgets/create_wallet_button_group.dart +++ b/lib/pages/add_wallet_views/create_or_restore_wallet_view/sub_widgets/create_wallet_button_group.dart @@ -8,6 +8,8 @@ * */ +import 'dart:io'; + import 'package:flutter/material.dart'; import 'package:stackwallet/pages/add_wallet_views/name_your_wallet_view/name_your_wallet_view.dart'; import 'package:stackwallet/themes/stack_colors.dart'; @@ -32,35 +34,37 @@ class CreateWalletButtonGroup extends StatelessWidget { crossAxisAlignment: isDesktop ? CrossAxisAlignment.center : CrossAxisAlignment.stretch, children: [ - ConstrainedBox( - constraints: BoxConstraints( - minHeight: isDesktop ? 70 : 0, - minWidth: isDesktop ? 480 : 0, - ), - child: TextButton( - style: Theme.of(context) - .extension()! - .getPrimaryEnabledButtonStyle(context), - onPressed: () { - Navigator.of(context).pushNamed( - NameYourWalletView.routeName, - arguments: Tuple2( - AddWalletType.New, - coin, - ), - ); - }, - child: Text( - "Create new wallet", - style: isDesktop - ? STextStyles.desktopButtonEnabled(context) - : STextStyles.button(context), + if (Platform.isAndroid || coin != Coin.wownero) + ConstrainedBox( + constraints: BoxConstraints( + minHeight: isDesktop ? 70 : 0, + minWidth: isDesktop ? 480 : 0, + ), + child: TextButton( + style: Theme.of(context) + .extension()! + .getPrimaryEnabledButtonStyle(context), + onPressed: () { + Navigator.of(context).pushNamed( + NameYourWalletView.routeName, + arguments: Tuple2( + AddWalletType.New, + coin, + ), + ); + }, + child: Text( + "Create new wallet", + style: isDesktop + ? STextStyles.desktopButtonEnabled(context) + : STextStyles.button(context), + ), ), ), - ), - SizedBox( - height: isDesktop ? 16 : 12, - ), + if (Platform.isAndroid || coin != Coin.wownero) + SizedBox( + height: isDesktop ? 16 : 12, + ), ConstrainedBox( constraints: BoxConstraints( minHeight: isDesktop ? 70 : 0, From 9ede1e9fc9507b54065224089f3015942652c208 Mon Sep 17 00:00:00 2001 From: julian-CStack Date: Mon, 17 Jul 2023 09:35:08 -0600 Subject: [PATCH 002/169] update refs --- crypto_plugins/flutter_libepiccash | 2 +- crypto_plugins/flutter_liblelantus | 2 +- crypto_plugins/flutter_libmonero | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/crypto_plugins/flutter_libepiccash b/crypto_plugins/flutter_libepiccash index cd12741de..c99e2a00b 160000 --- a/crypto_plugins/flutter_libepiccash +++ b/crypto_plugins/flutter_libepiccash @@ -1 +1 @@ -Subproject commit cd12741de19e4faef39a23b7d543a2452524990a +Subproject commit c99e2a00b6bb98d497e99858e75ffe75f5898133 diff --git a/crypto_plugins/flutter_liblelantus b/crypto_plugins/flutter_liblelantus index ec3cf5e8e..45d15a4d6 160000 --- a/crypto_plugins/flutter_liblelantus +++ b/crypto_plugins/flutter_liblelantus @@ -1 +1 @@ -Subproject commit ec3cf5e8e1b90e006188aa8c323d4cd52dbfa9b9 +Subproject commit 45d15a4d6f5aa276751dbf9fb3ae076ffbebab44 diff --git a/crypto_plugins/flutter_libmonero b/crypto_plugins/flutter_libmonero index 26a152fea..782c8d00c 160000 --- a/crypto_plugins/flutter_libmonero +++ b/crypto_plugins/flutter_libmonero @@ -1 +1 @@ -Subproject commit 26a152fea3ca4b8c3f1130392a02f579c2ff218c +Subproject commit 782c8d00c10d9dde906c1da982b1be7d45767ae7 From aec991600d2cecc5f87a4957f41b0c00a44c5f19 Mon Sep 17 00:00:00 2001 From: julian-CStack Date: Mon, 17 Jul 2023 09:36:16 -0600 Subject: [PATCH 003/169] add macos build script --- scripts/macos/build_all.sh | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100755 scripts/macos/build_all.sh diff --git a/scripts/macos/build_all.sh b/scripts/macos/build_all.sh new file mode 100755 index 000000000..646274593 --- /dev/null +++ b/scripts/macos/build_all.sh @@ -0,0 +1,8 @@ +#!/bin/bash + +(cd ../../crypto_plugins/flutter_liblelantus/scripts/macos && ./build_all.sh ) & +(cd ../../crypto_plugins/flutter_libepiccash/scripts/macos && ./build_all.sh ) & +(cd ../../crypto_plugins/flutter_libmonero/scripts/macos/ && ./build_all.sh ) & + +wait +echo "Done building" \ No newline at end of file From 047291353ee0ba3f2f5ce76a84e660951b204a37 Mon Sep 17 00:00:00 2001 From: julian-CStack Date: Mon, 17 Jul 2023 09:50:28 -0600 Subject: [PATCH 004/169] macos file changes --- macos/Flutter/GeneratedPluginRegistrant.swift | 8 ++- pubspec.lock | 58 +------------------ 2 files changed, 7 insertions(+), 59 deletions(-) diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index b741c3c0a..7cb956395 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -6,6 +6,9 @@ import FlutterMacOS import Foundation import connectivity_plus +import cw_monero +import cw_shared_external +import cw_wownero import desktop_drop import device_info_plus import devicelocale @@ -16,7 +19,6 @@ import isar_flutter_libs import package_info_plus import path_provider_foundation import share_plus -import shared_preferences_foundation import stack_wallet_backup import url_launcher_macos import wakelock_macos @@ -24,6 +26,9 @@ import window_size func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { ConnectivityPlugin.register(with: registry.registrar(forPlugin: "ConnectivityPlugin")) + CwMoneroPlugin.register(with: registry.registrar(forPlugin: "CwMoneroPlugin")) + CwSharedExternalPlugin.register(with: registry.registrar(forPlugin: "CwSharedExternalPlugin")) + CwWowneroPlugin.register(with: registry.registrar(forPlugin: "CwWowneroPlugin")) DesktopDropPlugin.register(with: registry.registrar(forPlugin: "DesktopDropPlugin")) DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin")) DevicelocalePlugin.register(with: registry.registrar(forPlugin: "DevicelocalePlugin")) @@ -34,7 +39,6 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { FLTPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FLTPackageInfoPlusPlugin")) PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin")) - SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) StackWalletBackupPlugin.register(with: registry.registrar(forPlugin: "StackWalletBackupPlugin")) UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) WakelockMacosPlugin.register(with: registry.registrar(forPlugin: "WakelockMacosPlugin")) diff --git a/pubspec.lock b/pubspec.lock index 2dcd3c598..9ec8db667 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1349,62 +1349,6 @@ packages: url: "https://pub.dev" source: hosted version: "3.2.1" - shared_preferences: - dependency: transitive - description: - name: shared_preferences - sha256: "16d3fb6b3692ad244a695c0183fca18cf81fd4b821664394a781de42386bf022" - url: "https://pub.dev" - source: hosted - version: "2.1.1" - shared_preferences_android: - dependency: transitive - description: - name: shared_preferences_android - sha256: "6478c6bbbecfe9aced34c483171e90d7c078f5883558b30ec3163cf18402c749" - url: "https://pub.dev" - source: hosted - version: "2.1.4" - shared_preferences_foundation: - dependency: transitive - description: - name: shared_preferences_foundation - sha256: e014107bb79d6d3297196f4f2d0db54b5d1f85b8ea8ff63b8e8b391a02700feb - url: "https://pub.dev" - source: hosted - version: "2.2.2" - shared_preferences_linux: - dependency: transitive - description: - name: shared_preferences_linux - sha256: "9d387433ca65717bbf1be88f4d5bb18f10508917a8fa2fb02e0fd0d7479a9afa" - url: "https://pub.dev" - source: hosted - version: "2.2.0" - shared_preferences_platform_interface: - dependency: transitive - description: - name: shared_preferences_platform_interface - sha256: fb5cf25c0235df2d0640ac1b1174f6466bd311f621574997ac59018a6664548d - url: "https://pub.dev" - source: hosted - version: "2.2.0" - shared_preferences_web: - dependency: transitive - description: - name: shared_preferences_web - sha256: "74083203a8eae241e0de4a0d597dbedab3b8fef5563f33cf3c12d7e93c655ca5" - url: "https://pub.dev" - source: hosted - version: "2.1.0" - shared_preferences_windows: - dependency: transitive - description: - name: shared_preferences_windows - sha256: "5e588e2efef56916a3b229c3bfe81e6a525665a454519ca51dbcc4236a274173" - url: "https://pub.dev" - source: hosted - version: "2.2.0" shelf: dependency: transitive description: @@ -1887,4 +1831,4 @@ packages: version: "1.0.0" sdks: dart: ">=3.0.2 <4.0.0" - flutter: ">=3.10.0" + flutter: ">=3.10.3" From 407bdc4d925610b706f2a1248dc4f3291c9e14c3 Mon Sep 17 00:00:00 2001 From: julian-CStack Date: Mon, 17 Jul 2023 10:29:08 -0600 Subject: [PATCH 005/169] update macos xcode project files --- .metadata | 16 +- macos/Podfile | 5 +- macos/Podfile.lock | 134 ++++++++--- macos/Runner.xcodeproj/project.pbxproj | 219 +++++++++++++++--- .../xcshareddata/xcschemes/Runner.xcscheme | 11 + .../AppIcon.appiconset/Contents.json | 132 +++++------ .../AppIcon.appiconset/app_icon_1024.png | Bin 69450 -> 102994 bytes .../AppIcon.appiconset/app_icon_128.png | Bin 4664 -> 5680 bytes .../AppIcon.appiconset/app_icon_16.png | Bin 926 -> 520 bytes .../AppIcon.appiconset/app_icon_256.png | Bin 10085 -> 14142 bytes .../AppIcon.appiconset/app_icon_32.png | Bin 1441 -> 1066 bytes .../AppIcon.appiconset/app_icon_512.png | Bin 26089 -> 36406 bytes .../AppIcon.appiconset/app_icon_64.png | Bin 2443 -> 2218 bytes macos/Runner/Configs/AppInfo.xcconfig | 2 +- macos/Runner/MainFlutterWindow.swift | 2 +- macos/RunnerTests/RunnerTests.swift | 12 + test/widget_test.dart | 30 +++ 17 files changed, 421 insertions(+), 142 deletions(-) create mode 100644 macos/RunnerTests/RunnerTests.swift create mode 100644 test/widget_test.dart diff --git a/.metadata b/.metadata index d1551205d..675f4a740 100644 --- a/.metadata +++ b/.metadata @@ -4,7 +4,7 @@ # This file should be version controlled. version: - revision: f1875d570e39de09040c8f79aa13cc56baab8db1 + revision: f92f44110e87bad5ff168335c36da6f6053036e6 channel: stable project_type: app @@ -13,17 +13,11 @@ project_type: app migration: platforms: - platform: root - create_revision: f1875d570e39de09040c8f79aa13cc56baab8db1 - base_revision: f1875d570e39de09040c8f79aa13cc56baab8db1 - - platform: linux - create_revision: f1875d570e39de09040c8f79aa13cc56baab8db1 - base_revision: f1875d570e39de09040c8f79aa13cc56baab8db1 + create_revision: f92f44110e87bad5ff168335c36da6f6053036e6 + base_revision: f92f44110e87bad5ff168335c36da6f6053036e6 - platform: macos - create_revision: f1875d570e39de09040c8f79aa13cc56baab8db1 - base_revision: f1875d570e39de09040c8f79aa13cc56baab8db1 - - platform: windows - create_revision: f1875d570e39de09040c8f79aa13cc56baab8db1 - base_revision: f1875d570e39de09040c8f79aa13cc56baab8db1 + create_revision: f92f44110e87bad5ff168335c36da6f6053036e6 + base_revision: f92f44110e87bad5ff168335c36da6f6053036e6 # User provided section diff --git a/macos/Podfile b/macos/Podfile index dade8dfad..c795730db 100644 --- a/macos/Podfile +++ b/macos/Podfile @@ -1,4 +1,4 @@ -platform :osx, '10.11' +platform :osx, '10.14' # CocoaPods analytics sends network stats synchronously affecting flutter build latency. ENV['COCOAPODS_DISABLE_STATS'] = 'true' @@ -31,6 +31,9 @@ target 'Runner' do use_modular_headers! flutter_install_all_macos_pods File.dirname(File.realpath(__FILE__)) + target 'RunnerTests' do + inherit! :search_paths + end end post_install do |installer| diff --git a/macos/Podfile.lock b/macos/Podfile.lock index 82579cd8e..2b6385a35 100644 --- a/macos/Podfile.lock +++ b/macos/Podfile.lock @@ -1,24 +1,74 @@ PODS: - - connectivity_plus_macos (0.0.1): + - connectivity_plus (0.0.1): - FlutterMacOS - ReachabilitySwift + - cw_monero (0.0.1): + - cw_monero/Boost (= 0.0.1) + - cw_monero/Monero (= 0.0.1) + - cw_monero/OpenSSL (= 0.0.1) + - cw_monero/Sodium (= 0.0.1) + - cw_monero/Unbound (= 0.0.1) + - FlutterMacOS + - cw_monero/Boost (0.0.1): + - FlutterMacOS + - cw_monero/Monero (0.0.1): + - FlutterMacOS + - cw_monero/OpenSSL (0.0.1): + - FlutterMacOS + - cw_monero/Sodium (0.0.1): + - FlutterMacOS + - cw_monero/Unbound (0.0.1): + - FlutterMacOS + - cw_shared_external (0.0.1): + - cw_shared_external/Boost (= 0.0.1) + - cw_shared_external/OpenSSL (= 0.0.1) + - cw_shared_external/Sodium (= 0.0.1) + - FlutterMacOS + - cw_shared_external/Boost (0.0.1): + - FlutterMacOS + - cw_shared_external/OpenSSL (0.0.1): + - FlutterMacOS + - cw_shared_external/Sodium (0.0.1): + - FlutterMacOS + - cw_wownero (0.0.1): + - cw_wownero/Boost (= 0.0.1) + - cw_wownero/OpenSSL (= 0.0.1) + - cw_wownero/Sodium (= 0.0.1) + - cw_wownero/Unbound (= 0.0.1) + - cw_wownero/Wownero (= 0.0.1) + - FlutterMacOS + - cw_wownero/Boost (0.0.1): + - FlutterMacOS + - cw_wownero/OpenSSL (0.0.1): + - FlutterMacOS + - cw_wownero/Sodium (0.0.1): + - FlutterMacOS + - cw_wownero/Unbound (0.0.1): + - FlutterMacOS + - cw_wownero/Wownero (0.0.1): + - FlutterMacOS + - desktop_drop (0.0.1): + - FlutterMacOS + - device_info_plus (0.0.1): + - FlutterMacOS + - devicelocale (0.0.1): + - FlutterMacOS - flutter_libepiccash (0.0.1): - FlutterMacOS - flutter_local_notifications (0.0.1): - FlutterMacOS - - flutter_secure_storage_macos (3.3.1): + - flutter_secure_storage_macos (6.1.1): - FlutterMacOS - FlutterMacOS (1.0.0) - isar_flutter_libs (1.0.0): - FlutterMacOS - - package_info_plus_macos (0.0.1): + - package_info_plus (0.0.1): - FlutterMacOS - - path_provider_macos (0.0.1): + - path_provider_foundation (0.0.1): + - Flutter - FlutterMacOS - ReachabilitySwift (5.0.0) - - share_plus_macos (0.0.1): - - FlutterMacOS - - shared_preferences_macos (0.0.1): + - share_plus (0.0.1): - FlutterMacOS - stack_wallet_backup (0.0.1): - FlutterMacOS @@ -30,16 +80,21 @@ PODS: - FlutterMacOS DEPENDENCIES: - - connectivity_plus_macos (from `Flutter/ephemeral/.symlinks/plugins/connectivity_plus_macos/macos`) + - connectivity_plus (from `Flutter/ephemeral/.symlinks/plugins/connectivity_plus/macos`) + - cw_monero (from `Flutter/ephemeral/.symlinks/plugins/cw_monero/macos`) + - cw_shared_external (from `Flutter/ephemeral/.symlinks/plugins/cw_shared_external/macos`) + - cw_wownero (from `Flutter/ephemeral/.symlinks/plugins/cw_wownero/macos`) + - desktop_drop (from `Flutter/ephemeral/.symlinks/plugins/desktop_drop/macos`) + - device_info_plus (from `Flutter/ephemeral/.symlinks/plugins/device_info_plus/macos`) + - devicelocale (from `Flutter/ephemeral/.symlinks/plugins/devicelocale/macos`) - flutter_libepiccash (from `Flutter/ephemeral/.symlinks/plugins/flutter_libepiccash/macos`) - flutter_local_notifications (from `Flutter/ephemeral/.symlinks/plugins/flutter_local_notifications/macos`) - flutter_secure_storage_macos (from `Flutter/ephemeral/.symlinks/plugins/flutter_secure_storage_macos/macos`) - FlutterMacOS (from `Flutter/ephemeral`) - isar_flutter_libs (from `Flutter/ephemeral/.symlinks/plugins/isar_flutter_libs/macos`) - - package_info_plus_macos (from `Flutter/ephemeral/.symlinks/plugins/package_info_plus_macos/macos`) - - path_provider_macos (from `Flutter/ephemeral/.symlinks/plugins/path_provider_macos/macos`) - - share_plus_macos (from `Flutter/ephemeral/.symlinks/plugins/share_plus_macos/macos`) - - shared_preferences_macos (from `Flutter/ephemeral/.symlinks/plugins/shared_preferences_macos/macos`) + - package_info_plus (from `Flutter/ephemeral/.symlinks/plugins/package_info_plus/macos`) + - path_provider_foundation (from `Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin`) + - share_plus (from `Flutter/ephemeral/.symlinks/plugins/share_plus/macos`) - stack_wallet_backup (from `Flutter/ephemeral/.symlinks/plugins/stack_wallet_backup/macos`) - url_launcher_macos (from `Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos`) - wakelock_macos (from `Flutter/ephemeral/.symlinks/plugins/wakelock_macos/macos`) @@ -50,8 +105,20 @@ SPEC REPOS: - ReachabilitySwift EXTERNAL SOURCES: - connectivity_plus_macos: - :path: Flutter/ephemeral/.symlinks/plugins/connectivity_plus_macos/macos + connectivity_plus: + :path: Flutter/ephemeral/.symlinks/plugins/connectivity_plus/macos + cw_monero: + :path: Flutter/ephemeral/.symlinks/plugins/cw_monero/macos + cw_shared_external: + :path: Flutter/ephemeral/.symlinks/plugins/cw_shared_external/macos + cw_wownero: + :path: Flutter/ephemeral/.symlinks/plugins/cw_wownero/macos + desktop_drop: + :path: Flutter/ephemeral/.symlinks/plugins/desktop_drop/macos + device_info_plus: + :path: Flutter/ephemeral/.symlinks/plugins/device_info_plus/macos + devicelocale: + :path: Flutter/ephemeral/.symlinks/plugins/devicelocale/macos flutter_libepiccash: :path: Flutter/ephemeral/.symlinks/plugins/flutter_libepiccash/macos flutter_local_notifications: @@ -62,14 +129,12 @@ EXTERNAL SOURCES: :path: Flutter/ephemeral isar_flutter_libs: :path: Flutter/ephemeral/.symlinks/plugins/isar_flutter_libs/macos - package_info_plus_macos: - :path: Flutter/ephemeral/.symlinks/plugins/package_info_plus_macos/macos - path_provider_macos: - :path: Flutter/ephemeral/.symlinks/plugins/path_provider_macos/macos - share_plus_macos: - :path: Flutter/ephemeral/.symlinks/plugins/share_plus_macos/macos - shared_preferences_macos: - :path: Flutter/ephemeral/.symlinks/plugins/shared_preferences_macos/macos + package_info_plus: + :path: Flutter/ephemeral/.symlinks/plugins/package_info_plus/macos + path_provider_foundation: + :path: Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin + share_plus: + :path: Flutter/ephemeral/.symlinks/plugins/share_plus/macos stack_wallet_backup: :path: Flutter/ephemeral/.symlinks/plugins/stack_wallet_backup/macos url_launcher_macos: @@ -80,22 +145,27 @@ EXTERNAL SOURCES: :path: Flutter/ephemeral/.symlinks/plugins/window_size/macos SPEC CHECKSUMS: - connectivity_plus_macos: f6e86fd000e971d361e54b5afcadc8c8fa773308 + connectivity_plus: 18d3c32514c886e046de60e9c13895109866c747 + cw_monero: a3442556ad3c06365c912735e4a23942a28692b1 + cw_shared_external: 1f631d1132521baac5f4caed43176fa10d4e0d8b + cw_wownero: b4adb1e701fc363de27fa222fcaf4eff6f5fa63a + desktop_drop: 69eeff437544aa619c8db7f4481b3a65f7696898 + device_info_plus: 5401765fde0b8d062a2f8eb65510fb17e77cf07f + devicelocale: 9f0f36ac651cabae2c33f32dcff4f32b61c38225 flutter_libepiccash: b33f7396504712b513b8ff019a3f6f3bdae54cfb flutter_local_notifications: 3805ca215b2fb7f397d78b66db91f6a747af52e4 - flutter_secure_storage_macos: 6ceee8fbc7f484553ad17f79361b556259df89aa - FlutterMacOS: ae6af50a8ea7d6103d888583d46bd8328a7e9811 - isar_flutter_libs: 1948109973b6c2e46d6196b1537688a36a6edeac - package_info_plus_macos: f010621b07802a241d96d01876d6705f15e77c1c - path_provider_macos: 3c0c3b4b0d4a76d2bf989a913c2de869c5641a19 + flutter_secure_storage_macos: d56e2d218c1130b262bef8b4a7d64f88d7f9c9ea + FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24 + isar_flutter_libs: 43385c99864c168fadba7c9adeddc5d38838ca6a + package_info_plus: 02d7a575e80f194102bef286361c6c326e4c29ce + path_provider_foundation: eaf5b3e458fc0e5fbb9940fb09980e853fe058b8 ReachabilitySwift: 985039c6f7b23a1da463388634119492ff86c825 - share_plus_macos: 853ee48e7dce06b633998ca0735d482dd671ade4 - shared_preferences_macos: a64dc611287ed6cbe28fd1297898db1336975727 + share_plus: 76dd39142738f7a68dd57b05093b5e8193f220f7 stack_wallet_backup: 6ebc60b1bdcf11cf1f1cbad9aa78332e1e15778c - url_launcher_macos: 597e05b8e514239626bcf4a850fcf9ef5c856ec3 + url_launcher_macos: 5335912b679c073563f29d89d33d10d459f95451 wakelock_macos: bc3f2a9bd8d2e6c89fee1e1822e7ddac3bd004a9 window_size: 339dafa0b27a95a62a843042038fa6c3c48de195 -PODFILE CHECKSUM: 6eac6b3292e5142cfc23bdeb71848a40ec51c14c +PODFILE CHECKSUM: 236401fc2c932af29a9fcf0e97baeeb2d750d367 COCOAPODS: 1.11.3 diff --git a/macos/Runner.xcodeproj/project.pbxproj b/macos/Runner.xcodeproj/project.pbxproj index 6506a5075..7b723f8a8 100644 --- a/macos/Runner.xcodeproj/project.pbxproj +++ b/macos/Runner.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 51; + objectVersion = 54; objects = { /* Begin PBXAggregateTarget section */ @@ -21,15 +21,24 @@ /* End PBXAggregateTarget section */ /* Begin PBXBuildFile section */ + 331C80D8294CF71000263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C80D7294CF71000263BE5 /* RunnerTests.swift */; }; 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */; }; 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC10F02044A3C60003C045 /* AppDelegate.swift */; }; 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F22044A3C60003C045 /* Assets.xcassets */; }; 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; }; 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; }; - 36299DF6FDF6725B2B9C51D5 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6BB87EF657A3ADFB1CE3E959 /* Pods_Runner.framework */; }; + BFD0376C00E1FFD46376BB9D /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9206484E84CB0AD93E3E68CA /* Pods_RunnerTests.framework */; }; + F653CA022D33E8B60E11A9F3 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E6036BF01BF05EA773C76D22 /* Pods_Runner.framework */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ + 331C80D9294CF71000263BE5 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 33CC10E52044A3C60003C045 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 33CC10EC2044A3C60003C045; + remoteInfo = Runner; + }; 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 33CC10E52044A3C60003C045 /* Project object */; @@ -53,9 +62,14 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + 0FA914E59929120BA65E8403 /* 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 = ""; }; + 174539D042E7AC2AB25A83EB /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = ""; }; + 27CB73AACA5743180CC6CD50 /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = ""; }; + 331C80D5294CF71000263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 331C80D7294CF71000263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; 333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = ""; }; 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = ""; }; - 33CC10ED2044A3C60003C045 /* Stack Wallet.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Stack Wallet.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 33CC10ED2044A3C60003C045 /* stack_wallet.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = stack_wallet.app; sourceTree = BUILT_PRODUCTS_DIR; }; 33CC10F02044A3C60003C045 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 33CC10F22044A3C60003C045 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = Runner/Assets.xcassets; sourceTree = ""; }; 33CC10F52044A3C60003C045 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = ""; }; @@ -67,26 +81,43 @@ 33E51913231747F40026EE4D /* DebugProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DebugProfile.entitlements; sourceTree = ""; }; 33E51914231749380026EE4D /* Release.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Release.entitlements; sourceTree = ""; }; 33E5194F232828860026EE4D /* AppInfo.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppInfo.xcconfig; sourceTree = ""; }; - 6BB87EF657A3ADFB1CE3E959 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 41EE721BF40B8DE895352A2C /* 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 = ""; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = ""; }; - 937DF254AD7EDA15AFE96BD9 /* 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 = ""; }; + 9206484E84CB0AD93E3E68CA /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = ""; }; - BC4589C48A71C3A1A477DD76 /* 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 = ""; }; - EA2D897BC13EBFB1DE697D5C /* 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 = ""; }; + ACB8E553D75AA4AC9A7656CE /* 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 = ""; }; + BF5E76865ACB46314AC27D8F /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = ""; }; + E6036BF01BF05EA773C76D22 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ + 331C80D2294CF70F00263BE5 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + BFD0376C00E1FFD46376BB9D /* Pods_RunnerTests.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; 33CC10EA2044A3C60003C045 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 36299DF6FDF6725B2B9C51D5 /* Pods_Runner.framework in Frameworks */, + F653CA022D33E8B60E11A9F3 /* Pods_Runner.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 331C80D6294CF71000263BE5 /* RunnerTests */ = { + isa = PBXGroup; + children = ( + 331C80D7294CF71000263BE5 /* RunnerTests.swift */, + ); + path = RunnerTests; + sourceTree = ""; + }; 33BA886A226E78AF003329D5 /* Configs */ = { isa = PBXGroup; children = ( @@ -103,16 +134,18 @@ children = ( 33FAB671232836740065AC1E /* Runner */, 33CEB47122A05771004F2AC0 /* Flutter */, + 331C80D6294CF71000263BE5 /* RunnerTests */, 33CC10EE2044A3C60003C045 /* Products */, D73912EC22F37F3D000D13A0 /* Frameworks */, - 9000119722579F22067B9BC0 /* Pods */, + F0D4A0626F78BE1EF2A1E0D6 /* Pods */, ); sourceTree = ""; }; 33CC10EE2044A3C60003C045 /* Products */ = { isa = PBXGroup; children = ( - 33CC10ED2044A3C60003C045 /* Stack Wallet.app */, + 33CC10ED2044A3C60003C045 /* stack_wallet.app */, + 331C80D5294CF71000263BE5 /* RunnerTests.xctest */, ); name = Products; sourceTree = ""; @@ -152,39 +185,62 @@ path = Runner; sourceTree = ""; }; - 9000119722579F22067B9BC0 /* Pods */ = { + D73912EC22F37F3D000D13A0 /* Frameworks */ = { isa = PBXGroup; children = ( - EA2D897BC13EBFB1DE697D5C /* Pods-Runner.debug.xcconfig */, - 937DF254AD7EDA15AFE96BD9 /* Pods-Runner.release.xcconfig */, - BC4589C48A71C3A1A477DD76 /* Pods-Runner.profile.xcconfig */, + E6036BF01BF05EA773C76D22 /* Pods_Runner.framework */, + 9206484E84CB0AD93E3E68CA /* Pods_RunnerTests.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; + F0D4A0626F78BE1EF2A1E0D6 /* Pods */ = { + isa = PBXGroup; + children = ( + 41EE721BF40B8DE895352A2C /* Pods-Runner.debug.xcconfig */, + 0FA914E59929120BA65E8403 /* Pods-Runner.release.xcconfig */, + ACB8E553D75AA4AC9A7656CE /* Pods-Runner.profile.xcconfig */, + BF5E76865ACB46314AC27D8F /* Pods-RunnerTests.debug.xcconfig */, + 174539D042E7AC2AB25A83EB /* Pods-RunnerTests.release.xcconfig */, + 27CB73AACA5743180CC6CD50 /* Pods-RunnerTests.profile.xcconfig */, ); name = Pods; path = Pods; sourceTree = ""; }; - D73912EC22F37F3D000D13A0 /* Frameworks */ = { - isa = PBXGroup; - children = ( - 6BB87EF657A3ADFB1CE3E959 /* Pods_Runner.framework */, - ); - name = Frameworks; - sourceTree = ""; - }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ + 331C80D4294CF70F00263BE5 /* RunnerTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; + buildPhases = ( + FF8CC09C83E12FC1C6EE6A8F /* [CP] Check Pods Manifest.lock */, + 331C80D1294CF70F00263BE5 /* Sources */, + 331C80D2294CF70F00263BE5 /* Frameworks */, + 331C80D3294CF70F00263BE5 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 331C80DA294CF71000263BE5 /* PBXTargetDependency */, + ); + name = RunnerTests; + productName = RunnerTests; + productReference = 331C80D5294CF71000263BE5 /* RunnerTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; 33CC10EC2044A3C60003C045 /* Runner */ = { isa = PBXNativeTarget; buildConfigurationList = 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */; buildPhases = ( - DF80A3E256A63BF2D2008937 /* [CP] Check Pods Manifest.lock */, + AAFD86C9DC38B4393BC9D8E0 /* [CP] Check Pods Manifest.lock */, 33CC10E92044A3C60003C045 /* Sources */, 33CC10EA2044A3C60003C045 /* Frameworks */, 33CC10EB2044A3C60003C045 /* Resources */, 33CC110E2044A8840003C045 /* Bundle Framework */, 3399D490228B24CF009A79C7 /* ShellScript */, - 8D7CC24E5AE846869656D4D1 /* [CP] Embed Pods Frameworks */, + 529691D83C3BADE14E2EAC03 /* [CP] Embed Pods Frameworks */, ); buildRules = ( ); @@ -193,7 +249,7 @@ ); name = Runner; productName = Runner; - productReference = 33CC10ED2044A3C60003C045 /* Stack Wallet.app */; + productReference = 33CC10ED2044A3C60003C045 /* stack_wallet.app */; productType = "com.apple.product-type.application"; }; /* End PBXNativeTarget section */ @@ -206,6 +262,10 @@ LastUpgradeCheck = 1300; ORGANIZATIONNAME = ""; TargetAttributes = { + 331C80D4294CF70F00263BE5 = { + CreatedOnToolsVersion = 14.0; + TestTargetID = 33CC10EC2044A3C60003C045; + }; 33CC10EC2044A3C60003C045 = { CreatedOnToolsVersion = 9.2; LastSwiftMigration = 1100; @@ -236,12 +296,20 @@ projectRoot = ""; targets = ( 33CC10EC2044A3C60003C045 /* Runner */, + 331C80D4294CF70F00263BE5 /* RunnerTests */, 33CC111A2044C6BA0003C045 /* Flutter Assemble */, ); }; /* End PBXProject section */ /* Begin PBXResourcesBuildPhase section */ + 331C80D3294CF70F00263BE5 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; 33CC10EB2044A3C60003C045 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; @@ -256,6 +324,7 @@ /* Begin PBXShellScriptBuildPhase section */ 3399D490228B24CF009A79C7 /* ShellScript */ = { isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); @@ -291,7 +360,7 @@ shellPath = /bin/sh; shellScript = "\"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh && touch Flutter/ephemeral/tripwire"; }; - 8D7CC24E5AE846869656D4D1 /* [CP] Embed Pods Frameworks */ = { + 529691D83C3BADE14E2EAC03 /* [CP] Embed Pods Frameworks */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( @@ -308,7 +377,7 @@ shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; showEnvVarsInLog = 0; }; - DF80A3E256A63BF2D2008937 /* [CP] Check Pods Manifest.lock */ = { + AAFD86C9DC38B4393BC9D8E0 /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( @@ -330,9 +399,39 @@ 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; }; + FF8CC09C83E12FC1C6EE6A8F /* [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-RunnerTests-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 */ + 331C80D1294CF70F00263BE5 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 331C80D8294CF71000263BE5 /* RunnerTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; 33CC10E92044A3C60003C045 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; @@ -346,6 +445,11 @@ /* End PBXSourcesBuildPhase section */ /* Begin PBXTargetDependency section */ + 331C80DA294CF71000263BE5 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 33CC10EC2044A3C60003C045 /* Runner */; + targetProxy = 331C80D9294CF71000263BE5 /* PBXContainerItemProxy */; + }; 33CC11202044C79F0003C045 /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = 33CC111A2044C6BA0003C045 /* Flutter Assemble */; @@ -366,6 +470,51 @@ /* End PBXVariantGroup section */ /* Begin XCBuildConfiguration section */ + 331C80DB294CF71000263BE5 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = BF5E76865ACB46314AC27D8F /* Pods-RunnerTests.debug.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.cypherstack.stackWallet.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/stack_wallet.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/stack_wallet"; + }; + name = Debug; + }; + 331C80DC294CF71000263BE5 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 174539D042E7AC2AB25A83EB /* Pods-RunnerTests.release.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.cypherstack.stackWallet.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/stack_wallet.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/stack_wallet"; + }; + name = Release; + }; + 331C80DD294CF71000263BE5 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 27CB73AACA5743180CC6CD50 /* Pods-RunnerTests.profile.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.cypherstack.stackWallet.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/stack_wallet.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/stack_wallet"; + }; + name = Profile; + }; 338D0CE9231458BD00FA5F75 /* Profile */ = { isa = XCBuildConfiguration; baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; @@ -404,7 +553,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.11; + MACOSX_DEPLOYMENT_TARGET = 10.14; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = macosx; SWIFT_COMPILATION_MODE = wholemodule; @@ -483,7 +632,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.11; + MACOSX_DEPLOYMENT_TARGET = 10.14; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = macosx; @@ -530,7 +679,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.11; + MACOSX_DEPLOYMENT_TARGET = 10.14; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = macosx; SWIFT_COMPILATION_MODE = wholemodule; @@ -596,6 +745,16 @@ /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ + 331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 331C80DB294CF71000263BE5 /* Debug */, + 331C80DC294CF71000263BE5 /* Release */, + 331C80DD294CF71000263BE5 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */ = { isa = XCConfigurationList; buildConfigurations = ( diff --git a/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index c9216c9be..a9d38bc3b 100644 --- a/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -37,6 +37,17 @@ + + + + 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ⅅ8MglG&ER{i5lN315cO?EdHNLrg? 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 z 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 Pe7HEdAEg|=c=LY&YVNkY literal 69450 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 PKlPC5u>%Drr*HfpG~rAC diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png index c8ba3b1a21bf014a0ed55ac2169dc48b4abb240f..13b35eba55c6dabc3aac36f33d859266c18fa0d0 100644 GIT binary patch literal 5680 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 literal 4664 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 diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png index e85f014180ccfa3df01d669e08620973393bc259..0a3f5fa40fb3d1e0710331a48de5d256da3f275d 100644 GIT binary patch delta 495 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 QaR2}S07*qoM6N<$g1Eli`~Uy| delta 1424 zcmV;B1#kMQ2%!s*BYy#eX+uL$Nkc;*aB^>EX>4Tx0C=2zkv&MmKpe$iQ%hA^6zm}4 zkfAzR5EXIMDionYs1;guFuC*#nlvOSE{=k0!NHHks)LKOt`4q(Aou~|=;Wm6A|?JW zDYS_7;J6>}?mh0_0YbgZG^=YI&~)2OCE{WxyDA1>5kwgM2!EhQW|lE0NlA1ZU-$6w z^)AMqI0G~)a%M8;d-XNadv<=St#1U4MRpN8vF_SJx{K$31<2TL)mj#{~ zG1IAe;s~)=Xk(>~S<%#pr--Afrc=I<@mS@&#aSy?S@WL!g`u3jvdndw!$@EeOOPN! zK@}yGVIxMXPJfDp6z#`5_=jA-L@tF~B`|Uj(dX-`!gI$q6qh6bAw?j`J}B z1b2Z(&2heu9j9>u_@99*z2&deftgRzYb`B$1oUkK7uPLK*#jS%3kmA?tkv~-u^w)?C%HcSaNYixY~^X z00Y=bL_t(|0qs{=C}v?8esJuRec#taaxcwDjZ%u#Fg2q@8C(#GJC`UUxp5sNZ+{Q_`}?rAwuX(3&A%hCxw(n)@o`*ST!_DKZf@Y{=!l@8AXr#f{5=B4$H(aI z?#95t0AgZdU}$J4{%>GlfUT`9Y;SMF%gYPCzP^wo{oAW1aCv!&zP>(mb#)06mX?;c z!L`9mEG#Twb!7$q0ReDxa}(sAO@JGYjg6tLt$$6VU4zrpQ(RwPKS+W&mrFA-F@eR! zMFa;2!`9aJ=>(RRmeAVTikX=iol{(0T?s;PCJe!^6XHa&kgoU?9xR&Gi%5 z+ucQbM+b(6hC~LGQaCj=C4{Gx1#N6>YzU1|A3i=lxB9q4fOYQa=@BA3KR?&IDO+%K zbbl1#;o+)Cuw?V|^H^P7MS!di7Z;a51p52?(a_LV;NSpZVPT4O zz7NJ=Htg-~e-o&vs6c&v{p|u*S6BG^`@_`KRMm#Na>;sod*R{Xfsl|8n3P&!3QapW!4$D(-rnA#W=2Lvs)%qezJJRA^a1VnjE#*&+V8iew6?bP;AT|$+}s>i zFEhSKfec!Ng|`il+J#q576g%GNw=sj*gDd+1V)! ztEi|*(G?{!WZfnwCm-FAbsiiXguA=DIE{&kiQ-!J{`B-Tc6N5e!IF}ako@r@vVU`O zbOx5h1-ZGo$jZu6b+YB`|#IkXXc2-%I#-=88$QjAb&PH-_@?9T@ zii$#BULLBds#KAnU(g*Hi0)=*XD5DVa6VH4eWSFr6wS@eqAZb-k$-$3BO^nUqP)CZ zY@`MpN+Tm9LgAXRtOm2Qv-mD|9)IKAQ3e^cw6vf}PFGD$jW8HBcuH`#bzxy4qNAfl ziFBmN)1;=RB0fGI)z#I45H0kj;3eg2W+py=`Ep0ViqnuzRZ>!dPbn!VEh`fppvDL8 zgYP6HBnY89I5_A$*Lxobl4p5cT^)Y@_FMb@ z7mrsVpaJJXnXHQxSs(mKtB#d3R8UYLhRsv(qVEFMxUsQOTrz0S&%uj7OlZZ=4gM+w eUWzv-@C)B~n#-8vW6l5o00{s|MNUMnLSTXcsKg8a diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png index f24fbe65de8627a88836014c48b7f8156a627594..326c0e72c9d820600887813b3b98d0dd69c5d4e8 100644 GIT binary patch literal 36406 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 literal 26089 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

?wGKN5P>m=1%)sND3~`BjaS)39faeQ z-m5Xsn1`Pk{PqA($e_svBTlEyZFOYoyHWnE!Qan*C|}sXu!H@nRq_-f%2yh=X_D21jc&;myx&^_sKAo!0eyw}Pl^^QoJ8{b27(X|(gLuC{Z+}_pmRz+o_8co>sbVEW3uJbh>t=lHDYG4z z=s8w?HD_AmQSdbHiMrFq1=hFqBCCegLOrEOjYr<}s~R-2Ud(9iw{XM$e-IpCub=j< z`yPQ3{f(+qS3!$?(Mu3glz?DZ@8G||y5Pc*fKI|jLPQf+csLE#pJgt#x zN&Ea~J;~2<{Qa0v&eJkNt&2sh}<|H3MOqS3!JkHA$V z&4=2X)u1tDHEx5q3z@&^*Ld7vb+;A+O*!|W0j4(D;L>%z{Uj_sTM?v5o{Bv}LhFE) zcby7De)RV!%>y0fJgVS&3nd6zTcYEhk+_Ob3H4E7*FFCSdlztY=ub0ULiamCTO2mKyf{$Q`9&RDP!Ou~bcKX<=SHdWyuzHU|R8#^*i( z>-eb0x%)Sn@&E4Y`4dd7J(UPFa*>7xY|MAu)#Va>Udw* zelYJnyUc>E7HDP7Fj4`9qJr~g-H4J-)yL-`wfXPWb`ylp~_LgHGL6p`0+thB#N%WYeeAJaRqS+9P|+90d|NN4-mAqHDh#lijh0vs>KeCF9V%(0*~a!j z!dgeNW8vH0ytD-Gh?GcZCoZ}N{I4#K>f)zC2r@bO+eVnEsQ;Rdkyf1eyi0(uqG>1R z#PP=(-`aTpL`m{b&(kfsc%YN-jlajv+~K`_kpS5@Rg~lnVLDcsXklTYO6FZws1^aDXSQi!0cAgQhkb~!s$^s&$X653 z{&qR^285hRXdlC;6GO|pfE1^dX z_yS`Nn0R<`c61b9uWifG?-j8UM(4*&4+})&uXxHBvvXOjhZ>ICglofjSbe{P1DK6U z6YM6v=A$nCavy(veDMMEp_!z@eObGZY-OZ43$1}ZC-V9oLdj|~UVr}g-RAgku#yxK z62l>Kn6NYHvsPg8t~pl=Zxz(fyZ6&$V=Rq6DJq8rNlQEhG+SGLe^!oE2^mo)y|pk% zkRdBD5DW03Yl#;(C!LOYBiWq`Yj?vPqqVKXE;IR%wGtqUT(MGyVL1@;2#KwaU|wB!zWckvr9ab|4e`_b9$6!yq|=1VT7On3uObZT z&$9cnsy&j$+L{CD=hB<@jN{Zj_wQHn$3$uXSL_T&Ce9gTSr3;|oX767`z`z!&XYZe zpr5gW?+Ee}IpK@6!vuvEF)eKZ?b=C4{=t$J<*TVtWC{ zdj*+&-GQlVv#{R5T+X(dW}u0wkKWxBZFX~W7tFEMHP!JN`}_JT5#Zx`v&|L!=43Gu zq%kFWY5nG-q~-sZ7CKFqUCabSw<*NU$Y}aNriNWukOqOdG%I`|_%zR8@I#@Itzo5e zWnTZ$KJ7PRv#I>>;vZGQ&D2V!o9>l(yWnWs!;NL!^%G&?&!~5m};on->$qzUdg^XV-5No zUamKvS5|0x`k=o~t&Z}UH5>S3 zoDzn;z+tFI{>9&kY((t8t21(j3R==_*o(g&5u|6`n|gq1?E z*B=g9s8S<>#xg_6U5dW2^8N&BJU2=#`p5noDgS;|`)!xds;iz<@#ci&-f~ZAH0p9h zHHyu#CX*Wal?P;ip>U3$X7*J>`NsC9TT_mfsI%^5Y5S!U$VjSHp(G5ug}vP(xi{sz zHQE|RXPi;I+I1WuLyG~XgzIw>)US6w7qyUSZ*1kOI9=5rPx7%}S%(UzR1yP>fTYWu zsB^FDqy$6j7oG7MoKG zHF@$_d67fBqt9OvpW|;??{<|{g7rO%X@Cj&xO16$+8M5j{u_$U{K;D30a4 zZ&2wpX+(r89zo+58Ux>BCTs_C#yc}q3VQqR8mBKD=H3%{5$gG#&=BYg0O-{84-xEs z+kqF$2g3!H9q;(r>k$|!bYz^;3T9EuW+e^P%&!L^*K;tt5qo5E)kRdYl)snj|9VU)wfXD5n=L~iH zMF?x^mwy>RH(rVcK6`Vwg)tL$nLZtMa42utA60yzLHREhX{U8-&D!>_d8|k> z>bJ>KlOPPN9EnE(zd{5;3gkkswKj0!>5}2=7-24FqnP53JuP04m z-0t7Ej0^?zmm3Qt55rpSK-`GjGULg1G_I>~q+iz)3geGMKQHa~BC9^T6NqU5Ueo?# zM^0l4yy3$p0h5ZS+bW)-bGyT;92lSGu{KVS;`1V4k+fUr`iVcZqL8KGV1+U`=CjY8 zaUI8+wxpBG@rzgz0Xfirz1M}y-7o4=ATf02cHvW~Kox@GB5*5hwnsgo%5YNxB?d#H zYww;lzoNlF5mP%*oW}%+1T1x5&2Yy`N%MBFj=bkL92!>Vz_^c*0}~fUsmHupNoMmM z(Ut1)yoM)ki{~VL>^IDdoX-iL(ntEf)S%72Ebellk(+&ykzPU+2*0PYO(KG~2g4>? zdrL$|SGG~vk8;Lf5%#92nsg18xeH{mLhMvY83SM*|2aEpKrsxXgSsfIRI+l{EA=U_ zL~`GFO9n(Lq8sJ7HpI0>@dS~mAF33n=O??17Xs2rg%0uwk8nv}q!7qBP56<-8X6kI zu9T=PB|G$`7Q*q4=q!kjr;xHTK+Q`>!j>Os7S{aR2U2z$|9vk!4CG85Qa|JzKsm5A zZS)50=}tyh<$k+;>8n99YIf}0`FR0NF=IDtK{)mtlwQQC@EJG%?x;2@wTbGG89hdu zLXai5)+tRy(~j5_eDuJ=-67em)nezD_W)GL#~_@FMWQoXC*2A3tW`Bi+eAf=T;i+v ziOo%W8|xi4!irtUlo3RtN?LJ5+^tv~7Ty&s8PeF1RS7be)Elw0-D4ew9jEcnwXFo? zkG#rmGGE3lyMsr2a^jlK?{gV>W%8L$2pwz%eI1r4ht|Xt_xGBTG_1jrIy*b7W(ip~m zpwjC<5pXme(9kiTBITG&Yx#SLPV9Zx<0XYzKc!TJDxByS4Hy`Rl;6QSMV{d@kp#j`DOksm3l}OcS}(Qh(Uca<9I$@&$UUFhOQ1M@kBItQ6pY`<3V%K zH8##a-5f2=x#hExk@kuzw~YPXZ2-MHxzag_Lz zHPd#Ix?Ah5XeVS+F=~&|jUpkj6U5iAfPLtOFVwne0X9Rag#XQo_)EOr`lnO-Nx_pY zs?D*%y^g~IMNQIh46v z^QkZqxuf&cIN-J!XJ))Uv)kM442Or;VY2NHo`$pbap*Z#OI?ETV{mU;n8W0z zn%9zqt%cxtMS0=jN7a-~u6qD()sJEexyjBu&#pvdwhx0LNZ^ofDid*|aaRaTHSG}d z8Z~k%`vI%)t$(LLA;}JVECH}#so|xfaSmO6z6(dkVvWN zn4e3Hp_9Gk50X)AR4@B8gQ>kc|B*qFVqqePNYObjuCN}CR(@9cOoZ9Fx4+9?{3{JY zIk!f!Vgl|A0>Ju#Efw9lP~HAx&pPY52kH_NOv#FUr>+xKu4$h!HhhH_|A;TfWLhVno_0q+cT&-=FwAs6{(`|byTmG#;J4#nQ34lIuQ+>y5-T(?J>Vy>IowdXo z<(gG7UREH2@STjKxclh7GU(gB{X*VpWZ^=EZgXA8m0h+{(?rULq+?KV=Oxx?hh`!` z+P3K}@!>Cl!jZuK+-L1qq2TYU%&TEPOzOt=2-O~g92>_;(Iu9Q+n|(#K&Hp`BFu00 zZsQDHN!yI)fB(AbR7WZtyj5vAfU3J|^~cR}=L$Ou@x{?+Tf${HE-Nv&1+^_!tRc)? zY1gcp0z=DT*u;9jo^~zT4kAwgM$+Mt*(5sIx({K8=DYX!mZ@^C9OOnA4FH!?Q1)QQ z4%YCa_qn?qpNCkZvuU+&re4wzaFLb3kR&dec5|OW=XcN{+d_9=TG^+Z?!)}Q&>W>T zVMVjw_?>}r#=?g^oyWrk&$m)O)a%5hOe~5-;HH zdjwSp_Y@bV8Z9QoCViWjQP^aC5JWZGoxyIxv31*{@m%4+0x*>KRDda@M*LfL1A<-y zI9?4Hmtx&tO1WGn{D@DCsbnzi)L2485&1Gv4{e%NiAK*w$YM1{dYg08o!&x$A@)*D z_}r)y#T+!9can4G*k#6|#OgSI{$bNIK1d^4zse=!VwO4}W)0PZAi(R0tq|S&rP>-o z-%K!u$Ei_4Aw%p)h|mz?YfWol7d1Y}ywd}n+F-SRceuCbb?p6g###l}5=&x6AY)6a zz+a!sHB9CvYSw_eeHN#Rv}dwC1$`cMzC2en+K z@tESN)))WIvXKL=mto5%y8~1`x25483i_Nehh{?mpQOMY1ZEdHNu=+Y-|$}Gc{P0I@u=A; z4Te?WIn~{Tn7WN&8vE0HGCrWnyR}Wpz5KBXUMG&P3akv&vC^{dTg_OXzQ8vpVe;3j{Okqx~7;Hr3bbBmFI3K-apPek=thaLmbIwe9B| zLoRPCulF_$!8Ol@I)0;=QBZXKZd8p^Wo+ll-<-Ia`p#m%96Xo6>7fJ%VG$O`LDEms zn~ro55GAR6HB@t$RmZzqCW1=uNpfh$$TcD4u|{vOJ?>pmCOY@LRnOzNvXU4fu2-xp#~9SGX# z);nrYYI08|=P^b6km($iSnd|+uNfI_?pX$g^uwYBo;L%c1fBD;b>)hfpx^lMP6n%WZ!anQA7zHDTb2FU?- z30!}s!}BTZ+a|y7`gPTd<#vm%{*PS1A^1{!cd)@?OmX))$uANx^9b#`c>>{DrDart z;wWD4%$x0x)-aE2DWJG}Gq)EEfjh?4|HOaaTcWi~f|2*pL~QD5vHnx#!nncgBa473 zpxs|QP}}#vqoCOi->!)p27lN+M$G@4NmK$Cv*}GTXE=(?X81_XO7n@(mO}#MN2c*F z?KSk2b|gT5%BPb8<}B%UJME9Ef3MNbO~WJQO*GISxE}2$M+VGwRszQt+=*4i56^GQ znYGL&)4*y#Enu}_TyBtE8WP;Yh1|;I&T3A2b%y0dn?lMbGO~$_ubB}<4XUv_R|X*{ ze((t%>I!AW_~Am7{|SQ`e>3NuMl?NWYlvoj&$7d%GR?S|dcU=KfFx}?u?`=oa2?O( zU{AS7H#RMVQ&sie>`SJj0_ug8sy`OfAc^KmXkf8qdX@`IT433Bj}Y%|301L7aWDyB z4%m(5d)nU8z13Y8v!f#f-U@SQRHw-Z9)Dj5=!A&7;~=ZI9_out7e`BK%u62=m5+@mbj z6I)yqzM?rfKnUYz9_9Mrlk8v%0p6t+0vc&gCsMlBlmZ2d#xbr&sx=pDpXY)ChjXqw z2HKpF%r+AuD%!HI|69nRvGBkJa}nXx*qGHdJXU$s-UCAJLu~dj<%yeU#6DXt^9E8f z>qwiEec}QKSe@&F#m7#jeX5ycG3#(=MJ|{di@EqOT$CDKi#vfN?u+IsBIo7YlGnmE z?U$@G`N)+;1<%ciQM)IFPJ@{OUIZ9Bu>3?!aCGaUCr2lJQ%GHk6HRQjn>`QSzzR5R zaToPxhnP5JwA&MkFY(gvj+9qo)W1}n-n|pknIyEOL$3rB{rpa=q_o1*X=}AHRxU)) zX|QCU6eR?@|MeD%7byz96-RKyqr?a}Bxm!9_dJ!%w$ga5cj;i*Aw>3O3GcTEQ=d3z z2!g5bSjYimMwa}h!CNnl`%ECPbGNANB>aqB9(2v%&`Eik9qt`dC?K(xC*odamX zF19qf%MI|Y=z}Vb0{4!0&wgsWuUiZ^I9k=LCvFTWFf2%3>WpAVq^fo5sX~irW?_wz z>QzcYO5jXMg4hur6j&@A%ZgCvKD<#Z@$i-2A~P5xM&D)#AT15vGSnn#A$C)`FPD_E|*ae z8wZ?Um82CzCTl!BqfL)1%d*8I-jHK8-V4-}&|3t+QV50AY{=m`Ux;7wk^jvGeI``& zRKyezzG>~9I=1I)!CTgmtgu-%FW>c$!TjTcdSZaO=Wt^@e#Zp!fli7Q2Ly%4yubOX zPuX`o8T6Z)YV@tFq0kp|h#zO^zj}}kjq2%i4YduUD&R=TY&1P1g<=jV31xA{!s6md znKHOwhQSLEr&oY+S%>yZ`8M^ZCGSr*iu*^=J#7cl5L zQC)Z(8%mQpj+#bKKX&~H#plMU9t84W=uA^r5^vWEAPJ*YvG*$HHshH`Y+Yqp!o zH!>rHN?2l{-x!Va=sUH@xB#!uPA6-o8%w@h<)`I#+@}h#oh)3kl@zCxgaMg53({NN z9D=Mz<`WxK&P&d>_WGD#EOdZM5M5p`IS3}2j1paM`1vgj<0HY#DXQCcfDA#k%f#2zt} zoKKN|4#T%b&RGRZ6eZ%jK{-K%Cz4YCxz%u0Uw1fW?*Ynd#%Dv>ofLfbw^<+HXTD`cR}2*x zo>lAWAujm9nS&JPk}PRYOmJ#Y2Ydph8-az&!V?@b;*m>$YC2gg45uf@>!0SXeUVz> zeW*)(rHIBdcs%7Ix_r@2!G7ZRii;>o9Q!a9s>4pU2>Dj>LPDhYS*~2H6YfVK#SM!f zBT%I8OXTuA&tKo_wpaB?K$rLIKb_8qB-G~RtJMf)y`*z22zUF_yyjpvbKwH{VA%Dz zR<0U4!vgm|H|mhHv*qw7qmA5qB*_TzWx-Lg@vt9{Y6+}VLov#O!Vy{o$`SV?4(pf$ z(iUfSG^F(mwGJ>v6TVw@^?$q?qBtS`W1}x>^!l4b%jq?QLSkdlU*s~v;KqF~ac`vo z9pD6FXMg+erZDqYQNCQhl}O-sNrIinv8pI;Vm^rj*DD8<8y#O8Qu zmbJ5a%|MvuaIimeTk_wZH`F^$R|pO|i+a$@nqgjiJKL=371EW&b|TPq5@|pykO9%= z1Z7W;^pw63-^}xpx_hvf>h?H9OZD`O1vr>mvL!ZPuwuYJI(kngE7aY>qBEY%^Cx_YPD+A2j z7s~ec2Xw}NE~kAL!FGDdxQcot?JDB{1tKmYOi4z9FxN@jhCcaR==2#F49dP2){n9I ztRUq;yii~ss-wuTL=<_Vvpirk|P`!je`%8Y(dpe znmbxWQb7|%u@)8L{@nQWdxfJ3caMLM?tAMgS>$eWVSt&6Oge_WkwCZiuhzA=G3GNL zv&qLhV>vJMok7~idFrFd2kvl^L-mOZW`Cp0dkRIoy2>KRDs?jXv-Q%8^4XTqv(v0N zCG_VRuI|L<(AI1zOJ!oBct}*nJA65=cJt`Jwv!U#+~tK zrzTYt_zryX2(TkTO-IiABn8VR41!>{9?O5Ac==%gq025O2Ga0ga8qDXV~G zuJVuKFRqI_`4(;AQx9S&O;}?A3e@5oT=a1sUBK*Ynp707@!d+_XuS%l?|%RN0?(}} zlZuF*NBpOk@qG?t^epNuSH~Tw_2Guga`08GHO;uwFn%+xsW_77RlFhPqj0qo6wmj= zpGtWlGG^g0+x+uRrbi3Q{@Wz#Dl_g;@eukipy28!glPqi&wcG)N+k)8X^M(KMG4 zunmEeZ-7$PPcF@{Lohe<33!d1BVa|PQ1U8K5R`&3$b~im-pTgXH78hN$BzQlOilC6 z;NTy2jUBmt`t8wSdMAm(?$Q;TJ-FHJAR@s`M}cBMww|7pmYfX)@s!)2EjJkjzPlyo zLmdceQ}p(hdXAo1S`RI{yzT>6^qpQKe z$*ZMW(eG3btS+3FMq272a&<(c6Yg}!pB|R4f;y-CpY8>b$%J7^bOxmEaa{MY7-b(Q zf@GV%Q}+WKjteDr>Uv;#-qOc*T9(4?&%>4&%ZLT7s@%-^iVg zm4VK%-asSD){of=HquBG5V7Bx6+QN!q*}%)4MD%5&M*jx0rM0nIf4k@OAG0Oa7H&>2kV+s(ev5YH0+(njY9o@Qu=!1)Sw<= zR6i0;+@nG%N*+PM0*Be&9FK`>8$+6wxWh2g%W)Yd+S<_cXKOHQoN&>7o{l&qgyr<-A zPL@(8HDd&)upGS(%0xF`@An^WC7P7zm%i9?#Btz`U0ogs8&&nC^hr2PjO-DXD5&%I zyZqPDbFJ?>%C~Ca5sHgAg5rrdbxFWt+(@p4f2S=6_T(?R~#j zOM~Nqt!Kn7Igb(3vxCx~VA~GZ6?Sx}!h~K&F-uv*T9xWbDxEo2r&|A2O;AH8S%HOx z&0T7`8^LN43#2jhosBhen^i3;<2mUb$sn}qqUy%J5L(pfB%E}ph6MmM{J z7&`rITN9Xmv-cqSMfP|PEr;&RLd2slulV<=5Ic;nF@lJfGK&MU-eBn7JefJt<$Lsn z?9LD*cG7OD_UE;<&0pboCMb

Ap^y&bmSMNJSYOXrydf$x0rnWoG%}(S)YINx{WS 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 delta 2433 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 z Date: Mon, 17 Jul 2023 10:36:08 -0600 Subject: [PATCH 006/169] update ref --- crypto_plugins/flutter_libmonero | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crypto_plugins/flutter_libmonero b/crypto_plugins/flutter_libmonero index 782c8d00c..c206a1434 160000 --- a/crypto_plugins/flutter_libmonero +++ b/crypto_plugins/flutter_libmonero @@ -1 +1 @@ -Subproject commit 782c8d00c10d9dde906c1da982b1be7d45767ae7 +Subproject commit c206a1434f138a20c38b7abf8d94b9dd4d14b57e From bc85b97f5fb9cc5a2c3b3c0498df5a0ff2f9a1e6 Mon Sep 17 00:00:00 2001 From: julian-CStack Date: Mon, 17 Jul 2023 10:36:33 -0600 Subject: [PATCH 007/169] null error fix on isar exchange data cache instance --- lib/services/exchange/exchange_data_loading_service.dart | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lib/services/exchange/exchange_data_loading_service.dart b/lib/services/exchange/exchange_data_loading_service.dart index 6fefba289..ee0ed50a0 100644 --- a/lib/services/exchange/exchange_data_loading_service.dart +++ b/lib/services/exchange/exchange_data_loading_service.dart @@ -135,6 +135,9 @@ class ExchangeDataLoadingService { Future loadAll() async { if (!_locked) { _locked = true; + if (_isar == null) { + await initDB(); + } Logging.instance.log( "ExchangeDataLoadingService.loadAll starting...", level: LogLevel.Info, From 27c2f34a33e039446f71b3f3260ae43907dcfd18 Mon Sep 17 00:00:00 2001 From: julian-CStack Date: Mon, 17 Jul 2023 10:36:45 -0600 Subject: [PATCH 008/169] macos specific notifications fix --- lib/services/notifications_api.dart | 10 ++++++++-- lib/utilities/stack_file_system.dart | 4 ++-- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/lib/services/notifications_api.dart b/lib/services/notifications_api.dart index e1c42fabe..41e95387b 100644 --- a/lib/services/notifications_api.dart +++ b/lib/services/notifications_api.dart @@ -26,6 +26,7 @@ class NotificationApi { priority: Priority.high, ticker: 'ticker'), iOS: IOSNotificationDetails(), + macOS: MacOSNotificationDetails(), ); } @@ -34,8 +35,13 @@ class NotificationApi { const iOS = IOSInitializationSettings(); const linux = LinuxInitializationSettings( defaultActionName: "temporary_stack_wallet"); - const settings = - InitializationSettings(android: android, iOS: iOS, linux: linux); + const macOS = MacOSInitializationSettings(); + const settings = InitializationSettings( + android: android, + iOS: iOS, + linux: linux, + macOS: macOS, + ); await _notifications.initialize( settings, onSelectNotification: (payload) async { diff --git a/lib/utilities/stack_file_system.dart b/lib/utilities/stack_file_system.dart index 843bb1b71..f36fadf01 100644 --- a/lib/utilities/stack_file_system.dart +++ b/lib/utilities/stack_file_system.dart @@ -27,8 +27,8 @@ abstract class StackFileSystem { } else if (Platform.isWindows) { appDirectory = await getApplicationSupportDirectory(); } else if (Platform.isMacOS) { - // currently run in ipad mode?? - throw Exception("Unsupported platform"); + appDirectory = await getLibraryDirectory(); + appDirectory = Directory("${appDirectory.path}/stackwallet"); } else if (Platform.isIOS) { // todo: check if we need different behaviour here if (Util.isDesktop) { From d966964cf85fa0127a4f96919295c2d03f4dabc8 Mon Sep 17 00:00:00 2001 From: julian-CStack Date: Mon, 17 Jul 2023 10:40:43 -0600 Subject: [PATCH 009/169] macos app network entitlements --- macos/Runner/DebugProfile.entitlements | 2 ++ macos/Runner/Release.entitlements | 2 ++ 2 files changed, 4 insertions(+) diff --git a/macos/Runner/DebugProfile.entitlements b/macos/Runner/DebugProfile.entitlements index dddb8a30c..c946719a1 100644 --- a/macos/Runner/DebugProfile.entitlements +++ b/macos/Runner/DebugProfile.entitlements @@ -8,5 +8,7 @@ com.apple.security.network.server + com.apple.security.network.client + diff --git a/macos/Runner/Release.entitlements b/macos/Runner/Release.entitlements index 852fa1a47..48271acc9 100644 --- a/macos/Runner/Release.entitlements +++ b/macos/Runner/Release.entitlements @@ -4,5 +4,7 @@ com.apple.security.app-sandbox + com.apple.security.network.client + From faf88542f24601bcc309907d1ae0e1b41c52d54b Mon Sep 17 00:00:00 2001 From: julian-CStack Date: Mon, 17 Jul 2023 14:21:28 -0600 Subject: [PATCH 010/169] macos app name --- macos/Runner.xcodeproj/project.pbxproj | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/macos/Runner.xcodeproj/project.pbxproj b/macos/Runner.xcodeproj/project.pbxproj index 7b723f8a8..21cf855f6 100644 --- a/macos/Runner.xcodeproj/project.pbxproj +++ b/macos/Runner.xcodeproj/project.pbxproj @@ -69,7 +69,7 @@ 331C80D7294CF71000263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; 333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = ""; }; 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = ""; }; - 33CC10ED2044A3C60003C045 /* stack_wallet.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = stack_wallet.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 33CC10ED2044A3C60003C045 /* Stack Wallet.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Stack Wallet.app"; sourceTree = BUILT_PRODUCTS_DIR; }; 33CC10F02044A3C60003C045 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 33CC10F22044A3C60003C045 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = Runner/Assets.xcassets; sourceTree = ""; }; 33CC10F52044A3C60003C045 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = ""; }; @@ -144,7 +144,7 @@ 33CC10EE2044A3C60003C045 /* Products */ = { isa = PBXGroup; children = ( - 33CC10ED2044A3C60003C045 /* stack_wallet.app */, + 33CC10ED2044A3C60003C045 /* Stack Wallet.app */, 331C80D5294CF71000263BE5 /* RunnerTests.xctest */, ); name = Products; @@ -204,7 +204,6 @@ 174539D042E7AC2AB25A83EB /* Pods-RunnerTests.release.xcconfig */, 27CB73AACA5743180CC6CD50 /* Pods-RunnerTests.profile.xcconfig */, ); - name = Pods; path = Pods; sourceTree = ""; }; @@ -249,7 +248,7 @@ ); name = Runner; productName = Runner; - productReference = 33CC10ED2044A3C60003C045 /* stack_wallet.app */; + productReference = 33CC10ED2044A3C60003C045 /* Stack Wallet.app */; productType = "com.apple.product-type.application"; }; /* End PBXNativeTarget section */ @@ -481,7 +480,7 @@ PRODUCT_BUNDLE_IDENTIFIER = com.cypherstack.stackWallet.RunnerTests; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_VERSION = 5.0; - TEST_HOST = "$(BUILT_PRODUCTS_DIR)/stack_wallet.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/stack_wallet"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Stack Wallet.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/stack_wallet"; }; name = Debug; }; @@ -496,7 +495,7 @@ PRODUCT_BUNDLE_IDENTIFIER = com.cypherstack.stackWallet.RunnerTests; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_VERSION = 5.0; - TEST_HOST = "$(BUILT_PRODUCTS_DIR)/stack_wallet.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/stack_wallet"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Stack Wallet.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/stack_wallet"; }; name = Release; }; @@ -511,7 +510,7 @@ PRODUCT_BUNDLE_IDENTIFIER = com.cypherstack.stackWallet.RunnerTests; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_VERSION = 5.0; - TEST_HOST = "$(BUILT_PRODUCTS_DIR)/stack_wallet.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/stack_wallet"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Stack Wallet.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/stack_wallet"; }; name = Profile; }; @@ -556,6 +555,7 @@ MACOSX_DEPLOYMENT_TARGET = 10.14; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = macosx; + STRIP_INSTALLED_PRODUCT = NO; SWIFT_COMPILATION_MODE = wholemodule; SWIFT_OPTIMIZATION_LEVEL = "-O"; }; @@ -636,6 +636,7 @@ MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = macosx; + STRIP_INSTALLED_PRODUCT = NO; SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; }; @@ -682,6 +683,7 @@ MACOSX_DEPLOYMENT_TARGET = 10.14; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = macosx; + STRIP_INSTALLED_PRODUCT = NO; SWIFT_COMPILATION_MODE = wholemodule; SWIFT_OPTIMIZATION_LEVEL = "-O"; }; From 58c97de86c03f60c887a287f086db78ede971cf8 Mon Sep 17 00:00:00 2001 From: julian Date: Tue, 18 Jul 2023 10:15:05 -0600 Subject: [PATCH 011/169] WIP ordinal ui --- dockerfile.linux | 17 -- lib/models/ordinal.dart | 13 ++ lib/pages/ordinals/ordinal_details_view.dart | 116 +++++++++++ lib/pages/ordinals/ordinals_list.dart | 36 ++++ lib/pages/ordinals/ordinals_view.dart | 197 ++++++++++++++++++ lib/pages/ordinals/widgets/ordinal_card.dart | 55 +++++ lib/pages/wallet_view/wallet_view.dart | 17 ++ lib/route_generator.dart | 32 +++ .../coins/litecoin/litecoin_wallet.dart | 8 +- lib/services/coins/manager.dart | 3 + lib/services/mixins/ordinals_interface.dart | 3 + lib/services/ordinals_api.dart | 9 + lib/utilities/text_styles.dart | 22 ++ 13 files changed, 510 insertions(+), 18 deletions(-) delete mode 100644 dockerfile.linux create mode 100644 lib/models/ordinal.dart create mode 100644 lib/pages/ordinals/ordinal_details_view.dart create mode 100644 lib/pages/ordinals/ordinals_list.dart create mode 100644 lib/pages/ordinals/ordinals_view.dart create mode 100644 lib/pages/ordinals/widgets/ordinal_card.dart create mode 100644 lib/services/mixins/ordinals_interface.dart create mode 100644 lib/services/ordinals_api.dart diff --git a/dockerfile.linux b/dockerfile.linux deleted file mode 100644 index 4a3867008..000000000 --- a/dockerfile.linux +++ /dev/null @@ -1,17 +0,0 @@ -FROM ubuntu:20.04 as base -COPY . /stack_wallet -WORKDIR /stack_wallet/scripts/linux -RUN apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install --no-install-recommends -y git=1:2.25.1-1ubuntu3.6 make=4.2.1-1.2 curl=7.68.0-1ubuntu2.14 cargo=0.62.0ubuntu0libgit2-0ubuntu0.20.04.1 \ - file=1:5.38-4 ca-certificates=20211016ubuntu0.20.04.1 cmake=3.16.3-1ubuntu1.20.04.1 cmake-data=3.16.3-1ubuntu1.20.04.1 g++=4:9.3.0-1ubuntu2 libgmp-dev=2:6.2.0+dfsg-4ubuntu0.1 libssl-dev=1.1.1f-1ubuntu2.16 \ - libclang-dev=1:10.0-50~exp1 unzip=6.0-25ubuntu1.1 python3=3.8.2-0ubuntu2 pkg-config=0.29.1-0ubuntu4 libglib2.0-dev=2.64.6-1~ubuntu20.04.4 libgcrypt20-dev=1.8.5-5ubuntu1.1 gettext-base=0.19.8.1-10build1 \ - libgirepository1.0-dev=1.64.1-1~ubuntu20.04.1 valac=0.48.6-0ubuntu1 xsltproc=1.1.34-4ubuntu0.20.04.1 docbook-xsl=1.79.1+dfsg-2 python3-pip=20.0.2-5ubuntu1.6 ninja-build=1.10.0-1build1 clang=1:10.0-50~exp1 \ - libgtk-3-dev=3.24.20-0ubuntu1.1 libunbound-dev=1.9.4-2ubuntu1.4 libzmq3-dev=4.3.2-2ubuntu1 libtool=2.4.6-14 autoconf=2.69-11.1 automake=1:1.16.1-4ubuntu6 bison=2:3.5.1+dfsg-1 \ - && apt-get clean \ - && rm -rf /var/lib/apt/lists/* \ - && pip3 install --upgrade meson==0.64.1 markdown==3.4.1 markupsafe==2.1.1 jinja2==3.1.2 pygments==2.13.0 toml==0.10.2 typogrify==2.0.7 tomli==2.0.1 && cd .. && ./prebuild.sh && cd linux && ./build_all.sh -WORKDIR / -RUN git clone https://github.com/flutter/flutter.git -b 3.3.4 -ENV PATH "$PATH:/flutter/bin" -WORKDIR /stack_wallet -RUN flutter pub get Linux && flutter build linux -ENTRYPOINT ["/bin/bash"] diff --git a/lib/models/ordinal.dart b/lib/models/ordinal.dart new file mode 100644 index 000000000..9c4e15392 --- /dev/null +++ b/lib/models/ordinal.dart @@ -0,0 +1,13 @@ +class Ordinal { + final String name; + final String inscription; + final String rank; + + // TODO: make a proper class instead of this placeholder + + Ordinal({ + required this.name, + required this.inscription, + required this.rank, + }); +} diff --git a/lib/pages/ordinals/ordinal_details_view.dart b/lib/pages/ordinals/ordinal_details_view.dart new file mode 100644 index 000000000..c39679903 --- /dev/null +++ b/lib/pages/ordinals/ordinal_details_view.dart @@ -0,0 +1,116 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:stackwallet/models/ordinal.dart'; +import 'package:stackwallet/themes/stack_colors.dart'; +import 'package:stackwallet/utilities/assets.dart'; +import 'package:stackwallet/utilities/text_styles.dart'; +import 'package:stackwallet/widgets/background.dart'; +import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; +import 'package:stackwallet/widgets/desktop/primary_button.dart'; +import 'package:stackwallet/widgets/desktop/secondary_button.dart'; + +class OrdinalDetailsView extends StatefulWidget { + const OrdinalDetailsView({ + super.key, + required this.walletId, + required this.ordinal, + }); + + final String walletId; + final Ordinal ordinal; + + static const routeName = "/ordinalDetailsView"; + + @override + State createState() => _OrdinalDetailsViewState(); +} + +class _OrdinalDetailsViewState extends State { + @override + Widget build(BuildContext context) { + return Background( + child: SafeArea( + child: Scaffold( + backgroundColor: + Theme.of(context).extension()!.background, + appBar: AppBar( + backgroundColor: + Theme.of(context).extension()!.background, + leading: const AppBarBackButton(), + title: Text( + "Ordinal details", + style: STextStyles.navBarTitle(context), + ), + ), + body: Column(), + ), + ), + ); + } +} + +class _OrdinalImageGroup extends StatelessWidget { + const _OrdinalImageGroup({ + super.key, + required this.ordinal, + }); + + final Ordinal ordinal; + + static const _spacing = 12.0; + + @override + Widget build(BuildContext context) { + return Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text( + ordinal.name, + style: STextStyles.w600_16(context), + ), + const SizedBox( + height: _spacing, + ), + AspectRatio( + aspectRatio: 1, + child: Container( + color: Colors.red, + ), + ), + const SizedBox( + height: _spacing, + ), + Row( + children: [ + Expanded( + child: SecondaryButton( + label: "Download", + icon: SvgPicture.asset(Assets.svg.arrowDown), + buttonHeight: ButtonHeight.s, + onPressed: () { + // TODO: save and download image to device + }, + ), + ), + const SizedBox( + width: _spacing, + ), + Expanded( + child: PrimaryButton( + label: "Send", + icon: SvgPicture.asset( + Assets.svg.star, + ), + buttonHeight: ButtonHeight.s, + onPressed: () { + // TODO: try send + }, + ), + ), + ], + ), + ], + ); + } +} diff --git a/lib/pages/ordinals/ordinals_list.dart b/lib/pages/ordinals/ordinals_list.dart new file mode 100644 index 000000000..d3b3cd83e --- /dev/null +++ b/lib/pages/ordinals/ordinals_list.dart @@ -0,0 +1,36 @@ +import 'package:flutter/material.dart'; +import 'package:stackwallet/models/ordinal.dart'; +import 'package:stackwallet/pages/ordinals/widgets/ordinal_card.dart'; + +class OrdinalsList extends StatefulWidget { + const OrdinalsList({ + super.key, + required this.ordinals, + }); + + final List ordinals; + + @override + State createState() => _OrdinalsListState(); +} + +class _OrdinalsListState extends State { + static const spacing = 10.0; + + @override + Widget build(BuildContext context) { + return GridView.builder( + shrinkWrap: true, + itemCount: widget.ordinals.length, + gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( + crossAxisSpacing: spacing, + mainAxisSpacing: spacing, + crossAxisCount: 2, + childAspectRatio: 3 / 4, + ), + itemBuilder: (_, i) => OrdinalCard( + ordinal: widget.ordinals[i], + ), + ); + } +} diff --git a/lib/pages/ordinals/ordinals_view.dart b/lib/pages/ordinals/ordinals_view.dart new file mode 100644 index 000000000..2bfa2e9f3 --- /dev/null +++ b/lib/pages/ordinals/ordinals_view.dart @@ -0,0 +1,197 @@ +/* + * This file is part of Stack Wallet. + * + * Copyright (c) 2023 Cypher Stack + * All Rights Reserved. + * The code is distributed under GPLv3 license, see LICENSE file for details. + * Generated by Cypher Stack on 2023-05-26 + * + */ + +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_svg/svg.dart'; +import 'package:stackwallet/models/ordinal.dart'; +import 'package:stackwallet/pages/ordinals/ordinals_list.dart'; +import 'package:stackwallet/themes/stack_colors.dart'; +import 'package:stackwallet/utilities/assets.dart'; +import 'package:stackwallet/utilities/constants.dart'; +import 'package:stackwallet/utilities/text_styles.dart'; +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/icon_widgets/x_icon.dart'; +import 'package:stackwallet/widgets/stack_text_field.dart'; +import 'package:stackwallet/widgets/textfield_icon_button.dart'; + +class OrdinalsView extends ConsumerStatefulWidget { + const OrdinalsView({ + super.key, + required this.walletId, + }); + + static const routeName = "/ordinalsView"; + + final String walletId; + + @override + ConsumerState createState() => _OrdinalsViewState(); +} + +class _OrdinalsViewState extends ConsumerState { + late final TextEditingController searchController; + late final FocusNode searchFocus; + + String _searchTerm = ""; + + @override + void initState() { + searchController = TextEditingController(); + searchFocus = FocusNode(); + super.initState(); + } + + @override + void dispose() { + searchController.dispose(); + searchFocus.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Background( + child: SafeArea( + child: Scaffold( + backgroundColor: + Theme.of(context).extension()!.background, + appBar: AppBar( + automaticallyImplyLeading: false, + leading: const AppBarBackButton(), + title: Text( + "Ordinals", + style: STextStyles.navBarTitle(context), + ), + titleSpacing: 0, + actions: [ + AspectRatio( + aspectRatio: 1, + child: AppBarIconButton( + size: 36, + icon: SvgPicture.asset( + Assets.svg.arrowRotate, + width: 20, + height: 20, + color: Theme.of(context) + .extension()! + .topNavIconPrimary, + ), + onPressed: () { + // todo refresh + }, + ), + ), + AspectRatio( + aspectRatio: 1, + child: AppBarIconButton( + size: 36, + icon: SvgPicture.asset( + Assets.svg.filter, + width: 20, + height: 20, + color: Theme.of(context) + .extension()! + .topNavIconPrimary, + ), + onPressed: () { + // todo filter view + }, + ), + ), + ], + ), + body: Padding( + padding: const EdgeInsets.only( + left: 16, + right: 16, + top: 8, + ), + child: Column( + children: [ + ClipRRect( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + child: TextField( + autocorrect: Util.isDesktop ? false : true, + enableSuggestions: Util.isDesktop ? false : true, + controller: searchController, + focusNode: searchFocus, + onChanged: (value) { + setState(() { + _searchTerm = value; + }); + }, + style: STextStyles.field(context), + decoration: standardInputDecoration( + "Search", + searchFocus, + context, + ).copyWith( + prefixIcon: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 10, + vertical: 16, + ), + child: SvgPicture.asset( + Assets.svg.search, + width: 16, + height: 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 = ""; + _searchTerm = ""; + }); + }, + ), + ], + ), + ), + ) + : null, + ), + ), + ), + const SizedBox( + height: 16, + ), + Expanded( + child: OrdinalsList( + ordinals: [ + for (int i = 0; i < 13; i++) + Ordinal( + name: "dummy name $i", + inscription: "insc$i", + rank: "r$i", + ), + ], + ), + ), + ], + ), + ), + ), + ), + ); + } +} diff --git a/lib/pages/ordinals/widgets/ordinal_card.dart b/lib/pages/ordinals/widgets/ordinal_card.dart new file mode 100644 index 000000000..2af610241 --- /dev/null +++ b/lib/pages/ordinals/widgets/ordinal_card.dart @@ -0,0 +1,55 @@ +import 'package:flutter/material.dart'; +import 'package:stackwallet/models/ordinal.dart'; +import 'package:stackwallet/pages/ordinals/ordinal_details_view.dart'; +import 'package:stackwallet/utilities/text_styles.dart'; +import 'package:stackwallet/widgets/rounded_white_container.dart'; + +class OrdinalCard extends StatelessWidget { + const OrdinalCard({ + super.key, + required this.walletId, + required this.ordinal, + }); + + final String walletId; + final Ordinal ordinal; + + @override + Widget build(BuildContext context) { + return RoundedWhiteContainer( + radiusMultiplier: 2, + onPressed: () { + Navigator.of(context).pushNamed( + OrdinalDetailsView.routeName, + arguments: widget.walletId, + ); + }, + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + AspectRatio( + aspectRatio: 1, + child: Container( + color: Colors.red, + child: const Center( + child: Text( + "replace red container with image", + ), + ), + ), + ), + const Spacer(), + Text( + ordinal.name, + style: STextStyles.w500_12(context), + ), + const Spacer(), + Text( + "INSC. ${ordinal.inscription} RANK ${ordinal.rank}", + style: STextStyles.w500_8(context), + ), + ], + ), + ); + } +} diff --git a/lib/pages/wallet_view/wallet_view.dart b/lib/pages/wallet_view/wallet_view.dart index 3e7fb37fa..278d43d35 100644 --- a/lib/pages/wallet_view/wallet_view.dart +++ b/lib/pages/wallet_view/wallet_view.dart @@ -23,6 +23,7 @@ import 'package:stackwallet/pages/coin_control/coin_control_view.dart'; import 'package:stackwallet/pages/exchange_view/wallet_initiated_exchange_view.dart'; import 'package:stackwallet/pages/home_view/home_view.dart'; import 'package:stackwallet/pages/notification_views/notifications_view.dart'; +import 'package:stackwallet/pages/ordinals/ordinals_view.dart'; import 'package:stackwallet/pages/paynym/paynym_claim_view.dart'; import 'package:stackwallet/pages/paynym/paynym_home_view.dart'; import 'package:stackwallet/pages/receive_view/receive_view.dart'; @@ -1007,6 +1008,22 @@ class _WalletViewState extends ConsumerState { } }, ), + if (ref.watch( + walletsChangeNotifierProvider.select( + (value) => + value.getManager(widget.walletId).hasOrdinalsSupport, + ), + )) + WalletNavigationBarItemData( + label: "Ordinals", + icon: const CoinControlNavIcon(), + onTap: () { + Navigator.of(context).pushNamed( + OrdinalsView.routeName, + arguments: widget.walletId, + ); + }, + ), ], ), ], diff --git a/lib/route_generator.dart b/lib/route_generator.dart index 7ccc378c9..cf82dce08 100644 --- a/lib/route_generator.dart +++ b/lib/route_generator.dart @@ -18,6 +18,7 @@ import 'package:stackwallet/models/buy/response_objects/quote.dart'; import 'package:stackwallet/models/exchange/incomplete_exchange.dart'; import 'package:stackwallet/models/exchange/response_objects/trade.dart'; import 'package:stackwallet/models/isar/models/isar_models.dart'; +import 'package:stackwallet/models/ordinal.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'; @@ -57,6 +58,8 @@ import 'package:stackwallet/pages/home_view/home_view.dart'; import 'package:stackwallet/pages/intro_view.dart'; import 'package:stackwallet/pages/manage_favorites_view/manage_favorites_view.dart'; import 'package:stackwallet/pages/notification_views/notifications_view.dart'; +import 'package:stackwallet/pages/ordinals/ordinal_details_view.dart'; +import 'package:stackwallet/pages/ordinals/ordinals_view.dart'; import 'package:stackwallet/pages/paynym/add_new_paynym_follow_view.dart'; import 'package:stackwallet/pages/paynym/paynym_claim_view.dart'; import 'package:stackwallet/pages/paynym/paynym_home_view.dart'; @@ -404,6 +407,35 @@ class RouteGenerator { } return _routeError("${settings.name} invalid args: ${args.toString()}"); + case OrdinalsView.routeName: + if (args is String) { + return getRoute( + shouldUseMaterialRoute: useMaterialPageRoute, + builder: (_) => OrdinalsView( + walletId: args, + ), + settings: RouteSettings( + name: settings.name, + ), + ); + } + return _routeError("${settings.name} invalid args: ${args.toString()}"); + + case OrdinalDetailsView.routeName: + if (args is ({Ordinal ordinal, String walletId})) { + return getRoute( + shouldUseMaterialRoute: useMaterialPageRoute, + builder: (_) => OrdinalDetailsView( + walletId: args.walletId, + ordinal: args.ordinal, + ), + settings: RouteSettings( + name: settings.name, + ), + ); + } + return _routeError("${settings.name} invalid args: ${args.toString()}"); + case UtxoDetailsView.routeName: if (args is Tuple2) { return getRoute( diff --git a/lib/services/coins/litecoin/litecoin_wallet.dart b/lib/services/coins/litecoin/litecoin_wallet.dart index b84c6c6a0..2edc32fc6 100644 --- a/lib/services/coins/litecoin/litecoin_wallet.dart +++ b/lib/services/coins/litecoin/litecoin_wallet.dart @@ -36,6 +36,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/mixins/coin_control_interface.dart'; import 'package:stackwallet/services/mixins/electrum_x_parsing.dart'; +import 'package:stackwallet/services/mixins/ordinals_interface.dart'; import 'package:stackwallet/services/mixins/wallet_cache.dart'; import 'package:stackwallet/services/mixins/wallet_db.dart'; import 'package:stackwallet/services/mixins/xpubable.dart'; @@ -109,7 +110,12 @@ String constructDerivePath({ } class LitecoinWallet extends CoinServiceAPI - with WalletCache, WalletDB, ElectrumXParsing, CoinControlInterface + with + WalletCache, + WalletDB, + ElectrumXParsing, + CoinControlInterface, + OrdinalsInterface implements XPubAble { LitecoinWallet({ required String walletId, diff --git a/lib/services/coins/manager.dart b/lib/services/coins/manager.dart index 4e3dd460e..1f5ae55f3 100644 --- a/lib/services/coins/manager.dart +++ b/lib/services/coins/manager.dart @@ -21,6 +21,7 @@ import 'package:stackwallet/services/event_bus/events/global/node_connection_sta import 'package:stackwallet/services/event_bus/events/global/updated_in_background_event.dart'; import 'package:stackwallet/services/event_bus/global_event_bus.dart'; import 'package:stackwallet/services/mixins/coin_control_interface.dart'; +import 'package:stackwallet/services/mixins/ordinals_interface.dart'; import 'package:stackwallet/services/mixins/paynym_wallet_interface.dart'; import 'package:stackwallet/services/mixins/xpubable.dart'; import 'package:stackwallet/utilities/amount/amount.dart'; @@ -244,6 +245,8 @@ class Manager with ChangeNotifier { bool get hasCoinControlSupport => _currentWallet is CoinControlInterface; + bool get hasOrdinalsSupport => _currentWallet is OrdinalsInterface; + bool get hasTokenSupport => _currentWallet.coin == Coin.ethereum; bool get hasWhirlpoolSupport => false; diff --git a/lib/services/mixins/ordinals_interface.dart b/lib/services/mixins/ordinals_interface.dart new file mode 100644 index 000000000..06f2377e9 --- /dev/null +++ b/lib/services/mixins/ordinals_interface.dart @@ -0,0 +1,3 @@ +mixin OrdinalsInterface { + // TODO wallet ordinals functionality +} diff --git a/lib/services/ordinals_api.dart b/lib/services/ordinals_api.dart new file mode 100644 index 000000000..97cc45834 --- /dev/null +++ b/lib/services/ordinals_api.dart @@ -0,0 +1,9 @@ +import 'package:stackwallet/models/ordinal.dart'; + +class OrdinalsAPI { + // dummy class with sample functions to be changed / filled out + + static Future> fetch() async { + return []; + } +} diff --git a/lib/utilities/text_styles.dart b/lib/utilities/text_styles.dart index 4a1ced812..fab828bef 100644 --- a/lib/utilities/text_styles.dart +++ b/lib/utilities/text_styles.dart @@ -306,6 +306,17 @@ class STextStyles { } } + static TextStyle w600_16(BuildContext context) { + switch (_theme(context).themeId) { + default: + return GoogleFonts.inter( + color: _theme(context).textDark, + fontWeight: FontWeight.w600, + fontSize: 16, + ); + } + } + static TextStyle w500_14(BuildContext context) { switch (_theme(context).themeId) { default: @@ -339,6 +350,17 @@ class STextStyles { } } + static TextStyle w500_8(BuildContext context) { + switch (_theme(context).themeId) { + default: + return GoogleFonts.inter( + color: _theme(context).textSubtitle1, + fontWeight: FontWeight.w500, + fontSize: 8, + ); + } + } + static TextStyle w600_20(BuildContext context) { switch (_theme(context).themeId) { default: From 79670c5d478472610762c664b2403e259c247b3e Mon Sep 17 00:00:00 2001 From: julian Date: Tue, 18 Jul 2023 11:03:57 -0600 Subject: [PATCH 012/169] WIP ordinal details view --- assets/svg/send.svg | 5 + lib/pages/ordinals/ordinal_details_view.dart | 148 ++++++++++++++++++- lib/pages/ordinals/ordinals_filter_view.dart | 0 lib/pages/ordinals/ordinals_list.dart | 3 + lib/pages/ordinals/ordinals_view.dart | 1 + lib/pages/ordinals/widgets/ordinal_card.dart | 2 +- lib/route_generator.dart | 3 + lib/utilities/assets.dart | 1 + pubspec.yaml | 1 + 9 files changed, 158 insertions(+), 6 deletions(-) create mode 100644 assets/svg/send.svg create mode 100644 lib/pages/ordinals/ordinals_filter_view.dart diff --git a/assets/svg/send.svg b/assets/svg/send.svg new file mode 100644 index 000000000..61fe2a206 --- /dev/null +++ b/assets/svg/send.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/lib/pages/ordinals/ordinal_details_view.dart b/lib/pages/ordinals/ordinal_details_view.dart index c39679903..a79847af7 100644 --- a/lib/pages/ordinals/ordinal_details_view.dart +++ b/lib/pages/ordinals/ordinal_details_view.dart @@ -1,6 +1,10 @@ +import 'dart:async'; + import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:stackwallet/models/ordinal.dart'; +import 'package:stackwallet/notifications/show_flush_bar.dart'; import 'package:stackwallet/themes/stack_colors.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/text_styles.dart'; @@ -8,6 +12,7 @@ import 'package:stackwallet/widgets/background.dart'; import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; import 'package:stackwallet/widgets/desktop/primary_button.dart'; import 'package:stackwallet/widgets/desktop/secondary_button.dart'; +import 'package:stackwallet/widgets/rounded_white_container.dart'; class OrdinalDetailsView extends StatefulWidget { const OrdinalDetailsView({ @@ -26,6 +31,8 @@ class OrdinalDetailsView extends StatefulWidget { } class _OrdinalDetailsViewState extends State { + static const _spacing = 12.0; + @override Widget build(BuildContext context) { return Background( @@ -42,19 +49,136 @@ class _OrdinalDetailsViewState extends State { style: STextStyles.navBarTitle(context), ), ), - body: Column(), + body: SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Column( + children: [ + Padding( + padding: const EdgeInsets.symmetric( + vertical: 12, + horizontal: 39, + ), + child: _OrdinalImageGroup( + ordinal: widget.ordinal, + walletId: widget.walletId, + ), + ), + _DetailsItemWCopy( + title: "Inscription number", + data: widget.ordinal.inscription, + ), + const SizedBox( + height: _spacing, + ), + _DetailsItemWCopy( + title: "Rank", + data: widget.ordinal.rank, + ), + const SizedBox( + height: _spacing, + ), + // todo: add utxo status + const SizedBox( + height: _spacing, + ), + _DetailsItemWCopy( + title: "Amount", + data: "FIXME", + ), + const SizedBox( + height: _spacing, + ), + _DetailsItemWCopy( + title: "Owner address", + data: "FIXME", + ), + const SizedBox( + height: _spacing, + ), + _DetailsItemWCopy( + title: "Transaction ID", + data: "FIXME", + ), + const SizedBox( + height: _spacing, + ), + ], + ), + ), + ), ), ), ); } } +class _DetailsItemWCopy extends StatelessWidget { + const _DetailsItemWCopy({ + super.key, + required this.title, + required this.data, + }); + + final String title; + final String data; + + @override + Widget build(BuildContext context) { + return RoundedWhiteContainer( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + title, + style: STextStyles.itemSubtitle(context), + ), + GestureDetector( + onTap: () async { + await Clipboard.setData(ClipboardData(text: data)); + if (context.mounted) { + unawaited( + showFloatingFlushBar( + type: FlushBarType.info, + message: "Copied to clipboard", + context: context, + ), + ); + } + }, + child: SvgPicture.asset( + Assets.svg.copy, + color: + Theme.of(context).extension()!.infoItemIcons, + width: 12, + ), + ), + ], + ), + const SizedBox( + height: 4, + ), + SelectableText( + data, + style: STextStyles.itemSubtitle12(context), + ), + ], + ), + ); + } +} + class _OrdinalImageGroup extends StatelessWidget { const _OrdinalImageGroup({ super.key, + required this.walletId, required this.ordinal, }); + final String walletId; final Ordinal ordinal; static const _spacing = 12.0; @@ -86,8 +210,16 @@ class _OrdinalImageGroup extends StatelessWidget { Expanded( child: SecondaryButton( label: "Download", - icon: SvgPicture.asset(Assets.svg.arrowDown), - buttonHeight: ButtonHeight.s, + icon: SvgPicture.asset( + Assets.svg.arrowDown, + width: 10, + height: 12, + color: Theme.of(context) + .extension()! + .buttonTextSecondary, + ), + buttonHeight: ButtonHeight.l, + iconSpacing: 4, onPressed: () { // TODO: save and download image to device }, @@ -100,9 +232,15 @@ class _OrdinalImageGroup extends StatelessWidget { child: PrimaryButton( label: "Send", icon: SvgPicture.asset( - Assets.svg.star, + Assets.svg.send, + width: 10, + height: 10, + color: Theme.of(context) + .extension()! + .buttonTextPrimary, ), - buttonHeight: ButtonHeight.s, + buttonHeight: ButtonHeight.l, + iconSpacing: 4, onPressed: () { // TODO: try send }, diff --git a/lib/pages/ordinals/ordinals_filter_view.dart b/lib/pages/ordinals/ordinals_filter_view.dart new file mode 100644 index 000000000..e69de29bb diff --git a/lib/pages/ordinals/ordinals_list.dart b/lib/pages/ordinals/ordinals_list.dart index d3b3cd83e..ca7c71446 100644 --- a/lib/pages/ordinals/ordinals_list.dart +++ b/lib/pages/ordinals/ordinals_list.dart @@ -5,9 +5,11 @@ import 'package:stackwallet/pages/ordinals/widgets/ordinal_card.dart'; class OrdinalsList extends StatefulWidget { const OrdinalsList({ super.key, + required this.walletId, required this.ordinals, }); + final String walletId; final List ordinals; @override @@ -29,6 +31,7 @@ class _OrdinalsListState extends State { childAspectRatio: 3 / 4, ), itemBuilder: (_, i) => OrdinalCard( + walletId: widget.walletId, ordinal: widget.ordinals[i], ), ); diff --git a/lib/pages/ordinals/ordinals_view.dart b/lib/pages/ordinals/ordinals_view.dart index 2bfa2e9f3..3eca289ef 100644 --- a/lib/pages/ordinals/ordinals_view.dart +++ b/lib/pages/ordinals/ordinals_view.dart @@ -177,6 +177,7 @@ class _OrdinalsViewState extends ConsumerState { ), Expanded( child: OrdinalsList( + walletId: widget.walletId, ordinals: [ for (int i = 0; i < 13; i++) Ordinal( diff --git a/lib/pages/ordinals/widgets/ordinal_card.dart b/lib/pages/ordinals/widgets/ordinal_card.dart index 2af610241..a3419ae87 100644 --- a/lib/pages/ordinals/widgets/ordinal_card.dart +++ b/lib/pages/ordinals/widgets/ordinal_card.dart @@ -21,7 +21,7 @@ class OrdinalCard extends StatelessWidget { onPressed: () { Navigator.of(context).pushNamed( OrdinalDetailsView.routeName, - arguments: widget.walletId, + arguments: (walletId: walletId, ordinal: ordinal), ); }, child: Column( diff --git a/lib/route_generator.dart b/lib/route_generator.dart index cf82dce08..a4239dfdd 100644 --- a/lib/route_generator.dart +++ b/lib/route_generator.dart @@ -422,6 +422,9 @@ class RouteGenerator { return _routeError("${settings.name} invalid args: ${args.toString()}"); case OrdinalDetailsView.routeName: + + print(args.runtimeType); + if (args is ({Ordinal ordinal, String walletId})) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, diff --git a/lib/utilities/assets.dart b/lib/utilities/assets.dart index b8ec501e8..bb3054325 100644 --- a/lib/utilities/assets.dart +++ b/lib/utilities/assets.dart @@ -205,6 +205,7 @@ class _SVG { String get messageQuestion => "assets/svg/message-question-1.svg"; String get list => "assets/svg/list-ul.svg"; String get unclaimedPaynym => "assets/svg/unclaimed.svg"; + String get send => "assets/svg/send.svg"; String get trocadorRatingA => "assets/svg/trocador_rating_a.svg"; String get trocadorRatingB => "assets/svg/trocador_rating_b.svg"; diff --git a/pubspec.yaml b/pubspec.yaml index fd5eac1dc..ca8289bc8 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -335,6 +335,7 @@ flutter: - assets/svg/trocador_rating_b.svg - assets/svg/trocador_rating_c.svg - assets/svg/trocador_rating_d.svg + - assets/svg/send.svg # coin control icons - assets/svg/coin_control/ From 8b71fa70c9fcf0bac38e44a3378527c88a90236e Mon Sep 17 00:00:00 2001 From: julian Date: Tue, 18 Jul 2023 11:26:43 -0600 Subject: [PATCH 013/169] added a couple dialogs --- lib/pages/ordinals/ordinal_details_view.dart | 11 +++- lib/pages/ordinals/ordinals_view.dart | 2 +- lib/pages/ordinals/widgets/dialogs.dart | 62 +++++++++++++++++++ .../ordinals/{ => widgets}/ordinals_list.dart | 0 4 files changed, 72 insertions(+), 3 deletions(-) create mode 100644 lib/pages/ordinals/widgets/dialogs.dart rename lib/pages/ordinals/{ => widgets}/ordinals_list.dart (100%) diff --git a/lib/pages/ordinals/ordinal_details_view.dart b/lib/pages/ordinals/ordinal_details_view.dart index a79847af7..2057e8792 100644 --- a/lib/pages/ordinals/ordinal_details_view.dart +++ b/lib/pages/ordinals/ordinal_details_view.dart @@ -5,6 +5,7 @@ import 'package:flutter/services.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:stackwallet/models/ordinal.dart'; import 'package:stackwallet/notifications/show_flush_bar.dart'; +import 'package:stackwallet/pages/ordinals/widgets/dialogs.dart'; import 'package:stackwallet/themes/stack_colors.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/text_styles.dart'; @@ -241,8 +242,14 @@ class _OrdinalImageGroup extends StatelessWidget { ), buttonHeight: ButtonHeight.l, iconSpacing: 4, - onPressed: () { - // TODO: try send + onPressed: () async { + final response = await showDialog( + context: context, + builder: (_) => const SendOrdinalUnfreezeDialog(), + ); + if (response == "unfreeze") { + // TODO: unfreeze and go to send ord screen + } }, ), ), diff --git a/lib/pages/ordinals/ordinals_view.dart b/lib/pages/ordinals/ordinals_view.dart index 3eca289ef..c45974281 100644 --- a/lib/pages/ordinals/ordinals_view.dart +++ b/lib/pages/ordinals/ordinals_view.dart @@ -12,7 +12,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/svg.dart'; import 'package:stackwallet/models/ordinal.dart'; -import 'package:stackwallet/pages/ordinals/ordinals_list.dart'; +import 'package:stackwallet/pages/ordinals/widgets/ordinals_list.dart'; import 'package:stackwallet/themes/stack_colors.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/constants.dart'; diff --git a/lib/pages/ordinals/widgets/dialogs.dart b/lib/pages/ordinals/widgets/dialogs.dart new file mode 100644 index 000000000..ee5b57d33 --- /dev/null +++ b/lib/pages/ordinals/widgets/dialogs.dart @@ -0,0 +1,62 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:stackwallet/themes/stack_colors.dart'; +import 'package:stackwallet/utilities/assets.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 SendOrdinalUnfreezeDialog extends StatelessWidget { + const SendOrdinalUnfreezeDialog({super.key}); + + @override + Widget build(BuildContext context) { + return StackDialog( + title: "This ordinal is frozen", + icon: SvgPicture.asset( + Assets.svg.coinControl.blocked, + width: 24, + height: 24, + color: Theme.of(context).extension()!.textDark, + ), + message: "To send this ordinal, you must unfreeze it first.", + leftButton: SecondaryButton( + label: "Cancel", + onPressed: Navigator.of(context).pop, + ), + rightButton: PrimaryButton( + label: "Unfreeze", + onPressed: () { + Navigator.of(context).pop("unfreeze"); + }, + ), + ); + } +} + +class UnfreezeOrdinalDialog extends StatelessWidget { + const UnfreezeOrdinalDialog({super.key}); + + @override + Widget build(BuildContext context) { + return StackDialog( + title: "Are you sure you want to unfreeze this ordinal?", + icon: SvgPicture.asset( + Assets.svg.coinControl.blocked, + width: 24, + height: 24, + color: Theme.of(context).extension()!.textDark, + ), + leftButton: SecondaryButton( + label: "Cancel", + onPressed: Navigator.of(context).pop, + ), + rightButton: PrimaryButton( + label: "Unfreeze", + onPressed: () { + Navigator.of(context).pop("unfreeze"); + }, + ), + ); + } +} diff --git a/lib/pages/ordinals/ordinals_list.dart b/lib/pages/ordinals/widgets/ordinals_list.dart similarity index 100% rename from lib/pages/ordinals/ordinals_list.dart rename to lib/pages/ordinals/widgets/ordinals_list.dart From 2af1a8db88a49e7bae0d63381f60f34129ef00d2 Mon Sep 17 00:00:00 2001 From: julian Date: Tue, 18 Jul 2023 11:48:15 -0600 Subject: [PATCH 014/169] ordinals filter view --- lib/pages/ordinals/ordinals_filter_view.dart | 890 +++++++++++++++++++ lib/pages/ordinals/ordinals_view.dart | 5 +- lib/route_generator.dart | 10 +- 3 files changed, 901 insertions(+), 4 deletions(-) diff --git a/lib/pages/ordinals/ordinals_filter_view.dart b/lib/pages/ordinals/ordinals_filter_view.dart index e69de29bb..e06294098 100644 --- a/lib/pages/ordinals/ordinals_filter_view.dart +++ b/lib/pages/ordinals/ordinals_filter_view.dart @@ -0,0 +1,890 @@ +/* + * This file is part of Stack Wallet. + * + * Copyright (c) 2023 Cypher Stack + * All Rights Reserved. + * The code is distributed under GPLv3 license, see LICENSE file for details. + * Generated by Cypher Stack on 2023-05-26 + * + */ + +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_rounded_date_picker/flutter_rounded_date_picker.dart'; +import 'package:flutter_svg/svg.dart'; +import 'package:stackwallet/themes/stack_colors.dart'; +import 'package:stackwallet/themes/theme_providers.dart'; +import 'package:stackwallet/utilities/assets.dart'; +import 'package:stackwallet/utilities/constants.dart'; +import 'package:stackwallet/utilities/format.dart'; +import 'package:stackwallet/utilities/text_styles.dart'; +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_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/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 OrdinalFilter { + final bool isMoonbird; + final bool isPunk; + final DateTime? from; + final DateTime? to; + final String? inscription; + final String keyword; + + OrdinalFilter({ + required this.isMoonbird, + required this.isPunk, + required this.from, + required this.to, + required this.inscription, + required this.keyword, + }); + + OrdinalFilter copyWith({ + bool? isMoonbird, + bool? isPunk, + DateTime? from, + DateTime? to, + String? inscription, + String? keyword, + }) { + return OrdinalFilter( + isMoonbird: isMoonbird ?? this.isMoonbird, + isPunk: isPunk ?? this.isPunk, + from: from ?? this.from, + to: to ?? this.to, + inscription: inscription ?? this.inscription, + keyword: keyword ?? this.keyword, + ); + } +} + +final ordinalFilterProvider = StateProvider((_) => null); + +class OrdinalsFilterView extends ConsumerStatefulWidget { + const OrdinalsFilterView({ + Key? key, + }) : super(key: key); + + static const String routeName = "/ordinalsFilterView"; + + @override + ConsumerState createState() => _OrdinalsFilterViewState(); +} + +class _OrdinalsFilterViewState extends ConsumerState { + final _inscriptionTextEditingController = TextEditingController(); + final _keywordTextEditingController = TextEditingController(); + + bool _isPunk = false; + bool _isMoonbird = false; + + String _fromDateString = ""; + String _toDateString = ""; + + final keywordTextFieldFocusNode = FocusNode(); + final inscriptionTextFieldFocusNode = FocusNode(); + + late Color baseColor; + + @override + initState() { + baseColor = ref.read(themeProvider.state).state.textSubtitle2; + final filterState = ref.read(ordinalFilterProvider.state).state; + if (filterState != null) { + _isMoonbird = filterState.isMoonbird; + _isPunk = filterState.isPunk; + _selectedToDate = filterState.to; + _selectedFromDate = filterState.from; + _keywordTextEditingController.text = filterState.keyword; + _inscriptionTextEditingController.text = filterState.inscription ?? ""; + } + + super.initState(); + } + + @override + dispose() { + _inscriptionTextEditingController.dispose(); + _keywordTextEditingController.dispose(); + keywordTextFieldFocusNode.dispose(); + inscriptionTextFieldFocusNode.dispose(); + + super.dispose(); + } + + // The following two getters are not required if the + // date fields are to remain unclearable. + Widget get _dateFromText { + final isDateSelected = _fromDateString.isEmpty; + return Text( + isDateSelected ? "From..." : _fromDateString, + style: STextStyles.fieldLabel(context).copyWith( + color: isDateSelected + ? Theme.of(context).extension()!.textSubtitle2 + : Theme.of(context).extension()!.accentColorDark), + ); + } + + Widget get _dateToText { + final isDateSelected = _toDateString.isEmpty; + return Text( + isDateSelected ? "To..." : _toDateString, + style: STextStyles.fieldLabel(context).copyWith( + color: isDateSelected + ? Theme.of(context).extension()!.textSubtitle2 + : Theme.of(context).extension()!.accentColorDark), + ); + } + + DateTime? _selectedFromDate = DateTime(2007); + DateTime? _selectedToDate = DateTime.now(); + + MaterialRoundedDatePickerStyle _buildDatePickerStyle() { + return MaterialRoundedDatePickerStyle( + backgroundPicker: Theme.of(context).extension()!.popupBG, + // backgroundHeader: Theme.of(context).extension()!.textSubtitle2, + paddingMonthHeader: const EdgeInsets.only(top: 11), + colorArrowNext: Theme.of(context).extension()!.textSubtitle1, + colorArrowPrevious: + Theme.of(context).extension()!.textSubtitle1, + textStyleButtonNegative: STextStyles.datePicker600(context).copyWith( + color: baseColor, + ), + textStyleButtonPositive: STextStyles.datePicker600(context).copyWith( + color: baseColor, + ), + textStyleCurrentDayOnCalendar: STextStyles.datePicker400(context), + textStyleDayHeader: STextStyles.datePicker600(context), + textStyleDayOnCalendar: STextStyles.datePicker400(context).copyWith( + color: baseColor, + ), + textStyleDayOnCalendarDisabled: + STextStyles.datePicker400(context).copyWith( + color: Theme.of(context).extension()!.textSubtitle3, + ), + textStyleDayOnCalendarSelected: + STextStyles.datePicker400(context).copyWith( + color: Theme.of(context).extension()!.textWhite, + ), + textStyleMonthYearHeader: STextStyles.datePicker600(context).copyWith( + color: Theme.of(context).extension()!.textSubtitle1, + ), + textStyleYearButton: STextStyles.datePicker600(context).copyWith( + color: Theme.of(context).extension()!.textWhite, + ), + // textStyleButtonAction: GoogleFonts.inter(), + ); + } + + MaterialRoundedYearPickerStyle _buildYearPickerStyle() { + return MaterialRoundedYearPickerStyle( + backgroundPicker: Theme.of(context).extension()!.popupBG, + textStyleYear: STextStyles.datePicker600(context).copyWith( + color: Theme.of(context).extension()!.textSubtitle2, + fontSize: 16, + ), + textStyleYearSelected: STextStyles.datePicker600(context).copyWith( + fontSize: 18, + ), + ); + } + + Widget _buildDateRangePicker() { + const middleSeparatorPadding = 2.0; + const middleSeparatorWidth = 12.0; + final isDesktop = Util.isDesktop; + + final width = isDesktop + ? null + : (MediaQuery.of(context).size.width - + (middleSeparatorWidth + + (2 * middleSeparatorPadding) + + (2 * Constants.size.standardPadding))) / + 2; + + return Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: GestureDetector( + key: const Key("OrdinalsViewFromDatePickerKey"), + onTap: () async { + final color = + Theme.of(context).extension()!.accentColorDark; + final height = MediaQuery.of(context).size.height; + // check and hide keyboard + if (FocusScope.of(context).hasFocus) { + FocusScope.of(context).unfocus(); + await Future.delayed(const Duration(milliseconds: 125)); + } + + if (mounted) { + final date = await showRoundedDatePicker( + // This doesn't change statusbar color... + // background: CFColors.starryNight.withOpacity(0.8), + context: context, + initialDate: DateTime.now(), + height: height * 0.5, + theme: ThemeData( + primarySwatch: Util.createMaterialColor( + color, + ), + ), + //TODO pick a better initial date + // 2007 chosen as that is just before bitcoin launched + firstDate: DateTime(2007), + lastDate: DateTime.now(), + borderRadius: Constants.size.circularBorderRadius * 2, + + textPositiveButton: "SELECT", + + styleDatePicker: _buildDatePickerStyle(), + styleYearPicker: _buildYearPickerStyle(), + ); + if (date != null) { + _selectedFromDate = date; + + // flag to adjust date so from date is always before to date + final flag = _selectedToDate != null && + !_selectedFromDate!.isBefore(_selectedToDate!); + if (flag) { + _selectedToDate = DateTime.fromMillisecondsSinceEpoch( + _selectedFromDate!.millisecondsSinceEpoch); + } + + setState(() { + if (flag) { + _toDateString = _selectedToDate == null + ? "" + : Format.formatDate(_selectedToDate!); + } + _fromDateString = _selectedFromDate == null + ? "" + : Format.formatDate(_selectedFromDate!); + }); + } + } + }, + child: Container( + width: width, + decoration: BoxDecoration( + color: Theme.of(context) + .extension()! + .textFieldDefaultBG, + borderRadius: + BorderRadius.circular(Constants.size.circularBorderRadius), + border: Border.all( + color: Theme.of(context) + .extension()! + .textFieldDefaultBG, + width: 1, + ), + ), + child: Padding( + padding: EdgeInsets.symmetric( + horizontal: 12, + vertical: isDesktop ? 17 : 12, + ), + child: Row( + children: [ + SvgPicture.asset( + Assets.svg.calendar, + height: 20, + width: 20, + color: Theme.of(context) + .extension()! + .textSubtitle2, + ), + const SizedBox( + width: 10, + ), + Align( + alignment: Alignment.centerLeft, + child: FittedBox( + child: _dateFromText, + ), + ) + ], + ), + ), + ), + ), + ), + Padding( + padding: + const EdgeInsets.symmetric(horizontal: middleSeparatorPadding), + child: Container( + width: middleSeparatorWidth, + // height: 1, + // color: CFColors.smoke, + ), + ), + Expanded( + child: GestureDetector( + key: const Key("OrdinalsViewToDatePickerKey"), + onTap: () async { + final color = + Theme.of(context).extension()!.accentColorDark; + final height = MediaQuery.of(context).size.height; + // check and hide keyboard + if (FocusScope.of(context).hasFocus) { + FocusScope.of(context).unfocus(); + await Future.delayed(const Duration(milliseconds: 125)); + } + + if (mounted) { + final date = await showRoundedDatePicker( + // This doesn't change statusbar color... + // background: CFColors.starryNight.withOpacity(0.8), + context: context, + height: height * 0.5, + theme: ThemeData( + primarySwatch: Util.createMaterialColor( + color, + ), + ), + //TODO pick a better initial date + // 2007 chosen as that is just before bitcoin launched + initialDate: DateTime.now(), + firstDate: DateTime(2007), + lastDate: DateTime.now(), + borderRadius: Constants.size.circularBorderRadius * 2, + + textPositiveButton: "SELECT", + + styleDatePicker: _buildDatePickerStyle(), + styleYearPicker: _buildYearPickerStyle(), + ); + if (date != null) { + _selectedToDate = date; + + // flag to adjust date so from date is always before to date + final flag = _selectedFromDate != null && + !_selectedToDate!.isAfter(_selectedFromDate!); + if (flag) { + _selectedFromDate = DateTime.fromMillisecondsSinceEpoch( + _selectedToDate!.millisecondsSinceEpoch); + } + + setState(() { + if (flag) { + _fromDateString = _selectedFromDate == null + ? "" + : Format.formatDate(_selectedFromDate!); + } + _toDateString = _selectedToDate == null + ? "" + : Format.formatDate(_selectedToDate!); + }); + } + } + }, + child: Container( + width: width, + decoration: BoxDecoration( + color: Theme.of(context) + .extension()! + .textFieldDefaultBG, + borderRadius: + BorderRadius.circular(Constants.size.circularBorderRadius), + border: Border.all( + color: Theme.of(context) + .extension()! + .textFieldDefaultBG, + width: 1, + ), + ), + child: Padding( + padding: EdgeInsets.symmetric( + horizontal: 12, + vertical: isDesktop ? 17 : 12, + ), + child: Row( + children: [ + SvgPicture.asset( + Assets.svg.calendar, + height: 20, + width: 20, + color: Theme.of(context) + .extension()! + .textSubtitle2, + ), + const SizedBox( + width: 10, + ), + Align( + alignment: Alignment.centerLeft, + child: FittedBox( + child: _dateToText, + ), + ) + ], + ), + ), + ), + ), + ), + if (isDesktop) + const SizedBox( + width: 24, + ), + ], + ); + } + + @override + Widget build(BuildContext context) { + if (Util.isDesktop) { + return DesktopDialog( + maxWidth: 576, + maxHeight: double.infinity, + child: Padding( + padding: const EdgeInsets.only( + left: 32, + bottom: 32, + ), + child: _buildContent(context), + ), + ); + } else { + return 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( + "Ordinals filter", + style: STextStyles.navBarTitle(context), + ), + ), + body: Padding( + padding: EdgeInsets.symmetric( + horizontal: Constants.size.standardPadding, + ), + child: LayoutBuilder( + builder: (context, constraints) { + return SingleChildScrollView( + child: ConstrainedBox( + constraints: + BoxConstraints(minHeight: constraints.maxHeight), + child: IntrinsicHeight( + child: _buildContent(context), + ), + ), + ); + }, + ), + ), + ), + ); + } + } + + Widget _buildContent(BuildContext context) { + final isDesktop = Util.isDesktop; + + return Column( + children: [ + if (isDesktop) + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Ordinals filter", + style: STextStyles.desktopH3(context), + textAlign: TextAlign.center, + ), + const DesktopDialogCloseButton(), + ], + ), + SizedBox( + height: isDesktop ? 14 : 10, + ), + if (!isDesktop) + Align( + alignment: Alignment.centerLeft, + child: FittedBox( + child: Text( + "Collection", + style: STextStyles.smallMed12(context), + ), + ), + ), + if (!isDesktop) + const SizedBox( + height: 12, + ), + RoundedWhiteContainer( + padding: EdgeInsets.all(isDesktop ? 0 : 12), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + GestureDetector( + onTap: () { + setState(() { + _isPunk = !_isPunk; + }); + }, + child: Container( + color: Colors.transparent, + child: Row( + children: [ + SizedBox( + height: 20, + width: 20, + child: Checkbox( + key: const Key("OrdinalsPunkCheckboxKey"), + materialTapTargetSize: + MaterialTapTargetSize.shrinkWrap, + value: _isPunk, + onChanged: (newValue) { + setState(() { + _isPunk = newValue!; + }); + }, + ), + ), + const SizedBox( + width: 14, + ), + Align( + alignment: Alignment.centerLeft, + child: FittedBox( + child: Column( + children: [ + Text( + "Punks", + style: isDesktop + ? STextStyles.desktopTextSmall(context) + : STextStyles.itemSubtitle12(context), + ), + if (isDesktop) + const SizedBox( + height: 4, + ), + ], + ), + ), + ) + ], + ), + ), + ), + ], + ), + SizedBox( + height: isDesktop ? 4 : 10, + ), + Row( + children: [ + GestureDetector( + onTap: () { + setState(() { + _isMoonbird = !_isMoonbird; + }); + }, + child: Container( + color: Colors.transparent, + child: Row( + children: [ + SizedBox( + height: 20, + width: 20, + child: Checkbox( + key: const Key( + "OrdinalsFilterMoonbirdCheckboxKey", + ), + materialTapTargetSize: + MaterialTapTargetSize.shrinkWrap, + value: _isMoonbird, + onChanged: (newValue) { + setState(() { + _isMoonbird = newValue!; + }); + }, + ), + ), + const SizedBox( + width: 14, + ), + Align( + alignment: Alignment.centerLeft, + child: FittedBox( + child: Column( + children: [ + Text( + "Moonbirds", + style: isDesktop + ? STextStyles.desktopTextSmall(context) + : STextStyles.itemSubtitle12(context), + ), + if (isDesktop) + const SizedBox( + height: 4, + ), + ], + ), + ), + ) + ], + ), + ), + ), + ], + ), + ], + ), + ), + SizedBox( + height: isDesktop ? 32 : 24, + ), + Align( + alignment: Alignment.centerLeft, + child: FittedBox( + child: Text( + "Date", + style: isDesktop + ? STextStyles.labelExtraExtraSmall(context) + : STextStyles.smallMed12(context), + ), + ), + ), + SizedBox( + height: isDesktop ? 10 : 8, + ), + _buildDateRangePicker(), + SizedBox( + height: isDesktop ? 32 : 24, + ), + Align( + alignment: Alignment.centerLeft, + child: FittedBox( + child: Text( + "Inscription", + style: isDesktop + ? STextStyles.labelExtraExtraSmall(context) + : STextStyles.smallMed12(context), + ), + ), + ), + SizedBox( + height: isDesktop ? 10 : 8, + ), + Padding( + padding: EdgeInsets.only(right: isDesktop ? 32 : 0), + child: ClipRRect( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + child: TextField( + autocorrect: Util.isDesktop ? false : true, + enableSuggestions: Util.isDesktop ? false : true, + key: const Key("OrdinalsInscriptionFieldKey"), + controller: _inscriptionTextEditingController, + focusNode: inscriptionTextFieldFocusNode, + onChanged: (_) => setState(() {}), + style: isDesktop + ? STextStyles.desktopTextExtraSmall(context).copyWith( + color: + Theme.of(context).extension()!.textDark, + height: 1.8, + ) + : STextStyles.field(context), + decoration: standardInputDecoration( + "Enter inscription number...", + keywordTextFieldFocusNode, + context, + desktopMed: isDesktop, + ).copyWith( + contentPadding: isDesktop + ? const EdgeInsets.symmetric( + vertical: 10, + horizontal: 16, + ) + : null, + suffixIcon: _inscriptionTextEditingController.text.isNotEmpty + ? Padding( + padding: const EdgeInsets.only(right: 0), + child: UnconstrainedBox( + child: Row( + children: [ + TextFieldIconButton( + child: const XIcon(), + onTap: () async { + setState(() { + _inscriptionTextEditingController.text = ""; + }); + }, + ), + ], + ), + ), + ) + : null, + ), + ), + ), + ), + SizedBox( + height: isDesktop ? 32 : 24, + ), + Align( + alignment: Alignment.centerLeft, + child: FittedBox( + child: Text( + "Keyword", + style: isDesktop + ? STextStyles.labelExtraExtraSmall(context) + : STextStyles.smallMed12(context), + ), + ), + ), + SizedBox( + height: isDesktop ? 10 : 8, + ), + Padding( + padding: EdgeInsets.only(right: isDesktop ? 32 : 0), + child: ClipRRect( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + child: TextField( + autocorrect: Util.isDesktop ? false : true, + enableSuggestions: Util.isDesktop ? false : true, + key: const Key("OrdinalsViewKeywordFieldKey"), + controller: _keywordTextEditingController, + focusNode: keywordTextFieldFocusNode, + style: isDesktop + ? STextStyles.desktopTextExtraSmall(context).copyWith( + color: + Theme.of(context).extension()!.textDark, + height: 1.8, + ) + : STextStyles.field(context), + onChanged: (_) => setState(() {}), + decoration: standardInputDecoration( + "Type keyword...", + keywordTextFieldFocusNode, + context, + desktopMed: isDesktop, + ).copyWith( + contentPadding: isDesktop + ? const EdgeInsets.symmetric( + vertical: 10, + horizontal: 16, + ) + : null, + suffixIcon: _keywordTextEditingController.text.isNotEmpty + ? Padding( + padding: const EdgeInsets.only(right: 0), + child: UnconstrainedBox( + child: Row( + children: [ + TextFieldIconButton( + child: const XIcon(), + onTap: () async { + setState(() { + _keywordTextEditingController.text = ""; + }); + }, + ), + ], + ), + ), + ) + : null, + ), + ), + ), + ), + if (!isDesktop) const Spacer(), + SizedBox( + height: isDesktop ? 32 : 20, + ), + Row( + children: [ + Expanded( + child: SecondaryButton( + label: "Cancel", + buttonHeight: isDesktop ? ButtonHeight.l : null, + onPressed: () async { + if (!isDesktop) { + if (FocusScope.of(context).hasFocus) { + FocusScope.of(context).unfocus(); + await Future.delayed( + const Duration( + milliseconds: 75, + ), + ); + } + } + if (mounted) { + Navigator.of(context).pop(); + } + }, + ), + ), + const SizedBox( + width: 16, + ), + Expanded( + child: PrimaryButton( + buttonHeight: isDesktop ? ButtonHeight.l : null, + onPressed: () async { + await _onApplyPressed(); + }, + label: "Save", + ), + ), + if (isDesktop) + const SizedBox( + width: 32, + ), + ], + ), + if (!isDesktop) + const SizedBox( + height: 20, + ), + ], + ); + } + + Future _onApplyPressed() async { + final filter = OrdinalFilter( + isPunk: _isPunk, + isMoonbird: _isMoonbird, + from: _selectedFromDate, + to: _selectedToDate, + inscription: _inscriptionTextEditingController.text, + keyword: _keywordTextEditingController.text, + ); + + ref.read(ordinalFilterProvider.state).state = filter; + + Navigator.of(context).pop(); + } +} diff --git a/lib/pages/ordinals/ordinals_view.dart b/lib/pages/ordinals/ordinals_view.dart index c45974281..fe60b5e28 100644 --- a/lib/pages/ordinals/ordinals_view.dart +++ b/lib/pages/ordinals/ordinals_view.dart @@ -12,6 +12,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/svg.dart'; import 'package:stackwallet/models/ordinal.dart'; +import 'package:stackwallet/pages/ordinals/ordinals_filter_view.dart'; import 'package:stackwallet/pages/ordinals/widgets/ordinals_list.dart'; import 'package:stackwallet/themes/stack_colors.dart'; import 'package:stackwallet/utilities/assets.dart'; @@ -104,7 +105,9 @@ class _OrdinalsViewState extends ConsumerState { .topNavIconPrimary, ), onPressed: () { - // todo filter view + Navigator.of(context).pushNamed( + OrdinalsFilterView.routeName, + ); }, ), ), diff --git a/lib/route_generator.dart b/lib/route_generator.dart index a4239dfdd..6217cf201 100644 --- a/lib/route_generator.dart +++ b/lib/route_generator.dart @@ -59,6 +59,7 @@ import 'package:stackwallet/pages/intro_view.dart'; import 'package:stackwallet/pages/manage_favorites_view/manage_favorites_view.dart'; import 'package:stackwallet/pages/notification_views/notifications_view.dart'; import 'package:stackwallet/pages/ordinals/ordinal_details_view.dart'; +import 'package:stackwallet/pages/ordinals/ordinals_filter_view.dart'; import 'package:stackwallet/pages/ordinals/ordinals_view.dart'; import 'package:stackwallet/pages/paynym/add_new_paynym_follow_view.dart'; import 'package:stackwallet/pages/paynym/paynym_claim_view.dart'; @@ -422,9 +423,6 @@ class RouteGenerator { return _routeError("${settings.name} invalid args: ${args.toString()}"); case OrdinalDetailsView.routeName: - - print(args.runtimeType); - if (args is ({Ordinal ordinal, String walletId})) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, @@ -439,6 +437,12 @@ class RouteGenerator { } return _routeError("${settings.name} invalid args: ${args.toString()}"); + case OrdinalsFilterView.routeName: + return getRoute( + shouldUseMaterialRoute: useMaterialPageRoute, + builder: (_) => const OrdinalsFilterView(), + settings: RouteSettings(name: settings.name)); + case UtxoDetailsView.routeName: if (args is Tuple2) { return getRoute( From 5b4a80321536b4f4f6a86c96dd666e0e15226d0a Mon Sep 17 00:00:00 2001 From: julian Date: Tue, 18 Jul 2023 11:51:57 -0600 Subject: [PATCH 015/169] add ordinals icon --- assets/svg/ordinal.svg | 12 ++++++++ lib/pages/wallet_view/wallet_view.dart | 3 +- lib/utilities/assets.dart | 1 + .../components/icons/ordinals_nav_icon.dart | 28 +++++++++++++++++++ pubspec.yaml | 1 + 5 files changed, 44 insertions(+), 1 deletion(-) create mode 100644 assets/svg/ordinal.svg create mode 100644 lib/widgets/wallet_navigation_bar/components/icons/ordinals_nav_icon.dart diff --git a/assets/svg/ordinal.svg b/assets/svg/ordinal.svg new file mode 100644 index 000000000..7ac863a84 --- /dev/null +++ b/assets/svg/ordinal.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/lib/pages/wallet_view/wallet_view.dart b/lib/pages/wallet_view/wallet_view.dart index 278d43d35..3e51d25a9 100644 --- a/lib/pages/wallet_view/wallet_view.dart +++ b/lib/pages/wallet_view/wallet_view.dart @@ -73,6 +73,7 @@ import 'package:stackwallet/widgets/stack_dialog.dart'; import 'package:stackwallet/widgets/wallet_navigation_bar/components/icons/buy_nav_icon.dart'; import 'package:stackwallet/widgets/wallet_navigation_bar/components/icons/coin_control_nav_icon.dart'; import 'package:stackwallet/widgets/wallet_navigation_bar/components/icons/exchange_nav_icon.dart'; +import 'package:stackwallet/widgets/wallet_navigation_bar/components/icons/ordinals_nav_icon.dart'; import 'package:stackwallet/widgets/wallet_navigation_bar/components/icons/paynym_nav_icon.dart'; import 'package:stackwallet/widgets/wallet_navigation_bar/components/icons/receive_nav_icon.dart'; import 'package:stackwallet/widgets/wallet_navigation_bar/components/icons/send_nav_icon.dart'; @@ -1016,7 +1017,7 @@ class _WalletViewState extends ConsumerState { )) WalletNavigationBarItemData( label: "Ordinals", - icon: const CoinControlNavIcon(), + icon: const OrdinalsNavIcon(), onTap: () { Navigator.of(context).pushNamed( OrdinalsView.routeName, diff --git a/lib/utilities/assets.dart b/lib/utilities/assets.dart index bb3054325..2686c1463 100644 --- a/lib/utilities/assets.dart +++ b/lib/utilities/assets.dart @@ -206,6 +206,7 @@ class _SVG { String get list => "assets/svg/list-ul.svg"; String get unclaimedPaynym => "assets/svg/unclaimed.svg"; String get send => "assets/svg/send.svg"; + String get ordinal => "assets/svg/ordinal.svg"; String get trocadorRatingA => "assets/svg/trocador_rating_a.svg"; String get trocadorRatingB => "assets/svg/trocador_rating_b.svg"; diff --git a/lib/widgets/wallet_navigation_bar/components/icons/ordinals_nav_icon.dart b/lib/widgets/wallet_navigation_bar/components/icons/ordinals_nav_icon.dart new file mode 100644 index 000000000..4c23bf20a --- /dev/null +++ b/lib/widgets/wallet_navigation_bar/components/icons/ordinals_nav_icon.dart @@ -0,0 +1,28 @@ +/* + * This file is part of Stack Wallet. + * + * Copyright (c) 2023 Cypher Stack + * All Rights Reserved. + * The code is distributed under GPLv3 license, see LICENSE file for details. + * Generated by Cypher Stack on 2023-05-26 + * + */ + +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:stackwallet/themes/stack_colors.dart'; +import 'package:stackwallet/utilities/assets.dart'; + +class OrdinalsNavIcon extends StatelessWidget { + const OrdinalsNavIcon({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return SvgPicture.asset( + Assets.svg.ordinal, + height: 20, + width: 20, + color: Theme.of(context).extension()!.bottomNavIconIcon, + ); + } +} diff --git a/pubspec.yaml b/pubspec.yaml index ca8289bc8..e68cae35f 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -336,6 +336,7 @@ flutter: - assets/svg/trocador_rating_c.svg - assets/svg/trocador_rating_d.svg - assets/svg/send.svg + - assets/svg/ordinal.svg # coin control icons - assets/svg/coin_control/ From 72e36caf1e0370ce751a6360d917c664479715fe Mon Sep 17 00:00:00 2001 From: julian-CStack Date: Tue, 18 Jul 2023 15:18:29 -0600 Subject: [PATCH 016/169] update macos refs --- crypto_plugins/flutter_libepiccash | 2 +- crypto_plugins/flutter_liblelantus | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/crypto_plugins/flutter_libepiccash b/crypto_plugins/flutter_libepiccash index c99e2a00b..1cb5373a8 160000 --- a/crypto_plugins/flutter_libepiccash +++ b/crypto_plugins/flutter_libepiccash @@ -1 +1 @@ -Subproject commit c99e2a00b6bb98d497e99858e75ffe75f5898133 +Subproject commit 1cb5373a8aec7ba171173a75449e40dd040206ea diff --git a/crypto_plugins/flutter_liblelantus b/crypto_plugins/flutter_liblelantus index 45d15a4d6..abe199d51 160000 --- a/crypto_plugins/flutter_liblelantus +++ b/crypto_plugins/flutter_liblelantus @@ -1 +1 @@ -Subproject commit 45d15a4d6f5aa276751dbf9fb3ae076ffbebab44 +Subproject commit abe199d519746831f9b852af5a90da4b7deab154 From b60e3fbf1bc58554207f0b27c1b81f6feb47f194 Mon Sep 17 00:00:00 2001 From: sneurlax Date: Tue, 18 Jul 2023 16:36:26 -0500 Subject: [PATCH 017/169] add dtos for ord-litecoin documented endpoint responses --- lib/dto/ordinals/address_response.dart | 48 +++++++++++ lib/dto/ordinals/block_response.dart | 58 +++++++++++++ lib/dto/ordinals/content_response.dart | 19 +++++ lib/dto/ordinals/feed_response.dart | 14 ++++ lib/dto/ordinals/inscription_response.dart | 95 ++++++++++++++++++++++ lib/dto/ordinals/output_response | 42 ++++++++++ lib/dto/ordinals/preview_response.dart | 19 +++++ lib/dto/ordinals/sat_response.dart | 82 +++++++++++++++++++ lib/dto/ordinals/transaction_response.dart | 78 ++++++++++++++++++ 9 files changed, 455 insertions(+) create mode 100644 lib/dto/ordinals/address_response.dart create mode 100644 lib/dto/ordinals/block_response.dart create mode 100644 lib/dto/ordinals/content_response.dart create mode 100644 lib/dto/ordinals/feed_response.dart create mode 100644 lib/dto/ordinals/inscription_response.dart create mode 100644 lib/dto/ordinals/output_response create mode 100644 lib/dto/ordinals/preview_response.dart create mode 100644 lib/dto/ordinals/sat_response.dart create mode 100644 lib/dto/ordinals/transaction_response.dart diff --git a/lib/dto/ordinals/address_response.dart b/lib/dto/ordinals/address_response.dart new file mode 100644 index 000000000..024543b92 --- /dev/null +++ b/lib/dto/ordinals/address_response.dart @@ -0,0 +1,48 @@ +class AddressResponse { + final AddressLinks links; + final String address; + final List inscriptions; + + AddressResponse({ + required this.links, + required this.address, + required this.inscriptions, + }); + + factory AddressResponse.fromJson(Map json) { + final inscriptionsJson = json['inscriptions'] as List; + final inscriptions = inscriptionsJson + .map((inscriptionJson) => InscriptionLink.fromJson(inscriptionJson)) + .toList(); + + return AddressResponse( + links: AddressLinks.fromJson(json['_links']), + address: json['address'], + inscriptions: inscriptions, + ); + } +} + +class AddressLinks { + final AddressLink? self; + + AddressLinks({ + this.self, + }); + + factory AddressLinks.fromJson(Map json) { + return AddressLinks( + self: AddressLink.fromJson(json['self']), + ); + } +} + +class AddressLink { + final String href; + + AddressLink({required this.href}); + + factory AddressLink.fromJson(Map json) { + return AddressLink(href: json['href']); + } +} diff --git a/lib/dto/ordinals/block_response.dart b/lib/dto/ordinals/block_response.dart new file mode 100644 index 000000000..365098594 --- /dev/null +++ b/lib/dto/ordinals/block_response.dart @@ -0,0 +1,58 @@ +class BlockResponse { + final BlockLinks links; + final String hash; + final String previousBlockhash; + final int size; + final String target; + final String timestamp; + final int weight; + + BlockResponse({ + required this.links, + required this.hash, + required this.previousBlockhash, + required this.size, + required this.target, + required this.timestamp, + required this.weight, + }); + + factory BlockResponse.fromJson(Map json) { + return BlockResponse( + links: BlockLinks.fromJson(json['_links']), + hash: json['hash'], + previousBlockhash: json['previous_blockhash'], + size: json['size'], + target: json['target'], + timestamp: json['timestamp'], + weight: json['weight'], + ); + } +} + +class BlockLinks { + final BlockLink? prev; + final BlockLink? self; + + BlockLinks({ + this.prev, + this.self, + }); + + factory BlockLinks.fromJson(Map json) { + return BlockLinks( + prev: BlockLink.fromJson(json['prev']), + self: BlockLink.fromJson(json['self']), + ); + } +} + +class BlockLink { + final String href; + + BlockLink({required this.href}); + + factory BlockLink.fromJson(Map json) { + return BlockLink(href: json['href']); + } +} diff --git a/lib/dto/ordinals/content_response.dart b/lib/dto/ordinals/content_response.dart new file mode 100644 index 000000000..35a2c8ef8 --- /dev/null +++ b/lib/dto/ordinals/content_response.dart @@ -0,0 +1,19 @@ +class ContentResponse { + final FileLink fileLink; + + ContentResponse({required this.fileLink}); + + factory ContentResponse.fromJson(Map json) { + return ContentResponse(fileLink: FileLink.fromJson(json['_links']['file'])); + } +} + +class FileLink { + final String href; + + FileLink({required this.href}); + + factory FileLink.fromJson(Map json) { + return FileLink(href: json['href']); + } +} diff --git a/lib/dto/ordinals/feed_response.dart b/lib/dto/ordinals/feed_response.dart new file mode 100644 index 000000000..b6af1c1ca --- /dev/null +++ b/lib/dto/ordinals/feed_response.dart @@ -0,0 +1,14 @@ +class FeedResponse { + final List inscriptions; + + FeedResponse(this.inscriptions); + + factory FeedResponse.fromJson(Map json) { + final inscriptionsJson = json['_links']['inscriptions'] as List; + final inscriptions = inscriptionsJson + .map((inscriptionJson) => InscriptionLink.fromJson(inscriptionJson)) + .toList(); + + return FeedResponse(inscriptions); + } +} diff --git a/lib/dto/ordinals/inscription_response.dart b/lib/dto/ordinals/inscription_response.dart new file mode 100644 index 000000000..e06ddca6d --- /dev/null +++ b/lib/dto/ordinals/inscription_response.dart @@ -0,0 +1,95 @@ +class InscriptionResponse { + final InscriptionLinks links; + final String address; + final int contentLength; + final String contentType; + final int genesisFee; + final int genesisHeight; + final String genesisTransaction; + final String location; + final int number; + final int offset; + final String output; + final dynamic sat; // Change to appropriate type if available + final String timestamp; + + InscriptionResponse({ + required this.links, + required this.address, + required this.contentLength, + required this.contentType, + required this.genesisFee, + required this.genesisHeight, + required this.genesisTransaction, + required this.location, + required this.number, + required this.offset, + required this.output, + required this.sat, + required this.timestamp, + }); + + factory InscriptionResponse.fromJson(Map json) { + return InscriptionResponse( + links: InscriptionLinks.fromJson(json['_links']), + address: json['address'], + contentLength: json['content_length'], + contentType: json['content_type'], + genesisFee: json['genesis_fee'], + genesisHeight: json['genesis_height'], + genesisTransaction: json['genesis_transaction'], + location: json['location'], + number: json['number'], + offset: json['offset'], + output: json['output'], + sat: json['sat'], + timestamp: json['timestamp'], + ); + } +} + +class InscriptionLinks { + final InscriptionLink? content; + final InscriptionLink? genesisTransaction; + final InscriptionLink? next; + final InscriptionLink? output; + final InscriptionLink? prev; + final InscriptionLink? preview; + final InscriptionLink? sat; + final InscriptionLink? self; + + InscriptionLinks({ + this.content, + this.genesisTransaction, + this.next, + this.output, + this.prev, + this.preview, + this.sat, + this.self, + }); + + factory InscriptionLinks.fromJson(Map json) { + return InscriptionLinks( + content: InscriptionLink.fromJson(json['content']), + genesisTransaction: InscriptionLink.fromJson(json['genesis_transaction']), + next: InscriptionLink.fromJson(json['next']), + output: InscriptionLink.fromJson(json['output']), + prev: InscriptionLink.fromJson(json['prev']), + preview: InscriptionLink.fromJson(json['preview']), + sat: InscriptionLink.fromJson(json['sat']), + self: InscriptionLink.fromJson(json['self']), + ); + } +} + +class InscriptionLink { + final String href; + final String title; + + InscriptionLink({required this.href, required this.title}); + + factory InscriptionLink.fromJson(Map json) { + return InscriptionLink(href: json['href'], title: json['title']); + } +} diff --git a/lib/dto/ordinals/output_response b/lib/dto/ordinals/output_response new file mode 100644 index 000000000..cd25fe961 --- /dev/null +++ b/lib/dto/ordinals/output_response @@ -0,0 +1,42 @@ +class OutputResponse { + final OutputLinks links; + final String address; + final String scriptPubkey; + final String transaction; + final int value; + + OutputResponse({ + required this.links, + required this.address, + required this.scriptPubkey, + required this.transaction, + required this.value, + }); + + factory OutputResponse.fromJson(Map json) { + return OutputResponse( + links: OutputLinks.fromJson(json['_links']), + address: json['address'], + scriptPubkey: json['script_pubkey'], + transaction: json['transaction'], + value: json['value'], + ); + } +} + +class OutputLinks { + final OutputLink? self; + final TransactionLink? transaction; + + OutputLinks({ + this.self, + this.transaction, + }); + + factory OutputLinks.fromJson(Map json) { + return OutputLinks( + self: OutputLink.fromJson(json['self']), + transaction: TransactionLink.fromJson(json['transaction']), + ); + } +} \ No newline at end of file diff --git a/lib/dto/ordinals/preview_response.dart b/lib/dto/ordinals/preview_response.dart new file mode 100644 index 000000000..d7171702d --- /dev/null +++ b/lib/dto/ordinals/preview_response.dart @@ -0,0 +1,19 @@ +class PreviewResponse { + final ImageLink imageLink; + + PreviewResponse({required this.imageLink}); + + factory PreviewResponse.fromJson(Map json) { + return PreviewResponse(imageLink: ImageLink.fromJson(json['_links']['image'])); + } +} + +class ImageLink { + final String href; + + ImageLink({required this.href}); + + factory ImageLink.fromJson(Map json) { + return ImageLink(href: json['href']); + } +} \ No newline at end of file diff --git a/lib/dto/ordinals/sat_response.dart b/lib/dto/ordinals/sat_response.dart new file mode 100644 index 000000000..0f88918f3 --- /dev/null +++ b/lib/dto/ordinals/sat_response.dart @@ -0,0 +1,82 @@ +class SatResponse { + final SatLinks links; + final int block; + final int cycle; + final String decimal; + final String degree; + final int epoch; + final String name; + final int offset; + final String percentile; + final int period; + final String rarity; + final String timestamp; + + SatResponse({ + required this.links, + required this.block, + required this.cycle, + required this.decimal, + required this.degree, + required this.epoch, + required this.name, + required this.offset, + required this.percentile, + required this.period, + required this.rarity, + required this.timestamp, + }); + + factory SatResponse.fromJson(Map json) { + return SatResponse( + links: SatLinks.fromJson(json['_links']), + block: json['block'], + cycle: json['cycle'], + decimal: json['decimal'], + degree: json['degree'], + epoch: json['epoch'], + name: json['name'], + offset: json['offset'], + percentile: json['percentile'], + period: json['period'], + rarity: json['rarity'], + timestamp: json['timestamp'], + ); + } +} + +class SatLinks { + final SatLink? block; + final SatLink? inscription; + final SatLink? next; + final SatLink? prev; + final SatLink? self; + + SatLinks({ + this.block, + this.inscription, + this.next, + this.prev, + this.self, + }); + + factory SatLinks.fromJson(Map json) { + return SatLinks( + block: SatLink.fromJson(json['block']), + inscription: SatLink.fromJson(json['inscription']), + next: SatLink.fromJson(json['next']), + prev: SatLink.fromJson(json['prev']), + self: SatLink.fromJson(json['self']), + ); + } +} + +class SatLink { + final String href; + + SatLink({required this.href}); + + factory SatLink.fromJson(Map json) { + return SatLink(href: json['href']); + } +} diff --git a/lib/dto/ordinals/transaction_response.dart b/lib/dto/ordinals/transaction_response.dart new file mode 100644 index 000000000..b030a1daa --- /dev/null +++ b/lib/dto/ordinals/transaction_response.dart @@ -0,0 +1,78 @@ +class TransactionResponse { + final TransactionLinks links; + final List inputs; + final InscriptionLink inscription; + final List outputs; + final TransactionLink self; + final String transaction; + + TransactionResponse({ + required this.links, + required this.inputs, + required this.inscription, + required this.outputs, + required this.self, + required this.transaction, + }); + + factory TransactionResponse.fromJson(Map json) { + final inputsJson = json['_links']['inputs'] as List; + final inputs = inputsJson + .map((inputJson) => OutputLink.fromJson(inputJson)) + .toList(); + + final outputsJson = json['_links']['outputs'] as List; + final outputs = outputsJson + .map((outputJson) => OutputLink.fromJson(outputJson)) + .toList(); + + return TransactionResponse( + links: TransactionLinks.fromJson(json['_links']), + inputs: inputs, + inscription: InscriptionLink.fromJson(json['_links']['inscription']), + outputs: outputs, + self: TransactionLink.fromJson(json['_links']['self']), + transaction: json['transaction'], + ); + } +} + +class TransactionLinks { + final TransactionLink? block; + final InscriptionLink? inscription; + final TransactionLink? self; + + TransactionLinks({ + this.block, + this.inscription, + this.self, + }); + + factory TransactionLinks.fromJson(Map json) { + return TransactionLinks( + block: TransactionLink.fromJson(json['block']), + inscription: InscriptionLink.fromJson(json['inscription']), + self: TransactionLink.fromJson(json['self']), + ); + } +} + +class TransactionLink { + final String href; + + TransactionLink({required this.href}); + + factory TransactionLink.fromJson(Map json) { + return TransactionLink(href: json['href']); + } +} + +class OutputLink { + final String href; + + OutputLink({required this.href}); + + factory OutputLink.fromJson(Map json) { + return OutputLink(href: json['href']); + } +} From c60721872a28273ce382543b388c35701d5738a0 Mon Sep 17 00:00:00 2001 From: julian-CStack Date: Tue, 18 Jul 2023 15:57:55 -0600 Subject: [PATCH 018/169] macos alpha --- macos/Podfile.lock | 8 ++- macos/Runner.xcodeproj/project.pbxproj | 82 ++++++++++++++++++++++++++ pubspec.lock | 2 +- 3 files changed, 90 insertions(+), 2 deletions(-) diff --git a/macos/Podfile.lock b/macos/Podfile.lock index 2b6385a35..69c91af48 100644 --- a/macos/Podfile.lock +++ b/macos/Podfile.lock @@ -62,6 +62,8 @@ PODS: - FlutterMacOS (1.0.0) - isar_flutter_libs (1.0.0): - FlutterMacOS + - lelantus (0.0.1): + - FlutterMacOS - package_info_plus (0.0.1): - FlutterMacOS - path_provider_foundation (0.0.1): @@ -92,6 +94,7 @@ DEPENDENCIES: - flutter_secure_storage_macos (from `Flutter/ephemeral/.symlinks/plugins/flutter_secure_storage_macos/macos`) - FlutterMacOS (from `Flutter/ephemeral`) - isar_flutter_libs (from `Flutter/ephemeral/.symlinks/plugins/isar_flutter_libs/macos`) + - lelantus (from `Flutter/ephemeral/.symlinks/plugins/lelantus/macos`) - package_info_plus (from `Flutter/ephemeral/.symlinks/plugins/package_info_plus/macos`) - path_provider_foundation (from `Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin`) - share_plus (from `Flutter/ephemeral/.symlinks/plugins/share_plus/macos`) @@ -129,6 +132,8 @@ EXTERNAL SOURCES: :path: Flutter/ephemeral isar_flutter_libs: :path: Flutter/ephemeral/.symlinks/plugins/isar_flutter_libs/macos + lelantus: + :path: Flutter/ephemeral/.symlinks/plugins/lelantus/macos package_info_plus: :path: Flutter/ephemeral/.symlinks/plugins/package_info_plus/macos path_provider_foundation: @@ -152,11 +157,12 @@ SPEC CHECKSUMS: desktop_drop: 69eeff437544aa619c8db7f4481b3a65f7696898 device_info_plus: 5401765fde0b8d062a2f8eb65510fb17e77cf07f devicelocale: 9f0f36ac651cabae2c33f32dcff4f32b61c38225 - flutter_libepiccash: b33f7396504712b513b8ff019a3f6f3bdae54cfb + flutter_libepiccash: 9113ac75dd325f8bcf00bc3ab583c7fc2780cf3c flutter_local_notifications: 3805ca215b2fb7f397d78b66db91f6a747af52e4 flutter_secure_storage_macos: d56e2d218c1130b262bef8b4a7d64f88d7f9c9ea FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24 isar_flutter_libs: 43385c99864c168fadba7c9adeddc5d38838ca6a + lelantus: 3dfbf92b1e66b3573494dfe3d6a21c4988b5361b package_info_plus: 02d7a575e80f194102bef286361c6c326e4c29ce path_provider_foundation: eaf5b3e458fc0e5fbb9940fb09980e853fe058b8 ReachabilitySwift: 985039c6f7b23a1da463388634119492ff86c825 diff --git a/macos/Runner.xcodeproj/project.pbxproj b/macos/Runner.xcodeproj/project.pbxproj index 21cf855f6..9b99a3480 100644 --- a/macos/Runner.xcodeproj/project.pbxproj +++ b/macos/Runner.xcodeproj/project.pbxproj @@ -27,6 +27,9 @@ 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F22044A3C60003C045 /* Assets.xcassets */; }; 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; }; 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; }; + B98151812A674022009D013C /* mobileliblelantus.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B98151802A674022009D013C /* mobileliblelantus.framework */; }; + B98151822A67402A009D013C /* mobileliblelantus.framework in Bundle Framework */ = {isa = PBXBuildFile; fileRef = B98151802A674022009D013C /* mobileliblelantus.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + B98151842A674143009D013C /* libsqlite3.0.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = B98151832A674143009D013C /* libsqlite3.0.tbd */; }; BFD0376C00E1FFD46376BB9D /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9206484E84CB0AD93E3E68CA /* Pods_RunnerTests.framework */; }; F653CA022D33E8B60E11A9F3 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E6036BF01BF05EA773C76D22 /* Pods_Runner.framework */; }; /* End PBXBuildFile section */ @@ -55,6 +58,7 @@ dstPath = ""; dstSubfolderSpec = 10; files = ( + B98151822A67402A009D013C /* mobileliblelantus.framework in Bundle Framework */, ); name = "Bundle Framework"; runOnlyForDeploymentPostprocessing = 0; @@ -86,6 +90,8 @@ 9206484E84CB0AD93E3E68CA /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = ""; }; ACB8E553D75AA4AC9A7656CE /* 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 = ""; }; + B98151802A674022009D013C /* mobileliblelantus.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = mobileliblelantus.framework; path = ../crypto_plugins/flutter_liblelantus/scripts/macos/mobileliblelantus/mobileliblelantus.framework; sourceTree = ""; }; + B98151832A674143009D013C /* libsqlite3.0.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libsqlite3.0.tbd; path = usr/lib/libsqlite3.0.tbd; sourceTree = SDKROOT; }; BF5E76865ACB46314AC27D8F /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = ""; }; E6036BF01BF05EA773C76D22 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; /* End PBXFileReference section */ @@ -103,6 +109,8 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + B98151842A674143009D013C /* libsqlite3.0.tbd in Frameworks */, + B98151812A674022009D013C /* mobileliblelantus.framework in Frameworks */, F653CA022D33E8B60E11A9F3 /* Pods_Runner.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -188,6 +196,8 @@ D73912EC22F37F3D000D13A0 /* Frameworks */ = { isa = PBXGroup; children = ( + B98151832A674143009D013C /* libsqlite3.0.tbd */, + B98151802A674022009D013C /* mobileliblelantus.framework */, E6036BF01BF05EA773C76D22 /* Pods_Runner.framework */, 9206484E84CB0AD93E3E68CA /* Pods_RunnerTests.framework */, ); @@ -570,6 +580,30 @@ CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "\"${PODS_CONFIGURATION_BUILD_DIR}/ReachabilitySwift\"", + "\"${PODS_CONFIGURATION_BUILD_DIR}/connectivity_plus\"", + "\"${PODS_CONFIGURATION_BUILD_DIR}/cw_monero\"", + "\"${PODS_CONFIGURATION_BUILD_DIR}/cw_shared_external\"", + "\"${PODS_CONFIGURATION_BUILD_DIR}/cw_wownero\"", + "\"${PODS_CONFIGURATION_BUILD_DIR}/desktop_drop\"", + "\"${PODS_CONFIGURATION_BUILD_DIR}/device_info_plus\"", + "\"${PODS_CONFIGURATION_BUILD_DIR}/devicelocale\"", + "\"${PODS_CONFIGURATION_BUILD_DIR}/flutter_libepiccash\"", + "\"${PODS_CONFIGURATION_BUILD_DIR}/flutter_local_notifications\"", + "\"${PODS_CONFIGURATION_BUILD_DIR}/flutter_secure_storage_macos\"", + "\"${PODS_CONFIGURATION_BUILD_DIR}/isar_flutter_libs\"", + "\"${PODS_CONFIGURATION_BUILD_DIR}/lelantus\"", + "\"${PODS_CONFIGURATION_BUILD_DIR}/package_info_plus\"", + "\"${PODS_CONFIGURATION_BUILD_DIR}/path_provider_foundation\"", + "\"${PODS_CONFIGURATION_BUILD_DIR}/share_plus\"", + "\"${PODS_CONFIGURATION_BUILD_DIR}/stack_wallet_backup\"", + "\"${PODS_CONFIGURATION_BUILD_DIR}/url_launcher_macos\"", + "\"${PODS_CONFIGURATION_BUILD_DIR}/wakelock_macos\"", + "\"${PODS_CONFIGURATION_BUILD_DIR}/window_size\"", + "\"${PROJECT_DIR}/../crypto_plugins/flutter_liblelantus/scripts/macos/mobileliblelantus\"", + ); INFOPLIST_FILE = Runner/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", @@ -698,6 +732,30 @@ CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "\"${PODS_CONFIGURATION_BUILD_DIR}/ReachabilitySwift\"", + "\"${PODS_CONFIGURATION_BUILD_DIR}/connectivity_plus\"", + "\"${PODS_CONFIGURATION_BUILD_DIR}/cw_monero\"", + "\"${PODS_CONFIGURATION_BUILD_DIR}/cw_shared_external\"", + "\"${PODS_CONFIGURATION_BUILD_DIR}/cw_wownero\"", + "\"${PODS_CONFIGURATION_BUILD_DIR}/desktop_drop\"", + "\"${PODS_CONFIGURATION_BUILD_DIR}/device_info_plus\"", + "\"${PODS_CONFIGURATION_BUILD_DIR}/devicelocale\"", + "\"${PODS_CONFIGURATION_BUILD_DIR}/flutter_libepiccash\"", + "\"${PODS_CONFIGURATION_BUILD_DIR}/flutter_local_notifications\"", + "\"${PODS_CONFIGURATION_BUILD_DIR}/flutter_secure_storage_macos\"", + "\"${PODS_CONFIGURATION_BUILD_DIR}/isar_flutter_libs\"", + "\"${PODS_CONFIGURATION_BUILD_DIR}/lelantus\"", + "\"${PODS_CONFIGURATION_BUILD_DIR}/package_info_plus\"", + "\"${PODS_CONFIGURATION_BUILD_DIR}/path_provider_foundation\"", + "\"${PODS_CONFIGURATION_BUILD_DIR}/share_plus\"", + "\"${PODS_CONFIGURATION_BUILD_DIR}/stack_wallet_backup\"", + "\"${PODS_CONFIGURATION_BUILD_DIR}/url_launcher_macos\"", + "\"${PODS_CONFIGURATION_BUILD_DIR}/wakelock_macos\"", + "\"${PODS_CONFIGURATION_BUILD_DIR}/window_size\"", + "\"${PROJECT_DIR}/../crypto_plugins/flutter_liblelantus/scripts/macos/mobileliblelantus\"", + ); INFOPLIST_FILE = Runner/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", @@ -718,6 +776,30 @@ CODE_SIGN_ENTITLEMENTS = Runner/Release.entitlements; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "\"${PODS_CONFIGURATION_BUILD_DIR}/ReachabilitySwift\"", + "\"${PODS_CONFIGURATION_BUILD_DIR}/connectivity_plus\"", + "\"${PODS_CONFIGURATION_BUILD_DIR}/cw_monero\"", + "\"${PODS_CONFIGURATION_BUILD_DIR}/cw_shared_external\"", + "\"${PODS_CONFIGURATION_BUILD_DIR}/cw_wownero\"", + "\"${PODS_CONFIGURATION_BUILD_DIR}/desktop_drop\"", + "\"${PODS_CONFIGURATION_BUILD_DIR}/device_info_plus\"", + "\"${PODS_CONFIGURATION_BUILD_DIR}/devicelocale\"", + "\"${PODS_CONFIGURATION_BUILD_DIR}/flutter_libepiccash\"", + "\"${PODS_CONFIGURATION_BUILD_DIR}/flutter_local_notifications\"", + "\"${PODS_CONFIGURATION_BUILD_DIR}/flutter_secure_storage_macos\"", + "\"${PODS_CONFIGURATION_BUILD_DIR}/isar_flutter_libs\"", + "\"${PODS_CONFIGURATION_BUILD_DIR}/lelantus\"", + "\"${PODS_CONFIGURATION_BUILD_DIR}/package_info_plus\"", + "\"${PODS_CONFIGURATION_BUILD_DIR}/path_provider_foundation\"", + "\"${PODS_CONFIGURATION_BUILD_DIR}/share_plus\"", + "\"${PODS_CONFIGURATION_BUILD_DIR}/stack_wallet_backup\"", + "\"${PODS_CONFIGURATION_BUILD_DIR}/url_launcher_macos\"", + "\"${PODS_CONFIGURATION_BUILD_DIR}/wakelock_macos\"", + "\"${PODS_CONFIGURATION_BUILD_DIR}/window_size\"", + "\"${PROJECT_DIR}/../crypto_plugins/flutter_liblelantus/scripts/macos/mobileliblelantus\"", + ); INFOPLIST_FILE = Runner/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", diff --git a/pubspec.lock b/pubspec.lock index 9ec8db667..1ad96c7a7 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -948,7 +948,7 @@ packages: path: "crypto_plugins/flutter_liblelantus" relative: true source: path - version: "0.0.1" + version: "0.0.2" lints: dependency: transitive description: From e74b0babf6c66bf1283932b7351950f635e6d80c Mon Sep 17 00:00:00 2001 From: julian Date: Wed, 19 Jul 2023 07:49:49 -0600 Subject: [PATCH 019/169] update plugin refs --- crypto_plugins/flutter_libepiccash | 2 +- crypto_plugins/flutter_liblelantus | 2 +- crypto_plugins/flutter_libmonero | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/crypto_plugins/flutter_libepiccash b/crypto_plugins/flutter_libepiccash index 1cb5373a8..686559344 160000 --- a/crypto_plugins/flutter_libepiccash +++ b/crypto_plugins/flutter_libepiccash @@ -1 +1 @@ -Subproject commit 1cb5373a8aec7ba171173a75449e40dd040206ea +Subproject commit 686559344a58f77732c3d0134fbf44d271a55229 diff --git a/crypto_plugins/flutter_liblelantus b/crypto_plugins/flutter_liblelantus index abe199d51..cdccef0e8 160000 --- a/crypto_plugins/flutter_liblelantus +++ b/crypto_plugins/flutter_liblelantus @@ -1 +1 @@ -Subproject commit abe199d519746831f9b852af5a90da4b7deab154 +Subproject commit cdccef0e8dc10b7fe703b5bb9b41b59b25177e83 diff --git a/crypto_plugins/flutter_libmonero b/crypto_plugins/flutter_libmonero index c206a1434..66f9ad2a6 160000 --- a/crypto_plugins/flutter_libmonero +++ b/crypto_plugins/flutter_libmonero @@ -1 +1 @@ -Subproject commit c206a1434f138a20c38b7abf8d94b9dd4d14b57e +Subproject commit 66f9ad2a6f41dd1f5f2748e7ff01c402835cb042 From d867270aaca54ab927090838dcc3cea1680761dc Mon Sep 17 00:00:00 2001 From: julian-CStack Date: Wed, 19 Jul 2023 08:30:15 -0600 Subject: [PATCH 020/169] exchange view amount string provider fix --- lib/pages/exchange_view/exchange_form.dart | 30 +++++---------- .../exchange_form_state_provider.dart | 38 ++++++++++++++++++- lib/utilities/amount/amount_unit.dart | 5 ++- 3 files changed, 50 insertions(+), 23 deletions(-) diff --git a/lib/pages/exchange_view/exchange_form.dart b/lib/pages/exchange_view/exchange_form.dart index e65242fbd..c4b92a714 100644 --- a/lib/pages/exchange_view/exchange_form.dart +++ b/lib/pages/exchange_view/exchange_form.dart @@ -35,6 +35,7 @@ import 'package:stackwallet/services/exchange/exchange_data_loading_service.dart import 'package:stackwallet/services/exchange/majestic_bank/majestic_bank_exchange.dart'; import 'package:stackwallet/services/exchange/trocador/trocador_exchange.dart'; import 'package:stackwallet/themes/stack_colors.dart'; +import 'package:stackwallet/utilities/amount/amount_unit.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; @@ -160,26 +161,15 @@ class _ExchangeFormState extends ConsumerState { if (value == null) { return null; } - try { - // wtf Dart????? - // This turns "99999999999999999999" into 100000000000000000000.0 - // final numFromLocalised = NumberFormat.decimalPattern( - // ref.read(localeServiceChangeNotifierProvider).locale) - // .parse(value); - // return Decimal.tryParse(numFromLocalised.toString()); - try { - return Decimal.parse(value); - } catch (_) { - try { - return Decimal.parse(value.replaceAll(",", ".")); - } catch (_) { - rethrow; - } - } - } catch (_) { - return null; - } + return AmountUnit.normal + .tryParse( + value, + locale: ref.read(localeServiceChangeNotifierProvider).locale, + coin: Coin.bitcoin, // dummy value (not used due to override) + overrideWithDecimalPlacesFromString: true, + ) + ?.decimal; } Future _getAggregateCurrency(Currency currency) async { @@ -824,7 +814,7 @@ class _ExchangeFormState extends ConsumerState { }); ref.listen(efEstimateProvider.notifier, (previous, next) { - final estimate = (next as StateController).state; + final estimate = (next).state; if (ref.read(efReversedProvider)) { updateSend(estimate); } else { diff --git a/lib/providers/exchange/exchange_form_state_provider.dart b/lib/providers/exchange/exchange_form_state_provider.dart index 07447f169..b4a923b78 100644 --- a/lib/providers/exchange/exchange_form_state_provider.dart +++ b/lib/providers/exchange/exchange_form_state_provider.dart @@ -13,8 +13,12 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:stackwallet/models/exchange/active_pair.dart'; import 'package:stackwallet/models/exchange/response_objects/estimate.dart'; import 'package:stackwallet/models/exchange/response_objects/range.dart'; +import 'package:stackwallet/providers/global/locale_provider.dart'; import 'package:stackwallet/services/exchange/exchange.dart'; import 'package:stackwallet/services/exchange/exchange_response.dart'; +import 'package:stackwallet/utilities/amount/amount.dart'; +import 'package:stackwallet/utilities/amount/amount_unit.dart'; +import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:stackwallet/utilities/enums/exchange_rate_type_enum.dart'; import 'package:tuple/tuple.dart'; @@ -44,7 +48,22 @@ final efSendAmountStringProvider = StateProvider((ref) { if (refreshing && reversed) { return "-"; } else { - return ref.watch(efSendAmountProvider)?.toString() ?? ""; + final decimal = ref.watch(efSendAmountProvider); + String string = ""; + if (decimal != null) { + final amount = Amount.fromDecimal(decimal, fractionDigits: decimal.scale); + final locale = ref.watch(localeServiceChangeNotifierProvider).locale; + string = AmountUnit.normal.displayAmount( + amount: amount, + locale: locale, + coin: Coin + .nano, // use nano just to ensure decimal.scale < Coin.value.decimals + withUnitName: false, + maxDecimalPlaces: decimal.scale, + ); + } + + return string; } }); final efReceiveAmountStringProvider = StateProvider((ref) { @@ -54,7 +73,22 @@ final efReceiveAmountStringProvider = StateProvider((ref) { if (refreshing && reversed == false) { return "-"; } else { - return ref.watch(efReceiveAmountProvider)?.toString() ?? ""; + final decimal = ref.watch(efReceiveAmountProvider); + String string = ""; + if (decimal != null) { + final amount = Amount.fromDecimal(decimal, fractionDigits: decimal.scale); + final locale = ref.watch(localeServiceChangeNotifierProvider).locale; + string = AmountUnit.normal.displayAmount( + amount: amount, + locale: locale, + coin: Coin + .nano, // use nano just to ensure decimal.scale < Coin.value.decimals + withUnitName: false, + maxDecimalPlaces: decimal.scale, + ); + } + + return string; } }); diff --git a/lib/utilities/amount/amount_unit.dart b/lib/utilities/amount/amount_unit.dart index 0c0600d7a..91fef8bc7 100644 --- a/lib/utilities/amount/amount_unit.dart +++ b/lib/utilities/amount/amount_unit.dart @@ -168,6 +168,7 @@ extension AmountUnitExt on AmountUnit { required String locale, required Coin coin, EthContract? tokenContract, + bool overrideWithDecimalPlacesFromString = false, }) { final precisionLost = value.startsWith("~"); @@ -201,7 +202,9 @@ extension AmountUnitExt on AmountUnit { return null; } - final decimalPlaces = tokenContract?.decimals ?? coin.decimals; + final decimalPlaces = overrideWithDecimalPlacesFromString + ? decimal.scale + : tokenContract?.decimals ?? coin.decimals; final realShift = math.min(shift, decimalPlaces); return decimal.shift(0 - realShift).toAmount(fractionDigits: decimalPlaces); From f2a872c44cf4df50f06b7ba9899c20a296592e99 Mon Sep 17 00:00:00 2001 From: julian-CStack Date: Wed, 19 Jul 2023 08:54:56 -0600 Subject: [PATCH 021/169] ensure cursor stays at end on exchange/swap form --- lib/pages/exchange_view/exchange_form.dart | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/lib/pages/exchange_view/exchange_form.dart b/lib/pages/exchange_view/exchange_form.dart index c4b92a714..a15e13843 100644 --- a/lib/pages/exchange_view/exchange_form.dart +++ b/lib/pages/exchange_view/exchange_form.dart @@ -799,6 +799,14 @@ class _ExchangeFormState extends ConsumerState { // if (_swapLock) { _sendController.text = ref.read(efSendAmountStringProvider); // } + + if (_sendFocusNode.hasFocus) { + _sendController.selection = TextSelection.fromPosition( + TextPosition( + offset: _sendController.text.length, + ), + ); + } } }); ref.listen(efSendAmountStringProvider, (previous, String next) { @@ -810,6 +818,14 @@ class _ExchangeFormState extends ConsumerState { ? "-" : ref.read(efReceiveAmountStringProvider); // } + + if (_receiveFocusNode.hasFocus) { + _receiveController.selection = TextSelection.fromPosition( + TextPosition( + offset: _receiveController.text.length, + ), + ); + } } }); From 35f3a05fc5c4d9a9f7b2001fc57502a83ccab64c Mon Sep 17 00:00:00 2001 From: julian-CStack Date: Wed, 19 Jul 2023 08:58:00 -0600 Subject: [PATCH 022/169] macos file --- macos/Flutter/GeneratedPluginRegistrant.swift | 2 ++ 1 file changed, 2 insertions(+) diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index 7cb956395..b044b4b00 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -16,6 +16,7 @@ import flutter_libepiccash import flutter_local_notifications import flutter_secure_storage_macos import isar_flutter_libs +import lelantus import package_info_plus import path_provider_foundation import share_plus @@ -36,6 +37,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { FlutterLocalNotificationsPlugin.register(with: registry.registrar(forPlugin: "FlutterLocalNotificationsPlugin")) FlutterSecureStoragePlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStoragePlugin")) IsarFlutterLibsPlugin.register(with: registry.registrar(forPlugin: "IsarFlutterLibsPlugin")) + LelantusPlugin.register(with: registry.registrar(forPlugin: "LelantusPlugin")) FLTPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FLTPackageInfoPlusPlugin")) PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin")) From 2c5e021783f9ce7490eecb2c89937e25d2c54a7e Mon Sep 17 00:00:00 2001 From: julian-CStack Date: Wed, 19 Jul 2023 09:21:26 -0600 Subject: [PATCH 023/169] update monero ref --- crypto_plugins/flutter_libmonero | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crypto_plugins/flutter_libmonero b/crypto_plugins/flutter_libmonero index 66f9ad2a6..c920c09df 160000 --- a/crypto_plugins/flutter_libmonero +++ b/crypto_plugins/flutter_libmonero @@ -1 +1 @@ -Subproject commit 66f9ad2a6f41dd1f5f2748e7ff01c402835cb042 +Subproject commit c920c09df5e415bba4bbe95dd50e1f0085f040e6 From 51155372d30ca33ccccd1b66c9370da9bb6e012c Mon Sep 17 00:00:00 2001 From: sneurlax Date: Wed, 19 Jul 2023 10:37:42 -0500 Subject: [PATCH 024/169] add inscription endpoint --- lib/dto/ordinals/feed_response.dart | 24 +++- lib/dto/ordinals/inscription_response.dart | 123 ++++++++++----------- lib/services/ordinals_api.dart | 38 ++++++- 3 files changed, 112 insertions(+), 73 deletions(-) diff --git a/lib/dto/ordinals/feed_response.dart b/lib/dto/ordinals/feed_response.dart index b6af1c1ca..d958fbe4c 100644 --- a/lib/dto/ordinals/feed_response.dart +++ b/lib/dto/ordinals/feed_response.dart @@ -1,14 +1,28 @@ class FeedResponse { final List inscriptions; - FeedResponse(this.inscriptions); + FeedResponse({required this.inscriptions}); factory FeedResponse.fromJson(Map json) { - final inscriptionsJson = json['_links']['inscriptions'] as List; - final inscriptions = inscriptionsJson - .map((inscriptionJson) => InscriptionLink.fromJson(inscriptionJson)) + final List inscriptionsJson = json['_links']['inscriptions'] as List; + final List inscriptions = inscriptionsJson + .map((json) => InscriptionLink.fromJson(json as Map)) .toList(); - return FeedResponse(inscriptions); + return FeedResponse(inscriptions: inscriptions); + } +} + +class InscriptionLink { + final String href; + final String title; + + InscriptionLink({required this.href, required this.title}); + + factory InscriptionLink.fromJson(Map json) { + return InscriptionLink( + href: json['href'] as String ?? '', + title: json['title'] as String ?? '', + ); } } diff --git a/lib/dto/ordinals/inscription_response.dart b/lib/dto/ordinals/inscription_response.dart index e06ddca6d..ab093b750 100644 --- a/lib/dto/ordinals/inscription_response.dart +++ b/lib/dto/ordinals/inscription_response.dart @@ -1,17 +1,17 @@ class InscriptionResponse { - final InscriptionLinks links; - final String address; - final int contentLength; - final String contentType; - final int genesisFee; - final int genesisHeight; - final String genesisTransaction; - final String location; - final int number; - final int offset; - final String output; - final dynamic sat; // Change to appropriate type if available - final String timestamp; + late final Links links; + late final String address; + late final int contentLength; + late final String contentType; + late final int genesisFee; + late final int genesisHeight; + late final String genesisTransaction; + late final String location; + late final int number; + late final int offset; + late final String output; + late final String? sat; // Make sure to update the type to allow null + late final String timestamp; InscriptionResponse({ required this.links, @@ -29,67 +29,62 @@ class InscriptionResponse { required this.timestamp, }); - factory InscriptionResponse.fromJson(Map json) { - return InscriptionResponse( - links: InscriptionLinks.fromJson(json['_links']), - address: json['address'], - contentLength: json['content_length'], - contentType: json['content_type'], - genesisFee: json['genesis_fee'], - genesisHeight: json['genesis_height'], - genesisTransaction: json['genesis_transaction'], - location: json['location'], - number: json['number'], - offset: json['offset'], - output: json['output'], - sat: json['sat'], - timestamp: json['timestamp'], - ); + InscriptionResponse.fromJson(Map json) { + links = Links.fromJson(json['_links'] as Map); + address = json['address'] as String; + contentLength = json['content_length'] as int; + contentType = json['content_type'] as String; + genesisFee = json['genesis_fee'] as int; + genesisHeight = json['genesis_height'] as int; + genesisTransaction = json['genesis_transaction'] as String; + location = json['location'] as String; + number = json['number'] as int; + offset = json['offset'] as int; + output = json['output'] as String; + sat = json['sat'] as String?; + timestamp = json['timestamp'] as String; } } -class InscriptionLinks { - final InscriptionLink? content; - final InscriptionLink? genesisTransaction; - final InscriptionLink? next; - final InscriptionLink? output; - final InscriptionLink? prev; - final InscriptionLink? preview; - final InscriptionLink? sat; - final InscriptionLink? self; +class Links { + late final Link content; + late final Link genesisTransaction; + late final Link next; + late final Link output; + late final Link prev; + late final Link preview; + late final Link? sat; // Make sure to update the type to allow null + late final Link self; - InscriptionLinks({ - this.content, - this.genesisTransaction, - this.next, - this.output, - this.prev, - this.preview, + Links({ + required this.content, + required this.genesisTransaction, + required this.next, + required this.output, + required this.prev, + required this.preview, this.sat, - this.self, + required this.self, }); - factory InscriptionLinks.fromJson(Map json) { - return InscriptionLinks( - content: InscriptionLink.fromJson(json['content']), - genesisTransaction: InscriptionLink.fromJson(json['genesis_transaction']), - next: InscriptionLink.fromJson(json['next']), - output: InscriptionLink.fromJson(json['output']), - prev: InscriptionLink.fromJson(json['prev']), - preview: InscriptionLink.fromJson(json['preview']), - sat: InscriptionLink.fromJson(json['sat']), - self: InscriptionLink.fromJson(json['self']), - ); + Links.fromJson(Map json) { + content = Link.fromJson(json['content'] as Map); + genesisTransaction = Link.fromJson(json['genesis_transaction'] as Map); + next = Link.fromJson(json['next'] as Map); + output = Link.fromJson(json['output'] as Map); + prev = Link.fromJson(json['prev'] as Map); + preview = Link.fromJson(json['preview'] as Map); + sat = json['sat'] != null ? Link.fromJson(json['sat'] as Map) : null; + self = Link.fromJson(json['self'] as Map); } } -class InscriptionLink { - final String href; - final String title; +class Link { + late final String href; - InscriptionLink({required this.href, required this.title}); + Link({required this.href}); - factory InscriptionLink.fromJson(Map json) { - return InscriptionLink(href: json['href'], title: json['title']); + Link.fromJson(Map json) { + href = json['href'] as String; } -} +} \ No newline at end of file diff --git a/lib/services/ordinals_api.dart b/lib/services/ordinals_api.dart index 97cc45834..a966988d7 100644 --- a/lib/services/ordinals_api.dart +++ b/lib/services/ordinals_api.dart @@ -1,9 +1,39 @@ -import 'package:stackwallet/models/ordinal.dart'; +import 'dart:convert'; +import 'package:http/http.dart' as http; + +import 'package:stackwallet/dto/ordinals/feed_response.dart'; +import 'package:stackwallet/dto/ordinals/inscription_response.dart'; class OrdinalsAPI { - // dummy class with sample functions to be changed / filled out + final String baseUrl; - static Future> fetch() async { - return []; + OrdinalsAPI({required this.baseUrl}); + + Future> _getResponse(String endpoint) async { + final response = await http.get(Uri.parse('$baseUrl$endpoint')); + if (response.statusCode == 200) { + return _validateJson(response.body); + } else { + throw Exception('Failed to load data'); + } + } + + Map _validateJson(String responseBody) { + final parsed = jsonDecode(responseBody); + if (parsed is Map) { + return parsed; + } else { + throw const FormatException('Invalid JSON format'); + } + } + + Future getLatestInscriptions() async { + final response = await _getResponse('/feed'); + return FeedResponse.fromJson(response); + } + + Future getInscriptionDetails(String inscriptionId) async { + final response = await _getResponse('/inscription/$inscriptionId'); + return InscriptionResponse.fromJson(response); } } From 828782f00cd120125a9996cb9666aa8ed0c075f2 Mon Sep 17 00:00:00 2001 From: sneurlax Date: Wed, 19 Jul 2023 10:41:23 -0500 Subject: [PATCH 025/169] add sat endpoint --- lib/dto/ordinals/sat_response.dart | 36 +++++++++++++++--------------- lib/services/ordinals_api.dart | 6 +++++ 2 files changed, 24 insertions(+), 18 deletions(-) diff --git a/lib/dto/ordinals/sat_response.dart b/lib/dto/ordinals/sat_response.dart index 0f88918f3..b57ccb3cf 100644 --- a/lib/dto/ordinals/sat_response.dart +++ b/lib/dto/ordinals/sat_response.dart @@ -29,18 +29,18 @@ class SatResponse { factory SatResponse.fromJson(Map json) { return SatResponse( - links: SatLinks.fromJson(json['_links']), - block: json['block'], - cycle: json['cycle'], - decimal: json['decimal'], - degree: json['degree'], - epoch: json['epoch'], - name: json['name'], - offset: json['offset'], - percentile: json['percentile'], - period: json['period'], - rarity: json['rarity'], - timestamp: json['timestamp'], + links: SatLinks.fromJson(json['_links'] as Map), + block: json['block'] as int, + cycle: json['cycle'] as int, + decimal: json['decimal'] as String, + degree: json['degree'] as String, + epoch: json['epoch'] as int, + name: json['name'] as String, + offset: json['offset'] as int, + percentile: json['percentile'] as String, + period: json['period'] as int, + rarity: json['rarity'] as String, + timestamp: json['timestamp'] as String, ); } } @@ -62,11 +62,11 @@ class SatLinks { factory SatLinks.fromJson(Map json) { return SatLinks( - block: SatLink.fromJson(json['block']), - inscription: SatLink.fromJson(json['inscription']), - next: SatLink.fromJson(json['next']), - prev: SatLink.fromJson(json['prev']), - self: SatLink.fromJson(json['self']), + block: SatLink.fromJson(json['block'] as Map), + inscription: SatLink.fromJson(json['inscription'] as Map), + next: SatLink.fromJson(json['next'] as Map), + prev: SatLink.fromJson(json['prev'] as Map), + self: SatLink.fromJson(json['self'] as Map), ); } } @@ -77,6 +77,6 @@ class SatLink { SatLink({required this.href}); factory SatLink.fromJson(Map json) { - return SatLink(href: json['href']); + return SatLink(href: json['href'] as String); } } diff --git a/lib/services/ordinals_api.dart b/lib/services/ordinals_api.dart index a966988d7..264223f8a 100644 --- a/lib/services/ordinals_api.dart +++ b/lib/services/ordinals_api.dart @@ -3,6 +3,7 @@ import 'package:http/http.dart' as http; import 'package:stackwallet/dto/ordinals/feed_response.dart'; import 'package:stackwallet/dto/ordinals/inscription_response.dart'; +import 'package:stackwallet/dto/ordinals/sat_response.dart'; class OrdinalsAPI { final String baseUrl; @@ -36,4 +37,9 @@ class OrdinalsAPI { final response = await _getResponse('/inscription/$inscriptionId'); return InscriptionResponse.fromJson(response); } + + Future getSatDetails(int satNumber) async { + final response = await _getResponse('/sat/$satNumber'); + return SatResponse.fromJson(response); + } } From 48e4f8c57790bd790fd194069f55f9e94d59e169 Mon Sep 17 00:00:00 2001 From: sneurlax Date: Wed, 19 Jul 2023 10:45:18 -0500 Subject: [PATCH 026/169] add output endpoint --- .../{output_response => output_response.dart} | 16 +++++++++------- lib/services/ordinals_api.dart | 12 ++++++++++++ 2 files changed, 21 insertions(+), 7 deletions(-) rename lib/dto/ordinals/{output_response => output_response.dart} (57%) diff --git a/lib/dto/ordinals/output_response b/lib/dto/ordinals/output_response.dart similarity index 57% rename from lib/dto/ordinals/output_response rename to lib/dto/ordinals/output_response.dart index cd25fe961..6faf40845 100644 --- a/lib/dto/ordinals/output_response +++ b/lib/dto/ordinals/output_response.dart @@ -1,3 +1,5 @@ +import 'package:stackwallet/dto/ordinals/transaction_response.dart'; + class OutputResponse { final OutputLinks links; final String address; @@ -15,11 +17,11 @@ class OutputResponse { factory OutputResponse.fromJson(Map json) { return OutputResponse( - links: OutputLinks.fromJson(json['_links']), - address: json['address'], - scriptPubkey: json['script_pubkey'], - transaction: json['transaction'], - value: json['value'], + links: OutputLinks.fromJson(json['_links'] as Map), + address: json['address'] as String, + scriptPubkey: json['script_pubkey'] as String, + transaction: json['transaction'] as String, + value: json['value'] as int, ); } } @@ -35,8 +37,8 @@ class OutputLinks { factory OutputLinks.fromJson(Map json) { return OutputLinks( - self: OutputLink.fromJson(json['self']), - transaction: TransactionLink.fromJson(json['transaction']), + self: OutputLink.fromJson(json['self'] as Map), + transaction: TransactionLink.fromJson(json['transaction'] as Map), ); } } \ No newline at end of file diff --git a/lib/services/ordinals_api.dart b/lib/services/ordinals_api.dart index 264223f8a..e83e9eb9e 100644 --- a/lib/services/ordinals_api.dart +++ b/lib/services/ordinals_api.dart @@ -4,6 +4,8 @@ import 'package:http/http.dart' as http; import 'package:stackwallet/dto/ordinals/feed_response.dart'; import 'package:stackwallet/dto/ordinals/inscription_response.dart'; import 'package:stackwallet/dto/ordinals/sat_response.dart'; +import 'package:stackwallet/dto/ordinals/transaction_response.dart'; +import 'package:stackwallet/dto/ordinals/output_response.dart'; class OrdinalsAPI { final String baseUrl; @@ -42,4 +44,14 @@ class OrdinalsAPI { final response = await _getResponse('/sat/$satNumber'); return SatResponse.fromJson(response); } + + Future getTransaction(String transactionId) async { + final response = await _getResponse('/tx/$transactionId'); + return TransactionResponse.fromJson(response); + } + + Future getTransactionOutputs(String transactionId) async { + final response = await _getResponse('/output/$transactionId'); + return OutputResponse.fromJson(response); + } } From f972c34dc4eb0a7f19627da3b4ecd0e375bce8a9 Mon Sep 17 00:00:00 2001 From: sneurlax Date: Wed, 19 Jul 2023 10:48:54 -0500 Subject: [PATCH 027/169] add address endpoint --- lib/dto/ordinals/address_response.dart | 23 ++++++++++++++++++----- lib/dto/ordinals/output_response.dart | 2 +- lib/services/ordinals_api.dart | 6 ++++++ 3 files changed, 25 insertions(+), 6 deletions(-) diff --git a/lib/dto/ordinals/address_response.dart b/lib/dto/ordinals/address_response.dart index 024543b92..7932d8ec7 100644 --- a/lib/dto/ordinals/address_response.dart +++ b/lib/dto/ordinals/address_response.dart @@ -1,3 +1,5 @@ +import 'package:stackwallet/dto/ordinals/inscription_response.dart'; + class AddressResponse { final AddressLinks links; final String address; @@ -12,12 +14,12 @@ class AddressResponse { factory AddressResponse.fromJson(Map json) { final inscriptionsJson = json['inscriptions'] as List; final inscriptions = inscriptionsJson - .map((inscriptionJson) => InscriptionLink.fromJson(inscriptionJson)) + .map((inscriptionJson) => InscriptionLink.fromJson(inscriptionJson as Map)) .toList(); return AddressResponse( - links: AddressLinks.fromJson(json['_links']), - address: json['address'], + links: AddressLinks.fromJson(json['_links'] as Map), + address: json['address'] as String, inscriptions: inscriptions, ); } @@ -32,7 +34,7 @@ class AddressLinks { factory AddressLinks.fromJson(Map json) { return AddressLinks( - self: AddressLink.fromJson(json['self']), + self: AddressLink.fromJson(json['self'] as Map), ); } } @@ -43,6 +45,17 @@ class AddressLink { AddressLink({required this.href}); factory AddressLink.fromJson(Map json) { - return AddressLink(href: json['href']); + return AddressLink(href: json['href'] as String); + } +} + +class InscriptionLink { + final String href; + final String title; + + InscriptionLink(this.href, this.title); + + factory InscriptionLink.fromJson(Map json) { + return InscriptionLink(json['href'] as String, json['title'] as String); } } diff --git a/lib/dto/ordinals/output_response.dart b/lib/dto/ordinals/output_response.dart index 6faf40845..cd4bde563 100644 --- a/lib/dto/ordinals/output_response.dart +++ b/lib/dto/ordinals/output_response.dart @@ -41,4 +41,4 @@ class OutputLinks { transaction: TransactionLink.fromJson(json['transaction'] as Map), ); } -} \ No newline at end of file +} diff --git a/lib/services/ordinals_api.dart b/lib/services/ordinals_api.dart index e83e9eb9e..a0586d1ec 100644 --- a/lib/services/ordinals_api.dart +++ b/lib/services/ordinals_api.dart @@ -6,6 +6,7 @@ import 'package:stackwallet/dto/ordinals/inscription_response.dart'; import 'package:stackwallet/dto/ordinals/sat_response.dart'; import 'package:stackwallet/dto/ordinals/transaction_response.dart'; import 'package:stackwallet/dto/ordinals/output_response.dart'; +import 'package:stackwallet/dto/ordinals/address_response.dart'; class OrdinalsAPI { final String baseUrl; @@ -54,4 +55,9 @@ class OrdinalsAPI { final response = await _getResponse('/output/$transactionId'); return OutputResponse.fromJson(response); } + + Future getInscriptionsByAddress(String address) async { + final response = await _getResponse('/address/$address'); + return AddressResponse.fromJson(response); + } } From 6d772b0acdabdd19f0ed66663528a9a2912eb165 Mon Sep 17 00:00:00 2001 From: sneurlax Date: Wed, 19 Jul 2023 10:50:56 -0500 Subject: [PATCH 028/169] add block endpoint --- lib/dto/ordinals/block_response.dart | 20 ++++++++++---------- lib/services/ordinals_api.dart | 6 ++++++ 2 files changed, 16 insertions(+), 10 deletions(-) diff --git a/lib/dto/ordinals/block_response.dart b/lib/dto/ordinals/block_response.dart index 365098594..a3de1ee1c 100644 --- a/lib/dto/ordinals/block_response.dart +++ b/lib/dto/ordinals/block_response.dart @@ -19,13 +19,13 @@ class BlockResponse { factory BlockResponse.fromJson(Map json) { return BlockResponse( - links: BlockLinks.fromJson(json['_links']), - hash: json['hash'], - previousBlockhash: json['previous_blockhash'], - size: json['size'], - target: json['target'], - timestamp: json['timestamp'], - weight: json['weight'], + links: BlockLinks.fromJson(json['_links'] as Map), + hash: json['hash'] as String, + previousBlockhash: json['previous_blockhash'] as String, + size: json['size'] as int, + target: json['target'] as String, + timestamp: json['timestamp'] as String, + weight: json['weight'] as int, ); } } @@ -41,8 +41,8 @@ class BlockLinks { factory BlockLinks.fromJson(Map json) { return BlockLinks( - prev: BlockLink.fromJson(json['prev']), - self: BlockLink.fromJson(json['self']), + prev: BlockLink.fromJson(json['prev'] as Map), + self: BlockLink.fromJson(json['self'] as Map), ); } } @@ -53,6 +53,6 @@ class BlockLink { BlockLink({required this.href}); factory BlockLink.fromJson(Map json) { - return BlockLink(href: json['href']); + return BlockLink(href: json['href'] as String); } } diff --git a/lib/services/ordinals_api.dart b/lib/services/ordinals_api.dart index a0586d1ec..de9740554 100644 --- a/lib/services/ordinals_api.dart +++ b/lib/services/ordinals_api.dart @@ -7,6 +7,7 @@ import 'package:stackwallet/dto/ordinals/sat_response.dart'; import 'package:stackwallet/dto/ordinals/transaction_response.dart'; import 'package:stackwallet/dto/ordinals/output_response.dart'; import 'package:stackwallet/dto/ordinals/address_response.dart'; +import 'package:stackwallet/dto/ordinals/block_response.dart'; class OrdinalsAPI { final String baseUrl; @@ -60,4 +61,9 @@ class OrdinalsAPI { final response = await _getResponse('/address/$address'); return AddressResponse.fromJson(response); } + + Future getBlock(int blockNumber) async { + final response = await _getResponse('/block/$blockNumber'); + return BlockResponse.fromJson(response); + } } From d623480a758ee0a1acdfdc707ea503be03f314d2 Mon Sep 17 00:00:00 2001 From: sneurlax Date: Wed, 19 Jul 2023 10:51:46 -0500 Subject: [PATCH 029/169] add content endpoint --- lib/dto/ordinals/content_response.dart | 4 ++-- lib/services/ordinals_api.dart | 6 ++++++ 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/lib/dto/ordinals/content_response.dart b/lib/dto/ordinals/content_response.dart index 35a2c8ef8..d1131950b 100644 --- a/lib/dto/ordinals/content_response.dart +++ b/lib/dto/ordinals/content_response.dart @@ -4,7 +4,7 @@ class ContentResponse { ContentResponse({required this.fileLink}); factory ContentResponse.fromJson(Map json) { - return ContentResponse(fileLink: FileLink.fromJson(json['_links']['file'])); + return ContentResponse(fileLink: FileLink.fromJson(json['_links']['file'] as Map)); } } @@ -14,6 +14,6 @@ class FileLink { FileLink({required this.href}); factory FileLink.fromJson(Map json) { - return FileLink(href: json['href']); + return FileLink(href: json['href'] as String); } } diff --git a/lib/services/ordinals_api.dart b/lib/services/ordinals_api.dart index de9740554..4228ddd72 100644 --- a/lib/services/ordinals_api.dart +++ b/lib/services/ordinals_api.dart @@ -8,6 +8,7 @@ import 'package:stackwallet/dto/ordinals/transaction_response.dart'; import 'package:stackwallet/dto/ordinals/output_response.dart'; import 'package:stackwallet/dto/ordinals/address_response.dart'; import 'package:stackwallet/dto/ordinals/block_response.dart'; +import 'package:stackwallet/dto/ordinals/content_response.dart'; class OrdinalsAPI { final String baseUrl; @@ -66,4 +67,9 @@ class OrdinalsAPI { final response = await _getResponse('/block/$blockNumber'); return BlockResponse.fromJson(response); } + + Future getInscriptionContent(String inscriptionId) async { + final response = await _getResponse('/content/$inscriptionId'); + return ContentResponse.fromJson(response); + } } From 697f40fce1835e1e6cd494fb6a99a02bf319b11d Mon Sep 17 00:00:00 2001 From: sneurlax Date: Wed, 19 Jul 2023 10:55:46 -0500 Subject: [PATCH 030/169] add preview endpoint --- lib/dto/ordinals/preview_response.dart | 4 ++-- lib/services/ordinals_api.dart | 6 ++++++ 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/lib/dto/ordinals/preview_response.dart b/lib/dto/ordinals/preview_response.dart index d7171702d..9eabd590a 100644 --- a/lib/dto/ordinals/preview_response.dart +++ b/lib/dto/ordinals/preview_response.dart @@ -4,7 +4,7 @@ class PreviewResponse { PreviewResponse({required this.imageLink}); factory PreviewResponse.fromJson(Map json) { - return PreviewResponse(imageLink: ImageLink.fromJson(json['_links']['image'])); + return PreviewResponse(imageLink: ImageLink.fromJson(json['_links']['image'] as Map)); } } @@ -14,6 +14,6 @@ class ImageLink { ImageLink({required this.href}); factory ImageLink.fromJson(Map json) { - return ImageLink(href: json['href']); + return ImageLink(href: json['href'] as String); } } \ No newline at end of file diff --git a/lib/services/ordinals_api.dart b/lib/services/ordinals_api.dart index 4228ddd72..4d8360e06 100644 --- a/lib/services/ordinals_api.dart +++ b/lib/services/ordinals_api.dart @@ -9,6 +9,7 @@ import 'package:stackwallet/dto/ordinals/output_response.dart'; import 'package:stackwallet/dto/ordinals/address_response.dart'; import 'package:stackwallet/dto/ordinals/block_response.dart'; import 'package:stackwallet/dto/ordinals/content_response.dart'; +import 'package:stackwallet/dto/ordinals/preview_response.dart'; class OrdinalsAPI { final String baseUrl; @@ -72,4 +73,9 @@ class OrdinalsAPI { final response = await _getResponse('/content/$inscriptionId'); return ContentResponse.fromJson(response); } + + Future getInscriptionPreview(String inscriptionId) async { + final response = await _getResponse('/preview/$inscriptionId'); + return PreviewResponse.fromJson(response); + } } From afbf818ab5815b07481bab41e1c1c27ffa704751 Mon Sep 17 00:00:00 2001 From: sneurlax Date: Wed, 19 Jul 2023 11:04:19 -0500 Subject: [PATCH 031/169] refactor InscriptionLink factory, casting TODO replace casting throughout ordinals DTOs with refactored validation --- lib/dto/ordinals/address_response.dart | 13 +----------- lib/dto/ordinals/feed_response.dart | 16 ++------------- lib/dto/ordinals/inscription_link.dart | 13 ++++++++++++ lib/dto/ordinals/transaction_response.dart | 24 ++++++++++++---------- 4 files changed, 29 insertions(+), 37 deletions(-) create mode 100644 lib/dto/ordinals/inscription_link.dart diff --git a/lib/dto/ordinals/address_response.dart b/lib/dto/ordinals/address_response.dart index 7932d8ec7..5c1f66323 100644 --- a/lib/dto/ordinals/address_response.dart +++ b/lib/dto/ordinals/address_response.dart @@ -1,4 +1,4 @@ -import 'package:stackwallet/dto/ordinals/inscription_response.dart'; +import 'package:stackwallet/dto/ordinals/inscription_link.dart'; class AddressResponse { final AddressLinks links; @@ -48,14 +48,3 @@ class AddressLink { return AddressLink(href: json['href'] as String); } } - -class InscriptionLink { - final String href; - final String title; - - InscriptionLink(this.href, this.title); - - factory InscriptionLink.fromJson(Map json) { - return InscriptionLink(json['href'] as String, json['title'] as String); - } -} diff --git a/lib/dto/ordinals/feed_response.dart b/lib/dto/ordinals/feed_response.dart index d958fbe4c..e29d2d334 100644 --- a/lib/dto/ordinals/feed_response.dart +++ b/lib/dto/ordinals/feed_response.dart @@ -1,3 +1,5 @@ +import 'package:stackwallet/dto/ordinals/inscription_link.dart'; + class FeedResponse { final List inscriptions; @@ -12,17 +14,3 @@ class FeedResponse { return FeedResponse(inscriptions: inscriptions); } } - -class InscriptionLink { - final String href; - final String title; - - InscriptionLink({required this.href, required this.title}); - - factory InscriptionLink.fromJson(Map json) { - return InscriptionLink( - href: json['href'] as String ?? '', - title: json['title'] as String ?? '', - ); - } -} diff --git a/lib/dto/ordinals/inscription_link.dart b/lib/dto/ordinals/inscription_link.dart new file mode 100644 index 000000000..f23b63248 --- /dev/null +++ b/lib/dto/ordinals/inscription_link.dart @@ -0,0 +1,13 @@ +class InscriptionLink { + final String href; + final String title; + + InscriptionLink({required this.href, required this.title}); + + factory InscriptionLink.fromJson(Map json) { + return InscriptionLink( + href: json['href'] as String ?? '', + title: json['title'] as String ?? '', + ); + } +} diff --git a/lib/dto/ordinals/transaction_response.dart b/lib/dto/ordinals/transaction_response.dart index b030a1daa..11912a3ad 100644 --- a/lib/dto/ordinals/transaction_response.dart +++ b/lib/dto/ordinals/transaction_response.dart @@ -1,3 +1,5 @@ +import 'package:stackwallet/dto/ordinals/inscription_link.dart'; + class TransactionResponse { final TransactionLinks links; final List inputs; @@ -18,21 +20,21 @@ class TransactionResponse { factory TransactionResponse.fromJson(Map json) { final inputsJson = json['_links']['inputs'] as List; final inputs = inputsJson - .map((inputJson) => OutputLink.fromJson(inputJson)) + .map((inputJson) => OutputLink.fromJson(inputJson as Map)) .toList(); final outputsJson = json['_links']['outputs'] as List; final outputs = outputsJson - .map((outputJson) => OutputLink.fromJson(outputJson)) + .map((outputJson) => OutputLink.fromJson(outputJson as Map)) .toList(); return TransactionResponse( - links: TransactionLinks.fromJson(json['_links']), + links: TransactionLinks.fromJson(json['_links'] as Map), inputs: inputs, - inscription: InscriptionLink.fromJson(json['_links']['inscription']), + inscription: InscriptionLink.fromJson(json['_links']['inscription'] as Map), outputs: outputs, - self: TransactionLink.fromJson(json['_links']['self']), - transaction: json['transaction'], + self: TransactionLink.fromJson(json['_links']['self'] as Map), + transaction: json['transaction'] as String, ); } } @@ -50,9 +52,9 @@ class TransactionLinks { factory TransactionLinks.fromJson(Map json) { return TransactionLinks( - block: TransactionLink.fromJson(json['block']), - inscription: InscriptionLink.fromJson(json['inscription']), - self: TransactionLink.fromJson(json['self']), + block: TransactionLink.fromJson(json['block'] as Map), + inscription: InscriptionLink.fromJson(json['inscription'] as Map), + self: TransactionLink.fromJson(json['self'] as Map), ); } } @@ -63,7 +65,7 @@ class TransactionLink { TransactionLink({required this.href}); factory TransactionLink.fromJson(Map json) { - return TransactionLink(href: json['href']); + return TransactionLink(href: json['href'] as String); } } @@ -73,6 +75,6 @@ class OutputLink { OutputLink({required this.href}); factory OutputLink.fromJson(Map json) { - return OutputLink(href: json['href']); + return OutputLink(href: json['href'] as String); } } From e47e861273ba55578c0ce1c5eca32fe54540e494 Mon Sep 17 00:00:00 2001 From: julian Date: Wed, 19 Jul 2023 10:22:18 -0600 Subject: [PATCH 032/169] delete unused file --- test/widget_test.dart | 30 ------------------------------ 1 file changed, 30 deletions(-) delete mode 100644 test/widget_test.dart diff --git a/test/widget_test.dart b/test/widget_test.dart deleted file mode 100644 index 81dacd980..000000000 --- a/test/widget_test.dart +++ /dev/null @@ -1,30 +0,0 @@ -// This is a basic Flutter widget test. -// -// To perform an interaction with a widget in your test, use the WidgetTester -// utility in the flutter_test package. For example, you can send tap and scroll -// gestures. You can also use WidgetTester to find child widgets in the widget -// tree, read text, and verify that the values of widget properties are correct. - -import 'package:flutter/material.dart'; -import 'package:flutter_test/flutter_test.dart'; - -import 'package:stack_wallet/main.dart'; - -void main() { - testWidgets('Counter increments smoke test', (WidgetTester tester) async { - // Build our app and trigger a frame. - await tester.pumpWidget(const MyApp()); - - // Verify that our counter starts at 0. - expect(find.text('0'), findsOneWidget); - expect(find.text('1'), findsNothing); - - // Tap the '+' icon and trigger a frame. - await tester.tap(find.byIcon(Icons.add)); - await tester.pump(); - - // Verify that our counter has incremented. - expect(find.text('0'), findsNothing); - expect(find.text('1'), findsOneWidget); - }); -} From db1e34d745e2b6036303dc8a0eb0679cae01ce80 Mon Sep 17 00:00:00 2001 From: Diego Salazar Date: Wed, 19 Jul 2023 11:30:50 -0600 Subject: [PATCH 033/169] Bump version (1.7.16, build 182) --- pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index fd5eac1dc..3621d787c 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.7.15+181 +version: 1.7.16+182 environment: sdk: ">=3.0.2 <4.0.0" From 8e17556e4128f00f915460f19eb7464c5ae54e16 Mon Sep 17 00:00:00 2001 From: sneurlax Date: Wed, 19 Jul 2023 13:03:17 -0500 Subject: [PATCH 034/169] use an OrdinalsResponse response type which can return an error --- lib/dto/ordinals/address_response.dart | 14 +++-- lib/dto/ordinals/block_response.dart | 25 +++++---- lib/dto/ordinals/content_response.dart | 9 ++- lib/dto/ordinals/feed_response.dart | 11 ++-- lib/dto/ordinals/inscription_response.dart | 61 ++++++++++++--------- lib/dto/ordinals/ordinals_response.dart | 6 ++ lib/dto/ordinals/output_response.dart | 21 ++++--- lib/dto/ordinals/preview_response.dart | 11 ++-- lib/dto/ordinals/sat_response.dart | 41 +++++++------- lib/dto/ordinals/transaction_response.dart | 24 ++++---- lib/services/mixins/ordinals_interface.dart | 18 +++++- lib/services/ordinals_api.dart | 5 +- 12 files changed, 148 insertions(+), 98 deletions(-) create mode 100644 lib/dto/ordinals/ordinals_response.dart diff --git a/lib/dto/ordinals/address_response.dart b/lib/dto/ordinals/address_response.dart index 5c1f66323..9136aa523 100644 --- a/lib/dto/ordinals/address_response.dart +++ b/lib/dto/ordinals/address_response.dart @@ -1,6 +1,7 @@ import 'package:stackwallet/dto/ordinals/inscription_link.dart'; +import 'package:stackwallet/dto/ordinals/ordinals_response.dart'; -class AddressResponse { +class AddressResponse extends OrdinalsResponse { final AddressLinks links; final String address; final List inscriptions; @@ -11,15 +12,16 @@ class AddressResponse { required this.inscriptions, }); - factory AddressResponse.fromJson(Map json) { - final inscriptionsJson = json['inscriptions'] as List; + factory AddressResponse.fromJson(OrdinalsResponse json) { + final data = json.data as Map; + final inscriptionsJson = data['inscriptions'] as List; final inscriptions = inscriptionsJson .map((inscriptionJson) => InscriptionLink.fromJson(inscriptionJson as Map)) .toList(); return AddressResponse( - links: AddressLinks.fromJson(json['_links'] as Map), - address: json['address'] as String, + links: AddressLinks.fromJson(data['_links'] as Map), + address: data['address'] as String, inscriptions: inscriptions, ); } @@ -34,7 +36,7 @@ class AddressLinks { factory AddressLinks.fromJson(Map json) { return AddressLinks( - self: AddressLink.fromJson(json['self'] as Map), + self: json['self'] != null ? AddressLink.fromJson(json['self'] as Map) : null, ); } } diff --git a/lib/dto/ordinals/block_response.dart b/lib/dto/ordinals/block_response.dart index a3de1ee1c..0eef8d569 100644 --- a/lib/dto/ordinals/block_response.dart +++ b/lib/dto/ordinals/block_response.dart @@ -1,4 +1,6 @@ -class BlockResponse { +import 'package:stackwallet/dto/ordinals/ordinals_response.dart'; + +class BlockResponse extends OrdinalsResponse { final BlockLinks links; final String hash; final String previousBlockhash; @@ -17,15 +19,16 @@ class BlockResponse { required this.weight, }); - factory BlockResponse.fromJson(Map json) { + factory BlockResponse.fromJson(OrdinalsResponse json) { + final data = json.data as Map; return BlockResponse( - links: BlockLinks.fromJson(json['_links'] as Map), - hash: json['hash'] as String, - previousBlockhash: json['previous_blockhash'] as String, - size: json['size'] as int, - target: json['target'] as String, - timestamp: json['timestamp'] as String, - weight: json['weight'] as int, + links: BlockLinks.fromJson(data['_links'] as Map), + hash: data['hash'] as String, + previousBlockhash: data['previous_blockhash'] as String, + size: data['size'] as int, + target: data['target'] as String, + timestamp: data['timestamp'] as String, + weight: data['weight'] as int, ); } } @@ -41,8 +44,8 @@ class BlockLinks { factory BlockLinks.fromJson(Map json) { return BlockLinks( - prev: BlockLink.fromJson(json['prev'] as Map), - self: BlockLink.fromJson(json['self'] as Map), + prev: json['prev'] != null ? BlockLink.fromJson(json['prev'] as Map) : null, + self: json['self'] != null ? BlockLink.fromJson(json['self'] as Map) : null, ); } } diff --git a/lib/dto/ordinals/content_response.dart b/lib/dto/ordinals/content_response.dart index d1131950b..7cfbaf9fd 100644 --- a/lib/dto/ordinals/content_response.dart +++ b/lib/dto/ordinals/content_response.dart @@ -1,10 +1,13 @@ -class ContentResponse { +import 'package:stackwallet/dto/ordinals/ordinals_response.dart'; + +class ContentResponse extends OrdinalsResponse { final FileLink fileLink; ContentResponse({required this.fileLink}); - factory ContentResponse.fromJson(Map json) { - return ContentResponse(fileLink: FileLink.fromJson(json['_links']['file'] as Map)); + factory ContentResponse.fromJson(OrdinalsResponse json) { + final data = json.data as Map; + return ContentResponse(fileLink: FileLink.fromJson(data['_links']['file'] as Map)); // TODO don't cast as Map } } diff --git a/lib/dto/ordinals/feed_response.dart b/lib/dto/ordinals/feed_response.dart index e29d2d334..525a7f727 100644 --- a/lib/dto/ordinals/feed_response.dart +++ b/lib/dto/ordinals/feed_response.dart @@ -1,16 +1,17 @@ import 'package:stackwallet/dto/ordinals/inscription_link.dart'; +import 'package:stackwallet/dto/ordinals/ordinals_response.dart'; -class FeedResponse { +class FeedResponse extends OrdinalsResponse { final List inscriptions; FeedResponse({required this.inscriptions}); - - factory FeedResponse.fromJson(Map json) { - final List inscriptionsJson = json['_links']['inscriptions'] as List; + + factory FeedResponse.fromJson(OrdinalsResponse json) { + final List inscriptionsJson = json.data['_links']['inscriptions'] as List; final List inscriptions = inscriptionsJson .map((json) => InscriptionLink.fromJson(json as Map)) .toList(); return FeedResponse(inscriptions: inscriptions); } -} +} \ No newline at end of file diff --git a/lib/dto/ordinals/inscription_response.dart b/lib/dto/ordinals/inscription_response.dart index ab093b750..d45300aee 100644 --- a/lib/dto/ordinals/inscription_response.dart +++ b/lib/dto/ordinals/inscription_response.dart @@ -1,4 +1,6 @@ -class InscriptionResponse { +import 'package:stackwallet/dto/ordinals/ordinals_response.dart'; + +class InscriptionResponse extends OrdinalsResponse { late final Links links; late final String address; late final int contentLength; @@ -29,20 +31,23 @@ class InscriptionResponse { required this.timestamp, }); - InscriptionResponse.fromJson(Map json) { - links = Links.fromJson(json['_links'] as Map); - address = json['address'] as String; - contentLength = json['content_length'] as int; - contentType = json['content_type'] as String; - genesisFee = json['genesis_fee'] as int; - genesisHeight = json['genesis_height'] as int; - genesisTransaction = json['genesis_transaction'] as String; - location = json['location'] as String; - number = json['number'] as int; - offset = json['offset'] as int; - output = json['output'] as String; - sat = json['sat'] as String?; - timestamp = json['timestamp'] as String; + factory InscriptionResponse.fromJson(OrdinalsResponse json) { + final data = json.data as Map; + return InscriptionResponse( + links: Links.fromJson(data['_links'] as Map), + address: data['address'] as String, + contentLength: data['content_length'] as int, + contentType: data['content_type'] as String, + genesisFee: data['genesis_fee'] as int, + genesisHeight: data['genesis_height'] as int, + genesisTransaction: data['genesis_transaction'] as String, + location: data['location'] as String, + number: data['number'] as int, + offset: data['offset'] as int, + output: data['output'] as String, + sat: data['sat'] as String?, + timestamp: data['timestamp'] as String, + ); } } @@ -67,15 +72,17 @@ class Links { required this.self, }); - Links.fromJson(Map json) { - content = Link.fromJson(json['content'] as Map); - genesisTransaction = Link.fromJson(json['genesis_transaction'] as Map); - next = Link.fromJson(json['next'] as Map); - output = Link.fromJson(json['output'] as Map); - prev = Link.fromJson(json['prev'] as Map); - preview = Link.fromJson(json['preview'] as Map); - sat = json['sat'] != null ? Link.fromJson(json['sat'] as Map) : null; - self = Link.fromJson(json['self'] as Map); + factory Links.fromJson(Map json) { + return Links( + content: Link.fromJson(json['content'] as Map), + genesisTransaction: Link.fromJson(json['genesis_transaction'] as Map), + next: Link.fromJson(json['next'] as Map), + output: Link.fromJson(json['output'] as Map), + prev: Link.fromJson(json['prev'] as Map), + preview: Link.fromJson(json['preview'] as Map), + sat: json['sat'] != null ? Link.fromJson(json['sat'] as Map) : null, + self: Link.fromJson(json['self'] as Map), + ); } } @@ -84,7 +91,7 @@ class Link { Link({required this.href}); - Link.fromJson(Map json) { - href = json['href'] as String; + factory Link.fromJson(Map json) { + return Link(href: json['href'] as String); } -} \ No newline at end of file +} diff --git a/lib/dto/ordinals/ordinals_response.dart b/lib/dto/ordinals/ordinals_response.dart new file mode 100644 index 000000000..bf57db46b --- /dev/null +++ b/lib/dto/ordinals/ordinals_response.dart @@ -0,0 +1,6 @@ +class OrdinalsResponse { + final T? data; + final String? error; + + OrdinalsResponse({this.data, this.error}); +} diff --git a/lib/dto/ordinals/output_response.dart b/lib/dto/ordinals/output_response.dart index cd4bde563..cc7b2107f 100644 --- a/lib/dto/ordinals/output_response.dart +++ b/lib/dto/ordinals/output_response.dart @@ -1,6 +1,7 @@ import 'package:stackwallet/dto/ordinals/transaction_response.dart'; +import 'package:stackwallet/dto/ordinals/ordinals_response.dart'; -class OutputResponse { +class OutputResponse extends OrdinalsResponse { final OutputLinks links; final String address; final String scriptPubkey; @@ -15,13 +16,15 @@ class OutputResponse { required this.value, }); - factory OutputResponse.fromJson(Map json) { + factory OutputResponse.fromJson(OrdinalsResponse json) { + final data = json.data as Map; + return OutputResponse( - links: OutputLinks.fromJson(json['_links'] as Map), - address: json['address'] as String, - scriptPubkey: json['script_pubkey'] as String, - transaction: json['transaction'] as String, - value: json['value'] as int, + links: OutputLinks.fromJson(data['_links'] as Map), + address: data['address'] as String, + scriptPubkey: data['script_pubkey'] as String, + transaction: data['transaction'] as String, + value: data['value'] as int, ); } } @@ -37,8 +40,8 @@ class OutputLinks { factory OutputLinks.fromJson(Map json) { return OutputLinks( - self: OutputLink.fromJson(json['self'] as Map), - transaction: TransactionLink.fromJson(json['transaction'] as Map), + self: json['self'] != null ? OutputLink.fromJson(json['self'] as Map) : null, + transaction: json['transaction'] != null ? TransactionLink.fromJson(json['transaction'] as Map) : null, ); } } diff --git a/lib/dto/ordinals/preview_response.dart b/lib/dto/ordinals/preview_response.dart index 9eabd590a..b3e184acd 100644 --- a/lib/dto/ordinals/preview_response.dart +++ b/lib/dto/ordinals/preview_response.dart @@ -1,10 +1,13 @@ -class PreviewResponse { +import 'package:stackwallet/dto/ordinals/ordinals_response.dart'; + +class PreviewResponse extends OrdinalsResponse { final ImageLink imageLink; PreviewResponse({required this.imageLink}); - factory PreviewResponse.fromJson(Map json) { - return PreviewResponse(imageLink: ImageLink.fromJson(json['_links']['image'] as Map)); + factory PreviewResponse.fromJson(OrdinalsResponse json) { + final data = json.data as Map; + return PreviewResponse(imageLink: ImageLink.fromJson(data['_links']['image'] as Map)); } } @@ -16,4 +19,4 @@ class ImageLink { factory ImageLink.fromJson(Map json) { return ImageLink(href: json['href'] as String); } -} \ No newline at end of file +} diff --git a/lib/dto/ordinals/sat_response.dart b/lib/dto/ordinals/sat_response.dart index b57ccb3cf..40efb1440 100644 --- a/lib/dto/ordinals/sat_response.dart +++ b/lib/dto/ordinals/sat_response.dart @@ -1,4 +1,6 @@ -class SatResponse { +import 'package:stackwallet/dto/ordinals/ordinals_response.dart'; + +class SatResponse extends OrdinalsResponse { final SatLinks links; final int block; final int cycle; @@ -27,20 +29,21 @@ class SatResponse { required this.timestamp, }); - factory SatResponse.fromJson(Map json) { + factory SatResponse.fromJson(OrdinalsResponse json) { + final data = json.data as Map; return SatResponse( - links: SatLinks.fromJson(json['_links'] as Map), - block: json['block'] as int, - cycle: json['cycle'] as int, - decimal: json['decimal'] as String, - degree: json['degree'] as String, - epoch: json['epoch'] as int, - name: json['name'] as String, - offset: json['offset'] as int, - percentile: json['percentile'] as String, - period: json['period'] as int, - rarity: json['rarity'] as String, - timestamp: json['timestamp'] as String, + links: SatLinks.fromJson(data['_links'] as Map), + block: data['block'] as int, + cycle: data['cycle'] as int, + decimal: data['decimal'] as String, + degree: data['degree'] as String, + epoch: data['epoch'] as int, + name: data['name'] as String, + offset: data['offset'] as int, + percentile: data['percentile'] as String, + period: data['period'] as int, + rarity: data['rarity'] as String, + timestamp: data['timestamp'] as String, ); } } @@ -62,11 +65,11 @@ class SatLinks { factory SatLinks.fromJson(Map json) { return SatLinks( - block: SatLink.fromJson(json['block'] as Map), - inscription: SatLink.fromJson(json['inscription'] as Map), - next: SatLink.fromJson(json['next'] as Map), - prev: SatLink.fromJson(json['prev'] as Map), - self: SatLink.fromJson(json['self'] as Map), + block: json['block'] != null ? SatLink.fromJson(json['block'] as Map) : null, + inscription: json['inscription'] != null ? SatLink.fromJson(json['inscription'] as Map) : null, + next: json['next'] != null ? SatLink.fromJson(json['next'] as Map) : null, + prev: json['prev'] != null ? SatLink.fromJson(json['prev'] as Map) : null, + self: json['self'] != null ? SatLink.fromJson(json['self'] as Map) : null, ); } } diff --git a/lib/dto/ordinals/transaction_response.dart b/lib/dto/ordinals/transaction_response.dart index 11912a3ad..c77e07914 100644 --- a/lib/dto/ordinals/transaction_response.dart +++ b/lib/dto/ordinals/transaction_response.dart @@ -1,6 +1,7 @@ import 'package:stackwallet/dto/ordinals/inscription_link.dart'; +import 'package:stackwallet/dto/ordinals/ordinals_response.dart'; -class TransactionResponse { +class TransactionResponse extends OrdinalsResponse { final TransactionLinks links; final List inputs; final InscriptionLink inscription; @@ -17,24 +18,25 @@ class TransactionResponse { required this.transaction, }); - factory TransactionResponse.fromJson(Map json) { - final inputsJson = json['_links']['inputs'] as List; + factory TransactionResponse.fromJson(OrdinalsResponse json) { + final data = json.data as Map; + final inputsJson = data['_links']['inputs'] as List; final inputs = inputsJson .map((inputJson) => OutputLink.fromJson(inputJson as Map)) .toList(); - final outputsJson = json['_links']['outputs'] as List; + final outputsJson = data['_links']['outputs'] as List; final outputs = outputsJson .map((outputJson) => OutputLink.fromJson(outputJson as Map)) .toList(); return TransactionResponse( - links: TransactionLinks.fromJson(json['_links'] as Map), + links: TransactionLinks.fromJson(data['_links'] as Map), inputs: inputs, - inscription: InscriptionLink.fromJson(json['_links']['inscription'] as Map), + inscription: InscriptionLink.fromJson(data['_links']['inscription'] as Map), outputs: outputs, - self: TransactionLink.fromJson(json['_links']['self'] as Map), - transaction: json['transaction'] as String, + self: TransactionLink.fromJson(data['_links']['self'] as Map), + transaction: data['transaction'] as String, ); } } @@ -52,9 +54,9 @@ class TransactionLinks { factory TransactionLinks.fromJson(Map json) { return TransactionLinks( - block: TransactionLink.fromJson(json['block'] as Map), - inscription: InscriptionLink.fromJson(json['inscription'] as Map), - self: TransactionLink.fromJson(json['self'] as Map), + block: json['block'] != null ? TransactionLink.fromJson(json['block'] as Map) : null, + inscription: json['inscription'] != null ? InscriptionLink.fromJson(json['inscription'] as Map) : null, + self: json['self'] != null ? TransactionLink.fromJson(json['self'] as Map) : null, ); } } diff --git a/lib/services/mixins/ordinals_interface.dart b/lib/services/mixins/ordinals_interface.dart index 06f2377e9..57a76fbd6 100644 --- a/lib/services/mixins/ordinals_interface.dart +++ b/lib/services/mixins/ordinals_interface.dart @@ -1,3 +1,19 @@ +import 'package:stackwallet/services/ordinals_api.dart'; +import 'package:stackwallet/dto/ordinals/feed_response.dart'; + mixin OrdinalsInterface { - // TODO wallet ordinals functionality + Future fetchLatestInscriptions(OrdinalsAPI ordinalsAPI) async { + try { + final feedResponse = await ordinalsAPI.getLatestInscriptions(); + // Process the feedResponse data as needed + print('Latest Inscriptions:'); + for (var inscription in feedResponse.inscriptions) { + print('Title: ${inscription.title}, Href: ${inscription.href}'); + } + return feedResponse; + } catch (e) { + // Handle errors + throw Exception('Error in OrdinalsInterface: $e'); + } + } } diff --git a/lib/services/ordinals_api.dart b/lib/services/ordinals_api.dart index 4d8360e06..e9d734203 100644 --- a/lib/services/ordinals_api.dart +++ b/lib/services/ordinals_api.dart @@ -1,6 +1,7 @@ import 'dart:convert'; import 'package:http/http.dart' as http; +import 'package:stackwallet/dto/ordinals/ordinals_response.dart'; import 'package:stackwallet/dto/ordinals/feed_response.dart'; import 'package:stackwallet/dto/ordinals/inscription_response.dart'; import 'package:stackwallet/dto/ordinals/sat_response.dart'; @@ -16,10 +17,10 @@ class OrdinalsAPI { OrdinalsAPI({required this.baseUrl}); - Future> _getResponse(String endpoint) async { + Future _getResponse(String endpoint) async { final response = await http.get(Uri.parse('$baseUrl$endpoint')); if (response.statusCode == 200) { - return _validateJson(response.body); + return OrdinalsResponse(data: _validateJson(response.body)); } else { throw Exception('Failed to load data'); } From 43d7f39f6f551c6178bb040c64b8c10ed64aeb38 Mon Sep 17 00:00:00 2001 From: julian-CStack Date: Wed, 19 Jul 2023 12:43:42 -0600 Subject: [PATCH 035/169] macos app icon --- assets/icon/macos-icon.png | Bin 0 -> 14912 bytes .../AppIcon.appiconset/Contents.json | 132 +++++++++--------- .../AppIcon.appiconset/app_icon_1024.png | Bin 102994 -> 63944 bytes .../AppIcon.appiconset/app_icon_128.png | Bin 5680 -> 4698 bytes .../AppIcon.appiconset/app_icon_16.png | Bin 520 -> 577 bytes .../AppIcon.appiconset/app_icon_256.png | Bin 14142 -> 8830 bytes .../AppIcon.appiconset/app_icon_32.png | Bin 1066 -> 1158 bytes .../AppIcon.appiconset/app_icon_512.png | Bin 36406 -> 16189 bytes .../AppIcon.appiconset/app_icon_64.png | Bin 2218 -> 2283 bytes pubspec.lock | 20 +-- pubspec.yaml | 6 +- 11 files changed, 79 insertions(+), 79 deletions(-) create mode 100644 assets/icon/macos-icon.png diff --git a/assets/icon/macos-icon.png b/assets/icon/macos-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..e4a26302216d12d5dfd46677e28f18746ead7e8b GIT binary patch literal 14912 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 QLJ_)W&S~XoTz&X|0N}vPSpWb4 literal 0 HcmV?d00001 diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json index a2ec33f19..96d3fee1a 100644 --- a/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json +++ b/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -1,68 +1,68 @@ { - "images" : [ - { - "size" : "16x16", - "idiom" : "mac", - "filename" : "app_icon_16.png", - "scale" : "1x" + "info": { + "version": 1, + "author": "xcode" }, - { - "size" : "16x16", - "idiom" : "mac", - "filename" : "app_icon_32.png", - "scale" : "2x" - }, - { - "size" : "32x32", - "idiom" : "mac", - "filename" : "app_icon_32.png", - "scale" : "1x" - }, - { - "size" : "32x32", - "idiom" : "mac", - "filename" : "app_icon_64.png", - "scale" : "2x" - }, - { - "size" : "128x128", - "idiom" : "mac", - "filename" : "app_icon_128.png", - "scale" : "1x" - }, - { - "size" : "128x128", - "idiom" : "mac", - "filename" : "app_icon_256.png", - "scale" : "2x" - }, - { - "size" : "256x256", - "idiom" : "mac", - "filename" : "app_icon_256.png", - "scale" : "1x" - }, - { - "size" : "256x256", - "idiom" : "mac", - "filename" : "app_icon_512.png", - "scale" : "2x" - }, - { - "size" : "512x512", - "idiom" : "mac", - "filename" : "app_icon_512.png", - "scale" : "1x" - }, - { - "size" : "512x512", - "idiom" : "mac", - "filename" : "app_icon_1024.png", - "scale" : "2x" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} + "images": [ + { + "size": "16x16", + "idiom": "mac", + "filename": "app_icon_16.png", + "scale": "1x" + }, + { + "size": "16x16", + "idiom": "mac", + "filename": "app_icon_32.png", + "scale": "2x" + }, + { + "size": "32x32", + "idiom": "mac", + "filename": "app_icon_32.png", + "scale": "1x" + }, + { + "size": "32x32", + "idiom": "mac", + "filename": "app_icon_64.png", + "scale": "2x" + }, + { + "size": "128x128", + "idiom": "mac", + "filename": "app_icon_128.png", + "scale": "1x" + }, + { + "size": "128x128", + "idiom": "mac", + "filename": "app_icon_256.png", + "scale": "2x" + }, + { + "size": "256x256", + "idiom": "mac", + "filename": "app_icon_256.png", + "scale": "1x" + }, + { + "size": "256x256", + "idiom": "mac", + "filename": "app_icon_512.png", + "scale": "2x" + }, + { + "size": "512x512", + "idiom": "mac", + "filename": "app_icon_512.png", + "scale": "1x" + }, + { + "size": "512x512", + "idiom": "mac", + "filename": "app_icon_1024.png", + "scale": "2x" + } + ] +} \ No newline at end of file diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png index 82b6f9d9a33e198f5747104729e1fcef999772a5..65564e63bdf156cffeafd776cb0c9c1a8d59ade6 100644 GIT binary patch literal 63944 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 literal 102994 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ⅅ8MglG&ER{i5lN315cO?EdHNLrg? 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 z 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 Pe7HEdAEg|=c=LY&YVNkY diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png index 13b35eba55c6dabc3aac36f33d859266c18fa0d0..9987503582cb9ca958259ed581224af17b9ced78 100644 GIT binary patch literal 4698 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 iHw5(R?f*GE476 literal 5680 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 diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png index 0a3f5fa40fb3d1e0710331a48de5d256da3f275d..7d6531e398abfef5338bbc839b4226dc41667885 100644 GIT binary patch delta 553 zcmV+^0@nSA1i=K5BYy%2NklkGorKMFQDk{34paEaLd=cEbb!*MRg9mdI6ckv28W}%-{%im= zY_FJ@*hV0~n1_ew9)93=(_#^ndAp6&02LY;0_dKrZ8} zSFZwIzkcnZq@>gb6mMr`W&Hv+;M}=$`o+b?3pqJC^<-sb8DWP0`0<1B(4j;Bfwuma zl$2xuh65uo);NF$d5VjRZxj*|It4bMudlBNXuCd;`Cm>>?w^H)1*3p~03$RI8PA?Q z%OET){9jK`?|(f|u3TPTel9Ff!3G!@7@S|abZH#OnU5YlVtn%C$$uRko&QEgMhqb5 z1HJcu|Ni|9{r&yCv9YlRt*xzszyN&@83VhOP4M&+`fGq z?EE`-?l64(_z~nHmf5psXRTVbDq`izm7(tL?)$(7uz$0&|6aCiS$t}0>g@dd{7IKD zUp7Q`J~EJzk$KqF)m7r==C%(b8XX-Sj{sd_0<u8N9^ zE?_i%$C(aTSXlmLW@gR~4-a46*w|P*efsq5z`($@ot>Q(z;yALU_yccU=)3rIB{Yz r(2xdTj3Ru-rH)`;z^9og0|)^C8*SEWc{23800000NkvXXu0mjfRigHb>FZqno^zN^3(+@_@XjP1uC>hJC?a{y* zD{|!jx7m$}htAgXd~cj3+ZrK}6l!2{cx$4>2i}b0%U|nx*4NknNi1N>>I*rxRx*XF zjWIp$e|p7N*%gv6a^xe`+J2V*GqU;lt>1BWf2ShLW$77g5go>ce}0@lm7sd+gu(Y? z@okUe{~JYoFgC6@om9@LBk=#Q%~7_H=W1tX8-^I diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png index bdb57226d5f2bd20f11934f4903f16459cf52379..e81fc06dcae8252b50dc2c07cddcda4a6cd02199 100644 GIT binary patch literal 8830 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 diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png index 2f1632cfddf3d9dade342351e627a0a75609fb46..1f195abc4a416bc8a9cd1ea3ed40c68922ef3ee5 100644 GIT binary patch delta 2272 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 diff --git a/pubspec.lock b/pubspec.lock index 1ad96c7a7..e260cb994 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -226,10 +226,10 @@ packages: dependency: transitive description: name: cli_util - sha256: "66f86e916d285c1a93d3b79587d94bd71984a66aac4ff74e524cfa7877f1395c" + sha256: b8db3080e59b2503ca9e7922c3df2072cf13992354d5e944074ffa836fba43b7 url: "https://pub.dev" source: hosted - version: "0.3.5" + version: "0.4.0" clock: dependency: transitive description: @@ -584,10 +584,10 @@ packages: dependency: "direct dev" description: name: flutter_launcher_icons - sha256: ce0e501cfc258907842238e4ca605e74b7fd1cdf04b3b43e86c43f3e40a1592c + sha256: "526faf84284b86a4cb36d20a5e45147747b7563d921373d4ee0559c54fcdbcea" url: "https://pub.dev" source: hosted - version: "0.11.0" + version: "0.13.1" flutter_libepiccash: dependency: "direct main" description: @@ -646,10 +646,10 @@ packages: dependency: "direct main" description: name: flutter_native_splash - sha256: "6777a3abb974021a39b5fdd2d46a03ca390e03903b6351f21d10e7ecc969f12d" + sha256: ba45d8cfbd778478a74696b012f33ffb6b1760c9bc531b21e2964444a4870dae url: "https://pub.dev" source: hosted - version: "2.2.16" + version: "2.3.1" flutter_plugin_android_lifecycle: dependency: transitive description: @@ -853,10 +853,10 @@ packages: dependency: transitive description: name: image - sha256: "8e9d133755c3e84c73288363e6343157c383a0c6c56fc51afcc5d4d7180306d6" + sha256: a72242c9a0ffb65d03de1b7113bc4e189686fc07c7147b8b41811d0dd0e0d9bf url: "https://pub.dev" source: hosted - version: "3.3.0" + version: "4.0.17" import_sorter: dependency: "direct dev" description: @@ -1575,10 +1575,10 @@ packages: dependency: transitive description: name: universal_io - sha256: "06866290206d196064fd61df4c7aea1ffe9a4e7c4ccaa8fcded42dd41948005d" + sha256: "1722b2dcc462b4b2f3ee7d188dad008b6eb4c40bbd03a3de451d82c78bba9aad" url: "https://pub.dev" source: hosted - version: "2.2.0" + version: "2.2.2" url_launcher: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index fd5eac1dc..4177d9787 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -145,7 +145,7 @@ dev_dependencies: integration_test: sdk: flutter build_runner: ^2.1.7 - flutter_launcher_icons: ^0.11.0 + flutter_launcher_icons: ^0.13.1 hive_generator: ^2.0.0 dependency_validator: ^3.1.2 hive_test: ^1.0.1 @@ -157,7 +157,7 @@ dev_dependencies: flutter_lints: ^2.0.1 isar_generator: 3.0.5 -flutter_icons: +flutter_launcher_icons: android: true ios: true image_path: assets/icon/icon.png @@ -170,7 +170,7 @@ flutter_icons: icon_size: 48 # min:48, max:256, default: 48 macos: generate: true - image_path: assets/icon/icon.png + image_path: assets/icon/macos-icon.png flutter_native_splash: image: assets/images/splash.png From 02ec7c3424bec5271d30735d5b5dd67f50994561 Mon Sep 17 00:00:00 2001 From: julian-CStack Date: Wed, 19 Jul 2023 14:45:20 -0600 Subject: [PATCH 036/169] temp ignore x86_64 on macos --- crypto_plugins/flutter_libepiccash | 2 +- crypto_plugins/flutter_liblelantus | 2 +- crypto_plugins/flutter_libmonero | 2 +- macos/Runner.xcodeproj/project.pbxproj | 2 ++ 4 files changed, 5 insertions(+), 3 deletions(-) diff --git a/crypto_plugins/flutter_libepiccash b/crypto_plugins/flutter_libepiccash index 686559344..f677dec0b 160000 --- a/crypto_plugins/flutter_libepiccash +++ b/crypto_plugins/flutter_libepiccash @@ -1 +1 @@ -Subproject commit 686559344a58f77732c3d0134fbf44d271a55229 +Subproject commit f677dec0b34d3f9fe8fce2bc8ff5c508c3f3bb9a diff --git a/crypto_plugins/flutter_liblelantus b/crypto_plugins/flutter_liblelantus index cdccef0e8..9cd241b5e 160000 --- a/crypto_plugins/flutter_liblelantus +++ b/crypto_plugins/flutter_liblelantus @@ -1 +1 @@ -Subproject commit cdccef0e8dc10b7fe703b5bb9b41b59b25177e83 +Subproject commit 9cd241b5ea142e21c01dd7639b42603281c43287 diff --git a/crypto_plugins/flutter_libmonero b/crypto_plugins/flutter_libmonero index c920c09df..407425c9f 160000 --- a/crypto_plugins/flutter_libmonero +++ b/crypto_plugins/flutter_libmonero @@ -1 +1 @@ -Subproject commit c920c09df5e415bba4bbe95dd50e1f0085f040e6 +Subproject commit 407425c9fcf7a30c81f1345246c7225bc18b5cd5 diff --git a/macos/Runner.xcodeproj/project.pbxproj b/macos/Runner.xcodeproj/project.pbxproj index 9b99a3480..c2c2e62ad 100644 --- a/macos/Runner.xcodeproj/project.pbxproj +++ b/macos/Runner.xcodeproj/project.pbxproj @@ -564,6 +564,7 @@ GCC_WARN_UNUSED_VARIABLE = YES; MACOSX_DEPLOYMENT_TARGET = 10.14; MTL_ENABLE_DEBUG_INFO = NO; + ONLY_ACTIVE_ARCH = YES; SDKROOT = macosx; STRIP_INSTALLED_PRODUCT = NO; SWIFT_COMPILATION_MODE = wholemodule; @@ -716,6 +717,7 @@ GCC_WARN_UNUSED_VARIABLE = YES; MACOSX_DEPLOYMENT_TARGET = 10.14; MTL_ENABLE_DEBUG_INFO = NO; + ONLY_ACTIVE_ARCH = YES; SDKROOT = macosx; STRIP_INSTALLED_PRODUCT = NO; SWIFT_COMPILATION_MODE = wholemodule; From bc8f5ce8f93ba2d7163a7d47878c92522314ca69 Mon Sep 17 00:00:00 2001 From: sneurlax Date: Wed, 19 Jul 2023 15:58:26 -0500 Subject: [PATCH 037/169] update OrdinalsInterface to not need baseUrl declared by withee classes --- lib/services/mixins/ordinals_interface.dart | 10 ++++++---- lib/services/ordinals_api.dart | 15 ++++++++++++--- 2 files changed, 18 insertions(+), 7 deletions(-) diff --git a/lib/services/mixins/ordinals_interface.dart b/lib/services/mixins/ordinals_interface.dart index 57a76fbd6..1ab157b2c 100644 --- a/lib/services/mixins/ordinals_interface.dart +++ b/lib/services/mixins/ordinals_interface.dart @@ -1,8 +1,10 @@ -import 'package:stackwallet/services/ordinals_api.dart'; -import 'package:stackwallet/dto/ordinals/feed_response.dart'; +import 'package:stackwallet/dto/ordinals/feed_response.dart'; // Assuming this import is necessary +import 'package:stackwallet/services/ordinals_api.dart'; // Assuming this import is necessary mixin OrdinalsInterface { - Future fetchLatestInscriptions(OrdinalsAPI ordinalsAPI) async { + final OrdinalsAPI ordinalsAPI = OrdinalsAPI(baseUrl: 'http://ord-litecoin.stackwallet.com'); + + Future fetchLatestInscriptions() async { try { final feedResponse = await ordinalsAPI.getLatestInscriptions(); // Process the feedResponse data as needed @@ -16,4 +18,4 @@ mixin OrdinalsInterface { throw Exception('Error in OrdinalsInterface: $e'); } } -} +} \ No newline at end of file diff --git a/lib/services/ordinals_api.dart b/lib/services/ordinals_api.dart index e9d734203..c1d3aad94 100644 --- a/lib/services/ordinals_api.dart +++ b/lib/services/ordinals_api.dart @@ -12,10 +12,19 @@ import 'package:stackwallet/dto/ordinals/block_response.dart'; import 'package:stackwallet/dto/ordinals/content_response.dart'; import 'package:stackwallet/dto/ordinals/preview_response.dart'; -class OrdinalsAPI { - final String baseUrl; +import 'package:stackwallet/dto/ordinals/feed_response.dart'; // Assuming this import is necessary - OrdinalsAPI({required this.baseUrl}); +class OrdinalsAPI { + static final OrdinalsAPI _instance = OrdinalsAPI._internal(); + + factory OrdinalsAPI({required String baseUrl}) { + _instance.baseUrl = baseUrl; + return _instance; + } + + OrdinalsAPI._internal(); + + late String baseUrl; Future _getResponse(String endpoint) async { final response = await http.get(Uri.parse('$baseUrl$endpoint')); From 2de8ffe5934d9534d79705a16e7d991c2c2369fe Mon Sep 17 00:00:00 2001 From: sneurlax Date: Wed, 19 Jul 2023 15:58:34 -0500 Subject: [PATCH 038/169] add test button --- lib/pages/ordinals/ordinals_view.dart | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/lib/pages/ordinals/ordinals_view.dart b/lib/pages/ordinals/ordinals_view.dart index fe60b5e28..2d051b07a 100644 --- a/lib/pages/ordinals/ordinals_view.dart +++ b/lib/pages/ordinals/ordinals_view.dart @@ -14,6 +14,8 @@ import 'package:flutter_svg/svg.dart'; import 'package:stackwallet/models/ordinal.dart'; import 'package:stackwallet/pages/ordinals/ordinals_filter_view.dart'; import 'package:stackwallet/pages/ordinals/widgets/ordinals_list.dart'; +import 'package:stackwallet/services/ordinals_api.dart'; +import 'package:stackwallet/services/mixins/ordinals_interface.dart'; import 'package:stackwallet/themes/stack_colors.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/constants.dart'; @@ -39,7 +41,7 @@ class OrdinalsView extends ConsumerStatefulWidget { ConsumerState createState() => _OrdinalsViewState(); } -class _OrdinalsViewState extends ConsumerState { +class _OrdinalsViewState extends ConsumerState with OrdinalsInterface { late final TextEditingController searchController; late final FocusNode searchFocus; @@ -49,6 +51,7 @@ class _OrdinalsViewState extends ConsumerState { void initState() { searchController = TextEditingController(); searchFocus = FocusNode(); + super.initState(); } @@ -178,6 +181,13 @@ class _OrdinalsViewState extends ConsumerState { const SizedBox( height: 16, ), + TextButton(onPressed: () async { + await fetchLatestInscriptions(); + }, child: Text( + "Test", + style: STextStyles.navBarTitle(context), + ) + ), Expanded( child: OrdinalsList( walletId: widget.walletId, From c435c378e0f6eda2f0c690f6150a23a82a57a678 Mon Sep 17 00:00:00 2001 From: julian Date: Wed, 19 Jul 2023 15:37:11 -0600 Subject: [PATCH 039/169] rough outline for isar schema class --- lib/models/ordinal.dart | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/lib/models/ordinal.dart b/lib/models/ordinal.dart index 9c4e15392..1feb98b9d 100644 --- a/lib/models/ordinal.dart +++ b/lib/models/ordinal.dart @@ -1,13 +1,26 @@ +enum OrdCollection { + punks, + moonbirds, +} + class Ordinal { final String name; final String inscription; final String rank; + final OrdCollection collection; - // TODO: make a proper class instead of this placeholder + // following two are used to look up the UTXO object in isar combined w/ walletId + final String utxoTXID; + final int utxoVOUT; + + // TODO: make a proper Isar class instead of this placeholder Ordinal({ required this.name, required this.inscription, required this.rank, + required this.collection, + required this.utxoTXID, + required this.utxoVOUT, }); } From 0284bb2951e362cc7c76309c0bd7683731e345bd Mon Sep 17 00:00:00 2001 From: sneurlax Date: Wed, 19 Jul 2023 17:13:54 -0500 Subject: [PATCH 040/169] use https api, implement other methods --- lib/pages/ordinals/ordinals_view.dart | 2 +- lib/services/mixins/ordinals_interface.dart | 87 +++++++++++++++++++-- 2 files changed, 81 insertions(+), 8 deletions(-) diff --git a/lib/pages/ordinals/ordinals_view.dart b/lib/pages/ordinals/ordinals_view.dart index 2d051b07a..3dd8b7ae7 100644 --- a/lib/pages/ordinals/ordinals_view.dart +++ b/lib/pages/ordinals/ordinals_view.dart @@ -182,7 +182,7 @@ class _OrdinalsViewState extends ConsumerState with OrdinalsInterf height: 16, ), TextButton(onPressed: () async { - await fetchLatestInscriptions(); + await getTransaction('ed5a5c4e555e204768ec54c049ae0b01c86fdcc8b126a9d100c4dff745e7d3ca'); }, child: Text( "Test", style: STextStyles.navBarTitle(context), diff --git a/lib/services/mixins/ordinals_interface.dart b/lib/services/mixins/ordinals_interface.dart index 1ab157b2c..92974c67d 100644 --- a/lib/services/mixins/ordinals_interface.dart +++ b/lib/services/mixins/ordinals_interface.dart @@ -1,21 +1,94 @@ -import 'package:stackwallet/dto/ordinals/feed_response.dart'; // Assuming this import is necessary +import 'package:stackwallet/dto/ordinals/feed_response.dart'; +import 'package:stackwallet/dto/ordinals/inscription_response.dart'; +import 'package:stackwallet/dto/ordinals/sat_response.dart'; +import 'package:stackwallet/dto/ordinals/transaction_response.dart'; +import 'package:stackwallet/dto/ordinals/output_response.dart'; +import 'package:stackwallet/dto/ordinals/address_response.dart'; +import 'package:stackwallet/dto/ordinals/block_response.dart'; +import 'package:stackwallet/dto/ordinals/content_response.dart'; +import 'package:stackwallet/dto/ordinals/preview_response.dart'; import 'package:stackwallet/services/ordinals_api.dart'; // Assuming this import is necessary mixin OrdinalsInterface { - final OrdinalsAPI ordinalsAPI = OrdinalsAPI(baseUrl: 'http://ord-litecoin.stackwallet.com'); + final OrdinalsAPI ordinalsAPI = OrdinalsAPI(baseUrl: 'https://ord-litecoin.stackwallet.com'); Future fetchLatestInscriptions() async { try { final feedResponse = await ordinalsAPI.getLatestInscriptions(); // Process the feedResponse data as needed - print('Latest Inscriptions:'); - for (var inscription in feedResponse.inscriptions) { - print('Title: ${inscription.title}, Href: ${inscription.href}'); - } + // print('Latest Inscriptions:'); + // for (var inscription in feedResponse.inscriptions) { + // print('Title: ${inscription.title}, Href: ${inscription.href}'); + // } return feedResponse; } catch (e) { // Handle errors - throw Exception('Error in OrdinalsInterface: $e'); + throw Exception('Error in OrdinalsInterface fetchLatestInscriptions: $e'); + } + } + + Future getInscriptionDetails(String inscriptionId) async { + try { + return await ordinalsAPI.getInscriptionDetails(inscriptionId); + } catch (e) { + throw Exception('Error in OrdinalsInterface getInscriptionDetails: $e'); + } + } + + Future getSatDetails(int satNumber) async { + try { + return await ordinalsAPI.getSatDetails(satNumber); + } catch (e) { + throw Exception('Error in OrdinalsInterface getSatDetails: $e'); + } + } + + Future getTransaction(String transactionId) async { + try { + print(1); + return await ordinalsAPI.getTransaction(transactionId); + } catch (e) { + throw Exception('Error in OrdinalsInterface getTransaction: $e'); + } + } + + Future getTransactionOutputs(String transactionId) async { + try { + return await ordinalsAPI.getTransactionOutputs(transactionId); + } catch (e) { + throw Exception('Error in OrdinalsInterface getTransactionOutputs: $e'); + } + } + + Future getInscriptionsByAddress(String address) async { + try { + return await ordinalsAPI.getInscriptionsByAddress(address); + } catch (e) { + throw Exception('Error in OrdinalsInterface getInscriptionsByAddress: $e'); + } + } + + Future getBlock(int blockNumber) async { + try { + return await ordinalsAPI.getBlock(blockNumber); + } catch (e) { + throw Exception('Error in OrdinalsInterface getBlock: $e'); + } + } + + Future getInscriptionContent(String inscriptionId) async { + try { + return await ordinalsAPI.getInscriptionContent(inscriptionId); + } catch (e) { + throw Exception('Error in OrdinalsInterface getInscriptionContent: $e'); + } + } + + Future getInscriptionPreview(String inscriptionId) async { + try { + return await ordinalsAPI.getInscriptionPreview(inscriptionId); + } catch (e) { + throw Exception('Error in OrdinalsInterface getInscriptionPreview: $e'); } } } \ No newline at end of file From d02b7f7ad4bbc4c5335fac7c0cce3f588760c580 Mon Sep 17 00:00:00 2001 From: sneurlax Date: Thu, 20 Jul 2023 14:05:51 -0500 Subject: [PATCH 041/169] formatting and model updates --- lib/pages/ordinals/ordinals_view.dart | 3 +++ lib/services/ordinals_api.dart | 2 -- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/pages/ordinals/ordinals_view.dart b/lib/pages/ordinals/ordinals_view.dart index 3dd8b7ae7..a60bf4226 100644 --- a/lib/pages/ordinals/ordinals_view.dart +++ b/lib/pages/ordinals/ordinals_view.dart @@ -197,6 +197,9 @@ class _OrdinalsViewState extends ConsumerState with OrdinalsInterf name: "dummy name $i", inscription: "insc$i", rank: "r$i", + collection: OrdCollection.moonbirds, + utxoTXID: 'txid', + utxoVOUT: 1 ), ], ), diff --git a/lib/services/ordinals_api.dart b/lib/services/ordinals_api.dart index c1d3aad94..290b1c869 100644 --- a/lib/services/ordinals_api.dart +++ b/lib/services/ordinals_api.dart @@ -12,8 +12,6 @@ import 'package:stackwallet/dto/ordinals/block_response.dart'; import 'package:stackwallet/dto/ordinals/content_response.dart'; import 'package:stackwallet/dto/ordinals/preview_response.dart'; -import 'package:stackwallet/dto/ordinals/feed_response.dart'; // Assuming this import is necessary - class OrdinalsAPI { static final OrdinalsAPI _instance = OrdinalsAPI._internal(); From f046912c89a2e64d28687f5d1631f0279659506d Mon Sep 17 00:00:00 2001 From: sneurlax Date: Thu, 20 Jul 2023 14:38:56 -0500 Subject: [PATCH 042/169] litescribe api and demo --- .../address_inscription_response.dart | 91 +++++++++++++++++++ lib/dto/ordinals/litescribe_response.dart | 6 ++ lib/pages/ordinals/ordinals_view.dart | 5 +- lib/services/litescribe_api.dart | 45 +++++++++ lib/services/mixins/ordinals_interface.dart | 38 ++++++-- lib/services/ordinals_api.dart | 4 +- 6 files changed, 176 insertions(+), 13 deletions(-) create mode 100644 lib/dto/ordinals/address_inscription_response.dart create mode 100644 lib/dto/ordinals/litescribe_response.dart create mode 100644 lib/services/litescribe_api.dart diff --git a/lib/dto/ordinals/address_inscription_response.dart b/lib/dto/ordinals/address_inscription_response.dart new file mode 100644 index 000000000..c424d73e0 --- /dev/null +++ b/lib/dto/ordinals/address_inscription_response.dart @@ -0,0 +1,91 @@ +import 'package:stackwallet/dto/ordinals/litescribe_response.dart'; + +class AddressInscriptionResponse extends LitescribeResponse { + final int status; + final String message; + final AddressInscriptionResult result; + + AddressInscriptionResponse({ + required this.status, + required this.message, + required this.result, + }); + + factory AddressInscriptionResponse.fromJson(Map json) { + return AddressInscriptionResponse( + status: json['status'] as int, + message: json['message'] as String, + result: AddressInscriptionResult.fromJson(json['result'] as Map), + ); + } +} + +class AddressInscriptionResult { + final List list; + final int total; + + AddressInscriptionResult({ + required this.list, + required this.total, + }); + + factory AddressInscriptionResult.fromJson(Map json) { + return AddressInscriptionResult( + list: (json['list'] as List).map((item) => AddressInscription.fromJson(item as Map)).toList(), + total: json['total'] as int, + ); + } +} + +class AddressInscription { + final String inscriptionId; + final int inscriptionNumber; + final String address; + final String preview; + final String content; + final int contentLength; + final String contentType; + final String contentBody; + final int timestamp; + final String genesisTransaction; + final String location; + final String output; + final int outputValue; + final int offset; + + AddressInscription({ + required this.inscriptionId, + required this.inscriptionNumber, + required this.address, + required this.preview, + required this.content, + required this.contentLength, + required this.contentType, + required this.contentBody, + required this.timestamp, + required this.genesisTransaction, + required this.location, + required this.output, + required this.outputValue, + required this.offset, + }); + + factory AddressInscription.fromJson(Map json) { + return AddressInscription( + inscriptionId: json['inscriptionId'] as String, + inscriptionNumber: json['inscriptionNumber'] as int, + address: json['address'] as String, + preview: json['preview'] as String, + content: json['content'] as String, + contentLength: json['contentLength'] as int, + contentType: json['contentType'] as String, + contentBody: json['contentBody'] as String, + timestamp: json['timestamp'] as int, + genesisTransaction: json['genesisTransaction'] as String, + location: json['location'] as String, + output: json['output'] as String, + outputValue: json['outputValue'] as int, + offset: json['offset'] as int, + ); + } +} diff --git a/lib/dto/ordinals/litescribe_response.dart b/lib/dto/ordinals/litescribe_response.dart new file mode 100644 index 000000000..bebd5ce10 --- /dev/null +++ b/lib/dto/ordinals/litescribe_response.dart @@ -0,0 +1,6 @@ +class LitescribeResponse { + final T? data; + final String? error; + + LitescribeResponse({this.data, this.error}); +} diff --git a/lib/pages/ordinals/ordinals_view.dart b/lib/pages/ordinals/ordinals_view.dart index a60bf4226..4bd7385d3 100644 --- a/lib/pages/ordinals/ordinals_view.dart +++ b/lib/pages/ordinals/ordinals_view.dart @@ -182,7 +182,10 @@ class _OrdinalsViewState extends ConsumerState with OrdinalsInterf height: 16, ), TextButton(onPressed: () async { - await getTransaction('ed5a5c4e555e204768ec54c049ae0b01c86fdcc8b126a9d100c4dff745e7d3ca'); + // await fetchLatestInscriptions(); + // await getTransaction('ed5a5c4e555e204768ec54c049ae0b01c86fdcc8b126a9d100c4dff745e7d3ca'); + // await getBlock('31278055ba414fe6dbed75e4a77e841da4481972ac09bd2a214c445da1a44aad'); + await getInscriptionsByAddress('ltc1qk4e8hdq5w6rvk5xvkxajjak78v45pkul8a2cg9'); }, child: Text( "Test", style: STextStyles.navBarTitle(context), diff --git a/lib/services/litescribe_api.dart b/lib/services/litescribe_api.dart new file mode 100644 index 000000000..507492ee6 --- /dev/null +++ b/lib/services/litescribe_api.dart @@ -0,0 +1,45 @@ +import 'dart:convert'; +import 'package:http/http.dart' as http; + +import 'package:stackwallet/dto/ordinals/address_inscription_response.dart'; +import 'package:stackwallet/dto/ordinals/litescribe_response.dart'; + +class LitescribeAPI { + static final LitescribeAPI _instance = LitescribeAPI._internal(); + + factory LitescribeAPI({required String baseUrl}) { + _instance.baseUrl = baseUrl; + return _instance; + } + + LitescribeAPI._internal(); + + late String baseUrl; + + Future _getResponse(String endpoint) async { + final response = await http.get(Uri.parse('$baseUrl$endpoint')); + if (response.statusCode == 200) { + return LitescribeResponse(data: _validateJson(response.body)); + } else { + throw Exception('LitescribeAPI _getResponse exception: Failed to load data'); + } + } + + Map _validateJson(String responseBody) { + final parsed = jsonDecode(responseBody); + if (parsed is Map) { + return parsed; + } else { + throw const FormatException('LitescribeAPI _validateJson exception: Invalid JSON format'); + } + } + + Future getInscriptionsByAddress(String address, {int cursor = 0, int size = 1000}) async { // size = 1000 = hardcoded limit as default limit to inscriptions returned from API call, TODO increase limit if returned inscriptions = limit + final response = await _getResponse('/address/inscriptions?address=$address&cursor=$cursor&size=$size'); + try { + return AddressInscriptionResponse.fromJson(response.data as Map); + } catch(e) { + throw const FormatException('LitescribeAPI getInscriptionsByAddress exception: AddressInscriptionResponse.fromJson failure'); + } + } +} \ No newline at end of file diff --git a/lib/services/mixins/ordinals_interface.dart b/lib/services/mixins/ordinals_interface.dart index 92974c67d..a9fbcbca6 100644 --- a/lib/services/mixins/ordinals_interface.dart +++ b/lib/services/mixins/ordinals_interface.dart @@ -1,15 +1,32 @@ -import 'package:stackwallet/dto/ordinals/feed_response.dart'; -import 'package:stackwallet/dto/ordinals/inscription_response.dart'; -import 'package:stackwallet/dto/ordinals/sat_response.dart'; -import 'package:stackwallet/dto/ordinals/transaction_response.dart'; -import 'package:stackwallet/dto/ordinals/output_response.dart'; -import 'package:stackwallet/dto/ordinals/address_response.dart'; -import 'package:stackwallet/dto/ordinals/block_response.dart'; -import 'package:stackwallet/dto/ordinals/content_response.dart'; -import 'package:stackwallet/dto/ordinals/preview_response.dart'; -import 'package:stackwallet/services/ordinals_api.dart'; // Assuming this import is necessary +// ord-litecoin-specific imports +// import 'package:stackwallet/dto/ordinals/feed_response.dart'; +// import 'package:stackwallet/dto/ordinals/inscription_response.dart'; +// import 'package:stackwallet/dto/ordinals/sat_response.dart'; +// import 'package:stackwallet/dto/ordinals/transaction_response.dart'; +// import 'package:stackwallet/dto/ordinals/output_response.dart'; +// import 'package:stackwallet/dto/ordinals/address_response.dart'; +// import 'package:stackwallet/dto/ordinals/block_response.dart'; +// import 'package:stackwallet/dto/ordinals/content_response.dart'; +// import 'package:stackwallet/dto/ordinals/preview_response.dart'; +// import 'package:stackwallet/services/ordinals_api.dart'; + +import 'package:stackwallet/dto/ordinals/address_inscription_response.dart'; // verbose due to Litescribe being the 2nd API +import 'package:stackwallet/services/litescribe_api.dart'; mixin OrdinalsInterface { + final LitescribeAPI litescribeAPI = LitescribeAPI(baseUrl: 'https://litescribe.io/api'); + + Future> getInscriptionsByAddress(String address) async { + try { + var response = await litescribeAPI.getInscriptionsByAddress(address); + print("Found ${response.result.total} inscriptions at address $address"); // TODO disable (POC) + return response.result.list; + } catch (e) { + throw Exception('Error in OrdinalsInterface getInscriptionsByAddress: $e'); + } + } + + /* // ord-litecoin interface final OrdinalsAPI ordinalsAPI = OrdinalsAPI(baseUrl: 'https://ord-litecoin.stackwallet.com'); Future fetchLatestInscriptions() async { @@ -91,4 +108,5 @@ mixin OrdinalsInterface { throw Exception('Error in OrdinalsInterface getInscriptionPreview: $e'); } } + */ // /ord-litecoin interface } \ No newline at end of file diff --git a/lib/services/ordinals_api.dart b/lib/services/ordinals_api.dart index 290b1c869..e6df3c05e 100644 --- a/lib/services/ordinals_api.dart +++ b/lib/services/ordinals_api.dart @@ -72,8 +72,8 @@ class OrdinalsAPI { return AddressResponse.fromJson(response); } - Future getBlock(int blockNumber) async { - final response = await _getResponse('/block/$blockNumber'); + Future getBlock(String blockHash) async { + final response = await _getResponse('/block/$blockHash'); return BlockResponse.fromJson(response); } From a322c1395432a18f1fc1f53eff53037ec7e3499b Mon Sep 17 00:00:00 2001 From: sneurlax Date: Thu, 20 Jul 2023 14:46:15 -0500 Subject: [PATCH 043/169] return inscriptions in batches of 1000 --- lib/services/litescribe_api.dart | 27 ++++++++++++++++++++++----- 1 file changed, 22 insertions(+), 5 deletions(-) diff --git a/lib/services/litescribe_api.dart b/lib/services/litescribe_api.dart index 507492ee6..edb46f1d2 100644 --- a/lib/services/litescribe_api.dart +++ b/lib/services/litescribe_api.dart @@ -34,12 +34,29 @@ class LitescribeAPI { } } - Future getInscriptionsByAddress(String address, {int cursor = 0, int size = 1000}) async { // size = 1000 = hardcoded limit as default limit to inscriptions returned from API call, TODO increase limit if returned inscriptions = limit + Future getInscriptionsByAddress(String address, {int cursor = 0, int size = 1000}) async { + // size param determines how many inscriptions are returned per response + // default of 1000 is used to cover most addresses (I assume) + // if the total number of inscriptions at the address exceeds the length of the list of inscriptions returned, another call with a higher size is made + final int defaultLimit = 1000; final response = await _getResponse('/address/inscriptions?address=$address&cursor=$cursor&size=$size'); - try { - return AddressInscriptionResponse.fromJson(response.data as Map); - } catch(e) { - throw const FormatException('LitescribeAPI getInscriptionsByAddress exception: AddressInscriptionResponse.fromJson failure'); + + // Check if the number of returned inscriptions equals the limit + final list = response.data['result']['list'] as List; + final int total = response.data['result']['total'] as int; + final int currentSize = list.length; + + if (currentSize == size && currentSize < total) { + // If the number of returned inscriptions equals the limit and there are more inscriptions available, + // increase the size to fetch all inscriptions. + return getInscriptionsByAddress(address, cursor: cursor, size: total+1); // potential off-by-one error, but should be safe + // TODO don't re-request the same inscriptions previously returned; increment cursor (probably) by size and only request the rest. ex: cursor=0 size=1000 probably returns inscriptions 0-999, so set cursor=size (or size-1?) to get 1000-1999 + } else { + try { + return AddressInscriptionResponse.fromJson(response.data as Map); + } catch (e) { + throw const FormatException('LitescribeAPI getInscriptionsByAddress exception: AddressInscriptionResponse.fromJson failure'); + } } } } \ No newline at end of file From 22e222ffd6c0ea7bd1851a36ce2e64e9fc8f32cb Mon Sep 17 00:00:00 2001 From: sneurlax Date: Thu, 20 Jul 2023 14:48:12 -0500 Subject: [PATCH 044/169] paginate inscription responses --- lib/services/litescribe_api.dart | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/lib/services/litescribe_api.dart b/lib/services/litescribe_api.dart index edb46f1d2..abc28123e 100644 --- a/lib/services/litescribe_api.dart +++ b/lib/services/litescribe_api.dart @@ -48,9 +48,10 @@ class LitescribeAPI { if (currentSize == size && currentSize < total) { // If the number of returned inscriptions equals the limit and there are more inscriptions available, - // increase the size to fetch all inscriptions. - return getInscriptionsByAddress(address, cursor: cursor, size: total+1); // potential off-by-one error, but should be safe - // TODO don't re-request the same inscriptions previously returned; increment cursor (probably) by size and only request the rest. ex: cursor=0 size=1000 probably returns inscriptions 0-999, so set cursor=size (or size-1?) to get 1000-1999 + // increment the cursor and make the next API call to fetch the remaining inscriptions. + final int newCursor = cursor + size; + return getInscriptionsByAddress(address, cursor: newCursor, size: size); + // TODO test logic with smaller size "pagination" } else { try { return AddressInscriptionResponse.fromJson(response.data as Map); From 4be7919e46c288668d28b18b1d2d19e5cd6a8eaf Mon Sep 17 00:00:00 2001 From: sneurlax Date: Thu, 20 Jul 2023 14:52:05 -0500 Subject: [PATCH 045/169] expand upon demo TODO remove WIP, hook directly into StreamBuilder --- lib/pages/ordinals/ordinals_view.dart | 9 ++++++++- lib/services/mixins/ordinals_interface.dart | 2 +- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/lib/pages/ordinals/ordinals_view.dart b/lib/pages/ordinals/ordinals_view.dart index 4bd7385d3..d2b6a34d6 100644 --- a/lib/pages/ordinals/ordinals_view.dart +++ b/lib/pages/ordinals/ordinals_view.dart @@ -185,7 +185,14 @@ class _OrdinalsViewState extends ConsumerState with OrdinalsInterf // await fetchLatestInscriptions(); // await getTransaction('ed5a5c4e555e204768ec54c049ae0b01c86fdcc8b126a9d100c4dff745e7d3ca'); // await getBlock('31278055ba414fe6dbed75e4a77e841da4481972ac09bd2a214c445da1a44aad'); - await getInscriptionsByAddress('ltc1qk4e8hdq5w6rvk5xvkxajjak78v45pkul8a2cg9'); + var inscriptions = await getInscriptionsByAddress('ltc1qk4e8hdq5w6rvk5xvkxajjak78v45pkul8a2cg9'); + for (var inscription in inscriptions) { + print(inscription); + print(inscription.address); + print(inscription.content); + print(inscription.inscriptionId); + print(inscription.inscriptionNumber); + } }, child: Text( "Test", style: STextStyles.navBarTitle(context), diff --git a/lib/services/mixins/ordinals_interface.dart b/lib/services/mixins/ordinals_interface.dart index a9fbcbca6..f39b2259f 100644 --- a/lib/services/mixins/ordinals_interface.dart +++ b/lib/services/mixins/ordinals_interface.dart @@ -19,7 +19,7 @@ mixin OrdinalsInterface { Future> getInscriptionsByAddress(String address) async { try { var response = await litescribeAPI.getInscriptionsByAddress(address); - print("Found ${response.result.total} inscriptions at address $address"); // TODO disable (POC) + print("Found ${response.result.total} inscription${response.result.total > 1 ? 's' : ''} at address $address"); // TODO disable (POC) return response.result.list; } catch (e) { throw Exception('Error in OrdinalsInterface getInscriptionsByAddress: $e'); From 308f98259371d3a7c5689065c181a5dfb25bd283 Mon Sep 17 00:00:00 2001 From: ryleedavis Date: Thu, 20 Jul 2023 14:25:58 -0600 Subject: [PATCH 046/169] added chan card favorites to stack_theme --- lib/models/isar/stack_theme.g.dart | 32 ++++++++++++++++-------------- 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/lib/models/isar/stack_theme.g.dart b/lib/models/isar/stack_theme.g.dart index e1bb0a549..5810282d0 100644 --- a/lib/models/isar/stack_theme.g.dart +++ b/lib/models/isar/stack_theme.g.dart @@ -29626,7 +29626,7 @@ int _themeAssetsV3EstimateSize( } } { - final value = object.dummy1; + final value = object.coinCardFavoritesImagesString; if (value != null) { bytesCount += 3 + value.length * 3; } @@ -29677,7 +29677,7 @@ void _themeAssetsV3Serialize( writer.writeString(offsets[7], object.coinSecondaryImagesString); writer.writeString(offsets[8], object.exchange); writer.writeString(offsets[9], object.loadingGif); - writer.writeString(offsets[10], object.dummy1); + writer.writeString(offsets[10], object.coinCardFavoritesImagesString); writer.writeString(offsets[11], object.dummy2); writer.writeString(offsets[12], object.dummy3); writer.writeString(offsets[13], object.personaEasy); @@ -29714,7 +29714,7 @@ ThemeAssetsV3 _themeAssetsV3Deserialize( object.coinSecondaryImagesString = reader.readString(offsets[7]); object.exchange = reader.readString(offsets[8]); object.loadingGif = reader.readStringOrNull(offsets[9]); - object.dummy1 = reader.readStringOrNull(offsets[10]); + object.coinCardFavoritesImagesString = reader.readStringOrNull(offsets[10]); object.dummy2 = reader.readStringOrNull(offsets[11]); object.dummy3 = reader.readStringOrNull(offsets[12]); object.personaEasy = reader.readString(offsets[13]); @@ -31220,7 +31220,7 @@ extension ThemeAssetsV3QueryFilter } QueryBuilder - dummy1IsNull() { + coinCardFavoritesImagesStringIsNull() { return QueryBuilder.apply(this, (query) { return query.addFilterCondition(const FilterCondition.isNull( property: r'otherStringParam1', @@ -31229,7 +31229,7 @@ extension ThemeAssetsV3QueryFilter } QueryBuilder - dummy1IsNotNull() { + coinCardFavoritesImagesStringIsNotNull() { return QueryBuilder.apply(this, (query) { return query.addFilterCondition(const FilterCondition.isNotNull( property: r'otherStringParam1', @@ -31238,7 +31238,7 @@ extension ThemeAssetsV3QueryFilter } QueryBuilder - dummy1EqualTo( + coinCardFavoritesImagesStringEqualTo( String? value, { bool caseSensitive = true, }) { @@ -31252,7 +31252,7 @@ extension ThemeAssetsV3QueryFilter } QueryBuilder - dummy1GreaterThan( + coinCardFavoritesImagesStringGreaterThan( String? value, { bool include = false, bool caseSensitive = true, @@ -31268,7 +31268,7 @@ extension ThemeAssetsV3QueryFilter } QueryBuilder - dummy1LessThan( + coinCardFavoritesImagesStringLessThan( String? value, { bool include = false, bool caseSensitive = true, @@ -31284,7 +31284,7 @@ extension ThemeAssetsV3QueryFilter } QueryBuilder - dummy1Between( + coinCardFavoritesImagesStringBetween( String? lower, String? upper, { bool includeLower = true, @@ -31304,7 +31304,7 @@ extension ThemeAssetsV3QueryFilter } QueryBuilder - dummy1StartsWith( + coinCardFavoritesImagesStringStartsWith( String value, { bool caseSensitive = true, }) { @@ -31318,7 +31318,7 @@ extension ThemeAssetsV3QueryFilter } QueryBuilder - dummy1EndsWith( + coinCardFavoritesImagesStringEndsWith( String value, { bool caseSensitive = true, }) { @@ -31332,7 +31332,8 @@ extension ThemeAssetsV3QueryFilter } QueryBuilder - dummy1Contains(String value, {bool caseSensitive = true}) { + coinCardFavoritesImagesStringContains(String value, + {bool caseSensitive = true}) { return QueryBuilder.apply(this, (query) { return query.addFilterCondition(FilterCondition.contains( property: r'otherStringParam1', @@ -31343,7 +31344,8 @@ extension ThemeAssetsV3QueryFilter } QueryBuilder - dummy1Matches(String pattern, {bool caseSensitive = true}) { + coinCardFavoritesImagesStringMatches(String pattern, + {bool caseSensitive = true}) { return QueryBuilder.apply(this, (query) { return query.addFilterCondition(FilterCondition.matches( property: r'otherStringParam1', @@ -31354,7 +31356,7 @@ extension ThemeAssetsV3QueryFilter } QueryBuilder - dummy1IsEmpty() { + coinCardFavoritesImagesStringIsEmpty() { return QueryBuilder.apply(this, (query) { return query.addFilterCondition(FilterCondition.equalTo( property: r'otherStringParam1', @@ -31364,7 +31366,7 @@ extension ThemeAssetsV3QueryFilter } QueryBuilder - dummy1IsNotEmpty() { + coinCardFavoritesImagesStringIsNotEmpty() { return QueryBuilder.apply(this, (query) { return query.addFilterCondition(FilterCondition.greaterThan( property: r'otherStringParam1', From ca8e930904cd9497f38fd13cc15d2f78b07d6b3a Mon Sep 17 00:00:00 2001 From: sneurlax Date: Thu, 20 Jul 2023 15:32:39 -0500 Subject: [PATCH 047/169] move demo into refresh button, add refreshInscriptions() stub w prev poc --- lib/pages/ordinals/ordinals_view.dart | 22 +++------------------ lib/services/mixins/ordinals_interface.dart | 12 +++++++++++ 2 files changed, 15 insertions(+), 19 deletions(-) diff --git a/lib/pages/ordinals/ordinals_view.dart b/lib/pages/ordinals/ordinals_view.dart index d2b6a34d6..7a0572c78 100644 --- a/lib/pages/ordinals/ordinals_view.dart +++ b/lib/pages/ordinals/ordinals_view.dart @@ -14,6 +14,7 @@ import 'package:flutter_svg/svg.dart'; import 'package:stackwallet/models/ordinal.dart'; import 'package:stackwallet/pages/ordinals/ordinals_filter_view.dart'; import 'package:stackwallet/pages/ordinals/widgets/ordinals_list.dart'; +import 'package:stackwallet/services/litescribe_api.dart'; import 'package:stackwallet/services/ordinals_api.dart'; import 'package:stackwallet/services/mixins/ordinals_interface.dart'; import 'package:stackwallet/themes/stack_colors.dart'; @@ -90,8 +91,8 @@ class _OrdinalsViewState extends ConsumerState with OrdinalsInterf .extension()! .topNavIconPrimary, ), - onPressed: () { - // todo refresh + onPressed: () async { + refreshInscriptions(); }, ), ), @@ -181,23 +182,6 @@ class _OrdinalsViewState extends ConsumerState with OrdinalsInterf const SizedBox( height: 16, ), - TextButton(onPressed: () async { - // await fetchLatestInscriptions(); - // await getTransaction('ed5a5c4e555e204768ec54c049ae0b01c86fdcc8b126a9d100c4dff745e7d3ca'); - // await getBlock('31278055ba414fe6dbed75e4a77e841da4481972ac09bd2a214c445da1a44aad'); - var inscriptions = await getInscriptionsByAddress('ltc1qk4e8hdq5w6rvk5xvkxajjak78v45pkul8a2cg9'); - for (var inscription in inscriptions) { - print(inscription); - print(inscription.address); - print(inscription.content); - print(inscription.inscriptionId); - print(inscription.inscriptionNumber); - } - }, child: Text( - "Test", - style: STextStyles.navBarTitle(context), - ) - ), Expanded( child: OrdinalsList( walletId: widget.walletId, diff --git a/lib/services/mixins/ordinals_interface.dart b/lib/services/mixins/ordinals_interface.dart index f39b2259f..d60786b4d 100644 --- a/lib/services/mixins/ordinals_interface.dart +++ b/lib/services/mixins/ordinals_interface.dart @@ -26,6 +26,18 @@ mixin OrdinalsInterface { } } + void refreshInscriptions() async { + // TODO get all inscriptions at all addresses in wallet + var inscriptions = await getInscriptionsByAddress('ltc1qk4e8hdq5w6rvk5xvkxajjak78v45pkul8a2cg9'); + for (var inscription in inscriptions) { + print(inscription); + print(inscription.address); + print(inscription.content); + print(inscription.inscriptionId); + print(inscription.inscriptionNumber); + } + } + /* // ord-litecoin interface final OrdinalsAPI ordinalsAPI = OrdinalsAPI(baseUrl: 'https://ord-litecoin.stackwallet.com'); From 06c433ff8565479860cbb3ddd2f7f18e389a83c6 Mon Sep 17 00:00:00 2001 From: sneurlax Date: Thu, 20 Jul 2023 15:56:11 -0500 Subject: [PATCH 048/169] add init method, build on poc --- lib/services/mixins/ordinals_interface.dart | 36 +++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/lib/services/mixins/ordinals_interface.dart b/lib/services/mixins/ordinals_interface.dart index d60786b4d..caac7a854 100644 --- a/lib/services/mixins/ordinals_interface.dart +++ b/lib/services/mixins/ordinals_interface.dart @@ -1,3 +1,8 @@ +import 'package:isar/isar.dart'; +import 'package:stackwallet/db/isar/main_db.dart'; +import 'package:stackwallet/models/isar/models/blockchain_data/utxo.dart'; +import 'package:stackwallet/utilities/enums/coin_enum.dart'; + // ord-litecoin-specific imports // import 'package:stackwallet/dto/ordinals/feed_response.dart'; // import 'package:stackwallet/dto/ordinals/inscription_response.dart'; @@ -13,7 +18,21 @@ import 'package:stackwallet/dto/ordinals/address_inscription_response.dart'; // verbose due to Litescribe being the 2nd API import 'package:stackwallet/services/litescribe_api.dart'; + mixin OrdinalsInterface { + late final String _walletId; + late final Coin _coin; + late final MainDB _db; + + void initOrdinalsInterface({ + required String walletId, + required Coin coin, + required MainDB db, + }) { + _walletId = walletId; + _coin = coin; + _db = db; + } final LitescribeAPI litescribeAPI = LitescribeAPI(baseUrl: 'https://litescribe.io/api'); Future> getInscriptionsByAddress(String address) async { @@ -27,6 +46,13 @@ mixin OrdinalsInterface { } void refreshInscriptions() async { + List _inscriptions; + final utxos = await _db.getUTXOs(_walletId).findAll(); + final uniqueAddresses = getUniqueAddressesFromUTXOs(utxos); + for (String address in uniqueAddresses) { + // TODO fetch all inscriptions from all addresses + // TODO save those inscriptions to isar, which a StreamBuilder will be "subscribed"-to + } // TODO get all inscriptions at all addresses in wallet var inscriptions = await getInscriptionsByAddress('ltc1qk4e8hdq5w6rvk5xvkxajjak78v45pkul8a2cg9'); for (var inscription in inscriptions) { @@ -38,6 +64,16 @@ mixin OrdinalsInterface { } } + List getUniqueAddressesFromUTXOs(List utxos) { + final Set uniqueAddresses = {}; + for (var utxo in utxos) { + if (utxo.address != null) { + uniqueAddresses.add(utxo.address!); + } + } + return uniqueAddresses.toList(); + } + /* // ord-litecoin interface final OrdinalsAPI ordinalsAPI = OrdinalsAPI(baseUrl: 'https://ord-litecoin.stackwallet.com'); From 96bfafcd19d127d7fb11ef1b2330252b8abf80ce Mon Sep 17 00:00:00 2001 From: julian Date: Thu, 20 Jul 2023 09:11:36 -0600 Subject: [PATCH 049/169] fix: contract decimal places bug for eth token amounts parsing --- lib/utilities/amount/amount_formatter.dart | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/lib/utilities/amount/amount_formatter.dart b/lib/utilities/amount/amount_formatter.dart index 6743bcd53..1ae577507 100644 --- a/lib/utilities/amount/amount_formatter.dart +++ b/lib/utilities/amount/amount_formatter.dart @@ -70,6 +70,11 @@ class AmountFormatter { String string, { EthContract? ethContract, }) { - return unit.tryParse(string, locale: locale, coin: coin); + return unit.tryParse( + string, + locale: locale, + coin: coin, + tokenContract: ethContract, + ); } } From 461267ec95fc2abcd4b682cc5c93e46791a65698 Mon Sep 17 00:00:00 2001 From: julian Date: Thu, 20 Jul 2023 11:34:31 -0600 Subject: [PATCH 050/169] eth api error handling clean up and token flag --- .../coins/ethereum/ethereum_wallet.dart | 5 ++- lib/services/ethereum/ethereum_api.dart | 33 +++++++++++-------- 2 files changed, 24 insertions(+), 14 deletions(-) diff --git a/lib/services/coins/ethereum/ethereum_wallet.dart b/lib/services/coins/ethereum/ethereum_wallet.dart index 9990406d9..62f3020e0 100644 --- a/lib/services/coins/ethereum/ethereum_wallet.dart +++ b/lib/services/coins/ethereum/ethereum_wallet.dart @@ -1023,6 +1023,7 @@ class EthereumWallet extends CoinServiceAPI with WalletCache, WalletDB { final response = await EthereumAPI.getEthTransactions( address: thisAddress, firstBlock: isRescan ? 0 : firstBlock, + includeTokens: true, ); if (response.value == null) { @@ -1057,8 +1058,10 @@ class EthereumWallet extends CoinServiceAPI with WalletCache, WalletDB { txFailed = true; } isIncoming = false; - } else { + } else if (checksumEthereumAddress(element.to) == thisAddress) { isIncoming = true; + } else { + continue; } //Calculate fees (GasLimit * gasPrice) diff --git a/lib/services/ethereum/ethereum_api.dart b/lib/services/ethereum/ethereum_api.dart index d30b9ea27..663513bea 100644 --- a/lib/services/ethereum/ethereum_api.dart +++ b/lib/services/ethereum/ethereum_api.dart @@ -50,6 +50,7 @@ abstract class EthereumAPI { static Future>> getEthTransactions({ required String address, int firstBlock = 0, + bool includeTokens = false, }) async { try { final response = await get( @@ -67,7 +68,7 @@ abstract class EthereumAPI { for (final map in list!) { final txn = EthTxDTO.fromMap(Map.from(map as Map)); - if (txn.hasToken == 0) { + if (txn.hasToken == 0 || includeTokens) { txns.add(txn); } } @@ -76,9 +77,11 @@ abstract class EthereumAPI { null, ); } else { - throw EthApiException( - "getEthTransactions($address) response is empty but status code is " - "${response.statusCode}", + // nice that the api returns an empty body instead of being + // consistent and returning a json object with no transactions + return EthereumResponse( + [], + null, ); } } else { @@ -196,9 +199,11 @@ abstract class EthereumAPI { null, ); } else { - throw EthApiException( - "getEthTransactionNonces($txns) response is empty but status code is " - "${response.statusCode}", + // nice that the api returns an empty body instead of being + // consistent and returning a json object with no transactions + return EthereumResponse( + [], + null, ); } } else { @@ -252,13 +257,13 @@ abstract class EthereumAPI { ); } else { throw EthApiException( - "getEthTransaction($txids) response is empty but status code is " + "getEthTokenTransactionsByTxids($txids) response is empty but status code is " "${response.statusCode}", ); } } else { throw EthApiException( - "getEthTransaction($txids) failed with status code: " + "getEthTokenTransactionsByTxids($txids) failed with status code: " "${response.statusCode}", ); } @@ -269,7 +274,7 @@ abstract class EthereumAPI { ); } catch (e, s) { Logging.instance.log( - "getEthTransaction($txids): $e\n$s", + "getEthTokenTransactionsByTxids($txids): $e\n$s", level: LogLevel.Error, ); return EthereumResponse( @@ -307,9 +312,11 @@ abstract class EthereumAPI { null, ); } else { - throw EthApiException( - "getTokenTransactions($address, $tokenContractAddress) response is empty but status code is " - "${response.statusCode}", + // nice that the api returns an empty body instead of being + // consistent and returning a json object with no transactions + return EthereumResponse( + [], + null, ); } } else { From 87a146b5a4107dee4d742291ebf20ed3d7775a67 Mon Sep 17 00:00:00 2001 From: julian Date: Thu, 20 Jul 2023 11:35:52 -0600 Subject: [PATCH 051/169] show failed instead of cancelled for failed eth tx --- lib/widgets/transaction_card.dart | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/widgets/transaction_card.dart b/lib/widgets/transaction_card.dart index 7791ff3a0..41b81e2da 100644 --- a/lib/widgets/transaction_card.dart +++ b/lib/widgets/transaction_card.dart @@ -230,7 +230,9 @@ class _TransactionCardState extends ConsumerState { fit: BoxFit.scaleDown, child: Text( _transaction.isCancelled - ? "Cancelled" + ? coin == Coin.ethereum + ? "Failed" + : "Cancelled" : whatIsIt( _transaction.type, coin, From 49e5641c3e8fb17ae0d50318a142d6e3cb5e52c2 Mon Sep 17 00:00:00 2001 From: julian Date: Thu, 20 Jul 2023 11:36:16 -0600 Subject: [PATCH 052/169] optional nonce --- lib/dto/ethereum/eth_token_tx_extra_dto.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/dto/ethereum/eth_token_tx_extra_dto.dart b/lib/dto/ethereum/eth_token_tx_extra_dto.dart index 144194914..aab41bb76 100644 --- a/lib/dto/ethereum/eth_token_tx_extra_dto.dart +++ b/lib/dto/ethereum/eth_token_tx_extra_dto.dart @@ -46,7 +46,7 @@ class EthTokenTxExtraDTO { ), gas: _amountFromJsonNum(map['gas']), gasPrice: _amountFromJsonNum(map['gasPrice']), - nonce: map['nonce'] as int, + nonce: map['nonce'] as int?, input: map['input'] as String, gasCost: _amountFromJsonNum(map['gasCost']), gasUsed: _amountFromJsonNum(map['gasUsed']), @@ -63,7 +63,7 @@ class EthTokenTxExtraDTO { final Amount gas; final Amount gasPrice; final String input; - final int nonce; + final int? nonce; final Amount gasCost; final Amount gasUsed; From 69aa1fb8859475e76665de78affa944da92fdd5b Mon Sep 17 00:00:00 2001 From: julian Date: Thu, 20 Jul 2023 11:36:30 -0600 Subject: [PATCH 053/169] toString fix --- lib/dto/ethereum/eth_tx_dto.dart | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/lib/dto/ethereum/eth_tx_dto.dart b/lib/dto/ethereum/eth_tx_dto.dart index 7289cda15..260675cf9 100644 --- a/lib/dto/ethereum/eth_tx_dto.dart +++ b/lib/dto/ethereum/eth_tx_dto.dart @@ -127,16 +127,16 @@ class EthTxDTO { 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['value'] = value.toString(); + map['gas'] = gas.toString(); + map['gasPrice'] = gasPrice.toString(); + map['maxFeePerGas'] = maxFeePerGas.toString(); + map['maxPriorityFeePerGas'] = maxPriorityFeePerGas.toString(); map['isError'] = isError; map['hasToken'] = hasToken; map['compressedTx'] = compressedTx; - map['gasCost'] = gasCost; - map['gasUsed'] = gasUsed; + map['gasCost'] = gasCost.toString(); + map['gasUsed'] = gasUsed.toString(); return map; } From 8a84c898241c26ee43bbc11c4a0f4c1877dee56e Mon Sep 17 00:00:00 2001 From: julian Date: Thu, 20 Jul 2023 15:08:43 -0600 Subject: [PATCH 054/169] show failed instead of cancelled for eth failed txns --- .../transaction_details_view.dart | 53 ++++++++++--------- 1 file changed, 29 insertions(+), 24 deletions(-) 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 e8ccf98de..2b1ba62c4 100644 --- a/lib/pages/wallet_view/transaction_views/transaction_details_view.dart +++ b/lib/pages/wallet_view/transaction_views/transaction_details_view.dart @@ -358,7 +358,6 @@ class _TransactionDetailsViewState final currentHeight = ref.watch(walletsChangeNotifierProvider .select((value) => value.getManager(walletId).currentHeight)); - print("THIS TRANSACTION IS $_transaction"); return ConditionalParent( @@ -474,7 +473,9 @@ class _TransactionDetailsViewState ), SelectableText( _transaction.isCancelled - ? "Cancelled" + ? coin == Coin.ethereum + ? "Failed" + : "Cancelled" : whatIsIt( _transaction, currentHeight, @@ -585,7 +586,9 @@ class _TransactionDetailsViewState // child: SelectableText( _transaction.isCancelled - ? "Cancelled" + ? coin == Coin.ethereum + ? "Failed" + : "Cancelled" : whatIsIt( _transaction, currentHeight, @@ -781,8 +784,8 @@ class _TransactionDetailsViewState isDesktop ? const _Divider() : const SizedBox( - height: 12, - ), + height: 12, + ), if (coin == Coin.epicCash) RoundedWhiteContainer( padding: isDesktop @@ -790,22 +793,22 @@ class _TransactionDetailsViewState : const EdgeInsets.all(12), child: Row( mainAxisAlignment: - MainAxisAlignment.spaceBetween, + MainAxisAlignment.spaceBetween, crossAxisAlignment: CrossAxisAlignment.start, children: [ Expanded( child: Column( crossAxisAlignment: - CrossAxisAlignment.start, + CrossAxisAlignment.start, children: [ Text( "On chain note", style: isDesktop ? STextStyles - .desktopTextExtraExtraSmall( - context) + .desktopTextExtraExtraSmall( + context) : STextStyles.itemSubtitle( - context), + context), ), const SizedBox( height: 8, @@ -814,18 +817,16 @@ class _TransactionDetailsViewState _transaction.otherData ?? "", style: isDesktop ? STextStyles - .desktopTextExtraExtraSmall( - context) - .copyWith( - color: Theme.of( - context) - .extension< - StackColors>()! - .textDark, - ) - : STextStyles - .itemSubtitle12( - context), + .desktopTextExtraExtraSmall( + context) + .copyWith( + color: Theme.of(context) + .extension< + StackColors>()! + .textDark, + ) + : STextStyles.itemSubtitle12( + context), ), ], ), @@ -854,7 +855,9 @@ class _TransactionDetailsViewState MainAxisAlignment.spaceBetween, children: [ Text( - (coin == Coin.epicCash) ? "Local Note" : "Note ", + (coin == Coin.epicCash) + ? "Local Note" + : "Note ", style: isDesktop ? STextStyles .desktopTextExtraExtraSmall( @@ -923,7 +926,9 @@ class _TransactionDetailsViewState notesServiceChangeNotifierProvider( walletId) .select((value) => value.getNoteFor( - txid: (coin == Coin.epicCash)? _transaction.slateId! : _transaction.txid ))), + txid: (coin == Coin.epicCash) + ? _transaction.slateId! + : _transaction.txid))), builder: (builderContext, AsyncSnapshot snapshot) { if (snapshot.connectionState == From 480f6376703a87098f95e4649876c98dd3235588 Mon Sep 17 00:00:00 2001 From: ryleedavis Date: Thu, 20 Jul 2023 15:15:03 -0600 Subject: [PATCH 055/169] WIP: added cardFavoriteImages + check if card should be favorites or wallet --- lib/models/isar/stack_theme.dart | 26 ++++++++++++++++--- .../sub_widgets/wallet_summary.dart | 1 + .../sub_widgets/favorite_card.dart | 1 + lib/themes/coin_card_provider.dart | 10 +++++++ lib/widgets/coin_card.dart | 7 ++++- 5 files changed, 40 insertions(+), 5 deletions(-) diff --git a/lib/models/isar/stack_theme.dart b/lib/models/isar/stack_theme.dart index b005c40ea..66a93ead4 100644 --- a/lib/models/isar/stack_theme.dart +++ b/lib/models/isar/stack_theme.dart @@ -2310,8 +2310,6 @@ class ThemeAssetsV3 implements IThemeAssets { // Added some future proof params in case we want to add anything else // This should provide some buffer in stead of creating assetsV4 etc - @Name("otherStringParam1") - late final String? dummy1; @Name("otherStringParam2") late final String? dummy2; @Name("otherStringParam3") @@ -2357,6 +2355,20 @@ class ThemeAssetsV3 implements IThemeAssets { Map? _coinCardImages; late final String? coinCardImagesString; + + @ignore + Map? get coinCardFavoritesImages => + _coinCardFavoritesImages ??= coinCardFavoritesImagesString == null + ? null + : parseCoinAssetsString( + coinCardFavoritesImagesString!, + placeHolder: coinPlaceholder, + ); + @ignore + Map? _coinCardFavoritesImages; + @Name("otherStringParam1") + late final String? coinCardFavoritesImagesString; + ThemeAssetsV3(); factory ThemeAssetsV3.fromJson({ @@ -2421,13 +2433,18 @@ class ThemeAssetsV3 implements IThemeAssets { Map.from(json["coins"]["cards"] as Map), ) : null + ..coinCardFavoritesImagesString = json["coins"]["favoriteCards"] is Map + ? createCoinAssetsString( + "$applicationThemesDirectoryPath/$themeId/assets", + Map.from(json["coins"]["favoriteCards"] as Map), + ) + : null ..loadingGif = json["loading_gif"] is String ? "$applicationThemesDirectoryPath/$themeId/assets/${json["loading_gif"] as String}" : null ..background = json["background"] is String ? "$applicationThemesDirectoryPath/$themeId/assets/${json["background"] as String}" : null - ..dummy1 = null ..dummy2 = null ..dummy3 = null; } @@ -2483,7 +2500,8 @@ class ThemeAssetsV3 implements IThemeAssets { 'coinIcons: $coinIcons, ' 'coinImages: $coinImages, ' 'coinSecondaryImages: $coinSecondaryImages, ' - 'coinCardImages: $coinCardImages' + 'coinCardWalletImages: $coinCardImages' + 'coinCardFavoritesImages: $coinCardFavoritesImages' ')'; } } diff --git a/lib/pages/wallet_view/sub_widgets/wallet_summary.dart b/lib/pages/wallet_view/sub_widgets/wallet_summary.dart index a82145817..08f5f22c8 100644 --- a/lib/pages/wallet_view/sub_widgets/wallet_summary.dart +++ b/lib/pages/wallet_view/sub_widgets/wallet_summary.dart @@ -52,6 +52,7 @@ class WalletSummary extends StatelessWidget { walletId: walletId, width: constraints.maxWidth, height: constraints.maxHeight, + isFavorite: false, ), Positioned.fill( child: Padding( diff --git a/lib/pages/wallets_view/sub_widgets/favorite_card.dart b/lib/pages/wallets_view/sub_widgets/favorite_card.dart index b3f5e97e5..fe8a6691d 100644 --- a/lib/pages/wallets_view/sub_widgets/favorite_card.dart +++ b/lib/pages/wallets_view/sub_widgets/favorite_card.dart @@ -149,6 +149,7 @@ class _FavoriteCardState extends ConsumerState { walletId: widget.walletId, width: widget.width, height: widget.height, + isFavorite: false, ), child: Padding( padding: const EdgeInsets.all(12.0), diff --git a/lib/themes/coin_card_provider.dart b/lib/themes/coin_card_provider.dart index fc84faebd..ea0dbc7fb 100644 --- a/lib/themes/coin_card_provider.dart +++ b/lib/themes/coin_card_provider.dart @@ -22,3 +22,13 @@ final coinCardProvider = Provider.family((ref, coin) { return null; } }); + +final coinCardFavoritesProvider = Provider.family((ref, coin) { + final assets = ref.watch(themeAssetsProvider); + + if (assets is ThemeAssetsV3) { + return assets.coinCardFavoritesImages?[coin.mainNetVersion]; + } else { + return null; + } +}); diff --git a/lib/widgets/coin_card.dart b/lib/widgets/coin_card.dart index 0bebc5fd8..4ce95b530 100644 --- a/lib/widgets/coin_card.dart +++ b/lib/widgets/coin_card.dart @@ -25,11 +25,13 @@ class CoinCard extends ConsumerWidget { required this.walletId, required this.width, required this.height, + required this.isFavorite, }); final String walletId; final double width; final double height; + final bool isFavorite; @override Widget build(BuildContext context, WidgetRef ref) { @@ -39,6 +41,7 @@ class CoinCard extends ConsumerWidget { ); final bool hasCardImageBg = ref.watch(coinCardProvider(coin)) != null; + final isFavorite = false; return Stack( children: [ @@ -54,7 +57,9 @@ class CoinCard extends ConsumerWidget { fit: BoxFit.cover, image: FileImage( File( - ref.watch(coinCardProvider(coin))!, + (isFavorite) + ? ref.watch(coinCardFavoritesProvider(coin))! + : ref.watch(coinCardProvider(coin))!, ), ), ), From 4db0328c73a859f4550fedafd3486e508a599921 Mon Sep 17 00:00:00 2001 From: sneurlax Date: Thu, 20 Jul 2023 16:30:39 -0500 Subject: [PATCH 056/169] WIP _db not initialized, refresh on ordinals view for poc --- .../address_inscription_response.dart | 58 +------------------ lib/dto/ordinals/inscription_data.dart | 53 +++++++++++++++++ lib/pages/ordinals/ordinals_view.dart | 6 +- .../coins/litecoin/litecoin_wallet.dart | 1 + lib/services/litescribe_api.dart | 8 +-- lib/services/mixins/ordinals_interface.dart | 52 +++++++++-------- 6 files changed, 93 insertions(+), 85 deletions(-) create mode 100644 lib/dto/ordinals/inscription_data.dart diff --git a/lib/dto/ordinals/address_inscription_response.dart b/lib/dto/ordinals/address_inscription_response.dart index c424d73e0..240374284 100644 --- a/lib/dto/ordinals/address_inscription_response.dart +++ b/lib/dto/ordinals/address_inscription_response.dart @@ -1,4 +1,5 @@ import 'package:stackwallet/dto/ordinals/litescribe_response.dart'; +import 'package:stackwallet/dto/ordinals/inscription_data.dart'; class AddressInscriptionResponse extends LitescribeResponse { final int status; @@ -21,7 +22,7 @@ class AddressInscriptionResponse extends LitescribeResponse list; + final List list; final int total; AddressInscriptionResult({ @@ -31,61 +32,8 @@ class AddressInscriptionResult { factory AddressInscriptionResult.fromJson(Map json) { return AddressInscriptionResult( - list: (json['list'] as List).map((item) => AddressInscription.fromJson(item as Map)).toList(), + list: (json['list'] as List).map((item) => InscriptionData.fromJson(item as Map)).toList(), total: json['total'] as int, ); } } - -class AddressInscription { - final String inscriptionId; - final int inscriptionNumber; - final String address; - final String preview; - final String content; - final int contentLength; - final String contentType; - final String contentBody; - final int timestamp; - final String genesisTransaction; - final String location; - final String output; - final int outputValue; - final int offset; - - AddressInscription({ - required this.inscriptionId, - required this.inscriptionNumber, - required this.address, - required this.preview, - required this.content, - required this.contentLength, - required this.contentType, - required this.contentBody, - required this.timestamp, - required this.genesisTransaction, - required this.location, - required this.output, - required this.outputValue, - required this.offset, - }); - - factory AddressInscription.fromJson(Map json) { - return AddressInscription( - inscriptionId: json['inscriptionId'] as String, - inscriptionNumber: json['inscriptionNumber'] as int, - address: json['address'] as String, - preview: json['preview'] as String, - content: json['content'] as String, - contentLength: json['contentLength'] as int, - contentType: json['contentType'] as String, - contentBody: json['contentBody'] as String, - timestamp: json['timestamp'] as int, - genesisTransaction: json['genesisTransaction'] as String, - location: json['location'] as String, - output: json['output'] as String, - outputValue: json['outputValue'] as int, - offset: json['offset'] as int, - ); - } -} diff --git a/lib/dto/ordinals/inscription_data.dart b/lib/dto/ordinals/inscription_data.dart new file mode 100644 index 000000000..b7bba8697 --- /dev/null +++ b/lib/dto/ordinals/inscription_data.dart @@ -0,0 +1,53 @@ +// inscription data from litescribe /address/inscriptions endpoint +class InscriptionData { + final String inscriptionId; + final int inscriptionNumber; + final String address; + final String preview; + final String content; + final int contentLength; + final String contentType; + final String contentBody; + final int timestamp; + final String genesisTransaction; + final String location; + final String output; + final int outputValue; + final int offset; + + InscriptionData({ + required this.inscriptionId, + required this.inscriptionNumber, + required this.address, + required this.preview, + required this.content, + required this.contentLength, + required this.contentType, + required this.contentBody, + required this.timestamp, + required this.genesisTransaction, + required this.location, + required this.output, + required this.outputValue, + required this.offset, + }); + + factory InscriptionData.fromJson(Map json) { + return InscriptionData( + inscriptionId: json['inscriptionId'] as String, + inscriptionNumber: json['inscriptionNumber'] as int, + address: json['address'] as String, + preview: json['preview'] as String, + content: json['content'] as String, + contentLength: json['contentLength'] as int, + contentType: json['contentType'] as String, + contentBody: json['contentBody'] as String, + timestamp: json['timestamp'] as int, + genesisTransaction: json['genesisTransaction'] as String, + location: json['location'] as String, + output: json['output'] as String, + outputValue: json['outputValue'] as int, + offset: json['offset'] as int, + ); + } +} diff --git a/lib/pages/ordinals/ordinals_view.dart b/lib/pages/ordinals/ordinals_view.dart index 7a0572c78..f5f3ca23a 100644 --- a/lib/pages/ordinals/ordinals_view.dart +++ b/lib/pages/ordinals/ordinals_view.dart @@ -14,8 +14,9 @@ import 'package:flutter_svg/svg.dart'; import 'package:stackwallet/models/ordinal.dart'; import 'package:stackwallet/pages/ordinals/ordinals_filter_view.dart'; import 'package:stackwallet/pages/ordinals/widgets/ordinals_list.dart'; -import 'package:stackwallet/services/litescribe_api.dart'; -import 'package:stackwallet/services/ordinals_api.dart'; +import 'package:stackwallet/providers/global/wallets_provider.dart'; +// import 'package:stackwallet/services/litescribe_api.dart'; +// import 'package:stackwallet/services/ordinals_api.dart'; import 'package:stackwallet/services/mixins/ordinals_interface.dart'; import 'package:stackwallet/themes/stack_colors.dart'; import 'package:stackwallet/utilities/assets.dart'; @@ -28,6 +29,7 @@ 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 OrdinalsView extends ConsumerStatefulWidget { const OrdinalsView({ super.key, diff --git a/lib/services/coins/litecoin/litecoin_wallet.dart b/lib/services/coins/litecoin/litecoin_wallet.dart index 2edc32fc6..944182abd 100644 --- a/lib/services/coins/litecoin/litecoin_wallet.dart +++ b/lib/services/coins/litecoin/litecoin_wallet.dart @@ -136,6 +136,7 @@ class LitecoinWallet extends CoinServiceAPI _secureStore = secureStore; initCache(walletId, coin); initWalletDB(mockableOverride: mockableOverride); + initOrdinalsInterface(walletId:walletId, coin: coin, db: db); initCoinControlInterface( walletId: walletId, walletName: walletName, diff --git a/lib/services/litescribe_api.dart b/lib/services/litescribe_api.dart index abc28123e..f7634b387 100644 --- a/lib/services/litescribe_api.dart +++ b/lib/services/litescribe_api.dart @@ -1,7 +1,7 @@ import 'dart:convert'; import 'package:http/http.dart' as http; -import 'package:stackwallet/dto/ordinals/address_inscription_response.dart'; +import 'package:stackwallet/dto/ordinals/inscription_data.dart'; import 'package:stackwallet/dto/ordinals/litescribe_response.dart'; class LitescribeAPI { @@ -34,7 +34,7 @@ class LitescribeAPI { } } - Future getInscriptionsByAddress(String address, {int cursor = 0, int size = 1000}) async { + Future> getInscriptionsByAddress(String address, {int cursor = 0, int size = 1000}) async { // size param determines how many inscriptions are returned per response // default of 1000 is used to cover most addresses (I assume) // if the total number of inscriptions at the address exceeds the length of the list of inscriptions returned, another call with a higher size is made @@ -42,7 +42,7 @@ class LitescribeAPI { final response = await _getResponse('/address/inscriptions?address=$address&cursor=$cursor&size=$size'); // Check if the number of returned inscriptions equals the limit - final list = response.data['result']['list'] as List; + final list = response.data['result']['list'] as List; final int total = response.data['result']['total'] as int; final int currentSize = list.length; @@ -54,7 +54,7 @@ class LitescribeAPI { // TODO test logic with smaller size "pagination" } else { try { - return AddressInscriptionResponse.fromJson(response.data as Map); + return list; } catch (e) { throw const FormatException('LitescribeAPI getInscriptionsByAddress exception: AddressInscriptionResponse.fromJson failure'); } diff --git a/lib/services/mixins/ordinals_interface.dart b/lib/services/mixins/ordinals_interface.dart index caac7a854..fbd41d80b 100644 --- a/lib/services/mixins/ordinals_interface.dart +++ b/lib/services/mixins/ordinals_interface.dart @@ -15,8 +15,8 @@ import 'package:stackwallet/utilities/enums/coin_enum.dart'; // import 'package:stackwallet/dto/ordinals/preview_response.dart'; // import 'package:stackwallet/services/ordinals_api.dart'; -import 'package:stackwallet/dto/ordinals/address_inscription_response.dart'; // verbose due to Litescribe being the 2nd API import 'package:stackwallet/services/litescribe_api.dart'; +import 'package:stackwallet/dto/ordinals/inscription_data.dart'; mixin OrdinalsInterface { @@ -29,39 +29,29 @@ mixin OrdinalsInterface { required Coin coin, required MainDB db, }) { + print('init'); _walletId = walletId; _coin = coin; _db = db; } final LitescribeAPI litescribeAPI = LitescribeAPI(baseUrl: 'https://litescribe.io/api'); - Future> getInscriptionsByAddress(String address) async { - try { - var response = await litescribeAPI.getInscriptionsByAddress(address); - print("Found ${response.result.total} inscription${response.result.total > 1 ? 's' : ''} at address $address"); // TODO disable (POC) - return response.result.list; - } catch (e) { - throw Exception('Error in OrdinalsInterface getInscriptionsByAddress: $e'); - } - } + // Future> getInscriptionsByAddress(String address) async { + // try { + // var response = await litescribeAPI.getInscriptionsByAddress(address); + // // print("Found ${response.result.total} inscription${response.result.total > 1 ? 's' : ''} at address $address"); // TODO disable (POC) + // return response.result.list; + // } catch (e) { + // throw Exception('Error in OrdinalsInterface getInscriptionsByAddress: $e'); + // } + // } void refreshInscriptions() async { List _inscriptions; final utxos = await _db.getUTXOs(_walletId).findAll(); final uniqueAddresses = getUniqueAddressesFromUTXOs(utxos); - for (String address in uniqueAddresses) { - // TODO fetch all inscriptions from all addresses - // TODO save those inscriptions to isar, which a StreamBuilder will be "subscribed"-to - } - // TODO get all inscriptions at all addresses in wallet - var inscriptions = await getInscriptionsByAddress('ltc1qk4e8hdq5w6rvk5xvkxajjak78v45pkul8a2cg9'); - for (var inscription in inscriptions) { - print(inscription); - print(inscription.address); - print(inscription.content); - print(inscription.inscriptionId); - print(inscription.inscriptionNumber); - } + _inscriptions = await getAllInscriptionsFromAddresses(uniqueAddresses); + // TODO save inscriptions to isar which gets watched by a StreamBuilder } List getUniqueAddressesFromUTXOs(List utxos) { @@ -74,7 +64,21 @@ mixin OrdinalsInterface { return uniqueAddresses.toList(); } - /* // ord-litecoin interface + Future> getAllInscriptionsFromAddresses(List addresses) async { + List allInscriptions = []; + for (String address in addresses) { + try { + var inscriptions = await litescribeAPI.getInscriptionsByAddress(address); + print("Found ${inscriptions.length} inscription${inscriptions.length > 1 ? 's' : ''} at address $address"); + allInscriptions.addAll(inscriptions); + } catch (e) { + print("Error fetching inscriptions for address $address: $e"); + } + } + return allInscriptions; + } + +/* // ord-litecoin interface final OrdinalsAPI ordinalsAPI = OrdinalsAPI(baseUrl: 'https://ord-litecoin.stackwallet.com'); Future fetchLatestInscriptions() async { From b773811eac6007dd878b4337711e7b2df963cdaa Mon Sep 17 00:00:00 2001 From: sneurlax Date: Thu, 20 Jul 2023 16:49:26 -0500 Subject: [PATCH 057/169] working proof of concept --- lib/pages/ordinals/ordinals_view.dart | 10 +++++----- lib/services/litescribe_api.dart | 11 ++++++++--- lib/services/mixins/ordinals_interface.dart | 7 ++++--- 3 files changed, 17 insertions(+), 11 deletions(-) diff --git a/lib/pages/ordinals/ordinals_view.dart b/lib/pages/ordinals/ordinals_view.dart index f5f3ca23a..8993ed4e7 100644 --- a/lib/pages/ordinals/ordinals_view.dart +++ b/lib/pages/ordinals/ordinals_view.dart @@ -15,8 +15,6 @@ import 'package:stackwallet/models/ordinal.dart'; import 'package:stackwallet/pages/ordinals/ordinals_filter_view.dart'; import 'package:stackwallet/pages/ordinals/widgets/ordinals_list.dart'; import 'package:stackwallet/providers/global/wallets_provider.dart'; -// import 'package:stackwallet/services/litescribe_api.dart'; -// import 'package:stackwallet/services/ordinals_api.dart'; import 'package:stackwallet/services/mixins/ordinals_interface.dart'; import 'package:stackwallet/themes/stack_colors.dart'; import 'package:stackwallet/utilities/assets.dart'; @@ -29,7 +27,6 @@ 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 OrdinalsView extends ConsumerStatefulWidget { const OrdinalsView({ super.key, @@ -44,7 +41,7 @@ class OrdinalsView extends ConsumerStatefulWidget { ConsumerState createState() => _OrdinalsViewState(); } -class _OrdinalsViewState extends ConsumerState with OrdinalsInterface { +class _OrdinalsViewState extends ConsumerState { late final TextEditingController searchController; late final FocusNode searchFocus; @@ -94,7 +91,10 @@ class _OrdinalsViewState extends ConsumerState with OrdinalsInterf .topNavIconPrimary, ), onPressed: () async { - refreshInscriptions(); + final manager = ref.watch(walletsChangeNotifierProvider + .select((value) => value.getManager(widget.walletId))); + + (manager.wallet as OrdinalsInterface).refreshInscriptions(); }, ), ), diff --git a/lib/services/litescribe_api.dart b/lib/services/litescribe_api.dart index f7634b387..7fc92910d 100644 --- a/lib/services/litescribe_api.dart +++ b/lib/services/litescribe_api.dart @@ -42,7 +42,7 @@ class LitescribeAPI { final response = await _getResponse('/address/inscriptions?address=$address&cursor=$cursor&size=$size'); // Check if the number of returned inscriptions equals the limit - final list = response.data['result']['list'] as List; + final list = response.data['result']['list']; final int total = response.data['result']['total'] as int; final int currentSize = list.length; @@ -51,10 +51,15 @@ class LitescribeAPI { // increment the cursor and make the next API call to fetch the remaining inscriptions. final int newCursor = cursor + size; return getInscriptionsByAddress(address, cursor: newCursor, size: size); - // TODO test logic with smaller size "pagination" + } else { try { - return list; + // Iterate through the list and create InscriptionData objects from each element + final List inscriptions = (list as List) + .map((json) => InscriptionData.fromJson(json as Map)) + .toList(); + + return inscriptions; } catch (e) { throw const FormatException('LitescribeAPI getInscriptionsByAddress exception: AddressInscriptionResponse.fromJson failure'); } diff --git a/lib/services/mixins/ordinals_interface.dart b/lib/services/mixins/ordinals_interface.dart index fbd41d80b..a04b6522c 100644 --- a/lib/services/mixins/ordinals_interface.dart +++ b/lib/services/mixins/ordinals_interface.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:isar/isar.dart'; import 'package:stackwallet/db/isar/main_db.dart'; import 'package:stackwallet/models/isar/models/blockchain_data/utxo.dart'; @@ -18,7 +20,6 @@ import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:stackwallet/services/litescribe_api.dart'; import 'package:stackwallet/dto/ordinals/inscription_data.dart'; - mixin OrdinalsInterface { late final String _walletId; late final Coin _coin; @@ -50,7 +51,7 @@ mixin OrdinalsInterface { List _inscriptions; final utxos = await _db.getUTXOs(_walletId).findAll(); final uniqueAddresses = getUniqueAddressesFromUTXOs(utxos); - _inscriptions = await getAllInscriptionsFromAddresses(uniqueAddresses); + _inscriptions = await getInscriptionsFromAddresses(uniqueAddresses); // TODO save inscriptions to isar which gets watched by a StreamBuilder } @@ -64,7 +65,7 @@ mixin OrdinalsInterface { return uniqueAddresses.toList(); } - Future> getAllInscriptionsFromAddresses(List addresses) async { + Future> getInscriptionsFromAddresses(List addresses) async { List allInscriptions = []; for (String address in addresses) { try { From 24d443886eab3b2a252875b3c1ad38c81ce8ca22 Mon Sep 17 00:00:00 2001 From: julian Date: Thu, 20 Jul 2023 16:04:01 -0600 Subject: [PATCH 058/169] hide empty tx note field on mobile confirm send screen --- .../send_view/confirm_transaction_view.dart | 81 ++++++++++--------- 1 file changed, 42 insertions(+), 39 deletions(-) diff --git a/lib/pages/send_view/confirm_transaction_view.dart b/lib/pages/send_view/confirm_transaction_view.dart index 3193e7bb7..a1fc66f21 100644 --- a/lib/pages/send_view/confirm_transaction_view.dart +++ b/lib/pages/send_view/confirm_transaction_view.dart @@ -493,51 +493,54 @@ class _ConfirmTransactionViewState ], ), ), - if (coin == Coin.epicCash) + if (coin == Coin.epicCash && + (transactionInfo["onChainNote"] as String).isNotEmpty) const SizedBox( height: 12, ), - if (coin == Coin.epicCash) + if (coin == Coin.epicCash && + (transactionInfo["onChainNote"] as String).isNotEmpty) RoundedWhiteContainer( - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Text( - "On chain note", - style: STextStyles.smallMed12(context), - ), - const SizedBox( - height: 4, - ), - Text( - transactionInfo["onChainNote"] as String, - style: STextStyles.itemSubtitle12(context), - ), - ], + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Text( + "On chain note", + style: STextStyles.smallMed12(context), + ), + const SizedBox( + height: 4, + ), + Text( + transactionInfo["onChainNote"] as String, + style: STextStyles.itemSubtitle12(context), + ), + ], + ), ), - ), - const SizedBox( - height: 12, - ), - RoundedWhiteContainer( - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Text( - (coin == Coin.epicCash) ? "Local Note" : - "Note", - style: STextStyles.smallMed12(context), - ), - const SizedBox( - height: 4, - ), - Text( - transactionInfo["note"] as String, - style: STextStyles.itemSubtitle12(context), - ), - ], + if ((transactionInfo["note"] as String).isNotEmpty) + const SizedBox( + height: 12, + ), + if ((transactionInfo["note"] as String).isNotEmpty) + RoundedWhiteContainer( + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Text( + (coin == Coin.epicCash) ? "Local Note" : "Note", + style: STextStyles.smallMed12(context), + ), + const SizedBox( + height: 4, + ), + Text( + transactionInfo["note"] as String, + style: STextStyles.itemSubtitle12(context), + ), + ], + ), ), - ), ], ), if (isDesktop) From 20fdcf48174c8cb102e950fd3f8ed146be96998a Mon Sep 17 00:00:00 2001 From: sneurlax Date: Thu, 20 Jul 2023 17:15:25 -0500 Subject: [PATCH 059/169] refactor manager var out to _manager, comment update --- lib/pages/ordinals/ordinals_view.dart | 9 +++++---- lib/services/mixins/ordinals_interface.dart | 2 +- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/lib/pages/ordinals/ordinals_view.dart b/lib/pages/ordinals/ordinals_view.dart index 8993ed4e7..bf0545d4c 100644 --- a/lib/pages/ordinals/ordinals_view.dart +++ b/lib/pages/ordinals/ordinals_view.dart @@ -46,12 +46,16 @@ class _OrdinalsViewState extends ConsumerState { late final FocusNode searchFocus; String _searchTerm = ""; + dynamic _manager; @override void initState() { searchController = TextEditingController(); searchFocus = FocusNode(); + _manager = ref.watch(walletsChangeNotifierProvider + .select((value) => value.getManager(widget.walletId))); + super.initState(); } @@ -91,10 +95,7 @@ class _OrdinalsViewState extends ConsumerState { .topNavIconPrimary, ), onPressed: () async { - final manager = ref.watch(walletsChangeNotifierProvider - .select((value) => value.getManager(widget.walletId))); - - (manager.wallet as OrdinalsInterface).refreshInscriptions(); + (_manager.wallet as OrdinalsInterface).refreshInscriptions(); }, ), ), diff --git a/lib/services/mixins/ordinals_interface.dart b/lib/services/mixins/ordinals_interface.dart index a04b6522c..2d887bfcd 100644 --- a/lib/services/mixins/ordinals_interface.dart +++ b/lib/services/mixins/ordinals_interface.dart @@ -52,7 +52,7 @@ mixin OrdinalsInterface { final utxos = await _db.getUTXOs(_walletId).findAll(); final uniqueAddresses = getUniqueAddressesFromUTXOs(utxos); _inscriptions = await getInscriptionsFromAddresses(uniqueAddresses); - // TODO save inscriptions to isar which gets watched by a StreamBuilder + // TODO save inscriptions to isar which gets watched by a FutureBuilder/StreamBuilder } List getUniqueAddressesFromUTXOs(List utxos) { From 0ce86ce7fe166b23bdff16b28073206443297539 Mon Sep 17 00:00:00 2001 From: ryleedavis Date: Thu, 20 Jul 2023 16:51:37 -0600 Subject: [PATCH 060/169] Chan card images for favorites and wallet --- lib/pages/wallets_view/sub_widgets/favorite_card.dart | 2 +- lib/themes/coin_card_provider.dart | 3 ++- lib/widgets/coin_card.dart | 3 +-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/pages/wallets_view/sub_widgets/favorite_card.dart b/lib/pages/wallets_view/sub_widgets/favorite_card.dart index fe8a6691d..107b323e0 100644 --- a/lib/pages/wallets_view/sub_widgets/favorite_card.dart +++ b/lib/pages/wallets_view/sub_widgets/favorite_card.dart @@ -149,7 +149,7 @@ class _FavoriteCardState extends ConsumerState { walletId: widget.walletId, width: widget.width, height: widget.height, - isFavorite: false, + isFavorite: true, ), child: Padding( padding: const EdgeInsets.all(12.0), diff --git a/lib/themes/coin_card_provider.dart b/lib/themes/coin_card_provider.dart index ea0dbc7fb..b34e9e6f1 100644 --- a/lib/themes/coin_card_provider.dart +++ b/lib/themes/coin_card_provider.dart @@ -27,7 +27,8 @@ final coinCardFavoritesProvider = Provider.family((ref, coin) { final assets = ref.watch(themeAssetsProvider); if (assets is ThemeAssetsV3) { - return assets.coinCardFavoritesImages?[coin.mainNetVersion]; + return assets.coinCardFavoritesImages?[coin.mainNetVersion] ?? + assets.coinCardImages?[coin.mainNetVersion]; } else { return null; } diff --git a/lib/widgets/coin_card.dart b/lib/widgets/coin_card.dart index 4ce95b530..ebc67bb4e 100644 --- a/lib/widgets/coin_card.dart +++ b/lib/widgets/coin_card.dart @@ -40,8 +40,7 @@ class CoinCard extends ConsumerWidget { .select((value) => value.getManager(walletId).coin), ); - final bool hasCardImageBg = ref.watch(coinCardProvider(coin)) != null; - final isFavorite = false; + final bool hasCardImageBg = (isFavorite) ? ref.watch(coinCardFavoritesProvider(coin)) != null : ref.watch(coinCardProvider(coin)) != null; return Stack( children: [ From 28f8fdc2331f2cf1f92876eb386d9c8136826d2b Mon Sep 17 00:00:00 2001 From: julian Date: Thu, 20 Jul 2023 16:15:23 -0600 Subject: [PATCH 061/169] clean up --- lib/pages_desktop_specific/password/create_password_view.dart | 2 +- lib/pages_desktop_specific/password/desktop_login_view.dart | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/pages_desktop_specific/password/create_password_view.dart b/lib/pages_desktop_specific/password/create_password_view.dart index 68205d077..2986cd0da 100644 --- a/lib/pages_desktop_specific/password/create_password_view.dart +++ b/lib/pages_desktop_specific/password/create_password_view.dart @@ -125,7 +125,7 @@ class _CreatePasswordViewState extends ConsumerState { } } - if (!widget.restoreFromSWB) { + if (!widget.restoreFromSWB && mounted) { unawaited(showFloatingFlushBar( type: FlushBarType.success, message: "Your password is set up", diff --git a/lib/pages_desktop_specific/password/desktop_login_view.dart b/lib/pages_desktop_specific/password/desktop_login_view.dart index 10e1477b4..471da2763 100644 --- a/lib/pages_desktop_specific/password/desktop_login_view.dart +++ b/lib/pages_desktop_specific/password/desktop_login_view.dart @@ -84,10 +84,10 @@ class _DesktopLoginViewState extends ConsumerState { unawaited( showDialog( context: context, - builder: (context) => Column( + builder: (context) => const Column( mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center, - children: const [ + children: [ LoadingIndicator( width: 200, height: 200, From af30826e9e9fa33502a5ac583eba14d0d4ff8dcd Mon Sep 17 00:00:00 2001 From: sneurlax Date: Fri, 21 Jul 2023 10:03:05 -0500 Subject: [PATCH 062/169] remove ord-litecoin imports --- lib/dto/ordinals/address_response.dart | 52 ----------- lib/dto/ordinals/block_response.dart | 61 ------------- lib/dto/ordinals/content_response.dart | 22 ----- lib/dto/ordinals/feed_response.dart | 17 ---- lib/dto/ordinals/inscription_link.dart | 13 --- lib/dto/ordinals/inscription_response.dart | 97 --------------------- lib/dto/ordinals/ordinals_response.dart | 6 -- lib/dto/ordinals/output_response.dart | 47 ---------- lib/dto/ordinals/preview_response.dart | 22 ----- lib/dto/ordinals/sat_response.dart | 85 ------------------ lib/dto/ordinals/transaction_response.dart | 82 ----------------- lib/services/mixins/ordinals_interface.dart | 12 --- 12 files changed, 516 deletions(-) delete mode 100644 lib/dto/ordinals/address_response.dart delete mode 100644 lib/dto/ordinals/block_response.dart delete mode 100644 lib/dto/ordinals/content_response.dart delete mode 100644 lib/dto/ordinals/feed_response.dart delete mode 100644 lib/dto/ordinals/inscription_link.dart delete mode 100644 lib/dto/ordinals/inscription_response.dart delete mode 100644 lib/dto/ordinals/ordinals_response.dart delete mode 100644 lib/dto/ordinals/output_response.dart delete mode 100644 lib/dto/ordinals/preview_response.dart delete mode 100644 lib/dto/ordinals/sat_response.dart delete mode 100644 lib/dto/ordinals/transaction_response.dart diff --git a/lib/dto/ordinals/address_response.dart b/lib/dto/ordinals/address_response.dart deleted file mode 100644 index 9136aa523..000000000 --- a/lib/dto/ordinals/address_response.dart +++ /dev/null @@ -1,52 +0,0 @@ -import 'package:stackwallet/dto/ordinals/inscription_link.dart'; -import 'package:stackwallet/dto/ordinals/ordinals_response.dart'; - -class AddressResponse extends OrdinalsResponse { - final AddressLinks links; - final String address; - final List inscriptions; - - AddressResponse({ - required this.links, - required this.address, - required this.inscriptions, - }); - - factory AddressResponse.fromJson(OrdinalsResponse json) { - final data = json.data as Map; - final inscriptionsJson = data['inscriptions'] as List; - final inscriptions = inscriptionsJson - .map((inscriptionJson) => InscriptionLink.fromJson(inscriptionJson as Map)) - .toList(); - - return AddressResponse( - links: AddressLinks.fromJson(data['_links'] as Map), - address: data['address'] as String, - inscriptions: inscriptions, - ); - } -} - -class AddressLinks { - final AddressLink? self; - - AddressLinks({ - this.self, - }); - - factory AddressLinks.fromJson(Map json) { - return AddressLinks( - self: json['self'] != null ? AddressLink.fromJson(json['self'] as Map) : null, - ); - } -} - -class AddressLink { - final String href; - - AddressLink({required this.href}); - - factory AddressLink.fromJson(Map json) { - return AddressLink(href: json['href'] as String); - } -} diff --git a/lib/dto/ordinals/block_response.dart b/lib/dto/ordinals/block_response.dart deleted file mode 100644 index 0eef8d569..000000000 --- a/lib/dto/ordinals/block_response.dart +++ /dev/null @@ -1,61 +0,0 @@ -import 'package:stackwallet/dto/ordinals/ordinals_response.dart'; - -class BlockResponse extends OrdinalsResponse { - final BlockLinks links; - final String hash; - final String previousBlockhash; - final int size; - final String target; - final String timestamp; - final int weight; - - BlockResponse({ - required this.links, - required this.hash, - required this.previousBlockhash, - required this.size, - required this.target, - required this.timestamp, - required this.weight, - }); - - factory BlockResponse.fromJson(OrdinalsResponse json) { - final data = json.data as Map; - return BlockResponse( - links: BlockLinks.fromJson(data['_links'] as Map), - hash: data['hash'] as String, - previousBlockhash: data['previous_blockhash'] as String, - size: data['size'] as int, - target: data['target'] as String, - timestamp: data['timestamp'] as String, - weight: data['weight'] as int, - ); - } -} - -class BlockLinks { - final BlockLink? prev; - final BlockLink? self; - - BlockLinks({ - this.prev, - this.self, - }); - - factory BlockLinks.fromJson(Map json) { - return BlockLinks( - prev: json['prev'] != null ? BlockLink.fromJson(json['prev'] as Map) : null, - self: json['self'] != null ? BlockLink.fromJson(json['self'] as Map) : null, - ); - } -} - -class BlockLink { - final String href; - - BlockLink({required this.href}); - - factory BlockLink.fromJson(Map json) { - return BlockLink(href: json['href'] as String); - } -} diff --git a/lib/dto/ordinals/content_response.dart b/lib/dto/ordinals/content_response.dart deleted file mode 100644 index 7cfbaf9fd..000000000 --- a/lib/dto/ordinals/content_response.dart +++ /dev/null @@ -1,22 +0,0 @@ -import 'package:stackwallet/dto/ordinals/ordinals_response.dart'; - -class ContentResponse extends OrdinalsResponse { - final FileLink fileLink; - - ContentResponse({required this.fileLink}); - - factory ContentResponse.fromJson(OrdinalsResponse json) { - final data = json.data as Map; - return ContentResponse(fileLink: FileLink.fromJson(data['_links']['file'] as Map)); // TODO don't cast as Map - } -} - -class FileLink { - final String href; - - FileLink({required this.href}); - - factory FileLink.fromJson(Map json) { - return FileLink(href: json['href'] as String); - } -} diff --git a/lib/dto/ordinals/feed_response.dart b/lib/dto/ordinals/feed_response.dart deleted file mode 100644 index 525a7f727..000000000 --- a/lib/dto/ordinals/feed_response.dart +++ /dev/null @@ -1,17 +0,0 @@ -import 'package:stackwallet/dto/ordinals/inscription_link.dart'; -import 'package:stackwallet/dto/ordinals/ordinals_response.dart'; - -class FeedResponse extends OrdinalsResponse { - final List inscriptions; - - FeedResponse({required this.inscriptions}); - - factory FeedResponse.fromJson(OrdinalsResponse json) { - final List inscriptionsJson = json.data['_links']['inscriptions'] as List; - final List inscriptions = inscriptionsJson - .map((json) => InscriptionLink.fromJson(json as Map)) - .toList(); - - return FeedResponse(inscriptions: inscriptions); - } -} \ No newline at end of file diff --git a/lib/dto/ordinals/inscription_link.dart b/lib/dto/ordinals/inscription_link.dart deleted file mode 100644 index f23b63248..000000000 --- a/lib/dto/ordinals/inscription_link.dart +++ /dev/null @@ -1,13 +0,0 @@ -class InscriptionLink { - final String href; - final String title; - - InscriptionLink({required this.href, required this.title}); - - factory InscriptionLink.fromJson(Map json) { - return InscriptionLink( - href: json['href'] as String ?? '', - title: json['title'] as String ?? '', - ); - } -} diff --git a/lib/dto/ordinals/inscription_response.dart b/lib/dto/ordinals/inscription_response.dart deleted file mode 100644 index d45300aee..000000000 --- a/lib/dto/ordinals/inscription_response.dart +++ /dev/null @@ -1,97 +0,0 @@ -import 'package:stackwallet/dto/ordinals/ordinals_response.dart'; - -class InscriptionResponse extends OrdinalsResponse { - late final Links links; - late final String address; - late final int contentLength; - late final String contentType; - late final int genesisFee; - late final int genesisHeight; - late final String genesisTransaction; - late final String location; - late final int number; - late final int offset; - late final String output; - late final String? sat; // Make sure to update the type to allow null - late final String timestamp; - - InscriptionResponse({ - required this.links, - required this.address, - required this.contentLength, - required this.contentType, - required this.genesisFee, - required this.genesisHeight, - required this.genesisTransaction, - required this.location, - required this.number, - required this.offset, - required this.output, - required this.sat, - required this.timestamp, - }); - - factory InscriptionResponse.fromJson(OrdinalsResponse json) { - final data = json.data as Map; - return InscriptionResponse( - links: Links.fromJson(data['_links'] as Map), - address: data['address'] as String, - contentLength: data['content_length'] as int, - contentType: data['content_type'] as String, - genesisFee: data['genesis_fee'] as int, - genesisHeight: data['genesis_height'] as int, - genesisTransaction: data['genesis_transaction'] as String, - location: data['location'] as String, - number: data['number'] as int, - offset: data['offset'] as int, - output: data['output'] as String, - sat: data['sat'] as String?, - timestamp: data['timestamp'] as String, - ); - } -} - -class Links { - late final Link content; - late final Link genesisTransaction; - late final Link next; - late final Link output; - late final Link prev; - late final Link preview; - late final Link? sat; // Make sure to update the type to allow null - late final Link self; - - Links({ - required this.content, - required this.genesisTransaction, - required this.next, - required this.output, - required this.prev, - required this.preview, - this.sat, - required this.self, - }); - - factory Links.fromJson(Map json) { - return Links( - content: Link.fromJson(json['content'] as Map), - genesisTransaction: Link.fromJson(json['genesis_transaction'] as Map), - next: Link.fromJson(json['next'] as Map), - output: Link.fromJson(json['output'] as Map), - prev: Link.fromJson(json['prev'] as Map), - preview: Link.fromJson(json['preview'] as Map), - sat: json['sat'] != null ? Link.fromJson(json['sat'] as Map) : null, - self: Link.fromJson(json['self'] as Map), - ); - } -} - -class Link { - late final String href; - - Link({required this.href}); - - factory Link.fromJson(Map json) { - return Link(href: json['href'] as String); - } -} diff --git a/lib/dto/ordinals/ordinals_response.dart b/lib/dto/ordinals/ordinals_response.dart deleted file mode 100644 index bf57db46b..000000000 --- a/lib/dto/ordinals/ordinals_response.dart +++ /dev/null @@ -1,6 +0,0 @@ -class OrdinalsResponse { - final T? data; - final String? error; - - OrdinalsResponse({this.data, this.error}); -} diff --git a/lib/dto/ordinals/output_response.dart b/lib/dto/ordinals/output_response.dart deleted file mode 100644 index cc7b2107f..000000000 --- a/lib/dto/ordinals/output_response.dart +++ /dev/null @@ -1,47 +0,0 @@ -import 'package:stackwallet/dto/ordinals/transaction_response.dart'; -import 'package:stackwallet/dto/ordinals/ordinals_response.dart'; - -class OutputResponse extends OrdinalsResponse { - final OutputLinks links; - final String address; - final String scriptPubkey; - final String transaction; - final int value; - - OutputResponse({ - required this.links, - required this.address, - required this.scriptPubkey, - required this.transaction, - required this.value, - }); - - factory OutputResponse.fromJson(OrdinalsResponse json) { - final data = json.data as Map; - - return OutputResponse( - links: OutputLinks.fromJson(data['_links'] as Map), - address: data['address'] as String, - scriptPubkey: data['script_pubkey'] as String, - transaction: data['transaction'] as String, - value: data['value'] as int, - ); - } -} - -class OutputLinks { - final OutputLink? self; - final TransactionLink? transaction; - - OutputLinks({ - this.self, - this.transaction, - }); - - factory OutputLinks.fromJson(Map json) { - return OutputLinks( - self: json['self'] != null ? OutputLink.fromJson(json['self'] as Map) : null, - transaction: json['transaction'] != null ? TransactionLink.fromJson(json['transaction'] as Map) : null, - ); - } -} diff --git a/lib/dto/ordinals/preview_response.dart b/lib/dto/ordinals/preview_response.dart deleted file mode 100644 index b3e184acd..000000000 --- a/lib/dto/ordinals/preview_response.dart +++ /dev/null @@ -1,22 +0,0 @@ -import 'package:stackwallet/dto/ordinals/ordinals_response.dart'; - -class PreviewResponse extends OrdinalsResponse { - final ImageLink imageLink; - - PreviewResponse({required this.imageLink}); - - factory PreviewResponse.fromJson(OrdinalsResponse json) { - final data = json.data as Map; - return PreviewResponse(imageLink: ImageLink.fromJson(data['_links']['image'] as Map)); - } -} - -class ImageLink { - final String href; - - ImageLink({required this.href}); - - factory ImageLink.fromJson(Map json) { - return ImageLink(href: json['href'] as String); - } -} diff --git a/lib/dto/ordinals/sat_response.dart b/lib/dto/ordinals/sat_response.dart deleted file mode 100644 index 40efb1440..000000000 --- a/lib/dto/ordinals/sat_response.dart +++ /dev/null @@ -1,85 +0,0 @@ -import 'package:stackwallet/dto/ordinals/ordinals_response.dart'; - -class SatResponse extends OrdinalsResponse { - final SatLinks links; - final int block; - final int cycle; - final String decimal; - final String degree; - final int epoch; - final String name; - final int offset; - final String percentile; - final int period; - final String rarity; - final String timestamp; - - SatResponse({ - required this.links, - required this.block, - required this.cycle, - required this.decimal, - required this.degree, - required this.epoch, - required this.name, - required this.offset, - required this.percentile, - required this.period, - required this.rarity, - required this.timestamp, - }); - - factory SatResponse.fromJson(OrdinalsResponse json) { - final data = json.data as Map; - return SatResponse( - links: SatLinks.fromJson(data['_links'] as Map), - block: data['block'] as int, - cycle: data['cycle'] as int, - decimal: data['decimal'] as String, - degree: data['degree'] as String, - epoch: data['epoch'] as int, - name: data['name'] as String, - offset: data['offset'] as int, - percentile: data['percentile'] as String, - period: data['period'] as int, - rarity: data['rarity'] as String, - timestamp: data['timestamp'] as String, - ); - } -} - -class SatLinks { - final SatLink? block; - final SatLink? inscription; - final SatLink? next; - final SatLink? prev; - final SatLink? self; - - SatLinks({ - this.block, - this.inscription, - this.next, - this.prev, - this.self, - }); - - factory SatLinks.fromJson(Map json) { - return SatLinks( - block: json['block'] != null ? SatLink.fromJson(json['block'] as Map) : null, - inscription: json['inscription'] != null ? SatLink.fromJson(json['inscription'] as Map) : null, - next: json['next'] != null ? SatLink.fromJson(json['next'] as Map) : null, - prev: json['prev'] != null ? SatLink.fromJson(json['prev'] as Map) : null, - self: json['self'] != null ? SatLink.fromJson(json['self'] as Map) : null, - ); - } -} - -class SatLink { - final String href; - - SatLink({required this.href}); - - factory SatLink.fromJson(Map json) { - return SatLink(href: json['href'] as String); - } -} diff --git a/lib/dto/ordinals/transaction_response.dart b/lib/dto/ordinals/transaction_response.dart deleted file mode 100644 index c77e07914..000000000 --- a/lib/dto/ordinals/transaction_response.dart +++ /dev/null @@ -1,82 +0,0 @@ -import 'package:stackwallet/dto/ordinals/inscription_link.dart'; -import 'package:stackwallet/dto/ordinals/ordinals_response.dart'; - -class TransactionResponse extends OrdinalsResponse { - final TransactionLinks links; - final List inputs; - final InscriptionLink inscription; - final List outputs; - final TransactionLink self; - final String transaction; - - TransactionResponse({ - required this.links, - required this.inputs, - required this.inscription, - required this.outputs, - required this.self, - required this.transaction, - }); - - factory TransactionResponse.fromJson(OrdinalsResponse json) { - final data = json.data as Map; - final inputsJson = data['_links']['inputs'] as List; - final inputs = inputsJson - .map((inputJson) => OutputLink.fromJson(inputJson as Map)) - .toList(); - - final outputsJson = data['_links']['outputs'] as List; - final outputs = outputsJson - .map((outputJson) => OutputLink.fromJson(outputJson as Map)) - .toList(); - - return TransactionResponse( - links: TransactionLinks.fromJson(data['_links'] as Map), - inputs: inputs, - inscription: InscriptionLink.fromJson(data['_links']['inscription'] as Map), - outputs: outputs, - self: TransactionLink.fromJson(data['_links']['self'] as Map), - transaction: data['transaction'] as String, - ); - } -} - -class TransactionLinks { - final TransactionLink? block; - final InscriptionLink? inscription; - final TransactionLink? self; - - TransactionLinks({ - this.block, - this.inscription, - this.self, - }); - - factory TransactionLinks.fromJson(Map json) { - return TransactionLinks( - block: json['block'] != null ? TransactionLink.fromJson(json['block'] as Map) : null, - inscription: json['inscription'] != null ? InscriptionLink.fromJson(json['inscription'] as Map) : null, - self: json['self'] != null ? TransactionLink.fromJson(json['self'] as Map) : null, - ); - } -} - -class TransactionLink { - final String href; - - TransactionLink({required this.href}); - - factory TransactionLink.fromJson(Map json) { - return TransactionLink(href: json['href'] as String); - } -} - -class OutputLink { - final String href; - - OutputLink({required this.href}); - - factory OutputLink.fromJson(Map json) { - return OutputLink(href: json['href'] as String); - } -} diff --git a/lib/services/mixins/ordinals_interface.dart b/lib/services/mixins/ordinals_interface.dart index 2d887bfcd..c7bc5ff28 100644 --- a/lib/services/mixins/ordinals_interface.dart +++ b/lib/services/mixins/ordinals_interface.dart @@ -5,18 +5,6 @@ import 'package:stackwallet/db/isar/main_db.dart'; import 'package:stackwallet/models/isar/models/blockchain_data/utxo.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; -// ord-litecoin-specific imports -// import 'package:stackwallet/dto/ordinals/feed_response.dart'; -// import 'package:stackwallet/dto/ordinals/inscription_response.dart'; -// import 'package:stackwallet/dto/ordinals/sat_response.dart'; -// import 'package:stackwallet/dto/ordinals/transaction_response.dart'; -// import 'package:stackwallet/dto/ordinals/output_response.dart'; -// import 'package:stackwallet/dto/ordinals/address_response.dart'; -// import 'package:stackwallet/dto/ordinals/block_response.dart'; -// import 'package:stackwallet/dto/ordinals/content_response.dart'; -// import 'package:stackwallet/dto/ordinals/preview_response.dart'; -// import 'package:stackwallet/services/ordinals_api.dart'; - import 'package:stackwallet/services/litescribe_api.dart'; import 'package:stackwallet/dto/ordinals/inscription_data.dart'; From 39eaa937fc83fb3aec9882ad1b6744ff1367abef Mon Sep 17 00:00:00 2001 From: sneurlax Date: Fri, 21 Jul 2023 10:06:34 -0500 Subject: [PATCH 063/169] add convenience method, remove ord-litecoin API file, cast dynamic->int --- lib/services/litescribe_api.dart | 2 +- lib/services/mixins/ordinals_interface.dart | 6 ++ lib/services/ordinals_api.dart | 89 --------------------- 3 files changed, 7 insertions(+), 90 deletions(-) delete mode 100644 lib/services/ordinals_api.dart diff --git a/lib/services/litescribe_api.dart b/lib/services/litescribe_api.dart index 7fc92910d..d5cd3d733 100644 --- a/lib/services/litescribe_api.dart +++ b/lib/services/litescribe_api.dart @@ -44,7 +44,7 @@ class LitescribeAPI { // Check if the number of returned inscriptions equals the limit final list = response.data['result']['list']; final int total = response.data['result']['total'] as int; - final int currentSize = list.length; + final int currentSize = list.length as int; if (currentSize == size && currentSize < total) { // If the number of returned inscriptions equals the limit and there are more inscriptions available, diff --git a/lib/services/mixins/ordinals_interface.dart b/lib/services/mixins/ordinals_interface.dart index c7bc5ff28..51323fa0c 100644 --- a/lib/services/mixins/ordinals_interface.dart +++ b/lib/services/mixins/ordinals_interface.dart @@ -43,6 +43,12 @@ mixin OrdinalsInterface { // TODO save inscriptions to isar which gets watched by a FutureBuilder/StreamBuilder } + Future> getInscriptions() async { + final utxos = await _db.getUTXOs(_walletId).findAll(); + final uniqueAddresses = getUniqueAddressesFromUTXOs(utxos); + return await getInscriptionsFromAddresses(uniqueAddresses); + } + List getUniqueAddressesFromUTXOs(List utxos) { final Set uniqueAddresses = {}; for (var utxo in utxos) { diff --git a/lib/services/ordinals_api.dart b/lib/services/ordinals_api.dart deleted file mode 100644 index e6df3c05e..000000000 --- a/lib/services/ordinals_api.dart +++ /dev/null @@ -1,89 +0,0 @@ -import 'dart:convert'; -import 'package:http/http.dart' as http; - -import 'package:stackwallet/dto/ordinals/ordinals_response.dart'; -import 'package:stackwallet/dto/ordinals/feed_response.dart'; -import 'package:stackwallet/dto/ordinals/inscription_response.dart'; -import 'package:stackwallet/dto/ordinals/sat_response.dart'; -import 'package:stackwallet/dto/ordinals/transaction_response.dart'; -import 'package:stackwallet/dto/ordinals/output_response.dart'; -import 'package:stackwallet/dto/ordinals/address_response.dart'; -import 'package:stackwallet/dto/ordinals/block_response.dart'; -import 'package:stackwallet/dto/ordinals/content_response.dart'; -import 'package:stackwallet/dto/ordinals/preview_response.dart'; - -class OrdinalsAPI { - static final OrdinalsAPI _instance = OrdinalsAPI._internal(); - - factory OrdinalsAPI({required String baseUrl}) { - _instance.baseUrl = baseUrl; - return _instance; - } - - OrdinalsAPI._internal(); - - late String baseUrl; - - Future _getResponse(String endpoint) async { - final response = await http.get(Uri.parse('$baseUrl$endpoint')); - if (response.statusCode == 200) { - return OrdinalsResponse(data: _validateJson(response.body)); - } else { - throw Exception('Failed to load data'); - } - } - - Map _validateJson(String responseBody) { - final parsed = jsonDecode(responseBody); - if (parsed is Map) { - return parsed; - } else { - throw const FormatException('Invalid JSON format'); - } - } - - Future getLatestInscriptions() async { - final response = await _getResponse('/feed'); - return FeedResponse.fromJson(response); - } - - Future getInscriptionDetails(String inscriptionId) async { - final response = await _getResponse('/inscription/$inscriptionId'); - return InscriptionResponse.fromJson(response); - } - - Future getSatDetails(int satNumber) async { - final response = await _getResponse('/sat/$satNumber'); - return SatResponse.fromJson(response); - } - - Future getTransaction(String transactionId) async { - final response = await _getResponse('/tx/$transactionId'); - return TransactionResponse.fromJson(response); - } - - Future getTransactionOutputs(String transactionId) async { - final response = await _getResponse('/output/$transactionId'); - return OutputResponse.fromJson(response); - } - - Future getInscriptionsByAddress(String address) async { - final response = await _getResponse('/address/$address'); - return AddressResponse.fromJson(response); - } - - Future getBlock(String blockHash) async { - final response = await _getResponse('/block/$blockHash'); - return BlockResponse.fromJson(response); - } - - Future getInscriptionContent(String inscriptionId) async { - final response = await _getResponse('/content/$inscriptionId'); - return ContentResponse.fromJson(response); - } - - Future getInscriptionPreview(String inscriptionId) async { - final response = await _getResponse('/preview/$inscriptionId'); - return PreviewResponse.fromJson(response); - } -} From c295ca9a6f1c7146960b455c3bb428a82fe7de72 Mon Sep 17 00:00:00 2001 From: sneurlax Date: Fri, 21 Jul 2023 10:12:31 -0500 Subject: [PATCH 064/169] fix ordinal view crash --- lib/pages/ordinals/ordinals_view.dart | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/lib/pages/ordinals/ordinals_view.dart b/lib/pages/ordinals/ordinals_view.dart index bf0545d4c..c340fa6c6 100644 --- a/lib/pages/ordinals/ordinals_view.dart +++ b/lib/pages/ordinals/ordinals_view.dart @@ -53,9 +53,6 @@ class _OrdinalsViewState extends ConsumerState { searchController = TextEditingController(); searchFocus = FocusNode(); - _manager = ref.watch(walletsChangeNotifierProvider - .select((value) => value.getManager(widget.walletId))); - super.initState(); } @@ -95,6 +92,8 @@ class _OrdinalsViewState extends ConsumerState { .topNavIconPrimary, ), onPressed: () async { + _manager = ref.watch(walletsChangeNotifierProvider + .select((value) => value.getManager(widget.walletId))); (_manager.wallet as OrdinalsInterface).refreshInscriptions(); }, ), From f750bbfe102239ec7d0f7ac195896ca23218693b Mon Sep 17 00:00:00 2001 From: sneurlax Date: Fri, 21 Jul 2023 10:30:47 -0500 Subject: [PATCH 065/169] hook OrdinalsList up to OrdinalsInterface --- lib/pages/ordinals/ordinals_view.dart | 22 +++---- lib/pages/ordinals/widgets/ordinal_card.dart | 24 +++---- lib/pages/ordinals/widgets/ordinals_list.dart | 62 +++++++++++-------- 3 files changed, 57 insertions(+), 51 deletions(-) diff --git a/lib/pages/ordinals/ordinals_view.dart b/lib/pages/ordinals/ordinals_view.dart index c340fa6c6..b12fb78c4 100644 --- a/lib/pages/ordinals/ordinals_view.dart +++ b/lib/pages/ordinals/ordinals_view.dart @@ -56,6 +56,14 @@ class _OrdinalsViewState extends ConsumerState { super.initState(); } + @override + void didChangeDependencies() { + super.didChangeDependencies(); + // Set _manager here when the widget's dependencies change + _manager = ref.watch(walletsChangeNotifierProvider + .select((value) => value.getManager(widget.walletId))); + } + @override void dispose() { searchController.dispose(); @@ -92,8 +100,6 @@ class _OrdinalsViewState extends ConsumerState { .topNavIconPrimary, ), onPressed: () async { - _manager = ref.watch(walletsChangeNotifierProvider - .select((value) => value.getManager(widget.walletId))); (_manager.wallet as OrdinalsInterface).refreshInscriptions(); }, ), @@ -187,17 +193,7 @@ class _OrdinalsViewState extends ConsumerState { Expanded( child: OrdinalsList( walletId: widget.walletId, - ordinals: [ - for (int i = 0; i < 13; i++) - Ordinal( - name: "dummy name $i", - inscription: "insc$i", - rank: "r$i", - collection: OrdCollection.moonbirds, - utxoTXID: 'txid', - utxoVOUT: 1 - ), - ], + ordinalsFuture: (_manager.wallet as OrdinalsInterface).getInscriptions(), ), ), ], diff --git a/lib/pages/ordinals/widgets/ordinal_card.dart b/lib/pages/ordinals/widgets/ordinal_card.dart index a3419ae87..a4d58711e 100644 --- a/lib/pages/ordinals/widgets/ordinal_card.dart +++ b/lib/pages/ordinals/widgets/ordinal_card.dart @@ -1,18 +1,19 @@ import 'package:flutter/material.dart'; -import 'package:stackwallet/models/ordinal.dart'; +import 'package:stackwallet/dto/ordinals/inscription_data.dart'; +import 'package:stackwallet/models/ordinal.dart'; // TODO generalize InscriptionData models -> Ordinal import 'package:stackwallet/pages/ordinals/ordinal_details_view.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/widgets/rounded_white_container.dart'; class OrdinalCard extends StatelessWidget { const OrdinalCard({ - super.key, + Key? key, required this.walletId, - required this.ordinal, - }); + required this.inscriptionData, + }) : super(key: key); final String walletId; - final Ordinal ordinal; + final InscriptionData inscriptionData; @override Widget build(BuildContext context) { @@ -21,7 +22,7 @@ class OrdinalCard extends StatelessWidget { onPressed: () { Navigator.of(context).pushNamed( OrdinalDetailsView.routeName, - arguments: (walletId: walletId, ordinal: ordinal), + arguments: (walletId: walletId, inscriptionData: inscriptionData), ); }, child: Column( @@ -31,21 +32,20 @@ class OrdinalCard extends StatelessWidget { aspectRatio: 1, child: Container( color: Colors.red, - child: const Center( - child: Text( - "replace red container with image", - ), + child: Image.network( + inscriptionData.preview, // Use the preview URL as the image source + fit: BoxFit.cover, ), ), ), const Spacer(), Text( - ordinal.name, + inscriptionData.address, style: STextStyles.w500_12(context), ), const Spacer(), Text( - "INSC. ${ordinal.inscription} RANK ${ordinal.rank}", + "INSC. ${inscriptionData.inscriptionNumber} ID ${inscriptionData.inscriptionId}", style: STextStyles.w500_8(context), ), ], diff --git a/lib/pages/ordinals/widgets/ordinals_list.dart b/lib/pages/ordinals/widgets/ordinals_list.dart index ca7c71446..d161cafeb 100644 --- a/lib/pages/ordinals/widgets/ordinals_list.dart +++ b/lib/pages/ordinals/widgets/ordinals_list.dart @@ -1,39 +1,49 @@ import 'package:flutter/material.dart'; -import 'package:stackwallet/models/ordinal.dart'; +import 'package:stackwallet/dto/ordinals/inscription_data.dart'; + import 'package:stackwallet/pages/ordinals/widgets/ordinal_card.dart'; -class OrdinalsList extends StatefulWidget { +class OrdinalsList extends StatelessWidget { const OrdinalsList({ - super.key, + Key? key, required this.walletId, - required this.ordinals, - }); + required this.ordinalsFuture, + }) : super(key: key); final String walletId; - final List ordinals; + final Future> ordinalsFuture; - @override - State createState() => _OrdinalsListState(); -} - -class _OrdinalsListState extends State { - static const spacing = 10.0; + get spacing => 2.0; @override Widget build(BuildContext context) { - return GridView.builder( - shrinkWrap: true, - itemCount: widget.ordinals.length, - gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( - crossAxisSpacing: spacing, - mainAxisSpacing: spacing, - crossAxisCount: 2, - childAspectRatio: 3 / 4, - ), - itemBuilder: (_, i) => OrdinalCard( - walletId: widget.walletId, - ordinal: widget.ordinals[i], - ), + return FutureBuilder>( + future: ordinalsFuture, + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.waiting) { + return const CircularProgressIndicator(); + } else if (snapshot.hasError) { + return Text('Error: ${snapshot.error}'); + } else if (snapshot.hasData) { + final List inscriptions = snapshot.data!; + return GridView.builder( + shrinkWrap: true, + itemCount: inscriptions.length, + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( + crossAxisSpacing: spacing as double, + mainAxisSpacing: spacing as double, + crossAxisCount: 2, + childAspectRatio: 3 / 4, + ), + itemBuilder: (_, i) => OrdinalCard( + walletId: walletId, + inscriptionData: inscriptions[i], + ), + ); + } else { + return Text('No data found.'); + } + }, ); } -} +} \ No newline at end of file From 65e8c34e34fd33dabff5909d3ee0666a06e1f50b Mon Sep 17 00:00:00 2001 From: sneurlax Date: Fri, 21 Jul 2023 10:43:02 -0500 Subject: [PATCH 066/169] hook up details view to OrdinalsInterface --- lib/pages/ordinals/ordinal_details_view.dart | 47 ++++++++++---------- lib/route_generator.dart | 10 ++--- 2 files changed, 29 insertions(+), 28 deletions(-) diff --git a/lib/pages/ordinals/ordinal_details_view.dart b/lib/pages/ordinals/ordinal_details_view.dart index 2057e8792..c1ca67500 100644 --- a/lib/pages/ordinals/ordinal_details_view.dart +++ b/lib/pages/ordinals/ordinal_details_view.dart @@ -3,7 +3,7 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_svg/flutter_svg.dart'; -import 'package:stackwallet/models/ordinal.dart'; +import 'package:stackwallet/dto/ordinals/inscription_data.dart'; import 'package:stackwallet/notifications/show_flush_bar.dart'; import 'package:stackwallet/pages/ordinals/widgets/dialogs.dart'; import 'package:stackwallet/themes/stack_colors.dart'; @@ -15,20 +15,21 @@ import 'package:stackwallet/widgets/desktop/primary_button.dart'; import 'package:stackwallet/widgets/desktop/secondary_button.dart'; import 'package:stackwallet/widgets/rounded_white_container.dart'; + class OrdinalDetailsView extends StatefulWidget { const OrdinalDetailsView({ - super.key, + Key? key, required this.walletId, - required this.ordinal, - }); + required this.inscriptionData, + }) : super(key: key); final String walletId; - final Ordinal ordinal; + final InscriptionData inscriptionData; static const routeName = "/ordinalDetailsView"; @override - State createState() => _OrdinalDetailsViewState(); + _OrdinalDetailsViewState createState() => _OrdinalDetailsViewState(); } class _OrdinalDetailsViewState extends State { @@ -40,10 +41,10 @@ class _OrdinalDetailsViewState extends State { child: SafeArea( child: Scaffold( backgroundColor: - Theme.of(context).extension()!.background, + Theme.of(context).extension()!.background, appBar: AppBar( backgroundColor: - Theme.of(context).extension()!.background, + Theme.of(context).extension()!.background, leading: const AppBarBackButton(), title: Text( "Ordinal details", @@ -61,20 +62,20 @@ class _OrdinalDetailsViewState extends State { horizontal: 39, ), child: _OrdinalImageGroup( - ordinal: widget.ordinal, + inscriptionData: widget.inscriptionData, walletId: widget.walletId, ), ), _DetailsItemWCopy( title: "Inscription number", - data: widget.ordinal.inscription, + data: widget.inscriptionData.inscriptionNumber.toString(), ), const SizedBox( height: _spacing, ), _DetailsItemWCopy( - title: "Rank", - data: widget.ordinal.rank, + title: "ID", + data: widget.inscriptionData.inscriptionId, ), const SizedBox( height: _spacing, @@ -85,21 +86,21 @@ class _OrdinalDetailsViewState extends State { ), _DetailsItemWCopy( title: "Amount", - data: "FIXME", + data: "${widget.inscriptionData.outputValue}", ), const SizedBox( height: _spacing, ), _DetailsItemWCopy( title: "Owner address", - data: "FIXME", + data: widget.inscriptionData.address, ), const SizedBox( height: _spacing, ), _DetailsItemWCopy( title: "Transaction ID", - data: "FIXME", + data: widget.inscriptionData.genesisTransaction, ), const SizedBox( height: _spacing, @@ -116,10 +117,10 @@ class _OrdinalDetailsViewState extends State { class _DetailsItemWCopy extends StatelessWidget { const _DetailsItemWCopy({ - super.key, + Key? key, required this.title, required this.data, - }); + }) : super(key: key); final String title; final String data; @@ -153,7 +154,7 @@ class _DetailsItemWCopy extends StatelessWidget { child: SvgPicture.asset( Assets.svg.copy, color: - Theme.of(context).extension()!.infoItemIcons, + Theme.of(context).extension()!.infoItemIcons, width: 12, ), ), @@ -174,13 +175,13 @@ class _DetailsItemWCopy extends StatelessWidget { class _OrdinalImageGroup extends StatelessWidget { const _OrdinalImageGroup({ - super.key, + Key? key, required this.walletId, - required this.ordinal, - }); + required this.inscriptionData, + }) : super(key: key); final String walletId; - final Ordinal ordinal; + final InscriptionData inscriptionData; static const _spacing = 12.0; @@ -191,7 +192,7 @@ class _OrdinalImageGroup extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.center, children: [ Text( - ordinal.name, + "${inscriptionData.inscriptionId}", // Use any other property you want style: STextStyles.w600_16(context), ), const SizedBox( diff --git a/lib/route_generator.dart b/lib/route_generator.dart index 6217cf201..e67254135 100644 --- a/lib/route_generator.dart +++ b/lib/route_generator.dart @@ -12,13 +12,15 @@ 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/dto/ordinals/inscription_data.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/exchange/incomplete_exchange.dart'; import 'package:stackwallet/models/exchange/response_objects/trade.dart'; +import 'package:stackwallet/models/isar/models/contact_entry.dart'; import 'package:stackwallet/models/isar/models/isar_models.dart'; -import 'package:stackwallet/models/ordinal.dart'; +import 'package:stackwallet/models/ordinal.dart'; // TODO generalize InscriptionData -> Ordinal 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'; @@ -168,8 +170,6 @@ import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:stackwallet/widgets/choose_coin_view.dart'; import 'package:tuple/tuple.dart'; -import 'models/isar/models/contact_entry.dart'; - /* * This file contains all the routes for the app. * To add a new route, add it to the switch statement in the generateRoute method. @@ -423,12 +423,12 @@ class RouteGenerator { return _routeError("${settings.name} invalid args: ${args.toString()}"); case OrdinalDetailsView.routeName: - if (args is ({Ordinal ordinal, String walletId})) { + if (args is ({InscriptionData inscriptionData, String walletId})) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, builder: (_) => OrdinalDetailsView( walletId: args.walletId, - ordinal: args.ordinal, + inscriptionData: args.inscriptionData, ), settings: RouteSettings( name: settings.name, From ddba1c54f76fcb82ede425cc6676813640b12295 Mon Sep 17 00:00:00 2001 From: sneurlax Date: Fri, 21 Jul 2023 10:49:30 -0500 Subject: [PATCH 067/169] show ordinal image --- lib/pages/ordinals/ordinal_details_view.dart | 12 ++++++++++-- lib/pages/ordinals/widgets/ordinal_card.dart | 9 +++++---- 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/lib/pages/ordinals/ordinal_details_view.dart b/lib/pages/ordinals/ordinal_details_view.dart index c1ca67500..bd5b9a17e 100644 --- a/lib/pages/ordinals/ordinal_details_view.dart +++ b/lib/pages/ordinals/ordinal_details_view.dart @@ -200,8 +200,16 @@ class _OrdinalImageGroup extends StatelessWidget { ), AspectRatio( aspectRatio: 1, - child: Container( - color: Colors.red, + child: AspectRatio( + aspectRatio: 1, + child: Container( + color: Colors.red, + child: Image.network( + inscriptionData.content, // Use the preview URL as the image source + fit: BoxFit.cover, + filterQuality: FilterQuality.none, // Set the filter mode to nearest + ), + ), ), ), const SizedBox( diff --git a/lib/pages/ordinals/widgets/ordinal_card.dart b/lib/pages/ordinals/widgets/ordinal_card.dart index a4d58711e..0c05f686a 100644 --- a/lib/pages/ordinals/widgets/ordinal_card.dart +++ b/lib/pages/ordinals/widgets/ordinal_card.dart @@ -28,13 +28,14 @@ class OrdinalCard extends StatelessWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.center, children: [ - AspectRatio( - aspectRatio: 1, - child: Container( + AspectRatio( + aspectRatio: 1, + child: Container( color: Colors.red, child: Image.network( - inscriptionData.preview, // Use the preview URL as the image source + inscriptionData.content, // Use the preview URL as the image source fit: BoxFit.cover, + filterQuality: FilterQuality.none, // Set the filter mode to nearest ), ), ), From f46a37d4d4775cb9f8bc3feb825350e577d202f2 Mon Sep 17 00:00:00 2001 From: sneurlax Date: Fri, 21 Jul 2023 11:48:31 -0500 Subject: [PATCH 068/169] convert/map/cast InscriptionData->Ordinal --- lib/models/ordinal.dart | 33 ++-- lib/pages/ordinals/ordinal_details_view.dart | 30 ++-- lib/pages/ordinals/ordinals_view.dart | 2 +- lib/pages/ordinals/widgets/ordinal_card.dart | 16 +- lib/pages/ordinals/widgets/ordinals_list.dart | 12 +- lib/route_generator.dart | 4 +- lib/services/mixins/ordinals_interface.dart | 146 ++++++------------ 7 files changed, 99 insertions(+), 144 deletions(-) diff --git a/lib/models/ordinal.dart b/lib/models/ordinal.dart index 1feb98b9d..66a69de94 100644 --- a/lib/models/ordinal.dart +++ b/lib/models/ordinal.dart @@ -1,26 +1,29 @@ -enum OrdCollection { - punks, - moonbirds, -} +import 'package:stackwallet/dto/ordinals/inscription_data.dart'; class Ordinal { - final String name; - final String inscription; - final String rank; - final OrdCollection collection; + final String inscriptionId; + final int inscriptionNumber; + final String content; // following two are used to look up the UTXO object in isar combined w/ walletId final String utxoTXID; final int utxoVOUT; - // TODO: make a proper Isar class instead of this placeholder - Ordinal({ - required this.name, - required this.inscription, - required this.rank, - required this.collection, + required this.inscriptionId, + required this.inscriptionNumber, + required this.content, required this.utxoTXID, required this.utxoVOUT, }); -} + + factory Ordinal.fromInscriptionData(InscriptionData data) { + return Ordinal( + inscriptionId: data.inscriptionId, + inscriptionNumber: data.inscriptionNumber, + content: data.content, + utxoTXID: data.output.split(':')[0], // "output": "062f32e21aa04246b8873b5d9a929576addd0339881e1ea478b406795d6b6c47:0" + utxoVOUT: int.parse(data.output.split(':')[1]), + ); + } +} \ No newline at end of file diff --git a/lib/pages/ordinals/ordinal_details_view.dart b/lib/pages/ordinals/ordinal_details_view.dart index bd5b9a17e..6aab95586 100644 --- a/lib/pages/ordinals/ordinal_details_view.dart +++ b/lib/pages/ordinals/ordinal_details_view.dart @@ -4,6 +4,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:stackwallet/dto/ordinals/inscription_data.dart'; +import 'package:stackwallet/models/ordinal.dart'; import 'package:stackwallet/notifications/show_flush_bar.dart'; import 'package:stackwallet/pages/ordinals/widgets/dialogs.dart'; import 'package:stackwallet/themes/stack_colors.dart'; @@ -15,16 +16,15 @@ import 'package:stackwallet/widgets/desktop/primary_button.dart'; import 'package:stackwallet/widgets/desktop/secondary_button.dart'; import 'package:stackwallet/widgets/rounded_white_container.dart'; - class OrdinalDetailsView extends StatefulWidget { const OrdinalDetailsView({ Key? key, required this.walletId, - required this.inscriptionData, + required this.ordinal, }) : super(key: key); final String walletId; - final InscriptionData inscriptionData; + final Ordinal ordinal; static const routeName = "/ordinalDetailsView"; @@ -62,20 +62,20 @@ class _OrdinalDetailsViewState extends State { horizontal: 39, ), child: _OrdinalImageGroup( - inscriptionData: widget.inscriptionData, + ordinal: widget.ordinal, walletId: widget.walletId, ), ), _DetailsItemWCopy( title: "Inscription number", - data: widget.inscriptionData.inscriptionNumber.toString(), + data: widget.ordinal.inscriptionNumber.toString(), ), const SizedBox( height: _spacing, ), _DetailsItemWCopy( title: "ID", - data: widget.inscriptionData.inscriptionId, + data: widget.ordinal.inscriptionId, ), const SizedBox( height: _spacing, @@ -84,23 +84,23 @@ class _OrdinalDetailsViewState extends State { const SizedBox( height: _spacing, ), - _DetailsItemWCopy( + const _DetailsItemWCopy( title: "Amount", - data: "${widget.inscriptionData.outputValue}", + data: "TODO", // TODO infer from utxo utxoTXID:utxoVOUT ), const SizedBox( height: _spacing, ), - _DetailsItemWCopy( + const _DetailsItemWCopy( title: "Owner address", - data: widget.inscriptionData.address, + data: "TODO", // infer from address associated w utxoTXID ), const SizedBox( height: _spacing, ), _DetailsItemWCopy( title: "Transaction ID", - data: widget.inscriptionData.genesisTransaction, + data: widget.ordinal.utxoTXID, ), const SizedBox( height: _spacing, @@ -177,11 +177,11 @@ class _OrdinalImageGroup extends StatelessWidget { const _OrdinalImageGroup({ Key? key, required this.walletId, - required this.inscriptionData, + required this.ordinal, }) : super(key: key); final String walletId; - final InscriptionData inscriptionData; + final Ordinal ordinal; static const _spacing = 12.0; @@ -192,7 +192,7 @@ class _OrdinalImageGroup extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.center, children: [ Text( - "${inscriptionData.inscriptionId}", // Use any other property you want + "${ordinal.inscriptionId}", // Use any other property you want style: STextStyles.w600_16(context), ), const SizedBox( @@ -205,7 +205,7 @@ class _OrdinalImageGroup extends StatelessWidget { child: Container( color: Colors.red, child: Image.network( - inscriptionData.content, // Use the preview URL as the image source + ordinal.content, // Use the preview URL as the image source fit: BoxFit.cover, filterQuality: FilterQuality.none, // Set the filter mode to nearest ), diff --git a/lib/pages/ordinals/ordinals_view.dart b/lib/pages/ordinals/ordinals_view.dart index b12fb78c4..a4adfa019 100644 --- a/lib/pages/ordinals/ordinals_view.dart +++ b/lib/pages/ordinals/ordinals_view.dart @@ -193,7 +193,7 @@ class _OrdinalsViewState extends ConsumerState { Expanded( child: OrdinalsList( walletId: widget.walletId, - ordinalsFuture: (_manager.wallet as OrdinalsInterface).getInscriptions(), + ordinalsFuture: (_manager.wallet as OrdinalsInterface).getOrdinals(), ), ), ], diff --git a/lib/pages/ordinals/widgets/ordinal_card.dart b/lib/pages/ordinals/widgets/ordinal_card.dart index 0c05f686a..c9060b32e 100644 --- a/lib/pages/ordinals/widgets/ordinal_card.dart +++ b/lib/pages/ordinals/widgets/ordinal_card.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; -import 'package:stackwallet/dto/ordinals/inscription_data.dart'; -import 'package:stackwallet/models/ordinal.dart'; // TODO generalize InscriptionData models -> Ordinal + +import 'package:stackwallet/models/ordinal.dart'; import 'package:stackwallet/pages/ordinals/ordinal_details_view.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/widgets/rounded_white_container.dart'; @@ -9,11 +9,11 @@ class OrdinalCard extends StatelessWidget { const OrdinalCard({ Key? key, required this.walletId, - required this.inscriptionData, + required this.ordinal, }) : super(key: key); final String walletId; - final InscriptionData inscriptionData; + final Ordinal ordinal; @override Widget build(BuildContext context) { @@ -22,7 +22,7 @@ class OrdinalCard extends StatelessWidget { onPressed: () { Navigator.of(context).pushNamed( OrdinalDetailsView.routeName, - arguments: (walletId: walletId, inscriptionData: inscriptionData), + arguments: (walletId: walletId, ordinal: ordinal), ); }, child: Column( @@ -33,7 +33,7 @@ class OrdinalCard extends StatelessWidget { child: Container( color: Colors.red, child: Image.network( - inscriptionData.content, // Use the preview URL as the image source + ordinal.content, // Use the preview URL as the image source fit: BoxFit.cover, filterQuality: FilterQuality.none, // Set the filter mode to nearest ), @@ -41,12 +41,12 @@ class OrdinalCard extends StatelessWidget { ), const Spacer(), Text( - inscriptionData.address, + 'TODO', // infer from address associated with utxoTXID style: STextStyles.w500_12(context), ), const Spacer(), Text( - "INSC. ${inscriptionData.inscriptionNumber} ID ${inscriptionData.inscriptionId}", + "INSC. ${ordinal.inscriptionNumber} ID ${ordinal.inscriptionId}", style: STextStyles.w500_8(context), ), ], diff --git a/lib/pages/ordinals/widgets/ordinals_list.dart b/lib/pages/ordinals/widgets/ordinals_list.dart index d161cafeb..fe7618cd0 100644 --- a/lib/pages/ordinals/widgets/ordinals_list.dart +++ b/lib/pages/ordinals/widgets/ordinals_list.dart @@ -1,5 +1,5 @@ import 'package:flutter/material.dart'; -import 'package:stackwallet/dto/ordinals/inscription_data.dart'; +import 'package:stackwallet/models/ordinal.dart'; import 'package:stackwallet/pages/ordinals/widgets/ordinal_card.dart'; @@ -11,13 +11,13 @@ class OrdinalsList extends StatelessWidget { }) : super(key: key); final String walletId; - final Future> ordinalsFuture; + final Future> ordinalsFuture; get spacing => 2.0; @override Widget build(BuildContext context) { - return FutureBuilder>( + return FutureBuilder>( future: ordinalsFuture, builder: (context, snapshot) { if (snapshot.connectionState == ConnectionState.waiting) { @@ -25,10 +25,10 @@ class OrdinalsList extends StatelessWidget { } else if (snapshot.hasError) { return Text('Error: ${snapshot.error}'); } else if (snapshot.hasData) { - final List inscriptions = snapshot.data!; + final List ordinals = snapshot.data!; return GridView.builder( shrinkWrap: true, - itemCount: inscriptions.length, + itemCount: ordinals.length, gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( crossAxisSpacing: spacing as double, mainAxisSpacing: spacing as double, @@ -37,7 +37,7 @@ class OrdinalsList extends StatelessWidget { ), itemBuilder: (_, i) => OrdinalCard( walletId: walletId, - inscriptionData: inscriptions[i], + ordinal: ordinals[i], ), ); } else { diff --git a/lib/route_generator.dart b/lib/route_generator.dart index e67254135..7c0807caf 100644 --- a/lib/route_generator.dart +++ b/lib/route_generator.dart @@ -423,12 +423,12 @@ class RouteGenerator { return _routeError("${settings.name} invalid args: ${args.toString()}"); case OrdinalDetailsView.routeName: - if (args is ({InscriptionData inscriptionData, String walletId})) { + if (args is ({Ordinal ordinal, String walletId})) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, builder: (_) => OrdinalDetailsView( walletId: args.walletId, - inscriptionData: args.inscriptionData, + ordinal: args.ordinal, ), settings: RouteSettings( name: settings.name, diff --git a/lib/services/mixins/ordinals_interface.dart b/lib/services/mixins/ordinals_interface.dart index 51323fa0c..a779f5b2e 100644 --- a/lib/services/mixins/ordinals_interface.dart +++ b/lib/services/mixins/ordinals_interface.dart @@ -2,11 +2,11 @@ import 'dart:async'; import 'package:isar/isar.dart'; import 'package:stackwallet/db/isar/main_db.dart'; -import 'package:stackwallet/models/isar/models/blockchain_data/utxo.dart'; -import 'package:stackwallet/utilities/enums/coin_enum.dart'; - -import 'package:stackwallet/services/litescribe_api.dart'; import 'package:stackwallet/dto/ordinals/inscription_data.dart'; +import 'package:stackwallet/models/isar/models/blockchain_data/utxo.dart'; +import 'package:stackwallet/models/ordinal.dart'; +import 'package:stackwallet/services/litescribe_api.dart'; +import 'package:stackwallet/utilities/enums/coin_enum.dart'; mixin OrdinalsInterface { late final String _walletId; @@ -25,28 +25,32 @@ mixin OrdinalsInterface { } final LitescribeAPI litescribeAPI = LitescribeAPI(baseUrl: 'https://litescribe.io/api'); - // Future> getInscriptionsByAddress(String address) async { - // try { - // var response = await litescribeAPI.getInscriptionsByAddress(address); - // // print("Found ${response.result.total} inscription${response.result.total > 1 ? 's' : ''} at address $address"); // TODO disable (POC) - // return response.result.list; - // } catch (e) { - // throw Exception('Error in OrdinalsInterface getInscriptionsByAddress: $e'); - // } - // } - void refreshInscriptions() async { List _inscriptions; final utxos = await _db.getUTXOs(_walletId).findAll(); final uniqueAddresses = getUniqueAddressesFromUTXOs(utxos); - _inscriptions = await getInscriptionsFromAddresses(uniqueAddresses); + _inscriptions = await getInscriptionDataFromAddresses(uniqueAddresses); // TODO save inscriptions to isar which gets watched by a FutureBuilder/StreamBuilder } - Future> getInscriptions() async { - final utxos = await _db.getUTXOs(_walletId).findAll(); - final uniqueAddresses = getUniqueAddressesFromUTXOs(utxos); - return await getInscriptionsFromAddresses(uniqueAddresses); + Future> getInscriptionData() async { + try { + final utxos = await _db.getUTXOs(_walletId).findAll(); + final uniqueAddresses = getUniqueAddressesFromUTXOs(utxos); + return await getInscriptionDataFromAddresses(uniqueAddresses); + } catch (e) { + throw Exception('Error in OrdinalsInterface getInscriptions: $e'); + } + } + + Future> getOrdinals() async { + try { + final utxos = await _db.getUTXOs(_walletId).findAll(); + final uniqueAddresses = getUniqueAddressesFromUTXOs(utxos); + return await getOrdinalsFromAddresses(uniqueAddresses); + } catch (e) { + throw Exception('Error in OrdinalsInterface getOrdinals: $e'); + } } List getUniqueAddressesFromUTXOs(List utxos) { @@ -59,12 +63,22 @@ mixin OrdinalsInterface { return uniqueAddresses.toList(); } - Future> getInscriptionsFromAddresses(List addresses) async { + Future> getInscriptionDataFromAddress(String address) async { + List allInscriptions = []; + try { + var inscriptions = await litescribeAPI.getInscriptionsByAddress(address); + allInscriptions.addAll(inscriptions); + } catch (e) { + throw Exception('Error in OrdinalsInterface getInscriptionsByAddress: $e'); + } + return allInscriptions; + } + + Future> getInscriptionDataFromAddresses(List addresses) async { List allInscriptions = []; for (String address in addresses) { try { var inscriptions = await litescribeAPI.getInscriptionsByAddress(address); - print("Found ${inscriptions.length} inscription${inscriptions.length > 1 ? 's' : ''} at address $address"); allInscriptions.addAll(inscriptions); } catch (e) { print("Error fetching inscriptions for address $address: $e"); @@ -73,87 +87,25 @@ mixin OrdinalsInterface { return allInscriptions; } -/* // ord-litecoin interface - final OrdinalsAPI ordinalsAPI = OrdinalsAPI(baseUrl: 'https://ord-litecoin.stackwallet.com'); - - Future fetchLatestInscriptions() async { + Future> getOrdinalsFromAddress(String address) async { try { - final feedResponse = await ordinalsAPI.getLatestInscriptions(); - // Process the feedResponse data as needed - // print('Latest Inscriptions:'); - // for (var inscription in feedResponse.inscriptions) { - // print('Title: ${inscription.title}, Href: ${inscription.href}'); - // } - return feedResponse; + var inscriptions = await litescribeAPI.getInscriptionsByAddress(address); + return inscriptions.map((data) => Ordinal.fromInscriptionData(data)).toList(); } catch (e) { - // Handle errors - throw Exception('Error in OrdinalsInterface fetchLatestInscriptions: $e'); + throw Exception('Error in OrdinalsInterface getOrdinalsFromAddress: $e'); } } - Future getInscriptionDetails(String inscriptionId) async { - try { - return await ordinalsAPI.getInscriptionDetails(inscriptionId); - } catch (e) { - throw Exception('Error in OrdinalsInterface getInscriptionDetails: $e'); + Future> getOrdinalsFromAddresses(List addresses) async { + List allOrdinals = []; + for (String address in addresses) { + try { + var inscriptions = await litescribeAPI.getInscriptionsByAddress(address); + allOrdinals.addAll(inscriptions.map((data) => Ordinal.fromInscriptionData(data))); + } catch (e) { + print("Error fetching inscriptions for address $address: $e"); + } } + return allOrdinals; } - - Future getSatDetails(int satNumber) async { - try { - return await ordinalsAPI.getSatDetails(satNumber); - } catch (e) { - throw Exception('Error in OrdinalsInterface getSatDetails: $e'); - } - } - - Future getTransaction(String transactionId) async { - try { - print(1); - return await ordinalsAPI.getTransaction(transactionId); - } catch (e) { - throw Exception('Error in OrdinalsInterface getTransaction: $e'); - } - } - - Future getTransactionOutputs(String transactionId) async { - try { - return await ordinalsAPI.getTransactionOutputs(transactionId); - } catch (e) { - throw Exception('Error in OrdinalsInterface getTransactionOutputs: $e'); - } - } - - Future getInscriptionsByAddress(String address) async { - try { - return await ordinalsAPI.getInscriptionsByAddress(address); - } catch (e) { - throw Exception('Error in OrdinalsInterface getInscriptionsByAddress: $e'); - } - } - - Future getBlock(int blockNumber) async { - try { - return await ordinalsAPI.getBlock(blockNumber); - } catch (e) { - throw Exception('Error in OrdinalsInterface getBlock: $e'); - } - } - - Future getInscriptionContent(String inscriptionId) async { - try { - return await ordinalsAPI.getInscriptionContent(inscriptionId); - } catch (e) { - throw Exception('Error in OrdinalsInterface getInscriptionContent: $e'); - } - } - - Future getInscriptionPreview(String inscriptionId) async { - try { - return await ordinalsAPI.getInscriptionPreview(inscriptionId); - } catch (e) { - throw Exception('Error in OrdinalsInterface getInscriptionPreview: $e'); - } - } - */ // /ord-litecoin interface } \ No newline at end of file From 269f47d6f4245ce95c09c500130245c10474b3ca Mon Sep 17 00:00:00 2001 From: ryleedavis Date: Fri, 21 Jul 2023 11:34:14 -0600 Subject: [PATCH 069/169] resolved merge conflict --- lib/models/isar/stack_theme.dart | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/lib/models/isar/stack_theme.dart b/lib/models/isar/stack_theme.dart index c6d526d85..32878366a 100644 --- a/lib/models/isar/stack_theme.dart +++ b/lib/models/isar/stack_theme.dart @@ -2386,15 +2386,14 @@ class ThemeAssetsV3 implements IThemeAssets { Map? _coinCardImages; late final String? coinCardImagesString; - @ignore Map? get coinCardFavoritesImages => _coinCardFavoritesImages ??= coinCardFavoritesImagesString == null ? null : parseCoinAssetsString( - coinCardFavoritesImagesString!, - placeHolder: coinPlaceholder, - ); + coinCardFavoritesImagesString!, + placeHolder: coinPlaceholder, + ); @ignore Map? _coinCardFavoritesImages; @Name("otherStringParam1") @@ -2467,7 +2466,6 @@ class ThemeAssetsV3 implements IThemeAssets { ..backgroundRelative = json["background"] is String ? "$themeId/assets/${json["background"] as String}" : null - ..dummy1 = null ..dummy2 = null ..dummy3 = null; } From b8fcc98ac6ca64dda77f69a2e4765b9806275996 Mon Sep 17 00:00:00 2001 From: julian Date: Fri, 21 Jul 2023 13:23:36 -0600 Subject: [PATCH 070/169] eth token balance fix --- lib/services/ethereum/ethereum_api.dart | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/lib/services/ethereum/ethereum_api.dart b/lib/services/ethereum/ethereum_api.dart index 663513bea..5e95e3aa1 100644 --- a/lib/services/ethereum/ethereum_api.dart +++ b/lib/services/ethereum/ethereum_api.dart @@ -10,7 +10,6 @@ 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'; @@ -431,10 +430,10 @@ abstract class EthereumAPI { final map = json["data"].first as Map; final balance = - Decimal.tryParse(map["balance"].toString()) ?? Decimal.zero; + BigInt.tryParse(map["units"].toString()) ?? BigInt.zero; return EthereumResponse( - Amount.fromDecimal(balance, fractionDigits: map["decimals"] as int), + Amount(rawValue: balance, fractionDigits: map["decimals"] as int), null, ); } else { From e7b480e5852e72f8355ffe1a91ee56763b528ac2 Mon Sep 17 00:00:00 2001 From: julian Date: Fri, 21 Jul 2023 14:11:30 -0600 Subject: [PATCH 071/169] add ba/nano wallet representative change option to wallet settings, remove coin control from ba/nano, and some other clean up --- .../add_token_view/add_custom_token_view.dart | 4 +- .../install_theme_from_file_dialog.dart | 2 +- .../sub_widgets/stack_theme_card.dart | 4 +- .../wallet_settings_view.dart | 42 +- .../change_representative_view.dart | 367 ++++++++++++++++++ .../xpub_view.dart | 0 .../sub_widgets/my_token_select_item.dart | 2 +- .../sub_widgets/wallet_options_button.dart | 2 +- lib/route_generator.dart | 17 +- lib/services/coins/banano/banano_wallet.dart | 52 ++- lib/services/coins/nano/nano_wallet.dart | 52 ++- lib/services/nano_api.dart | 130 +++++++ lib/utilities/show_loading.dart | 20 +- lib/widgets/wallet_card.dart | 2 +- 14 files changed, 669 insertions(+), 27 deletions(-) create mode 100644 lib/pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/change_representative_view.dart rename lib/pages/settings_views/{global_settings_view => wallet_settings_view/wallet_settings_wallet_settings}/xpub_view.dart (100%) create mode 100644 lib/services/nano_api.dart 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 index a3d297b8c..dd807976b 100644 --- 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 @@ -142,7 +142,7 @@ class _AddCustomTokenViewState extends ConsumerState { context: context, message: "Looking up contract", ); - currentToken = response.value; + currentToken = response!.value; if (currentToken != null) { nameController.text = currentToken!.name; symbolController.text = currentToken!.symbol; @@ -157,7 +157,7 @@ class _AddCustomTokenViewState extends ConsumerState { context: context, builder: (context) => StackOkDialog( title: "Failed to look up token", - message: response.exception?.message, + message: response!.exception?.message, ), ), ); diff --git a/lib/pages/settings_views/global_settings_view/appearance_settings/sub_widgets/install_theme_from_file_dialog.dart b/lib/pages/settings_views/global_settings_view/appearance_settings/sub_widgets/install_theme_from_file_dialog.dart index 235bb5e95..812fcc94b 100644 --- a/lib/pages/settings_views/global_settings_view/appearance_settings/sub_widgets/install_theme_from_file_dialog.dart +++ b/lib/pages/settings_views/global_settings_view/appearance_settings/sub_widgets/install_theme_from_file_dialog.dart @@ -164,7 +164,7 @@ class _InstallThemeFromFileDialogState ); if (mounted) { Navigator.of(context).pop(); - if (!result) { + if (!result!) { unawaited( showDialog( context: context, diff --git a/lib/pages/settings_views/global_settings_view/appearance_settings/sub_widgets/stack_theme_card.dart b/lib/pages/settings_views/global_settings_view/appearance_settings/sub_widgets/stack_theme_card.dart index 0c45a68f7..003063bde 100644 --- a/lib/pages/settings_views/global_settings_view/appearance_settings/sub_widgets/stack_theme_card.dart +++ b/lib/pages/settings_views/global_settings_view/appearance_settings/sub_widgets/stack_theme_card.dart @@ -72,11 +72,11 @@ class _StackThemeCardState extends ConsumerState { } Future _downloadPressed() async { - final result = await showLoading( + final result = (await showLoading( whileFuture: _downloadAndInstall(), context: context, message: "Downloading and installing theme...", - ); + ))!; if (mounted) { final message = result diff --git a/lib/pages/settings_views/wallet_settings_view/wallet_settings_view.dart b/lib/pages/settings_views/wallet_settings_view/wallet_settings_view.dart index 59f443db9..fcacc60d4 100644 --- a/lib/pages/settings_views/wallet_settings_view/wallet_settings_view.dart +++ b/lib/pages/settings_views/wallet_settings_view/wallet_settings_view.dart @@ -20,11 +20,12 @@ import 'package:stackwallet/pages/home_view/home_view.dart'; import 'package:stackwallet/pages/pinpad_views/lock_screen_view.dart'; import 'package:stackwallet/pages/settings_views/global_settings_view/advanced_views/debug_view.dart'; import 'package:stackwallet/pages/settings_views/global_settings_view/syncing_preferences_views/syncing_preferences_view.dart'; -import 'package:stackwallet/pages/settings_views/global_settings_view/xpub_view.dart'; import 'package:stackwallet/pages/settings_views/sub_widgets/settings_list_button.dart'; import 'package:stackwallet/pages/settings_views/wallet_settings_view/wallet_backup_views/wallet_backup_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_wallet_settings/change_representative_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/settings_views/wallet_settings_view/wallet_settings_wallet_settings/xpub_view.dart'; import 'package:stackwallet/providers/global/wallets_provider.dart'; import 'package:stackwallet/providers/ui/transaction_filter_provider.dart'; import 'package:stackwallet/route_generator.dart'; @@ -231,7 +232,7 @@ class _WalletSettingsViewState extends ConsumerState { .mnemonic; if (mounted) { - Navigator.push( + await Navigator.push( context, RouteGenerator.getRoute( shouldUseMaterialRoute: @@ -305,6 +306,25 @@ class _WalletSettingsViewState extends ConsumerState { ); }, ), + if (coin == Coin.nano || coin == Coin.banano) + const SizedBox( + height: 8, + ), + if (coin == Coin.nano || coin == Coin.banano) + Consumer( + builder: (_, ref, __) { + return SettingsListButton( + iconAssetName: Assets.svg.eye, + title: "Change representative", + onPressed: () { + Navigator.of(context).pushNamed( + ChangeRepresentativeView.routeName, + arguments: widget.walletId, + ); + }, + ); + }, + ), const SizedBox( height: 8, ), @@ -434,18 +454,20 @@ class _EpiBoxInfoFormState extends ConsumerState { TextButton( onPressed: () async { try { - wallet.updateEpicboxConfig( + await wallet.updateEpicboxConfig( hostController.text, int.parse(portController.text), ); - showFloatingFlushBar( - context: context, - message: "Epicbox info saved!", - type: FlushBarType.success, - ); - wallet.refresh(); + if (mounted) { + await showFloatingFlushBar( + context: context, + message: "Epicbox info saved!", + type: FlushBarType.success, + ); + } + unawaited(wallet.refresh()); } catch (e) { - showFloatingFlushBar( + await showFloatingFlushBar( context: context, message: "Failed to save epicbox info: $e", type: FlushBarType.warning, diff --git a/lib/pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/change_representative_view.dart b/lib/pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/change_representative_view.dart new file mode 100644 index 000000000..76bb833b1 --- /dev/null +++ b/lib/pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/change_representative_view.dart @@ -0,0 +1,367 @@ +/* + * This file is part of Stack Wallet. + * + * Copyright (c) 2023 Cypher Stack + * All Rights Reserved. + * The code is distributed under GPLv3 license, see LICENSE file for details. + * Generated by Cypher Stack on 2023-05-26 + * + */ + +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:stackwallet/notifications/show_flush_bar.dart'; +import 'package:stackwallet/providers/global/wallets_provider.dart'; +import 'package:stackwallet/services/coins/banano/banano_wallet.dart'; +import 'package:stackwallet/services/coins/nano/nano_wallet.dart'; +import 'package:stackwallet/themes/stack_colors.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/show_loading.dart'; +import 'package:stackwallet/utilities/text_styles.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.dart'; +import 'package:stackwallet/widgets/desktop/desktop_dialog_close_button.dart'; +import 'package:stackwallet/widgets/desktop/primary_button.dart'; +import 'package:stackwallet/widgets/icon_widgets/x_icon.dart'; +import 'package:stackwallet/widgets/loading_indicator.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 ChangeRepresentativeView extends ConsumerStatefulWidget { + const ChangeRepresentativeView({ + Key? key, + required this.walletId, + this.clipboardInterface = const ClipboardWrapper(), + }) : super(key: key); + + final String walletId; + final ClipboardInterface clipboardInterface; + + static const String routeName = "/changeRepresentative"; + + @override + ConsumerState createState() => _XPubViewState(); +} + +class _XPubViewState extends ConsumerState { + final _textController = TextEditingController(); + final _textFocusNode = FocusNode(); + final bool isDesktop = Util.isDesktop; + + late ClipboardInterface _clipboardInterface; + + String? representative; + + Future loadRepresentative() async { + final manager = + ref.read(walletsChangeNotifierProvider).getManager(widget.walletId); + + if (manager.coin == Coin.nano) { + return (manager.wallet as NanoWallet).getCurrentRepresentative(); + } else if (manager.coin == Coin.banano) { + return (manager.wallet as BananoWallet).getCurrentRepresentative(); + } + throw Exception("Unsupported wallet attempted to show representative!"); + } + + Future _save() async { + final manager = + ref.read(walletsChangeNotifierProvider).getManager(widget.walletId); + + final changeFuture = manager.coin == Coin.nano + ? (manager.wallet as NanoWallet).changeRepresentative + : (manager.wallet as BananoWallet).changeRepresentative; + + final result = await showLoading( + whileFuture: changeFuture(_textController.text), + context: context, + message: "Updating representative...", + onException: (ex) { + String msg = ex.toString(); + while (msg.isNotEmpty && msg.startsWith("Exception:")) { + msg = msg.substring(10).trim(); + } + showFloatingFlushBar( + type: FlushBarType.warning, + message: msg, + context: context, + ); + }); + + if (mounted) { + if (result != null && result) { + setState(() { + representative = _textController.text; + _textController.text = ""; + }); + await showFloatingFlushBar( + type: FlushBarType.success, + message: "Representative changed", + context: context, + ); + } + } + } + + @override + void initState() { + _clipboardInterface = widget.clipboardInterface; + + super.initState(); + } + + @override + void dispose() { + _textController.dispose(); + _textFocusNode.dispose(); + super.dispose(); + } + + Future _copy() async { + await _clipboardInterface + .setData(ClipboardData(text: representative ?? "")); + if (mounted) { + unawaited(showFloatingFlushBar( + type: FlushBarType.info, + message: "Copied to clipboard", + iconAsset: Assets.svg.copy, + context: context, + )); + } + } + + @override + Widget build(BuildContext context) { + return ConditionalParent( + condition: !isDesktop, + builder: (child) => Background( + child: SafeArea( + child: Scaffold( + backgroundColor: + Theme.of(context).extension()!.background, + appBar: AppBar( + leading: AppBarBackButton( + onPressed: () async { + Navigator.of(context).pop(); + }, + ), + title: Text( + "Wallet representative", + style: STextStyles.navBarTitle(context), + ), + actions: [ + Padding( + padding: const EdgeInsets.all(10), + child: AspectRatio( + aspectRatio: 1, + child: AppBarIconButton( + color: Theme.of(context) + .extension()! + .background, + shadows: const [], + icon: SvgPicture.asset( + Assets.svg.copy, + width: 24, + height: 24, + color: Theme.of(context) + .extension()! + .topNavIconPrimary, + ), + onPressed: () { + if (representative != null) { + _copy(); + } + }, + ), + ), + ), + ], + ), + body: Padding( + padding: const EdgeInsets.only( + top: 12, + left: 16, + right: 16, + ), + child: child, + ), + ), + ), + ), + child: ConditionalParent( + condition: isDesktop, + builder: (child) => DesktopDialog( + maxWidth: 600, + maxHeight: double.infinity, + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Padding( + padding: const EdgeInsets.only( + left: 32, + ), + child: Text( + "${ref.watch(walletsChangeNotifierProvider.select((value) => value.getManager(widget.walletId).walletName))} xPub", + style: STextStyles.desktopH2(context), + ), + ), + DesktopDialogCloseButton( + onPressedOverride: Navigator.of( + context, + rootNavigator: true, + ).pop, + ), + ], + ), + AnimatedSize( + duration: const Duration( + milliseconds: 150, + ), + child: Padding( + padding: const EdgeInsets.fromLTRB(32, 0, 32, 32), + child: child, + ), + ), + ], + ), + ), + child: Column( + children: [ + if (isDesktop) const SizedBox(height: 44), + ConditionalParent( + condition: !isDesktop, + builder: (child) => Expanded( + child: child, + ), + child: FutureBuilder( + future: loadRepresentative(), + builder: (context, AsyncSnapshot snapshot) { + if (snapshot.connectionState == ConnectionState.done && + snapshot.hasData) { + representative = snapshot.data!; + } + + const height = 600.0; + Widget child; + if (representative == null) { + child = const SizedBox( + key: Key("loadingRepresentative"), + height: height, + child: Center( + child: LoadingIndicator( + width: 100, + ), + ), + ); + } else { + child = Column( + children: [ + RoundedWhiteContainer( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + representative!, + style: STextStyles.itemSubtitle(context), + ), + ], + ), + ), + const SizedBox( + height: 24, + ), + ClipRRect( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + child: TextField( + autocorrect: Util.isDesktop ? false : true, + enableSuggestions: Util.isDesktop ? false : true, + controller: _textController, + style: isDesktop + ? STextStyles.desktopTextExtraSmall(context) + .copyWith( + color: Theme.of(context) + .extension()! + .textFieldActiveText, + height: 1.8, + ) + : STextStyles.field(context), + focusNode: _textFocusNode, + decoration: standardInputDecoration( + "Enter new representative", + _textFocusNode, + context, + desktopMed: isDesktop, + ).copyWith( + contentPadding: isDesktop + ? const EdgeInsets.only( + left: 16, + top: 11, + bottom: 12, + right: 5, + ) + : null, + suffixIcon: _textController.text.isNotEmpty + ? Padding( + padding: const EdgeInsets.only(right: 0), + child: UnconstrainedBox( + child: Row( + children: [ + TextFieldIconButton( + child: const XIcon(), + onTap: () async { + setState(() { + _textController.text = ""; + }); + }, + ), + ], + ), + ), + ) + : null, + ), + ), + ), + if (!isDesktop) const Spacer(), + PrimaryButton( + label: "Save", + onPressed: _save, + ), + if (!isDesktop) + const SizedBox( + height: 16, + ), + ], + ); + } + + return AnimatedSwitcher( + duration: const Duration( + milliseconds: 200, + ), + child: child, + ); + }, + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/pages/settings_views/global_settings_view/xpub_view.dart b/lib/pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/xpub_view.dart similarity index 100% rename from lib/pages/settings_views/global_settings_view/xpub_view.dart rename to lib/pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/xpub_view.dart 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 index 31dc2b085..323ddcf8b 100644 --- a/lib/pages/token_view/sub_widgets/my_token_select_item.dart +++ b/lib/pages/token_view/sub_widgets/my_token_select_item.dart @@ -100,7 +100,7 @@ class _MyTokenSelectItemState extends ConsumerState { message: "Loading ${widget.token.name}", ); - if (!success) { + if (!success!) { return; } diff --git a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/wallet_options_button.dart b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/wallet_options_button.dart index 68fe09aa8..0eda83ce4 100644 --- a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/wallet_options_button.dart +++ b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/wallet_options_button.dart @@ -13,7 +13,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/pages/settings_views/global_settings_view/xpub_view.dart'; +import 'package:stackwallet/pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/xpub_view.dart'; import 'package:stackwallet/pages_desktop_specific/addresses/desktop_wallet_addresses_view.dart'; import 'package:stackwallet/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_delete_wallet_dialog.dart'; import 'package:stackwallet/providers/providers.dart'; diff --git a/lib/route_generator.dart b/lib/route_generator.dart index 7ccc378c9..66239aaf7 100644 --- a/lib/route_generator.dart +++ b/lib/route_generator.dart @@ -103,14 +103,15 @@ import 'package:stackwallet/pages/settings_views/global_settings_view/support_vi import 'package:stackwallet/pages/settings_views/global_settings_view/syncing_preferences_views/syncing_options_view.dart'; import 'package:stackwallet/pages/settings_views/global_settings_view/syncing_preferences_views/syncing_preferences_view.dart'; import 'package:stackwallet/pages/settings_views/global_settings_view/syncing_preferences_views/wallet_syncing_options_view.dart'; -import 'package:stackwallet/pages/settings_views/global_settings_view/xpub_view.dart'; import 'package:stackwallet/pages/settings_views/wallet_settings_view/wallet_backup_views/wallet_backup_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/settings_views/wallet_settings_view/wallet_settings_wallet_settings/change_representative_view.dart'; import 'package:stackwallet/pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/delete_wallet_recovery_phrase_view.dart'; import 'package:stackwallet/pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/delete_wallet_warning_view.dart'; 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/settings_views/wallet_settings_view/wallet_settings_wallet_settings/xpub_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'; @@ -564,6 +565,20 @@ class RouteGenerator { } return _routeError("${settings.name} invalid args: ${args.toString()}"); + case ChangeRepresentativeView.routeName: + if (args is String) { + return getRoute( + shouldUseMaterialRoute: useMaterialPageRoute, + builder: (_) => ChangeRepresentativeView( + walletId: args, + ), + settings: RouteSettings( + name: settings.name, + ), + ); + } + return _routeError("${settings.name} invalid args: ${args.toString()}"); + case AppearanceSettingsView.routeName: return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, diff --git a/lib/services/coins/banano/banano_wallet.dart b/lib/services/coins/banano/banano_wallet.dart index 8511f5599..a050b4027 100644 --- a/lib/services/coins/banano/banano_wallet.dart +++ b/lib/services/coins/banano/banano_wallet.dart @@ -14,9 +14,9 @@ import 'package:stackwallet/services/event_bus/events/global/node_connection_sta 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/coin_control_interface.dart'; import 'package:stackwallet/services/mixins/wallet_cache.dart'; import 'package:stackwallet/services/mixins/wallet_db.dart'; +import 'package:stackwallet/services/nano_api.dart'; import 'package:stackwallet/services/node_service.dart'; import 'package:stackwallet/services/transaction_notification_tracker.dart'; import 'package:stackwallet/utilities/amount/amount.dart'; @@ -32,8 +32,7 @@ const int MINIMUM_CONFIRMATIONS = 1; const String DEFAULT_REPRESENTATIVE = "ban_1ka1ium4pfue3uxtntqsrib8mumxgazsjf58gidh1xeo5te3whsq8z476goo"; -class BananoWallet extends CoinServiceAPI - with WalletCache, WalletDB, CoinControlInterface { +class BananoWallet extends CoinServiceAPI with WalletCache, WalletDB { BananoWallet({ required String walletId, required String walletName, @@ -925,4 +924,51 @@ class BananoWallet extends CoinServiceAPI ); await updateCachedChainHeight(height ?? 0); } + + Future getCurrentRepresentative() async { + final serverURI = Uri.parse(getCurrentNode().host); + final address = await currentReceivingAddress; + + final response = await NanoAPI.getAccountInfo( + server: serverURI, + representative: true, + account: address, + ); + + return response.accountInfo?.representative ?? DEFAULT_REPRESENTATIVE; + } + + Future changeRepresentative(String newRepresentative) async { + try { + final serverURI = Uri.parse(getCurrentNode().host); + final balance = this.balance.spendable.raw.toString(); + final String privateKey = await getPrivateKeyFromMnemonic(); + final address = await currentReceivingAddress; + + final response = await NanoAPI.getAccountInfo( + server: serverURI, + representative: true, + account: address, + ); + + if (response.accountInfo == null) { + throw response.exception ?? Exception("Failed to get account info"); + } + + final work = await requestWork(response.accountInfo!.frontier); + + return await NanoAPI.changeRepresentative( + server: serverURI, + accountType: NanoAccountType.BANANO, + account: address, + newRepresentative: newRepresentative, + previousBlock: response.accountInfo!.frontier, + balance: balance, + privateKey: privateKey, + work: work!, + ); + } catch (_) { + rethrow; + } + } } diff --git a/lib/services/coins/nano/nano_wallet.dart b/lib/services/coins/nano/nano_wallet.dart index 1e783bf5f..391303675 100644 --- a/lib/services/coins/nano/nano_wallet.dart +++ b/lib/services/coins/nano/nano_wallet.dart @@ -24,9 +24,9 @@ import 'package:stackwallet/services/event_bus/events/global/node_connection_sta 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/coin_control_interface.dart'; import 'package:stackwallet/services/mixins/wallet_cache.dart'; import 'package:stackwallet/services/mixins/wallet_db.dart'; +import 'package:stackwallet/services/nano_api.dart'; import 'package:stackwallet/services/node_service.dart'; import 'package:stackwallet/services/transaction_notification_tracker.dart'; import 'package:stackwallet/utilities/amount/amount.dart'; @@ -42,8 +42,7 @@ const int MINIMUM_CONFIRMATIONS = 1; const String DEFAULT_REPRESENTATIVE = "nano_38713x95zyjsqzx6nm1dsom1jmm668owkeb9913ax6nfgj15az3nu8xkx579"; -class NanoWallet extends CoinServiceAPI - with WalletCache, WalletDB, CoinControlInterface { +class NanoWallet extends CoinServiceAPI with WalletCache, WalletDB { NanoWallet({ required String walletId, required String walletName, @@ -937,4 +936,51 @@ class NanoWallet extends CoinServiceAPI ); await updateCachedChainHeight(height ?? 0); } + + Future getCurrentRepresentative() async { + final serverURI = Uri.parse(getCurrentNode().host); + final address = await currentReceivingAddress; + + final response = await NanoAPI.getAccountInfo( + server: serverURI, + representative: true, + account: address, + ); + + return response.accountInfo?.representative ?? DEFAULT_REPRESENTATIVE; + } + + Future changeRepresentative(String newRepresentative) async { + try { + final serverURI = Uri.parse(getCurrentNode().host); + final balance = this.balance.spendable.raw.toString(); + final String privateKey = await getPrivateKeyFromMnemonic(); + final address = await currentReceivingAddress; + + final response = await NanoAPI.getAccountInfo( + server: serverURI, + representative: true, + account: address, + ); + + if (response.accountInfo == null) { + throw response.exception ?? Exception("Failed to get account info"); + } + + final work = await requestWork(response.accountInfo!.frontier); + + return await NanoAPI.changeRepresentative( + server: serverURI, + accountType: NanoAccountType.NANO, + account: address, + newRepresentative: newRepresentative, + previousBlock: response.accountInfo!.frontier, + balance: balance, + privateKey: privateKey, + work: work!, + ); + } catch (_) { + rethrow; + } + } } diff --git a/lib/services/nano_api.dart b/lib/services/nano_api.dart new file mode 100644 index 000000000..3173bfbe6 --- /dev/null +++ b/lib/services/nano_api.dart @@ -0,0 +1,130 @@ +import 'dart:convert'; + +import 'package:http/http.dart' as http; +import 'package:nanodart/nanodart.dart'; + +class NanoAPI { + static Future< + ({ + NAccountInfo? accountInfo, + Exception? exception, + })> getAccountInfo({ + required Uri server, + required bool representative, + required String account, + }) async { + NAccountInfo? accountInfo; + Exception? exception; + + try { + final response = await http.post( + server, + headers: { + "Content-Type": "application/json", + }, + body: jsonEncode({ + "action": "account_info", + "representative": "true", + "account": account, + }), + ); + + final map = jsonDecode(response.body); + + if (map is Map && map["error"] != null) { + throw Exception(map["error"].toString()); + } + + accountInfo = NAccountInfo( + frontier: map["frontier"] as String, + representative: map["representative"] as String, + ); + } on Exception catch (e) { + exception = e; + } catch (e) { + exception = Exception(e.toString()); + } + + return (accountInfo: accountInfo, exception: exception); + } + + static Future changeRepresentative({ + required Uri server, + required int accountType, + required String account, + required String newRepresentative, + required String previousBlock, + required String balance, + required String privateKey, + required String work, + }) async { + Map block = { + "type": "state", + "account": account, + "previous": previousBlock, + "representative": newRepresentative, + "balance": balance, + "link": + "0000000000000000000000000000000000000000000000000000000000000000", + "work": work, + }; + + final String hash; + + try { + hash = NanoBlocks.computeStateHash( + accountType, + account, + previousBlock, + newRepresentative, + BigInt.parse(balance), + block["link"] as String, + ); + } catch (e) { + if (e is RangeError) { + throw Exception("Invalid representative format"); + } + rethrow; + } + + final signature = NanoSignatures.signBlock(hash, privateKey); + + block["signature"] = signature; + + final map = await postBlock(server: server, block: block); + + if (map is Map && map["error"] != null) { + throw Exception(map["error"].toString()); + } + + return map["error"] == null; + } + + // TODO: GET RID OF DYNAMIC AND USED TYPED DATA + static Future postBlock({ + required Uri server, + required Map block, + }) async { + final response = await http.post( + server, + headers: { + "Content-Type": "application/json", + }, + body: jsonEncode({ + "action": "process", + "json_block": "true", + "subtype": "change", + "block": block, + }), + ); + + return jsonDecode(response.body); + } +} + +class NAccountInfo { + final String frontier; + final String representative; + + NAccountInfo({required this.frontier, required this.representative}); +} diff --git a/lib/utilities/show_loading.dart b/lib/utilities/show_loading.dart index db4437d6f..759eaa05c 100644 --- a/lib/utilities/show_loading.dart +++ b/lib/utilities/show_loading.dart @@ -12,15 +12,17 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:stackwallet/themes/stack_colors.dart'; +import 'package:stackwallet/utilities/logger.dart'; import 'package:stackwallet/widgets/custom_loading_overlay.dart'; -Future showLoading({ +Future showLoading({ required Future whileFuture, required BuildContext context, required String message, String? subMessage, bool isDesktop = false, bool opaqueBG = false, + void Function(Exception)? onException, }) async { unawaited( showDialog( @@ -43,10 +45,24 @@ Future showLoading({ ), ); - final result = await whileFuture; + Exception? ex; + T? result; + + try { + result = await whileFuture; + } catch (e, s) { + Logging.instance.log( + "showLoading caught: $e\n$s", + level: LogLevel.Warning, + ); + ex = e is Exception ? e : Exception(e.toString()); + } if (context.mounted) { Navigator.of(context, rootNavigator: isDesktop).pop(); + if (ex != null) { + onException?.call(ex); + } } return result; diff --git a/lib/widgets/wallet_card.dart b/lib/widgets/wallet_card.dart index 63b3a4a0e..09dcb0e2e 100644 --- a/lib/widgets/wallet_card.dart +++ b/lib/widgets/wallet_card.dart @@ -140,7 +140,7 @@ class SimpleWalletCard extends ConsumerWidget { isDesktop: Util.isDesktop, ); - if (!success) { + if (!success!) { // TODO: show error dialog here? Logging.instance.log( "Failed to load token wallet for $contract", From 0d8b710f295b2e2cd4313ed0dbd39d72962cb51c Mon Sep 17 00:00:00 2001 From: julian Date: Fri, 21 Jul 2023 14:52:22 -0600 Subject: [PATCH 072/169] salty sodium ref --- crypto_plugins/flutter_libmonero | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crypto_plugins/flutter_libmonero b/crypto_plugins/flutter_libmonero index 407425c9f..d410d983f 160000 --- a/crypto_plugins/flutter_libmonero +++ b/crypto_plugins/flutter_libmonero @@ -1 +1 @@ -Subproject commit 407425c9fcf7a30c81f1345246c7225bc18b5cd5 +Subproject commit d410d983f071eaf01372c358d9f0b16af85611f7 From 5a845f866936bdc6e2f5b66e540cfb29878ed05c Mon Sep 17 00:00:00 2001 From: sneurlax Date: Fri, 21 Jul 2023 16:35:00 -0500 Subject: [PATCH 073/169] hide certain fields on ordinals views, change ordinal card aspect ratio --- lib/pages/ordinals/ordinal_details_view.dart | 14 +++++++------- lib/pages/ordinals/widgets/ordinal_card.dart | 12 ++++++------ lib/pages/ordinals/widgets/ordinals_list.dart | 4 ++-- 3 files changed, 15 insertions(+), 15 deletions(-) diff --git a/lib/pages/ordinals/ordinal_details_view.dart b/lib/pages/ordinals/ordinal_details_view.dart index 6aab95586..95cff734e 100644 --- a/lib/pages/ordinals/ordinal_details_view.dart +++ b/lib/pages/ordinals/ordinal_details_view.dart @@ -191,13 +191,13 @@ class _OrdinalImageGroup extends StatelessWidget { mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.center, children: [ - Text( - "${ordinal.inscriptionId}", // Use any other property you want - style: STextStyles.w600_16(context), - ), - const SizedBox( - height: _spacing, - ), + // Text( + // "${ordinal.inscriptionId}", // Use any other property you want + // style: STextStyles.w600_16(context), + // ), + // const SizedBox( + // height: _spacing, + // ), AspectRatio( aspectRatio: 1, child: AspectRatio( diff --git a/lib/pages/ordinals/widgets/ordinal_card.dart b/lib/pages/ordinals/widgets/ordinal_card.dart index c9060b32e..4ac4fd877 100644 --- a/lib/pages/ordinals/widgets/ordinal_card.dart +++ b/lib/pages/ordinals/widgets/ordinal_card.dart @@ -41,14 +41,14 @@ class OrdinalCard extends StatelessWidget { ), const Spacer(), Text( - 'TODO', // infer from address associated with utxoTXID + 'INSC. ${ordinal.inscriptionNumber}', // infer from address associated with utxoTXID style: STextStyles.w500_12(context), ), - const Spacer(), - Text( - "INSC. ${ordinal.inscriptionNumber} ID ${ordinal.inscriptionId}", - style: STextStyles.w500_8(context), - ), + // const Spacer(), + // Text( + // "ID ${ordinal.inscriptionId}", + // style: STextStyles.w500_8(context), + // ), ], ), ); diff --git a/lib/pages/ordinals/widgets/ordinals_list.dart b/lib/pages/ordinals/widgets/ordinals_list.dart index fe7618cd0..62d620130 100644 --- a/lib/pages/ordinals/widgets/ordinals_list.dart +++ b/lib/pages/ordinals/widgets/ordinals_list.dart @@ -33,7 +33,7 @@ class OrdinalsList extends StatelessWidget { crossAxisSpacing: spacing as double, mainAxisSpacing: spacing as double, crossAxisCount: 2, - childAspectRatio: 3 / 4, + childAspectRatio: 6 / 7, // was 3/4, less data displayed now ), itemBuilder: (_, i) => OrdinalCard( walletId: walletId, @@ -41,7 +41,7 @@ class OrdinalsList extends StatelessWidget { ), ); } else { - return Text('No data found.'); + return const Text('No data found.'); } }, ); From 3125a25a690198dff6b4c7012f84b6937f3c074c Mon Sep 17 00:00:00 2001 From: julian Date: Fri, 21 Jul 2023 15:58:28 -0600 Subject: [PATCH 074/169] add change rep to desktop --- .../change_representative_view.dart | 57 ++++++++++--- .../sub_widgets/wallet_options_button.dart | 81 ++++++++++++++++++- 2 files changed, 125 insertions(+), 13 deletions(-) diff --git a/lib/pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/change_representative_view.dart b/lib/pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/change_representative_view.dart index 76bb833b1..5c94c0bc5 100644 --- a/lib/pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/change_representative_view.dart +++ b/lib/pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/change_representative_view.dart @@ -87,6 +87,7 @@ class _XPubViewState extends ConsumerState { whileFuture: changeFuture(_textController.text), context: context, message: "Updating representative...", + isDesktop: Util.isDesktop, onException: (ex) { String msg = ex.toString(); while (msg.isNotEmpty && msg.startsWith("Exception:")) { @@ -214,7 +215,7 @@ class _XPubViewState extends ConsumerState { left: 32, ), child: Text( - "${ref.watch(walletsChangeNotifierProvider.select((value) => value.getManager(widget.walletId).walletName))} xPub", + "Change representative", style: STextStyles.desktopH2(context), ), ), @@ -240,7 +241,7 @@ class _XPubViewState extends ConsumerState { ), child: Column( children: [ - if (isDesktop) const SizedBox(height: 44), + if (isDesktop) const SizedBox(height: 24), ConditionalParent( condition: !isDesktop, builder: (child) => Expanded( @@ -269,15 +270,48 @@ class _XPubViewState extends ConsumerState { } else { child = Column( children: [ - RoundedWhiteContainer( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - representative!, - style: STextStyles.itemSubtitle(context), - ), - ], + ConditionalParent( + condition: !isDesktop, + builder: (child) => RoundedWhiteContainer( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + child, + ], + ), + ), + child: ConditionalParent( + condition: isDesktop, + builder: (child) => Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Current representative", + style: STextStyles.desktopTextExtraExtraSmall( + context), + ), + const SizedBox( + height: 4, + ), + child, + ], + ), + child: Row( + children: [ + SelectableText( + representative!, + style: isDesktop + ? STextStyles.desktopTextExtraExtraSmall( + context) + .copyWith( + color: Theme.of(context) + .extension()! + .textDark, + ) + : STextStyles.itemSubtitle12(context), + ), + ], + ), ), ), const SizedBox( @@ -337,6 +371,7 @@ class _XPubViewState extends ConsumerState { ), ), ), + if (isDesktop) const SizedBox(height: 60), if (!isDesktop) const Spacer(), PrimaryButton( label: "Save", diff --git a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/wallet_options_button.dart b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/wallet_options_button.dart index 0eda83ce4..61c61a807 100644 --- a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/wallet_options_button.dart +++ b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/wallet_options_button.dart @@ -13,6 +13,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/pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/change_representative_view.dart'; import 'package:stackwallet/pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/xpub_view.dart'; import 'package:stackwallet/pages_desktop_specific/addresses/desktop_wallet_addresses_view.dart'; import 'package:stackwallet/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/desktop_delete_wallet_dialog.dart'; @@ -21,11 +22,13 @@ import 'package:stackwallet/route_generator.dart'; import 'package:stackwallet/themes/stack_colors.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'; enum _WalletOptions { addressList, deleteWallet, + changeRepresentative, showXpub; String get prettyName { @@ -34,6 +37,8 @@ enum _WalletOptions { return "Address list"; case _WalletOptions.deleteWallet: return "Delete wallet"; + case _WalletOptions.changeRepresentative: + return "Change representative"; case _WalletOptions.showXpub: return "Show xPub"; } @@ -70,6 +75,9 @@ class WalletOptionsButton extends StatelessWidget { onAddressListPressed: () async { Navigator.of(context).pop(_WalletOptions.addressList); }, + onChangeRepPressed: () async { + Navigator.of(context).pop(_WalletOptions.changeRepresentative); + }, onShowXpubPressed: () async { Navigator.of(context).pop(_WalletOptions.showXpub); }, @@ -134,6 +142,32 @@ class WalletOptionsButton extends StatelessWidget { ), ); + if (result == true) { + if (context.mounted) { + Navigator.of(context).pop(); + } + } + break; + case _WalletOptions.changeRepresentative: + final result = await showDialog( + context: context, + barrierDismissible: false, + builder: (context) => Navigator( + initialRoute: ChangeRepresentativeView.routeName, + onGenerateRoute: RouteGenerator.generateRoute, + onGenerateInitialRoutes: (_, __) { + return [ + RouteGenerator.generateRoute( + RouteSettings( + name: ChangeRepresentativeView.routeName, + arguments: walletId, + ), + ), + ]; + }, + ), + ); + if (result == true) { if (context.mounted) { Navigator.of(context).pop(); @@ -171,18 +205,24 @@ class WalletOptionsPopupMenu extends ConsumerWidget { required this.onDeletePressed, required this.onAddressListPressed, required this.onShowXpubPressed, + required this.onChangeRepPressed, required this.walletId, }) : super(key: key); final VoidCallback onDeletePressed; final VoidCallback onAddressListPressed; final VoidCallback onShowXpubPressed; + final VoidCallback onChangeRepPressed; final String walletId; @override Widget build(BuildContext context, WidgetRef ref) { - final bool xpubEnabled = ref.watch(walletsChangeNotifierProvider - .select((value) => value.getManager(walletId).hasXPub)); + final manager = ref.watch(walletsChangeNotifierProvider + .select((value) => value.getManager(walletId))); + final bool xpubEnabled = manager.hasXPub; + + final bool canChangeRep = + manager.coin == Coin.nano || manager.coin == Coin.banano; return Stack( children: [ @@ -237,6 +277,43 @@ class WalletOptionsPopupMenu extends ConsumerWidget { ), ), ), + if (canChangeRep) + const SizedBox( + height: 8, + ), + if (canChangeRep) + TransparentButton( + onPressed: onChangeRepPressed, + child: Padding( + padding: const EdgeInsets.all(8), + child: Row( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + SvgPicture.asset( + Assets.svg.eye, + width: 20, + height: 20, + color: Theme.of(context) + .extension()! + .textFieldActiveSearchIconLeft, + ), + const SizedBox(width: 14), + Expanded( + child: Text( + _WalletOptions.changeRepresentative.prettyName, + style: STextStyles.desktopTextExtraExtraSmall( + context) + .copyWith( + color: Theme.of(context) + .extension()! + .textDark, + ), + ), + ), + ], + ), + ), + ), if (xpubEnabled) const SizedBox( height: 8, From 8a0a45f131419380fd21e6e753cfb599d3250fa6 Mon Sep 17 00:00:00 2001 From: julian Date: Fri, 21 Jul 2023 16:09:47 -0600 Subject: [PATCH 075/169] mobile rep fix --- .../change_representative_view.dart | 32 +++++++++---------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/lib/pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/change_representative_view.dart b/lib/pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/change_representative_view.dart index 5c94c0bc5..a91b0cb4e 100644 --- a/lib/pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/change_representative_view.dart +++ b/lib/pages/settings_views/wallet_settings_view/wallet_settings_wallet_settings/change_representative_view.dart @@ -293,25 +293,25 @@ class _XPubViewState extends ConsumerState { const SizedBox( height: 4, ), - child, - ], - ), - child: Row( - children: [ - SelectableText( - representative!, - style: isDesktop - ? STextStyles.desktopTextExtraExtraSmall( - context) - .copyWith( - color: Theme.of(context) - .extension()! - .textDark, - ) - : STextStyles.itemSubtitle12(context), + Row( + children: [ + child, + ], ), ], ), + child: SelectableText( + representative!, + style: isDesktop + ? STextStyles.desktopTextExtraExtraSmall( + context) + .copyWith( + color: Theme.of(context) + .extension()! + .textDark, + ) + : STextStyles.itemSubtitle12(context), + ), ), ), const SizedBox( From af5f6849b333b8dd0aae745717830f71a7e38702 Mon Sep 17 00:00:00 2001 From: sneurlax Date: Fri, 21 Jul 2023 17:54:51 -0500 Subject: [PATCH 076/169] add desktop ordinals page stub --- .../sub_widgets/desktop_wallet_features.dart | 11 ++ .../more_features/more_features_dialog.dart | 11 +- .../ordinals/desktop_ordinals_view.dart | 115 ++++++++++++++++++ lib/route_generator.dart | 15 +++ 4 files changed, 151 insertions(+), 1 deletion(-) create mode 100644 lib/pages_desktop_specific/ordinals/desktop_ordinals_view.dart 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 aa5922959..f116e6656 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 @@ -19,6 +19,7 @@ import 'package:stackwallet/notifications/show_flush_bar.dart'; import 'package:stackwallet/pages/paynym/paynym_claim_view.dart'; import 'package:stackwallet/pages/paynym/paynym_home_view.dart'; import 'package:stackwallet/pages_desktop_specific/coin_control/desktop_coin_control_view.dart'; +import 'package:stackwallet/pages_desktop_specific/ordinals/desktop_ordinals_view.dart'; import 'package:stackwallet/pages_desktop_specific/desktop_menu.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/more_features/more_features_dialog.dart'; @@ -80,6 +81,7 @@ class _DesktopWalletFeaturesState extends ConsumerState { onCoinControlPressed: _onCoinControlPressed, onAnonymizeAllPressed: _onAnonymizeAllPressed, onWhirlpoolPressed: _onWhirlpoolPressed, + onOrdinalsPressed: _onOrdinalsPressed, ), ); } @@ -313,6 +315,15 @@ class _DesktopWalletFeaturesState extends ConsumerState { } } + void _onOrdinalsPressed() { + Navigator.of(context, rootNavigator: true).pop(); + + Navigator.of(context).pushNamed( + DesktopOrdinalsView.routeName, + arguments: widget.walletId, + ); + } + @override Widget build(BuildContext context) { final manager = ref.watch( diff --git a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/more_features/more_features_dialog.dart b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/more_features/more_features_dialog.dart index 4c8f47c25..fc9c03de4 100644 --- a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/more_features/more_features_dialog.dart +++ b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/more_features/more_features_dialog.dart @@ -29,6 +29,7 @@ class MoreFeaturesDialog extends ConsumerStatefulWidget { required this.onCoinControlPressed, required this.onAnonymizeAllPressed, required this.onWhirlpoolPressed, + required this.onOrdinalsPressed, }) : super(key: key); final String walletId; @@ -36,6 +37,7 @@ class MoreFeaturesDialog extends ConsumerStatefulWidget { final VoidCallback? onCoinControlPressed; final VoidCallback? onAnonymizeAllPressed; final VoidCallback? onWhirlpoolPressed; + final VoidCallback? onOrdinalsPressed; @override ConsumerState createState() => _MoreFeaturesDialogState(); @@ -100,9 +102,16 @@ class _MoreFeaturesDialogState extends ConsumerState { _MoreFeaturesItem( label: "PayNym", detail: "Increased address privacy using BIP47", - iconAsset: Assets.svg.robotHead, + iconAsset: Assets.svg.ordinal, onPressed: () => widget.onPaynymPressed?.call(), ), + if (manager.hasOrdinalsSupport) + _MoreFeaturesItem( + label: "Ordinals", + detail: "View and control your ordinals in Stack", + iconAsset: Assets.svg.ordinal, + onPressed: () => widget.onOrdinalsPressed?.call(), + ), const SizedBox( height: 28, ), diff --git a/lib/pages_desktop_specific/ordinals/desktop_ordinals_view.dart b/lib/pages_desktop_specific/ordinals/desktop_ordinals_view.dart new file mode 100644 index 000000000..a2f6adf68 --- /dev/null +++ b/lib/pages_desktop_specific/ordinals/desktop_ordinals_view.dart @@ -0,0 +1,115 @@ +/* + * This file is part of Stack Wallet. + * + * Copyright (c) 2023 Cypher Stack + * All Rights Reserved. + * The code is distributed under GPLv3 license, see LICENSE file for details. + * Generated by Cypher Stack on 2023-05-26 + * + */ + +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_svg/svg.dart'; +import 'package:stackwallet/models/isar/models/contact_entry.dart'; +import 'package:stackwallet/pages/address_book_views/subviews/add_address_book_entry_view.dart'; +import 'package:stackwallet/pages/address_book_views/subviews/address_book_filter_view.dart'; +import 'package:stackwallet/pages_desktop_specific/address_book_view/subwidgets/desktop_address_book_scaffold.dart'; +import 'package:stackwallet/pages_desktop_specific/address_book_view/subwidgets/desktop_contact_details.dart'; +import 'package:stackwallet/providers/global/address_book_service_provider.dart'; +import 'package:stackwallet/providers/providers.dart'; +import 'package:stackwallet/providers/ui/address_book_providers/address_book_filter_provider.dart'; +import 'package:stackwallet/themes/stack_colors.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/util.dart'; +import 'package:stackwallet/widgets/address_book_card.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/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_container.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 DesktopOrdinalsView extends ConsumerStatefulWidget { + const DesktopOrdinalsView({ + super.key, + required this.walletId, + }); + + static const String routeName = "/desktopOrdinalsView"; + + final String walletId; + + @override + ConsumerState createState() => _DesktopOrdinals(); +} + +class _DesktopOrdinals extends ConsumerState { + late final TextEditingController _searchController; + late final FocusNode _searchFocusNode; + + String _searchTerm = ""; + dynamic _manager; + + @override + void initState() { + _searchController = TextEditingController(); + _searchFocusNode = FocusNode(); + + super.initState(); + } + + @override + void didChangeDependencies() { + super.didChangeDependencies(); + // Set _manager here when the widget's dependencies change + _manager = ref.watch(walletsChangeNotifierProvider + .select((value) => value.getManager(widget.walletId))); + } + + @override + void dispose() { + _searchController.dispose(); + _searchFocusNode.dispose(); + + super.dispose(); + } + + @override + Widget build(BuildContext context) { + debugPrint("BUILD: $runtimeType"); + + return DesktopScaffold( + appBar: DesktopAppBar( + isCompactHeight: true, + leading: Row( + children: [ + const SizedBox( + width: 24, + ), + Text( + "Ordinals", + style: STextStyles.desktopH3(context), + ) + ], + ), + ), + body: const Padding( + padding: EdgeInsets.only( + left: 24, + right: 24, + bottom: 24, + ), + child: Text( + "TODO") + ), + ); + } +} diff --git a/lib/route_generator.dart b/lib/route_generator.dart index 7c0807caf..06395906e 100644 --- a/lib/route_generator.dart +++ b/lib/route_generator.dart @@ -146,6 +146,7 @@ import 'package:stackwallet/pages_desktop_specific/my_stack_view/wallet_view/sub import 'package:stackwallet/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/unlock_wallet_keys_desktop.dart'; import 'package:stackwallet/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/wallet_keys_desktop_popup.dart'; import 'package:stackwallet/pages_desktop_specific/notifications/desktop_notifications_view.dart'; +import 'package:stackwallet/pages_desktop_specific/ordinals/desktop_ordinals_view.dart'; import 'package:stackwallet/pages_desktop_specific/password/create_password_view.dart'; import 'package:stackwallet/pages_desktop_specific/password/delete_password_warning_view.dart'; import 'package:stackwallet/pages_desktop_specific/password/forgot_password_desktop_view.dart'; @@ -422,6 +423,20 @@ class RouteGenerator { } return _routeError("${settings.name} invalid args: ${args.toString()}"); + case DesktopOrdinalsView.routeName: + if (args is String) { + return getRoute( + shouldUseMaterialRoute: useMaterialPageRoute, + builder: (_) => DesktopOrdinalsView( + walletId: args, + ), + settings: RouteSettings( + name: settings.name, + ), + ); + } + return _routeError("${settings.name} invalid args: ${args.toString()}"); + case OrdinalDetailsView.routeName: if (args is ({Ordinal ordinal, String walletId})) { return getRoute( From 89e803d77d38c8fea74de6f6be0be6888bfb9be6 Mon Sep 17 00:00:00 2001 From: julian Date: Fri, 21 Jul 2023 16:58:20 -0600 Subject: [PATCH 077/169] update xmr ref --- crypto_plugins/flutter_libmonero | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crypto_plugins/flutter_libmonero b/crypto_plugins/flutter_libmonero index d410d983f..e48952185 160000 --- a/crypto_plugins/flutter_libmonero +++ b/crypto_plugins/flutter_libmonero @@ -1 +1 @@ -Subproject commit d410d983f071eaf01372c358d9f0b16af85611f7 +Subproject commit e48952185556a10f182184fd572bcb04365f5831 From 8118f98e5eac1cb774977294e357c29222dcf6af Mon Sep 17 00:00:00 2001 From: sneurlax Date: Fri, 21 Jul 2023 18:13:40 -0500 Subject: [PATCH 078/169] mobile ordinals in desktop TODO make desktop OrdinalsList and OrdinalsDetailsView --- .../ordinals/desktop_ordinals_view.dart | 97 ++++++++++++++++--- 1 file changed, 86 insertions(+), 11 deletions(-) diff --git a/lib/pages_desktop_specific/ordinals/desktop_ordinals_view.dart b/lib/pages_desktop_specific/ordinals/desktop_ordinals_view.dart index a2f6adf68..46d732e3a 100644 --- a/lib/pages_desktop_specific/ordinals/desktop_ordinals_view.dart +++ b/lib/pages_desktop_specific/ordinals/desktop_ordinals_view.dart @@ -14,11 +14,13 @@ import 'package:flutter_svg/svg.dart'; import 'package:stackwallet/models/isar/models/contact_entry.dart'; import 'package:stackwallet/pages/address_book_views/subviews/add_address_book_entry_view.dart'; import 'package:stackwallet/pages/address_book_views/subviews/address_book_filter_view.dart'; +import 'package:stackwallet/pages/ordinals/widgets/ordinals_list.dart'; import 'package:stackwallet/pages_desktop_specific/address_book_view/subwidgets/desktop_address_book_scaffold.dart'; import 'package:stackwallet/pages_desktop_specific/address_book_view/subwidgets/desktop_contact_details.dart'; import 'package:stackwallet/providers/global/address_book_service_provider.dart'; import 'package:stackwallet/providers/providers.dart'; import 'package:stackwallet/providers/ui/address_book_providers/address_book_filter_provider.dart'; +import 'package:stackwallet/services/mixins/ordinals_interface.dart'; import 'package:stackwallet/themes/stack_colors.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/constants.dart'; @@ -37,6 +39,7 @@ 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 DesktopOrdinalsView extends ConsumerStatefulWidget { const DesktopOrdinalsView({ super.key, @@ -52,16 +55,16 @@ class DesktopOrdinalsView extends ConsumerStatefulWidget { } class _DesktopOrdinals extends ConsumerState { - late final TextEditingController _searchController; - late final FocusNode _searchFocusNode; + late final TextEditingController searchController; + late final FocusNode searchFocusNode; String _searchTerm = ""; dynamic _manager; @override - void initState() { - _searchController = TextEditingController(); - _searchFocusNode = FocusNode(); + void initState() { + searchController = TextEditingController(); + searchFocusNode = FocusNode(); super.initState(); } @@ -76,8 +79,8 @@ class _DesktopOrdinals extends ConsumerState { @override void dispose() { - _searchController.dispose(); - _searchFocusNode.dispose(); + searchController.dispose(); + searchFocusNode.dispose(); super.dispose(); } @@ -101,14 +104,86 @@ class _DesktopOrdinals extends ConsumerState { ], ), ), - body: const Padding( - padding: EdgeInsets.only( + body: Padding( + padding: const EdgeInsets.only( left: 24, right: 24, bottom: 24, ), - child: Text( - "TODO") + child: Padding( + padding: const EdgeInsets.only( + left: 16, + right: 16, + top: 8, + ), + child: Column( + children: [ + ClipRRect( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + child: TextField( + autocorrect: Util.isDesktop ? false : true, + enableSuggestions: Util.isDesktop ? false : true, + controller: searchController, + focusNode: searchFocusNode, + onChanged: (value) { + setState(() { + _searchTerm = value; + }); + }, + style: STextStyles.field(context), + decoration: standardInputDecoration( + "Search", + searchFocusNode, + context, + ).copyWith( + prefixIcon: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 10, + vertical: 16, + ), + child: SvgPicture.asset( + Assets.svg.search, + width: 16, + height: 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 = ""; + _searchTerm = ""; + }); + }, + ), + ], + ), + ), + ) + : null, + ), + ), + ), + const SizedBox( + height: 16, + ), + Expanded( + child: OrdinalsList( + walletId: widget.walletId, + ordinalsFuture: (_manager.wallet as OrdinalsInterface).getOrdinals(), + ), + ), + ], + ), + ), ), ); } From 9d5e33da01dbb17799c4e1f8dc88a2c707c2518f Mon Sep 17 00:00:00 2001 From: sneurlax Date: Fri, 21 Jul 2023 18:21:15 -0500 Subject: [PATCH 079/169] DesktopOrdinalsList widget --- .../ordinals/desktop_ordinals_view.dart | 4 +- .../subwidgets/desktop_ordinals_list.dart | 49 +++++++++++++++++++ 2 files changed, 51 insertions(+), 2 deletions(-) create mode 100644 lib/pages_desktop_specific/ordinals/subwidgets/desktop_ordinals_list.dart diff --git a/lib/pages_desktop_specific/ordinals/desktop_ordinals_view.dart b/lib/pages_desktop_specific/ordinals/desktop_ordinals_view.dart index 46d732e3a..a50cc85db 100644 --- a/lib/pages_desktop_specific/ordinals/desktop_ordinals_view.dart +++ b/lib/pages_desktop_specific/ordinals/desktop_ordinals_view.dart @@ -14,7 +14,7 @@ import 'package:flutter_svg/svg.dart'; import 'package:stackwallet/models/isar/models/contact_entry.dart'; import 'package:stackwallet/pages/address_book_views/subviews/add_address_book_entry_view.dart'; import 'package:stackwallet/pages/address_book_views/subviews/address_book_filter_view.dart'; -import 'package:stackwallet/pages/ordinals/widgets/ordinals_list.dart'; +import 'package:stackwallet/pages_desktop_specific/ordinals/subwidgets/desktop_ordinals_list.dart'; import 'package:stackwallet/pages_desktop_specific/address_book_view/subwidgets/desktop_address_book_scaffold.dart'; import 'package:stackwallet/pages_desktop_specific/address_book_view/subwidgets/desktop_contact_details.dart'; import 'package:stackwallet/providers/global/address_book_service_provider.dart'; @@ -176,7 +176,7 @@ class _DesktopOrdinals extends ConsumerState { height: 16, ), Expanded( - child: OrdinalsList( + child: DesktopOrdinalsList( walletId: widget.walletId, ordinalsFuture: (_manager.wallet as OrdinalsInterface).getOrdinals(), ), diff --git a/lib/pages_desktop_specific/ordinals/subwidgets/desktop_ordinals_list.dart b/lib/pages_desktop_specific/ordinals/subwidgets/desktop_ordinals_list.dart new file mode 100644 index 000000000..f12fe6834 --- /dev/null +++ b/lib/pages_desktop_specific/ordinals/subwidgets/desktop_ordinals_list.dart @@ -0,0 +1,49 @@ +import 'package:flutter/material.dart'; +import 'package:stackwallet/models/ordinal.dart'; + +import 'package:stackwallet/pages/ordinals/widgets/ordinal_card.dart'; + +class DesktopOrdinalsList extends StatelessWidget { + const DesktopOrdinalsList({ + Key? key, + required this.walletId, + required this.ordinalsFuture, + }) : super(key: key); + + final String walletId; + final Future> ordinalsFuture; + + get spacing => 2.0; + + @override + Widget build(BuildContext context) { + return FutureBuilder>( + future: ordinalsFuture, + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.waiting) { + return const CircularProgressIndicator(); + } else if (snapshot.hasError) { + return Text('Error: ${snapshot.error}'); + } else if (snapshot.hasData) { + final List ordinals = snapshot.data!; + return GridView.builder( + shrinkWrap: true, + itemCount: ordinals.length, + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( + crossAxisSpacing: spacing as double, + mainAxisSpacing: spacing as double, + crossAxisCount: 4, + childAspectRatio: 6 / 7, // was 3/4, less data displayed now + ), + itemBuilder: (_, i) => OrdinalCard( + walletId: walletId, + ordinal: ordinals[i], + ), + ); + } else { + return const Text('No data found.'); + } + }, + ); + } +} \ No newline at end of file From fe41bce563cd4f3ca1a3a24be9f5635fc7bf2dad Mon Sep 17 00:00:00 2001 From: julian Date: Fri, 21 Jul 2023 17:24:20 -0600 Subject: [PATCH 080/169] disable collection type in ord filter --- lib/pages/ordinals/ordinals_filter_view.dart | 309 +++++++++---------- 1 file changed, 154 insertions(+), 155 deletions(-) diff --git a/lib/pages/ordinals/ordinals_filter_view.dart b/lib/pages/ordinals/ordinals_filter_view.dart index e06294098..631a9833a 100644 --- a/lib/pages/ordinals/ordinals_filter_view.dart +++ b/lib/pages/ordinals/ordinals_filter_view.dart @@ -26,21 +26,20 @@ 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/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 OrdinalFilter { - final bool isMoonbird; - final bool isPunk; + // final bool isMoonbird; + // final bool isPunk; final DateTime? from; final DateTime? to; final String? inscription; final String keyword; OrdinalFilter({ - required this.isMoonbird, - required this.isPunk, + // required this.isMoonbird, + // required this.isPunk, required this.from, required this.to, required this.inscription, @@ -48,16 +47,16 @@ class OrdinalFilter { }); OrdinalFilter copyWith({ - bool? isMoonbird, - bool? isPunk, + // bool? isMoonbird, + // bool? isPunk, DateTime? from, DateTime? to, String? inscription, String? keyword, }) { return OrdinalFilter( - isMoonbird: isMoonbird ?? this.isMoonbird, - isPunk: isPunk ?? this.isPunk, + // isMoonbird: isMoonbird ?? this.isMoonbird, + // isPunk: isPunk ?? this.isPunk, from: from ?? this.from, to: to ?? this.to, inscription: inscription ?? this.inscription, @@ -83,8 +82,8 @@ class _OrdinalsFilterViewState extends ConsumerState { final _inscriptionTextEditingController = TextEditingController(); final _keywordTextEditingController = TextEditingController(); - bool _isPunk = false; - bool _isMoonbird = false; + // bool _isPunk = false; + // bool _isMoonbird = false; String _fromDateString = ""; String _toDateString = ""; @@ -99,8 +98,8 @@ class _OrdinalsFilterViewState extends ConsumerState { baseColor = ref.read(themeProvider.state).state.textSubtitle2; final filterState = ref.read(ordinalFilterProvider.state).state; if (filterState != null) { - _isMoonbird = filterState.isMoonbird; - _isPunk = filterState.isPunk; + // _isMoonbird = filterState.isMoonbird; + // _isPunk = filterState.isPunk; _selectedToDate = filterState.to; _selectedFromDate = filterState.from; _keywordTextEditingController.text = filterState.keyword; @@ -521,146 +520,146 @@ class _OrdinalsFilterViewState extends ConsumerState { SizedBox( height: isDesktop ? 14 : 10, ), - if (!isDesktop) - Align( - alignment: Alignment.centerLeft, - child: FittedBox( - child: Text( - "Collection", - style: STextStyles.smallMed12(context), - ), - ), - ), - if (!isDesktop) - const SizedBox( - height: 12, - ), - RoundedWhiteContainer( - padding: EdgeInsets.all(isDesktop ? 0 : 12), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - GestureDetector( - onTap: () { - setState(() { - _isPunk = !_isPunk; - }); - }, - child: Container( - color: Colors.transparent, - child: Row( - children: [ - SizedBox( - height: 20, - width: 20, - child: Checkbox( - key: const Key("OrdinalsPunkCheckboxKey"), - materialTapTargetSize: - MaterialTapTargetSize.shrinkWrap, - value: _isPunk, - onChanged: (newValue) { - setState(() { - _isPunk = newValue!; - }); - }, - ), - ), - const SizedBox( - width: 14, - ), - Align( - alignment: Alignment.centerLeft, - child: FittedBox( - child: Column( - children: [ - Text( - "Punks", - style: isDesktop - ? STextStyles.desktopTextSmall(context) - : STextStyles.itemSubtitle12(context), - ), - if (isDesktop) - const SizedBox( - height: 4, - ), - ], - ), - ), - ) - ], - ), - ), - ), - ], - ), - SizedBox( - height: isDesktop ? 4 : 10, - ), - Row( - children: [ - GestureDetector( - onTap: () { - setState(() { - _isMoonbird = !_isMoonbird; - }); - }, - child: Container( - color: Colors.transparent, - child: Row( - children: [ - SizedBox( - height: 20, - width: 20, - child: Checkbox( - key: const Key( - "OrdinalsFilterMoonbirdCheckboxKey", - ), - materialTapTargetSize: - MaterialTapTargetSize.shrinkWrap, - value: _isMoonbird, - onChanged: (newValue) { - setState(() { - _isMoonbird = newValue!; - }); - }, - ), - ), - const SizedBox( - width: 14, - ), - Align( - alignment: Alignment.centerLeft, - child: FittedBox( - child: Column( - children: [ - Text( - "Moonbirds", - style: isDesktop - ? STextStyles.desktopTextSmall(context) - : STextStyles.itemSubtitle12(context), - ), - if (isDesktop) - const SizedBox( - height: 4, - ), - ], - ), - ), - ) - ], - ), - ), - ), - ], - ), - ], - ), - ), - SizedBox( - height: isDesktop ? 32 : 24, - ), + // if (!isDesktop) + // Align( + // alignment: Alignment.centerLeft, + // child: FittedBox( + // child: Text( + // "Collection", + // style: STextStyles.smallMed12(context), + // ), + // ), + // ), + // if (!isDesktop) + // const SizedBox( + // height: 12, + // ), + // RoundedWhiteContainer( + // padding: EdgeInsets.all(isDesktop ? 0 : 12), + // child: Column( + // crossAxisAlignment: CrossAxisAlignment.start, + // children: [ + // Row( + // children: [ + // GestureDetector( + // onTap: () { + // setState(() { + // _isPunk = !_isPunk; + // }); + // }, + // child: Container( + // color: Colors.transparent, + // child: Row( + // children: [ + // SizedBox( + // height: 20, + // width: 20, + // child: Checkbox( + // key: const Key("OrdinalsPunkCheckboxKey"), + // materialTapTargetSize: + // MaterialTapTargetSize.shrinkWrap, + // value: _isPunk, + // onChanged: (newValue) { + // setState(() { + // _isPunk = newValue!; + // }); + // }, + // ), + // ), + // const SizedBox( + // width: 14, + // ), + // Align( + // alignment: Alignment.centerLeft, + // child: FittedBox( + // child: Column( + // children: [ + // Text( + // "Punks", + // style: isDesktop + // ? STextStyles.desktopTextSmall(context) + // : STextStyles.itemSubtitle12(context), + // ), + // if (isDesktop) + // const SizedBox( + // height: 4, + // ), + // ], + // ), + // ), + // ) + // ], + // ), + // ), + // ), + // ], + // ), + // SizedBox( + // height: isDesktop ? 4 : 10, + // ), + // Row( + // children: [ + // GestureDetector( + // onTap: () { + // setState(() { + // _isMoonbird = !_isMoonbird; + // }); + // }, + // child: Container( + // color: Colors.transparent, + // child: Row( + // children: [ + // SizedBox( + // height: 20, + // width: 20, + // child: Checkbox( + // key: const Key( + // "OrdinalsFilterMoonbirdCheckboxKey", + // ), + // materialTapTargetSize: + // MaterialTapTargetSize.shrinkWrap, + // value: _isMoonbird, + // onChanged: (newValue) { + // setState(() { + // _isMoonbird = newValue!; + // }); + // }, + // ), + // ), + // const SizedBox( + // width: 14, + // ), + // Align( + // alignment: Alignment.centerLeft, + // child: FittedBox( + // child: Column( + // children: [ + // Text( + // "Moonbirds", + // style: isDesktop + // ? STextStyles.desktopTextSmall(context) + // : STextStyles.itemSubtitle12(context), + // ), + // if (isDesktop) + // const SizedBox( + // height: 4, + // ), + // ], + // ), + // ), + // ) + // ], + // ), + // ), + // ), + // ], + // ), + // ], + // ), + // ), + // SizedBox( + // height: isDesktop ? 32 : 24, + // ), Align( alignment: Alignment.centerLeft, child: FittedBox( @@ -875,8 +874,8 @@ class _OrdinalsFilterViewState extends ConsumerState { Future _onApplyPressed() async { final filter = OrdinalFilter( - isPunk: _isPunk, - isMoonbird: _isMoonbird, + // isPunk: _isPunk, + // isMoonbird: _isMoonbird, from: _selectedFromDate, to: _selectedToDate, inscription: _inscriptionTextEditingController.text, From d9d7f256927bf8289230f1292a483de0759d1b24 Mon Sep 17 00:00:00 2001 From: sneurlax Date: Fri, 21 Jul 2023 18:30:41 -0500 Subject: [PATCH 081/169] add desktop ordinals list, card, and details view --- .../desktop_ordinal_details_view.dart | 269 ++++++++++++++++++ .../subwidgets/desktop_ordinal_card.dart | 56 ++++ .../subwidgets/desktop_ordinals_list.dart | 4 +- lib/route_generator.dart | 16 ++ 4 files changed, 343 insertions(+), 2 deletions(-) create mode 100644 lib/pages_desktop_specific/ordinals/desktop_ordinal_details_view.dart create mode 100644 lib/pages_desktop_specific/ordinals/subwidgets/desktop_ordinal_card.dart diff --git a/lib/pages_desktop_specific/ordinals/desktop_ordinal_details_view.dart b/lib/pages_desktop_specific/ordinals/desktop_ordinal_details_view.dart new file mode 100644 index 000000000..36c8e33cb --- /dev/null +++ b/lib/pages_desktop_specific/ordinals/desktop_ordinal_details_view.dart @@ -0,0 +1,269 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:stackwallet/models/ordinal.dart'; +import 'package:stackwallet/notifications/show_flush_bar.dart'; +import 'package:stackwallet/pages/ordinals/widgets/dialogs.dart'; +import 'package:stackwallet/themes/stack_colors.dart'; +import 'package:stackwallet/utilities/assets.dart'; +import 'package:stackwallet/utilities/text_styles.dart'; +import 'package:stackwallet/widgets/background.dart'; +import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; +import 'package:stackwallet/widgets/desktop/primary_button.dart'; +import 'package:stackwallet/widgets/desktop/secondary_button.dart'; +import 'package:stackwallet/widgets/rounded_white_container.dart'; + +class DesktopOrdinalDetailsView extends StatefulWidget { + const DesktopOrdinalDetailsView({ + Key? key, + required this.walletId, + required this.ordinal, + }) : super(key: key); + + final String walletId; + final Ordinal ordinal; + + static const routeName = "/desktopOrdinalDetailsView"; + + @override + _DesktopOrdinalDetailsViewState createState() => _DesktopOrdinalDetailsViewState(); +} + +class _DesktopOrdinalDetailsViewState extends State { + static const _spacing = 12.0; + + @override + Widget build(BuildContext context) { + return Background( + child: SafeArea( + child: Scaffold( + backgroundColor: + Theme.of(context).extension()!.background, + appBar: AppBar( + backgroundColor: + Theme.of(context).extension()!.background, + leading: const AppBarBackButton(), + title: Text( + "Ordinal details", + style: STextStyles.navBarTitle(context), + ), + ), + body: SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Column( + children: [ + Padding( + padding: const EdgeInsets.symmetric( + vertical: 12, + horizontal: 39, + ), + child: _OrdinalImageGroup( + ordinal: widget.ordinal, + walletId: widget.walletId, + ), + ), + _DetailsItemWCopy( + title: "Inscription number", + data: widget.ordinal.inscriptionNumber.toString(), + ), + const SizedBox( + height: _spacing, + ), + _DetailsItemWCopy( + title: "ID", + data: widget.ordinal.inscriptionId, + ), + const SizedBox( + height: _spacing, + ), + // todo: add utxo status + const SizedBox( + height: _spacing, + ), + const _DetailsItemWCopy( + title: "Amount", + data: "TODO", // TODO infer from utxo utxoTXID:utxoVOUT + ), + const SizedBox( + height: _spacing, + ), + const _DetailsItemWCopy( + title: "Owner address", + data: "TODO", // infer from address associated w utxoTXID + ), + const SizedBox( + height: _spacing, + ), + _DetailsItemWCopy( + title: "Transaction ID", + data: widget.ordinal.utxoTXID, + ), + const SizedBox( + height: _spacing, + ), + ], + ), + ), + ), + ), + ), + ); + } +} + +class _DetailsItemWCopy extends StatelessWidget { + const _DetailsItemWCopy({ + Key? key, + required this.title, + required this.data, + }) : super(key: key); + + final String title; + final String data; + + @override + Widget build(BuildContext context) { + return RoundedWhiteContainer( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + title, + style: STextStyles.itemSubtitle(context), + ), + GestureDetector( + onTap: () async { + await Clipboard.setData(ClipboardData(text: data)); + if (context.mounted) { + unawaited( + showFloatingFlushBar( + type: FlushBarType.info, + message: "Copied to clipboard", + context: context, + ), + ); + } + }, + child: SvgPicture.asset( + Assets.svg.copy, + color: + Theme.of(context).extension()!.infoItemIcons, + width: 12, + ), + ), + ], + ), + const SizedBox( + height: 4, + ), + SelectableText( + data, + style: STextStyles.itemSubtitle12(context), + ), + ], + ), + ); + } +} + +class _OrdinalImageGroup extends StatelessWidget { + const _OrdinalImageGroup({ + Key? key, + required this.walletId, + required this.ordinal, + }) : super(key: key); + + final String walletId; + final Ordinal ordinal; + + static const _spacing = 12.0; + + @override + Widget build(BuildContext context) { + return Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + // Text( + // "${ordinal.inscriptionId}", // Use any other property you want + // style: STextStyles.w600_16(context), + // ), + // const SizedBox( + // height: _spacing, + // ), + AspectRatio( + aspectRatio: 1, + child: AspectRatio( + aspectRatio: 1, + child: Container( + color: Colors.red, + child: Image.network( + ordinal.content, // Use the preview URL as the image source + fit: BoxFit.cover, + filterQuality: FilterQuality.none, // Set the filter mode to nearest + ), + ), + ), + ), + const SizedBox( + height: _spacing, + ), + Row( + children: [ + Expanded( + child: SecondaryButton( + label: "Download", + icon: SvgPicture.asset( + Assets.svg.arrowDown, + width: 10, + height: 12, + color: Theme.of(context) + .extension()! + .buttonTextSecondary, + ), + buttonHeight: ButtonHeight.l, + iconSpacing: 4, + onPressed: () { + // TODO: save and download image to device + }, + ), + ), + const SizedBox( + width: _spacing, + ), + Expanded( + child: PrimaryButton( + label: "Send", + icon: SvgPicture.asset( + Assets.svg.send, + width: 10, + height: 10, + color: Theme.of(context) + .extension()! + .buttonTextPrimary, + ), + buttonHeight: ButtonHeight.l, + iconSpacing: 4, + onPressed: () async { + final response = await showDialog( + context: context, + builder: (_) => const SendOrdinalUnfreezeDialog(), + ); + if (response == "unfreeze") { + // TODO: unfreeze and go to send ord screen + } + }, + ), + ), + ], + ), + ], + ); + } +} diff --git a/lib/pages_desktop_specific/ordinals/subwidgets/desktop_ordinal_card.dart b/lib/pages_desktop_specific/ordinals/subwidgets/desktop_ordinal_card.dart new file mode 100644 index 000000000..9d94a4b0a --- /dev/null +++ b/lib/pages_desktop_specific/ordinals/subwidgets/desktop_ordinal_card.dart @@ -0,0 +1,56 @@ +import 'package:flutter/material.dart'; + +import 'package:stackwallet/models/ordinal.dart'; +import 'package:stackwallet/pages_desktop_specific/ordinals/desktop_ordinal_details_view.dart'; +import 'package:stackwallet/utilities/text_styles.dart'; +import 'package:stackwallet/widgets/rounded_white_container.dart'; + +class DesktopOrdinalCard extends StatelessWidget { + const DesktopOrdinalCard({ + Key? key, + required this.walletId, + required this.ordinal, + }) : super(key: key); + + final String walletId; + final Ordinal ordinal; + + @override + Widget build(BuildContext context) { + return RoundedWhiteContainer( + radiusMultiplier: 2, + onPressed: () { + Navigator.of(context).pushNamed( + DesktopOrdinalDetailsView.routeName, + arguments: (walletId: walletId, ordinal: ordinal), + ); + }, + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + AspectRatio( + aspectRatio: 1, + child: Container( + color: Colors.red, + child: Image.network( + ordinal.content, // Use the preview URL as the image source + fit: BoxFit.cover, + filterQuality: FilterQuality.none, // Set the filter mode to nearest + ), + ), + ), + const Spacer(), + Text( + 'INSC. ${ordinal.inscriptionNumber}', // infer from address associated with utxoTXID + style: STextStyles.w500_12(context), + ), + // const Spacer(), + // Text( + // "ID ${ordinal.inscriptionId}", + // style: STextStyles.w500_8(context), + // ), + ], + ), + ); + } +} diff --git a/lib/pages_desktop_specific/ordinals/subwidgets/desktop_ordinals_list.dart b/lib/pages_desktop_specific/ordinals/subwidgets/desktop_ordinals_list.dart index f12fe6834..b30e94dd4 100644 --- a/lib/pages_desktop_specific/ordinals/subwidgets/desktop_ordinals_list.dart +++ b/lib/pages_desktop_specific/ordinals/subwidgets/desktop_ordinals_list.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:stackwallet/models/ordinal.dart'; -import 'package:stackwallet/pages/ordinals/widgets/ordinal_card.dart'; +import 'package:stackwallet/pages_desktop_specific/ordinals/subwidgets/desktop_ordinal_card.dart'; class DesktopOrdinalsList extends StatelessWidget { const DesktopOrdinalsList({ @@ -35,7 +35,7 @@ class DesktopOrdinalsList extends StatelessWidget { crossAxisCount: 4, childAspectRatio: 6 / 7, // was 3/4, less data displayed now ), - itemBuilder: (_, i) => OrdinalCard( + itemBuilder: (_, i) => DesktopOrdinalCard( walletId: walletId, ordinal: ordinals[i], ), diff --git a/lib/route_generator.dart b/lib/route_generator.dart index 5a5c1df17..03c7f13bc 100644 --- a/lib/route_generator.dart +++ b/lib/route_generator.dart @@ -147,6 +147,7 @@ import 'package:stackwallet/pages_desktop_specific/my_stack_view/wallet_view/sub import 'package:stackwallet/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/unlock_wallet_keys_desktop.dart'; import 'package:stackwallet/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/wallet_keys_desktop_popup.dart'; import 'package:stackwallet/pages_desktop_specific/notifications/desktop_notifications_view.dart'; +import 'package:stackwallet/pages_desktop_specific/ordinals/desktop_ordinal_details_view.dart'; import 'package:stackwallet/pages_desktop_specific/ordinals/desktop_ordinals_view.dart'; import 'package:stackwallet/pages_desktop_specific/password/create_password_view.dart'; import 'package:stackwallet/pages_desktop_specific/password/delete_password_warning_view.dart'; @@ -453,6 +454,21 @@ class RouteGenerator { } return _routeError("${settings.name} invalid args: ${args.toString()}"); + case DesktopOrdinalDetailsView.routeName: + if (args is ({Ordinal ordinal, String walletId})) { + return getRoute( + shouldUseMaterialRoute: useMaterialPageRoute, + builder: (_) => OrdinalDetailsView( + walletId: args.walletId, + ordinal: args.ordinal, + ), + settings: RouteSettings( + name: settings.name, + ), + ); + } + return _routeError("${settings.name} invalid args: ${args.toString()}"); + case OrdinalsFilterView.routeName: return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, From 8edaa9d353cfbe34b5547d2ea57b4000345224d2 Mon Sep 17 00:00:00 2001 From: sneurlax Date: Fri, 21 Jul 2023 18:50:18 -0500 Subject: [PATCH 082/169] desktop ordinal details view fix --- .../desktop_ordinal_details_view.dart | 172 +++++++++++------- lib/route_generator.dart | 5 +- 2 files changed, 105 insertions(+), 72 deletions(-) diff --git a/lib/pages_desktop_specific/ordinals/desktop_ordinal_details_view.dart b/lib/pages_desktop_specific/ordinals/desktop_ordinal_details_view.dart index 36c8e33cb..03cae351e 100644 --- a/lib/pages_desktop_specific/ordinals/desktop_ordinal_details_view.dart +++ b/lib/pages_desktop_specific/ordinals/desktop_ordinal_details_view.dart @@ -9,8 +9,9 @@ import 'package:stackwallet/pages/ordinals/widgets/dialogs.dart'; import 'package:stackwallet/themes/stack_colors.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/text_styles.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_scaffold.dart'; import 'package:stackwallet/widgets/desktop/primary_button.dart'; import 'package:stackwallet/widgets/desktop/secondary_button.dart'; import 'package:stackwallet/widgets/rounded_white_container.dart'; @@ -36,77 +37,110 @@ class _DesktopOrdinalDetailsViewState extends State { @override Widget build(BuildContext context) { - return Background( - child: SafeArea( - child: Scaffold( - backgroundColor: - Theme.of(context).extension()!.background, - appBar: AppBar( - backgroundColor: - Theme.of(context).extension()!.background, - leading: const AppBarBackButton(), - title: Text( - "Ordinal details", - style: STextStyles.navBarTitle(context), + return DesktopScaffold( + appBar: DesktopAppBar( + background: Theme.of(context).extension()!.popupBG, + leading: Expanded( + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + const SizedBox( + width: 32, + ), + AppBarIconButton( + size: 32, + color: Theme.of(context) + .extension()! + .textFieldDefaultBG, + shadows: const [], + icon: SvgPicture.asset( + Assets.svg.arrowLeft, + width: 18, + height: 18, + color: Theme.of(context) + .extension()! + .topNavIconPrimary, + ), + onPressed: Navigator.of(context).pop, + ), + const SizedBox( + width: 18, + ), + SvgPicture.asset( + Assets.svg.ordinal, + width: 32, + height: 32, + color: + Theme.of(context).extension()!.textSubtitle1, + ), + const SizedBox( + width: 12, + ), + Text( + "Ordinals", + style: STextStyles.desktopH3(context), + ), + ], ), ), - body: SingleChildScrollView( - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 16), - child: Column( - children: [ - Padding( - padding: const EdgeInsets.symmetric( - vertical: 12, - horizontal: 39, - ), - child: _OrdinalImageGroup( - ordinal: widget.ordinal, - walletId: widget.walletId, - ), - ), - _DetailsItemWCopy( - title: "Inscription number", - data: widget.ordinal.inscriptionNumber.toString(), - ), - const SizedBox( - height: _spacing, - ), - _DetailsItemWCopy( - title: "ID", - data: widget.ordinal.inscriptionId, - ), - const SizedBox( - height: _spacing, - ), - // todo: add utxo status - const SizedBox( - height: _spacing, - ), - const _DetailsItemWCopy( - title: "Amount", - data: "TODO", // TODO infer from utxo utxoTXID:utxoVOUT - ), - const SizedBox( - height: _spacing, - ), - const _DetailsItemWCopy( - title: "Owner address", - data: "TODO", // infer from address associated w utxoTXID - ), - const SizedBox( - height: _spacing, - ), - _DetailsItemWCopy( - title: "Transaction ID", - data: widget.ordinal.utxoTXID, - ), - const SizedBox( - height: _spacing, - ), - ], + useSpacers: false, + isCompactHeight: true, + ), + body: SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Column( + children: [ + Padding( + padding: const EdgeInsets.symmetric( + vertical: 12, + horizontal: 39, + ), + child: _OrdinalImageGroup( + ordinal: widget.ordinal, + walletId: widget.walletId, + ), ), - ), + _DetailsItemWCopy( + title: "Inscription number", + data: widget.ordinal.inscriptionNumber.toString(), + ), + const SizedBox( + height: _spacing, + ), + _DetailsItemWCopy( + title: "ID", + data: widget.ordinal.inscriptionId, + ), + const SizedBox( + height: _spacing, + ), + // todo: add utxo status + const SizedBox( + height: _spacing, + ), + const _DetailsItemWCopy( + title: "Amount", + data: "TODO", // TODO infer from utxo utxoTXID:utxoVOUT + ), + const SizedBox( + height: _spacing, + ), + const _DetailsItemWCopy( + title: "Owner address", + data: "TODO", // infer from address associated w utxoTXID + ), + const SizedBox( + height: _spacing, + ), + _DetailsItemWCopy( + title: "Transaction ID", + data: widget.ordinal.utxoTXID, + ), + const SizedBox( + height: _spacing, + ), + ], ), ), ), diff --git a/lib/route_generator.dart b/lib/route_generator.dart index 03c7f13bc..b3a104dd0 100644 --- a/lib/route_generator.dart +++ b/lib/route_generator.dart @@ -12,7 +12,6 @@ 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/dto/ordinals/inscription_data.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'; @@ -20,7 +19,7 @@ import 'package:stackwallet/models/exchange/incomplete_exchange.dart'; import 'package:stackwallet/models/exchange/response_objects/trade.dart'; import 'package:stackwallet/models/isar/models/contact_entry.dart'; import 'package:stackwallet/models/isar/models/isar_models.dart'; -import 'package:stackwallet/models/ordinal.dart'; // TODO generalize InscriptionData -> Ordinal +import 'package:stackwallet/models/ordinal.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'; @@ -458,7 +457,7 @@ class RouteGenerator { if (args is ({Ordinal ordinal, String walletId})) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => OrdinalDetailsView( + builder: (_) => DesktopOrdinalDetailsView( walletId: args.walletId, ordinal: args.ordinal, ), From fb39e96308acfd452119737cd7a3b6a42f895b51 Mon Sep 17 00:00:00 2001 From: ryleedavis Date: Fri, 21 Jul 2023 17:56:01 -0600 Subject: [PATCH 083/169] WIP: monkey view + fetching monkey dialog --- lib/pages/monkey/monkey_view.dart | 127 ++++++++++++++++ .../sub_widgets/fetch_monkey_dialog.dart | 135 ++++++++++++++++++ lib/pages/wallet_view/wallet_view.dart | 25 ++++ lib/route_generator.dart | 16 +++ 4 files changed, 303 insertions(+) create mode 100644 lib/pages/monkey/monkey_view.dart create mode 100644 lib/pages/monkey/sub_widgets/fetch_monkey_dialog.dart diff --git a/lib/pages/monkey/monkey_view.dart b/lib/pages/monkey/monkey_view.dart new file mode 100644 index 000000000..e74d5bbdf --- /dev/null +++ b/lib/pages/monkey/monkey_view.dart @@ -0,0 +1,127 @@ +import 'dart:io'; + +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_svg/svg.dart'; +import 'package:stackwallet/pages/monkey/sub_widgets/fetch_monkey_dialog.dart'; +import 'package:stackwallet/services/coins/manager.dart'; +import 'package:stackwallet/themes/coin_icon_provider.dart'; +import 'package:stackwallet/themes/stack_colors.dart'; +import 'package:stackwallet/utilities/enums/coin_enum.dart'; +import 'package:stackwallet/utilities/text_styles.dart'; +import 'package:stackwallet/widgets/background.dart'; +import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; +import 'package:stackwallet/widgets/desktop/primary_button.dart'; + +class MonkeyView extends ConsumerStatefulWidget { + const MonkeyView({ + Key? key, + required this.walletId, + required this.managerProvider, + }) : super(key: key); + + static const String routeName = "/monkey"; + static const double navBarHeight = 65.0; + + final String walletId; + final ChangeNotifierProvider managerProvider; + + @override + ConsumerState createState() => _MonkeyViewState(); +} + +class _MonkeyViewState extends ConsumerState { + late final String walletId; + late final ChangeNotifierProvider managerProvider; + + @override + void initState() { + walletId = widget.walletId; + managerProvider = widget.managerProvider; + + super.initState(); + } + + @override + void dispose() { + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final Coin coin = ref.watch(managerProvider.select((value) => value.coin)); + + return Background( + child: Stack( + children: [ + Scaffold( + appBar: AppBar( + leading: AppBarBackButton( + onPressed: () { + Navigator.of(context).pop(); + }, + ), + title: Text( + "MonKey", + style: STextStyles.navBarTitle(context), + ), + ), + body: Column( + // mainAxisAlignment: MainAxisAlignment.center, + // crossAxisAlignment: CrossAxisAlignment.center, + children: [ + const Spacer(), + Center( + child: Column( + children: [ + SvgPicture.file( + File( + ref.watch(coinIconProvider(coin)), + ), + width: 164, + height: 164, + ), + const SizedBox( + height: 40, + ), + Text( + "You do not have a MonKey yet. \nFetch yours now!", + style: STextStyles.smallMed14(context).copyWith( + color: Theme.of(context) + .extension()! + .textDark3, + ), + textAlign: TextAlign.center, + ), + ], + ), + ), + const Spacer(), + Padding( + padding: const EdgeInsets.all(16.0), + child: PrimaryButton( + label: "Fetch MonKey", + onPressed: () { + showDialog( + context: context, + useSafeArea: false, + barrierDismissible: false, + builder: (context) { + return FetchMonkeyDialog( + onCancel: () async { + Navigator.of(context).pop(); + }, + ); + }, + ); + }, + ), + ), + ], + ), + ), + ], + ), + ); + } +} diff --git a/lib/pages/monkey/sub_widgets/fetch_monkey_dialog.dart b/lib/pages/monkey/sub_widgets/fetch_monkey_dialog.dart new file mode 100644 index 000000000..94034fb78 --- /dev/null +++ b/lib/pages/monkey/sub_widgets/fetch_monkey_dialog.dart @@ -0,0 +1,135 @@ +/* + * This file is part of Stack Wallet. + * + * Copyright (c) 2023 Cypher Stack + * All Rights Reserved. + * The code is distributed under GPLv3 license, see LICENSE file for details. + * Generated by Cypher Stack on 2023-05-26 + * + */ + +import 'package:flutter/material.dart'; +import 'package:stackwallet/themes/stack_colors.dart'; +import 'package:stackwallet/utilities/text_styles.dart'; +import 'package:stackwallet/utilities/util.dart'; +import 'package:stackwallet/widgets/animated_widgets/rotating_arrows.dart'; +import 'package:stackwallet/widgets/desktop/desktop_dialog.dart'; +import 'package:stackwallet/widgets/desktop/desktop_dialog_close_button.dart'; +import 'package:stackwallet/widgets/desktop/secondary_button.dart'; +import 'package:stackwallet/widgets/stack_dialog.dart'; + +class FetchMonkeyDialog extends StatefulWidget { + const FetchMonkeyDialog({ + Key? key, + required this.onCancel, + }) : super(key: key); + + final Future Function() onCancel; + + @override + State createState() => _FetchMonkeyDialogState(); +} + +class _FetchMonkeyDialogState extends State { + late final Future Function() onCancel; + @override + void initState() { + onCancel = widget.onCancel; + + super.initState(); + } + + @override + Widget build(BuildContext context) { + if (Util.isDesktop) { + return DesktopDialog( + child: Column( + children: [ + DesktopDialogCloseButton( + onPressedOverride: () async { + await onCancel.call(); + if (mounted) { + Navigator.of(context).pop(); + } + }, + ), + const Spacer( + flex: 1, + ), + const RotatingArrows( + width: 40, + height: 40, + ), + const Spacer( + flex: 2, + ), + Text( + "Fetching MonKey", + style: STextStyles.desktopH2(context), + textAlign: TextAlign.center, + ), + const SizedBox( + height: 16, + ), + Text( + "We are fetching your MonKey", + style: STextStyles.desktopTextMedium(context).copyWith( + color: Theme.of(context).extension()!.textDark3, + ), + textAlign: TextAlign.center, + ), + const Spacer( + flex: 2, + ), + Padding( + padding: const EdgeInsets.only( + left: 32, + right: 32, + bottom: 32, + ), + child: SecondaryButton( + label: "Cancel", + width: 272.5, + onPressed: () async { + await onCancel.call(); + if (mounted) { + Navigator.of(context).pop(); + } + }, + ), + ), + ], + ), + ); + } else { + return WillPopScope( + onWillPop: () async { + return false; + }, + child: StackDialog( + title: "Fetching MonKey", + message: "We are fetching your MonKey", + icon: const RotatingArrows( + width: 24, + height: 24, + ), + rightButton: TextButton( + style: Theme.of(context) + .extension()! + .getSecondaryEnabledButtonStyle(context), + child: Text( + "Cancel", + style: STextStyles.itemSubtitle12(context), + ), + onPressed: () async { + await onCancel.call(); + if (mounted) { + Navigator.of(context).pop(); + } + }, + ), + ), + ); + } + } +} diff --git a/lib/pages/wallet_view/wallet_view.dart b/lib/pages/wallet_view/wallet_view.dart index 3e7fb37fa..bd1c10b3c 100644 --- a/lib/pages/wallet_view/wallet_view.dart +++ b/lib/pages/wallet_view/wallet_view.dart @@ -22,6 +22,7 @@ import 'package:stackwallet/pages/buy_view/buy_in_wallet_view.dart'; import 'package:stackwallet/pages/coin_control/coin_control_view.dart'; import 'package:stackwallet/pages/exchange_view/wallet_initiated_exchange_view.dart'; import 'package:stackwallet/pages/home_view/home_view.dart'; +import 'package:stackwallet/pages/monkey/monkey_view.dart'; import 'package:stackwallet/pages/notification_views/notifications_view.dart'; import 'package:stackwallet/pages/paynym/paynym_claim_view.dart'; import 'package:stackwallet/pages/paynym/paynym_home_view.dart'; @@ -924,6 +925,30 @@ class _WalletViewState extends ConsumerState { ); }, ), + if (ref.watch( + walletsChangeNotifierProvider.select( + (value) => value + .getManager(widget.walletId) + .hasCoinControlSupport, + ), + ) && + ref.watch( + prefsChangeNotifierProvider.select( + (value) => value.enableCoinControl, + ), + )) + WalletNavigationBarItemData( + icon: SvgPicture.asset(Assets.svg.circlePlus), + label: "MonKey", + onTap: () { + Navigator.of(context).pushNamed( + MonkeyView.routeName, + arguments: Tuple2( + widget.walletId, + widget.managerProvider, + ), + ); + }), if (ref.watch( walletsChangeNotifierProvider.select( (value) => value diff --git a/lib/route_generator.dart b/lib/route_generator.dart index 7ccc378c9..4513fd1d6 100644 --- a/lib/route_generator.dart +++ b/lib/route_generator.dart @@ -56,6 +56,7 @@ import 'package:stackwallet/pages/generic/single_field_edit_view.dart'; import 'package:stackwallet/pages/home_view/home_view.dart'; import 'package:stackwallet/pages/intro_view.dart'; import 'package:stackwallet/pages/manage_favorites_view/manage_favorites_view.dart'; +import 'package:stackwallet/pages/monkey/monkey_view.dart'; import 'package:stackwallet/pages/notification_views/notifications_view.dart'; import 'package:stackwallet/pages/paynym/add_new_paynym_follow_view.dart'; import 'package:stackwallet/pages/paynym/paynym_claim_view.dart'; @@ -375,6 +376,21 @@ class RouteGenerator { } return _routeError("${settings.name} invalid args: ${args.toString()}"); + case MonkeyView.routeName: + if (args is Tuple2>) { + return getRoute( + shouldUseMaterialRoute: useMaterialPageRoute, + builder: (_) => MonkeyView( + walletId: args.item1, + managerProvider: args.item2, + ), + settings: RouteSettings( + name: settings.name, + ), + ); + } + return _routeError("${settings.name} invalid args: ${args.toString()}"); + case CoinControlView.routeName: if (args is Tuple2) { return getRoute( From ee5a97c2fbccd9d7a116b0e84bad2611c9ed988e Mon Sep 17 00:00:00 2001 From: ryleedavis Date: Fri, 21 Jul 2023 17:59:01 -0600 Subject: [PATCH 084/169] add monkey icon --- assets/svg/monkey.svg | 3 +++ lib/pages/wallet_view/wallet_view.dart | 2 +- lib/utilities/assets.dart | 1 + pubspec.yaml | 1 + 4 files changed, 6 insertions(+), 1 deletion(-) create mode 100644 assets/svg/monkey.svg diff --git a/assets/svg/monkey.svg b/assets/svg/monkey.svg new file mode 100644 index 000000000..565ac4fdf --- /dev/null +++ b/assets/svg/monkey.svg @@ -0,0 +1,3 @@ + + + diff --git a/lib/pages/wallet_view/wallet_view.dart b/lib/pages/wallet_view/wallet_view.dart index bd1c10b3c..6af2eeb65 100644 --- a/lib/pages/wallet_view/wallet_view.dart +++ b/lib/pages/wallet_view/wallet_view.dart @@ -938,7 +938,7 @@ class _WalletViewState extends ConsumerState { ), )) WalletNavigationBarItemData( - icon: SvgPicture.asset(Assets.svg.circlePlus), + icon: SvgPicture.asset(Assets.svg.monkey), label: "MonKey", onTap: () { Navigator.of(context).pushNamed( diff --git a/lib/utilities/assets.dart b/lib/utilities/assets.dart index b8ec501e8..f22ab6341 100644 --- a/lib/utilities/assets.dart +++ b/lib/utilities/assets.dart @@ -92,6 +92,7 @@ class _SVG { final coinControl = const _COIN_CONTROL(); + String get monkey => "assets/svg/monkey.svg"; String get circleSliders => "assets/svg/configuration.svg"; String get circlePlus => "assets/svg/plus-circle.svg"; String get circlePlusFilled => "assets/svg/circle-plus-filled.svg"; diff --git a/pubspec.yaml b/pubspec.yaml index f5b4cf27b..9999e8e65 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -335,6 +335,7 @@ flutter: - assets/svg/trocador_rating_b.svg - assets/svg/trocador_rating_c.svg - assets/svg/trocador_rating_d.svg + - assets/svg/monkey.svg # coin control icons - assets/svg/coin_control/ From 8ac085fe240b001b3db939352ff8d8549319570d Mon Sep 17 00:00:00 2001 From: ryleedavis Date: Fri, 21 Jul 2023 18:01:38 -0600 Subject: [PATCH 085/169] fix monkey icon --- lib/pages/wallet_view/wallet_view.dart | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lib/pages/wallet_view/wallet_view.dart b/lib/pages/wallet_view/wallet_view.dart index 6af2eeb65..0dcfe18dc 100644 --- a/lib/pages/wallet_view/wallet_view.dart +++ b/lib/pages/wallet_view/wallet_view.dart @@ -938,7 +938,10 @@ class _WalletViewState extends ConsumerState { ), )) WalletNavigationBarItemData( - icon: SvgPicture.asset(Assets.svg.monkey), + icon: SvgPicture.asset(Assets.svg.monkey, + height: 20, + width: 20, + color: Theme.of(context).extension()!.bottomNavIconIcon,), label: "MonKey", onTap: () { Navigator.of(context).pushNamed( From c7d114e2683eaa1a0fd8315ca199ffd4c3a63481 Mon Sep 17 00:00:00 2001 From: sneurlax Date: Fri, 21 Jul 2023 19:11:33 -0500 Subject: [PATCH 086/169] remove nested AspectRatio --- lib/pages/ordinals/ordinal_details_view.dart | 23 ++-- .../desktop_ordinal_details_view.dart | 109 +++++++++--------- 2 files changed, 64 insertions(+), 68 deletions(-) diff --git a/lib/pages/ordinals/ordinal_details_view.dart b/lib/pages/ordinals/ordinal_details_view.dart index 95cff734e..6f6d0b8a7 100644 --- a/lib/pages/ordinals/ordinal_details_view.dart +++ b/lib/pages/ordinals/ordinal_details_view.dart @@ -3,7 +3,6 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_svg/flutter_svg.dart'; -import 'package:stackwallet/dto/ordinals/inscription_data.dart'; import 'package:stackwallet/models/ordinal.dart'; import 'package:stackwallet/notifications/show_flush_bar.dart'; import 'package:stackwallet/pages/ordinals/widgets/dialogs.dart'; @@ -41,10 +40,10 @@ class _OrdinalDetailsViewState extends State { child: SafeArea( child: Scaffold( backgroundColor: - Theme.of(context).extension()!.background, + Theme.of(context).extension()!.background, appBar: AppBar( backgroundColor: - Theme.of(context).extension()!.background, + Theme.of(context).extension()!.background, leading: const AppBarBackButton(), title: Text( "Ordinal details", @@ -154,7 +153,7 @@ class _DetailsItemWCopy extends StatelessWidget { child: SvgPicture.asset( Assets.svg.copy, color: - Theme.of(context).extension()!.infoItemIcons, + Theme.of(context).extension()!.infoItemIcons, width: 12, ), ), @@ -200,15 +199,13 @@ class _OrdinalImageGroup extends StatelessWidget { // ), AspectRatio( aspectRatio: 1, - child: AspectRatio( - aspectRatio: 1, - child: Container( - color: Colors.red, - child: Image.network( - ordinal.content, // Use the preview URL as the image source - fit: BoxFit.cover, - filterQuality: FilterQuality.none, // Set the filter mode to nearest - ), + child: Container( + color: Colors.red, + child: Image.network( + ordinal.content, // Use the preview URL as the image source + fit: BoxFit.cover, + filterQuality: + FilterQuality.none, // Set the filter mode to nearest ), ), ), diff --git a/lib/pages_desktop_specific/ordinals/desktop_ordinal_details_view.dart b/lib/pages_desktop_specific/ordinals/desktop_ordinal_details_view.dart index 03cae351e..1477a0d2f 100644 --- a/lib/pages_desktop_specific/ordinals/desktop_ordinal_details_view.dart +++ b/lib/pages_desktop_specific/ordinals/desktop_ordinal_details_view.dart @@ -29,7 +29,8 @@ class DesktopOrdinalDetailsView extends StatefulWidget { static const routeName = "/desktopOrdinalDetailsView"; @override - _DesktopOrdinalDetailsViewState createState() => _DesktopOrdinalDetailsViewState(); + _DesktopOrdinalDetailsViewState createState() => + _DesktopOrdinalDetailsViewState(); } class _DesktopOrdinalDetailsViewState extends State { @@ -38,55 +39,55 @@ class _DesktopOrdinalDetailsViewState extends State { @override Widget build(BuildContext context) { return DesktopScaffold( - appBar: DesktopAppBar( - background: Theme.of(context).extension()!.popupBG, - leading: Expanded( - child: Row( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - const SizedBox( - width: 32, - ), - AppBarIconButton( - size: 32, + appBar: DesktopAppBar( + background: Theme.of(context).extension()!.popupBG, + leading: Expanded( + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + const SizedBox( + width: 32, + ), + AppBarIconButton( + size: 32, + color: Theme.of(context) + .extension()! + .textFieldDefaultBG, + shadows: const [], + icon: SvgPicture.asset( + Assets.svg.arrowLeft, + width: 18, + height: 18, color: Theme.of(context) .extension()! - .textFieldDefaultBG, - shadows: const [], - icon: SvgPicture.asset( - Assets.svg.arrowLeft, - width: 18, - height: 18, - color: Theme.of(context) - .extension()! - .topNavIconPrimary, - ), - onPressed: Navigator.of(context).pop, + .topNavIconPrimary, ), - const SizedBox( - width: 18, - ), - SvgPicture.asset( - Assets.svg.ordinal, - width: 32, - height: 32, - color: - Theme.of(context).extension()!.textSubtitle1, - ), - const SizedBox( - width: 12, - ), - Text( - "Ordinals", - style: STextStyles.desktopH3(context), - ), - ], - ), + onPressed: Navigator.of(context).pop, + ), + const SizedBox( + width: 18, + ), + SvgPicture.asset( + Assets.svg.ordinal, + width: 32, + height: 32, + color: + Theme.of(context).extension()!.textSubtitle1, + ), + const SizedBox( + width: 12, + ), + Text( + "Ordinals", + style: STextStyles.desktopH3(context), + ), + ], ), - useSpacers: false, - isCompactHeight: true, ), - body: SingleChildScrollView( + useSpacers: false, + isCompactHeight: true, + ), + body: SingleChildScrollView( child: Padding( padding: const EdgeInsets.symmetric(horizontal: 16), child: Column( @@ -187,7 +188,7 @@ class _DetailsItemWCopy extends StatelessWidget { child: SvgPicture.asset( Assets.svg.copy, color: - Theme.of(context).extension()!.infoItemIcons, + Theme.of(context).extension()!.infoItemIcons, width: 12, ), ), @@ -233,15 +234,13 @@ class _OrdinalImageGroup extends StatelessWidget { // ), AspectRatio( aspectRatio: 1, - child: AspectRatio( - aspectRatio: 1, - child: Container( - color: Colors.red, - child: Image.network( - ordinal.content, // Use the preview URL as the image source - fit: BoxFit.cover, - filterQuality: FilterQuality.none, // Set the filter mode to nearest - ), + child: Container( + color: Colors.red, + child: Image.network( + ordinal.content, // Use the preview URL as the image source + fit: BoxFit.cover, + filterQuality: + FilterQuality.none, // Set the filter mode to nearest ), ), ), From b3e4d63a89b1d98fdbff751bdfe6c0df9bcb7b74 Mon Sep 17 00:00:00 2001 From: julian Date: Fri, 21 Jul 2023 17:30:27 -0600 Subject: [PATCH 087/169] strongly typed spacing --- lib/pages/ordinals/widgets/ordinals_list.dart | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/lib/pages/ordinals/widgets/ordinals_list.dart b/lib/pages/ordinals/widgets/ordinals_list.dart index 62d620130..c654f1522 100644 --- a/lib/pages/ordinals/widgets/ordinals_list.dart +++ b/lib/pages/ordinals/widgets/ordinals_list.dart @@ -1,6 +1,5 @@ import 'package:flutter/material.dart'; import 'package:stackwallet/models/ordinal.dart'; - import 'package:stackwallet/pages/ordinals/widgets/ordinal_card.dart'; class OrdinalsList extends StatelessWidget { @@ -13,7 +12,7 @@ class OrdinalsList extends StatelessWidget { final String walletId; final Future> ordinalsFuture; - get spacing => 2.0; + double get spacing => 2.0; @override Widget build(BuildContext context) { @@ -30,8 +29,8 @@ class OrdinalsList extends StatelessWidget { shrinkWrap: true, itemCount: ordinals.length, gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( - crossAxisSpacing: spacing as double, - mainAxisSpacing: spacing as double, + crossAxisSpacing: spacing, + mainAxisSpacing: spacing, crossAxisCount: 2, childAspectRatio: 6 / 7, // was 3/4, less data displayed now ), @@ -46,4 +45,4 @@ class OrdinalsList extends StatelessWidget { }, ); } -} \ No newline at end of file +} From 974924de8df5ca2a46192dd2cca67f4684ad0d1f Mon Sep 17 00:00:00 2001 From: julian Date: Fri, 21 Jul 2023 17:50:21 -0600 Subject: [PATCH 088/169] updated mocks --- test/pages/send_view/send_view_test.mocks.dart | 5 +++++ .../subviews/add_address_book_view_screen_test.mocks.dart | 5 +++++ .../address_book_entry_details_view_screen_test.mocks.dart | 5 +++++ .../edit_address_book_entry_view_screen_test.mocks.dart | 5 +++++ test/screen_tests/lockscreen_view_screen_test.mocks.dart | 5 +++++ .../main_view_tests/main_view_screen_testA_test.mocks.dart | 5 +++++ .../main_view_tests/main_view_screen_testB_test.mocks.dart | 5 +++++ .../main_view_tests/main_view_screen_testC_test.mocks.dart | 5 +++++ .../onboarding/backup_key_view_screen_test.mocks.dart | 5 +++++ .../backup_key_warning_view_screen_test.mocks.dart | 5 +++++ .../onboarding/create_pin_view_screen_test.mocks.dart | 5 +++++ .../onboarding/restore_wallet_view_screen_test.mocks.dart | 5 +++++ .../onboarding/verify_backup_key_view_screen_test.mocks.dart | 5 +++++ .../settings_subviews/currency_view_screen_test.mocks.dart | 5 +++++ .../add_custom_node_view_screen_test.mocks.dart | 5 +++++ .../node_details_view_screen_test.mocks.dart | 5 +++++ .../wallet_backup_view_screen_test.mocks.dart | 5 +++++ .../rescan_warning_view_screen_test.mocks.dart | 5 +++++ .../wallet_delete_mnemonic_view_screen_test.mocks.dart | 5 +++++ .../wallet_settings_view_screen_test.mocks.dart | 5 +++++ .../settings_view/settings_view_screen_test.mocks.dart | 5 +++++ .../transaction_search_results_view_screen_test.mocks.dart | 5 +++++ .../wallet_view/confirm_send_view_screen_test.mocks.dart | 5 +++++ .../wallet_view/receive_view_screen_test.mocks.dart | 5 +++++ .../wallet_view/send_view_screen_test.mocks.dart | 5 +++++ .../wallet_view/wallet_view_screen_test.mocks.dart | 5 +++++ test/widget_tests/managed_favorite_test.mocks.dart | 5 +++++ test/widget_tests/table_view/table_view_row_test.mocks.dart | 5 +++++ test/widget_tests/transaction_card_test.mocks.dart | 5 +++++ .../wallet_info_row_balance_future_test.mocks.dart | 5 +++++ .../wallet_info_row/wallet_info_row_test.mocks.dart | 5 +++++ 31 files changed, 155 insertions(+) diff --git a/test/pages/send_view/send_view_test.mocks.dart b/test/pages/send_view/send_view_test.mocks.dart index 71401e8cd..6ad56e711 100644 --- a/test/pages/send_view/send_view_test.mocks.dart +++ b/test/pages/send_view/send_view_test.mocks.dart @@ -2958,6 +2958,11 @@ class MockManager extends _i1.Mock implements _i6.Manager { returnValue: false, ) as bool); @override + bool get hasOrdinalsSupport => (super.noSuchMethod( + Invocation.getter(#hasOrdinalsSupport), + returnValue: false, + ) as bool); + @override bool get hasTokenSupport => (super.noSuchMethod( Invocation.getter(#hasTokenSupport), returnValue: false, 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 a5f53d82b..b389043c0 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 @@ -388,6 +388,11 @@ class MockManager extends _i1.Mock implements _i12.Manager { returnValue: false, ) as bool); @override + bool get hasOrdinalsSupport => (super.noSuchMethod( + Invocation.getter(#hasOrdinalsSupport), + returnValue: false, + ) as bool); + @override bool get hasTokenSupport => (super.noSuchMethod( Invocation.getter(#hasTokenSupport), returnValue: false, 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 630726884..87f97997e 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 @@ -349,6 +349,11 @@ class MockManager extends _i1.Mock implements _i10.Manager { returnValue: false, ) as bool); @override + bool get hasOrdinalsSupport => (super.noSuchMethod( + Invocation.getter(#hasOrdinalsSupport), + returnValue: false, + ) as bool); + @override bool get hasTokenSupport => (super.noSuchMethod( Invocation.getter(#hasTokenSupport), returnValue: false, 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 c64135241..72ac2f1a7 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 @@ -347,6 +347,11 @@ class MockManager extends _i1.Mock implements _i10.Manager { returnValue: false, ) as bool); @override + bool get hasOrdinalsSupport => (super.noSuchMethod( + Invocation.getter(#hasOrdinalsSupport), + returnValue: false, + ) as bool); + @override bool get hasTokenSupport => (super.noSuchMethod( Invocation.getter(#hasTokenSupport), returnValue: false, diff --git a/test/screen_tests/lockscreen_view_screen_test.mocks.dart b/test/screen_tests/lockscreen_view_screen_test.mocks.dart index 3f7609c1f..b028c6819 100644 --- a/test/screen_tests/lockscreen_view_screen_test.mocks.dart +++ b/test/screen_tests/lockscreen_view_screen_test.mocks.dart @@ -667,6 +667,11 @@ class MockManager extends _i1.Mock implements _i13.Manager { returnValue: false, ) as bool); @override + bool get hasOrdinalsSupport => (super.noSuchMethod( + Invocation.getter(#hasOrdinalsSupport), + returnValue: false, + ) as bool); + @override bool get hasTokenSupport => (super.noSuchMethod( Invocation.getter(#hasTokenSupport), returnValue: false, 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 909d04a86..d0f5f89a4 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 @@ -454,6 +454,11 @@ class MockManager extends _i1.Mock implements _i10.Manager { returnValue: false, ) as bool); @override + bool get hasOrdinalsSupport => (super.noSuchMethod( + Invocation.getter(#hasOrdinalsSupport), + returnValue: false, + ) as bool); + @override bool get hasTokenSupport => (super.noSuchMethod( Invocation.getter(#hasTokenSupport), returnValue: false, 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 76ca1a64a..fa528b669 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 @@ -454,6 +454,11 @@ class MockManager extends _i1.Mock implements _i10.Manager { returnValue: false, ) as bool); @override + bool get hasOrdinalsSupport => (super.noSuchMethod( + Invocation.getter(#hasOrdinalsSupport), + returnValue: false, + ) as bool); + @override bool get hasTokenSupport => (super.noSuchMethod( Invocation.getter(#hasTokenSupport), returnValue: false, 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 416090add..be30eacbc 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 @@ -454,6 +454,11 @@ class MockManager extends _i1.Mock implements _i10.Manager { returnValue: false, ) as bool); @override + bool get hasOrdinalsSupport => (super.noSuchMethod( + Invocation.getter(#hasOrdinalsSupport), + returnValue: false, + ) as bool); + @override bool get hasTokenSupport => (super.noSuchMethod( Invocation.getter(#hasTokenSupport), returnValue: false, 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 7022de852..34f177901 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 @@ -221,6 +221,11 @@ class MockManager extends _i1.Mock implements _i6.Manager { returnValue: false, ) as bool); @override + bool get hasOrdinalsSupport => (super.noSuchMethod( + Invocation.getter(#hasOrdinalsSupport), + returnValue: false, + ) as bool); + @override bool get hasTokenSupport => (super.noSuchMethod( Invocation.getter(#hasTokenSupport), returnValue: false, 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 4fad26d9f..9e5723ef5 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 @@ -452,6 +452,11 @@ class MockManager extends _i1.Mock implements _i10.Manager { returnValue: false, ) as bool); @override + bool get hasOrdinalsSupport => (super.noSuchMethod( + Invocation.getter(#hasOrdinalsSupport), + returnValue: false, + ) as bool); + @override bool get hasTokenSupport => (super.noSuchMethod( Invocation.getter(#hasTokenSupport), returnValue: false, 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 c04081c52..bb9e1c91b 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 @@ -667,6 +667,11 @@ class MockManager extends _i1.Mock implements _i13.Manager { returnValue: false, ) as bool); @override + bool get hasOrdinalsSupport => (super.noSuchMethod( + Invocation.getter(#hasOrdinalsSupport), + returnValue: false, + ) as bool); + @override bool get hasTokenSupport => (super.noSuchMethod( Invocation.getter(#hasTokenSupport), returnValue: false, 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 9a351b683..bd475e066 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 @@ -508,6 +508,11 @@ class MockManager extends _i1.Mock implements _i13.Manager { returnValue: false, ) as bool); @override + bool get hasOrdinalsSupport => (super.noSuchMethod( + Invocation.getter(#hasOrdinalsSupport), + returnValue: false, + ) as bool); + @override bool get hasTokenSupport => (super.noSuchMethod( Invocation.getter(#hasTokenSupport), returnValue: false, 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 496739e8e..a02c62b4d 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 @@ -221,6 +221,11 @@ class MockManager extends _i1.Mock implements _i6.Manager { returnValue: false, ) as bool); @override + bool get hasOrdinalsSupport => (super.noSuchMethod( + Invocation.getter(#hasOrdinalsSupport), + returnValue: false, + ) as bool); + @override bool get hasTokenSupport => (super.noSuchMethod( Invocation.getter(#hasTokenSupport), returnValue: false, 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 5b81467df..fbf9c2ed5 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 @@ -221,6 +221,11 @@ class MockManager extends _i1.Mock implements _i6.Manager { returnValue: false, ) as bool); @override + bool get hasOrdinalsSupport => (super.noSuchMethod( + Invocation.getter(#hasOrdinalsSupport), + returnValue: false, + ) as bool); + @override bool get hasTokenSupport => (super.noSuchMethod( Invocation.getter(#hasTokenSupport), returnValue: false, 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 1c5de9829..5922e7f9b 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 @@ -436,6 +436,11 @@ class MockManager extends _i1.Mock implements _i12.Manager { returnValue: false, ) as bool); @override + bool get hasOrdinalsSupport => (super.noSuchMethod( + Invocation.getter(#hasOrdinalsSupport), + returnValue: false, + ) as bool); + @override bool get hasTokenSupport => (super.noSuchMethod( Invocation.getter(#hasTokenSupport), returnValue: false, 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 760b143bd..133ca38a5 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 @@ -436,6 +436,11 @@ class MockManager extends _i1.Mock implements _i12.Manager { returnValue: false, ) as bool); @override + bool get hasOrdinalsSupport => (super.noSuchMethod( + Invocation.getter(#hasOrdinalsSupport), + returnValue: false, + ) as bool); + @override bool get hasTokenSupport => (super.noSuchMethod( Invocation.getter(#hasTokenSupport), returnValue: false, 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 191e1eca7..99b8dc33f 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 @@ -221,6 +221,11 @@ class MockManager extends _i1.Mock implements _i6.Manager { returnValue: false, ) as bool); @override + bool get hasOrdinalsSupport => (super.noSuchMethod( + Invocation.getter(#hasOrdinalsSupport), + returnValue: false, + ) as bool); + @override bool get hasTokenSupport => (super.noSuchMethod( Invocation.getter(#hasTokenSupport), returnValue: false, 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 df6bac383..c216d11b3 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 @@ -221,6 +221,11 @@ class MockManager extends _i1.Mock implements _i6.Manager { returnValue: false, ) as bool); @override + bool get hasOrdinalsSupport => (super.noSuchMethod( + Invocation.getter(#hasOrdinalsSupport), + returnValue: false, + ) as bool); + @override bool get hasTokenSupport => (super.noSuchMethod( Invocation.getter(#hasTokenSupport), returnValue: false, 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 2ae50c123..f12df9272 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 @@ -452,6 +452,11 @@ class MockManager extends _i1.Mock implements _i10.Manager { returnValue: false, ) as bool); @override + bool get hasOrdinalsSupport => (super.noSuchMethod( + Invocation.getter(#hasOrdinalsSupport), + returnValue: false, + ) as bool); + @override bool get hasTokenSupport => (super.noSuchMethod( Invocation.getter(#hasTokenSupport), returnValue: false, 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 3548ce5a4..6f60f60bc 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 @@ -688,6 +688,11 @@ class MockManager extends _i1.Mock implements _i15.Manager { returnValue: false, ) as bool); @override + bool get hasOrdinalsSupport => (super.noSuchMethod( + Invocation.getter(#hasOrdinalsSupport), + returnValue: false, + ) as bool); + @override bool get hasTokenSupport => (super.noSuchMethod( Invocation.getter(#hasTokenSupport), returnValue: false, 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 daee1f95e..6b20221c3 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 @@ -452,6 +452,11 @@ class MockManager extends _i1.Mock implements _i10.Manager { returnValue: false, ) as bool); @override + bool get hasOrdinalsSupport => (super.noSuchMethod( + Invocation.getter(#hasOrdinalsSupport), + returnValue: false, + ) as bool); + @override bool get hasTokenSupport => (super.noSuchMethod( Invocation.getter(#hasTokenSupport), returnValue: false, 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 7914d71f4..cffb87869 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 @@ -223,6 +223,11 @@ class MockManager extends _i1.Mock implements _i6.Manager { returnValue: false, ) as bool); @override + bool get hasOrdinalsSupport => (super.noSuchMethod( + Invocation.getter(#hasOrdinalsSupport), + returnValue: false, + ) as bool); + @override bool get hasTokenSupport => (super.noSuchMethod( Invocation.getter(#hasTokenSupport), returnValue: false, 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 f493222d9..01c78d796 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 @@ -222,6 +222,11 @@ class MockManager extends _i1.Mock implements _i6.Manager { returnValue: false, ) as bool); @override + bool get hasOrdinalsSupport => (super.noSuchMethod( + Invocation.getter(#hasOrdinalsSupport), + returnValue: false, + ) as bool); + @override bool get hasTokenSupport => (super.noSuchMethod( Invocation.getter(#hasTokenSupport), returnValue: false, 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 6e0382474..b5999d56c 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 @@ -221,6 +221,11 @@ class MockManager extends _i1.Mock implements _i6.Manager { returnValue: false, ) as bool); @override + bool get hasOrdinalsSupport => (super.noSuchMethod( + Invocation.getter(#hasOrdinalsSupport), + returnValue: false, + ) as bool); + @override bool get hasTokenSupport => (super.noSuchMethod( Invocation.getter(#hasTokenSupport), returnValue: false, 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 97f744995..964343a65 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 @@ -263,6 +263,11 @@ class MockManager extends _i1.Mock implements _i9.Manager { returnValue: false, ) as bool); @override + bool get hasOrdinalsSupport => (super.noSuchMethod( + Invocation.getter(#hasOrdinalsSupport), + returnValue: false, + ) as bool); + @override bool get hasTokenSupport => (super.noSuchMethod( Invocation.getter(#hasTokenSupport), returnValue: false, 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 4a0a14f47..e4b7791c6 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 @@ -223,6 +223,11 @@ class MockManager extends _i1.Mock implements _i6.Manager { returnValue: false, ) as bool); @override + bool get hasOrdinalsSupport => (super.noSuchMethod( + Invocation.getter(#hasOrdinalsSupport), + returnValue: false, + ) as bool); + @override bool get hasTokenSupport => (super.noSuchMethod( Invocation.getter(#hasTokenSupport), returnValue: false, diff --git a/test/widget_tests/managed_favorite_test.mocks.dart b/test/widget_tests/managed_favorite_test.mocks.dart index ee7b22e84..723a76d30 100644 --- a/test/widget_tests/managed_favorite_test.mocks.dart +++ b/test/widget_tests/managed_favorite_test.mocks.dart @@ -2952,6 +2952,11 @@ class MockManager extends _i1.Mock implements _i6.Manager { returnValue: false, ) as bool); @override + bool get hasOrdinalsSupport => (super.noSuchMethod( + Invocation.getter(#hasOrdinalsSupport), + returnValue: false, + ) as bool); + @override bool get hasTokenSupport => (super.noSuchMethod( Invocation.getter(#hasTokenSupport), returnValue: false, 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 cce7fd163..cfae6ecb9 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 @@ -2198,6 +2198,11 @@ class MockManager extends _i1.Mock implements _i6.Manager { returnValue: false, ) as bool); @override + bool get hasOrdinalsSupport => (super.noSuchMethod( + Invocation.getter(#hasOrdinalsSupport), + returnValue: false, + ) as bool); + @override bool get hasTokenSupport => (super.noSuchMethod( Invocation.getter(#hasTokenSupport), returnValue: false, diff --git a/test/widget_tests/transaction_card_test.mocks.dart b/test/widget_tests/transaction_card_test.mocks.dart index 4fc6f8832..83c8e8c82 100644 --- a/test/widget_tests/transaction_card_test.mocks.dart +++ b/test/widget_tests/transaction_card_test.mocks.dart @@ -585,6 +585,11 @@ class MockManager extends _i1.Mock implements _i6.Manager { returnValue: false, ) as bool); @override + bool get hasOrdinalsSupport => (super.noSuchMethod( + Invocation.getter(#hasOrdinalsSupport), + returnValue: false, + ) as bool); + @override bool get hasTokenSupport => (super.noSuchMethod( Invocation.getter(#hasTokenSupport), returnValue: false, 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 78483fb45..56c5d2485 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 @@ -2307,6 +2307,11 @@ class MockManager extends _i1.Mock implements _i6.Manager { returnValue: false, ) as bool); @override + bool get hasOrdinalsSupport => (super.noSuchMethod( + Invocation.getter(#hasOrdinalsSupport), + returnValue: false, + ) as bool); + @override bool get hasTokenSupport => (super.noSuchMethod( Invocation.getter(#hasTokenSupport), returnValue: false, 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 f0a2750fd..a234b0e9c 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 @@ -2410,6 +2410,11 @@ class MockManager extends _i1.Mock implements _i6.Manager { returnValue: false, ) as bool); @override + bool get hasOrdinalsSupport => (super.noSuchMethod( + Invocation.getter(#hasOrdinalsSupport), + returnValue: false, + ) as bool); + @override bool get hasTokenSupport => (super.noSuchMethod( Invocation.getter(#hasTokenSupport), returnValue: false, From 48109f3c49cab97568332ef1bb25c009e4453199 Mon Sep 17 00:00:00 2001 From: julian Date: Fri, 21 Jul 2023 17:51:04 -0600 Subject: [PATCH 089/169] use isar ordinal model --- lib/models/isar/ordinal.dart | 76 + lib/models/isar/ordinal.g.dart | 1489 +++++++++++++++++ lib/models/ordinal.dart | 29 - lib/pages/ordinals/widgets/ordinals_list.dart | 3 +- lib/route_generator.dart | 2 +- lib/services/mixins/ordinals_interface.dart | 31 +- 6 files changed, 1588 insertions(+), 42 deletions(-) create mode 100644 lib/models/isar/ordinal.dart create mode 100644 lib/models/isar/ordinal.g.dart delete mode 100644 lib/models/ordinal.dart diff --git a/lib/models/isar/ordinal.dart b/lib/models/isar/ordinal.dart new file mode 100644 index 000000000..0af1d1120 --- /dev/null +++ b/lib/models/isar/ordinal.dart @@ -0,0 +1,76 @@ +import 'package:isar/isar.dart'; +import 'package:stackwallet/dto/ordinals/inscription_data.dart'; + +part 'ordinal.g.dart'; + +@collection +class Ordinal { + Id id = Isar.autoIncrement; + + final String walletId; + + @Index(unique: true, replace: true, composite: [ + CompositeIndex("utxoTXID"), + CompositeIndex("utxoVOUT"), + ]) + final String inscriptionId; + + final int inscriptionNumber; + + final String content; + + // following two are used to look up the UTXO object in isar combined w/ walletId + final String utxoTXID; + final int utxoVOUT; + + Ordinal({ + required this.walletId, + required this.inscriptionId, + required this.inscriptionNumber, + required this.content, + required this.utxoTXID, + required this.utxoVOUT, + }); + + factory Ordinal.fromInscriptionData(InscriptionData data, String walletId) { + return Ordinal( + walletId: walletId, + inscriptionId: data.inscriptionId, + inscriptionNumber: data.inscriptionNumber, + content: data.content, + utxoTXID: data.output.split(':')[ + 0], // "output": "062f32e21aa04246b8873b5d9a929576addd0339881e1ea478b406795d6b6c47:0" + utxoVOUT: int.parse(data.output.split(':')[1]), + ); + } + + Ordinal copyWith({ + String? walletId, + String? inscriptionId, + int? inscriptionNumber, + String? content, + String? utxoTXID, + int? utxoVOUT, + }) { + return Ordinal( + walletId: walletId ?? this.walletId, + inscriptionId: inscriptionId ?? this.inscriptionId, + inscriptionNumber: inscriptionNumber ?? this.inscriptionNumber, + content: content ?? this.content, + utxoTXID: utxoTXID ?? this.utxoTXID, + utxoVOUT: utxoVOUT ?? this.utxoVOUT, + ); + } + + @override + String toString() { + return 'Ordinal {' + ' walletId: $walletId,' + ' inscriptionId: $inscriptionId,' + ' inscriptionNumber: $inscriptionNumber,' + ' content: $content,' + ' utxoTXID: $utxoTXID,' + ' utxoVOUT: $utxoVOUT' + ' }'; + } +} diff --git a/lib/models/isar/ordinal.g.dart b/lib/models/isar/ordinal.g.dart new file mode 100644 index 000000000..89c967cb0 --- /dev/null +++ b/lib/models/isar/ordinal.g.dart @@ -0,0 +1,1489 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'ordinal.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 GetOrdinalCollection on Isar { + IsarCollection get ordinals => this.collection(); +} + +const OrdinalSchema = CollectionSchema( + name: r'Ordinal', + id: -7772149326141951436, + properties: { + r'content': PropertySchema( + id: 0, + name: r'content', + type: IsarType.string, + ), + r'inscriptionId': PropertySchema( + id: 1, + name: r'inscriptionId', + type: IsarType.string, + ), + r'inscriptionNumber': PropertySchema( + id: 2, + name: r'inscriptionNumber', + type: IsarType.long, + ), + r'utxoTXID': PropertySchema( + id: 3, + name: r'utxoTXID', + type: IsarType.string, + ), + r'utxoVOUT': PropertySchema( + id: 4, + name: r'utxoVOUT', + type: IsarType.long, + ), + r'walletId': PropertySchema( + id: 5, + name: r'walletId', + type: IsarType.string, + ) + }, + estimateSize: _ordinalEstimateSize, + serialize: _ordinalSerialize, + deserialize: _ordinalDeserialize, + deserializeProp: _ordinalDeserializeProp, + idName: r'id', + indexes: { + r'inscriptionId_utxoTXID_utxoVOUT': IndexSchema( + id: 2138008085066605381, + name: r'inscriptionId_utxoTXID_utxoVOUT', + unique: true, + replace: true, + properties: [ + IndexPropertySchema( + name: r'inscriptionId', + type: IndexType.hash, + caseSensitive: true, + ), + IndexPropertySchema( + name: r'utxoTXID', + type: IndexType.hash, + caseSensitive: true, + ), + IndexPropertySchema( + name: r'utxoVOUT', + type: IndexType.value, + caseSensitive: false, + ) + ], + ) + }, + links: {}, + embeddedSchemas: {}, + getId: _ordinalGetId, + getLinks: _ordinalGetLinks, + attach: _ordinalAttach, + version: '3.0.5', +); + +int _ordinalEstimateSize( + Ordinal object, + List offsets, + Map> allOffsets, +) { + var bytesCount = offsets.last; + bytesCount += 3 + object.content.length * 3; + bytesCount += 3 + object.inscriptionId.length * 3; + bytesCount += 3 + object.utxoTXID.length * 3; + bytesCount += 3 + object.walletId.length * 3; + return bytesCount; +} + +void _ordinalSerialize( + Ordinal object, + IsarWriter writer, + List offsets, + Map> allOffsets, +) { + writer.writeString(offsets[0], object.content); + writer.writeString(offsets[1], object.inscriptionId); + writer.writeLong(offsets[2], object.inscriptionNumber); + writer.writeString(offsets[3], object.utxoTXID); + writer.writeLong(offsets[4], object.utxoVOUT); + writer.writeString(offsets[5], object.walletId); +} + +Ordinal _ordinalDeserialize( + Id id, + IsarReader reader, + List offsets, + Map> allOffsets, +) { + final object = Ordinal( + content: reader.readString(offsets[0]), + inscriptionId: reader.readString(offsets[1]), + inscriptionNumber: reader.readLong(offsets[2]), + utxoTXID: reader.readString(offsets[3]), + utxoVOUT: reader.readLong(offsets[4]), + walletId: reader.readString(offsets[5]), + ); + object.id = id; + return object; +} + +P _ordinalDeserializeProp

( + IsarReader reader, + int propertyId, + int offset, + Map> allOffsets, +) { + switch (propertyId) { + case 0: + return (reader.readString(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.readLong(offset)) as P; + case 5: + return (reader.readString(offset)) as P; + default: + throw IsarError('Unknown property with id $propertyId'); + } +} + +Id _ordinalGetId(Ordinal object) { + return object.id; +} + +List> _ordinalGetLinks(Ordinal object) { + return []; +} + +void _ordinalAttach(IsarCollection col, Id id, Ordinal object) { + object.id = id; +} + +extension OrdinalByIndex on IsarCollection { + Future getByInscriptionIdUtxoTXIDUtxoVOUT( + String inscriptionId, String utxoTXID, int utxoVOUT) { + return getByIndex(r'inscriptionId_utxoTXID_utxoVOUT', + [inscriptionId, utxoTXID, utxoVOUT]); + } + + Ordinal? getByInscriptionIdUtxoTXIDUtxoVOUTSync( + String inscriptionId, String utxoTXID, int utxoVOUT) { + return getByIndexSync(r'inscriptionId_utxoTXID_utxoVOUT', + [inscriptionId, utxoTXID, utxoVOUT]); + } + + Future deleteByInscriptionIdUtxoTXIDUtxoVOUT( + String inscriptionId, String utxoTXID, int utxoVOUT) { + return deleteByIndex(r'inscriptionId_utxoTXID_utxoVOUT', + [inscriptionId, utxoTXID, utxoVOUT]); + } + + bool deleteByInscriptionIdUtxoTXIDUtxoVOUTSync( + String inscriptionId, String utxoTXID, int utxoVOUT) { + return deleteByIndexSync(r'inscriptionId_utxoTXID_utxoVOUT', + [inscriptionId, utxoTXID, utxoVOUT]); + } + + Future> getAllByInscriptionIdUtxoTXIDUtxoVOUT( + List inscriptionIdValues, + List utxoTXIDValues, + List utxoVOUTValues) { + final len = inscriptionIdValues.length; + assert(utxoTXIDValues.length == len && utxoVOUTValues.length == len, + 'All index values must have the same length'); + final values = >[]; + for (var i = 0; i < len; i++) { + values + .add([inscriptionIdValues[i], utxoTXIDValues[i], utxoVOUTValues[i]]); + } + + return getAllByIndex(r'inscriptionId_utxoTXID_utxoVOUT', values); + } + + List getAllByInscriptionIdUtxoTXIDUtxoVOUTSync( + List inscriptionIdValues, + List utxoTXIDValues, + List utxoVOUTValues) { + final len = inscriptionIdValues.length; + assert(utxoTXIDValues.length == len && utxoVOUTValues.length == len, + 'All index values must have the same length'); + final values = >[]; + for (var i = 0; i < len; i++) { + values + .add([inscriptionIdValues[i], utxoTXIDValues[i], utxoVOUTValues[i]]); + } + + return getAllByIndexSync(r'inscriptionId_utxoTXID_utxoVOUT', values); + } + + Future deleteAllByInscriptionIdUtxoTXIDUtxoVOUT( + List inscriptionIdValues, + List utxoTXIDValues, + List utxoVOUTValues) { + final len = inscriptionIdValues.length; + assert(utxoTXIDValues.length == len && utxoVOUTValues.length == len, + 'All index values must have the same length'); + final values = >[]; + for (var i = 0; i < len; i++) { + values + .add([inscriptionIdValues[i], utxoTXIDValues[i], utxoVOUTValues[i]]); + } + + return deleteAllByIndex(r'inscriptionId_utxoTXID_utxoVOUT', values); + } + + int deleteAllByInscriptionIdUtxoTXIDUtxoVOUTSync( + List inscriptionIdValues, + List utxoTXIDValues, + List utxoVOUTValues) { + final len = inscriptionIdValues.length; + assert(utxoTXIDValues.length == len && utxoVOUTValues.length == len, + 'All index values must have the same length'); + final values = >[]; + for (var i = 0; i < len; i++) { + values + .add([inscriptionIdValues[i], utxoTXIDValues[i], utxoVOUTValues[i]]); + } + + return deleteAllByIndexSync(r'inscriptionId_utxoTXID_utxoVOUT', values); + } + + Future putByInscriptionIdUtxoTXIDUtxoVOUT(Ordinal object) { + return putByIndex(r'inscriptionId_utxoTXID_utxoVOUT', object); + } + + Id putByInscriptionIdUtxoTXIDUtxoVOUTSync(Ordinal object, + {bool saveLinks = true}) { + return putByIndexSync(r'inscriptionId_utxoTXID_utxoVOUT', object, + saveLinks: saveLinks); + } + + Future> putAllByInscriptionIdUtxoTXIDUtxoVOUT( + List objects) { + return putAllByIndex(r'inscriptionId_utxoTXID_utxoVOUT', objects); + } + + List putAllByInscriptionIdUtxoTXIDUtxoVOUTSync(List objects, + {bool saveLinks = true}) { + return putAllByIndexSync(r'inscriptionId_utxoTXID_utxoVOUT', objects, + saveLinks: saveLinks); + } +} + +extension OrdinalQueryWhereSort on QueryBuilder { + QueryBuilder anyId() { + return QueryBuilder.apply(this, (query) { + return query.addWhereClause(const IdWhereClause.any()); + }); + } +} + +extension OrdinalQueryWhere 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 + inscriptionIdEqualToAnyUtxoTXIDUtxoVOUT(String inscriptionId) { + return QueryBuilder.apply(this, (query) { + return query.addWhereClause(IndexWhereClause.equalTo( + indexName: r'inscriptionId_utxoTXID_utxoVOUT', + value: [inscriptionId], + )); + }); + } + + QueryBuilder + inscriptionIdNotEqualToAnyUtxoTXIDUtxoVOUT(String inscriptionId) { + return QueryBuilder.apply(this, (query) { + if (query.whereSort == Sort.asc) { + return query + .addWhereClause(IndexWhereClause.between( + indexName: r'inscriptionId_utxoTXID_utxoVOUT', + lower: [], + upper: [inscriptionId], + includeUpper: false, + )) + .addWhereClause(IndexWhereClause.between( + indexName: r'inscriptionId_utxoTXID_utxoVOUT', + lower: [inscriptionId], + includeLower: false, + upper: [], + )); + } else { + return query + .addWhereClause(IndexWhereClause.between( + indexName: r'inscriptionId_utxoTXID_utxoVOUT', + lower: [inscriptionId], + includeLower: false, + upper: [], + )) + .addWhereClause(IndexWhereClause.between( + indexName: r'inscriptionId_utxoTXID_utxoVOUT', + lower: [], + upper: [inscriptionId], + includeUpper: false, + )); + } + }); + } + + QueryBuilder + inscriptionIdUtxoTXIDEqualToAnyUtxoVOUT( + String inscriptionId, String utxoTXID) { + return QueryBuilder.apply(this, (query) { + return query.addWhereClause(IndexWhereClause.equalTo( + indexName: r'inscriptionId_utxoTXID_utxoVOUT', + value: [inscriptionId, utxoTXID], + )); + }); + } + + QueryBuilder + inscriptionIdEqualToUtxoTXIDNotEqualToAnyUtxoVOUT( + String inscriptionId, String utxoTXID) { + return QueryBuilder.apply(this, (query) { + if (query.whereSort == Sort.asc) { + return query + .addWhereClause(IndexWhereClause.between( + indexName: r'inscriptionId_utxoTXID_utxoVOUT', + lower: [inscriptionId], + upper: [inscriptionId, utxoTXID], + includeUpper: false, + )) + .addWhereClause(IndexWhereClause.between( + indexName: r'inscriptionId_utxoTXID_utxoVOUT', + lower: [inscriptionId, utxoTXID], + includeLower: false, + upper: [inscriptionId], + )); + } else { + return query + .addWhereClause(IndexWhereClause.between( + indexName: r'inscriptionId_utxoTXID_utxoVOUT', + lower: [inscriptionId, utxoTXID], + includeLower: false, + upper: [inscriptionId], + )) + .addWhereClause(IndexWhereClause.between( + indexName: r'inscriptionId_utxoTXID_utxoVOUT', + lower: [inscriptionId], + upper: [inscriptionId, utxoTXID], + includeUpper: false, + )); + } + }); + } + + QueryBuilder + inscriptionIdUtxoTXIDUtxoVOUTEqualTo( + String inscriptionId, String utxoTXID, int utxoVOUT) { + return QueryBuilder.apply(this, (query) { + return query.addWhereClause(IndexWhereClause.equalTo( + indexName: r'inscriptionId_utxoTXID_utxoVOUT', + value: [inscriptionId, utxoTXID, utxoVOUT], + )); + }); + } + + QueryBuilder + inscriptionIdUtxoTXIDEqualToUtxoVOUTNotEqualTo( + String inscriptionId, String utxoTXID, int utxoVOUT) { + return QueryBuilder.apply(this, (query) { + if (query.whereSort == Sort.asc) { + return query + .addWhereClause(IndexWhereClause.between( + indexName: r'inscriptionId_utxoTXID_utxoVOUT', + lower: [inscriptionId, utxoTXID], + upper: [inscriptionId, utxoTXID, utxoVOUT], + includeUpper: false, + )) + .addWhereClause(IndexWhereClause.between( + indexName: r'inscriptionId_utxoTXID_utxoVOUT', + lower: [inscriptionId, utxoTXID, utxoVOUT], + includeLower: false, + upper: [inscriptionId, utxoTXID], + )); + } else { + return query + .addWhereClause(IndexWhereClause.between( + indexName: r'inscriptionId_utxoTXID_utxoVOUT', + lower: [inscriptionId, utxoTXID, utxoVOUT], + includeLower: false, + upper: [inscriptionId, utxoTXID], + )) + .addWhereClause(IndexWhereClause.between( + indexName: r'inscriptionId_utxoTXID_utxoVOUT', + lower: [inscriptionId, utxoTXID], + upper: [inscriptionId, utxoTXID, utxoVOUT], + includeUpper: false, + )); + } + }); + } + + QueryBuilder + inscriptionIdUtxoTXIDEqualToUtxoVOUTGreaterThan( + String inscriptionId, + String utxoTXID, + int utxoVOUT, { + bool include = false, + }) { + return QueryBuilder.apply(this, (query) { + return query.addWhereClause(IndexWhereClause.between( + indexName: r'inscriptionId_utxoTXID_utxoVOUT', + lower: [inscriptionId, utxoTXID, utxoVOUT], + includeLower: include, + upper: [inscriptionId, utxoTXID], + )); + }); + } + + QueryBuilder + inscriptionIdUtxoTXIDEqualToUtxoVOUTLessThan( + String inscriptionId, + String utxoTXID, + int utxoVOUT, { + bool include = false, + }) { + return QueryBuilder.apply(this, (query) { + return query.addWhereClause(IndexWhereClause.between( + indexName: r'inscriptionId_utxoTXID_utxoVOUT', + lower: [inscriptionId, utxoTXID], + upper: [inscriptionId, utxoTXID, utxoVOUT], + includeUpper: include, + )); + }); + } + + QueryBuilder + inscriptionIdUtxoTXIDEqualToUtxoVOUTBetween( + String inscriptionId, + String utxoTXID, + int lowerUtxoVOUT, + int upperUtxoVOUT, { + bool includeLower = true, + bool includeUpper = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addWhereClause(IndexWhereClause.between( + indexName: r'inscriptionId_utxoTXID_utxoVOUT', + lower: [inscriptionId, utxoTXID, lowerUtxoVOUT], + includeLower: includeLower, + upper: [inscriptionId, utxoTXID, upperUtxoVOUT], + includeUpper: includeUpper, + )); + }); + } +} + +extension OrdinalQueryFilter + on QueryBuilder { + QueryBuilder contentEqualTo( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'content', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder contentGreaterThan( + String value, { + bool include = false, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + include: include, + property: r'content', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder contentLessThan( + String value, { + bool include = false, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.lessThan( + include: include, + property: r'content', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder contentBetween( + 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'content', + lower: lower, + includeLower: includeLower, + upper: upper, + includeUpper: includeUpper, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder contentStartsWith( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.startsWith( + property: r'content', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder contentEndsWith( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.endsWith( + property: r'content', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder contentContains( + String value, + {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.contains( + property: r'content', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder contentMatches( + String pattern, + {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.matches( + property: r'content', + wildcard: pattern, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder contentIsEmpty() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'content', + value: '', + )); + }); + } + + QueryBuilder contentIsNotEmpty() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + property: r'content', + value: '', + )); + }); + } + + 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 inscriptionIdEqualTo( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'inscriptionId', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + inscriptionIdGreaterThan( + String value, { + bool include = false, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + include: include, + property: r'inscriptionId', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder inscriptionIdLessThan( + String value, { + bool include = false, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.lessThan( + include: include, + property: r'inscriptionId', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder inscriptionIdBetween( + 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'inscriptionId', + lower: lower, + includeLower: includeLower, + upper: upper, + includeUpper: includeUpper, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder inscriptionIdStartsWith( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.startsWith( + property: r'inscriptionId', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder inscriptionIdEndsWith( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.endsWith( + property: r'inscriptionId', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder inscriptionIdContains( + String value, + {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.contains( + property: r'inscriptionId', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder inscriptionIdMatches( + String pattern, + {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.matches( + property: r'inscriptionId', + wildcard: pattern, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder inscriptionIdIsEmpty() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'inscriptionId', + value: '', + )); + }); + } + + QueryBuilder + inscriptionIdIsNotEmpty() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + property: r'inscriptionId', + value: '', + )); + }); + } + + QueryBuilder + inscriptionNumberEqualTo(int value) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'inscriptionNumber', + value: value, + )); + }); + } + + QueryBuilder + inscriptionNumberGreaterThan( + int value, { + bool include = false, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + include: include, + property: r'inscriptionNumber', + value: value, + )); + }); + } + + QueryBuilder + inscriptionNumberLessThan( + int value, { + bool include = false, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.lessThan( + include: include, + property: r'inscriptionNumber', + value: value, + )); + }); + } + + QueryBuilder + inscriptionNumberBetween( + int lower, + int upper, { + bool includeLower = true, + bool includeUpper = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.between( + property: r'inscriptionNumber', + lower: lower, + includeLower: includeLower, + upper: upper, + includeUpper: includeUpper, + )); + }); + } + + QueryBuilder utxoTXIDEqualTo( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'utxoTXID', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder utxoTXIDGreaterThan( + String value, { + bool include = false, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + include: include, + property: r'utxoTXID', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder utxoTXIDLessThan( + String value, { + bool include = false, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.lessThan( + include: include, + property: r'utxoTXID', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder utxoTXIDBetween( + 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'utxoTXID', + lower: lower, + includeLower: includeLower, + upper: upper, + includeUpper: includeUpper, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder utxoTXIDStartsWith( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.startsWith( + property: r'utxoTXID', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder utxoTXIDEndsWith( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.endsWith( + property: r'utxoTXID', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder utxoTXIDContains( + String value, + {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.contains( + property: r'utxoTXID', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder utxoTXIDMatches( + String pattern, + {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.matches( + property: r'utxoTXID', + wildcard: pattern, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder utxoTXIDIsEmpty() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'utxoTXID', + value: '', + )); + }); + } + + QueryBuilder utxoTXIDIsNotEmpty() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + property: r'utxoTXID', + value: '', + )); + }); + } + + QueryBuilder utxoVOUTEqualTo( + int value) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'utxoVOUT', + value: value, + )); + }); + } + + QueryBuilder utxoVOUTGreaterThan( + int value, { + bool include = false, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + include: include, + property: r'utxoVOUT', + value: value, + )); + }); + } + + QueryBuilder utxoVOUTLessThan( + int value, { + bool include = false, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.lessThan( + include: include, + property: r'utxoVOUT', + value: value, + )); + }); + } + + QueryBuilder utxoVOUTBetween( + int lower, + int upper, { + bool includeLower = true, + bool includeUpper = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.between( + property: r'utxoVOUT', + lower: lower, + includeLower: includeLower, + upper: upper, + includeUpper: includeUpper, + )); + }); + } + + QueryBuilder walletIdEqualTo( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'walletId', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder walletIdGreaterThan( + String value, { + bool include = false, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + include: include, + property: r'walletId', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder walletIdLessThan( + String value, { + bool include = false, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.lessThan( + include: include, + property: r'walletId', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder walletIdBetween( + 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'walletId', + lower: lower, + includeLower: includeLower, + upper: upper, + includeUpper: includeUpper, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder walletIdStartsWith( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.startsWith( + property: r'walletId', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder walletIdEndsWith( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.endsWith( + property: r'walletId', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder walletIdContains( + String value, + {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.contains( + property: r'walletId', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder walletIdMatches( + String pattern, + {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.matches( + property: r'walletId', + wildcard: pattern, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder walletIdIsEmpty() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'walletId', + value: '', + )); + }); + } + + QueryBuilder walletIdIsNotEmpty() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + property: r'walletId', + value: '', + )); + }); + } +} + +extension OrdinalQueryObject + on QueryBuilder {} + +extension OrdinalQueryLinks + on QueryBuilder {} + +extension OrdinalQuerySortBy on QueryBuilder { + QueryBuilder sortByContent() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'content', Sort.asc); + }); + } + + QueryBuilder sortByContentDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'content', Sort.desc); + }); + } + + QueryBuilder sortByInscriptionId() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'inscriptionId', Sort.asc); + }); + } + + QueryBuilder sortByInscriptionIdDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'inscriptionId', Sort.desc); + }); + } + + QueryBuilder sortByInscriptionNumber() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'inscriptionNumber', Sort.asc); + }); + } + + QueryBuilder sortByInscriptionNumberDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'inscriptionNumber', Sort.desc); + }); + } + + QueryBuilder sortByUtxoTXID() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'utxoTXID', Sort.asc); + }); + } + + QueryBuilder sortByUtxoTXIDDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'utxoTXID', Sort.desc); + }); + } + + QueryBuilder sortByUtxoVOUT() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'utxoVOUT', Sort.asc); + }); + } + + QueryBuilder sortByUtxoVOUTDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'utxoVOUT', Sort.desc); + }); + } + + QueryBuilder sortByWalletId() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'walletId', Sort.asc); + }); + } + + QueryBuilder sortByWalletIdDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'walletId', Sort.desc); + }); + } +} + +extension OrdinalQuerySortThenBy + on QueryBuilder { + QueryBuilder thenByContent() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'content', Sort.asc); + }); + } + + QueryBuilder thenByContentDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'content', 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 thenByInscriptionId() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'inscriptionId', Sort.asc); + }); + } + + QueryBuilder thenByInscriptionIdDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'inscriptionId', Sort.desc); + }); + } + + QueryBuilder thenByInscriptionNumber() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'inscriptionNumber', Sort.asc); + }); + } + + QueryBuilder thenByInscriptionNumberDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'inscriptionNumber', Sort.desc); + }); + } + + QueryBuilder thenByUtxoTXID() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'utxoTXID', Sort.asc); + }); + } + + QueryBuilder thenByUtxoTXIDDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'utxoTXID', Sort.desc); + }); + } + + QueryBuilder thenByUtxoVOUT() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'utxoVOUT', Sort.asc); + }); + } + + QueryBuilder thenByUtxoVOUTDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'utxoVOUT', Sort.desc); + }); + } + + QueryBuilder thenByWalletId() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'walletId', Sort.asc); + }); + } + + QueryBuilder thenByWalletIdDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'walletId', Sort.desc); + }); + } +} + +extension OrdinalQueryWhereDistinct + on QueryBuilder { + QueryBuilder distinctByContent( + {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addDistinctBy(r'content', caseSensitive: caseSensitive); + }); + } + + QueryBuilder distinctByInscriptionId( + {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addDistinctBy(r'inscriptionId', + caseSensitive: caseSensitive); + }); + } + + QueryBuilder distinctByInscriptionNumber() { + return QueryBuilder.apply(this, (query) { + return query.addDistinctBy(r'inscriptionNumber'); + }); + } + + QueryBuilder distinctByUtxoTXID( + {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addDistinctBy(r'utxoTXID', caseSensitive: caseSensitive); + }); + } + + QueryBuilder distinctByUtxoVOUT() { + return QueryBuilder.apply(this, (query) { + return query.addDistinctBy(r'utxoVOUT'); + }); + } + + QueryBuilder distinctByWalletId( + {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addDistinctBy(r'walletId', caseSensitive: caseSensitive); + }); + } +} + +extension OrdinalQueryProperty + on QueryBuilder { + QueryBuilder idProperty() { + return QueryBuilder.apply(this, (query) { + return query.addPropertyName(r'id'); + }); + } + + QueryBuilder contentProperty() { + return QueryBuilder.apply(this, (query) { + return query.addPropertyName(r'content'); + }); + } + + QueryBuilder inscriptionIdProperty() { + return QueryBuilder.apply(this, (query) { + return query.addPropertyName(r'inscriptionId'); + }); + } + + QueryBuilder inscriptionNumberProperty() { + return QueryBuilder.apply(this, (query) { + return query.addPropertyName(r'inscriptionNumber'); + }); + } + + QueryBuilder utxoTXIDProperty() { + return QueryBuilder.apply(this, (query) { + return query.addPropertyName(r'utxoTXID'); + }); + } + + QueryBuilder utxoVOUTProperty() { + return QueryBuilder.apply(this, (query) { + return query.addPropertyName(r'utxoVOUT'); + }); + } + + QueryBuilder walletIdProperty() { + return QueryBuilder.apply(this, (query) { + return query.addPropertyName(r'walletId'); + }); + } +} diff --git a/lib/models/ordinal.dart b/lib/models/ordinal.dart deleted file mode 100644 index 66a69de94..000000000 --- a/lib/models/ordinal.dart +++ /dev/null @@ -1,29 +0,0 @@ -import 'package:stackwallet/dto/ordinals/inscription_data.dart'; - -class Ordinal { - final String inscriptionId; - final int inscriptionNumber; - final String content; - - // following two are used to look up the UTXO object in isar combined w/ walletId - final String utxoTXID; - final int utxoVOUT; - - Ordinal({ - required this.inscriptionId, - required this.inscriptionNumber, - required this.content, - required this.utxoTXID, - required this.utxoVOUT, - }); - - factory Ordinal.fromInscriptionData(InscriptionData data) { - return Ordinal( - inscriptionId: data.inscriptionId, - inscriptionNumber: data.inscriptionNumber, - content: data.content, - utxoTXID: data.output.split(':')[0], // "output": "062f32e21aa04246b8873b5d9a929576addd0339881e1ea478b406795d6b6c47:0" - utxoVOUT: int.parse(data.output.split(':')[1]), - ); - } -} \ No newline at end of file diff --git a/lib/pages/ordinals/widgets/ordinals_list.dart b/lib/pages/ordinals/widgets/ordinals_list.dart index c654f1522..6f5a5fbf1 100644 --- a/lib/pages/ordinals/widgets/ordinals_list.dart +++ b/lib/pages/ordinals/widgets/ordinals_list.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:stackwallet/models/ordinal.dart'; import 'package:stackwallet/pages/ordinals/widgets/ordinal_card.dart'; +import 'package:stackwallet/widgets/loading_indicator.dart'; class OrdinalsList extends StatelessWidget { const OrdinalsList({ @@ -20,7 +21,7 @@ class OrdinalsList extends StatelessWidget { future: ordinalsFuture, builder: (context, snapshot) { if (snapshot.connectionState == ConnectionState.waiting) { - return const CircularProgressIndicator(); + return const LoadingIndicator(); } else if (snapshot.hasError) { return Text('Error: ${snapshot.error}'); } else if (snapshot.hasData) { diff --git a/lib/route_generator.dart b/lib/route_generator.dart index b3a104dd0..4c77021ea 100644 --- a/lib/route_generator.dart +++ b/lib/route_generator.dart @@ -19,7 +19,7 @@ import 'package:stackwallet/models/exchange/incomplete_exchange.dart'; import 'package:stackwallet/models/exchange/response_objects/trade.dart'; import 'package:stackwallet/models/isar/models/contact_entry.dart'; import 'package:stackwallet/models/isar/models/isar_models.dart'; -import 'package:stackwallet/models/ordinal.dart'; +import 'package:stackwallet/models/isar/ordinal.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'; diff --git a/lib/services/mixins/ordinals_interface.dart b/lib/services/mixins/ordinals_interface.dart index a779f5b2e..0b7d5497d 100644 --- a/lib/services/mixins/ordinals_interface.dart +++ b/lib/services/mixins/ordinals_interface.dart @@ -4,7 +4,7 @@ import 'package:isar/isar.dart'; import 'package:stackwallet/db/isar/main_db.dart'; import 'package:stackwallet/dto/ordinals/inscription_data.dart'; import 'package:stackwallet/models/isar/models/blockchain_data/utxo.dart'; -import 'package:stackwallet/models/ordinal.dart'; +import 'package:stackwallet/models/isar/ordinal.dart'; import 'package:stackwallet/services/litescribe_api.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; @@ -18,12 +18,13 @@ mixin OrdinalsInterface { required Coin coin, required MainDB db, }) { - print('init'); _walletId = walletId; _coin = coin; _db = db; } - final LitescribeAPI litescribeAPI = LitescribeAPI(baseUrl: 'https://litescribe.io/api'); + + final LitescribeAPI litescribeAPI = + LitescribeAPI(baseUrl: 'https://litescribe.io/api'); void refreshInscriptions() async { List _inscriptions; @@ -63,22 +64,26 @@ mixin OrdinalsInterface { return uniqueAddresses.toList(); } - Future> getInscriptionDataFromAddress(String address) async { + Future> getInscriptionDataFromAddress( + String address) async { List allInscriptions = []; try { var inscriptions = await litescribeAPI.getInscriptionsByAddress(address); allInscriptions.addAll(inscriptions); } catch (e) { - throw Exception('Error in OrdinalsInterface getInscriptionsByAddress: $e'); + throw Exception( + 'Error in OrdinalsInterface getInscriptionsByAddress: $e'); } return allInscriptions; } - Future> getInscriptionDataFromAddresses(List addresses) async { + Future> getInscriptionDataFromAddresses( + List addresses) async { List allInscriptions = []; for (String address in addresses) { try { - var inscriptions = await litescribeAPI.getInscriptionsByAddress(address); + var inscriptions = + await litescribeAPI.getInscriptionsByAddress(address); allInscriptions.addAll(inscriptions); } catch (e) { print("Error fetching inscriptions for address $address: $e"); @@ -90,7 +95,9 @@ mixin OrdinalsInterface { Future> getOrdinalsFromAddress(String address) async { try { var inscriptions = await litescribeAPI.getInscriptionsByAddress(address); - return inscriptions.map((data) => Ordinal.fromInscriptionData(data)).toList(); + return inscriptions + .map((data) => Ordinal.fromInscriptionData(data, _walletId)) + .toList(); } catch (e) { throw Exception('Error in OrdinalsInterface getOrdinalsFromAddress: $e'); } @@ -100,12 +107,14 @@ mixin OrdinalsInterface { List allOrdinals = []; for (String address in addresses) { try { - var inscriptions = await litescribeAPI.getInscriptionsByAddress(address); - allOrdinals.addAll(inscriptions.map((data) => Ordinal.fromInscriptionData(data))); + var inscriptions = + await litescribeAPI.getInscriptionsByAddress(address); + allOrdinals.addAll(inscriptions + .map((data) => Ordinal.fromInscriptionData(data, _walletId))); } catch (e) { print("Error fetching inscriptions for address $address: $e"); } } return allOrdinals; } -} \ No newline at end of file +} From dffa33abd33e668f9fc72862f8432199927deda0 Mon Sep 17 00:00:00 2001 From: julian Date: Fri, 21 Jul 2023 18:43:59 -0600 Subject: [PATCH 090/169] load ordinals into isar as a cache and backend view model --- lib/db/isar/main_db.dart | 2 + lib/pages/ordinals/ordinal_details_view.dart | 4 +- lib/pages/ordinals/ordinals_view.dart | 25 +-- lib/pages/ordinals/widgets/ordinal_card.dart | 17 +- lib/pages/ordinals/widgets/ordinals_list.dart | 112 ++++++++---- .../desktop_ordinal_details_view.dart | 4 +- .../ordinals/desktop_ordinals_view.dart | 56 ++---- .../subwidgets/desktop_ordinal_card.dart | 56 ------ .../subwidgets/desktop_ordinals_list.dart | 49 ------ lib/services/mixins/ordinals_interface.dart | 163 ++++++++++-------- 10 files changed, 218 insertions(+), 270 deletions(-) delete mode 100644 lib/pages_desktop_specific/ordinals/subwidgets/desktop_ordinal_card.dart delete mode 100644 lib/pages_desktop_specific/ordinals/subwidgets/desktop_ordinals_list.dart diff --git a/lib/db/isar/main_db.dart b/lib/db/isar/main_db.dart index bb6ea5e8d..9d445aebb 100644 --- a/lib/db/isar/main_db.dart +++ b/lib/db/isar/main_db.dart @@ -15,6 +15,7 @@ import 'package:stackwallet/exceptions/main_db/main_db_exception.dart'; import 'package:stackwallet/models/isar/models/block_explorer.dart'; import 'package:stackwallet/models/isar/models/contact_entry.dart'; import 'package:stackwallet/models/isar/models/isar_models.dart'; +import 'package:stackwallet/models/isar/ordinal.dart'; import 'package:stackwallet/models/isar/stack_theme.dart'; import 'package:stackwallet/utilities/amount/amount.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; @@ -54,6 +55,7 @@ class MainDB { TransactionBlockExplorerSchema, StackThemeSchema, ContactEntrySchema, + OrdinalSchema, ], directory: (await StackFileSystem.applicationIsarDirectory()).path, // inspector: kDebugMode, diff --git a/lib/pages/ordinals/ordinal_details_view.dart b/lib/pages/ordinals/ordinal_details_view.dart index 6f6d0b8a7..0bbcbe37a 100644 --- a/lib/pages/ordinals/ordinal_details_view.dart +++ b/lib/pages/ordinals/ordinal_details_view.dart @@ -3,7 +3,7 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_svg/flutter_svg.dart'; -import 'package:stackwallet/models/ordinal.dart'; +import 'package:stackwallet/models/isar/ordinal.dart'; import 'package:stackwallet/notifications/show_flush_bar.dart'; import 'package:stackwallet/pages/ordinals/widgets/dialogs.dart'; import 'package:stackwallet/themes/stack_colors.dart'; @@ -28,7 +28,7 @@ class OrdinalDetailsView extends StatefulWidget { static const routeName = "/ordinalDetailsView"; @override - _OrdinalDetailsViewState createState() => _OrdinalDetailsViewState(); + State createState() => _OrdinalDetailsViewState(); } class _OrdinalDetailsViewState extends State { diff --git a/lib/pages/ordinals/ordinals_view.dart b/lib/pages/ordinals/ordinals_view.dart index a4adfa019..4e1e985dd 100644 --- a/lib/pages/ordinals/ordinals_view.dart +++ b/lib/pages/ordinals/ordinals_view.dart @@ -11,7 +11,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/svg.dart'; -import 'package:stackwallet/models/ordinal.dart'; import 'package:stackwallet/pages/ordinals/ordinals_filter_view.dart'; import 'package:stackwallet/pages/ordinals/widgets/ordinals_list.dart'; import 'package:stackwallet/providers/global/wallets_provider.dart'; @@ -19,6 +18,7 @@ import 'package:stackwallet/services/mixins/ordinals_interface.dart'; import 'package:stackwallet/themes/stack_colors.dart'; import 'package:stackwallet/utilities/assets.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/background.dart'; @@ -46,7 +46,6 @@ class _OrdinalsViewState extends ConsumerState { late final FocusNode searchFocus; String _searchTerm = ""; - dynamic _manager; @override void initState() { @@ -56,14 +55,6 @@ class _OrdinalsViewState extends ConsumerState { super.initState(); } - @override - void didChangeDependencies() { - super.didChangeDependencies(); - // Set _manager here when the widget's dependencies change - _manager = ref.watch(walletsChangeNotifierProvider - .select((value) => value.getManager(widget.walletId))); - } - @override void dispose() { searchController.dispose(); @@ -73,6 +64,9 @@ class _OrdinalsViewState extends ConsumerState { @override Widget build(BuildContext context) { + final manager = ref.watch(walletsChangeNotifierProvider + .select((value) => value.getManager(widget.walletId))); + return Background( child: SafeArea( child: Scaffold( @@ -100,7 +94,15 @@ class _OrdinalsViewState extends ConsumerState { .topNavIconPrimary, ), onPressed: () async { - (_manager.wallet as OrdinalsInterface).refreshInscriptions(); + // show loading for a minimum of 2 seconds on refreshing + await showLoading( + whileFuture: Future.wait([ + Future.delayed(const Duration(seconds: 2)), + (manager.wallet as OrdinalsInterface) + .refreshInscriptions() + ]), + context: context, + message: "Refreshing..."); }, ), ), @@ -193,7 +195,6 @@ class _OrdinalsViewState extends ConsumerState { Expanded( child: OrdinalsList( walletId: widget.walletId, - ordinalsFuture: (_manager.wallet as OrdinalsInterface).getOrdinals(), ), ), ], diff --git a/lib/pages/ordinals/widgets/ordinal_card.dart b/lib/pages/ordinals/widgets/ordinal_card.dart index 4ac4fd877..e537611f8 100644 --- a/lib/pages/ordinals/widgets/ordinal_card.dart +++ b/lib/pages/ordinals/widgets/ordinal_card.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; - -import 'package:stackwallet/models/ordinal.dart'; +import 'package:stackwallet/models/isar/ordinal.dart'; import 'package:stackwallet/pages/ordinals/ordinal_details_view.dart'; +import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/widgets/rounded_white_container.dart'; @@ -28,14 +28,17 @@ class OrdinalCard extends StatelessWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.center, children: [ - AspectRatio( - aspectRatio: 1, - child: Container( - color: Colors.red, + AspectRatio( + aspectRatio: 1, + child: ClipRRect( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), child: Image.network( ordinal.content, // Use the preview URL as the image source fit: BoxFit.cover, - filterQuality: FilterQuality.none, // Set the filter mode to nearest + filterQuality: + FilterQuality.none, // Set the filter mode to nearest ), ), ), diff --git a/lib/pages/ordinals/widgets/ordinals_list.dart b/lib/pages/ordinals/widgets/ordinals_list.dart index 6f5a5fbf1..ad333d2ed 100644 --- a/lib/pages/ordinals/widgets/ordinals_list.dart +++ b/lib/pages/ordinals/widgets/ordinals_list.dart @@ -1,48 +1,96 @@ -import 'package:flutter/material.dart'; -import 'package:stackwallet/models/ordinal.dart'; -import 'package:stackwallet/pages/ordinals/widgets/ordinal_card.dart'; -import 'package:stackwallet/widgets/loading_indicator.dart'; +import 'dart:async'; -class OrdinalsList extends StatelessWidget { +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:isar/isar.dart'; +import 'package:stackwallet/models/isar/ordinal.dart'; +import 'package:stackwallet/pages/ordinals/widgets/ordinal_card.dart'; +import 'package:stackwallet/providers/db/main_db_provider.dart'; +import 'package:stackwallet/utilities/text_styles.dart'; +import 'package:stackwallet/utilities/util.dart'; +import 'package:stackwallet/widgets/rounded_white_container.dart'; + +class OrdinalsList extends ConsumerStatefulWidget { const OrdinalsList({ Key? key, required this.walletId, - required this.ordinalsFuture, }) : super(key: key); final String walletId; - final Future> ordinalsFuture; - double get spacing => 2.0; + @override + ConsumerState createState() => _OrdinalsListState(); +} + +class _OrdinalsListState extends ConsumerState { + static const double _spacing = 10.0; + + late List _data; + + late final Stream?> _stream; + + @override + void initState() { + _stream = ref + .read(mainDBProvider) + .isar + .ordinals + .where() + .filter() + .walletIdEqualTo(widget.walletId) + .watch(); + + _data = ref + .read(mainDBProvider) + .isar + .ordinals + .where() + .filter() + .walletIdEqualTo(widget.walletId) + .findAllSync(); + + super.initState(); + } @override Widget build(BuildContext context) { - return FutureBuilder>( - future: ordinalsFuture, + return StreamBuilder?>( + stream: _stream, builder: (context, snapshot) { - if (snapshot.connectionState == ConnectionState.waiting) { - return const LoadingIndicator(); - } else if (snapshot.hasError) { - return Text('Error: ${snapshot.error}'); - } else if (snapshot.hasData) { - final List ordinals = snapshot.data!; - return GridView.builder( - shrinkWrap: true, - itemCount: ordinals.length, - gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( - crossAxisSpacing: spacing, - mainAxisSpacing: spacing, - crossAxisCount: 2, - childAspectRatio: 6 / 7, // was 3/4, less data displayed now - ), - itemBuilder: (_, i) => OrdinalCard( - walletId: walletId, - ordinal: ordinals[i], - ), - ); - } else { - return const Text('No data found.'); + if (snapshot.hasData) { + _data = snapshot.data!; } + + if (_data.isEmpty) { + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + RoundedWhiteContainer( + child: Center( + child: Text( + "Your ordinals will appear here", + style: STextStyles.label(context), + ), + ), + ), + ], + ); + } + + return GridView.builder( + shrinkWrap: true, + itemCount: _data.length, + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( + crossAxisSpacing: _spacing, + mainAxisSpacing: _spacing, + crossAxisCount: Util.isDesktop ? 4 : 2, + childAspectRatio: 6 / 7, // was 3/4, less data displayed now + ), + itemBuilder: (_, i) => OrdinalCard( + walletId: widget.walletId, + ordinal: _data[i], + ), + ); }, ); } diff --git a/lib/pages_desktop_specific/ordinals/desktop_ordinal_details_view.dart b/lib/pages_desktop_specific/ordinals/desktop_ordinal_details_view.dart index 1477a0d2f..a477f6228 100644 --- a/lib/pages_desktop_specific/ordinals/desktop_ordinal_details_view.dart +++ b/lib/pages_desktop_specific/ordinals/desktop_ordinal_details_view.dart @@ -3,7 +3,7 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_svg/flutter_svg.dart'; -import 'package:stackwallet/models/ordinal.dart'; +import 'package:stackwallet/models/isar/ordinal.dart'; import 'package:stackwallet/notifications/show_flush_bar.dart'; import 'package:stackwallet/pages/ordinals/widgets/dialogs.dart'; import 'package:stackwallet/themes/stack_colors.dart'; @@ -29,7 +29,7 @@ class DesktopOrdinalDetailsView extends StatefulWidget { static const routeName = "/desktopOrdinalDetailsView"; @override - _DesktopOrdinalDetailsViewState createState() => + State createState() => _DesktopOrdinalDetailsViewState(); } diff --git a/lib/pages_desktop_specific/ordinals/desktop_ordinals_view.dart b/lib/pages_desktop_specific/ordinals/desktop_ordinals_view.dart index a50cc85db..0068e4033 100644 --- a/lib/pages_desktop_specific/ordinals/desktop_ordinals_view.dart +++ b/lib/pages_desktop_specific/ordinals/desktop_ordinals_view.dart @@ -11,35 +11,18 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/svg.dart'; -import 'package:stackwallet/models/isar/models/contact_entry.dart'; -import 'package:stackwallet/pages/address_book_views/subviews/add_address_book_entry_view.dart'; -import 'package:stackwallet/pages/address_book_views/subviews/address_book_filter_view.dart'; -import 'package:stackwallet/pages_desktop_specific/ordinals/subwidgets/desktop_ordinals_list.dart'; -import 'package:stackwallet/pages_desktop_specific/address_book_view/subwidgets/desktop_address_book_scaffold.dart'; -import 'package:stackwallet/pages_desktop_specific/address_book_view/subwidgets/desktop_contact_details.dart'; -import 'package:stackwallet/providers/global/address_book_service_provider.dart'; +import 'package:stackwallet/pages/ordinals/widgets/ordinals_list.dart'; import 'package:stackwallet/providers/providers.dart'; -import 'package:stackwallet/providers/ui/address_book_providers/address_book_filter_provider.dart'; -import 'package:stackwallet/services/mixins/ordinals_interface.dart'; -import 'package:stackwallet/themes/stack_colors.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/util.dart'; -import 'package:stackwallet/widgets/address_book_card.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/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_container.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 DesktopOrdinalsView extends ConsumerStatefulWidget { const DesktopOrdinalsView({ super.key, @@ -62,7 +45,7 @@ class _DesktopOrdinals extends ConsumerState { dynamic _manager; @override - void initState() { + void initState() { searchController = TextEditingController(); searchFocusNode = FocusNode(); @@ -151,23 +134,23 @@ class _DesktopOrdinals extends ConsumerState { ), 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 = ""; - _searchTerm = ""; - }); - }, + padding: const EdgeInsets.only(right: 0), + child: UnconstrainedBox( + child: Row( + children: [ + TextFieldIconButton( + child: const XIcon(), + onTap: () async { + setState(() { + searchController.text = ""; + _searchTerm = ""; + }); + }, + ), + ], + ), ), - ], - ), - ), - ) + ) : null, ), ), @@ -176,9 +159,8 @@ class _DesktopOrdinals extends ConsumerState { height: 16, ), Expanded( - child: DesktopOrdinalsList( + child: OrdinalsList( walletId: widget.walletId, - ordinalsFuture: (_manager.wallet as OrdinalsInterface).getOrdinals(), ), ), ], diff --git a/lib/pages_desktop_specific/ordinals/subwidgets/desktop_ordinal_card.dart b/lib/pages_desktop_specific/ordinals/subwidgets/desktop_ordinal_card.dart deleted file mode 100644 index 9d94a4b0a..000000000 --- a/lib/pages_desktop_specific/ordinals/subwidgets/desktop_ordinal_card.dart +++ /dev/null @@ -1,56 +0,0 @@ -import 'package:flutter/material.dart'; - -import 'package:stackwallet/models/ordinal.dart'; -import 'package:stackwallet/pages_desktop_specific/ordinals/desktop_ordinal_details_view.dart'; -import 'package:stackwallet/utilities/text_styles.dart'; -import 'package:stackwallet/widgets/rounded_white_container.dart'; - -class DesktopOrdinalCard extends StatelessWidget { - const DesktopOrdinalCard({ - Key? key, - required this.walletId, - required this.ordinal, - }) : super(key: key); - - final String walletId; - final Ordinal ordinal; - - @override - Widget build(BuildContext context) { - return RoundedWhiteContainer( - radiusMultiplier: 2, - onPressed: () { - Navigator.of(context).pushNamed( - DesktopOrdinalDetailsView.routeName, - arguments: (walletId: walletId, ordinal: ordinal), - ); - }, - child: Column( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - AspectRatio( - aspectRatio: 1, - child: Container( - color: Colors.red, - child: Image.network( - ordinal.content, // Use the preview URL as the image source - fit: BoxFit.cover, - filterQuality: FilterQuality.none, // Set the filter mode to nearest - ), - ), - ), - const Spacer(), - Text( - 'INSC. ${ordinal.inscriptionNumber}', // infer from address associated with utxoTXID - style: STextStyles.w500_12(context), - ), - // const Spacer(), - // Text( - // "ID ${ordinal.inscriptionId}", - // style: STextStyles.w500_8(context), - // ), - ], - ), - ); - } -} diff --git a/lib/pages_desktop_specific/ordinals/subwidgets/desktop_ordinals_list.dart b/lib/pages_desktop_specific/ordinals/subwidgets/desktop_ordinals_list.dart deleted file mode 100644 index b30e94dd4..000000000 --- a/lib/pages_desktop_specific/ordinals/subwidgets/desktop_ordinals_list.dart +++ /dev/null @@ -1,49 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:stackwallet/models/ordinal.dart'; - -import 'package:stackwallet/pages_desktop_specific/ordinals/subwidgets/desktop_ordinal_card.dart'; - -class DesktopOrdinalsList extends StatelessWidget { - const DesktopOrdinalsList({ - Key? key, - required this.walletId, - required this.ordinalsFuture, - }) : super(key: key); - - final String walletId; - final Future> ordinalsFuture; - - get spacing => 2.0; - - @override - Widget build(BuildContext context) { - return FutureBuilder>( - future: ordinalsFuture, - builder: (context, snapshot) { - if (snapshot.connectionState == ConnectionState.waiting) { - return const CircularProgressIndicator(); - } else if (snapshot.hasError) { - return Text('Error: ${snapshot.error}'); - } else if (snapshot.hasData) { - final List ordinals = snapshot.data!; - return GridView.builder( - shrinkWrap: true, - itemCount: ordinals.length, - gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( - crossAxisSpacing: spacing as double, - mainAxisSpacing: spacing as double, - crossAxisCount: 4, - childAspectRatio: 6 / 7, // was 3/4, less data displayed now - ), - itemBuilder: (_, i) => DesktopOrdinalCard( - walletId: walletId, - ordinal: ordinals[i], - ), - ); - } else { - return const Text('No data found.'); - } - }, - ); - } -} \ No newline at end of file diff --git a/lib/services/mixins/ordinals_interface.dart b/lib/services/mixins/ordinals_interface.dart index 0b7d5497d..51a82307a 100644 --- a/lib/services/mixins/ordinals_interface.dart +++ b/lib/services/mixins/ordinals_interface.dart @@ -26,58 +26,75 @@ mixin OrdinalsInterface { final LitescribeAPI litescribeAPI = LitescribeAPI(baseUrl: 'https://litescribe.io/api'); - void refreshInscriptions() async { - List _inscriptions; - final utxos = await _db.getUTXOs(_walletId).findAll(); - final uniqueAddresses = getUniqueAddressesFromUTXOs(utxos); - _inscriptions = await getInscriptionDataFromAddresses(uniqueAddresses); - // TODO save inscriptions to isar which gets watched by a FutureBuilder/StreamBuilder - } + Future refreshInscriptions() async { + final uniqueAddresses = await _db + .getUTXOs(_walletId) + .filter() + .addressIsNotNull() + .distinctByAddress() + .addressProperty() + .findAll(); + final inscriptions = + await _getInscriptionDataFromAddresses(uniqueAddresses.cast()); - Future> getInscriptionData() async { - try { - final utxos = await _db.getUTXOs(_walletId).findAll(); - final uniqueAddresses = getUniqueAddressesFromUTXOs(utxos); - return await getInscriptionDataFromAddresses(uniqueAddresses); - } catch (e) { - throw Exception('Error in OrdinalsInterface getInscriptions: $e'); - } - } + final ords = inscriptions + .map((e) => Ordinal.fromInscriptionData(e, _walletId)) + .toList(); - Future> getOrdinals() async { - try { - final utxos = await _db.getUTXOs(_walletId).findAll(); - final uniqueAddresses = getUniqueAddressesFromUTXOs(utxos); - return await getOrdinalsFromAddresses(uniqueAddresses); - } catch (e) { - throw Exception('Error in OrdinalsInterface getOrdinals: $e'); - } + await _db.isar.writeTxn(() async { + await _db.isar.ordinals + .where() + .filter() + .walletIdEqualTo(_walletId) + .deleteAll(); + await _db.isar.ordinals.putAll(ords); + }); } + // + // Future> getInscriptionData() async { + // try { + // final utxos = await _db.getUTXOs(_walletId).findAll(); + // final uniqueAddresses = getUniqueAddressesFromUTXOs(utxos); + // return await _getInscriptionDataFromAddresses(uniqueAddresses); + // } catch (e) { + // throw Exception('Error in OrdinalsInterface getInscriptions: $e'); + // } + // } + // + // Future> getOrdinals() async { + // try { + // final utxos = await _db.getUTXOs(_walletId).findAll(); + // final uniqueAddresses = getUniqueAddressesFromUTXOs(utxos); + // return await getOrdinalsFromAddresses(uniqueAddresses); + // } catch (e) { + // throw Exception('Error in OrdinalsInterface getOrdinals: $e'); + // } + // } + // + // List getUniqueAddressesFromUTXOs(List utxos) { + // final Set uniqueAddresses = {}; + // for (var utxo in utxos) { + // if (utxo.address != null) { + // uniqueAddresses.add(utxo.address!); + // } + // } + // return uniqueAddresses.toList(); + // } + // + // Future> getInscriptionDataFromAddress( + // String address) async { + // List allInscriptions = []; + // try { + // var inscriptions = await litescribeAPI.getInscriptionsByAddress(address); + // allInscriptions.addAll(inscriptions); + // } catch (e) { + // throw Exception( + // 'Error in OrdinalsInterface getInscriptionsByAddress: $e'); + // } + // return allInscriptions; + // } - List getUniqueAddressesFromUTXOs(List utxos) { - final Set uniqueAddresses = {}; - for (var utxo in utxos) { - if (utxo.address != null) { - uniqueAddresses.add(utxo.address!); - } - } - return uniqueAddresses.toList(); - } - - Future> getInscriptionDataFromAddress( - String address) async { - List allInscriptions = []; - try { - var inscriptions = await litescribeAPI.getInscriptionsByAddress(address); - allInscriptions.addAll(inscriptions); - } catch (e) { - throw Exception( - 'Error in OrdinalsInterface getInscriptionsByAddress: $e'); - } - return allInscriptions; - } - - Future> getInscriptionDataFromAddresses( + Future> _getInscriptionDataFromAddresses( List addresses) async { List allInscriptions = []; for (String address in addresses) { @@ -92,29 +109,29 @@ mixin OrdinalsInterface { return allInscriptions; } - Future> getOrdinalsFromAddress(String address) async { - try { - var inscriptions = await litescribeAPI.getInscriptionsByAddress(address); - return inscriptions - .map((data) => Ordinal.fromInscriptionData(data, _walletId)) - .toList(); - } catch (e) { - throw Exception('Error in OrdinalsInterface getOrdinalsFromAddress: $e'); - } - } - - Future> getOrdinalsFromAddresses(List addresses) async { - List allOrdinals = []; - for (String address in addresses) { - try { - var inscriptions = - await litescribeAPI.getInscriptionsByAddress(address); - allOrdinals.addAll(inscriptions - .map((data) => Ordinal.fromInscriptionData(data, _walletId))); - } catch (e) { - print("Error fetching inscriptions for address $address: $e"); - } - } - return allOrdinals; - } + // Future> getOrdinalsFromAddress(String address) async { + // try { + // var inscriptions = await litescribeAPI.getInscriptionsByAddress(address); + // return inscriptions + // .map((data) => Ordinal.fromInscriptionData(data, _walletId)) + // .toList(); + // } catch (e) { + // throw Exception('Error in OrdinalsInterface getOrdinalsFromAddress: $e'); + // } + // } + // + // Future> getOrdinalsFromAddresses(List addresses) async { + // List allOrdinals = []; + // for (String address in addresses) { + // try { + // var inscriptions = + // await litescribeAPI.getInscriptionsByAddress(address); + // allOrdinals.addAll(inscriptions + // .map((data) => Ordinal.fromInscriptionData(data, _walletId))); + // } catch (e) { + // print("Error fetching inscriptions for address $address: $e"); + // } + // } + // return allOrdinals; + // } } From c2df7ea1132f187b9f3cdaef1857efba224c9ae7 Mon Sep 17 00:00:00 2001 From: julian Date: Fri, 21 Jul 2023 19:05:19 -0600 Subject: [PATCH 091/169] wip desktop ordinals list --- lib/pages/ordinals/ordinals_view.dart | 8 +- lib/pages/ordinals/widgets/ordinals_list.dart | 10 +- .../ordinals/desktop_ordinals_view.dart | 250 +++++++++++------- 3 files changed, 172 insertions(+), 96 deletions(-) diff --git a/lib/pages/ordinals/ordinals_view.dart b/lib/pages/ordinals/ordinals_view.dart index 4e1e985dd..059c81c41 100644 --- a/lib/pages/ordinals/ordinals_view.dart +++ b/lib/pages/ordinals/ordinals_view.dart @@ -64,9 +64,6 @@ class _OrdinalsViewState extends ConsumerState { @override Widget build(BuildContext context) { - final manager = ref.watch(walletsChangeNotifierProvider - .select((value) => value.getManager(widget.walletId))); - return Background( child: SafeArea( child: Scaffold( @@ -98,7 +95,10 @@ class _OrdinalsViewState extends ConsumerState { await showLoading( whileFuture: Future.wait([ Future.delayed(const Duration(seconds: 2)), - (manager.wallet as OrdinalsInterface) + (ref + .read(walletsChangeNotifierProvider) + .getManager(widget.walletId) + .wallet as OrdinalsInterface) .refreshInscriptions() ]), context: context, diff --git a/lib/pages/ordinals/widgets/ordinals_list.dart b/lib/pages/ordinals/widgets/ordinals_list.dart index ad333d2ed..8244416b9 100644 --- a/lib/pages/ordinals/widgets/ordinals_list.dart +++ b/lib/pages/ordinals/widgets/ordinals_list.dart @@ -6,6 +6,7 @@ import 'package:isar/isar.dart'; import 'package:stackwallet/models/isar/ordinal.dart'; import 'package:stackwallet/pages/ordinals/widgets/ordinal_card.dart'; import 'package:stackwallet/providers/db/main_db_provider.dart'; +import 'package:stackwallet/themes/stack_colors.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/utilities/util.dart'; import 'package:stackwallet/widgets/rounded_white_container.dart'; @@ -23,7 +24,7 @@ class OrdinalsList extends ConsumerStatefulWidget { } class _OrdinalsListState extends ConsumerState { - static const double _spacing = 10.0; + final double _spacing = Util.isDesktop ? 16 : 10; late List _data; @@ -69,7 +70,12 @@ class _OrdinalsListState extends ConsumerState { child: Center( child: Text( "Your ordinals will appear here", - style: STextStyles.label(context), + style: Util.isDesktop + ? STextStyles.w500_14(context).copyWith( + color: Theme.of(context) + .extension()! + .textSubtitle1) + : STextStyles.label(context), ), ), ), diff --git a/lib/pages_desktop_specific/ordinals/desktop_ordinals_view.dart b/lib/pages_desktop_specific/ordinals/desktop_ordinals_view.dart index 0068e4033..9d4742db7 100644 --- a/lib/pages_desktop_specific/ordinals/desktop_ordinals_view.dart +++ b/lib/pages_desktop_specific/ordinals/desktop_ordinals_view.dart @@ -13,12 +13,17 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/svg.dart'; import 'package:stackwallet/pages/ordinals/widgets/ordinals_list.dart'; import 'package:stackwallet/providers/providers.dart'; +import 'package:stackwallet/services/mixins/ordinals_interface.dart'; +import 'package:stackwallet/themes/stack_colors.dart'; import 'package:stackwallet/utilities/assets.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/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/secondary_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'; @@ -42,7 +47,6 @@ class _DesktopOrdinals extends ConsumerState { late final FocusNode searchFocusNode; String _searchTerm = ""; - dynamic _manager; @override void initState() { @@ -52,14 +56,6 @@ class _DesktopOrdinals extends ConsumerState { super.initState(); } - @override - void didChangeDependencies() { - super.didChangeDependencies(); - // Set _manager here when the widget's dependencies change - _manager = ref.watch(walletsChangeNotifierProvider - .select((value) => value.getManager(widget.walletId))); - } - @override void dispose() { searchController.dispose(); @@ -74,97 +70,171 @@ class _DesktopOrdinals extends ConsumerState { return DesktopScaffold( appBar: DesktopAppBar( + background: Theme.of(context).extension()!.popupBG, isCompactHeight: true, - leading: Row( - children: [ - const SizedBox( - width: 24, - ), - Text( - "Ordinals", - style: STextStyles.desktopH3(context), - ) - ], + useSpacers: false, + leading: Expanded( + child: Row( + children: [ + const SizedBox( + width: 32, + ), + AppBarIconButton( + size: 32, + color: Theme.of(context) + .extension()! + .textFieldDefaultBG, + shadows: const [], + 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, + ), + SvgPicture.asset( + Assets.svg.ordinal, + width: 32, + height: 32, + ), + const SizedBox( + width: 12, + ), + Text( + "Ordinals", + style: STextStyles.desktopH3(context), + ) + ], + ), ), ), body: Padding( - padding: const EdgeInsets.only( - left: 24, - right: 24, - bottom: 24, - ), - child: Padding( - padding: const EdgeInsets.only( - left: 16, - right: 16, - top: 8, - ), - child: Column( - children: [ - ClipRRect( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), - child: TextField( - autocorrect: Util.isDesktop ? false : true, - enableSuggestions: Util.isDesktop ? false : true, - controller: searchController, - focusNode: searchFocusNode, - onChanged: (value) { - setState(() { - _searchTerm = value; - }); - }, - style: STextStyles.field(context), - decoration: standardInputDecoration( - "Search", - searchFocusNode, - context, - ).copyWith( - prefixIcon: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 10, - vertical: 16, - ), - child: SvgPicture.asset( - Assets.svg.search, - width: 16, - height: 16, + padding: const EdgeInsets.all(24), + child: Column( + children: [ + Row( + children: [ + Expanded( + child: ClipRRect( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + child: TextField( + autocorrect: Util.isDesktop ? false : true, + enableSuggestions: Util.isDesktop ? false : true, + controller: searchController, + focusNode: searchFocusNode, + onChanged: (value) { + setState(() { + _searchTerm = value; + }); + }, + style: STextStyles.field(context), + decoration: standardInputDecoration( + "Search", + searchFocusNode, + context, + ).copyWith( + prefixIcon: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 10, + vertical: 20, + ), + child: SvgPicture.asset( + Assets.svg.search, + width: 16, + height: 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 = ""; + _searchTerm = ""; + }); + }, + ), + ], + ), + ), + ) + : null, ), ), - 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 = ""; - _searchTerm = ""; - }); - }, - ), - ], - ), - ), - ) - : null, ), ), - ), - const SizedBox( - height: 16, - ), - Expanded( - child: OrdinalsList( - walletId: widget.walletId, + // const SizedBox( + // width: 16, + // ), + // SecondaryButton( + // width: 184, + // label: "Filter", + // buttonHeight: ButtonHeight.l, + // icon: SvgPicture.asset( + // Assets.svg.filter, + // color: Theme.of(context) + // .extension()! + // .buttonTextSecondary, + // ), + // onPressed: () { + // Navigator.of(context).pushNamed( + // OrdinalsFilterView.routeName, + // ); + // }, + // ), + const SizedBox( + width: 16, ), + SecondaryButton( + width: 184, + label: "Update", + buttonHeight: ButtonHeight.l, + icon: SvgPicture.asset( + Assets.svg.arrowRotate, + color: Theme.of(context) + .extension()! + .buttonTextSecondary, + ), + onPressed: () async { + // show loading for a minimum of 2 seconds on refreshing + await showLoading( + isDesktop: true, + whileFuture: Future.wait([ + Future.delayed(const Duration(seconds: 2)), + (ref + .read(walletsChangeNotifierProvider) + .getManager(widget.walletId) + .wallet as OrdinalsInterface) + .refreshInscriptions() + ]), + context: context, + message: "Refreshing..."); + }, + ), + ], + ), + const SizedBox( + height: 16, + ), + Expanded( + child: OrdinalsList( + walletId: widget.walletId, ), - ], - ), + ), + ], ), ), ); From 6c6c9c832516ea55bc5fab1ce7b486df6838d1a8 Mon Sep 17 00:00:00 2001 From: julian Date: Fri, 21 Jul 2023 19:15:26 -0600 Subject: [PATCH 092/169] desktop ordinals in wrap --- lib/pages/ordinals/widgets/ordinals_list.dart | 44 +++++++++++++------ .../ordinals/desktop_ordinals_view.dart | 7 ++- 2 files changed, 35 insertions(+), 16 deletions(-) diff --git a/lib/pages/ordinals/widgets/ordinals_list.dart b/lib/pages/ordinals/widgets/ordinals_list.dart index 8244416b9..481b0ef0a 100644 --- a/lib/pages/ordinals/widgets/ordinals_list.dart +++ b/lib/pages/ordinals/widgets/ordinals_list.dart @@ -83,20 +83,36 @@ class _OrdinalsListState extends ConsumerState { ); } - return GridView.builder( - shrinkWrap: true, - itemCount: _data.length, - gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( - crossAxisSpacing: _spacing, - mainAxisSpacing: _spacing, - crossAxisCount: Util.isDesktop ? 4 : 2, - childAspectRatio: 6 / 7, // was 3/4, less data displayed now - ), - itemBuilder: (_, i) => OrdinalCard( - walletId: widget.walletId, - ordinal: _data[i], - ), - ); + if (Util.isDesktop) { + return Wrap( + spacing: _spacing, + runSpacing: _spacing, + children: _data + .map((e) => SizedBox( + width: 220, + height: 270, + child: OrdinalCard( + walletId: widget.walletId, + ordinal: e, + ))) + .toList(), + ); + } else { + return GridView.builder( + shrinkWrap: true, + itemCount: _data.length, + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( + crossAxisSpacing: _spacing, + mainAxisSpacing: _spacing, + crossAxisCount: Util.isDesktop ? 4 : 2, + childAspectRatio: 6 / 7, // was 3/4, less data displayed now + ), + itemBuilder: (_, i) => OrdinalCard( + walletId: widget.walletId, + ordinal: _data[i], + ), + ); + } }, ); } diff --git a/lib/pages_desktop_specific/ordinals/desktop_ordinals_view.dart b/lib/pages_desktop_specific/ordinals/desktop_ordinals_view.dart index 9d4742db7..414ece75d 100644 --- a/lib/pages_desktop_specific/ordinals/desktop_ordinals_view.dart +++ b/lib/pages_desktop_specific/ordinals/desktop_ordinals_view.dart @@ -117,6 +117,7 @@ class _DesktopOrdinals extends ConsumerState { body: Padding( padding: const EdgeInsets.all(24), child: Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ @@ -230,8 +231,10 @@ class _DesktopOrdinals extends ConsumerState { height: 16, ), Expanded( - child: OrdinalsList( - walletId: widget.walletId, + child: SingleChildScrollView( + child: OrdinalsList( + walletId: widget.walletId, + ), ), ), ], From e559ee1cc5cde4d244665754ebc0eadff79c8d43 Mon Sep 17 00:00:00 2001 From: julian Date: Fri, 21 Jul 2023 19:52:43 -0600 Subject: [PATCH 093/169] WIP desktop ordinal details view --- lib/dto/ordinals/inscription_data.dart | 20 ++ lib/models/isar/ordinal.dart | 13 + lib/pages/ordinals/widgets/ordinal_card.dart | 6 +- .../desktop_ordinal_details_view.dart | 335 +++++++++--------- 4 files changed, 213 insertions(+), 161 deletions(-) diff --git a/lib/dto/ordinals/inscription_data.dart b/lib/dto/ordinals/inscription_data.dart index b7bba8697..2f12bd670 100644 --- a/lib/dto/ordinals/inscription_data.dart +++ b/lib/dto/ordinals/inscription_data.dart @@ -50,4 +50,24 @@ class InscriptionData { offset: json['offset'] as int, ); } + + @override + String toString() { + return 'InscriptionData {' + ' inscriptionId: $inscriptionId,' + ' inscriptionNumber: $inscriptionNumber,' + ' address: $address,' + ' preview: $preview,' + ' content: $content,' + ' contentLength: $contentLength,' + ' contentType: $contentType,' + ' contentBody: $contentBody,' + ' timestamp: $timestamp,' + ' genesisTransaction: $genesisTransaction,' + ' location: $location,' + ' output: $output,' + ' outputValue: $outputValue,' + ' offset: $offset' + ' }'; + } } diff --git a/lib/models/isar/ordinal.dart b/lib/models/isar/ordinal.dart index 0af1d1120..06ba52ef5 100644 --- a/lib/models/isar/ordinal.dart +++ b/lib/models/isar/ordinal.dart @@ -1,5 +1,7 @@ import 'package:isar/isar.dart'; +import 'package:stackwallet/db/isar/main_db.dart'; import 'package:stackwallet/dto/ordinals/inscription_data.dart'; +import 'package:stackwallet/models/isar/models/isar_models.dart'; part 'ordinal.g.dart'; @@ -62,6 +64,17 @@ class Ordinal { ); } + UTXO? getUTXO(MainDB db) { + return db.isar.utxos + .where() + .walletIdEqualTo(walletId) + .filter() + .txidEqualTo(utxoTXID) + .and() + .voutEqualTo(utxoVOUT) + .findFirstSync(); + } + @override String toString() { return 'Ordinal {' diff --git a/lib/pages/ordinals/widgets/ordinal_card.dart b/lib/pages/ordinals/widgets/ordinal_card.dart index e537611f8..c74366d74 100644 --- a/lib/pages/ordinals/widgets/ordinal_card.dart +++ b/lib/pages/ordinals/widgets/ordinal_card.dart @@ -1,8 +1,10 @@ import 'package:flutter/material.dart'; import 'package:stackwallet/models/isar/ordinal.dart'; import 'package:stackwallet/pages/ordinals/ordinal_details_view.dart'; +import 'package:stackwallet/pages_desktop_specific/ordinals/desktop_ordinal_details_view.dart'; import 'package:stackwallet/utilities/constants.dart'; import 'package:stackwallet/utilities/text_styles.dart'; +import 'package:stackwallet/utilities/util.dart'; import 'package:stackwallet/widgets/rounded_white_container.dart'; class OrdinalCard extends StatelessWidget { @@ -21,7 +23,9 @@ class OrdinalCard extends StatelessWidget { radiusMultiplier: 2, onPressed: () { Navigator.of(context).pushNamed( - OrdinalDetailsView.routeName, + Util.isDesktop + ? DesktopOrdinalDetailsView.routeName + : OrdinalDetailsView.routeName, arguments: (walletId: walletId, ordinal: ordinal), ); }, diff --git a/lib/pages_desktop_specific/ordinals/desktop_ordinal_details_view.dart b/lib/pages_desktop_specific/ordinals/desktop_ordinal_details_view.dart index a477f6228..27c651213 100644 --- a/lib/pages_desktop_specific/ordinals/desktop_ordinal_details_view.dart +++ b/lib/pages_desktop_specific/ordinals/desktop_ordinal_details_view.dart @@ -2,12 +2,20 @@ import 'dart:async'; 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/blockchain_data/utxo.dart'; import 'package:stackwallet/models/isar/ordinal.dart'; import 'package:stackwallet/notifications/show_flush_bar.dart'; import 'package:stackwallet/pages/ordinals/widgets/dialogs.dart'; +import 'package:stackwallet/providers/db/main_db_provider.dart'; +import 'package:stackwallet/providers/global/wallets_provider.dart'; import 'package:stackwallet/themes/stack_colors.dart'; +import 'package:stackwallet/utilities/amount/amount.dart'; +import 'package:stackwallet/utilities/amount/amount_formatter.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/widgets/custom_buttons/app_bar_icon_button.dart'; import 'package:stackwallet/widgets/desktop/desktop_app_bar.dart'; @@ -16,7 +24,7 @@ import 'package:stackwallet/widgets/desktop/primary_button.dart'; import 'package:stackwallet/widgets/desktop/secondary_button.dart'; import 'package:stackwallet/widgets/rounded_white_container.dart'; -class DesktopOrdinalDetailsView extends StatefulWidget { +class DesktopOrdinalDetailsView extends ConsumerStatefulWidget { const DesktopOrdinalDetailsView({ Key? key, required this.walletId, @@ -29,15 +37,27 @@ class DesktopOrdinalDetailsView extends StatefulWidget { static const routeName = "/desktopOrdinalDetailsView"; @override - State createState() => + ConsumerState createState() => _DesktopOrdinalDetailsViewState(); } -class _DesktopOrdinalDetailsViewState extends State { +class _DesktopOrdinalDetailsViewState + extends ConsumerState { static const _spacing = 12.0; + late final UTXO? utxo; + + @override + void initState() { + utxo = widget.ordinal.getUTXO(ref.read(mainDBProvider)); + super.initState(); + } + @override Widget build(BuildContext context) { + final coin = ref.watch(walletsChangeNotifierProvider + .select((value) => value.getManager(widget.walletId).coin)); + return DesktopScaffold( appBar: DesktopAppBar( background: Theme.of(context).extension()!.popupBG, @@ -67,18 +87,8 @@ class _DesktopOrdinalDetailsViewState extends State { const SizedBox( width: 18, ), - SvgPicture.asset( - Assets.svg.ordinal, - width: 32, - height: 32, - color: - Theme.of(context).extension()!.textSubtitle1, - ), - const SizedBox( - width: 12, - ), Text( - "Ordinals", + "Ordinal details", style: STextStyles.desktopH3(context), ), ], @@ -87,62 +97,161 @@ class _DesktopOrdinalDetailsViewState extends State { useSpacers: false, isCompactHeight: true, ), - body: SingleChildScrollView( - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 16), - child: Column( - children: [ - Padding( - padding: const EdgeInsets.symmetric( - vertical: 12, - horizontal: 39, + body: Padding( + padding: const EdgeInsets.only( + left: 24, + top: 24, + right: 24, + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox( + width: 300, + height: 300, + child: ClipRRect( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, ), - child: _OrdinalImageGroup( - ordinal: widget.ordinal, - walletId: widget.walletId, + child: Image.network( + widget.ordinal + .content, // Use the preview URL as the image source + fit: BoxFit.cover, + filterQuality: + FilterQuality.none, // Set the filter mode to nearest ), ), - _DetailsItemWCopy( - title: "Inscription number", - data: widget.ordinal.inscriptionNumber.toString(), + ), + const SizedBox( + width: 16, + ), + Expanded( + child: SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Column( + children: [ + RoundedWhiteContainer( + padding: const EdgeInsets.all(16), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "INSC. ${widget.ordinal.inscriptionNumber}", + style: STextStyles.w600_20(context), + ), + ], + ), + ), + const SizedBox( + width: 16, + ), + PrimaryButton( + width: 150, + label: "Send", + icon: SvgPicture.asset( + Assets.svg.send, + width: 18, + height: 18, + color: Theme.of(context) + .extension()! + .buttonTextPrimary, + ), + buttonHeight: ButtonHeight.l, + iconSpacing: 8, + onPressed: () async { + final response = await showDialog( + context: context, + builder: (_) => + const SendOrdinalUnfreezeDialog(), + ); + if (response == "unfreeze") { + // TODO: unfreeze and go to send ord screen + } + }, + ), + const SizedBox( + width: 16, + ), + SecondaryButton( + width: 150, + label: "Download", + icon: SvgPicture.asset( + Assets.svg.arrowDown, + width: 13, + height: 18, + color: Theme.of(context) + .extension()! + .buttonTextSecondary, + ), + buttonHeight: ButtonHeight.l, + iconSpacing: 8, + onPressed: () { + // TODO: save and download image to device + }, + ), + ], + ), + ), + const SizedBox( + height: 16, + ), + _DetailsItemWCopy( + title: "Inscription number", + data: widget.ordinal.inscriptionNumber.toString(), + ), + const SizedBox( + height: _spacing, + ), + _DetailsItemWCopy( + title: "Inscription ID", + data: widget.ordinal.inscriptionId, + ), + const SizedBox( + height: _spacing, + ), + // todo: add utxo status + const SizedBox( + height: _spacing, + ), + _DetailsItemWCopy( + title: "Amount", + data: utxo == null + ? "ERROR" + : ref.watch(pAmountFormatter(coin)).format( + Amount( + rawValue: BigInt.from(utxo!.value), + fractionDigits: coin.decimals, + ), + ), + ), + const SizedBox( + height: _spacing, + ), + _DetailsItemWCopy( + title: "Owner address", + data: utxo?.address ?? "ERROR", + ), + const SizedBox( + height: _spacing, + ), + _DetailsItemWCopy( + title: "Transaction ID", + data: widget.ordinal.utxoTXID, + ), + const SizedBox( + height: _spacing, + ), + ], + ), + ), ), - const SizedBox( - height: _spacing, - ), - _DetailsItemWCopy( - title: "ID", - data: widget.ordinal.inscriptionId, - ), - const SizedBox( - height: _spacing, - ), - // todo: add utxo status - const SizedBox( - height: _spacing, - ), - const _DetailsItemWCopy( - title: "Amount", - data: "TODO", // TODO infer from utxo utxoTXID:utxoVOUT - ), - const SizedBox( - height: _spacing, - ), - const _DetailsItemWCopy( - title: "Owner address", - data: "TODO", // infer from address associated w utxoTXID - ), - const SizedBox( - height: _spacing, - ), - _DetailsItemWCopy( - title: "Transaction ID", - data: widget.ordinal.utxoTXID, - ), - const SizedBox( - height: _spacing, - ), - ], - ), + ), + ], ), ), ); @@ -206,97 +315,3 @@ class _DetailsItemWCopy extends StatelessWidget { ); } } - -class _OrdinalImageGroup extends StatelessWidget { - const _OrdinalImageGroup({ - Key? key, - required this.walletId, - required this.ordinal, - }) : super(key: key); - - final String walletId; - final Ordinal ordinal; - - static const _spacing = 12.0; - - @override - Widget build(BuildContext context) { - return Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - // Text( - // "${ordinal.inscriptionId}", // Use any other property you want - // style: STextStyles.w600_16(context), - // ), - // const SizedBox( - // height: _spacing, - // ), - AspectRatio( - aspectRatio: 1, - child: Container( - color: Colors.red, - child: Image.network( - ordinal.content, // Use the preview URL as the image source - fit: BoxFit.cover, - filterQuality: - FilterQuality.none, // Set the filter mode to nearest - ), - ), - ), - const SizedBox( - height: _spacing, - ), - Row( - children: [ - Expanded( - child: SecondaryButton( - label: "Download", - icon: SvgPicture.asset( - Assets.svg.arrowDown, - width: 10, - height: 12, - color: Theme.of(context) - .extension()! - .buttonTextSecondary, - ), - buttonHeight: ButtonHeight.l, - iconSpacing: 4, - onPressed: () { - // TODO: save and download image to device - }, - ), - ), - const SizedBox( - width: _spacing, - ), - Expanded( - child: PrimaryButton( - label: "Send", - icon: SvgPicture.asset( - Assets.svg.send, - width: 10, - height: 10, - color: Theme.of(context) - .extension()! - .buttonTextPrimary, - ), - buttonHeight: ButtonHeight.l, - iconSpacing: 4, - onPressed: () async { - final response = await showDialog( - context: context, - builder: (_) => const SendOrdinalUnfreezeDialog(), - ); - if (response == "unfreeze") { - // TODO: unfreeze and go to send ord screen - } - }, - ), - ), - ], - ), - ], - ); - } -} From 55e55ef8c887b493e717409d525e8ff435634083 Mon Sep 17 00:00:00 2001 From: sneurlax Date: Sat, 22 Jul 2023 21:09:42 -0500 Subject: [PATCH 094/169] print->throw Exception --- lib/services/mixins/ordinals_interface.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/services/mixins/ordinals_interface.dart b/lib/services/mixins/ordinals_interface.dart index 51a82307a..88a416a1c 100644 --- a/lib/services/mixins/ordinals_interface.dart +++ b/lib/services/mixins/ordinals_interface.dart @@ -103,7 +103,7 @@ mixin OrdinalsInterface { await litescribeAPI.getInscriptionsByAddress(address); allInscriptions.addAll(inscriptions); } catch (e) { - print("Error fetching inscriptions for address $address: $e"); + throw Exception("Error fetching inscriptions for address $address: $e"); } } return allInscriptions; From 475490d4f0ea0f72f1c3ad8a0aea1924a43ce8f3 Mon Sep 17 00:00:00 2001 From: sneurlax Date: Sat, 22 Jul 2023 21:10:22 -0500 Subject: [PATCH 095/169] remove commented functions --- lib/services/mixins/ordinals_interface.dart | 69 --------------------- 1 file changed, 69 deletions(-) diff --git a/lib/services/mixins/ordinals_interface.dart b/lib/services/mixins/ordinals_interface.dart index 88a416a1c..0dfe2a782 100644 --- a/lib/services/mixins/ordinals_interface.dart +++ b/lib/services/mixins/ordinals_interface.dart @@ -50,49 +50,6 @@ mixin OrdinalsInterface { await _db.isar.ordinals.putAll(ords); }); } - // - // Future> getInscriptionData() async { - // try { - // final utxos = await _db.getUTXOs(_walletId).findAll(); - // final uniqueAddresses = getUniqueAddressesFromUTXOs(utxos); - // return await _getInscriptionDataFromAddresses(uniqueAddresses); - // } catch (e) { - // throw Exception('Error in OrdinalsInterface getInscriptions: $e'); - // } - // } - // - // Future> getOrdinals() async { - // try { - // final utxos = await _db.getUTXOs(_walletId).findAll(); - // final uniqueAddresses = getUniqueAddressesFromUTXOs(utxos); - // return await getOrdinalsFromAddresses(uniqueAddresses); - // } catch (e) { - // throw Exception('Error in OrdinalsInterface getOrdinals: $e'); - // } - // } - // - // List getUniqueAddressesFromUTXOs(List utxos) { - // final Set uniqueAddresses = {}; - // for (var utxo in utxos) { - // if (utxo.address != null) { - // uniqueAddresses.add(utxo.address!); - // } - // } - // return uniqueAddresses.toList(); - // } - // - // Future> getInscriptionDataFromAddress( - // String address) async { - // List allInscriptions = []; - // try { - // var inscriptions = await litescribeAPI.getInscriptionsByAddress(address); - // allInscriptions.addAll(inscriptions); - // } catch (e) { - // throw Exception( - // 'Error in OrdinalsInterface getInscriptionsByAddress: $e'); - // } - // return allInscriptions; - // } Future> _getInscriptionDataFromAddresses( List addresses) async { @@ -108,30 +65,4 @@ mixin OrdinalsInterface { } return allInscriptions; } - - // Future> getOrdinalsFromAddress(String address) async { - // try { - // var inscriptions = await litescribeAPI.getInscriptionsByAddress(address); - // return inscriptions - // .map((data) => Ordinal.fromInscriptionData(data, _walletId)) - // .toList(); - // } catch (e) { - // throw Exception('Error in OrdinalsInterface getOrdinalsFromAddress: $e'); - // } - // } - // - // Future> getOrdinalsFromAddresses(List addresses) async { - // List allOrdinals = []; - // for (String address in addresses) { - // try { - // var inscriptions = - // await litescribeAPI.getInscriptionsByAddress(address); - // allOrdinals.addAll(inscriptions - // .map((data) => Ordinal.fromInscriptionData(data, _walletId))); - // } catch (e) { - // print("Error fetching inscriptions for address $address: $e"); - // } - // } - // return allOrdinals; - // } } From 58c3bdbf2be041296809f4b7f17c742d91cdded2 Mon Sep 17 00:00:00 2001 From: sneurlax Date: Sat, 22 Jul 2023 21:50:29 -0500 Subject: [PATCH 096/169] don't flash red on load --- lib/pages/ordinals/ordinal_details_view.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pages/ordinals/ordinal_details_view.dart b/lib/pages/ordinals/ordinal_details_view.dart index 0bbcbe37a..d54f1e24c 100644 --- a/lib/pages/ordinals/ordinal_details_view.dart +++ b/lib/pages/ordinals/ordinal_details_view.dart @@ -200,7 +200,7 @@ class _OrdinalImageGroup extends StatelessWidget { AspectRatio( aspectRatio: 1, child: Container( - color: Colors.red, + color: Colors.transparent, child: Image.network( ordinal.content, // Use the preview URL as the image source fit: BoxFit.cover, From 8e5f2d190d81b68d01ec2f477e1e1e42857cbb4c Mon Sep 17 00:00:00 2001 From: sneurlax Date: Sat, 22 Jul 2023 21:50:49 -0500 Subject: [PATCH 097/169] temporarily disable send button --- lib/pages/ordinals/ordinal_details_view.dart | 54 +++++++++--------- .../desktop_ordinal_details_view.dart | 56 +++++++++---------- 2 files changed, 54 insertions(+), 56 deletions(-) diff --git a/lib/pages/ordinals/ordinal_details_view.dart b/lib/pages/ordinals/ordinal_details_view.dart index d54f1e24c..ced80c42c 100644 --- a/lib/pages/ordinals/ordinal_details_view.dart +++ b/lib/pages/ordinals/ordinal_details_view.dart @@ -232,33 +232,33 @@ class _OrdinalImageGroup extends StatelessWidget { }, ), ), - const SizedBox( - width: _spacing, - ), - Expanded( - child: PrimaryButton( - label: "Send", - icon: SvgPicture.asset( - Assets.svg.send, - width: 10, - height: 10, - color: Theme.of(context) - .extension()! - .buttonTextPrimary, - ), - buttonHeight: ButtonHeight.l, - iconSpacing: 4, - onPressed: () async { - final response = await showDialog( - context: context, - builder: (_) => const SendOrdinalUnfreezeDialog(), - ); - if (response == "unfreeze") { - // TODO: unfreeze and go to send ord screen - } - }, - ), - ), + // const SizedBox( + // width: _spacing, + // ), + // Expanded( + // child: PrimaryButton( + // label: "Send", + // icon: SvgPicture.asset( + // Assets.svg.send, + // width: 10, + // height: 10, + // color: Theme.of(context) + // .extension()! + // .buttonTextPrimary, + // ), + // buttonHeight: ButtonHeight.l, + // iconSpacing: 4, + // onPressed: () async { + // final response = await showDialog( + // context: context, + // builder: (_) => const SendOrdinalUnfreezeDialog(), + // ); + // if (response == "unfreeze") { + // // TODO: unfreeze and go to send ord screen + // } + // }, + // ), + // ), ], ), ], diff --git a/lib/pages_desktop_specific/ordinals/desktop_ordinal_details_view.dart b/lib/pages_desktop_specific/ordinals/desktop_ordinal_details_view.dart index 27c651213..cb76e036d 100644 --- a/lib/pages_desktop_specific/ordinals/desktop_ordinal_details_view.dart +++ b/lib/pages_desktop_specific/ordinals/desktop_ordinal_details_view.dart @@ -7,7 +7,6 @@ import 'package:flutter_svg/flutter_svg.dart'; import 'package:stackwallet/models/isar/models/blockchain_data/utxo.dart'; import 'package:stackwallet/models/isar/ordinal.dart'; import 'package:stackwallet/notifications/show_flush_bar.dart'; -import 'package:stackwallet/pages/ordinals/widgets/dialogs.dart'; import 'package:stackwallet/providers/db/main_db_provider.dart'; import 'package:stackwallet/providers/global/wallets_provider.dart'; import 'package:stackwallet/themes/stack_colors.dart'; @@ -20,7 +19,6 @@ import 'package:stackwallet/utilities/text_styles.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/desktop/secondary_button.dart'; import 'package:stackwallet/widgets/rounded_white_container.dart'; @@ -150,33 +148,33 @@ class _DesktopOrdinalDetailsViewState const SizedBox( width: 16, ), - PrimaryButton( - width: 150, - label: "Send", - icon: SvgPicture.asset( - Assets.svg.send, - width: 18, - height: 18, - color: Theme.of(context) - .extension()! - .buttonTextPrimary, - ), - buttonHeight: ButtonHeight.l, - iconSpacing: 8, - onPressed: () async { - final response = await showDialog( - context: context, - builder: (_) => - const SendOrdinalUnfreezeDialog(), - ); - if (response == "unfreeze") { - // TODO: unfreeze and go to send ord screen - } - }, - ), - const SizedBox( - width: 16, - ), + // PrimaryButton( + // width: 150, + // label: "Send", + // icon: SvgPicture.asset( + // Assets.svg.send, + // width: 18, + // height: 18, + // color: Theme.of(context) + // .extension()! + // .buttonTextPrimary, + // ), + // buttonHeight: ButtonHeight.l, + // iconSpacing: 8, + // onPressed: () async { + // final response = await showDialog( + // context: context, + // builder: (_) => + // const SendOrdinalUnfreezeDialog(), + // ); + // if (response == "unfreeze") { + // // TODO: unfreeze and go to send ord screen + // } + // }, + // ), + // const SizedBox( + // width: 16, + // ), SecondaryButton( width: 150, label: "Download", From 7da49c7ea0d0c4c6b437c2b9e55f6a243ab1506c Mon Sep 17 00:00:00 2001 From: sneurlax Date: Sat, 22 Jul 2023 22:14:18 -0500 Subject: [PATCH 098/169] improve ordinal heuristic and formatting --- .../coins/litecoin/litecoin_wallet.dart | 29 +++++++++++++------ lib/services/mixins/ordinals_interface.dart | 26 +++++++++++++++++ 2 files changed, 46 insertions(+), 9 deletions(-) diff --git a/lib/services/coins/litecoin/litecoin_wallet.dart b/lib/services/coins/litecoin/litecoin_wallet.dart index 944182abd..5ea1e67d9 100644 --- a/lib/services/coins/litecoin/litecoin_wallet.dart +++ b/lib/services/coins/litecoin/litecoin_wallet.dart @@ -136,7 +136,7 @@ class LitecoinWallet extends CoinServiceAPI _secureStore = secureStore; initCache(walletId, coin); initWalletDB(mockableOverride: mockableOverride); - initOrdinalsInterface(walletId:walletId, coin: coin, db: db); + initOrdinalsInterface(walletId: walletId, coin: coin, db: db); initCoinControlInterface( walletId: walletId, walletName: walletName, @@ -1871,14 +1871,6 @@ class LitecoinWallet extends CoinServiceAPI String? blockReason; String? label; - final utxoAmount = jsonUTXO["value"] as int; - - if (utxoAmount <= 10000) { - shouldBlock = true; - blockReason = "May contain ordinal"; - label = "Possible ordinal"; - } - final vout = jsonUTXO["tx_pos"] as int; final outputs = txn["vout"] as List; @@ -1893,6 +1885,25 @@ class LitecoinWallet extends CoinServiceAPI } } + final utxoAmount = jsonUTXO["value"] as int; + + // TODO check the specific output, not just the address in general + // TODO optimize by querying the litescribe API for all addresses at once, instead of one API call per output + if (utxoOwnerAddress != null) { + if (await inscriptionInAddress(utxoOwnerAddress!)) { + shouldBlock = true; + blockReason = "Ordinal"; + label = "Ordinal detected at address"; + } + } else { + // TODO implement inscriptionInOutput + if (utxoAmount <= 10000) { + shouldBlock = true; + blockReason = "May contain ordinal"; + label = "Possible ordinal"; + } + } + final utxo = isar_models.UTXO( walletId: walletId, txid: txn["txid"] as String, diff --git a/lib/services/mixins/ordinals_interface.dart b/lib/services/mixins/ordinals_interface.dart index 0dfe2a782..5b17f3b48 100644 --- a/lib/services/mixins/ordinals_interface.dart +++ b/lib/services/mixins/ordinals_interface.dart @@ -65,4 +65,30 @@ mixin OrdinalsInterface { } return allInscriptions; } + + // check if an inscription is in a given output + Future inscriptionInOutput(UTXO output) async { + if (output.address != null) { + var inscriptions = + await litescribeAPI.getInscriptionsByAddress("${output.address}"); + if (inscriptions.isNotEmpty) { + return true; + } else { + return false; + } + } else { + throw UnimplementedError( + 'TODO look up utxo without address. utxo->txid:output->address'); + } + } + + // check if an inscription is in a given output + Future inscriptionInAddress(String address) async { + var inscriptions = await litescribeAPI.getInscriptionsByAddress(address); + if (inscriptions.isNotEmpty) { + return true; + } else { + return false; + } + } } From f32359ac1e8d96e97e4e0a186f50551373b6a4d8 Mon Sep 17 00:00:00 2001 From: sneurlax Date: Sun, 23 Jul 2023 18:01:51 -0500 Subject: [PATCH 099/169] refresh inscriptions after general refresh if any are detected --- lib/services/coins/litecoin/litecoin_wallet.dart | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/lib/services/coins/litecoin/litecoin_wallet.dart b/lib/services/coins/litecoin/litecoin_wallet.dart index 5ea1e67d9..4698883f8 100644 --- a/lib/services/coins/litecoin/litecoin_wallet.dart +++ b/lib/services/coins/litecoin/litecoin_wallet.dart @@ -1856,6 +1856,7 @@ class LitecoinWallet extends CoinServiceAPI } final List outputArray = []; + bool inscriptionsRefreshNeeded = false; for (int i = 0; i < fetchedUtxoList.length; i++) { for (int j = 0; j < fetchedUtxoList[i].length; j++) { @@ -1890,10 +1891,12 @@ class LitecoinWallet extends CoinServiceAPI // TODO check the specific output, not just the address in general // TODO optimize by querying the litescribe API for all addresses at once, instead of one API call per output if (utxoOwnerAddress != null) { + // TODO add inscription to database if (await inscriptionInAddress(utxoOwnerAddress!)) { shouldBlock = true; blockReason = "Ordinal"; label = "Ordinal detected at address"; + inscriptionsRefreshNeeded = true; } } else { // TODO implement inscriptionInOutput @@ -1901,6 +1904,7 @@ class LitecoinWallet extends CoinServiceAPI shouldBlock = true; blockReason = "May contain ordinal"; label = "Possible ordinal"; + inscriptionsRefreshNeeded = true; } } @@ -1923,6 +1927,10 @@ class LitecoinWallet extends CoinServiceAPI } } + if (inscriptionsRefreshNeeded) { + await refreshInscriptions(); + } + Logging.instance.log( 'Outputs fetched: $outputArray', level: LogLevel.Info, From abd4bf9d7a7f5f592a0148835e65fbe5f64afe2d Mon Sep 17 00:00:00 2001 From: sneurlax Date: Sun, 23 Jul 2023 18:19:38 -0500 Subject: [PATCH 100/169] refresh inscriptions if needed and add utility func --- lib/services/coins/litecoin/litecoin_wallet.dart | 10 +++++----- lib/services/mixins/ordinals_interface.dart | 10 ++++++++++ 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/lib/services/coins/litecoin/litecoin_wallet.dart b/lib/services/coins/litecoin/litecoin_wallet.dart index 4698883f8..ff36bc5c6 100644 --- a/lib/services/coins/litecoin/litecoin_wallet.dart +++ b/lib/services/coins/litecoin/litecoin_wallet.dart @@ -1891,7 +1891,6 @@ class LitecoinWallet extends CoinServiceAPI // TODO check the specific output, not just the address in general // TODO optimize by querying the litescribe API for all addresses at once, instead of one API call per output if (utxoOwnerAddress != null) { - // TODO add inscription to database if (await inscriptionInAddress(utxoOwnerAddress!)) { shouldBlock = true; blockReason = "Ordinal"; @@ -1927,15 +1926,16 @@ class LitecoinWallet extends CoinServiceAPI } } - if (inscriptionsRefreshNeeded) { - await refreshInscriptions(); - } - Logging.instance.log( 'Outputs fetched: $outputArray', level: LogLevel.Info, ); + // TODO replace with refreshInscriptions if outputs are changed + if (inscriptionsRefreshNeeded) { + await refreshInscriptions(); + } + await db.updateUTXOs(walletId, outputArray); // finally update balance diff --git a/lib/services/mixins/ordinals_interface.dart b/lib/services/mixins/ordinals_interface.dart index 5b17f3b48..f25508517 100644 --- a/lib/services/mixins/ordinals_interface.dart +++ b/lib/services/mixins/ordinals_interface.dart @@ -91,4 +91,14 @@ mixin OrdinalsInterface { return false; } } + + // check if an inscription is in a given output + Future inscriptionInAddresses(List addresses) async { + var inscriptions = await _getInscriptionDataFromAddresses(addresses); + if (inscriptions.isNotEmpty) { + return true; + } else { + return false; + } + } } From 18a0bf279ef451b9036d4a538a310cbf7f4cd5f3 Mon Sep 17 00:00:00 2001 From: sneurlax Date: Sun, 23 Jul 2023 18:29:42 -0500 Subject: [PATCH 101/169] refresh inscriptions if new output detected --- lib/db/isar/main_db.dart | 7 ++++++- lib/services/coins/litecoin/litecoin_wallet.dart | 11 ++++------- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/lib/db/isar/main_db.dart b/lib/db/isar/main_db.dart index 9d445aebb..80ecde478 100644 --- a/lib/db/isar/main_db.dart +++ b/lib/db/isar/main_db.dart @@ -248,7 +248,8 @@ class MainDB { await isar.utxos.putAll(utxos); }); - Future updateUTXOs(String walletId, List utxos) async { + Future updateUTXOs(String walletId, List utxos) async { + bool newUTXO = false; await isar.writeTxn(() async { final set = utxos.toSet(); for (final utxo in utxos) { @@ -270,12 +271,16 @@ class MainDB { blockHash: utxo.blockHash, ), ); + } else { + newUTXO = true; } } await isar.utxos.where().walletIdEqualTo(walletId).deleteAll(); await isar.utxos.putAll(set.toList()); }); + + return newUTXO; } Stream watchUTXO({ diff --git a/lib/services/coins/litecoin/litecoin_wallet.dart b/lib/services/coins/litecoin/litecoin_wallet.dart index ff36bc5c6..703ec5cf6 100644 --- a/lib/services/coins/litecoin/litecoin_wallet.dart +++ b/lib/services/coins/litecoin/litecoin_wallet.dart @@ -1856,7 +1856,6 @@ class LitecoinWallet extends CoinServiceAPI } final List outputArray = []; - bool inscriptionsRefreshNeeded = false; for (int i = 0; i < fetchedUtxoList.length; i++) { for (int j = 0; j < fetchedUtxoList[i].length; j++) { @@ -1889,13 +1888,12 @@ class LitecoinWallet extends CoinServiceAPI final utxoAmount = jsonUTXO["value"] as int; // TODO check the specific output, not just the address in general - // TODO optimize by querying the litescribe API for all addresses at once, instead of one API call per output + // TODO optimize by freezing output in OrdinalsInterface, so one ordinal API calls is made (or at least many less) if (utxoOwnerAddress != null) { if (await inscriptionInAddress(utxoOwnerAddress!)) { shouldBlock = true; blockReason = "Ordinal"; label = "Ordinal detected at address"; - inscriptionsRefreshNeeded = true; } } else { // TODO implement inscriptionInOutput @@ -1903,7 +1901,6 @@ class LitecoinWallet extends CoinServiceAPI shouldBlock = true; blockReason = "May contain ordinal"; label = "Possible ordinal"; - inscriptionsRefreshNeeded = true; } } @@ -1931,13 +1928,13 @@ class LitecoinWallet extends CoinServiceAPI level: LogLevel.Info, ); - // TODO replace with refreshInscriptions if outputs are changed + bool inscriptionsRefreshNeeded = + await db.updateUTXOs(walletId, outputArray); + if (inscriptionsRefreshNeeded) { await refreshInscriptions(); } - await db.updateUTXOs(walletId, outputArray); - // finally update balance await _updateBalance(); } catch (e, s) { From 38c2415a61c7a4883d5e2e24f971b18676eb448a Mon Sep 17 00:00:00 2001 From: sneurlax Date: Sun, 23 Jul 2023 23:16:43 -0500 Subject: [PATCH 102/169] change function signature of test to match inscription-refresh hackfix --- test/widget_tests/transaction_card_test.mocks.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/widget_tests/transaction_card_test.mocks.dart b/test/widget_tests/transaction_card_test.mocks.dart index 83c8e8c82..667ef211a 100644 --- a/test/widget_tests/transaction_card_test.mocks.dart +++ b/test/widget_tests/transaction_card_test.mocks.dart @@ -3266,7 +3266,7 @@ class MockMainDB extends _i1.Mock implements _i14.MainDB { returnValueForMissingStub: _i19.Future.value(), ) as _i19.Future); @override - _i19.Future updateUTXOs( + _i19.Future updateUTXOs( String? walletId, List<_i22.UTXO>? utxos, ) => @@ -3280,7 +3280,7 @@ class MockMainDB extends _i1.Mock implements _i14.MainDB { ), returnValue: _i19.Future.value(), returnValueForMissingStub: _i19.Future.value(), - ) as _i19.Future); + ) as _i19.Future); @override _i19.Stream<_i22.UTXO?> watchUTXO({ required int? id, From ce1bd8212fc4326ad580420475a9b27e81d4e14c Mon Sep 17 00:00:00 2001 From: sneurlax Date: Sun, 23 Jul 2023 23:18:23 -0500 Subject: [PATCH 103/169] remove Future inscriptionInAddresses(List addresses) inscriptionInAddress is next, after its usage is removed from _updateUTXOs, updateUTXOs (or another hook for detecting if a refresh has detected a new output) will trigger the more general refreshInscriptions --- lib/services/mixins/ordinals_interface.dart | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/lib/services/mixins/ordinals_interface.dart b/lib/services/mixins/ordinals_interface.dart index f25508517..5b17f3b48 100644 --- a/lib/services/mixins/ordinals_interface.dart +++ b/lib/services/mixins/ordinals_interface.dart @@ -91,14 +91,4 @@ mixin OrdinalsInterface { return false; } } - - // check if an inscription is in a given output - Future inscriptionInAddresses(List addresses) async { - var inscriptions = await _getInscriptionDataFromAddresses(addresses); - if (inscriptions.isNotEmpty) { - return true; - } else { - return false; - } - } } From 6ff4c5d49aa50359f41998b24269ec40e9c70dcb Mon Sep 17 00:00:00 2001 From: julian Date: Mon, 24 Jul 2023 09:06:10 -0600 Subject: [PATCH 104/169] clean up --- lib/services/coins/firo/firo_wallet.dart | 64 ------------------------ 1 file changed, 64 deletions(-) diff --git a/lib/services/coins/firo/firo_wallet.dart b/lib/services/coins/firo/firo_wallet.dart index e62b7d069..3527c9802 100644 --- a/lib/services/coins/firo/firo_wallet.dart +++ b/lib/services/coins/firo/firo_wallet.dart @@ -3133,17 +3133,6 @@ class FiroWallet extends CoinServiceAPI txnsData.add(Tuple2(transaction, transactionAddress)); await db.addNewTransactionData(txnsData, walletId); - - // final models.TransactionData newTxData = - // models.TransactionData.fromMap(transactions); - // await DB.instance.put( - // boxName: walletId, - // key: 'latest_lelantus_tx_model', - // value: newTxData); - // final ldata = DB.instance.get( - // boxName: walletId, - // key: 'latest_lelantus_tx_model') as models.TransactionData; - // _lelantusTransactionData = Future(() => ldata); } else { // This is a mint Logging.instance.log("this is a mint", level: LogLevel.Info); @@ -3204,20 +3193,6 @@ class FiroWallet extends CoinServiceAPI Logging.instance.log("fetched fees: $feeObject", level: LogLevel.Info); return feeObject; - - // final result = await electrumXClient.getFeeRate(); - // - // final locale = await Devicelocale.currentLocale; - // final String fee = - // Format.satoshiAmountToPrettyString(result["rate"] as int, locale!); - // - // final fees = { - // "fast": fee, - // "average": fee, - // "slow": fee, - // }; - // final FeeObject feeObject = FeeObject.fromJson(fees); - // return feeObject; } catch (e) { Logging.instance .log("Exception rethrown from _getFees(): $e", level: LogLevel.Error); @@ -4072,45 +4047,6 @@ class FiroWallet extends CoinServiceAPI ); } - // /// Takes in a list of isar_models.UTXOs and adds a name (dependent on object index within list) - // /// and checks for the txid associated with the utxo being blocked and marks it accordingly. - // /// Now also checks for output labeling. - // Future _sortOutputs(List utxos) async { - // final blockedHashArray = - // DB.instance.get(boxName: walletId, key: 'blocked_tx_hashes') - // as List?; - // final List lst = []; - // if (blockedHashArray != null) { - // for (var hash in blockedHashArray) { - // lst.add(hash as String); - // } - // } - // final labels = - // DB.instance.get(boxName: walletId, key: 'labels') as Map? ?? - // {}; - // - // _outputsList = []; - // - // for (var i = 0; i < utxos.length; i++) { - // if (labels[utxos[i].txid] != null) { - // utxos[i].txName = labels[utxos[i].txid] as String? ?? ""; - // } else { - // utxos[i].txName = 'Output #$i'; - // } - // - // if (utxos[i].status.confirmed == false) { - // _outputsList.add(utxos[i]); - // } else { - // if (lst.contains(utxos[i].txid)) { - // utxos[i].blocked = true; - // _outputsList.add(utxos[i]); - // } else if (!lst.contains(utxos[i].txid)) { - // _outputsList.add(utxos[i]); - // } - // } - // } - // } - @override Future fullRescan( int maxUnusedAddressGap, From baa34ca9f2cc4c9165073680930be8551a0fd905 Mon Sep 17 00:00:00 2001 From: julian Date: Mon, 24 Jul 2023 09:06:48 -0600 Subject: [PATCH 105/169] isar lelantus coin schema --- lib/db/isar/main_db.dart | 2 + .../models/firo_specific/lelantus_coin.dart | 75 + .../models/firo_specific/lelantus_coin.g.dart | 1570 +++++++++++++++++ 3 files changed, 1647 insertions(+) create mode 100644 lib/models/isar/models/firo_specific/lelantus_coin.dart create mode 100644 lib/models/isar/models/firo_specific/lelantus_coin.g.dart diff --git a/lib/db/isar/main_db.dart b/lib/db/isar/main_db.dart index bb6ea5e8d..209f13c5d 100644 --- a/lib/db/isar/main_db.dart +++ b/lib/db/isar/main_db.dart @@ -14,6 +14,7 @@ import 'package:isar/isar.dart'; import 'package:stackwallet/exceptions/main_db/main_db_exception.dart'; import 'package:stackwallet/models/isar/models/block_explorer.dart'; import 'package:stackwallet/models/isar/models/contact_entry.dart'; +import 'package:stackwallet/models/isar/models/firo_specific/lelantus_coin.dart'; import 'package:stackwallet/models/isar/models/isar_models.dart'; import 'package:stackwallet/models/isar/stack_theme.dart'; import 'package:stackwallet/utilities/amount/amount.dart'; @@ -54,6 +55,7 @@ class MainDB { TransactionBlockExplorerSchema, StackThemeSchema, ContactEntrySchema, + LelantusCoinSchema, ], directory: (await StackFileSystem.applicationIsarDirectory()).path, // inspector: kDebugMode, diff --git a/lib/models/isar/models/firo_specific/lelantus_coin.dart b/lib/models/isar/models/firo_specific/lelantus_coin.dart new file mode 100644 index 000000000..3dd48b2dc --- /dev/null +++ b/lib/models/isar/models/firo_specific/lelantus_coin.dart @@ -0,0 +1,75 @@ +import 'package:isar/isar.dart'; + +part 'lelantus_coin.g.dart'; + +@collection +class LelantusCoin { + Id id = Isar.autoIncrement; + + @Index() + final String walletId; + + @Index( + unique: true, + composite: [ + CompositeIndex("walletId"), + CompositeIndex("txid"), + ], + replace: false, + ) + final String publicCoin; + + final String txid; + + final String value; // can't use BigInt in isar :shrug: + + final int index; + + final int anonymitySetId; + + final bool isUsed; + + LelantusCoin({ + required this.walletId, + required this.publicCoin, + required this.txid, + required this.value, + required this.index, + required this.anonymitySetId, + required this.isUsed, + }); + + LelantusCoin copyWith({ + String? walletId, + String? publicCoin, + String? txid, + String? value, + int? index, + int? anonymitySetId, + bool? isUsed, + }) { + return LelantusCoin( + walletId: walletId ?? this.walletId, + publicCoin: publicCoin ?? this.publicCoin, + txid: txid ?? this.txid, + value: value ?? this.value, + index: index ?? this.index, + anonymitySetId: anonymitySetId ?? this.anonymitySetId, + isUsed: isUsed ?? this.isUsed, + ); + } + + @override + String toString() { + return 'LelantusCoin{' + 'id: $id, ' + 'walletId: $walletId, ' + 'publicCoin: $publicCoin, ' + 'txid: $txid, ' + 'value: $value, ' + 'index: $index, ' + 'anonymitySetId: $anonymitySetId, ' + 'isUsed: $isUsed' + '}'; + } +} diff --git a/lib/models/isar/models/firo_specific/lelantus_coin.g.dart b/lib/models/isar/models/firo_specific/lelantus_coin.g.dart new file mode 100644 index 000000000..8fafde571 --- /dev/null +++ b/lib/models/isar/models/firo_specific/lelantus_coin.g.dart @@ -0,0 +1,1570 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'lelantus_coin.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 GetLelantusCoinCollection on Isar { + IsarCollection get lelantusCoins => this.collection(); +} + +const LelantusCoinSchema = CollectionSchema( + name: r'LelantusCoin', + id: -6795633185033299066, + properties: { + r'anonymitySetId': PropertySchema( + id: 0, + name: r'anonymitySetId', + type: IsarType.long, + ), + r'index': PropertySchema( + id: 1, + name: r'index', + type: IsarType.long, + ), + r'isUsed': PropertySchema( + id: 2, + name: r'isUsed', + type: IsarType.bool, + ), + r'publicCoin': PropertySchema( + id: 3, + name: r'publicCoin', + type: IsarType.string, + ), + r'txid': PropertySchema( + id: 4, + name: r'txid', + type: IsarType.string, + ), + r'value': PropertySchema( + id: 5, + name: r'value', + type: IsarType.string, + ), + r'walletId': PropertySchema( + id: 6, + name: r'walletId', + type: IsarType.string, + ) + }, + estimateSize: _lelantusCoinEstimateSize, + serialize: _lelantusCoinSerialize, + deserialize: _lelantusCoinDeserialize, + deserializeProp: _lelantusCoinDeserializeProp, + idName: r'id', + indexes: { + r'walletId': IndexSchema( + id: -1783113319798776304, + name: r'walletId', + unique: false, + replace: false, + properties: [ + IndexPropertySchema( + name: r'walletId', + type: IndexType.hash, + caseSensitive: true, + ) + ], + ), + r'publicCoin_walletId_txid': IndexSchema( + id: 5610740154835640070, + name: r'publicCoin_walletId_txid', + unique: true, + replace: false, + properties: [ + IndexPropertySchema( + name: r'publicCoin', + type: IndexType.hash, + caseSensitive: true, + ), + IndexPropertySchema( + name: r'walletId', + type: IndexType.hash, + caseSensitive: true, + ), + IndexPropertySchema( + name: r'txid', + type: IndexType.hash, + caseSensitive: true, + ) + ], + ) + }, + links: {}, + embeddedSchemas: {}, + getId: _lelantusCoinGetId, + getLinks: _lelantusCoinGetLinks, + attach: _lelantusCoinAttach, + version: '3.0.5', +); + +int _lelantusCoinEstimateSize( + LelantusCoin object, + List offsets, + Map> allOffsets, +) { + var bytesCount = offsets.last; + bytesCount += 3 + object.publicCoin.length * 3; + bytesCount += 3 + object.txid.length * 3; + bytesCount += 3 + object.value.length * 3; + bytesCount += 3 + object.walletId.length * 3; + return bytesCount; +} + +void _lelantusCoinSerialize( + LelantusCoin object, + IsarWriter writer, + List offsets, + Map> allOffsets, +) { + writer.writeLong(offsets[0], object.anonymitySetId); + writer.writeLong(offsets[1], object.index); + writer.writeBool(offsets[2], object.isUsed); + writer.writeString(offsets[3], object.publicCoin); + writer.writeString(offsets[4], object.txid); + writer.writeString(offsets[5], object.value); + writer.writeString(offsets[6], object.walletId); +} + +LelantusCoin _lelantusCoinDeserialize( + Id id, + IsarReader reader, + List offsets, + Map> allOffsets, +) { + final object = LelantusCoin( + anonymitySetId: reader.readLong(offsets[0]), + index: reader.readLong(offsets[1]), + isUsed: reader.readBool(offsets[2]), + publicCoin: reader.readString(offsets[3]), + txid: reader.readString(offsets[4]), + value: reader.readString(offsets[5]), + walletId: reader.readString(offsets[6]), + ); + object.id = id; + return object; +} + +P _lelantusCoinDeserializeProp

( + IsarReader reader, + int propertyId, + int offset, + Map> allOffsets, +) { + switch (propertyId) { + case 0: + return (reader.readLong(offset)) as P; + case 1: + return (reader.readLong(offset)) as P; + case 2: + return (reader.readBool(offset)) as P; + case 3: + return (reader.readString(offset)) as P; + case 4: + return (reader.readString(offset)) as P; + case 5: + return (reader.readString(offset)) as P; + case 6: + return (reader.readString(offset)) as P; + default: + throw IsarError('Unknown property with id $propertyId'); + } +} + +Id _lelantusCoinGetId(LelantusCoin object) { + return object.id; +} + +List> _lelantusCoinGetLinks(LelantusCoin object) { + return []; +} + +void _lelantusCoinAttach( + IsarCollection col, Id id, LelantusCoin object) { + object.id = id; +} + +extension LelantusCoinByIndex on IsarCollection { + Future getByPublicCoinWalletIdTxid( + String publicCoin, String walletId, String txid) { + return getByIndex( + r'publicCoin_walletId_txid', [publicCoin, walletId, txid]); + } + + LelantusCoin? getByPublicCoinWalletIdTxidSync( + String publicCoin, String walletId, String txid) { + return getByIndexSync( + r'publicCoin_walletId_txid', [publicCoin, walletId, txid]); + } + + Future deleteByPublicCoinWalletIdTxid( + String publicCoin, String walletId, String txid) { + return deleteByIndex( + r'publicCoin_walletId_txid', [publicCoin, walletId, txid]); + } + + bool deleteByPublicCoinWalletIdTxidSync( + String publicCoin, String walletId, String txid) { + return deleteByIndexSync( + r'publicCoin_walletId_txid', [publicCoin, walletId, txid]); + } + + Future> getAllByPublicCoinWalletIdTxid( + List publicCoinValues, + List walletIdValues, + List txidValues) { + final len = publicCoinValues.length; + assert(walletIdValues.length == len && txidValues.length == len, + 'All index values must have the same length'); + final values = >[]; + for (var i = 0; i < len; i++) { + values.add([publicCoinValues[i], walletIdValues[i], txidValues[i]]); + } + + return getAllByIndex(r'publicCoin_walletId_txid', values); + } + + List getAllByPublicCoinWalletIdTxidSync( + List publicCoinValues, + List walletIdValues, + List txidValues) { + final len = publicCoinValues.length; + assert(walletIdValues.length == len && txidValues.length == len, + 'All index values must have the same length'); + final values = >[]; + for (var i = 0; i < len; i++) { + values.add([publicCoinValues[i], walletIdValues[i], txidValues[i]]); + } + + return getAllByIndexSync(r'publicCoin_walletId_txid', values); + } + + Future deleteAllByPublicCoinWalletIdTxid(List publicCoinValues, + List walletIdValues, List txidValues) { + final len = publicCoinValues.length; + assert(walletIdValues.length == len && txidValues.length == len, + 'All index values must have the same length'); + final values = >[]; + for (var i = 0; i < len; i++) { + values.add([publicCoinValues[i], walletIdValues[i], txidValues[i]]); + } + + return deleteAllByIndex(r'publicCoin_walletId_txid', values); + } + + int deleteAllByPublicCoinWalletIdTxidSync(List publicCoinValues, + List walletIdValues, List txidValues) { + final len = publicCoinValues.length; + assert(walletIdValues.length == len && txidValues.length == len, + 'All index values must have the same length'); + final values = >[]; + for (var i = 0; i < len; i++) { + values.add([publicCoinValues[i], walletIdValues[i], txidValues[i]]); + } + + return deleteAllByIndexSync(r'publicCoin_walletId_txid', values); + } + + Future putByPublicCoinWalletIdTxid(LelantusCoin object) { + return putByIndex(r'publicCoin_walletId_txid', object); + } + + Id putByPublicCoinWalletIdTxidSync(LelantusCoin object, + {bool saveLinks = true}) { + return putByIndexSync(r'publicCoin_walletId_txid', object, + saveLinks: saveLinks); + } + + Future> putAllByPublicCoinWalletIdTxid(List objects) { + return putAllByIndex(r'publicCoin_walletId_txid', objects); + } + + List putAllByPublicCoinWalletIdTxidSync(List objects, + {bool saveLinks = true}) { + return putAllByIndexSync(r'publicCoin_walletId_txid', objects, + saveLinks: saveLinks); + } +} + +extension LelantusCoinQueryWhereSort + on QueryBuilder { + QueryBuilder anyId() { + return QueryBuilder.apply(this, (query) { + return query.addWhereClause(const IdWhereClause.any()); + }); + } +} + +extension LelantusCoinQueryWhere + 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 walletIdEqualTo( + String walletId) { + return QueryBuilder.apply(this, (query) { + return query.addWhereClause(IndexWhereClause.equalTo( + indexName: r'walletId', + value: [walletId], + )); + }); + } + + QueryBuilder + walletIdNotEqualTo(String walletId) { + return QueryBuilder.apply(this, (query) { + if (query.whereSort == Sort.asc) { + return query + .addWhereClause(IndexWhereClause.between( + indexName: r'walletId', + lower: [], + upper: [walletId], + includeUpper: false, + )) + .addWhereClause(IndexWhereClause.between( + indexName: r'walletId', + lower: [walletId], + includeLower: false, + upper: [], + )); + } else { + return query + .addWhereClause(IndexWhereClause.between( + indexName: r'walletId', + lower: [walletId], + includeLower: false, + upper: [], + )) + .addWhereClause(IndexWhereClause.between( + indexName: r'walletId', + lower: [], + upper: [walletId], + includeUpper: false, + )); + } + }); + } + + QueryBuilder + publicCoinEqualToAnyWalletIdTxid(String publicCoin) { + return QueryBuilder.apply(this, (query) { + return query.addWhereClause(IndexWhereClause.equalTo( + indexName: r'publicCoin_walletId_txid', + value: [publicCoin], + )); + }); + } + + QueryBuilder + publicCoinNotEqualToAnyWalletIdTxid(String publicCoin) { + return QueryBuilder.apply(this, (query) { + if (query.whereSort == Sort.asc) { + return query + .addWhereClause(IndexWhereClause.between( + indexName: r'publicCoin_walletId_txid', + lower: [], + upper: [publicCoin], + includeUpper: false, + )) + .addWhereClause(IndexWhereClause.between( + indexName: r'publicCoin_walletId_txid', + lower: [publicCoin], + includeLower: false, + upper: [], + )); + } else { + return query + .addWhereClause(IndexWhereClause.between( + indexName: r'publicCoin_walletId_txid', + lower: [publicCoin], + includeLower: false, + upper: [], + )) + .addWhereClause(IndexWhereClause.between( + indexName: r'publicCoin_walletId_txid', + lower: [], + upper: [publicCoin], + includeUpper: false, + )); + } + }); + } + + QueryBuilder + publicCoinWalletIdEqualToAnyTxid(String publicCoin, String walletId) { + return QueryBuilder.apply(this, (query) { + return query.addWhereClause(IndexWhereClause.equalTo( + indexName: r'publicCoin_walletId_txid', + value: [publicCoin, walletId], + )); + }); + } + + QueryBuilder + publicCoinEqualToWalletIdNotEqualToAnyTxid( + String publicCoin, String walletId) { + return QueryBuilder.apply(this, (query) { + if (query.whereSort == Sort.asc) { + return query + .addWhereClause(IndexWhereClause.between( + indexName: r'publicCoin_walletId_txid', + lower: [publicCoin], + upper: [publicCoin, walletId], + includeUpper: false, + )) + .addWhereClause(IndexWhereClause.between( + indexName: r'publicCoin_walletId_txid', + lower: [publicCoin, walletId], + includeLower: false, + upper: [publicCoin], + )); + } else { + return query + .addWhereClause(IndexWhereClause.between( + indexName: r'publicCoin_walletId_txid', + lower: [publicCoin, walletId], + includeLower: false, + upper: [publicCoin], + )) + .addWhereClause(IndexWhereClause.between( + indexName: r'publicCoin_walletId_txid', + lower: [publicCoin], + upper: [publicCoin, walletId], + includeUpper: false, + )); + } + }); + } + + QueryBuilder + publicCoinWalletIdTxidEqualTo( + String publicCoin, String walletId, String txid) { + return QueryBuilder.apply(this, (query) { + return query.addWhereClause(IndexWhereClause.equalTo( + indexName: r'publicCoin_walletId_txid', + value: [publicCoin, walletId, txid], + )); + }); + } + + QueryBuilder + publicCoinWalletIdEqualToTxidNotEqualTo( + String publicCoin, String walletId, String txid) { + return QueryBuilder.apply(this, (query) { + if (query.whereSort == Sort.asc) { + return query + .addWhereClause(IndexWhereClause.between( + indexName: r'publicCoin_walletId_txid', + lower: [publicCoin, walletId], + upper: [publicCoin, walletId, txid], + includeUpper: false, + )) + .addWhereClause(IndexWhereClause.between( + indexName: r'publicCoin_walletId_txid', + lower: [publicCoin, walletId, txid], + includeLower: false, + upper: [publicCoin, walletId], + )); + } else { + return query + .addWhereClause(IndexWhereClause.between( + indexName: r'publicCoin_walletId_txid', + lower: [publicCoin, walletId, txid], + includeLower: false, + upper: [publicCoin, walletId], + )) + .addWhereClause(IndexWhereClause.between( + indexName: r'publicCoin_walletId_txid', + lower: [publicCoin, walletId], + upper: [publicCoin, walletId, txid], + includeUpper: false, + )); + } + }); + } +} + +extension LelantusCoinQueryFilter + on QueryBuilder { + QueryBuilder + anonymitySetIdEqualTo(int value) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'anonymitySetId', + value: value, + )); + }); + } + + QueryBuilder + anonymitySetIdGreaterThan( + int value, { + bool include = false, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + include: include, + property: r'anonymitySetId', + value: value, + )); + }); + } + + QueryBuilder + anonymitySetIdLessThan( + int value, { + bool include = false, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.lessThan( + include: include, + property: r'anonymitySetId', + value: value, + )); + }); + } + + QueryBuilder + anonymitySetIdBetween( + int lower, + int upper, { + bool includeLower = true, + bool includeUpper = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.between( + property: r'anonymitySetId', + 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 indexEqualTo( + int value) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'index', + value: value, + )); + }); + } + + QueryBuilder + indexGreaterThan( + int value, { + bool include = false, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + include: include, + property: r'index', + value: value, + )); + }); + } + + QueryBuilder indexLessThan( + int value, { + bool include = false, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.lessThan( + include: include, + property: r'index', + value: value, + )); + }); + } + + QueryBuilder indexBetween( + int lower, + int upper, { + bool includeLower = true, + bool includeUpper = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.between( + property: r'index', + lower: lower, + includeLower: includeLower, + upper: upper, + includeUpper: includeUpper, + )); + }); + } + + QueryBuilder isUsedEqualTo( + bool value) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'isUsed', + value: value, + )); + }); + } + + QueryBuilder + publicCoinEqualTo( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'publicCoin', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + publicCoinGreaterThan( + String value, { + bool include = false, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + include: include, + property: r'publicCoin', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + publicCoinLessThan( + String value, { + bool include = false, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.lessThan( + include: include, + property: r'publicCoin', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + publicCoinBetween( + 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'publicCoin', + lower: lower, + includeLower: includeLower, + upper: upper, + includeUpper: includeUpper, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + publicCoinStartsWith( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.startsWith( + property: r'publicCoin', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + publicCoinEndsWith( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.endsWith( + property: r'publicCoin', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + publicCoinContains(String value, {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.contains( + property: r'publicCoin', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + publicCoinMatches(String pattern, {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.matches( + property: r'publicCoin', + wildcard: pattern, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + publicCoinIsEmpty() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'publicCoin', + value: '', + )); + }); + } + + QueryBuilder + publicCoinIsNotEmpty() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + property: r'publicCoin', + value: '', + )); + }); + } + + QueryBuilder txidEqualTo( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'txid', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + txidGreaterThan( + String value, { + bool include = false, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + include: include, + property: r'txid', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder txidLessThan( + String value, { + bool include = false, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.lessThan( + include: include, + property: r'txid', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder txidBetween( + 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'txid', + lower: lower, + includeLower: includeLower, + upper: upper, + includeUpper: includeUpper, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + txidStartsWith( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.startsWith( + property: r'txid', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder txidEndsWith( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.endsWith( + property: r'txid', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder txidContains( + String value, + {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.contains( + property: r'txid', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder txidMatches( + String pattern, + {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.matches( + property: r'txid', + wildcard: pattern, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + txidIsEmpty() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'txid', + value: '', + )); + }); + } + + QueryBuilder + txidIsNotEmpty() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + property: r'txid', + value: '', + )); + }); + } + + QueryBuilder valueEqualTo( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'value', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + valueGreaterThan( + String value, { + bool include = false, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + include: include, + property: r'value', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder valueLessThan( + String value, { + bool include = false, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.lessThan( + include: include, + property: r'value', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder valueBetween( + 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'value', + lower: lower, + includeLower: includeLower, + upper: upper, + includeUpper: includeUpper, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + valueStartsWith( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.startsWith( + property: r'value', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder valueEndsWith( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.endsWith( + property: r'value', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder valueContains( + String value, + {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.contains( + property: r'value', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder valueMatches( + String pattern, + {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.matches( + property: r'value', + wildcard: pattern, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + valueIsEmpty() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'value', + value: '', + )); + }); + } + + QueryBuilder + valueIsNotEmpty() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + property: r'value', + value: '', + )); + }); + } + + QueryBuilder + walletIdEqualTo( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'walletId', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + walletIdGreaterThan( + String value, { + bool include = false, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + include: include, + property: r'walletId', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + walletIdLessThan( + String value, { + bool include = false, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.lessThan( + include: include, + property: r'walletId', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + walletIdBetween( + 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'walletId', + lower: lower, + includeLower: includeLower, + upper: upper, + includeUpper: includeUpper, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + walletIdStartsWith( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.startsWith( + property: r'walletId', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + walletIdEndsWith( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.endsWith( + property: r'walletId', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + walletIdContains(String value, {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.contains( + property: r'walletId', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + walletIdMatches(String pattern, {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.matches( + property: r'walletId', + wildcard: pattern, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + walletIdIsEmpty() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'walletId', + value: '', + )); + }); + } + + QueryBuilder + walletIdIsNotEmpty() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + property: r'walletId', + value: '', + )); + }); + } +} + +extension LelantusCoinQueryObject + on QueryBuilder {} + +extension LelantusCoinQueryLinks + on QueryBuilder {} + +extension LelantusCoinQuerySortBy + on QueryBuilder { + QueryBuilder + sortByAnonymitySetId() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'anonymitySetId', Sort.asc); + }); + } + + QueryBuilder + sortByAnonymitySetIdDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'anonymitySetId', Sort.desc); + }); + } + + QueryBuilder sortByIndex() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'index', Sort.asc); + }); + } + + QueryBuilder sortByIndexDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'index', Sort.desc); + }); + } + + QueryBuilder sortByIsUsed() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'isUsed', Sort.asc); + }); + } + + QueryBuilder sortByIsUsedDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'isUsed', Sort.desc); + }); + } + + QueryBuilder sortByPublicCoin() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'publicCoin', Sort.asc); + }); + } + + QueryBuilder + sortByPublicCoinDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'publicCoin', Sort.desc); + }); + } + + QueryBuilder sortByTxid() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'txid', Sort.asc); + }); + } + + QueryBuilder sortByTxidDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'txid', Sort.desc); + }); + } + + QueryBuilder sortByValue() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'value', Sort.asc); + }); + } + + QueryBuilder sortByValueDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'value', Sort.desc); + }); + } + + QueryBuilder sortByWalletId() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'walletId', Sort.asc); + }); + } + + QueryBuilder sortByWalletIdDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'walletId', Sort.desc); + }); + } +} + +extension LelantusCoinQuerySortThenBy + on QueryBuilder { + QueryBuilder + thenByAnonymitySetId() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'anonymitySetId', Sort.asc); + }); + } + + QueryBuilder + thenByAnonymitySetIdDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'anonymitySetId', 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 thenByIndex() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'index', Sort.asc); + }); + } + + QueryBuilder thenByIndexDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'index', Sort.desc); + }); + } + + QueryBuilder thenByIsUsed() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'isUsed', Sort.asc); + }); + } + + QueryBuilder thenByIsUsedDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'isUsed', Sort.desc); + }); + } + + QueryBuilder thenByPublicCoin() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'publicCoin', Sort.asc); + }); + } + + QueryBuilder + thenByPublicCoinDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'publicCoin', Sort.desc); + }); + } + + QueryBuilder thenByTxid() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'txid', Sort.asc); + }); + } + + QueryBuilder thenByTxidDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'txid', Sort.desc); + }); + } + + QueryBuilder thenByValue() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'value', Sort.asc); + }); + } + + QueryBuilder thenByValueDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'value', Sort.desc); + }); + } + + QueryBuilder thenByWalletId() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'walletId', Sort.asc); + }); + } + + QueryBuilder thenByWalletIdDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'walletId', Sort.desc); + }); + } +} + +extension LelantusCoinQueryWhereDistinct + on QueryBuilder { + QueryBuilder + distinctByAnonymitySetId() { + return QueryBuilder.apply(this, (query) { + return query.addDistinctBy(r'anonymitySetId'); + }); + } + + QueryBuilder distinctByIndex() { + return QueryBuilder.apply(this, (query) { + return query.addDistinctBy(r'index'); + }); + } + + QueryBuilder distinctByIsUsed() { + return QueryBuilder.apply(this, (query) { + return query.addDistinctBy(r'isUsed'); + }); + } + + QueryBuilder distinctByPublicCoin( + {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addDistinctBy(r'publicCoin', caseSensitive: caseSensitive); + }); + } + + QueryBuilder distinctByTxid( + {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addDistinctBy(r'txid', caseSensitive: caseSensitive); + }); + } + + QueryBuilder distinctByValue( + {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addDistinctBy(r'value', caseSensitive: caseSensitive); + }); + } + + QueryBuilder distinctByWalletId( + {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addDistinctBy(r'walletId', caseSensitive: caseSensitive); + }); + } +} + +extension LelantusCoinQueryProperty + on QueryBuilder { + QueryBuilder idProperty() { + return QueryBuilder.apply(this, (query) { + return query.addPropertyName(r'id'); + }); + } + + QueryBuilder anonymitySetIdProperty() { + return QueryBuilder.apply(this, (query) { + return query.addPropertyName(r'anonymitySetId'); + }); + } + + QueryBuilder indexProperty() { + return QueryBuilder.apply(this, (query) { + return query.addPropertyName(r'index'); + }); + } + + QueryBuilder isUsedProperty() { + return QueryBuilder.apply(this, (query) { + return query.addPropertyName(r'isUsed'); + }); + } + + QueryBuilder publicCoinProperty() { + return QueryBuilder.apply(this, (query) { + return query.addPropertyName(r'publicCoin'); + }); + } + + QueryBuilder txidProperty() { + return QueryBuilder.apply(this, (query) { + return query.addPropertyName(r'txid'); + }); + } + + QueryBuilder valueProperty() { + return QueryBuilder.apply(this, (query) { + return query.addPropertyName(r'value'); + }); + } + + QueryBuilder walletIdProperty() { + return QueryBuilder.apply(this, (query) { + return query.addPropertyName(r'walletId'); + }); + } +} From a5ba67aa1d3a44de01d1ef931931f951a9db4429 Mon Sep 17 00:00:00 2001 From: julian Date: Mon, 24 Jul 2023 09:42:29 -0600 Subject: [PATCH 106/169] WIP migrate lcoins hive => isar --- lib/models/isar/models/isar_models.dart | 1 + lib/models/lelantus_coin.dart | 2 + lib/utilities/constants.dart | 2 +- lib/utilities/db_version_migration.dart | 65 ++++++++++++++++++++++++- 4 files changed, 68 insertions(+), 2 deletions(-) diff --git a/lib/models/isar/models/isar_models.dart b/lib/models/isar/models/isar_models.dart index ce7652a46..9de91fc84 100644 --- a/lib/models/isar/models/isar_models.dart +++ b/lib/models/isar/models/isar_models.dart @@ -15,5 +15,6 @@ export 'blockchain_data/output.dart'; export 'blockchain_data/transaction.dart'; export 'blockchain_data/utxo.dart'; export 'ethereum/eth_contract.dart'; +export 'firo_specific/lelantus_coin.dart'; export 'log.dart'; export 'transaction_note.dart'; diff --git a/lib/models/lelantus_coin.dart b/lib/models/lelantus_coin.dart index 0e32d33bf..56557c1cd 100644 --- a/lib/models/lelantus_coin.dart +++ b/lib/models/lelantus_coin.dart @@ -12,6 +12,7 @@ import 'package:hive/hive.dart'; part 'type_adaptors/lelantus_coin.g.dart'; +@Deprecated("Use Isar object instead") // @HiveType(typeId: 9) class LelantusCoin { // @HiveField(0) @@ -27,6 +28,7 @@ class LelantusCoin { // @HiveField(5) bool isUsed; + @Deprecated("Use Isar object instead") LelantusCoin( this.index, this.value, diff --git a/lib/utilities/constants.dart b/lib/utilities/constants.dart index 2dd7be287..ad9d68997 100644 --- a/lib/utilities/constants.dart +++ b/lib/utilities/constants.dart @@ -58,7 +58,7 @@ abstract class Constants { // Enable Logger.print statements static const bool disableLogger = false; - static const int currentDataVersion = 10; + static const int currentDataVersion = 11; static const int rescanV1 = 1; diff --git a/lib/utilities/db_version_migration.dart b/lib/utilities/db_version_migration.dart index 293b5d48c..3f69161f4 100644 --- a/lib/utilities/db_version_migration.dart +++ b/lib/utilities/db_version_migration.dart @@ -180,7 +180,6 @@ class DbVersionMigrator with WalletDB { // clear possible broken firo cache await DB.instance.clearSharedTransactionCache(coin: Coin.firo); - // update version await DB.instance.put( boxName: DB.boxNameDBInfo, key: "hive_data_version", value: 4); @@ -343,12 +342,76 @@ class DbVersionMigrator with WalletDB { // try to continue migrating return await migrate(10, secureStore: secureStore); + case 10: + // migrate + await _v10(secureStore); + + // update version + await DB.instance.put( + boxName: DB.boxNameDBInfo, key: "hive_data_version", value: 11); + + // try to continue migrating + return await migrate(11, secureStore: secureStore); + default: // finally return return; } } + Future _v10(SecureStorageInterface secureStore) async { + await Hive.openBox(DB.boxNameAllWalletsData); + await Hive.openBox(DB.boxNamePrefs); + final walletsService = WalletsService(secureStorageInterface: secureStore); + final prefs = Prefs.instance; + final walletInfoList = await walletsService.walletNames; + await prefs.init(); + await MainDB.instance.initMainDB(); + + for (final walletId in walletInfoList.keys) { + final info = walletInfoList[walletId]!; + assert(info.walletId == walletId); + + if (info.coin == Coin.firo && + MainDB.instance.isar.lelantusCoins + .where() + .walletIdEqualTo(walletId) + .countSync() == + 0) { + final walletBox = await Hive.openBox(walletId); + + final hiveLCoins = DB.instance.get( + boxName: walletId, + key: "_lelantus_coins", + ) as List? ?? + []; + + final List coins = []; + for (final e in hiveLCoins) { + final lcoin = e as LelantusCoin; + + final coin = isar_models.LelantusCoin( + walletId: walletId, + publicCoin: lcoin.publicCoin, + txid: lcoin.txId, + value: lcoin.value.toString(), + index: lcoin.index, + anonymitySetId: lcoin.anonymitySetId, + isUsed: lcoin.isUsed, + ); + + coins.add(coin); + } + + if (coins.isNotEmpty) { + await MainDB.instance.isar.writeTxn(() async { + await MainDB.instance.isar.lelantusCoins.putAll(coins); + }); + } + } + } + } + Future _v4(SecureStorageInterface secureStore) async { await Hive.openBox(DB.boxNameAllWalletsData); await Hive.openBox(DB.boxNamePrefs); From df0b004b82478600ddcc073c222701b820022591 Mon Sep 17 00:00:00 2001 From: julian Date: Mon, 24 Jul 2023 12:30:01 -0600 Subject: [PATCH 107/169] finish up basic migrate lcoins hive => isar --- lib/services/coins/firo/firo_wallet.dart | 565 ++++++++++++----------- lib/services/mixins/firo_hive.dart | 26 +- lib/utilities/db_version_migration.dart | 3 +- 3 files changed, 301 insertions(+), 293 deletions(-) diff --git a/lib/services/coins/firo/firo_wallet.dart b/lib/services/coins/firo/firo_wallet.dart index 3527c9802..004e5399c 100644 --- a/lib/services/coins/firo/firo_wallet.dart +++ b/lib/services/coins/firo/firo_wallet.dart @@ -26,7 +26,6 @@ import 'package:stackwallet/electrumx_rpc/cached_electrumx.dart'; import 'package:stackwallet/electrumx_rpc/electrumx.dart'; import 'package:stackwallet/models/balance.dart'; import 'package:stackwallet/models/isar/models/isar_models.dart' as isar_models; -import 'package:stackwallet/models/lelantus_coin.dart'; import 'package:stackwallet/models/lelantus_fee_data.dart'; import 'package:stackwallet/models/paymint/fee_object_model.dart'; import 'package:stackwallet/models/signing_data.dart'; @@ -161,6 +160,7 @@ Future executeNative(Map arguments) async { final mnemonicPassphrase = arguments['mnemonicPassphrase'] as String; final coin = arguments['coin'] as Coin; final network = arguments['network'] as NetworkType; + final walletId = arguments['walletId'] as String; final restoreData = await isolateRestore( mnemonic, @@ -170,6 +170,7 @@ Future executeNative(Map arguments) async { setDataMap, usedSerialNumbers, network, + walletId, ); sendPort.send(restoreData); return; @@ -206,13 +207,14 @@ Future> isolateRestore( Map _setDataMap, List _usedSerialNumbers, NetworkType network, + String walletId, ) async { List jindexes = []; - List> lelantusCoins = []; + List lelantusCoins = []; final List spendTxIds = []; - var lastFoundIndex = 0; - var currentIndex = 0; + int lastFoundIndex = 0; + int currentIndex = 0; try { Set usedSerialNumbersSet = _usedSerialNumbers.toSet(); @@ -239,7 +241,7 @@ Future> isolateRestore( isTestnet: coin == Coin.firoTestNet, ); - for (var setId = 1; setId <= _latestSetId; setId++) { + for (int setId = 1; setId <= _latestSetId; setId++) { final setData = _setDataMap[setId] as Map; final foundCoin = (setData["coins"] as List).firstWhere( (e) => e[1] == mintTag, @@ -264,32 +266,23 @@ Future> isolateRestore( isTestnet: coin == Coin.firoTestNet, ); final bool isUsed = usedSerialNumbersSet.contains(serialNumber); - final duplicateCoin = lelantusCoins.firstWhere( - (element) { - final coin = element.values.first; - return coin.txId == txId && - coin.index == currentIndex && - coin.anonymitySetId != setId; - }, - orElse: () => {}, + + lelantusCoins.removeWhere((e) => + e.txid == txId && + e.index == currentIndex && + e.anonymitySetId != setId); + + lelantusCoins.add( + isar_models.LelantusCoin( + walletId: walletId, + index: currentIndex, + value: amount.toString(), + publicCoin: publicCoin, + txid: txId, + anonymitySetId: setId, + isUsed: isUsed, + ), ); - if (duplicateCoin.isNotEmpty) { - Logging.instance.log( - "Firo isolateRestore removing duplicate coin: $duplicateCoin", - level: LogLevel.Info, - ); - lelantusCoins.remove(duplicateCoin); - } - lelantusCoins.add({ - txId: LelantusCoin( - currentIndex, - amount, - publicCoin, - txId, - setId, - isUsed, - ) - }); Logging.instance.log( "amount $amount used $isUsed", level: LogLevel.Info, @@ -322,32 +315,22 @@ Future> isolateRestore( isTestnet: coin == Coin.firoTestNet, ); bool isUsed = usedSerialNumbersSet.contains(serialNumber); - final duplicateCoin = lelantusCoins.firstWhere( - (element) { - final coin = element.values.first; - return coin.txId == txId && - coin.index == currentIndex && - coin.anonymitySetId != setId; - }, - orElse: () => {}, + lelantusCoins.removeWhere((e) => + e.txid == txId && + e.index == currentIndex && + e.anonymitySetId != setId); + + lelantusCoins.add( + isar_models.LelantusCoin( + walletId: walletId, + index: currentIndex, + value: amount.toString(), + publicCoin: publicCoin, + txid: txId, + anonymitySetId: setId, + isUsed: isUsed, + ), ); - if (duplicateCoin.isNotEmpty) { - Logging.instance.log( - "Firo isolateRestore removing duplicate coin: $duplicateCoin", - level: LogLevel.Info, - ); - lelantusCoins.remove(duplicateCoin); - } - lelantusCoins.add({ - txId: LelantusCoin( - currentIndex, - amount, - publicCoin, - txId, - setId, - isUsed, - ) - }); jindexes.add(currentIndex); spendTxIds.add(txId); @@ -396,73 +379,74 @@ Future> staticProcessRestore( Map result, int currentHeight, ) async { - List? _l = result['_lelantus_coins'] as List?; - final List> lelantusCoins = []; - for (var el in _l ?? []) { - lelantusCoins.add({el.keys.first: el.values.first as LelantusCoin}); - } + List lelantusCoins = + result['_lelantus_coins'] as List; // Edit the receive transactions with the mint fees. - Map editedTransactions = - {}; - for (var item in lelantusCoins) { - item.forEach((key, value) { - String txid = value.txId; - isar_models.Transaction? tx; + List editedTransactions = []; + + for (final coin in lelantusCoins) { + String txid = coin.txid; + isar_models.Transaction? tx; + try { + tx = txns.firstWhere((e) => e.txid == txid); + } catch (_) { + tx = null; + } + + if (tx == null || tx.subType == isar_models.TransactionSubType.join) { + // This is a jmint. + continue; + } + + List inputTxns = []; + for (final input in tx.inputs) { + isar_models.Transaction? inputTx; try { - tx = txns.firstWhere((e) => e.txid == txid); + inputTx = txns.firstWhere((e) => e.txid == input.txid); } catch (_) { - tx = null; + inputTx = null; } + if (inputTx != null) { + inputTxns.add(inputTx); + } + } + if (inputTxns.isEmpty) { + //some error. + Logging.instance.log( + "cryptic \"//some error\" occurred in staticProcessRestore on lelantus coin: $coin", + level: LogLevel.Error, + ); + continue; + } - if (tx == null || tx.subType == isar_models.TransactionSubType.join) { - // This is a jmint. - return; - } - List inputs = []; - for (var element in tx.inputs) { - isar_models.Transaction? input; - try { - input = txns.firstWhere((e) => e.txid == element.txid); - } catch (_) { - input = null; - } - if (input != null) { - inputs.add(input); - } - } - if (inputs.isEmpty) { - //some error. - return; - } - - int mintFee = tx.fee; - int sharedFee = mintFee ~/ inputs.length; - for (var element in inputs) { - editedTransactions[element.txid] = isar_models.Transaction( - walletId: element.walletId, - txid: element.txid, - timestamp: element.timestamp, - 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, - numberOfMessages: null, - )..address.value = element.address.value; - } - }); + int mintFee = tx.fee; + int sharedFee = mintFee ~/ inputTxns.length; + for (final inputTx in inputTxns) { + final edited = isar_models.Transaction( + walletId: inputTx.walletId, + txid: inputTx.txid, + timestamp: inputTx.timestamp, + type: inputTx.type, + subType: isar_models.TransactionSubType.mint, + amount: inputTx.amount, + amountString: Amount( + rawValue: BigInt.from(inputTx.amount), + fractionDigits: Coin.firo.decimals, + ).toJsonString(), + fee: sharedFee, + height: inputTx.height, + isCancelled: false, + isLelantus: true, + slateId: null, + otherData: txid, + nonce: null, + inputs: inputTx.inputs, + outputs: inputTx.outputs, + numberOfMessages: null, + )..address.value = inputTx.address.value; + editedTransactions.add(edited); + } } // Logging.instance.log(editedTransactions, addToDebugMessagesDB: false); @@ -472,12 +456,13 @@ Future> staticProcessRestore( } // Logging.instance.log(transactionMap, addToDebugMessagesDB: false); - editedTransactions.forEach((key, value) { - transactionMap.update(key, (_value) => value); - }); + // update with edited transactions + for (final tx in editedTransactions) { + transactionMap[tx.txid] = tx; + } transactionMap.removeWhere((key, value) => - lelantusCoins.any((element) => element.containsKey(key)) || + lelantusCoins.any((element) => element.txid == key) || ((value.height == -1 || value.height == null) && !value.isConfirmed(currentHeight, MINIMUM_CONFIRMATIONS))); @@ -2248,9 +2233,9 @@ class FiroWallet extends CoinServiceAPI _feeObject = Future(() => feeObj); GlobalEventBus.instance.fire(RefreshPercentChangedEvent(0.60, walletId)); - final lelantusCoins = getLelantusCoinMap(); - Logging.instance.log("_lelantus_coins at refresh: $lelantusCoins", - level: LogLevel.Warning, printFullLength: true); + // final lelantusCoins = getLelantusCoinMap(); + // Logging.instance.log("_lelantus_coins at refresh: $lelantusCoins", + // level: LogLevel.Warning, printFullLength: true); GlobalEventBus.instance.fire(RefreshPercentChangedEvent(0.70, walletId)); await _refreshLelantusData(); @@ -2327,7 +2312,8 @@ class FiroWallet extends CoinServiceAPI level: LogLevel.Error); } - final List lelantusCoins = await _getUnspentCoins(); + final List lelantusCoins = + await _getUnspentCoins(); final root = await Bip32Utils.getBip32Root( _mnemonic!, @@ -2349,7 +2335,7 @@ class FiroWallet extends CoinServiceAPI } final String privateKey = Format.uint8listToString(keyPair.privateKey!); return DartLelantusEntry(coin.isUsed ? 1 : 0, 0, coin.anonymitySetId, - coin.value, coin.index, privateKey); + int.parse(coin.value), coin.index, privateKey); }).toList(); final lelantusEntries = await Future.wait(waitLelantusEntries); @@ -2361,61 +2347,55 @@ class FiroWallet extends CoinServiceAPI return lelantusEntries; } - List> getLelantusCoinMap() { - final _l = firoGetLelantusCoins(); - final List> lelantusCoins = []; - for (var el in _l ?? []) { - lelantusCoins.add({el.keys.first: el.values.first as LelantusCoin}); - } - return lelantusCoins; - } + Future> _getUnspentCoins() async { + final jindexes = firoGetJIndex() ?? []; - Future> _getUnspentCoins() async { - final List> lelantusCoins = getLelantusCoinMap(); - if (lelantusCoins.isNotEmpty) { - lelantusCoins.removeWhere((element) => - element.values.any((elementCoin) => elementCoin.value == 0)); - } - final jindexes = firoGetJIndex(); + final lelantusCoinsList = await db.isar.lelantusCoins + .where() + .walletIdEqualTo(walletId) + .filter() + .not() + .valueEqualTo("0") + .findAll(); - List coins = []; - - List lelantusCoinsList = - lelantusCoins.fold([], (previousValue, element) { - previousValue.add(element.values.first); - return previousValue; - }); + List coins = []; final currentChainHeight = await chainHeight; for (int i = 0; i < lelantusCoinsList.length; i++) { // Logging.instance.log("lelantusCoinsList[$i]: ${lelantusCoinsList[i]}"); - final txid = lelantusCoinsList[i].txId; - final txn = await cachedElectrumXClient.getTransaction( - txHash: txid, - verbose: true, - coin: coin, - ); - final confirmations = txn["confirmations"]; - bool isUnconfirmed = confirmations is int && confirmations < 1; + final coin = lelantusCoinsList[i]; - final tx = await db.getTransaction(walletId, txid); + final tx = await db.getTransaction(walletId, coin.txid); - if (!jindexes!.contains(lelantusCoinsList[i].index) && - tx != null && - tx.isLelantus == true && - !(tx.isConfirmed(currentChainHeight, MINIMUM_CONFIRMATIONS))) { - isUnconfirmed = true; + // TODO check if sane default + bool isUnconfirmed = false; + + if (tx != null) { + bool isConfirmed = tx.isConfirmed( + currentChainHeight, + MINIMUM_CONFIRMATIONS, + ); + if (!jindexes.contains(coin.index) && + tx.isLelantus == true && + !isConfirmed) { + isUnconfirmed = true; + } else if (!isConfirmed) { + continue; + } + } else { + final txn = await cachedElectrumXClient.getTransaction( + txHash: coin.txid, + verbose: true, + coin: this.coin, + ); + final confirmations = txn["confirmations"]; + isUnconfirmed = confirmations is int && confirmations < 1; } - - if (tx != null && - !tx.isConfirmed(currentChainHeight, MINIMUM_CONFIRMATIONS)) { - continue; - } - if (!lelantusCoinsList[i].isUsed && - lelantusCoinsList[i].anonymitySetId != ANONYMITY_SET_EMPTY_ID && + if (!coin.isUsed && + coin.anonymitySetId != ANONYMITY_SET_EMPTY_ID && !isUnconfirmed) { - coins.add(lelantusCoinsList[i]); + coins.add(coin); } } return coins; @@ -2427,62 +2407,65 @@ class FiroWallet extends CoinServiceAPI Future _refreshBalance() async { try { final utxosUpdateFuture = _refreshUTXOs(); - final List> lelantusCoins = - getLelantusCoinMap(); - if (lelantusCoins.isNotEmpty) { - lelantusCoins.removeWhere((element) => - element.values.any((elementCoin) => elementCoin.value == 0)); - } + final lelantusCoins = await db.isar.lelantusCoins + .where() + .walletIdEqualTo(walletId) + .filter() + .not() + .valueEqualTo(0.toString()) + .findAll(); final currentChainHeight = await chainHeight; final jindexes = firoGetJIndex(); int intLelantusBalance = 0; int unconfirmedLelantusBalance = 0; - for (final element in lelantusCoins) { - element.forEach((key, lelantusCoin) { - isar_models.Transaction? txn = db.isar.transactions - .where() - .txidWalletIdEqualTo( - lelantusCoin.txId, - walletId, - ) - .findFirstSync(); + for (final lelantusCoin in lelantusCoins) { + isar_models.Transaction? txn = db.isar.transactions + .where() + .txidWalletIdEqualTo( + lelantusCoin.txid, + walletId, + ) + .findFirstSync(); - if (txn == null) { - // TODO: ?????????????????????????????????????? - } else { - bool isLelantus = txn.isLelantus == true; - if (!jindexes!.contains(lelantusCoin.index) && isLelantus) { - if (!lelantusCoin.isUsed && - txn.isConfirmed(currentChainHeight, MINIMUM_CONFIRMATIONS)) { - // mint tx, add value to balance - intLelantusBalance += lelantusCoin.value; - } /* else { + if (txn == null) { + // TODO: ?????????????????????????????????????? + Logging.instance.log( + "Transaction not found in DB for lelantus coin: $lelantusCoin", + level: LogLevel.Fatal, + ); + } else { + bool isLelantus = txn.isLelantus == true; + if (!jindexes!.contains(lelantusCoin.index) && isLelantus) { + if (!lelantusCoin.isUsed && + txn.isConfirmed(currentChainHeight, MINIMUM_CONFIRMATIONS)) { + // mint tx, add value to balance + intLelantusBalance += int.parse(lelantusCoin.value); + } /* else { // This coin is not confirmed and may be replaced }*/ - } else if (jindexes.contains(lelantusCoin.index) && - isLelantus && - !lelantusCoin.isUsed && - !txn.isConfirmed(currentChainHeight, MINIMUM_CONFIRMATIONS)) { - unconfirmedLelantusBalance += lelantusCoin.value; - } else if (jindexes.contains(lelantusCoin.index) && - !lelantusCoin.isUsed) { - intLelantusBalance += lelantusCoin.value; - } else if (!lelantusCoin.isUsed && - (txn.isLelantus == true - ? true - : txn.isConfirmed( - currentChainHeight, MINIMUM_CONFIRMATIONS) != - false)) { - intLelantusBalance += lelantusCoin.value; - } else if (!isLelantus && - txn.isConfirmed(currentChainHeight, MINIMUM_CONFIRMATIONS) == - false) { - unconfirmedLelantusBalance += lelantusCoin.value; - } + } else if (jindexes.contains(lelantusCoin.index) && + isLelantus && + !lelantusCoin.isUsed && + !txn.isConfirmed(currentChainHeight, MINIMUM_CONFIRMATIONS)) { + unconfirmedLelantusBalance += int.parse(lelantusCoin.value); + } else if (jindexes.contains(lelantusCoin.index) && + !lelantusCoin.isUsed) { + intLelantusBalance += int.parse(lelantusCoin.value); + } else if (!lelantusCoin.isUsed && + (txn.isLelantus == true + ? true + : txn.isConfirmed( + currentChainHeight, MINIMUM_CONFIRMATIONS) != + false)) { + intLelantusBalance += int.parse(lelantusCoin.value); + } else if (!isLelantus && + txn.isConfirmed(currentChainHeight, MINIMUM_CONFIRMATIONS) == + false) { + unconfirmedLelantusBalance += int.parse(lelantusCoin.value); } - }); + } } _balancePrivate = Balance( @@ -2551,17 +2534,19 @@ class FiroWallet extends CoinServiceAPI } } - final List> lelantusCoins = getLelantusCoinMap(); - if (lelantusCoins.isNotEmpty) { - lelantusCoins.removeWhere((element) => - element.values.any((elementCoin) => elementCoin.value == 0)); - } + final lelantusCoins = await db.isar.lelantusCoins + .where() + .walletIdEqualTo(walletId) + .filter() + .not() + .valueEqualTo(0.toString()) + .findAll(); + final data = await _txnData; for (final value in data) { if (value.inputs.isNotEmpty) { for (var element in value.inputs) { - if (lelantusCoins - .any((element) => element.keys.contains(value.txid)) && + if (lelantusCoins.any((e) => e.txid == value.txid) && spendableOutputs.firstWhere( (output) => output?.txid == element.txid, orElse: () => null) != @@ -2844,7 +2829,14 @@ class FiroWallet extends CoinServiceAPI } Future _refreshLelantusData() async { - final List> lelantusCoins = getLelantusCoinMap(); + final lelantusCoins = await db.isar.lelantusCoins + .where() + .walletIdEqualTo(walletId) + .filter() + .not() + .valueEqualTo(0.toString()) + .findAll(); + final jindexes = firoGetJIndex(); // Get all joinsplit transaction ids @@ -2855,19 +2847,16 @@ class FiroWallet extends CoinServiceAPI .isLelantusEqualTo(true) .findAll(); List joinsplits = []; + for (final tx in listLelantusTxData) { if (tx.subType == isar_models.TransactionSubType.join) { joinsplits.add(tx.txid); } } - for (final coin - in lelantusCoins.fold([], (previousValue, element) { - (previousValue as List).add(element.values.first); - return previousValue; - })) { + for (final coin in lelantusCoins) { if (jindexes != null) { - if (jindexes.contains(coin.index) && !joinsplits.contains(coin.txId)) { - joinsplits.add(coin.txId); + if (jindexes.contains(coin.index) && !joinsplits.contains(coin.txid)) { + joinsplits.add(coin.txid); } } } @@ -3030,51 +3019,66 @@ class FiroWallet extends CoinServiceAPI Logging.instance.log( "_submitLelantusToNetwork txid: ${transactionInfo['txid']}", level: LogLevel.Info); + if (txid == transactionInfo['txid']) { final index = firoGetMintIndex(); - final List> lelantusCoins = - getLelantusCoinMap(); - List> coins; - if (lelantusCoins.isEmpty) { - coins = []; - } else { - coins = [...lelantusCoins]; - } if (transactionInfo['spendCoinIndexes'] != null) { // This is a joinsplit + final spentCoinIndexes = + transactionInfo['spendCoinIndexes'] as List; + final List updatedCoins = []; + // Update all of the coins that have been spent. - for (final lCoinMap in coins) { - final lCoin = lCoinMap.values.first; - if ((transactionInfo['spendCoinIndexes'] as List) - .contains(lCoin.index)) { - lCoinMap[lCoinMap.keys.first] = LelantusCoin( - lCoin.index, - lCoin.value, - lCoin.publicCoin, - lCoin.txId, - lCoin.anonymitySetId, - true); + + for (final index in spentCoinIndexes) { + final possibleCoins = await db.isar.lelantusCoins + .where() + .walletIdEqualTo(walletId) + .filter() + .indexEqualTo(index) + .findAll(); + + if (possibleCoins.isNotEmpty) { + if (possibleCoins.length > 1) { + print( + "======================= possibleCoins.length > 1 !!! ================================="); + } else { + final spentCoin = possibleCoins.first; + updatedCoins.add(spentCoin.copyWith(isUsed: true)); + } } } // if a jmint was made add it to the unspent coin index - LelantusCoin jmint = LelantusCoin( - index, - transactionInfo['jmintValue'] as int? ?? 0, - transactionInfo['publicCoin'] as String, - transactionInfo['txid'] as String, - latestSetId, - false); - if (jmint.value > 0) { - coins.add({jmint.txId: jmint}); + final jmint = isar_models.LelantusCoin( + walletId: walletId, + index: index, + value: (transactionInfo['jmintValue'] as int? ?? 0).toString(), + publicCoin: transactionInfo['publicCoin'] as String, + txid: transactionInfo['txid'] as String, + anonymitySetId: latestSetId, + isUsed: false, + ); + if (int.parse(jmint.value) > 0) { + updatedCoins.add(jmint); final jindexes = firoGetJIndex()!; jindexes.add(index); await firoUpdateJIndex(jindexes); await firoUpdateMintIndex(index + 1); } - await firoUpdateLelantusCoins(coins); + + await db.isar.writeTxn(() async { + for (final c in updatedCoins) { + await db.isar.lelantusCoins.deleteByPublicCoinWalletIdTxid( + c.publicCoin, + c.walletId, + c.txid, + ); + } + await db.isar.lelantusCoins.putAll(updatedCoins); + }); final amount = Amount.fromDecimal( Decimal.parse(transactionInfo["amount"].toString()), @@ -3087,14 +3091,8 @@ class FiroWallet extends CoinServiceAPI txid: transactionInfo['txid'] as String, timestamp: transactionInfo['timestamp'] as int? ?? (DateTime.now().millisecondsSinceEpoch ~/ 1000), - type: transactionInfo['txType'] == "Received" - ? isar_models.TransactionType.incoming - : isar_models.TransactionType.outgoing, - subType: transactionInfo["subType"] == "mint" - ? isar_models.TransactionSubType.mint - : transactionInfo["subType"] == "join" - ? isar_models.TransactionSubType.join - : isar_models.TransactionSubType.none, + type: isar_models.TransactionType.outgoing, + subType: isar_models.TransactionSubType.join, amount: amount.raw.toInt(), amountString: amount.toJsonString(), fee: Amount.fromDecimal( @@ -3137,25 +3135,30 @@ class FiroWallet extends CoinServiceAPI // This is a mint Logging.instance.log("this is a mint", level: LogLevel.Info); + final List updatedCoins = []; + // TODO: transactionInfo['mintsMap'] for (final mintMap in transactionInfo['mintsMap'] as List>) { - final index = mintMap['index'] as int?; - LelantusCoin mint = LelantusCoin( - index!, - mintMap['value'] as int, - mintMap['publicCoin'] as String, - transactionInfo['txid'] as String, - latestSetId, - false, + final index = mintMap['index'] as int; + final mint = isar_models.LelantusCoin( + walletId: walletId, + index: index, + value: (mintMap['value'] as int).toString(), + publicCoin: mintMap['publicCoin'] as String, + txid: transactionInfo['txid'] as String, + anonymitySetId: latestSetId, + isUsed: false, ); - if (mint.value > 0) { - coins.add({mint.txId: mint}); + if (int.parse(mint.value) > 0) { + updatedCoins.add(mint); await firoUpdateMintIndex(index + 1); } } // Logging.instance.log(coins); - await firoUpdateLelantusCoins(coins); + await db.isar.writeTxn(() async { + await db.isar.lelantusCoins.putAll(updatedCoins); + }); } return true; } else { @@ -4556,6 +4559,7 @@ class FiroWallet extends CoinServiceAPI "setDataMap": setDataMap, "usedSerialNumbers": usedSerialNumbers, "network": _network, + "walletId": walletId, }); await Future.wait([dataFuture]); @@ -4576,7 +4580,10 @@ class FiroWallet extends CoinServiceAPI await Future.wait([ firoUpdateMintIndex(message['mintIndex'] as int), - firoUpdateLelantusCoins(message['_lelantus_coins'] as List), + db.isar.writeTxn(() async { + await db.isar.lelantusCoins.putAll( + message['_lelantus_coins'] as List); + }), firoUpdateJIndex(message['jindex'] as List), ]); diff --git a/lib/services/mixins/firo_hive.dart b/lib/services/mixins/firo_hive.dart index be9845453..631aebe6d 100644 --- a/lib/services/mixins/firo_hive.dart +++ b/lib/services/mixins/firo_hive.dart @@ -30,19 +30,19 @@ mixin FiroHive { ); } - // _lelantus_coins - List? firoGetLelantusCoins() { - return DB.instance.get(boxName: _walletId, key: "_lelantus_coins") - as List?; - } - - Future firoUpdateLelantusCoins(List lelantusCoins) async { - await DB.instance.put( - boxName: _walletId, - key: "_lelantus_coins", - value: lelantusCoins, - ); - } + // // _lelantus_coins + // List? firoGetLelantusCoins() { + // return DB.instance.get(boxName: _walletId, key: "_lelantus_coins") + // as List?; + // } + // + // Future firoUpdateLelantusCoins(List lelantusCoins) async { + // await DB.instance.put( + // boxName: _walletId, + // key: "_lelantus_coins", + // value: lelantusCoins, + // ); + // } // mintIndex int firoGetMintIndex() { diff --git a/lib/utilities/db_version_migration.dart b/lib/utilities/db_version_migration.dart index 3f69161f4..dac1dbe82 100644 --- a/lib/utilities/db_version_migration.dart +++ b/lib/utilities/db_version_migration.dart @@ -388,7 +388,8 @@ class DbVersionMigrator with WalletDB { final List coins = []; for (final e in hiveLCoins) { - final lcoin = e as LelantusCoin; + final map = e as Map; + final lcoin = map.values.first as LelantusCoin; final coin = isar_models.LelantusCoin( walletId: walletId, From a286ba7b8f3dbc088ed3a811b27d7102778d2658 Mon Sep 17 00:00:00 2001 From: ryleedavis Date: Mon, 24 Jul 2023 12:43:33 -0600 Subject: [PATCH 108/169] monkey view page --- lib/pages/monkey/monkey_view.dart | 164 ++++++++++++++++++++++-------- 1 file changed, 119 insertions(+), 45 deletions(-) diff --git a/lib/pages/monkey/monkey_view.dart b/lib/pages/monkey/monkey_view.dart index e74d5bbdf..10b9c91bf 100644 --- a/lib/pages/monkey/monkey_view.dart +++ b/lib/pages/monkey/monkey_view.dart @@ -7,11 +7,13 @@ import 'package:stackwallet/pages/monkey/sub_widgets/fetch_monkey_dialog.dart'; import 'package:stackwallet/services/coins/manager.dart'; import 'package:stackwallet/themes/coin_icon_provider.dart'; import 'package:stackwallet/themes/stack_colors.dart'; +import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/widgets/background.dart'; import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; import 'package:stackwallet/widgets/desktop/primary_button.dart'; +import 'package:stackwallet/widgets/desktop/secondary_button.dart'; class MonkeyView extends ConsumerStatefulWidget { const MonkeyView({ @@ -50,6 +52,7 @@ class _MonkeyViewState extends ConsumerState { @override Widget build(BuildContext context) { final Coin coin = ref.watch(managerProvider.select((value) => value.coin)); + bool isMonkey = false; return Background( child: Stack( @@ -65,60 +68,131 @@ class _MonkeyViewState extends ConsumerState { "MonKey", style: STextStyles.navBarTitle(context), ), + actions: [ + AspectRatio( + aspectRatio: 1, + child: AppBarIconButton( + icon: SvgPicture.asset(Assets.svg.circleQuestion), + onPressed: () { + showDialog( + context: context, + useSafeArea: false, + barrierDismissible: true, + builder: (context) { + return Dialog( + child: Material( + borderRadius: BorderRadius.circular( + 20, + ), + child: Container( + height: 200, + decoration: BoxDecoration( + color: Theme.of(context) + .extension()! + .popupBG, + borderRadius: BorderRadius.circular( + 20, + ), + ), + child: Column( + children: [ + Center( + child: Text( + "Help", + style: STextStyles.pageTitleH2( + context), + ), + ) + ], + ), + ), + ), + ); + }); + }), + ) + ], ), - body: Column( - // mainAxisAlignment: MainAxisAlignment.center, - // crossAxisAlignment: CrossAxisAlignment.center, - children: [ - const Spacer(), - Center( - child: Column( + body: isMonkey + ? Column( children: [ - SvgPicture.file( - File( - ref.watch(coinIconProvider(coin)), + Spacer(), + Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + children: [ + SecondaryButton( + label: "Download as SVG", + onPressed: () {}, + ), + const SizedBox(height: 12), + SecondaryButton( + label: "Download as PNG", + onPressed: () {}, + ), + ], ), - width: 164, - height: 164, ), - const SizedBox( - height: 40, + ], + ) + : Column( + children: [ + const Spacer( + flex: 4, ), - Text( - "You do not have a MonKey yet. \nFetch yours now!", - style: STextStyles.smallMed14(context).copyWith( - color: Theme.of(context) - .extension()! - .textDark3, + Center( + child: Column( + children: [ + Opacity( + opacity: 0.2, + child: SvgPicture.file( + File( + ref.watch(coinIconProvider(coin)), + ), + width: 200, + height: 200, + ), + ), + const SizedBox( + height: 40, + ), + Text( + "You do not have a MonKey yet. \nFetch yours now!", + style: STextStyles.smallMed14(context).copyWith( + color: Theme.of(context) + .extension()! + .textDark3, + ), + textAlign: TextAlign.center, + ), + ], + ), + ), + const Spacer( + flex: 6, + ), + Padding( + padding: const EdgeInsets.all(16.0), + child: PrimaryButton( + label: "Fetch MonKey", + onPressed: () { + showDialog( + context: context, + useSafeArea: false, + barrierDismissible: false, + builder: (context) { + return FetchMonkeyDialog( + onCancel: () async { + Navigator.of(context).pop(); + }, + ); + }, + ); + }, ), - textAlign: TextAlign.center, ), ], ), - ), - const Spacer(), - Padding( - padding: const EdgeInsets.all(16.0), - child: PrimaryButton( - label: "Fetch MonKey", - onPressed: () { - showDialog( - context: context, - useSafeArea: false, - barrierDismissible: false, - builder: (context) { - return FetchMonkeyDialog( - onCancel: () async { - Navigator.of(context).pop(); - }, - ); - }, - ); - }, - ), - ), - ], - ), ), ], ), From c97de6017bec6a8dcf5d51ab6467464e3b2b223f Mon Sep 17 00:00:00 2001 From: julian Date: Mon, 24 Jul 2023 15:42:45 -0600 Subject: [PATCH 109/169] update lelantus coin schema --- lib/db/isar/main_db.dart | 1 - .../models/firo_specific/lelantus_coin.dart | 29 +- .../models/firo_specific/lelantus_coin.g.dart | 663 +++++++++++++++--- lib/services/coins/firo/firo_wallet.dart | 24 +- lib/utilities/db_version_migration.dart | 11 +- test/services/coins/manager_test.mocks.dart | 55 -- .../transaction_card_test.mocks.dart | 54 -- 7 files changed, 634 insertions(+), 203 deletions(-) diff --git a/lib/db/isar/main_db.dart b/lib/db/isar/main_db.dart index 209f13c5d..fa565c0cd 100644 --- a/lib/db/isar/main_db.dart +++ b/lib/db/isar/main_db.dart @@ -14,7 +14,6 @@ import 'package:isar/isar.dart'; import 'package:stackwallet/exceptions/main_db/main_db_exception.dart'; import 'package:stackwallet/models/isar/models/block_explorer.dart'; import 'package:stackwallet/models/isar/models/contact_entry.dart'; -import 'package:stackwallet/models/isar/models/firo_specific/lelantus_coin.dart'; import 'package:stackwallet/models/isar/models/isar_models.dart'; import 'package:stackwallet/models/isar/stack_theme.dart'; import 'package:stackwallet/utilities/amount/amount.dart'; diff --git a/lib/models/isar/models/firo_specific/lelantus_coin.dart b/lib/models/isar/models/firo_specific/lelantus_coin.dart index 3dd48b2dc..9ad5bc5a3 100644 --- a/lib/models/isar/models/firo_specific/lelantus_coin.dart +++ b/lib/models/isar/models/firo_specific/lelantus_coin.dart @@ -23,20 +23,33 @@ class LelantusCoin { final String value; // can't use BigInt in isar :shrug: - final int index; + @Index( + unique: true, + replace: false, + composite: [ + CompositeIndex("walletId"), + ], + ) + final int mintIndex; final int anonymitySetId; final bool isUsed; + final bool isJMint; + + final String? otherData; + LelantusCoin({ required this.walletId, required this.publicCoin, required this.txid, required this.value, - required this.index, + required this.mintIndex, required this.anonymitySetId, required this.isUsed, + required this.isJMint, + required this.otherData, }); LelantusCoin copyWith({ @@ -44,18 +57,22 @@ class LelantusCoin { String? publicCoin, String? txid, String? value, - int? index, + int? mintIndex, int? anonymitySetId, bool? isUsed, + bool? isJMint, + String? otherData, }) { return LelantusCoin( walletId: walletId ?? this.walletId, publicCoin: publicCoin ?? this.publicCoin, txid: txid ?? this.txid, value: value ?? this.value, - index: index ?? this.index, + mintIndex: mintIndex ?? this.mintIndex, anonymitySetId: anonymitySetId ?? this.anonymitySetId, isUsed: isUsed ?? this.isUsed, + isJMint: isJMint ?? this.isJMint, + otherData: otherData ?? this.otherData, ); } @@ -67,8 +84,10 @@ class LelantusCoin { 'publicCoin: $publicCoin, ' 'txid: $txid, ' 'value: $value, ' - 'index: $index, ' + 'mintIndex: $mintIndex, ' 'anonymitySetId: $anonymitySetId, ' + 'otherData: $otherData, ' + 'isJMint: $isJMint, ' 'isUsed: $isUsed' '}'; } diff --git a/lib/models/isar/models/firo_specific/lelantus_coin.g.dart b/lib/models/isar/models/firo_specific/lelantus_coin.g.dart index 8fafde571..d2e27638d 100644 --- a/lib/models/isar/models/firo_specific/lelantus_coin.g.dart +++ b/lib/models/isar/models/firo_specific/lelantus_coin.g.dart @@ -22,33 +22,43 @@ const LelantusCoinSchema = CollectionSchema( name: r'anonymitySetId', type: IsarType.long, ), - r'index': PropertySchema( + r'isJMint': PropertySchema( id: 1, - name: r'index', - type: IsarType.long, + name: r'isJMint', + type: IsarType.bool, ), r'isUsed': PropertySchema( id: 2, name: r'isUsed', type: IsarType.bool, ), - r'publicCoin': PropertySchema( + r'mintIndex': PropertySchema( id: 3, + name: r'mintIndex', + type: IsarType.long, + ), + r'otherData': PropertySchema( + id: 4, + name: r'otherData', + type: IsarType.string, + ), + r'publicCoin': PropertySchema( + id: 5, name: r'publicCoin', type: IsarType.string, ), r'txid': PropertySchema( - id: 4, + id: 6, name: r'txid', type: IsarType.string, ), r'value': PropertySchema( - id: 5, + id: 7, name: r'value', type: IsarType.string, ), r'walletId': PropertySchema( - id: 6, + id: 8, name: r'walletId', type: IsarType.string, ) @@ -94,6 +104,24 @@ const LelantusCoinSchema = CollectionSchema( caseSensitive: true, ) ], + ), + r'mintIndex_walletId': IndexSchema( + id: -9147309777276196770, + name: r'mintIndex_walletId', + unique: true, + replace: false, + properties: [ + IndexPropertySchema( + name: r'mintIndex', + type: IndexType.value, + caseSensitive: false, + ), + IndexPropertySchema( + name: r'walletId', + type: IndexType.hash, + caseSensitive: true, + ) + ], ) }, links: {}, @@ -110,6 +138,12 @@ int _lelantusCoinEstimateSize( Map> allOffsets, ) { var bytesCount = offsets.last; + { + final value = object.otherData; + if (value != null) { + bytesCount += 3 + value.length * 3; + } + } bytesCount += 3 + object.publicCoin.length * 3; bytesCount += 3 + object.txid.length * 3; bytesCount += 3 + object.value.length * 3; @@ -124,12 +158,14 @@ void _lelantusCoinSerialize( Map> allOffsets, ) { writer.writeLong(offsets[0], object.anonymitySetId); - writer.writeLong(offsets[1], object.index); + writer.writeBool(offsets[1], object.isJMint); writer.writeBool(offsets[2], object.isUsed); - writer.writeString(offsets[3], object.publicCoin); - writer.writeString(offsets[4], object.txid); - writer.writeString(offsets[5], object.value); - writer.writeString(offsets[6], object.walletId); + writer.writeLong(offsets[3], object.mintIndex); + writer.writeString(offsets[4], object.otherData); + writer.writeString(offsets[5], object.publicCoin); + writer.writeString(offsets[6], object.txid); + writer.writeString(offsets[7], object.value); + writer.writeString(offsets[8], object.walletId); } LelantusCoin _lelantusCoinDeserialize( @@ -140,12 +176,14 @@ LelantusCoin _lelantusCoinDeserialize( ) { final object = LelantusCoin( anonymitySetId: reader.readLong(offsets[0]), - index: reader.readLong(offsets[1]), + isJMint: reader.readBool(offsets[1]), isUsed: reader.readBool(offsets[2]), - publicCoin: reader.readString(offsets[3]), - txid: reader.readString(offsets[4]), - value: reader.readString(offsets[5]), - walletId: reader.readString(offsets[6]), + mintIndex: reader.readLong(offsets[3]), + otherData: reader.readStringOrNull(offsets[4]), + publicCoin: reader.readString(offsets[5]), + txid: reader.readString(offsets[6]), + value: reader.readString(offsets[7]), + walletId: reader.readString(offsets[8]), ); object.id = id; return object; @@ -161,17 +199,21 @@ P _lelantusCoinDeserializeProp

( case 0: return (reader.readLong(offset)) as P; case 1: - return (reader.readLong(offset)) as P; + return (reader.readBool(offset)) as P; case 2: return (reader.readBool(offset)) as P; case 3: - return (reader.readString(offset)) as P; + return (reader.readLong(offset)) as P; case 4: - return (reader.readString(offset)) as P; + return (reader.readStringOrNull(offset)) as P; case 5: return (reader.readString(offset)) as P; case 6: return (reader.readString(offset)) as P; + case 7: + return (reader.readString(offset)) as P; + case 8: + return (reader.readString(offset)) as P; default: throw IsarError('Unknown property with id $propertyId'); } @@ -290,6 +332,92 @@ extension LelantusCoinByIndex on IsarCollection { return putAllByIndexSync(r'publicCoin_walletId_txid', objects, saveLinks: saveLinks); } + + Future getByMintIndexWalletId(int mintIndex, String walletId) { + return getByIndex(r'mintIndex_walletId', [mintIndex, walletId]); + } + + LelantusCoin? getByMintIndexWalletIdSync(int mintIndex, String walletId) { + return getByIndexSync(r'mintIndex_walletId', [mintIndex, walletId]); + } + + Future deleteByMintIndexWalletId(int mintIndex, String walletId) { + return deleteByIndex(r'mintIndex_walletId', [mintIndex, walletId]); + } + + bool deleteByMintIndexWalletIdSync(int mintIndex, String walletId) { + return deleteByIndexSync(r'mintIndex_walletId', [mintIndex, walletId]); + } + + Future> getAllByMintIndexWalletId( + List mintIndexValues, List walletIdValues) { + final len = mintIndexValues.length; + assert(walletIdValues.length == len, + 'All index values must have the same length'); + final values = >[]; + for (var i = 0; i < len; i++) { + values.add([mintIndexValues[i], walletIdValues[i]]); + } + + return getAllByIndex(r'mintIndex_walletId', values); + } + + List getAllByMintIndexWalletIdSync( + List mintIndexValues, List walletIdValues) { + final len = mintIndexValues.length; + assert(walletIdValues.length == len, + 'All index values must have the same length'); + final values = >[]; + for (var i = 0; i < len; i++) { + values.add([mintIndexValues[i], walletIdValues[i]]); + } + + return getAllByIndexSync(r'mintIndex_walletId', values); + } + + Future deleteAllByMintIndexWalletId( + List mintIndexValues, List walletIdValues) { + final len = mintIndexValues.length; + assert(walletIdValues.length == len, + 'All index values must have the same length'); + final values = >[]; + for (var i = 0; i < len; i++) { + values.add([mintIndexValues[i], walletIdValues[i]]); + } + + return deleteAllByIndex(r'mintIndex_walletId', values); + } + + int deleteAllByMintIndexWalletIdSync( + List mintIndexValues, List walletIdValues) { + final len = mintIndexValues.length; + assert(walletIdValues.length == len, + 'All index values must have the same length'); + final values = >[]; + for (var i = 0; i < len; i++) { + values.add([mintIndexValues[i], walletIdValues[i]]); + } + + return deleteAllByIndexSync(r'mintIndex_walletId', values); + } + + Future putByMintIndexWalletId(LelantusCoin object) { + return putByIndex(r'mintIndex_walletId', object); + } + + Id putByMintIndexWalletIdSync(LelantusCoin object, {bool saveLinks = true}) { + return putByIndexSync(r'mintIndex_walletId', object, saveLinks: saveLinks); + } + + Future> putAllByMintIndexWalletId(List objects) { + return putAllByIndex(r'mintIndex_walletId', objects); + } + + List putAllByMintIndexWalletIdSync(List objects, + {bool saveLinks = true}) { + return putAllByIndexSync(r'mintIndex_walletId', objects, + saveLinks: saveLinks); + } } extension LelantusCoinQueryWhereSort @@ -552,6 +680,144 @@ extension LelantusCoinQueryWhere } }); } + + QueryBuilder + mintIndexEqualToAnyWalletId(int mintIndex) { + return QueryBuilder.apply(this, (query) { + return query.addWhereClause(IndexWhereClause.equalTo( + indexName: r'mintIndex_walletId', + value: [mintIndex], + )); + }); + } + + QueryBuilder + mintIndexNotEqualToAnyWalletId(int mintIndex) { + return QueryBuilder.apply(this, (query) { + if (query.whereSort == Sort.asc) { + return query + .addWhereClause(IndexWhereClause.between( + indexName: r'mintIndex_walletId', + lower: [], + upper: [mintIndex], + includeUpper: false, + )) + .addWhereClause(IndexWhereClause.between( + indexName: r'mintIndex_walletId', + lower: [mintIndex], + includeLower: false, + upper: [], + )); + } else { + return query + .addWhereClause(IndexWhereClause.between( + indexName: r'mintIndex_walletId', + lower: [mintIndex], + includeLower: false, + upper: [], + )) + .addWhereClause(IndexWhereClause.between( + indexName: r'mintIndex_walletId', + lower: [], + upper: [mintIndex], + includeUpper: false, + )); + } + }); + } + + QueryBuilder + mintIndexGreaterThanAnyWalletId( + int mintIndex, { + bool include = false, + }) { + return QueryBuilder.apply(this, (query) { + return query.addWhereClause(IndexWhereClause.between( + indexName: r'mintIndex_walletId', + lower: [mintIndex], + includeLower: include, + upper: [], + )); + }); + } + + QueryBuilder + mintIndexLessThanAnyWalletId( + int mintIndex, { + bool include = false, + }) { + return QueryBuilder.apply(this, (query) { + return query.addWhereClause(IndexWhereClause.between( + indexName: r'mintIndex_walletId', + lower: [], + upper: [mintIndex], + includeUpper: include, + )); + }); + } + + QueryBuilder + mintIndexBetweenAnyWalletId( + int lowerMintIndex, + int upperMintIndex, { + bool includeLower = true, + bool includeUpper = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addWhereClause(IndexWhereClause.between( + indexName: r'mintIndex_walletId', + lower: [lowerMintIndex], + includeLower: includeLower, + upper: [upperMintIndex], + includeUpper: includeUpper, + )); + }); + } + + QueryBuilder + mintIndexWalletIdEqualTo(int mintIndex, String walletId) { + return QueryBuilder.apply(this, (query) { + return query.addWhereClause(IndexWhereClause.equalTo( + indexName: r'mintIndex_walletId', + value: [mintIndex, walletId], + )); + }); + } + + QueryBuilder + mintIndexEqualToWalletIdNotEqualTo(int mintIndex, String walletId) { + return QueryBuilder.apply(this, (query) { + if (query.whereSort == Sort.asc) { + return query + .addWhereClause(IndexWhereClause.between( + indexName: r'mintIndex_walletId', + lower: [mintIndex], + upper: [mintIndex, walletId], + includeUpper: false, + )) + .addWhereClause(IndexWhereClause.between( + indexName: r'mintIndex_walletId', + lower: [mintIndex, walletId], + includeLower: false, + upper: [mintIndex], + )); + } else { + return query + .addWhereClause(IndexWhereClause.between( + indexName: r'mintIndex_walletId', + lower: [mintIndex, walletId], + includeLower: false, + upper: [mintIndex], + )) + .addWhereClause(IndexWhereClause.between( + indexName: r'mintIndex_walletId', + lower: [mintIndex], + upper: [mintIndex, walletId], + includeUpper: false, + )); + } + }); + } } extension LelantusCoinQueryFilter @@ -665,60 +931,16 @@ extension LelantusCoinQueryFilter }); } - QueryBuilder indexEqualTo( - int value) { + QueryBuilder + isJMintEqualTo(bool value) { return QueryBuilder.apply(this, (query) { return query.addFilterCondition(FilterCondition.equalTo( - property: r'index', + property: r'isJMint', value: value, )); }); } - QueryBuilder - indexGreaterThan( - int value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.greaterThan( - include: include, - property: r'index', - value: value, - )); - }); - } - - QueryBuilder indexLessThan( - int value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.lessThan( - include: include, - property: r'index', - value: value, - )); - }); - } - - QueryBuilder indexBetween( - int lower, - int upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.between( - property: r'index', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - )); - }); - } - QueryBuilder isUsedEqualTo( bool value) { return QueryBuilder.apply(this, (query) { @@ -729,6 +951,216 @@ extension LelantusCoinQueryFilter }); } + QueryBuilder + mintIndexEqualTo(int value) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'mintIndex', + value: value, + )); + }); + } + + QueryBuilder + mintIndexGreaterThan( + int value, { + bool include = false, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + include: include, + property: r'mintIndex', + value: value, + )); + }); + } + + QueryBuilder + mintIndexLessThan( + int value, { + bool include = false, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.lessThan( + include: include, + property: r'mintIndex', + value: value, + )); + }); + } + + QueryBuilder + mintIndexBetween( + int lower, + int upper, { + bool includeLower = true, + bool includeUpper = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.between( + property: r'mintIndex', + lower: lower, + includeLower: includeLower, + upper: upper, + includeUpper: includeUpper, + )); + }); + } + + QueryBuilder + otherDataIsNull() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(const FilterCondition.isNull( + property: r'otherData', + )); + }); + } + + QueryBuilder + otherDataIsNotNull() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(const FilterCondition.isNotNull( + property: r'otherData', + )); + }); + } + + QueryBuilder + otherDataEqualTo( + String? value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'otherData', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + otherDataGreaterThan( + String? value, { + bool include = false, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + include: include, + property: r'otherData', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + otherDataLessThan( + String? value, { + bool include = false, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.lessThan( + include: include, + property: r'otherData', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + otherDataBetween( + 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'otherData', + lower: lower, + includeLower: includeLower, + upper: upper, + includeUpper: includeUpper, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + otherDataStartsWith( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.startsWith( + property: r'otherData', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + otherDataEndsWith( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.endsWith( + property: r'otherData', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + otherDataContains(String value, {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.contains( + property: r'otherData', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + otherDataMatches(String pattern, {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.matches( + property: r'otherData', + wildcard: pattern, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + otherDataIsEmpty() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'otherData', + value: '', + )); + }); + } + + QueryBuilder + otherDataIsNotEmpty() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + property: r'otherData', + value: '', + )); + }); + } + QueryBuilder publicCoinEqualTo( String value, { @@ -1292,15 +1724,15 @@ extension LelantusCoinQuerySortBy }); } - QueryBuilder sortByIndex() { + QueryBuilder sortByIsJMint() { return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'index', Sort.asc); + return query.addSortBy(r'isJMint', Sort.asc); }); } - QueryBuilder sortByIndexDesc() { + QueryBuilder sortByIsJMintDesc() { return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'index', Sort.desc); + return query.addSortBy(r'isJMint', Sort.desc); }); } @@ -1316,6 +1748,30 @@ extension LelantusCoinQuerySortBy }); } + QueryBuilder sortByMintIndex() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'mintIndex', Sort.asc); + }); + } + + QueryBuilder sortByMintIndexDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'mintIndex', Sort.desc); + }); + } + + QueryBuilder sortByOtherData() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'otherData', Sort.asc); + }); + } + + QueryBuilder sortByOtherDataDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'otherData', Sort.desc); + }); + } + QueryBuilder sortByPublicCoin() { return QueryBuilder.apply(this, (query) { return query.addSortBy(r'publicCoin', Sort.asc); @@ -1394,15 +1850,15 @@ extension LelantusCoinQuerySortThenBy }); } - QueryBuilder thenByIndex() { + QueryBuilder thenByIsJMint() { return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'index', Sort.asc); + return query.addSortBy(r'isJMint', Sort.asc); }); } - QueryBuilder thenByIndexDesc() { + QueryBuilder thenByIsJMintDesc() { return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'index', Sort.desc); + return query.addSortBy(r'isJMint', Sort.desc); }); } @@ -1418,6 +1874,30 @@ extension LelantusCoinQuerySortThenBy }); } + QueryBuilder thenByMintIndex() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'mintIndex', Sort.asc); + }); + } + + QueryBuilder thenByMintIndexDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'mintIndex', Sort.desc); + }); + } + + QueryBuilder thenByOtherData() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'otherData', Sort.asc); + }); + } + + QueryBuilder thenByOtherDataDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'otherData', Sort.desc); + }); + } + QueryBuilder thenByPublicCoin() { return QueryBuilder.apply(this, (query) { return query.addSortBy(r'publicCoin', Sort.asc); @@ -1477,9 +1957,9 @@ extension LelantusCoinQueryWhereDistinct }); } - QueryBuilder distinctByIndex() { + QueryBuilder distinctByIsJMint() { return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'index'); + return query.addDistinctBy(r'isJMint'); }); } @@ -1489,6 +1969,19 @@ extension LelantusCoinQueryWhereDistinct }); } + QueryBuilder distinctByMintIndex() { + return QueryBuilder.apply(this, (query) { + return query.addDistinctBy(r'mintIndex'); + }); + } + + QueryBuilder distinctByOtherData( + {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addDistinctBy(r'otherData', caseSensitive: caseSensitive); + }); + } + QueryBuilder distinctByPublicCoin( {bool caseSensitive = true}) { return QueryBuilder.apply(this, (query) { @@ -1532,9 +2025,9 @@ extension LelantusCoinQueryProperty }); } - QueryBuilder indexProperty() { + QueryBuilder isJMintProperty() { return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'index'); + return query.addPropertyName(r'isJMint'); }); } @@ -1544,6 +2037,18 @@ extension LelantusCoinQueryProperty }); } + QueryBuilder mintIndexProperty() { + return QueryBuilder.apply(this, (query) { + return query.addPropertyName(r'mintIndex'); + }); + } + + QueryBuilder otherDataProperty() { + return QueryBuilder.apply(this, (query) { + return query.addPropertyName(r'otherData'); + }); + } + QueryBuilder publicCoinProperty() { return QueryBuilder.apply(this, (query) { return query.addPropertyName(r'publicCoin'); diff --git a/lib/services/coins/firo/firo_wallet.dart b/lib/services/coins/firo/firo_wallet.dart index 004e5399c..67882a6b6 100644 --- a/lib/services/coins/firo/firo_wallet.dart +++ b/lib/services/coins/firo/firo_wallet.dart @@ -269,18 +269,20 @@ Future> isolateRestore( lelantusCoins.removeWhere((e) => e.txid == txId && - e.index == currentIndex && + e.mintIndex == currentIndex && e.anonymitySetId != setId); lelantusCoins.add( isar_models.LelantusCoin( walletId: walletId, - index: currentIndex, + mintIndex: currentIndex, value: amount.toString(), publicCoin: publicCoin, txid: txId, anonymitySetId: setId, isUsed: isUsed, + isJMint: false, + otherData: null, ), ); Logging.instance.log( @@ -317,18 +319,20 @@ Future> isolateRestore( bool isUsed = usedSerialNumbersSet.contains(serialNumber); lelantusCoins.removeWhere((e) => e.txid == txId && - e.index == currentIndex && + e.mintIndex == currentIndex && e.anonymitySetId != setId); lelantusCoins.add( isar_models.LelantusCoin( walletId: walletId, - index: currentIndex, + mintIndex: currentIndex, value: amount.toString(), publicCoin: publicCoin, txid: txId, anonymitySetId: setId, isUsed: isUsed, + isJMint: true, + otherData: null, ), ); jindexes.add(currentIndex); @@ -2325,7 +2329,7 @@ class FiroWallet extends CoinServiceAPI final derivePath = constructDerivePath( networkWIF: _network.wif, chain: MINT_INDEX, - index: coin.index, + index: coin.mintIndex, ); final keyPair = await Bip32Utils.getBip32NodeFromRoot(root, derivePath); @@ -2335,7 +2339,7 @@ class FiroWallet extends CoinServiceAPI } final String privateKey = Format.uint8listToString(keyPair.privateKey!); return DartLelantusEntry(coin.isUsed ? 1 : 0, 0, coin.anonymitySetId, - int.parse(coin.value), coin.index, privateKey); + int.parse(coin.value), coin.mintIndex, privateKey); }).toList(); final lelantusEntries = await Future.wait(waitLelantusEntries); @@ -3054,12 +3058,14 @@ class FiroWallet extends CoinServiceAPI // if a jmint was made add it to the unspent coin index final jmint = isar_models.LelantusCoin( walletId: walletId, - index: index, + mintIndex: nextFreeMintIndex, value: (transactionInfo['jmintValue'] as int? ?? 0).toString(), publicCoin: transactionInfo['publicCoin'] as String, txid: transactionInfo['txid'] as String, anonymitySetId: latestSetId, isUsed: false, + isJMint: true, + otherData: null, ); if (int.parse(jmint.value) > 0) { updatedCoins.add(jmint); @@ -3143,12 +3149,14 @@ class FiroWallet extends CoinServiceAPI final index = mintMap['index'] as int; final mint = isar_models.LelantusCoin( walletId: walletId, - index: index, + mintIndex: index, value: (mintMap['value'] as int).toString(), publicCoin: mintMap['publicCoin'] as String, txid: transactionInfo['txid'] as String, anonymitySetId: latestSetId, isUsed: false, + isJMint: false, + otherData: null, ); if (int.parse(mint.value) > 0) { updatedCoins.add(mint); diff --git a/lib/utilities/db_version_migration.dart b/lib/utilities/db_version_migration.dart index dac1dbe82..d2c0adbe8 100644 --- a/lib/utilities/db_version_migration.dart +++ b/lib/utilities/db_version_migration.dart @@ -386,19 +386,28 @@ class DbVersionMigrator with WalletDB { ) as List? ?? []; + final jindexes = (DB.instance + .get(boxName: walletId, key: "jindex") as List? ?? + []) + .cast(); + final List coins = []; for (final e in hiveLCoins) { final map = e as Map; final lcoin = map.values.first as LelantusCoin; + final isJMint = jindexes.contains(lcoin.index); + final coin = isar_models.LelantusCoin( walletId: walletId, publicCoin: lcoin.publicCoin, txid: lcoin.txId, value: lcoin.value.toString(), - index: lcoin.index, + mintIndex: lcoin.index, anonymitySetId: lcoin.anonymitySetId, isUsed: lcoin.isUsed, + isJMint: isJMint, + otherData: null, ); coins.add(coin); diff --git a/test/services/coins/manager_test.mocks.dart b/test/services/coins/manager_test.mocks.dart index 8b48a38a9..25fd46d9d 100644 --- a/test/services/coins/manager_test.mocks.dart +++ b/test/services/coins/manager_test.mocks.dart @@ -14,7 +14,6 @@ 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; import 'package:stackwallet/models/isar/models/isar_models.dart' as _i13; -import 'package:stackwallet/models/lelantus_coin.dart' as _i15; import 'package:stackwallet/models/paymint/fee_object_model.dart' as _i3; import 'package:stackwallet/models/signing_data.dart' as _i14; import 'package:stackwallet/services/coins/firo/firo_wallet.dart' as _i10; @@ -589,15 +588,6 @@ class MockFiroWallet extends _i1.Mock implements _i10.FiroWallet { returnValueForMissingStub: _i11.Future.value(), ) as _i11.Future); @override - List> getLelantusCoinMap() => - (super.noSuchMethod( - Invocation.method( - #getLelantusCoinMap, - [], - ), - returnValue: >[], - ) as List>); - @override _i11.Future anonymizeAllPublicFunds() => (super.noSuchMethod( Invocation.method( #anonymizeAllPublicFunds, @@ -1061,51 +1051,6 @@ class MockFiroWallet extends _i1.Mock implements _i10.FiroWallet { ), returnValueForMissingStub: null, ); - @override - void initFiroHive(String? walletId) => super.noSuchMethod( - Invocation.method( - #initFiroHive, - [walletId], - ), - returnValueForMissingStub: null, - ); - @override - _i11.Future firoUpdateJIndex(List? jIndex) => - (super.noSuchMethod( - Invocation.method( - #firoUpdateJIndex, - [jIndex], - ), - returnValue: _i11.Future.value(), - returnValueForMissingStub: _i11.Future.value(), - ) as _i11.Future); - @override - _i11.Future firoUpdateLelantusCoins(List? lelantusCoins) => - (super.noSuchMethod( - Invocation.method( - #firoUpdateLelantusCoins, - [lelantusCoins], - ), - returnValue: _i11.Future.value(), - returnValueForMissingStub: _i11.Future.value(), - ) as _i11.Future); - @override - int firoGetMintIndex() => (super.noSuchMethod( - Invocation.method( - #firoGetMintIndex, - [], - ), - returnValue: 0, - ) as int); - @override - _i11.Future firoUpdateMintIndex(int? mintIndex) => (super.noSuchMethod( - Invocation.method( - #firoUpdateMintIndex, - [mintIndex], - ), - returnValue: _i11.Future.value(), - returnValueForMissingStub: _i11.Future.value(), - ) as _i11.Future); } /// A class which mocks [ElectrumX]. diff --git a/test/widget_tests/transaction_card_test.mocks.dart b/test/widget_tests/transaction_card_test.mocks.dart index 4fc6f8832..29bd7bd29 100644 --- a/test/widget_tests/transaction_card_test.mocks.dart +++ b/test/widget_tests/transaction_card_test.mocks.dart @@ -1602,15 +1602,6 @@ class MockFiroWallet extends _i1.Mock implements _i23.FiroWallet { returnValueForMissingStub: _i19.Future.value(), ) as _i19.Future); @override - List> getLelantusCoinMap() => - (super.noSuchMethod( - Invocation.method( - #getLelantusCoinMap, - [], - ), - returnValue: >[], - ) as List>); - @override _i19.Future anonymizeAllPublicFunds() => (super.noSuchMethod( Invocation.method( #anonymizeAllPublicFunds, @@ -2075,51 +2066,6 @@ class MockFiroWallet extends _i1.Mock implements _i23.FiroWallet { ), returnValueForMissingStub: null, ); - @override - void initFiroHive(String? walletId) => super.noSuchMethod( - Invocation.method( - #initFiroHive, - [walletId], - ), - returnValueForMissingStub: null, - ); - @override - _i19.Future firoUpdateJIndex(List? jIndex) => - (super.noSuchMethod( - Invocation.method( - #firoUpdateJIndex, - [jIndex], - ), - returnValue: _i19.Future.value(), - returnValueForMissingStub: _i19.Future.value(), - ) as _i19.Future); - @override - _i19.Future firoUpdateLelantusCoins(List? lelantusCoins) => - (super.noSuchMethod( - Invocation.method( - #firoUpdateLelantusCoins, - [lelantusCoins], - ), - returnValue: _i19.Future.value(), - returnValueForMissingStub: _i19.Future.value(), - ) as _i19.Future); - @override - int firoGetMintIndex() => (super.noSuchMethod( - Invocation.method( - #firoGetMintIndex, - [], - ), - returnValue: 0, - ) as int); - @override - _i19.Future firoUpdateMintIndex(int? mintIndex) => (super.noSuchMethod( - Invocation.method( - #firoUpdateMintIndex, - [mintIndex], - ), - returnValue: _i19.Future.value(), - returnValueForMissingStub: _i19.Future.value(), - ) as _i19.Future); } /// A class which mocks [LocaleService]. From 2fb94444c46965c34d5adcaaa9257fda649a718a Mon Sep 17 00:00:00 2001 From: julian Date: Mon, 24 Jul 2023 16:49:11 -0600 Subject: [PATCH 110/169] no more firo specific hive --- lib/services/coins/firo/firo_wallet.dart | 322 ++++++++++------------- lib/services/mixins/firo_hive.dart | 61 ----- 2 files changed, 145 insertions(+), 238 deletions(-) delete mode 100644 lib/services/mixins/firo_hive.dart diff --git a/lib/services/coins/firo/firo_wallet.dart b/lib/services/coins/firo/firo_wallet.dart index 67882a6b6..a5a7b3b47 100644 --- a/lib/services/coins/firo/firo_wallet.dart +++ b/lib/services/coins/firo/firo_wallet.dart @@ -35,7 +35,6 @@ import 'package:stackwallet/services/event_bus/events/global/refresh_percent_cha 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/firo_hive.dart'; import 'package:stackwallet/services/mixins/wallet_cache.dart'; import 'package:stackwallet/services/mixins/wallet_db.dart'; import 'package:stackwallet/services/mixins/xpubable.dart'; @@ -752,7 +751,7 @@ Future _setTestnetWrapper(bool isTestnet) async { /// Handles a single instance of a firo wallet class FiroWallet extends CoinServiceAPI - with WalletCache, WalletDB, FiroHive + with WalletCache, WalletDB implements XPubAble { // Constructor FiroWallet({ @@ -773,7 +772,6 @@ class FiroWallet extends CoinServiceAPI _cachedElectrumXClient = cachedClient; _secureStore = secureStore; initCache(walletId, coin); - initFiroHive(walletId); initWalletDB(mockableOverride: mockableOverride); Logging.instance.log("$walletName isolates length: ${isolates.length}", @@ -2181,7 +2179,6 @@ class FiroWallet extends CoinServiceAPI value: "", ); - await firoUpdateJIndex([]); // Generate and add addresses to relevant arrays final initialReceivingAddress = await _generateAddressForChain(0, 0); final initialChangeAddress = await _generateAddressForChain(1, 0); @@ -2352,57 +2349,60 @@ class FiroWallet extends CoinServiceAPI } Future> _getUnspentCoins() async { - final jindexes = firoGetJIndex() ?? []; + // final jindexes = firoGetJIndex() ?? []; final lelantusCoinsList = await db.isar.lelantusCoins .where() .walletIdEqualTo(walletId) .filter() + .isUsedEqualTo(false) // TODO add this? .not() .valueEqualTo("0") .findAll(); - List coins = []; + return lelantusCoinsList; - final currentChainHeight = await chainHeight; - - for (int i = 0; i < lelantusCoinsList.length; i++) { - // Logging.instance.log("lelantusCoinsList[$i]: ${lelantusCoinsList[i]}"); - final coin = lelantusCoinsList[i]; - - final tx = await db.getTransaction(walletId, coin.txid); - - // TODO check if sane default - bool isUnconfirmed = false; - - if (tx != null) { - bool isConfirmed = tx.isConfirmed( - currentChainHeight, - MINIMUM_CONFIRMATIONS, - ); - if (!jindexes.contains(coin.index) && - tx.isLelantus == true && - !isConfirmed) { - isUnconfirmed = true; - } else if (!isConfirmed) { - continue; - } - } else { - final txn = await cachedElectrumXClient.getTransaction( - txHash: coin.txid, - verbose: true, - coin: this.coin, - ); - final confirmations = txn["confirmations"]; - isUnconfirmed = confirmations is int && confirmations < 1; - } - if (!coin.isUsed && - coin.anonymitySetId != ANONYMITY_SET_EMPTY_ID && - !isUnconfirmed) { - coins.add(coin); - } - } - return coins; + // List coins = []; + // + // final currentChainHeight = await chainHeight; + // + // for (int i = 0; i < lelantusCoinsList.length; i++) { + // // Logging.instance.log("lelantusCoinsList[$i]: ${lelantusCoinsList[i]}"); + // final coin = lelantusCoinsList[i]; + // + // final tx = await db.getTransaction(walletId, coin.txid); + // + // // TODO check if sane default + // bool isUnconfirmed = false; + // + // if (tx != null) { + // bool isConfirmed = tx.isConfirmed( + // currentChainHeight, + // MINIMUM_CONFIRMATIONS, + // ); + // if (!jindexes.contains(coin.index) && + // tx.isLelantus == true && + // !isConfirmed) { + // isUnconfirmed = true; + // } else if (!isConfirmed) { + // continue; + // } + // } else { + // final txn = await cachedElectrumXClient.getTransaction( + // txHash: coin.txid, + // verbose: true, + // coin: this.coin, + // ); + // final confirmations = txn["confirmations"]; + // isUnconfirmed = confirmations is int && confirmations < 1; + // } + // if (!coin.isUsed && + // coin.anonymitySetId != ANONYMITY_SET_EMPTY_ID && + // !isUnconfirmed) { + // coins.add(coin); + // } + // } + // return coins; } // index 0 and 1 for the funds available to spend. @@ -2415,12 +2415,12 @@ class FiroWallet extends CoinServiceAPI .where() .walletIdEqualTo(walletId) .filter() + .isUsedEqualTo(false) .not() .valueEqualTo(0.toString()) .findAll(); final currentChainHeight = await chainHeight; - final jindexes = firoGetJIndex(); int intLelantusBalance = 0; int unconfirmedLelantusBalance = 0; @@ -2440,33 +2440,17 @@ class FiroWallet extends CoinServiceAPI level: LogLevel.Fatal, ); } else { - bool isLelantus = txn.isLelantus == true; - if (!jindexes!.contains(lelantusCoin.index) && isLelantus) { - if (!lelantusCoin.isUsed && - txn.isConfirmed(currentChainHeight, MINIMUM_CONFIRMATIONS)) { - // mint tx, add value to balance - intLelantusBalance += int.parse(lelantusCoin.value); - } /* else { - // This coin is not confirmed and may be replaced - }*/ - } else if (jindexes.contains(lelantusCoin.index) && - isLelantus && - !lelantusCoin.isUsed && - !txn.isConfirmed(currentChainHeight, MINIMUM_CONFIRMATIONS)) { - unconfirmedLelantusBalance += int.parse(lelantusCoin.value); - } else if (jindexes.contains(lelantusCoin.index) && - !lelantusCoin.isUsed) { + if (txn.isLelantus != true) { + Logging.instance.log( + "Bad database state found in $walletName $walletId for _refreshBalance lelantus", + level: LogLevel.Fatal, + ); + } + + if (txn.isConfirmed(currentChainHeight, MINIMUM_CONFIRMATIONS)) { + // mint tx, add value to balance intLelantusBalance += int.parse(lelantusCoin.value); - } else if (!lelantusCoin.isUsed && - (txn.isLelantus == true - ? true - : txn.isConfirmed( - currentChainHeight, MINIMUM_CONFIRMATIONS) != - false)) { - intLelantusBalance += int.parse(lelantusCoin.value); - } else if (!isLelantus && - txn.isConfirmed(currentChainHeight, MINIMUM_CONFIRMATIONS) == - false) { + } else { unconfirmedLelantusBalance += int.parse(lelantusCoin.value); } } @@ -2615,10 +2599,16 @@ class FiroWallet extends CoinServiceAPI } Future>> createMintsFromAmount(int total) async { - var tmpTotal = total; - var index = 1; - var mints = >[]; - final nextFreeMintIndex = firoGetMintIndex(); + int tmpTotal = total; + int index = 0; + final mints = >[]; + final lastUsedIndex = await db.isar.lelantusCoins + .where() + .walletIdEqualTo(walletId) + .sortByMintIndexDesc() + .mintIndexProperty() + .findFirst(); + final nextFreeMintIndex = (lastUsedIndex ?? 0) + 1; while (tmpTotal > 0) { final mintValue = min(tmpTotal, MINT_LIMIT); final mint = await _getMintHex( @@ -2783,9 +2773,6 @@ class FiroWallet extends CoinServiceAPI amount += utxosToUse[i].value; } - final index = firoGetMintIndex(); - Logging.instance.log("index of mint $index", level: LogLevel.Info); - for (var mintsElement in mintsMap) { Logging.instance.log("using $mintsElement", level: LogLevel.Info); Uint8List mintu8 = @@ -2837,45 +2824,39 @@ class FiroWallet extends CoinServiceAPI .where() .walletIdEqualTo(walletId) .filter() + .isUsedEqualTo(false) + .and() + .isJMintEqualTo(true) .not() .valueEqualTo(0.toString()) .findAll(); - final jindexes = firoGetJIndex(); - // Get all joinsplit transaction ids - final listLelantusTxData = await db + final lelantusJoinSplitTxns = await db .getTransactions(walletId) .filter() .isLelantusEqualTo(true) + .and() + .subTypeEqualTo(isar_models.TransactionSubType.join) .findAll(); - List joinsplits = []; - for (final tx in listLelantusTxData) { - if (tx.subType == isar_models.TransactionSubType.join) { - joinsplits.add(tx.txid); - } - } + Set joinSplitTXIDs = {}; + + // for (final tx in lelantusJoinSplitTxns) { + // joinSplitTXIDs.add(tx.txid); + // } for (final coin in lelantusCoins) { - if (jindexes != null) { - if (jindexes.contains(coin.index) && !joinsplits.contains(coin.txid)) { - joinsplits.add(coin.txid); - } - } + joinSplitTXIDs.add(coin.txid); } - Map> data = - {}; - for (final entry in listLelantusTxData) { - data[entry.txid] = Tuple2(entry.address.value, entry); - } + Map> + updatedData = {}; // Grab the most recent information on all the joinsplits - final updatedJSplit = await getJMintTransactions( cachedElectrumXClient, - joinsplits, + joinSplitTXIDs.toList(), coin, ); @@ -2887,7 +2868,7 @@ class FiroWallet extends CoinServiceAPI try { currentTx = - listLelantusTxData.firstWhere((e) => e.txid == tx.value.txid); + lelantusJoinSplitTxns.firstWhere((e) => e.txid == tx.value.txid); } catch (_) { currentTx = null; } @@ -2895,54 +2876,23 @@ class FiroWallet extends CoinServiceAPI if (currentTx == null) { // this send was accidentally not included in the list tx.value.isLelantus = true; - data[tx.value.txid] = + updatedData[tx.value.txid] = Tuple2(tx.value.address.value ?? tx.key, tx.value); - - continue; - } - if (currentTx.isConfirmed(currentChainHeight, MINIMUM_CONFIRMATIONS) != + } else if (currentTx.isConfirmed( + currentChainHeight, MINIMUM_CONFIRMATIONS) != tx.value.isConfirmed(currentChainHeight, MINIMUM_CONFIRMATIONS)) { tx.value.isLelantus = true; - data[tx.value.txid] = + updatedData[tx.value.txid] = Tuple2(tx.value.address.value ?? tx.key, tx.value); } } - // Logging.instance.log(txData.txChunks); - final listTxData = await _txnData; - for (final value in listTxData) { - // ignore change addresses - // bool hasAtLeastOneReceive = false; - // int howManyReceiveInputs = 0; - // for (var element in value.inputs) { - // if (listLelantusTxData.containsKey(element.txid) && - // listLelantusTxData[element.txid]!.txType == "Received" - // // && - // // listLelantusTxData[element.txid].subType != "mint" - // ) { - // // hasAtLeastOneReceive = true; - // // howManyReceiveInputs++; - // } - // } - - if (value.type == isar_models.TransactionType.incoming && - value.subType != isar_models.TransactionSubType.mint) { - // Every receive other than a mint should be shown. Mints will be collected and shown from the send side - value.isLelantus = true; - data[value.txid] = Tuple2(value.address.value, value); - } else if (value.type == isar_models.TransactionType.outgoing) { - // all sends should be shown, mints will be displayed correctly in the ui - value.isLelantus = true; - data[value.txid] = Tuple2(value.address.value, value); - } - } - // TODO: optimize this whole lelantus process final List> txnsData = []; - for (final value in data.values) { + for (final value in updatedData.values) { // allow possible null address on mints as we don't display address // this should normally never be null anyways but old (dbVersion up to 4) // migrated transactions may not have had an address (full rescan should @@ -2967,15 +2917,6 @@ class FiroWallet extends CoinServiceAPI } await db.addNewTransactionData(txnsData, walletId); - - // // update the _lelantusTransactionData - // final models.TransactionData newTxData = - // models.TransactionData.fromMap(listLelantusTxData); - // // Logging.instance.log(newTxData.txChunks); - // _lelantusTransactionData = Future(() => newTxData); - // await DB.instance.put( - // boxName: walletId, key: 'latest_lelantus_tx_model', value: newTxData); - // return newTxData; } Future _getMintHex(int amount, int index) async { @@ -3025,7 +2966,13 @@ class FiroWallet extends CoinServiceAPI level: LogLevel.Info); if (txid == transactionInfo['txid']) { - final index = firoGetMintIndex(); + final lastUsedIndex = await db.isar.lelantusCoins + .where() + .walletIdEqualTo(walletId) + .sortByMintIndexDesc() + .mintIndexProperty() + .findFirst(); + final nextFreeMintIndex = (lastUsedIndex ?? 0) + 1; if (transactionInfo['spendCoinIndexes'] != null) { // This is a joinsplit @@ -3041,7 +2988,7 @@ class FiroWallet extends CoinServiceAPI .where() .walletIdEqualTo(walletId) .filter() - .indexEqualTo(index) + .mintIndexEqualTo(index) .findAll(); if (possibleCoins.isNotEmpty) { @@ -3069,22 +3016,26 @@ class FiroWallet extends CoinServiceAPI ); if (int.parse(jmint.value) > 0) { updatedCoins.add(jmint); - final jindexes = firoGetJIndex()!; - jindexes.add(index); - await firoUpdateJIndex(jindexes); - await firoUpdateMintIndex(index + 1); } - await db.isar.writeTxn(() async { - for (final c in updatedCoins) { - await db.isar.lelantusCoins.deleteByPublicCoinWalletIdTxid( - c.publicCoin, - c.walletId, - c.txid, - ); - } - await db.isar.lelantusCoins.putAll(updatedCoins); - }); + try { + await db.isar.writeTxn(() async { + for (final c in updatedCoins) { + await db.isar.lelantusCoins.deleteByPublicCoinWalletIdTxid( + c.publicCoin, + c.walletId, + c.txid, + ); + } + await db.isar.lelantusCoins.putAll(updatedCoins); + }); + } catch (e, s) { + Logging.instance.log( + "$e\n$s", + level: LogLevel.Fatal, + ); + rethrow; + } final amount = Amount.fromDecimal( Decimal.parse(transactionInfo["amount"].toString()), @@ -3160,13 +3111,20 @@ class FiroWallet extends CoinServiceAPI ); if (int.parse(mint.value) > 0) { updatedCoins.add(mint); - await firoUpdateMintIndex(index + 1); } } // Logging.instance.log(coins); - await db.isar.writeTxn(() async { - await db.isar.lelantusCoins.putAll(updatedCoins); - }); + try { + await db.isar.writeTxn(() async { + await db.isar.lelantusCoins.putAll(updatedCoins); + }); + } catch (e, s) { + Logging.instance.log( + "$e\n$s", + level: LogLevel.Fatal, + ); + rethrow; + } } return true; } else { @@ -3933,7 +3891,6 @@ class FiroWallet extends CoinServiceAPI coin: coin, ); - // todo check here if we should mark as blocked final utxo = isar_models.UTXO( walletId: walletId, txid: txn["txid"] as String, @@ -4586,14 +4543,19 @@ class FiroWallet extends CoinServiceAPI await chainHeight, ); - await Future.wait([ - firoUpdateMintIndex(message['mintIndex'] as int), - db.isar.writeTxn(() async { - await db.isar.lelantusCoins.putAll( - message['_lelantus_coins'] as List); - }), - firoUpdateJIndex(message['jindex'] as List), - ]); + final coins = message['_lelantus_coins'] as List; + + try { + await db.isar.writeTxn(() async { + await db.isar.lelantusCoins.putAll(coins); + }); + } catch (e, s) { + Logging.instance.log( + "$e\n$s", + level: LogLevel.Fatal, + ); + rethrow; + } final transactionMap = message["newTxMap"] as Map; @@ -4677,7 +4639,13 @@ class FiroWallet extends CoinServiceAPI int spendAmount, String address, bool subtractFeeFromAmount) async { final _mnemonic = await mnemonicString; final _mnemonicPassphrase = await mnemonicPassphrase; - final index = firoGetMintIndex(); + final lastUsedIndex = await db.isar.lelantusCoins + .where() + .walletIdEqualTo(walletId) + .sortByMintIndexDesc() + .mintIndexProperty() + .findFirst(); + final nextFreeMintIndex = (lastUsedIndex ?? 0) + 1; final lelantusEntry = await _getLelantusEntry(); final anonymitySets = await fetchAnonymitySets(); final locktime = await getBlockHead(electrumXClient); @@ -4691,7 +4659,7 @@ class FiroWallet extends CoinServiceAPI "subtractFeeFromAmount": subtractFeeFromAmount, "mnemonic": _mnemonic, "mnemonicPassphrase": _mnemonicPassphrase, - "index": index, + "index": nextFreeMintIndex, // "price": price, "lelantusEntries": lelantusEntry, "locktime": locktime, diff --git a/lib/services/mixins/firo_hive.dart b/lib/services/mixins/firo_hive.dart deleted file mode 100644 index 631aebe6d..000000000 --- a/lib/services/mixins/firo_hive.dart +++ /dev/null @@ -1,61 +0,0 @@ -/* - * This file is part of Stack Wallet. - * - * Copyright (c) 2023 Cypher Stack - * All Rights Reserved. - * The code is distributed under GPLv3 license, see LICENSE file for details. - * Generated by Cypher Stack on 2023-05-26 - * - */ - -import 'package:stackwallet/db/hive/db.dart'; - -mixin FiroHive { - late final String _walletId; - - void initFiroHive(String walletId) { - _walletId = walletId; - } - - // jindex - List? firoGetJIndex() { - return DB.instance.get(boxName: _walletId, key: "jindex") as List?; - } - - Future firoUpdateJIndex(List jIndex) async { - await DB.instance.put( - boxName: _walletId, - key: "jindex", - value: jIndex, - ); - } - - // // _lelantus_coins - // List? firoGetLelantusCoins() { - // return DB.instance.get(boxName: _walletId, key: "_lelantus_coins") - // as List?; - // } - // - // Future firoUpdateLelantusCoins(List lelantusCoins) async { - // await DB.instance.put( - // boxName: _walletId, - // key: "_lelantus_coins", - // value: lelantusCoins, - // ); - // } - - // mintIndex - int firoGetMintIndex() { - return DB.instance.get(boxName: _walletId, key: "mintIndex") - as int? ?? - 0; - } - - Future firoUpdateMintIndex(int mintIndex) async { - await DB.instance.put( - boxName: _walletId, - key: "mintIndex", - value: mintIndex, - ); - } -} From 462b845bd40b1f448d86d75e366363e53d459512 Mon Sep 17 00:00:00 2001 From: julian Date: Mon, 24 Jul 2023 16:54:38 -0600 Subject: [PATCH 111/169] clean up unspent coins --- lib/services/coins/firo/firo_wallet.dart | 51 +++--------------------- 1 file changed, 5 insertions(+), 46 deletions(-) diff --git a/lib/services/coins/firo/firo_wallet.dart b/lib/services/coins/firo/firo_wallet.dart index a5a7b3b47..fe1956ce9 100644 --- a/lib/services/coins/firo/firo_wallet.dart +++ b/lib/services/coins/firo/firo_wallet.dart @@ -2349,60 +2349,19 @@ class FiroWallet extends CoinServiceAPI } Future> _getUnspentCoins() async { - // final jindexes = firoGetJIndex() ?? []; - final lelantusCoinsList = await db.isar.lelantusCoins .where() .walletIdEqualTo(walletId) .filter() - .isUsedEqualTo(false) // TODO add this? + .isUsedEqualTo(false) .not() - .valueEqualTo("0") + .group((q) => q + .valueEqualTo("0") + .or() + .anonymitySetIdEqualTo(ANONYMITY_SET_EMPTY_ID)) .findAll(); return lelantusCoinsList; - - // List coins = []; - // - // final currentChainHeight = await chainHeight; - // - // for (int i = 0; i < lelantusCoinsList.length; i++) { - // // Logging.instance.log("lelantusCoinsList[$i]: ${lelantusCoinsList[i]}"); - // final coin = lelantusCoinsList[i]; - // - // final tx = await db.getTransaction(walletId, coin.txid); - // - // // TODO check if sane default - // bool isUnconfirmed = false; - // - // if (tx != null) { - // bool isConfirmed = tx.isConfirmed( - // currentChainHeight, - // MINIMUM_CONFIRMATIONS, - // ); - // if (!jindexes.contains(coin.index) && - // tx.isLelantus == true && - // !isConfirmed) { - // isUnconfirmed = true; - // } else if (!isConfirmed) { - // continue; - // } - // } else { - // final txn = await cachedElectrumXClient.getTransaction( - // txHash: coin.txid, - // verbose: true, - // coin: this.coin, - // ); - // final confirmations = txn["confirmations"]; - // isUnconfirmed = confirmations is int && confirmations < 1; - // } - // if (!coin.isUsed && - // coin.anonymitySetId != ANONYMITY_SET_EMPTY_ID && - // !isUnconfirmed) { - // coins.add(coin); - // } - // } - // return coins; } // index 0 and 1 for the funds available to spend. From 18f3a2056c382a9a31446d8be81135d367e0c4f1 Mon Sep 17 00:00:00 2001 From: julian Date: Tue, 25 Jul 2023 08:55:50 -0600 Subject: [PATCH 112/169] check anon set when creating list of mints --- lib/services/coins/firo/firo_wallet.dart | 89 ++++++++++++++++++++---- 1 file changed, 75 insertions(+), 14 deletions(-) diff --git a/lib/services/coins/firo/firo_wallet.dart b/lib/services/coins/firo/firo_wallet.dart index fe1956ce9..331b5b519 100644 --- a/lib/services/coins/firo/firo_wallet.dart +++ b/lib/services/coins/firo/firo_wallet.dart @@ -2559,8 +2559,7 @@ class FiroWallet extends CoinServiceAPI Future>> createMintsFromAmount(int total) async { int tmpTotal = total; - int index = 0; - final mints = >[]; + int counter = 0; final lastUsedIndex = await db.isar.lelantusCoins .where() .walletIdEqualTo(walletId) @@ -2568,20 +2567,82 @@ class FiroWallet extends CoinServiceAPI .mintIndexProperty() .findFirst(); final nextFreeMintIndex = (lastUsedIndex ?? 0) + 1; + + final root = await Bip32Utils.getBip32Root( + (await mnemonic).join(" "), + (await mnemonicPassphrase)!, + _network, + ); + + final mints = >[]; while (tmpTotal > 0) { - final mintValue = min(tmpTotal, MINT_LIMIT); - final mint = await _getMintHex( - mintValue, - nextFreeMintIndex + index, + final index = nextFreeMintIndex + counter; + + final bip32.BIP32 mintKeyPair = await Bip32Utils.getBip32NodeFromRoot( + root, + constructDerivePath( + networkWIF: _network.wif, + chain: MINT_INDEX, + index: index, + ), ); - mints.add({ - "value": mintValue, - "script": mint, - "index": nextFreeMintIndex + index, - "publicCoin": "", - }); - tmpTotal = tmpTotal - MINT_LIMIT; - index++; + + final String mintTag = CreateTag( + Format.uint8listToString(mintKeyPair.privateKey!), + index, + Format.uint8listToString(mintKeyPair.identifier), + isTestnet: coin == Coin.firoTestNet, + ); + final List> anonymitySets; + try { + anonymitySets = await fetchAnonymitySets(); + } catch (e, s) { + Logging.instance.log( + "Firo needs better internet to create mints: $e\n$s", + level: LogLevel.Fatal, + ); + rethrow; + } + + bool isUsedMintTag = false; + + // stupid dynamic maps + for (final set in anonymitySets) { + final setCoins = set["coins"] as List; + for (final coin in setCoins) { + if (coin[1] == mintTag) { + isUsedMintTag = true; + break; + } + } + if (isUsedMintTag) { + break; + } + } + + if (isUsedMintTag) { + Logging.instance.log( + "Found used index when minting", + level: LogLevel.Warning, + ); + } + + if (!isUsedMintTag) { + final mintValue = min(tmpTotal, MINT_LIMIT); + final mint = await _getMintHex( + mintValue, + index, + ); + mints.add({ + "value": mintValue, + "script": mint, + "index": index, + "publicCoin": "", + }); + tmpTotal = tmpTotal - MINT_LIMIT; + } + + counter++; } return mints; } From 99a36f158731fad192f7c30bc59ba64dfd671479 Mon Sep 17 00:00:00 2001 From: julian Date: Tue, 25 Jul 2023 09:03:26 -0600 Subject: [PATCH 113/169] fix mint limit constant --- lib/services/coins/firo/firo_wallet.dart | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/lib/services/coins/firo/firo_wallet.dart b/lib/services/coins/firo/firo_wallet.dart index 331b5b519..b02816497 100644 --- a/lib/services/coins/firo/firo_wallet.dart +++ b/lib/services/coins/firo/firo_wallet.dart @@ -58,8 +58,8 @@ import 'package:uuid/uuid.dart'; const DUST_LIMIT = 1000; const MINIMUM_CONFIRMATIONS = 1; -const MINT_LIMIT = 100100000000; -const int LELANTUS_VALUE_SPEND_LIMIT_PER_TRANSACTION = 5001 * 100000000; +const MINT_LIMIT = 5001 * 100000000; +const MINT_LIMIT_TESTNET = 1001 * 100000000; const JMINT_INDEX = 5; const MINT_INDEX = 2; @@ -2628,7 +2628,8 @@ class FiroWallet extends CoinServiceAPI } if (!isUsedMintTag) { - final mintValue = min(tmpTotal, MINT_LIMIT); + final mintValue = min(tmpTotal, + (coin == Coin.firoTestNet ? MINT_LIMIT_TESTNET : MINT_LIMIT)); final mint = await _getMintHex( mintValue, index, @@ -2639,7 +2640,8 @@ class FiroWallet extends CoinServiceAPI "index": index, "publicCoin": "", }); - tmpTotal = tmpTotal - MINT_LIMIT; + tmpTotal = tmpTotal - + (coin == Coin.firoTestNet ? MINT_LIMIT_TESTNET : MINT_LIMIT); } counter++; From 140f1468f9d3e243d80a128d65f305b3521b3d07 Mon Sep 17 00:00:00 2001 From: julian Date: Tue, 25 Jul 2023 09:04:09 -0600 Subject: [PATCH 114/169] remove unused code --- lib/services/coins/firo/firo_wallet.dart | 69 ------------------------ 1 file changed, 69 deletions(-) diff --git a/lib/services/coins/firo/firo_wallet.dart b/lib/services/coins/firo/firo_wallet.dart index b02816497..659a6d94b 100644 --- a/lib/services/coins/firo/firo_wallet.dart +++ b/lib/services/coins/firo/firo_wallet.dart @@ -4769,75 +4769,6 @@ class FiroWallet extends CoinServiceAPI this.isActive = isActive; }; - Future getCoinsToJoinSplit( - int required, - ) async { - List coins = await _getLelantusEntry(); - if (required > LELANTUS_VALUE_SPEND_LIMIT_PER_TRANSACTION) { - return false; - } - - int availableBalance = coins.fold( - 0, (previousValue, element) => previousValue + element.amount); - - if (required > availableBalance) { - return false; - } - - // sort by biggest amount. if it is same amount we will prefer the older block - coins.sort((a, b) => - (a.amount != b.amount ? a.amount > b.amount : a.height < b.height) - ? -1 - : 1); - int spendVal = 0; - - List coinsToSpend = []; - - while (spendVal < required) { - if (coins.isEmpty) { - break; - } - - DartLelantusEntry? chosen; - int need = required - spendVal; - - var itr = coins.first; - if (need >= itr.amount) { - chosen = itr; - coins.remove(itr); - } else { - for (int index = coins.length - 1; index != 0; index--) { - var coinIt = coins[index]; - var nextItr = coins[index - 1]; - - if (coinIt.amount >= need && - (index - 1 == 0 || nextItr.amount != coinIt.amount)) { - chosen = coinIt; - coins.remove(chosen); - break; - } - } - } - - // TODO: investigate the bug here where chosen is null, conditions, given one mint - spendVal += chosen!.amount; - coinsToSpend.insert(coinsToSpend.length, chosen); - } - - // sort by group id ay ascending order. it is mandatory for creating proper joinsplit - coinsToSpend.sort((a, b) => a.anonymitySetId < b.anonymitySetId ? 1 : -1); - - int changeToMint = spendVal - required; - List indices = []; - for (var l in coinsToSpend) { - indices.add(l.index); - } - List coinsToBeSpentOut = []; - coinsToBeSpentOut.addAll(coinsToSpend); - - return {"changeToMint": changeToMint, "coinsToSpend": coinsToBeSpentOut}; - } - Future estimateJoinSplitFee( int spendAmount, ) async { From a0b42226f1f9937e4bbbacc3e8f8e473e41e0636 Mon Sep 17 00:00:00 2001 From: julian Date: Tue, 25 Jul 2023 10:39:23 -0600 Subject: [PATCH 115/169] fix test --- test/services/coins/firo/firo_wallet_test.dart | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test/services/coins/firo/firo_wallet_test.dart b/test/services/coins/firo/firo_wallet_test.dart index 68db15437..fcd7bd50e 100644 --- a/test/services/coins/firo/firo_wallet_test.dart +++ b/test/services/coins/firo/firo_wallet_test.dart @@ -72,6 +72,7 @@ void main() { setData, List.from(usedSerials), firoNetwork, + "walletId", ); const currentHeight = 100000000000; @@ -133,6 +134,7 @@ void main() { setData, List.from(usedSerials), firoNetwork, + "walletId", ), throwsA(isA())); }); From c7f1392734df24cd177324f44cdee12f6af8c789 Mon Sep 17 00:00:00 2001 From: julian Date: Tue, 25 Jul 2023 10:49:49 -0600 Subject: [PATCH 116/169] cache boxes hive error fix --- lib/db/hive/db.dart | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/lib/db/hive/db.dart b/lib/db/hive/db.dart index 931a1cee4..25efcdb3e 100644 --- a/lib/db/hive/db.dart +++ b/lib/db/hive/db.dart @@ -177,6 +177,9 @@ class DB { } Future> getTxCacheBox({required Coin coin}) async { + if (_txCacheBoxes[coin]?.isOpen != true) { + _txCacheBoxes.remove(coin); + } return _txCacheBoxes[coin] ??= await Hive.openBox(_boxNameTxCache(coin: coin)); } @@ -186,6 +189,9 @@ class DB { } Future> getAnonymitySetCacheBox({required Coin coin}) async { + if (_setCacheBoxes[coin]?.isOpen != true) { + _setCacheBoxes.remove(coin); + } return _setCacheBoxes[coin] ??= await Hive.openBox(_boxNameSetCache(coin: coin)); } @@ -195,6 +201,9 @@ class DB { } Future> getUsedSerialsCacheBox({required Coin coin}) async { + if (_usedSerialsCacheBoxes[coin]?.isOpen != true) { + _usedSerialsCacheBoxes.remove(coin); + } return _usedSerialsCacheBoxes[coin] ??= await Hive.openBox(_boxNameUsedSerialsCache(coin: coin)); } From a8abd388271f0e6ebfaee51d44490cd374e3e408 Mon Sep 17 00:00:00 2001 From: julian Date: Tue, 25 Jul 2023 11:17:30 -0600 Subject: [PATCH 117/169] invalid vsize fix --- lib/services/coins/firo/firo_wallet.dart | 26 +++++++++++++++++++----- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/lib/services/coins/firo/firo_wallet.dart b/lib/services/coins/firo/firo_wallet.dart index 659a6d94b..7b42698dc 100644 --- a/lib/services/coins/firo/firo_wallet.dart +++ b/lib/services/coins/firo/firo_wallet.dart @@ -1429,16 +1429,32 @@ class FiroWallet extends CoinServiceAPI feeRatePerKB: selectedTxFeeRate, ); - if (feeForOneOutput < vSizeForOneOutput + 1) { - feeForOneOutput = vSizeForOneOutput + 1; - } - - final int amount = satoshiAmountToSend - feeForOneOutput; + int amount = satoshiAmountToSend - feeForOneOutput; dynamic txn = await buildTransaction( utxoSigningData: utxoSigningData, recipients: recipientsArray, satoshiAmounts: [amount], ); + + int count = 0; + int fee = feeForOneOutput; + int vsize = txn["vSize"] as int; + + while (fee < vsize && count < 10) { + // 10 being some reasonable max + count++; + fee += count; + amount = satoshiAmountToSend - fee; + + txn = await buildTransaction( + utxoSigningData: utxoSigningData, + recipients: recipientsArray, + satoshiAmounts: [amount], + ); + + vsize = txn["vSize"] as int; + } + Map transactionObject = { "hex": txn["hex"], "recipient": recipientsArray[0], From 5f9b1f77f58114eb0f39b2f10911da15b169b791 Mon Sep 17 00:00:00 2001 From: julian Date: Tue, 25 Jul 2023 11:17:54 -0600 Subject: [PATCH 118/169] clearer exception --- lib/services/coins/firo/firo_wallet.dart | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/services/coins/firo/firo_wallet.dart b/lib/services/coins/firo/firo_wallet.dart index 7b42698dc..10cf4473c 100644 --- a/lib/services/coins/firo/firo_wallet.dart +++ b/lib/services/coins/firo/firo_wallet.dart @@ -4592,7 +4592,8 @@ class FiroWallet extends CoinServiceAPI "$e\n$s", level: LogLevel.Fatal, ); - rethrow; + // don't just rethrow since isar likes to strip stack traces for some reason + throw Exception("e=$e & s=$s"); } final transactionMap = From e1140e6fa6dc863ca09b43ea2b3596d899be026d Mon Sep 17 00:00:00 2001 From: julian Date: Tue, 25 Jul 2023 11:18:21 -0600 Subject: [PATCH 119/169] clear lelantus coins on full rescan --- lib/db/isar/main_db.dart | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/lib/db/isar/main_db.dart b/lib/db/isar/main_db.dart index fa565c0cd..4b60e2cda 100644 --- a/lib/db/isar/main_db.dart +++ b/lib/db/isar/main_db.dart @@ -371,6 +371,7 @@ class MainDB { final transactionCount = await getTransactions(walletId).count(); final addressCount = await getAddresses(walletId).count(); final utxoCount = await getUTXOs(walletId).count(); + final lelantusCoinCount = await isar.lelantusCoins.where().walletIdEqualTo(walletId).count(); await isar.writeTxn(() async { const paginateLimit = 50; @@ -404,6 +405,16 @@ class MainDB { .findAll(); await isar.utxos.deleteAll(utxoIds); } + + // lelantusCoins + for (int i = 0; i < lelantusCoinCount; i += paginateLimit) { + final lelantusCoinIds = await isar.lelantusCoins.where().walletIdEqualTo(walletId) + .offset(i) + .limit(paginateLimit) + .idProperty() + .findAll(); + await isar.lelantusCoins.deleteAll(lelantusCoinIds); + } }); } From 5439ce895433bbf9c9e5dee3cb6e1596954c38b9 Mon Sep 17 00:00:00 2001 From: julian Date: Tue, 25 Jul 2023 12:06:06 -0600 Subject: [PATCH 120/169] show error before data in case dart decides string is too long and cuts it off --- lib/electrumx_rpc/electrumx.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/electrumx_rpc/electrumx.dart b/lib/electrumx_rpc/electrumx.dart index e0a3118eb..804a1364c 100644 --- a/lib/electrumx_rpc/electrumx.dart +++ b/lib/electrumx_rpc/electrumx.dart @@ -159,8 +159,8 @@ class ElectrumX { throw Exception( "JSONRPC response\n" " command: $command\n" - " args: $args\n" - " error: ${response.data}", + " error: ${response.data}" + " args: $args\n", ); } From 3c1a3f23e379fb187e40e1738ce34154f17a8b43 Mon Sep 17 00:00:00 2001 From: julian Date: Tue, 25 Jul 2023 12:07:09 -0600 Subject: [PATCH 121/169] clean up --- lib/services/coins/firo/firo_wallet.dart | 47 ------------------------ 1 file changed, 47 deletions(-) diff --git a/lib/services/coins/firo/firo_wallet.dart b/lib/services/coins/firo/firo_wallet.dart index 10cf4473c..a31e0f62c 100644 --- a/lib/services/coins/firo/firo_wallet.dart +++ b/lib/services/coins/firo/firo_wallet.dart @@ -1224,18 +1224,6 @@ class FiroWallet extends CoinServiceAPI try { final txid = txData["txid"] as String; - // temporarily update apdate available balance until a full refresh is done - - // TODO: something here causes an exception to be thrown giving user false info that the tx failed - // Decimal sendTotal = - // Format.satoshisToAmount(txData["value"] as int, coin: coin); - // sendTotal += Decimal.parse(txData["fees"].toString()); - - // TODO: is this needed? - // final bals = await balances; - // bals[0] -= sendTotal; - // _balances = Future(() => bals); - return txid; } catch (e, s) { //todo: come back to this @@ -2409,7 +2397,6 @@ class FiroWallet extends CoinServiceAPI .findFirstSync(); if (txn == null) { - // TODO: ?????????????????????????????????????? Logging.instance.log( "Transaction not found in DB for lelantus coin: $lelantusCoin", level: LogLevel.Fatal, @@ -2686,8 +2673,6 @@ class FiroWallet extends CoinServiceAPI int satoshisPerRecipient, List> mintsMap, ) async { - //todo: check if print needed - // debugPrint(utxosToUse.toString()); List addressStringsToGet = []; // Populating the addresses to derive @@ -3966,7 +3951,6 @@ class FiroWallet extends CoinServiceAPI Logging.instance .log('Outputs fetched: $outputArray', level: LogLevel.Info); - // TODO move this out of here and into IDB await db.isar.writeTxn(() async { await db.isar.utxos.where().walletIdEqualTo(walletId).deleteAll(); await db.isar.utxos.putAll(outputArray); @@ -4816,36 +4800,6 @@ class FiroWallet extends CoinServiceAPI Logging.instance.log('Closing estimateJoinSplit!', level: LogLevel.Info); return (message as LelantusFeeData).fee; } - // int fee; - // int size; - // - // for (fee = 0;;) { - // int currentRequired = spendAmount; - // - // TODO: investigate the bug here - // var map = await getCoinsToJoinSplit(currentRequired); - // if (map is bool && !map) { - // return 0; - // } - // - // List coinsToBeSpent = - // map['coinsToSpend'] as List; - // - // // 1054 is constant part, mainly Schnorr and Range proofs, 2560 is for each sigma/aux data - // // 179 other parts of tx, assuming 1 utxo and 1 jmint - // size = 1054 + 2560 * coinsToBeSpent.length + 180; - // // uint64_t feeNeeded = GetMinimumFee(size, DEFAULT_TX_CONFIRM_TARGET); - // int feeNeeded = - // size; //TODO(Levon) temporary, use real estimation methods here - // - // if (fee >= feeNeeded) { - // break; - // } - // - // fee = feeNeeded; - // } - // - // return fee; @override Future estimateFeeFor(Amount amount, int feeRate) async { @@ -4908,7 +4862,6 @@ class FiroWallet extends CoinServiceAPI } } - // TODO: correct formula for firo? Amount roughFeeEstimate(int inputCount, int outputCount, int feeRatePerKB) { return Amount( rawValue: BigInt.from(((181 * inputCount) + (34 * outputCount) + 10) * From 7d1d7cb8049c08195b146e77e0c77131e2f1c08b Mon Sep 17 00:00:00 2001 From: julian Date: Tue, 25 Jul 2023 12:07:44 -0600 Subject: [PATCH 122/169] auto format on save --- lib/db/isar/main_db.dart | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/lib/db/isar/main_db.dart b/lib/db/isar/main_db.dart index 4b60e2cda..93c9052da 100644 --- a/lib/db/isar/main_db.dart +++ b/lib/db/isar/main_db.dart @@ -371,7 +371,8 @@ class MainDB { final transactionCount = await getTransactions(walletId).count(); final addressCount = await getAddresses(walletId).count(); final utxoCount = await getUTXOs(walletId).count(); - final lelantusCoinCount = await isar.lelantusCoins.where().walletIdEqualTo(walletId).count(); + final lelantusCoinCount = + await isar.lelantusCoins.where().walletIdEqualTo(walletId).count(); await isar.writeTxn(() async { const paginateLimit = 50; @@ -408,7 +409,9 @@ class MainDB { // lelantusCoins for (int i = 0; i < lelantusCoinCount; i += paginateLimit) { - final lelantusCoinIds = await isar.lelantusCoins.where().walletIdEqualTo(walletId) + final lelantusCoinIds = await isar.lelantusCoins + .where() + .walletIdEqualTo(walletId) .offset(i) .limit(paginateLimit) .idProperty() From 590717560e3469ab1b65ea80b355c601285e1023 Mon Sep 17 00:00:00 2001 From: julian Date: Tue, 25 Jul 2023 12:12:10 -0600 Subject: [PATCH 123/169] notif error fix? --- lib/widgets/crypto_notifications.dart | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/widgets/crypto_notifications.dart b/lib/widgets/crypto_notifications.dart index f94c74ed9..88a96d422 100644 --- a/lib/widgets/crypto_notifications.dart +++ b/lib/widgets/crypto_notifications.dart @@ -13,6 +13,7 @@ import 'dart:async'; import 'package:event_bus/event_bus.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:stackwallet/providers/global/prefs_provider.dart'; import 'package:stackwallet/services/notifications_api.dart'; import 'package:stackwallet/themes/coin_icon_provider.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; @@ -84,6 +85,7 @@ class _CryptoNotificationsState extends ConsumerState { @override void initState() { + NotificationApi.prefs = ref.read(prefsChangeNotifierProvider); _streamSubscription = CryptoNotificationsEventBus.instance .on() .listen( From 20c706c7e5558ca6a170b739234f79021b8d4e0d Mon Sep 17 00:00:00 2001 From: julian Date: Tue, 25 Jul 2023 12:16:08 -0600 Subject: [PATCH 124/169] add todo check --- lib/services/coins/firo/firo_wallet.dart | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/services/coins/firo/firo_wallet.dart b/lib/services/coins/firo/firo_wallet.dart index a31e0f62c..ea0a255d4 100644 --- a/lib/services/coins/firo/firo_wallet.dart +++ b/lib/services/coins/firo/firo_wallet.dart @@ -2637,6 +2637,8 @@ class FiroWallet extends CoinServiceAPI mintValue, index, ); + + // TODO publicCoin prob shouldn't be empty? mints.add({ "value": mintValue, "script": mint, From c9470a5078168f1359f9b771bbb3d7d9e4d86f09 Mon Sep 17 00:00:00 2001 From: julian Date: Tue, 25 Jul 2023 13:48:19 -0600 Subject: [PATCH 125/169] desktop notifications fix --- lib/widgets/crypto_notifications.dart | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/widgets/crypto_notifications.dart b/lib/widgets/crypto_notifications.dart index 88a96d422..b570ae4f9 100644 --- a/lib/widgets/crypto_notifications.dart +++ b/lib/widgets/crypto_notifications.dart @@ -13,7 +13,7 @@ import 'dart:async'; import 'package:event_bus/event_bus.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:stackwallet/providers/global/prefs_provider.dart'; +import 'package:stackwallet/providers/providers.dart'; import 'package:stackwallet/services/notifications_api.dart'; import 'package:stackwallet/themes/coin_icon_provider.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; @@ -86,6 +86,7 @@ class _CryptoNotificationsState extends ConsumerState { @override void initState() { NotificationApi.prefs = ref.read(prefsChangeNotifierProvider); + NotificationApi.notificationsService = ref.read(notificationsProvider); _streamSubscription = CryptoNotificationsEventBus.instance .on() .listen( From 128fa9db6cb79dc6c5dc8ecfadb2afcc8c6121dc Mon Sep 17 00:00:00 2001 From: julian Date: Tue, 25 Jul 2023 13:48:39 -0600 Subject: [PATCH 126/169] rpc timeout mod --- lib/electrumx_rpc/rpc.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/electrumx_rpc/rpc.dart b/lib/electrumx_rpc/rpc.dart index 0c3834ae8..e865356d6 100644 --- a/lib/electrumx_rpc/rpc.dart +++ b/lib/electrumx_rpc/rpc.dart @@ -79,7 +79,7 @@ class JsonRPC { // TODO different timeout length? req.initiateTimeout( - const Duration(seconds: 10), + Duration(seconds: connectionTimeout.inSeconds ~/ 2), onTimedOut: () { _requestQueue.remove(req); }, From 04658f8eef091b31ba78da10d1a9ab194544fb83 Mon Sep 17 00:00:00 2001 From: julian Date: Tue, 25 Jul 2023 13:55:19 -0600 Subject: [PATCH 127/169] null error bandaid --- lib/services/notifications_api.dart | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/lib/services/notifications_api.dart b/lib/services/notifications_api.dart index 41e95387b..3ef97462a 100644 --- a/lib/services/notifications_api.dart +++ b/lib/services/notifications_api.dart @@ -77,8 +77,10 @@ class NotificationApi { final id = prefs.currentNotificationId; String confirms = ""; - if (txid != null) { - confirms = " (${confirmations!}/${requiredConfirmations!})"; + if (txid != null && + confirmations != null && + requiredConfirmations != null) { + confirms = " ($confirmations/$requiredConfirmations)"; } final NotificationModel model = NotificationModel( From 2cca59532e43fc84a0d637ad3bd35032418ed0fc Mon Sep 17 00:00:00 2001 From: julian Date: Tue, 25 Jul 2023 14:14:07 -0600 Subject: [PATCH 128/169] delete by index instead of public Coin since who tf knows what it could be in our code --- lib/services/coins/firo/firo_wallet.dart | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/lib/services/coins/firo/firo_wallet.dart b/lib/services/coins/firo/firo_wallet.dart index ea0a255d4..44a29ee96 100644 --- a/lib/services/coins/firo/firo_wallet.dart +++ b/lib/services/coins/firo/firo_wallet.dart @@ -3046,10 +3046,9 @@ class FiroWallet extends CoinServiceAPI try { await db.isar.writeTxn(() async { for (final c in updatedCoins) { - await db.isar.lelantusCoins.deleteByPublicCoinWalletIdTxid( - c.publicCoin, + await db.isar.lelantusCoins.deleteByMintIndexWalletId( + c.mintIndex, c.walletId, - c.txid, ); } await db.isar.lelantusCoins.putAll(updatedCoins); From 3cbde8b5c1b91b77297a7f1d436a63509127df35 Mon Sep 17 00:00:00 2001 From: ryleedavis Date: Tue, 25 Jul 2023 14:52:58 -0600 Subject: [PATCH 129/169] able to download monkey.png --- lib/pages/monkey/monkey_view.dart | 79 +++++++++++++++++++++++++++++-- 1 file changed, 75 insertions(+), 4 deletions(-) diff --git a/lib/pages/monkey/monkey_view.dart b/lib/pages/monkey/monkey_view.dart index 10b9c91bf..f109dd5c7 100644 --- a/lib/pages/monkey/monkey_view.dart +++ b/lib/pages/monkey/monkey_view.dart @@ -3,7 +3,10 @@ import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/svg.dart'; +import 'package:http/http.dart' as http; +import 'package:path_provider/path_provider.dart'; import 'package:stackwallet/pages/monkey/sub_widgets/fetch_monkey_dialog.dart'; +import 'package:stackwallet/providers/global/wallets_provider.dart'; import 'package:stackwallet/services/coins/manager.dart'; import 'package:stackwallet/themes/coin_icon_provider.dart'; import 'package:stackwallet/themes/stack_colors.dart'; @@ -36,11 +39,69 @@ class _MonkeyViewState extends ConsumerState { late final String walletId; late final ChangeNotifierProvider managerProvider; + String receivingAddress = ""; + + void getMonkeySVG(String address) async { + if (address.isEmpty) { + //address shouldn't be empty + return; + } + + final http.Response response = await http + .get(Uri.parse('https://monkey.banano.cc/api/v1/monkey/$address')); + final decodedResponse = SvgPicture.memory(response.bodyBytes); + // final decodedResponse = json.decode(response.body); + // return decodedResponse; + debugPrint("$decodedResponse"); + } + + void getMonkeyPNG(String address) async { + if (address.isEmpty) { + //address shouldn't be empty + return; + } + + final http.Response response = await http.get(Uri.parse( + 'https://monkey.banano.cc/api/v1/monkey/${address}?format=png&size=512&background=false')); + + if (response.statusCode == 200) { + final decodedResponse = response.bodyBytes; + final directory = await getApplicationDocumentsDirectory(); + // Directory appDir = await getTemporaryDirectory(); + final docPath = directory.path; + final filePath = "$docPath/monkey.png"; + + File imgFile = File(filePath); + await imgFile.writeAsBytes(decodedResponse); + print("$imgFile"); + + // final directory = await getApplicationDocumentsDirectory(); + // final docPath = directory.path; + // final filePath = "$do/monkey.png"; + } else { + throw Exception("Failed to get MonKey"); + } + + // final decodedResponse = json.decode(response.body); + // return decodedResponse; + // debugPrint("$decodedResponse"); + } + @override void initState() { walletId = widget.walletId; managerProvider = widget.managerProvider; + WidgetsBinding.instance.addPostFrameCallback((timeStamp) async { + final address = await ref + .read(walletsChangeNotifierProvider) + .getManager(walletId) + .currentReceivingAddress; + setState(() { + receivingAddress = address; + }); + }); + super.initState(); } @@ -52,7 +113,7 @@ class _MonkeyViewState extends ConsumerState { @override Widget build(BuildContext context) { final Coin coin = ref.watch(managerProvider.select((value) => value.coin)); - bool isMonkey = false; + bool isMonkey = true; return Background( child: Stack( @@ -116,19 +177,29 @@ class _MonkeyViewState extends ConsumerState { body: isMonkey ? Column( children: [ - Spacer(), + Spacer( + flex: 1, + ), + Image.network( + 'https://monkey.banano.cc/api/v1/monkey/$receivingAddress?format=png&size=512', + ), + Spacer( + flex: 1, + ), Padding( padding: const EdgeInsets.all(16.0), child: Column( children: [ SecondaryButton( label: "Download as SVG", - onPressed: () {}, + onPressed: () async {}, ), const SizedBox(height: 12), SecondaryButton( label: "Download as PNG", - onPressed: () {}, + onPressed: () { + getMonkeyPNG(receivingAddress); + }, ), ], ), From ce1dce113062ead4fbd74ef70aa5f08e19ad91ae Mon Sep 17 00:00:00 2001 From: julian Date: Tue, 25 Jul 2023 14:56:31 -0600 Subject: [PATCH 130/169] comment and fix error messages --- lib/services/coins/firo/firo_wallet.dart | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/lib/services/coins/firo/firo_wallet.dart b/lib/services/coins/firo/firo_wallet.dart index 44a29ee96..861263625 100644 --- a/lib/services/coins/firo/firo_wallet.dart +++ b/lib/services/coins/firo/firo_wallet.dart @@ -1138,7 +1138,8 @@ class FiroWallet extends CoinServiceAPI } } } catch (e, s) { - Logging.instance.log("Exception rethrown from prepareSend(): $e\n$s", + Logging.instance.log( + "Exception rethrown from prepareSendPublic(): $e\n$s", level: LogLevel.Error); rethrow; } @@ -1146,7 +1147,8 @@ class FiroWallet extends CoinServiceAPI throw ArgumentError("Invalid fee rate argument provided!"); } } catch (e, s) { - Logging.instance.log("Exception rethrown from prepareSend(): $e\n$s", + Logging.instance.log( + "Exception rethrown from prepareSendPublic(): $e\n$s", level: LogLevel.Error); rethrow; } @@ -2346,6 +2348,8 @@ class FiroWallet extends CoinServiceAPI final lelantusEntries = await Future.wait(waitLelantusEntries); if (lelantusEntries.isNotEmpty) { + // should be redundant as _getUnspentCoins() should + // already remove all where value=0 lelantusEntries.removeWhere((element) => element.amount == 0); } From 759e5624e43d139381027264deebb7b4c444cb0b Mon Sep 17 00:00:00 2001 From: julian Date: Tue, 25 Jul 2023 15:11:34 -0600 Subject: [PATCH 131/169] stop storing public coin --- .../models/firo_specific/lelantus_coin.dart | 13 - .../models/firo_specific/lelantus_coin.g.dart | 464 +----------------- lib/services/coins/firo/firo_wallet.dart | 14 +- lib/utilities/db_version_migration.dart | 1 - 4 files changed, 16 insertions(+), 476 deletions(-) diff --git a/lib/models/isar/models/firo_specific/lelantus_coin.dart b/lib/models/isar/models/firo_specific/lelantus_coin.dart index 9ad5bc5a3..ca4c11919 100644 --- a/lib/models/isar/models/firo_specific/lelantus_coin.dart +++ b/lib/models/isar/models/firo_specific/lelantus_coin.dart @@ -9,16 +9,6 @@ class LelantusCoin { @Index() final String walletId; - @Index( - unique: true, - composite: [ - CompositeIndex("walletId"), - CompositeIndex("txid"), - ], - replace: false, - ) - final String publicCoin; - final String txid; final String value; // can't use BigInt in isar :shrug: @@ -42,7 +32,6 @@ class LelantusCoin { LelantusCoin({ required this.walletId, - required this.publicCoin, required this.txid, required this.value, required this.mintIndex, @@ -65,7 +54,6 @@ class LelantusCoin { }) { return LelantusCoin( walletId: walletId ?? this.walletId, - publicCoin: publicCoin ?? this.publicCoin, txid: txid ?? this.txid, value: value ?? this.value, mintIndex: mintIndex ?? this.mintIndex, @@ -81,7 +69,6 @@ class LelantusCoin { return 'LelantusCoin{' 'id: $id, ' 'walletId: $walletId, ' - 'publicCoin: $publicCoin, ' 'txid: $txid, ' 'value: $value, ' 'mintIndex: $mintIndex, ' diff --git a/lib/models/isar/models/firo_specific/lelantus_coin.g.dart b/lib/models/isar/models/firo_specific/lelantus_coin.g.dart index d2e27638d..4b9214889 100644 --- a/lib/models/isar/models/firo_specific/lelantus_coin.g.dart +++ b/lib/models/isar/models/firo_specific/lelantus_coin.g.dart @@ -42,23 +42,18 @@ const LelantusCoinSchema = CollectionSchema( name: r'otherData', type: IsarType.string, ), - r'publicCoin': PropertySchema( - id: 5, - name: r'publicCoin', - type: IsarType.string, - ), r'txid': PropertySchema( - id: 6, + id: 5, name: r'txid', type: IsarType.string, ), r'value': PropertySchema( - id: 7, + id: 6, name: r'value', type: IsarType.string, ), r'walletId': PropertySchema( - id: 8, + id: 7, name: r'walletId', type: IsarType.string, ) @@ -82,29 +77,6 @@ const LelantusCoinSchema = CollectionSchema( ) ], ), - r'publicCoin_walletId_txid': IndexSchema( - id: 5610740154835640070, - name: r'publicCoin_walletId_txid', - unique: true, - replace: false, - properties: [ - IndexPropertySchema( - name: r'publicCoin', - type: IndexType.hash, - caseSensitive: true, - ), - IndexPropertySchema( - name: r'walletId', - type: IndexType.hash, - caseSensitive: true, - ), - IndexPropertySchema( - name: r'txid', - type: IndexType.hash, - caseSensitive: true, - ) - ], - ), r'mintIndex_walletId': IndexSchema( id: -9147309777276196770, name: r'mintIndex_walletId', @@ -144,7 +116,6 @@ int _lelantusCoinEstimateSize( bytesCount += 3 + value.length * 3; } } - bytesCount += 3 + object.publicCoin.length * 3; bytesCount += 3 + object.txid.length * 3; bytesCount += 3 + object.value.length * 3; bytesCount += 3 + object.walletId.length * 3; @@ -162,10 +133,9 @@ void _lelantusCoinSerialize( writer.writeBool(offsets[2], object.isUsed); writer.writeLong(offsets[3], object.mintIndex); writer.writeString(offsets[4], object.otherData); - writer.writeString(offsets[5], object.publicCoin); - writer.writeString(offsets[6], object.txid); - writer.writeString(offsets[7], object.value); - writer.writeString(offsets[8], object.walletId); + writer.writeString(offsets[5], object.txid); + writer.writeString(offsets[6], object.value); + writer.writeString(offsets[7], object.walletId); } LelantusCoin _lelantusCoinDeserialize( @@ -180,10 +150,9 @@ LelantusCoin _lelantusCoinDeserialize( isUsed: reader.readBool(offsets[2]), mintIndex: reader.readLong(offsets[3]), otherData: reader.readStringOrNull(offsets[4]), - publicCoin: reader.readString(offsets[5]), - txid: reader.readString(offsets[6]), - value: reader.readString(offsets[7]), - walletId: reader.readString(offsets[8]), + txid: reader.readString(offsets[5]), + value: reader.readString(offsets[6]), + walletId: reader.readString(offsets[7]), ); object.id = id; return object; @@ -212,8 +181,6 @@ P _lelantusCoinDeserializeProp

( return (reader.readString(offset)) as P; case 7: return (reader.readString(offset)) as P; - case 8: - return (reader.readString(offset)) as P; default: throw IsarError('Unknown property with id $propertyId'); } @@ -233,106 +200,6 @@ void _lelantusCoinAttach( } extension LelantusCoinByIndex on IsarCollection { - Future getByPublicCoinWalletIdTxid( - String publicCoin, String walletId, String txid) { - return getByIndex( - r'publicCoin_walletId_txid', [publicCoin, walletId, txid]); - } - - LelantusCoin? getByPublicCoinWalletIdTxidSync( - String publicCoin, String walletId, String txid) { - return getByIndexSync( - r'publicCoin_walletId_txid', [publicCoin, walletId, txid]); - } - - Future deleteByPublicCoinWalletIdTxid( - String publicCoin, String walletId, String txid) { - return deleteByIndex( - r'publicCoin_walletId_txid', [publicCoin, walletId, txid]); - } - - bool deleteByPublicCoinWalletIdTxidSync( - String publicCoin, String walletId, String txid) { - return deleteByIndexSync( - r'publicCoin_walletId_txid', [publicCoin, walletId, txid]); - } - - Future> getAllByPublicCoinWalletIdTxid( - List publicCoinValues, - List walletIdValues, - List txidValues) { - final len = publicCoinValues.length; - assert(walletIdValues.length == len && txidValues.length == len, - 'All index values must have the same length'); - final values = >[]; - for (var i = 0; i < len; i++) { - values.add([publicCoinValues[i], walletIdValues[i], txidValues[i]]); - } - - return getAllByIndex(r'publicCoin_walletId_txid', values); - } - - List getAllByPublicCoinWalletIdTxidSync( - List publicCoinValues, - List walletIdValues, - List txidValues) { - final len = publicCoinValues.length; - assert(walletIdValues.length == len && txidValues.length == len, - 'All index values must have the same length'); - final values = >[]; - for (var i = 0; i < len; i++) { - values.add([publicCoinValues[i], walletIdValues[i], txidValues[i]]); - } - - return getAllByIndexSync(r'publicCoin_walletId_txid', values); - } - - Future deleteAllByPublicCoinWalletIdTxid(List publicCoinValues, - List walletIdValues, List txidValues) { - final len = publicCoinValues.length; - assert(walletIdValues.length == len && txidValues.length == len, - 'All index values must have the same length'); - final values = >[]; - for (var i = 0; i < len; i++) { - values.add([publicCoinValues[i], walletIdValues[i], txidValues[i]]); - } - - return deleteAllByIndex(r'publicCoin_walletId_txid', values); - } - - int deleteAllByPublicCoinWalletIdTxidSync(List publicCoinValues, - List walletIdValues, List txidValues) { - final len = publicCoinValues.length; - assert(walletIdValues.length == len && txidValues.length == len, - 'All index values must have the same length'); - final values = >[]; - for (var i = 0; i < len; i++) { - values.add([publicCoinValues[i], walletIdValues[i], txidValues[i]]); - } - - return deleteAllByIndexSync(r'publicCoin_walletId_txid', values); - } - - Future putByPublicCoinWalletIdTxid(LelantusCoin object) { - return putByIndex(r'publicCoin_walletId_txid', object); - } - - Id putByPublicCoinWalletIdTxidSync(LelantusCoin object, - {bool saveLinks = true}) { - return putByIndexSync(r'publicCoin_walletId_txid', object, - saveLinks: saveLinks); - } - - Future> putAllByPublicCoinWalletIdTxid(List objects) { - return putAllByIndex(r'publicCoin_walletId_txid', objects); - } - - List putAllByPublicCoinWalletIdTxidSync(List objects, - {bool saveLinks = true}) { - return putAllByIndexSync(r'publicCoin_walletId_txid', objects, - saveLinks: saveLinks); - } - Future getByMintIndexWalletId(int mintIndex, String walletId) { return getByIndex(r'mintIndex_walletId', [mintIndex, walletId]); } @@ -543,144 +410,6 @@ extension LelantusCoinQueryWhere }); } - QueryBuilder - publicCoinEqualToAnyWalletIdTxid(String publicCoin) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause(IndexWhereClause.equalTo( - indexName: r'publicCoin_walletId_txid', - value: [publicCoin], - )); - }); - } - - QueryBuilder - publicCoinNotEqualToAnyWalletIdTxid(String publicCoin) { - return QueryBuilder.apply(this, (query) { - if (query.whereSort == Sort.asc) { - return query - .addWhereClause(IndexWhereClause.between( - indexName: r'publicCoin_walletId_txid', - lower: [], - upper: [publicCoin], - includeUpper: false, - )) - .addWhereClause(IndexWhereClause.between( - indexName: r'publicCoin_walletId_txid', - lower: [publicCoin], - includeLower: false, - upper: [], - )); - } else { - return query - .addWhereClause(IndexWhereClause.between( - indexName: r'publicCoin_walletId_txid', - lower: [publicCoin], - includeLower: false, - upper: [], - )) - .addWhereClause(IndexWhereClause.between( - indexName: r'publicCoin_walletId_txid', - lower: [], - upper: [publicCoin], - includeUpper: false, - )); - } - }); - } - - QueryBuilder - publicCoinWalletIdEqualToAnyTxid(String publicCoin, String walletId) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause(IndexWhereClause.equalTo( - indexName: r'publicCoin_walletId_txid', - value: [publicCoin, walletId], - )); - }); - } - - QueryBuilder - publicCoinEqualToWalletIdNotEqualToAnyTxid( - String publicCoin, String walletId) { - return QueryBuilder.apply(this, (query) { - if (query.whereSort == Sort.asc) { - return query - .addWhereClause(IndexWhereClause.between( - indexName: r'publicCoin_walletId_txid', - lower: [publicCoin], - upper: [publicCoin, walletId], - includeUpper: false, - )) - .addWhereClause(IndexWhereClause.between( - indexName: r'publicCoin_walletId_txid', - lower: [publicCoin, walletId], - includeLower: false, - upper: [publicCoin], - )); - } else { - return query - .addWhereClause(IndexWhereClause.between( - indexName: r'publicCoin_walletId_txid', - lower: [publicCoin, walletId], - includeLower: false, - upper: [publicCoin], - )) - .addWhereClause(IndexWhereClause.between( - indexName: r'publicCoin_walletId_txid', - lower: [publicCoin], - upper: [publicCoin, walletId], - includeUpper: false, - )); - } - }); - } - - QueryBuilder - publicCoinWalletIdTxidEqualTo( - String publicCoin, String walletId, String txid) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause(IndexWhereClause.equalTo( - indexName: r'publicCoin_walletId_txid', - value: [publicCoin, walletId, txid], - )); - }); - } - - QueryBuilder - publicCoinWalletIdEqualToTxidNotEqualTo( - String publicCoin, String walletId, String txid) { - return QueryBuilder.apply(this, (query) { - if (query.whereSort == Sort.asc) { - return query - .addWhereClause(IndexWhereClause.between( - indexName: r'publicCoin_walletId_txid', - lower: [publicCoin, walletId], - upper: [publicCoin, walletId, txid], - includeUpper: false, - )) - .addWhereClause(IndexWhereClause.between( - indexName: r'publicCoin_walletId_txid', - lower: [publicCoin, walletId, txid], - includeLower: false, - upper: [publicCoin, walletId], - )); - } else { - return query - .addWhereClause(IndexWhereClause.between( - indexName: r'publicCoin_walletId_txid', - lower: [publicCoin, walletId, txid], - includeLower: false, - upper: [publicCoin, walletId], - )) - .addWhereClause(IndexWhereClause.between( - indexName: r'publicCoin_walletId_txid', - lower: [publicCoin, walletId], - upper: [publicCoin, walletId, txid], - includeUpper: false, - )); - } - }); - } - QueryBuilder mintIndexEqualToAnyWalletId(int mintIndex) { return QueryBuilder.apply(this, (query) { @@ -1161,142 +890,6 @@ extension LelantusCoinQueryFilter }); } - QueryBuilder - publicCoinEqualTo( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.equalTo( - property: r'publicCoin', - value: value, - caseSensitive: caseSensitive, - )); - }); - } - - QueryBuilder - publicCoinGreaterThan( - String value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.greaterThan( - include: include, - property: r'publicCoin', - value: value, - caseSensitive: caseSensitive, - )); - }); - } - - QueryBuilder - publicCoinLessThan( - String value, { - bool include = false, - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.lessThan( - include: include, - property: r'publicCoin', - value: value, - caseSensitive: caseSensitive, - )); - }); - } - - QueryBuilder - publicCoinBetween( - 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'publicCoin', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - caseSensitive: caseSensitive, - )); - }); - } - - QueryBuilder - publicCoinStartsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.startsWith( - property: r'publicCoin', - value: value, - caseSensitive: caseSensitive, - )); - }); - } - - QueryBuilder - publicCoinEndsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.endsWith( - property: r'publicCoin', - value: value, - caseSensitive: caseSensitive, - )); - }); - } - - QueryBuilder - publicCoinContains(String value, {bool caseSensitive = true}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.contains( - property: r'publicCoin', - value: value, - caseSensitive: caseSensitive, - )); - }); - } - - QueryBuilder - publicCoinMatches(String pattern, {bool caseSensitive = true}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.matches( - property: r'publicCoin', - wildcard: pattern, - caseSensitive: caseSensitive, - )); - }); - } - - QueryBuilder - publicCoinIsEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.equalTo( - property: r'publicCoin', - value: '', - )); - }); - } - - QueryBuilder - publicCoinIsNotEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.greaterThan( - property: r'publicCoin', - value: '', - )); - }); - } - QueryBuilder txidEqualTo( String value, { bool caseSensitive = true, @@ -1772,19 +1365,6 @@ extension LelantusCoinQuerySortBy }); } - QueryBuilder sortByPublicCoin() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'publicCoin', Sort.asc); - }); - } - - QueryBuilder - sortByPublicCoinDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'publicCoin', Sort.desc); - }); - } - QueryBuilder sortByTxid() { return QueryBuilder.apply(this, (query) { return query.addSortBy(r'txid', Sort.asc); @@ -1898,19 +1478,6 @@ extension LelantusCoinQuerySortThenBy }); } - QueryBuilder thenByPublicCoin() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'publicCoin', Sort.asc); - }); - } - - QueryBuilder - thenByPublicCoinDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'publicCoin', Sort.desc); - }); - } - QueryBuilder thenByTxid() { return QueryBuilder.apply(this, (query) { return query.addSortBy(r'txid', Sort.asc); @@ -1982,13 +1549,6 @@ extension LelantusCoinQueryWhereDistinct }); } - QueryBuilder distinctByPublicCoin( - {bool caseSensitive = true}) { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'publicCoin', caseSensitive: caseSensitive); - }); - } - QueryBuilder distinctByTxid( {bool caseSensitive = true}) { return QueryBuilder.apply(this, (query) { @@ -2049,12 +1609,6 @@ extension LelantusCoinQueryProperty }); } - QueryBuilder publicCoinProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'publicCoin'); - }); - } - QueryBuilder txidProperty() { return QueryBuilder.apply(this, (query) { return query.addPropertyName(r'txid'); diff --git a/lib/services/coins/firo/firo_wallet.dart b/lib/services/coins/firo/firo_wallet.dart index 861263625..3b67d96c3 100644 --- a/lib/services/coins/firo/firo_wallet.dart +++ b/lib/services/coins/firo/firo_wallet.dart @@ -276,7 +276,7 @@ Future> isolateRestore( walletId: walletId, mintIndex: currentIndex, value: amount.toString(), - publicCoin: publicCoin, + // publicCoin: publicCoin, txid: txId, anonymitySetId: setId, isUsed: isUsed, @@ -326,7 +326,7 @@ Future> isolateRestore( walletId: walletId, mintIndex: currentIndex, value: amount.toString(), - publicCoin: publicCoin, + // publicCoin: publicCoin, txid: txId, anonymitySetId: setId, isUsed: isUsed, @@ -682,7 +682,7 @@ Future isolateCreateJoinSplitTransaction( "fee": fee, "vSize": extTx.virtualSize(), "jmintValue": changeToMint, - "publicCoin": "jmintData.publicCoin", + // "publicCoin": "jmintData.publicCoin", "spendCoinIndexes": spendCoinIndexes, "height": locktime, "txType": "Sent", @@ -2647,7 +2647,7 @@ class FiroWallet extends CoinServiceAPI "value": mintValue, "script": mint, "index": index, - "publicCoin": "", + // "publicCoin": "", }); tmpTotal = tmpTotal - (coin == Coin.firoTestNet ? MINT_LIMIT_TESTNET : MINT_LIMIT); @@ -2834,7 +2834,7 @@ class FiroWallet extends CoinServiceAPI rawValue: BigInt.from(fee), fractionDigits: coin.decimals, ).decimal.toDouble(), - "publicCoin": "", + // "publicCoin": "", "height": height, "txType": "Sent", "confirmed_status": false, @@ -3036,7 +3036,7 @@ class FiroWallet extends CoinServiceAPI walletId: walletId, mintIndex: nextFreeMintIndex, value: (transactionInfo['jmintValue'] as int? ?? 0).toString(), - publicCoin: transactionInfo['publicCoin'] as String, + // publicCoin: transactionInfo['publicCoin'] as String, txid: transactionInfo['txid'] as String, anonymitySetId: latestSetId, isUsed: false, @@ -3130,7 +3130,7 @@ class FiroWallet extends CoinServiceAPI walletId: walletId, mintIndex: index, value: (mintMap['value'] as int).toString(), - publicCoin: mintMap['publicCoin'] as String, + // publicCoin: mintMap['publicCoin'] as String, txid: transactionInfo['txid'] as String, anonymitySetId: latestSetId, isUsed: false, diff --git a/lib/utilities/db_version_migration.dart b/lib/utilities/db_version_migration.dart index d2c0adbe8..f54c2b85a 100644 --- a/lib/utilities/db_version_migration.dart +++ b/lib/utilities/db_version_migration.dart @@ -400,7 +400,6 @@ class DbVersionMigrator with WalletDB { final coin = isar_models.LelantusCoin( walletId: walletId, - publicCoin: lcoin.publicCoin, txid: lcoin.txId, value: lcoin.value.toString(), mintIndex: lcoin.index, From 7919353881c132684d23cc74a033be91836210e0 Mon Sep 17 00:00:00 2001 From: julian Date: Tue, 25 Jul 2023 15:11:56 -0600 Subject: [PATCH 132/169] update mocks --- test/services/coins/manager_test.mocks.dart | 9 --------- test/widget_tests/transaction_card_test.mocks.dart | 9 --------- 2 files changed, 18 deletions(-) diff --git a/test/services/coins/manager_test.mocks.dart b/test/services/coins/manager_test.mocks.dart index 25fd46d9d..5656feb88 100644 --- a/test/services/coins/manager_test.mocks.dart +++ b/test/services/coins/manager_test.mocks.dart @@ -745,15 +745,6 @@ class MockFiroWallet extends _i1.Mock implements _i10.FiroWallet { returnValueForMissingStub: _i11.Future.value(), ) as _i11.Future); @override - _i11.Future getCoinsToJoinSplit(int? required) => - (super.noSuchMethod( - Invocation.method( - #getCoinsToJoinSplit, - [required], - ), - returnValue: _i11.Future.value(), - ) as _i11.Future); - @override _i11.Future estimateJoinSplitFee(int? spendAmount) => (super.noSuchMethod( Invocation.method( diff --git a/test/widget_tests/transaction_card_test.mocks.dart b/test/widget_tests/transaction_card_test.mocks.dart index 29bd7bd29..74fa6e89e 100644 --- a/test/widget_tests/transaction_card_test.mocks.dart +++ b/test/widget_tests/transaction_card_test.mocks.dart @@ -1759,15 +1759,6 @@ class MockFiroWallet extends _i1.Mock implements _i23.FiroWallet { returnValueForMissingStub: _i19.Future.value(), ) as _i19.Future); @override - _i19.Future getCoinsToJoinSplit(int? required) => - (super.noSuchMethod( - Invocation.method( - #getCoinsToJoinSplit, - [required], - ), - returnValue: _i19.Future.value(), - ) as _i19.Future); - @override _i19.Future estimateJoinSplitFee(int? spendAmount) => (super.noSuchMethod( Invocation.method( From 90b16c576e2cacc9437230ecd9b8d066b814f98a Mon Sep 17 00:00:00 2001 From: julian Date: Tue, 25 Jul 2023 15:36:37 -0600 Subject: [PATCH 133/169] clean up --- lib/services/coins/firo/firo_wallet.dart | 84 +++--------------------- 1 file changed, 10 insertions(+), 74 deletions(-) diff --git a/lib/services/coins/firo/firo_wallet.dart b/lib/services/coins/firo/firo_wallet.dart index 3b67d96c3..7dedf2958 100644 --- a/lib/services/coins/firo/firo_wallet.dart +++ b/lib/services/coins/firo/firo_wallet.dart @@ -276,12 +276,12 @@ Future> isolateRestore( walletId: walletId, mintIndex: currentIndex, value: amount.toString(), - // publicCoin: publicCoin, txid: txId, anonymitySetId: setId, isUsed: isUsed, isJMint: false, - otherData: null, + otherData: + publicCoin, // not really needed but saved just in case ), ); Logging.instance.log( @@ -326,12 +326,12 @@ Future> isolateRestore( walletId: walletId, mintIndex: currentIndex, value: amount.toString(), - // publicCoin: publicCoin, txid: txId, anonymitySetId: setId, isUsed: isUsed, isJMint: true, - otherData: null, + otherData: + publicCoin, // not really needed but saved just in case ), ); jindexes.add(currentIndex); @@ -370,7 +370,6 @@ Future> isolateRestore( // Logging.instance.log("jmints $spendTxIds", addToDebugMessagesDB: false); result['_lelantus_coins'] = lelantusCoins; - result['mintIndex'] = lastFoundIndex + 1; result['jindex'] = jindexes; result['spendTxIds'] = spendTxIds; @@ -682,7 +681,6 @@ Future isolateCreateJoinSplitTransaction( "fee": fee, "vSize": extTx.virtualSize(), "jmintValue": changeToMint, - // "publicCoin": "jmintData.publicCoin", "spendCoinIndexes": spendCoinIndexes, "height": locktime, "txType": "Sent", @@ -1241,53 +1239,6 @@ class FiroWallet extends CoinServiceAPI } } - // /// returns txid on successful send - // /// - // /// can throw - // @override - // Future send({ - // required String toAddress, - // required int amount, - // Map args = const {}, - // }) async { - // try { - // dynamic txHexOrError = - // await _createJoinSplitTransaction(amount, toAddress, false); - // Logging.instance.log("txHexOrError $txHexOrError", level: LogLevel.Error); - // if (txHexOrError is int) { - // // Here, we assume that transaction crafting returned an error - // switch (txHexOrError) { - // case 1: - // throw Exception("Insufficient balance!"); - // default: - // throw Exception("Error Creating Transaction!"); - // } - // } else { - // if (await _submitLelantusToNetwork( - // txHexOrError as Map)) { - // final txid = txHexOrError["txid"] as String; - // - // // temporarily update apdate available balance until a full refresh is done - // Decimal sendTotal = - // Format.satoshisToAmount(txHexOrError["value"] as int, coin: coin); - // sendTotal += Decimal.parse(txHexOrError["fees"].toString()); - // final bals = await balances; - // bals[0] -= sendTotal; - // _balances = Future(() => bals); - // - // return txid; - // } else { - // //TODO provide more info - // throw Exception("Transaction failed."); - // } - // } - // } catch (e, s) { - // Logging.instance.log("Exception rethrown in firo send(): $e\n$s", - // level: LogLevel.Error); - // rethrow; - // } - // } - Future> _getMnemonicList() async { final _mnemonicString = await mnemonicString; if (_mnemonicString == null) { @@ -2642,12 +2593,10 @@ class FiroWallet extends CoinServiceAPI index, ); - // TODO publicCoin prob shouldn't be empty? mints.add({ "value": mintValue, "script": mint, "index": index, - // "publicCoin": "", }); tmpTotal = tmpTotal - (coin == Coin.firoTestNet ? MINT_LIMIT_TESTNET : MINT_LIMIT); @@ -2834,7 +2783,6 @@ class FiroWallet extends CoinServiceAPI rawValue: BigInt.from(fee), fractionDigits: coin.decimals, ).decimal.toDouble(), - // "publicCoin": "", "height": height, "txType": "Sent", "confirmed_status": false, @@ -2848,6 +2796,7 @@ class FiroWallet extends CoinServiceAPI }; } + // TODO: verify this function does what we think it does Future _refreshLelantusData() async { final lelantusCoins = await db.isar.lelantusCoins .where() @@ -2916,8 +2865,6 @@ class FiroWallet extends CoinServiceAPI } } - // TODO: optimize this whole lelantus process - final List> txnsData = []; @@ -3013,21 +2960,13 @@ class FiroWallet extends CoinServiceAPI // Update all of the coins that have been spent. for (final index in spentCoinIndexes) { - final possibleCoins = await db.isar.lelantusCoins + final possibleCoin = await db.isar.lelantusCoins .where() - .walletIdEqualTo(walletId) - .filter() - .mintIndexEqualTo(index) - .findAll(); + .mintIndexWalletIdEqualTo(index, walletId) + .findFirst(); - if (possibleCoins.isNotEmpty) { - if (possibleCoins.length > 1) { - print( - "======================= possibleCoins.length > 1 !!! ================================="); - } else { - final spentCoin = possibleCoins.first; - updatedCoins.add(spentCoin.copyWith(isUsed: true)); - } + if (possibleCoin != null) { + updatedCoins.add(possibleCoin.copyWith(isUsed: true)); } } @@ -3036,7 +2975,6 @@ class FiroWallet extends CoinServiceAPI walletId: walletId, mintIndex: nextFreeMintIndex, value: (transactionInfo['jmintValue'] as int? ?? 0).toString(), - // publicCoin: transactionInfo['publicCoin'] as String, txid: transactionInfo['txid'] as String, anonymitySetId: latestSetId, isUsed: false, @@ -3122,7 +3060,6 @@ class FiroWallet extends CoinServiceAPI final List updatedCoins = []; - // TODO: transactionInfo['mintsMap'] for (final mintMap in transactionInfo['mintsMap'] as List>) { final index = mintMap['index'] as int; @@ -3130,7 +3067,6 @@ class FiroWallet extends CoinServiceAPI walletId: walletId, mintIndex: index, value: (mintMap['value'] as int).toString(), - // publicCoin: mintMap['publicCoin'] as String, txid: transactionInfo['txid'] as String, anonymitySetId: latestSetId, isUsed: false, From c1f73a89be16827fddd36343029c60212061f948 Mon Sep 17 00:00:00 2001 From: julian Date: Tue, 25 Jul 2023 15:41:06 -0600 Subject: [PATCH 134/169] store jmint separately from updated coins --- lib/services/coins/firo/firo_wallet.dart | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/lib/services/coins/firo/firo_wallet.dart b/lib/services/coins/firo/firo_wallet.dart index 7dedf2958..2a17d0849 100644 --- a/lib/services/coins/firo/firo_wallet.dart +++ b/lib/services/coins/firo/firo_wallet.dart @@ -2981,9 +2981,6 @@ class FiroWallet extends CoinServiceAPI isJMint: true, otherData: null, ); - if (int.parse(jmint.value) > 0) { - updatedCoins.add(jmint); - } try { await db.isar.writeTxn(() async { @@ -2994,6 +2991,10 @@ class FiroWallet extends CoinServiceAPI ); } await db.isar.lelantusCoins.putAll(updatedCoins); + + if (int.parse(jmint.value) > 0) { + await db.isar.lelantusCoins.put(jmint); + } }); } catch (e, s) { Logging.instance.log( From 5d3e976601e7b2e9f63f456931a26301107d48e3 Mon Sep 17 00:00:00 2001 From: julian Date: Tue, 25 Jul 2023 16:08:54 -0600 Subject: [PATCH 135/169] always add lelantus coin to locale db --- lib/services/coins/firo/firo_wallet.dart | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/lib/services/coins/firo/firo_wallet.dart b/lib/services/coins/firo/firo_wallet.dart index 2a17d0849..37c31dce4 100644 --- a/lib/services/coins/firo/firo_wallet.dart +++ b/lib/services/coins/firo/firo_wallet.dart @@ -370,7 +370,6 @@ Future> isolateRestore( // Logging.instance.log("jmints $spendTxIds", addToDebugMessagesDB: false); result['_lelantus_coins'] = lelantusCoins; - result['jindex'] = jindexes; result['spendTxIds'] = spendTxIds; return result; @@ -2992,9 +2991,7 @@ class FiroWallet extends CoinServiceAPI } await db.isar.lelantusCoins.putAll(updatedCoins); - if (int.parse(jmint.value) > 0) { - await db.isar.lelantusCoins.put(jmint); - } + await db.isar.lelantusCoins.put(jmint); }); } catch (e, s) { Logging.instance.log( @@ -3074,9 +3071,8 @@ class FiroWallet extends CoinServiceAPI isJMint: false, otherData: null, ); - if (int.parse(mint.value) > 0) { - updatedCoins.add(mint); - } + + updatedCoins.add(mint); } // Logging.instance.log(coins); try { From b2ec2763fb6aaa8b07db166dee18523a10792189 Mon Sep 17 00:00:00 2001 From: ryleedavis Date: Tue, 25 Jul 2023 16:51:21 -0600 Subject: [PATCH 136/169] monkey.png downloads to user device --- lib/pages/monkey/monkey_view.dart | 55 +++++++++++++++++++++++-------- 1 file changed, 41 insertions(+), 14 deletions(-) diff --git a/lib/pages/monkey/monkey_view.dart b/lib/pages/monkey/monkey_view.dart index f109dd5c7..5c1dbede1 100644 --- a/lib/pages/monkey/monkey_view.dart +++ b/lib/pages/monkey/monkey_view.dart @@ -5,6 +5,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/svg.dart'; import 'package:http/http.dart' as http; import 'package:path_provider/path_provider.dart'; +import 'package:permission_handler/permission_handler.dart'; import 'package:stackwallet/pages/monkey/sub_widgets/fetch_monkey_dialog.dart'; import 'package:stackwallet/providers/global/wallets_provider.dart'; import 'package:stackwallet/services/coins/manager.dart'; @@ -49,10 +50,19 @@ class _MonkeyViewState extends ConsumerState { final http.Response response = await http .get(Uri.parse('https://monkey.banano.cc/api/v1/monkey/$address')); - final decodedResponse = SvgPicture.memory(response.bodyBytes); - // final decodedResponse = json.decode(response.body); - // return decodedResponse; - debugPrint("$decodedResponse"); + if (response.statusCode == 200) { + final decodedResponse = response.bodyBytes; + final directory = await getApplicationDocumentsDirectory(); + // Directory appDir = await getTemporaryDirectory(); + final docPath = directory.path; + final filePath = "$docPath/monkey.svg"; + + File imgFile = File(filePath); + await imgFile.writeAsBytes(decodedResponse); + print("$imgFile"); + } else { + throw Exception("Failed to get MonKey"); + } } void getMonkeyPNG(String address) async { @@ -65,10 +75,29 @@ class _MonkeyViewState extends ConsumerState { 'https://monkey.banano.cc/api/v1/monkey/${address}?format=png&size=512&background=false')); if (response.statusCode == 200) { + if (Platform.isAndroid) { + await Permission.storage.request(); + } + final decodedResponse = response.bodyBytes; - final directory = await getApplicationDocumentsDirectory(); - // Directory appDir = await getTemporaryDirectory(); - final docPath = directory.path; + Directory directory = await getApplicationDocumentsDirectory(); + late Directory sampleFolder; + + if (Platform.isAndroid) { + directory = Directory("/storage/emulated/0/"); + sampleFolder = Directory('${directory!.path}Documents'); + } + + try { + if (!sampleFolder.existsSync()) { + sampleFolder.createSync(recursive: true); + } + } catch (e, s) { + // todo: come back to this + debugPrint("$e $s"); + } + + final docPath = sampleFolder.path; final filePath = "$docPath/monkey.png"; File imgFile = File(filePath); @@ -81,10 +110,6 @@ class _MonkeyViewState extends ConsumerState { } else { throw Exception("Failed to get MonKey"); } - - // final decodedResponse = json.decode(response.body); - // return decodedResponse; - // debugPrint("$decodedResponse"); } @override @@ -177,13 +202,13 @@ class _MonkeyViewState extends ConsumerState { body: isMonkey ? Column( children: [ - Spacer( + const Spacer( flex: 1, ), Image.network( 'https://monkey.banano.cc/api/v1/monkey/$receivingAddress?format=png&size=512', ), - Spacer( + const Spacer( flex: 1, ), Padding( @@ -192,7 +217,9 @@ class _MonkeyViewState extends ConsumerState { children: [ SecondaryButton( label: "Download as SVG", - onPressed: () async {}, + onPressed: () async { + getMonkeySVG(receivingAddress); + }, ), const SizedBox(height: 12), SecondaryButton( From c6c2b429238c120ca19a4f726b8e5935e888cd35 Mon Sep 17 00:00:00 2001 From: ryleedavis Date: Tue, 25 Jul 2023 17:06:32 -0600 Subject: [PATCH 137/169] monkey.svg download to user device --- lib/pages/monkey/monkey_view.dart | 38 ++++++++++++++++++++++++++++--- 1 file changed, 35 insertions(+), 3 deletions(-) diff --git a/lib/pages/monkey/monkey_view.dart b/lib/pages/monkey/monkey_view.dart index 5c1dbede1..df449a91d 100644 --- a/lib/pages/monkey/monkey_view.dart +++ b/lib/pages/monkey/monkey_view.dart @@ -50,11 +50,35 @@ class _MonkeyViewState extends ConsumerState { final http.Response response = await http .get(Uri.parse('https://monkey.banano.cc/api/v1/monkey/$address')); + if (response.statusCode == 200) { final decodedResponse = response.bodyBytes; - final directory = await getApplicationDocumentsDirectory(); - // Directory appDir = await getTemporaryDirectory(); - final docPath = directory.path; + Directory directory = await getApplicationDocumentsDirectory(); + late Directory sampleFolder; + + if (Platform.isAndroid) { + directory = Directory("/storage/emulated/0/"); + sampleFolder = Directory('${directory!.path}Documents'); + } else if (Platform.isIOS) { + sampleFolder = Directory(directory!.path); + } else if (Platform.isLinux) { + sampleFolder = Directory('${directory!.path}Documents'); + } else if (Platform.isWindows) { + sampleFolder = Directory('${directory!.path}Documents'); + } else if (Platform.isMacOS) { + sampleFolder = Directory('${directory!.path}Documents'); + } + + try { + if (!sampleFolder.existsSync()) { + sampleFolder.createSync(recursive: true); + } + } catch (e, s) { + // todo: come back to this + debugPrint("$e $s"); + } + + final docPath = sampleFolder.path; final filePath = "$docPath/monkey.svg"; File imgFile = File(filePath); @@ -86,6 +110,14 @@ class _MonkeyViewState extends ConsumerState { if (Platform.isAndroid) { directory = Directory("/storage/emulated/0/"); sampleFolder = Directory('${directory!.path}Documents'); + } else if (Platform.isIOS) { + sampleFolder = Directory(directory!.path); + } else if (Platform.isLinux) { + sampleFolder = Directory('${directory!.path}Documents'); + } else if (Platform.isWindows) { + sampleFolder = Directory('${directory!.path}Documents'); + } else if (Platform.isMacOS) { + sampleFolder = Directory('${directory!.path}Documents'); } try { From 512960d9b92cf23c1936337927651a9915be9d81 Mon Sep 17 00:00:00 2001 From: ryleedavis Date: Tue, 25 Jul 2023 17:29:05 -0600 Subject: [PATCH 138/169] changed monkey files around --- lib/pages/monkey/monkey_loaded_view.dart | 272 +++++++++++++++++++++++ lib/pages/monkey/monkey_view.dart | 253 ++++++--------------- lib/route_generator.dart | 16 ++ 3 files changed, 350 insertions(+), 191 deletions(-) create mode 100644 lib/pages/monkey/monkey_loaded_view.dart diff --git a/lib/pages/monkey/monkey_loaded_view.dart b/lib/pages/monkey/monkey_loaded_view.dart new file mode 100644 index 000000000..0b49fc738 --- /dev/null +++ b/lib/pages/monkey/monkey_loaded_view.dart @@ -0,0 +1,272 @@ +import 'dart:io'; + +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_svg/svg.dart'; +import 'package:http/http.dart' as http; +import 'package:path_provider/path_provider.dart'; +import 'package:permission_handler/permission_handler.dart'; +import 'package:stackwallet/pages/wallet_view/wallet_view.dart'; +import 'package:stackwallet/providers/global/wallets_provider.dart'; +import 'package:stackwallet/services/coins/manager.dart'; +import 'package:stackwallet/themes/stack_colors.dart'; +import 'package:stackwallet/utilities/assets.dart'; +import 'package:stackwallet/utilities/enums/coin_enum.dart'; +import 'package:stackwallet/utilities/text_styles.dart'; +import 'package:stackwallet/widgets/background.dart'; +import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; +import 'package:stackwallet/widgets/desktop/secondary_button.dart'; + +class MonkeyLoadedView extends ConsumerStatefulWidget { + const MonkeyLoadedView({ + Key? key, + required this.walletId, + required this.managerProvider, + }) : super(key: key); + + static const String routeName = "/hasMonkey"; + static const double navBarHeight = 65.0; + + final String walletId; + final ChangeNotifierProvider managerProvider; + + @override + ConsumerState createState() => _MonkeyLoadedViewState(); +} + +class _MonkeyLoadedViewState extends ConsumerState { + late final String walletId; + late final ChangeNotifierProvider managerProvider; + + String receivingAddress = ""; + + void getMonkeySVG(String address) async { + if (address.isEmpty) { + //address shouldn't be empty + return; + } + + final http.Response response = await http + .get(Uri.parse('https://monkey.banano.cc/api/v1/monkey/$address')); + + if (response.statusCode == 200) { + final decodedResponse = response.bodyBytes; + Directory directory = await getApplicationDocumentsDirectory(); + late Directory sampleFolder; + + if (Platform.isAndroid) { + directory = Directory("/storage/emulated/0/"); + sampleFolder = Directory('${directory!.path}Documents'); + } else if (Platform.isIOS) { + sampleFolder = Directory(directory!.path); + } else if (Platform.isLinux) { + sampleFolder = Directory('${directory!.path}Documents'); + } else if (Platform.isWindows) { + sampleFolder = Directory('${directory!.path}Documents'); + } else if (Platform.isMacOS) { + sampleFolder = Directory('${directory!.path}Documents'); + } + + try { + if (!sampleFolder.existsSync()) { + sampleFolder.createSync(recursive: true); + } + } catch (e, s) { + // todo: come back to this + debugPrint("$e $s"); + } + + final docPath = sampleFolder.path; + final filePath = "$docPath/monkey.svg"; + + File imgFile = File(filePath); + await imgFile.writeAsBytes(decodedResponse); + print("$imgFile"); + } else { + throw Exception("Failed to get MonKey"); + } + } + + void getMonkeyPNG(String address) async { + if (address.isEmpty) { + //address shouldn't be empty + return; + } + + final http.Response response = await http.get(Uri.parse( + 'https://monkey.banano.cc/api/v1/monkey/${address}?format=png&size=512&background=false')); + + if (response.statusCode == 200) { + if (Platform.isAndroid) { + await Permission.storage.request(); + } + + final decodedResponse = response.bodyBytes; + Directory directory = await getApplicationDocumentsDirectory(); + late Directory sampleFolder; + + if (Platform.isAndroid) { + directory = Directory("/storage/emulated/0/"); + sampleFolder = Directory('${directory!.path}Documents'); + } else if (Platform.isIOS) { + sampleFolder = Directory(directory!.path); + } else if (Platform.isLinux) { + sampleFolder = Directory('${directory!.path}Documents'); + } else if (Platform.isWindows) { + sampleFolder = Directory('${directory!.path}Documents'); + } else if (Platform.isMacOS) { + sampleFolder = Directory('${directory!.path}Documents'); + } + + try { + if (!sampleFolder.existsSync()) { + sampleFolder.createSync(recursive: true); + } + } catch (e, s) { + // todo: come back to this + debugPrint("$e $s"); + } + + final docPath = sampleFolder.path; + final filePath = "$docPath/monkey.png"; + + File imgFile = File(filePath); + await imgFile.writeAsBytes(decodedResponse); + print("$imgFile"); + + // final directory = await getApplicationDocumentsDirectory(); + // final docPath = directory.path; + // final filePath = "$do/monkey.png"; + } else { + throw Exception("Failed to get MonKey"); + } + } + + @override + void initState() { + walletId = widget.walletId; + managerProvider = widget.managerProvider; + + WidgetsBinding.instance.addPostFrameCallback((timeStamp) async { + final address = await ref + .read(walletsChangeNotifierProvider) + .getManager(walletId) + .currentReceivingAddress; + setState(() { + receivingAddress = address; + }); + }); + + super.initState(); + } + + @override + void dispose() { + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final Coin coin = ref.watch(managerProvider.select((value) => value.coin)); + bool isMonkey = false; + + return Background( + child: Stack( + children: [ + Scaffold( + appBar: AppBar( + leading: AppBarBackButton( + onPressed: () { + Navigator.of(context).popUntil( + ModalRoute.withName(WalletView.routeName), + ); + }, + ), + title: Text( + "MonKey", + style: STextStyles.navBarTitle(context), + ), + actions: [ + AspectRatio( + aspectRatio: 1, + child: AppBarIconButton( + icon: SvgPicture.asset(Assets.svg.circleQuestion), + onPressed: () { + showDialog( + context: context, + useSafeArea: false, + barrierDismissible: true, + builder: (context) { + return Dialog( + child: Material( + borderRadius: BorderRadius.circular( + 20, + ), + child: Container( + height: 200, + decoration: BoxDecoration( + color: Theme.of(context) + .extension()! + .popupBG, + borderRadius: BorderRadius.circular( + 20, + ), + ), + child: Column( + children: [ + Center( + child: Text( + "Help", + style: STextStyles.pageTitleH2( + context), + ), + ) + ], + ), + ), + ), + ); + }); + }), + ) + ], + ), + body: Column( + children: [ + const Spacer( + flex: 1, + ), + Image.network( + 'https://monkey.banano.cc/api/v1/monkey/$receivingAddress?format=png&size=512', + ), + const Spacer( + flex: 1, + ), + Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + children: [ + SecondaryButton( + label: "Download as SVG", + onPressed: () async { + getMonkeySVG(receivingAddress); + }, + ), + const SizedBox(height: 12), + SecondaryButton( + label: "Download as PNG", + onPressed: () { + getMonkeyPNG(receivingAddress); + }, + ), + ], + ), + ), + ], + ), + ), + ], + ), + ); + } +} diff --git a/lib/pages/monkey/monkey_view.dart b/lib/pages/monkey/monkey_view.dart index df449a91d..17758b9f7 100644 --- a/lib/pages/monkey/monkey_view.dart +++ b/lib/pages/monkey/monkey_view.dart @@ -3,9 +3,7 @@ import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/svg.dart'; -import 'package:http/http.dart' as http; -import 'package:path_provider/path_provider.dart'; -import 'package:permission_handler/permission_handler.dart'; +import 'package:stackwallet/pages/monkey/monkey_loaded_view.dart'; import 'package:stackwallet/pages/monkey/sub_widgets/fetch_monkey_dialog.dart'; import 'package:stackwallet/providers/global/wallets_provider.dart'; import 'package:stackwallet/services/coins/manager.dart'; @@ -17,7 +15,7 @@ import 'package:stackwallet/utilities/text_styles.dart'; import 'package:stackwallet/widgets/background.dart'; import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; import 'package:stackwallet/widgets/desktop/primary_button.dart'; -import 'package:stackwallet/widgets/desktop/secondary_button.dart'; +import 'package:tuple/tuple.dart'; class MonkeyView extends ConsumerStatefulWidget { const MonkeyView({ @@ -42,108 +40,6 @@ class _MonkeyViewState extends ConsumerState { String receivingAddress = ""; - void getMonkeySVG(String address) async { - if (address.isEmpty) { - //address shouldn't be empty - return; - } - - final http.Response response = await http - .get(Uri.parse('https://monkey.banano.cc/api/v1/monkey/$address')); - - if (response.statusCode == 200) { - final decodedResponse = response.bodyBytes; - Directory directory = await getApplicationDocumentsDirectory(); - late Directory sampleFolder; - - if (Platform.isAndroid) { - directory = Directory("/storage/emulated/0/"); - sampleFolder = Directory('${directory!.path}Documents'); - } else if (Platform.isIOS) { - sampleFolder = Directory(directory!.path); - } else if (Platform.isLinux) { - sampleFolder = Directory('${directory!.path}Documents'); - } else if (Platform.isWindows) { - sampleFolder = Directory('${directory!.path}Documents'); - } else if (Platform.isMacOS) { - sampleFolder = Directory('${directory!.path}Documents'); - } - - try { - if (!sampleFolder.existsSync()) { - sampleFolder.createSync(recursive: true); - } - } catch (e, s) { - // todo: come back to this - debugPrint("$e $s"); - } - - final docPath = sampleFolder.path; - final filePath = "$docPath/monkey.svg"; - - File imgFile = File(filePath); - await imgFile.writeAsBytes(decodedResponse); - print("$imgFile"); - } else { - throw Exception("Failed to get MonKey"); - } - } - - void getMonkeyPNG(String address) async { - if (address.isEmpty) { - //address shouldn't be empty - return; - } - - final http.Response response = await http.get(Uri.parse( - 'https://monkey.banano.cc/api/v1/monkey/${address}?format=png&size=512&background=false')); - - if (response.statusCode == 200) { - if (Platform.isAndroid) { - await Permission.storage.request(); - } - - final decodedResponse = response.bodyBytes; - Directory directory = await getApplicationDocumentsDirectory(); - late Directory sampleFolder; - - if (Platform.isAndroid) { - directory = Directory("/storage/emulated/0/"); - sampleFolder = Directory('${directory!.path}Documents'); - } else if (Platform.isIOS) { - sampleFolder = Directory(directory!.path); - } else if (Platform.isLinux) { - sampleFolder = Directory('${directory!.path}Documents'); - } else if (Platform.isWindows) { - sampleFolder = Directory('${directory!.path}Documents'); - } else if (Platform.isMacOS) { - sampleFolder = Directory('${directory!.path}Documents'); - } - - try { - if (!sampleFolder.existsSync()) { - sampleFolder.createSync(recursive: true); - } - } catch (e, s) { - // todo: come back to this - debugPrint("$e $s"); - } - - final docPath = sampleFolder.path; - final filePath = "$docPath/monkey.png"; - - File imgFile = File(filePath); - await imgFile.writeAsBytes(decodedResponse); - print("$imgFile"); - - // final directory = await getApplicationDocumentsDirectory(); - // final docPath = directory.path; - // final filePath = "$do/monkey.png"; - } else { - throw Exception("Failed to get MonKey"); - } - } - @override void initState() { walletId = widget.walletId; @@ -170,7 +66,6 @@ class _MonkeyViewState extends ConsumerState { @override Widget build(BuildContext context) { final Coin coin = ref.watch(managerProvider.select((value) => value.coin)); - bool isMonkey = true; return Background( child: Stack( @@ -231,98 +126,74 @@ class _MonkeyViewState extends ConsumerState { ) ], ), - body: isMonkey - ? Column( + body: Column( + children: [ + const Spacer( + flex: 4, + ), + Center( + child: Column( children: [ - const Spacer( - flex: 1, - ), - Image.network( - 'https://monkey.banano.cc/api/v1/monkey/$receivingAddress?format=png&size=512', - ), - const Spacer( - flex: 1, - ), - Padding( - padding: const EdgeInsets.all(16.0), - child: Column( - children: [ - SecondaryButton( - label: "Download as SVG", - onPressed: () async { - getMonkeySVG(receivingAddress); - }, - ), - const SizedBox(height: 12), - SecondaryButton( - label: "Download as PNG", - onPressed: () { - getMonkeyPNG(receivingAddress); - }, - ), - ], + Opacity( + opacity: 0.2, + child: SvgPicture.file( + File( + ref.watch(coinIconProvider(coin)), + ), + width: 200, + height: 200, ), ), - ], - ) - : Column( - children: [ - const Spacer( - flex: 4, + const SizedBox( + height: 40, ), - Center( - child: Column( - children: [ - Opacity( - opacity: 0.2, - child: SvgPicture.file( - File( - ref.watch(coinIconProvider(coin)), - ), - width: 200, - height: 200, - ), - ), - const SizedBox( - height: 40, - ), - Text( - "You do not have a MonKey yet. \nFetch yours now!", - style: STextStyles.smallMed14(context).copyWith( - color: Theme.of(context) - .extension()! - .textDark3, - ), - textAlign: TextAlign.center, - ), - ], - ), - ), - const Spacer( - flex: 6, - ), - Padding( - padding: const EdgeInsets.all(16.0), - child: PrimaryButton( - label: "Fetch MonKey", - onPressed: () { - showDialog( - context: context, - useSafeArea: false, - barrierDismissible: false, - builder: (context) { - return FetchMonkeyDialog( - onCancel: () async { - Navigator.of(context).pop(); - }, - ); - }, - ); - }, + Text( + "You do not have a MonKey yet. \nFetch yours now!", + style: STextStyles.smallMed14(context).copyWith( + color: Theme.of(context) + .extension()! + .textDark3, ), + textAlign: TextAlign.center, ), ], ), + ), + const Spacer( + flex: 6, + ), + Padding( + padding: const EdgeInsets.all(16.0), + child: PrimaryButton( + label: "Fetch MonKey", + onPressed: () async { + showDialog( + context: context, + useSafeArea: false, + barrierDismissible: false, + builder: (context) { + return FetchMonkeyDialog( + onCancel: () async { + Navigator.of(context).pop(); + }, + ); + }, + ); + + await Future.delayed(const Duration(seconds: 2)); + + Navigator.of(context).pushNamed( + MonkeyLoadedView.routeName, + arguments: Tuple2( + widget.walletId, + widget.managerProvider, + ), + ); + }, + ), + ), + ], + ), ), ], ), diff --git a/lib/route_generator.dart b/lib/route_generator.dart index 4513fd1d6..b02638082 100644 --- a/lib/route_generator.dart +++ b/lib/route_generator.dart @@ -56,6 +56,7 @@ import 'package:stackwallet/pages/generic/single_field_edit_view.dart'; import 'package:stackwallet/pages/home_view/home_view.dart'; import 'package:stackwallet/pages/intro_view.dart'; import 'package:stackwallet/pages/manage_favorites_view/manage_favorites_view.dart'; +import 'package:stackwallet/pages/monkey/monkey_loaded_view.dart'; import 'package:stackwallet/pages/monkey/monkey_view.dart'; import 'package:stackwallet/pages/notification_views/notifications_view.dart'; import 'package:stackwallet/pages/paynym/add_new_paynym_follow_view.dart'; @@ -391,6 +392,21 @@ class RouteGenerator { } return _routeError("${settings.name} invalid args: ${args.toString()}"); + case MonkeyLoadedView.routeName: + if (args is Tuple2>) { + return getRoute( + shouldUseMaterialRoute: useMaterialPageRoute, + builder: (_) => MonkeyLoadedView( + walletId: args.item1, + managerProvider: args.item2, + ), + settings: RouteSettings( + name: settings.name, + ), + ); + } + return _routeError("${settings.name} invalid args: ${args.toString()}"); + case CoinControlView.routeName: if (args is Tuple2) { return getRoute( From 78e4cd463172258cdde6fd2ca98a709ca2b0f7e1 Mon Sep 17 00:00:00 2001 From: ryleedavis Date: Tue, 25 Jul 2023 17:42:36 -0600 Subject: [PATCH 139/169] removed unnecessary lines --- lib/pages/monkey/monkey_loaded_view.dart | 7 ------- 1 file changed, 7 deletions(-) diff --git a/lib/pages/monkey/monkey_loaded_view.dart b/lib/pages/monkey/monkey_loaded_view.dart index 0b49fc738..32570e6a7 100644 --- a/lib/pages/monkey/monkey_loaded_view.dart +++ b/lib/pages/monkey/monkey_loaded_view.dart @@ -81,7 +81,6 @@ class _MonkeyLoadedViewState extends ConsumerState { File imgFile = File(filePath); await imgFile.writeAsBytes(decodedResponse); - print("$imgFile"); } else { throw Exception("Failed to get MonKey"); } @@ -132,11 +131,6 @@ class _MonkeyLoadedViewState extends ConsumerState { File imgFile = File(filePath); await imgFile.writeAsBytes(decodedResponse); - print("$imgFile"); - - // final directory = await getApplicationDocumentsDirectory(); - // final docPath = directory.path; - // final filePath = "$do/monkey.png"; } else { throw Exception("Failed to get MonKey"); } @@ -168,7 +162,6 @@ class _MonkeyLoadedViewState extends ConsumerState { @override Widget build(BuildContext context) { final Coin coin = ref.watch(managerProvider.select((value) => value.coin)); - bool isMonkey = false; return Background( child: Stack( From c3054ca753d314ad5eab3752a76804a4e105d1ab Mon Sep 17 00:00:00 2001 From: julian Date: Tue, 25 Jul 2023 20:28:27 -0600 Subject: [PATCH 140/169] fix broken used serials call and optimize cache a bit --- lib/electrumx_rpc/cached_electrumx.dart | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/lib/electrumx_rpc/cached_electrumx.dart b/lib/electrumx_rpc/cached_electrumx.dart index 67f170bb4..91b7d1bc8 100644 --- a/lib/electrumx_rpc/cached_electrumx.dart +++ b/lib/electrumx_rpc/cached_electrumx.dart @@ -164,14 +164,16 @@ class CachedElectrumX { final _list = box.get("serials") as List?; - List cachedSerials = - _list == null ? [] : List.from(_list); + Set cachedSerials = + _list == null ? {} : List.from(_list).toSet(); - final startNumber = cachedSerials.length; + // startNumber is broken currently + final startNumber = 0; // cachedSerials.length; - final serials = - await electrumXClient.getUsedCoinSerials(startNumber: startNumber); - List newSerials = []; + final serials = await electrumXClient.getUsedCoinSerials( + startNumber: startNumber, + ); + Set newSerials = {}; for (final element in (serials["serials"] as List)) { if (!isHexadecimal(element as String)) { @@ -182,12 +184,14 @@ class CachedElectrumX { } cachedSerials.addAll(newSerials); + final resultingList = cachedSerials.toList(); + await box.put( "serials", - cachedSerials, + resultingList, ); - return cachedSerials; + return resultingList; } catch (e, s) { Logging.instance.log( "Failed to process CachedElectrumX.getTransaction(): $e\n$s", From c9da22601ef0d92057d5ebe0ad58dd5d69dab5b2 Mon Sep 17 00:00:00 2001 From: julian Date: Tue, 25 Jul 2023 20:28:51 -0600 Subject: [PATCH 141/169] clean up spam logs a bit --- lib/services/coins/firo/firo_wallet.dart | 5 ----- 1 file changed, 5 deletions(-) diff --git a/lib/services/coins/firo/firo_wallet.dart b/lib/services/coins/firo/firo_wallet.dart index 37c31dce4..6b2bf733f 100644 --- a/lib/services/coins/firo/firo_wallet.dart +++ b/lib/services/coins/firo/firo_wallet.dart @@ -349,11 +349,6 @@ Future> isolateRestore( level: LogLevel.Warning, ); } - } else { - Logging.instance.log( - "Coin not found in data with the mint tag: $mintTag", - level: LogLevel.Warning, - ); } } From 57839c2d181a06461b8a8640968af0269802220e Mon Sep 17 00:00:00 2001 From: julian Date: Tue, 25 Jul 2023 20:29:44 -0600 Subject: [PATCH 142/169] WIP fixing _refreshLelantusData --- lib/services/coins/firo/firo_wallet.dart | 116 +++++++++-------------- 1 file changed, 44 insertions(+), 72 deletions(-) diff --git a/lib/services/coins/firo/firo_wallet.dart b/lib/services/coins/firo/firo_wallet.dart index 6b2bf733f..da0919570 100644 --- a/lib/services/coins/firo/firo_wallet.dart +++ b/lib/services/coins/firo/firo_wallet.dart @@ -2797,96 +2797,68 @@ class FiroWallet extends CoinServiceAPI .walletIdEqualTo(walletId) .filter() .isUsedEqualTo(false) - .and() - .isJMintEqualTo(true) .not() .valueEqualTo(0.toString()) .findAll(); - // Get all joinsplit transaction ids + final List updatedCoins = []; - final lelantusJoinSplitTxns = await db - .getTransactions(walletId) - .filter() - .isLelantusEqualTo(true) - .and() - .subTypeEqualTo(isar_models.TransactionSubType.join) - .findAll(); + final usedSerialNumbersSet = (await getUsedCoinSerials()).toSet(); - Set joinSplitTXIDs = {}; - - // for (final tx in lelantusJoinSplitTxns) { - // joinSplitTXIDs.add(tx.txid); - // } - for (final coin in lelantusCoins) { - joinSplitTXIDs.add(coin.txid); - } - - Map> - updatedData = {}; - - // Grab the most recent information on all the joinsplits - final updatedJSplit = await getJMintTransactions( - cachedElectrumXClient, - joinSplitTXIDs.toList(), - coin, + final root = await Bip32Utils.getBip32Root( + (await mnemonic).join(" "), + (await mnemonicPassphrase)!, + _network, ); - final currentChainHeight = await chainHeight; + for (final coin in lelantusCoins) { + final _derivePath = constructDerivePath( + networkWIF: _network.wif, + chain: MINT_INDEX, + index: coin.mintIndex, + ); + final bip32.BIP32 mintKeyPair = await Bip32Utils.getBip32NodeFromRoot( + root, + _derivePath, + ); - // update all of joinsplits that are now confirmed. - for (final tx in updatedJSplit.entries) { - isar_models.Transaction? currentTx; + final String serialNumber = GetSerialNumber( + int.parse(coin.value), + Format.uint8listToString(mintKeyPair.privateKey!), + coin.mintIndex, + isTestnet: this.coin == Coin.firoTestNet, + ); + final bool isUsed = usedSerialNumbersSet.contains(serialNumber); - try { - currentTx = - lelantusJoinSplitTxns.firstWhere((e) => e.txid == tx.value.txid); - } catch (_) { - currentTx = null; + if (isUsed) { + updatedCoins.add(coin.copyWith(isUsed: isUsed)); } - if (currentTx == null) { - // this send was accidentally not included in the list - tx.value.isLelantus = true; - updatedData[tx.value.txid] = - Tuple2(tx.value.address.value ?? tx.key, tx.value); - } else if (currentTx.isConfirmed( - currentChainHeight, MINIMUM_CONFIRMATIONS) != - tx.value.isConfirmed(currentChainHeight, MINIMUM_CONFIRMATIONS)) { - tx.value.isLelantus = true; - updatedData[tx.value.txid] = - Tuple2(tx.value.address.value ?? tx.key, tx.value); + final tx = await db.getTransaction(walletId, coin.txid); + if (tx == null) { + print("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"); } } - final List> txnsData = - []; - - for (final value in updatedData.values) { - // allow possible null address on mints as we don't display address - // this should normally never be null anyways but old (dbVersion up to 4) - // migrated transactions may not have had an address (full rescan should - // fix this) - isar_models.Address? transactionAddress; + if (updatedCoins.isNotEmpty) { try { - transactionAddress = - value.item2.subType == isar_models.TransactionSubType.mint - ? value.item1 - : value.item1!; - } catch (_) { - Logging.instance - .log("_refreshLelantusData value: $value", level: LogLevel.Fatal); + await db.isar.writeTxn(() async { + for (final c in updatedCoins) { + await db.isar.lelantusCoins.deleteByMintIndexWalletId( + c.mintIndex, + c.walletId, + ); + } + await db.isar.lelantusCoins.putAll(updatedCoins); + }); + } catch (e, s) { + Logging.instance.log( + "$e\n$s", + level: LogLevel.Fatal, + ); + rethrow; } - final outs = - value.item2.outputs.where((_) => true).toList(growable: false); - final ins = value.item2.inputs.where((_) => true).toList(growable: false); - - txnsData.add(Tuple2( - value.item2.copyWith(inputs: ins, outputs: outs).item1, - transactionAddress)); } - - await db.addNewTransactionData(txnsData, walletId); } Future _getMintHex(int amount, int index) async { From 7b8f26206e7c5883a363d2f76976fef74b0f197c Mon Sep 17 00:00:00 2001 From: julian Date: Tue, 25 Jul 2023 20:48:03 -0600 Subject: [PATCH 143/169] show sent to self jmint transactions correctly --- lib/services/coins/firo/firo_wallet.dart | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/lib/services/coins/firo/firo_wallet.dart b/lib/services/coins/firo/firo_wallet.dart index da0919570..0ccc6436c 100644 --- a/lib/services/coins/firo/firo_wallet.dart +++ b/lib/services/coins/firo/firo_wallet.dart @@ -3538,11 +3538,21 @@ class FiroWallet extends CoinServiceAPI ), ); } + final txid = txObject["txid"] as String; const subType = isar_models.TransactionSubType.join; + final type = nonWalletAddressFoundInOutputs ? isar_models.TransactionType.outgoing - : isar_models.TransactionType.incoming; + : (await db.isar.lelantusCoins + .where() + .walletIdEqualTo(walletId) + .filter() + .txidEqualTo(txid) + .findFirst()) == + null + ? isar_models.TransactionType.incoming + : isar_models.TransactionType.sentToSelf; final amount = nonWalletAddressFoundInOutputs ? totalOutputValue @@ -3569,7 +3579,7 @@ class FiroWallet extends CoinServiceAPI final tx = isar_models.Transaction( walletId: walletId, - txid: txObject["txid"] as String, + txid: txid, timestamp: txObject["blocktime"] as int? ?? (DateTime.now().millisecondsSinceEpoch ~/ 1000), type: type, From 683364750adfab6c04ae7509a332defb77c761b1 Mon Sep 17 00:00:00 2001 From: ryleedavis Date: Wed, 26 Jul 2023 11:13:48 -0600 Subject: [PATCH 144/169] structure change to add monkey image --- .../sub_widgets/wallet_summary_info.dart | 160 +++++++++++------- 1 file changed, 95 insertions(+), 65 deletions(-) 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 a4716d190..57658498c 100644 --- a/lib/pages/wallet_view/sub_widgets/wallet_summary_info.dart +++ b/lib/pages/wallet_view/sub_widgets/wallet_summary_info.dart @@ -31,6 +31,7 @@ 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/text_styles.dart'; +import 'package:stackwallet/widgets/conditional_parent.dart'; class WalletSummaryInfo extends ConsumerStatefulWidget { const WalletSummaryInfo({ @@ -49,6 +50,8 @@ class WalletSummaryInfo extends ConsumerStatefulWidget { class _WalletSummaryInfoState extends ConsumerState { late StreamSubscription _balanceUpdated; + String receivingAddress = ""; + void showSheet() { showModalBottomSheet( backgroundColor: Colors.transparent, @@ -72,6 +75,17 @@ class _WalletSummaryInfoState extends ConsumerState { } }, ); + + // managerProvider = widget.managerProvider; + WidgetsBinding.instance.addPostFrameCallback((timeStamp) async { + final address = await ref + .read(walletsChangeNotifierProvider) + .getManager(widget.walletId) + .currentReceivingAddress; + setState(() { + receivingAddress = address; + }); + }); super.initState(); } @@ -85,6 +99,8 @@ class _WalletSummaryInfoState extends ConsumerState { Widget build(BuildContext context) { debugPrint("BUILD: $runtimeType"); + bool isMonkey = true; + final externalCalls = ref.watch( prefsChangeNotifierProvider.select((value) => value.externalCalls)); final coin = ref.watch(walletsChangeNotifierProvider @@ -125,84 +141,98 @@ class _WalletSummaryInfoState extends ConsumerState { title = _showAvailable ? "Available balance" : "Full balance"; } - return Row( - children: [ - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - GestureDetector( - onTap: showSheet, - child: Row( - children: [ - Text( - title, - style: STextStyles.subtitle500(context).copyWith( + return ConditionalParent( + condition: isMonkey && Coin == Coin.banano, + builder: (child) => Container( + decoration: BoxDecoration( + image: DecorationImage( + alignment: Alignment.centerRight, + image: NetworkImage( + 'https://monkey.banano.cc/api/v1/monkey/$receivingAddress?format=png&size=512', + ), + ), + ), + child: child, + ), + child: Row( + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + GestureDetector( + onTap: showSheet, + child: Row( + children: [ + Text( + title, + style: STextStyles.subtitle500(context).copyWith( + color: Theme.of(context) + .extension()! + .textFavoriteCard, + ), + ), + const SizedBox( + width: 4, + ), + SvgPicture.asset( + Assets.svg.chevronDown, color: Theme.of(context) .extension()! .textFavoriteCard, + width: 8, + height: 4, ), - ), - const SizedBox( - width: 4, - ), - SvgPicture.asset( - Assets.svg.chevronDown, + ], + ), + ), + const Spacer(), + FittedBox( + fit: BoxFit.scaleDown, + child: SelectableText( + ref.watch(pAmountFormatter(coin)).format(balanceToShow), + style: STextStyles.pageTitleH1(context).copyWith( + fontSize: 24, color: Theme.of(context) .extension()! .textFavoriteCard, - width: 8, - height: 4, ), - ], + ), ), + if (externalCalls) + Text( + "${(priceTuple.item1 * balanceToShow.decimal).toAmount( + fractionDigits: 2, + ).fiatString( + locale: locale, + )} $baseCurrency", + style: STextStyles.subtitle500(context).copyWith( + color: Theme.of(context) + .extension()! + .textFavoriteCard, + ), + ), + ], + ), + ), + Column( + children: [ + SvgPicture.file( + File( + ref.watch(coinIconProvider(coin)), + ), + width: 24, + height: 24, ), const Spacer(), - FittedBox( - fit: BoxFit.scaleDown, - child: SelectableText( - ref.watch(pAmountFormatter(coin)).format(balanceToShow), - style: STextStyles.pageTitleH1(context).copyWith( - fontSize: 24, - color: Theme.of(context) - .extension()! - .textFavoriteCard, - ), - ), + WalletRefreshButton( + walletId: widget.walletId, + initialSyncStatus: widget.initialSyncStatus, ), - if (externalCalls) - Text( - "${(priceTuple.item1 * balanceToShow.decimal).toAmount( - fractionDigits: 2, - ).fiatString( - locale: locale, - )} $baseCurrency", - style: STextStyles.subtitle500(context).copyWith( - color: Theme.of(context) - .extension()! - .textFavoriteCard, - ), - ), ], - ), - ), - Column( - children: [ - SvgPicture.file( - File( - ref.watch(coinIconProvider(coin)), - ), - width: 24, - height: 24, - ), - const Spacer(), - WalletRefreshButton( - walletId: widget.walletId, - initialSyncStatus: widget.initialSyncStatus, - ), - ], - ) - ], + ) + ], + ), ); } } From 6733a367e3461ab40a14ac0a6fb53b22de65701b Mon Sep 17 00:00:00 2001 From: julian Date: Wed, 26 Jul 2023 11:47:49 -0600 Subject: [PATCH 145/169] create mockable wrapper function --- lib/db/isar/main_db.dart | 11 +++++++++++ lib/services/coins/firo/firo_wallet.dart | 22 ++++------------------ 2 files changed, 15 insertions(+), 18 deletions(-) diff --git a/lib/db/isar/main_db.dart b/lib/db/isar/main_db.dart index 93c9052da..c7a593d03 100644 --- a/lib/db/isar/main_db.dart +++ b/lib/db/isar/main_db.dart @@ -512,4 +512,15 @@ class MainDB { isar.writeTxn(() async { await isar.ethContracts.putAll(contracts); }); + + // ========== Lelantus ======================================================= + + Future getHighestUsedMintIndex({required String walletId}) async { + return await isar.lelantusCoins + .where() + .walletIdEqualTo(walletId) + .sortByMintIndexDesc() + .mintIndexProperty() + .findFirst(); + } } diff --git a/lib/services/coins/firo/firo_wallet.dart b/lib/services/coins/firo/firo_wallet.dart index 0ccc6436c..079b96ff4 100644 --- a/lib/services/coins/firo/firo_wallet.dart +++ b/lib/services/coins/firo/firo_wallet.dart @@ -2512,12 +2512,7 @@ class FiroWallet extends CoinServiceAPI Future>> createMintsFromAmount(int total) async { int tmpTotal = total; int counter = 0; - final lastUsedIndex = await db.isar.lelantusCoins - .where() - .walletIdEqualTo(walletId) - .sortByMintIndexDesc() - .mintIndexProperty() - .findFirst(); + final lastUsedIndex = await db.getHighestUsedMintIndex(walletId: walletId); final nextFreeMintIndex = (lastUsedIndex ?? 0) + 1; final root = await Bip32Utils.getBip32Root( @@ -2908,12 +2903,8 @@ class FiroWallet extends CoinServiceAPI level: LogLevel.Info); if (txid == transactionInfo['txid']) { - final lastUsedIndex = await db.isar.lelantusCoins - .where() - .walletIdEqualTo(walletId) - .sortByMintIndexDesc() - .mintIndexProperty() - .findFirst(); + final lastUsedIndex = + await db.getHighestUsedMintIndex(walletId: walletId); final nextFreeMintIndex = (lastUsedIndex ?? 0) + 1; if (transactionInfo['spendCoinIndexes'] != null) { @@ -4577,12 +4568,7 @@ class FiroWallet extends CoinServiceAPI int spendAmount, String address, bool subtractFeeFromAmount) async { final _mnemonic = await mnemonicString; final _mnemonicPassphrase = await mnemonicPassphrase; - final lastUsedIndex = await db.isar.lelantusCoins - .where() - .walletIdEqualTo(walletId) - .sortByMintIndexDesc() - .mintIndexProperty() - .findFirst(); + final lastUsedIndex = await db.getHighestUsedMintIndex(walletId: walletId); final nextFreeMintIndex = (lastUsedIndex ?? 0) + 1; final lelantusEntry = await _getLelantusEntry(); final anonymitySets = await fetchAnonymitySets(); From d169ea0b109b87c289d77fb996daf73d728be20a Mon Sep 17 00:00:00 2001 From: sneurlax Date: Wed, 26 Jul 2023 12:53:19 -0500 Subject: [PATCH 146/169] fix firo test --- test/services/coins/firo/firo_wallet_test.mocks.dart | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/test/services/coins/firo/firo_wallet_test.mocks.dart b/test/services/coins/firo/firo_wallet_test.mocks.dart index 95d1750e9..dc9bb266f 100644 --- a/test/services/coins/firo/firo_wallet_test.mocks.dart +++ b/test/services/coins/firo/firo_wallet_test.mocks.dart @@ -856,7 +856,7 @@ class MockMainDB extends _i1.Mock implements _i9.MainDB { returnValueForMissingStub: _i5.Future.value(), ) as _i5.Future); @override - _i5.Future updateUTXOs( + _i5.Future updateUTXOs( String? walletId, List<_i12.UTXO>? utxos, ) => @@ -868,9 +868,9 @@ class MockMainDB extends _i1.Mock implements _i9.MainDB { utxos, ], ), - returnValue: _i5.Future.value(), - returnValueForMissingStub: _i5.Future.value(), - ) as _i5.Future); + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); @override _i5.Stream<_i12.UTXO?> watchUTXO({ required int? id, From 2eb10e249f1ee4d9543aea4e583b4f1e0c0bebff Mon Sep 17 00:00:00 2001 From: ryleedavis Date: Wed, 26 Jul 2023 11:54:10 -0600 Subject: [PATCH 147/169] add monkey image to hive and display on wallet card --- .../sub_widgets/wallet_summary_info.dart | 32 ++++++++++++------- lib/services/coins/banano/banano_wallet.dart | 16 ++++++++++ 2 files changed, 37 insertions(+), 11 deletions(-) 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 57658498c..0c4e6fb08 100644 --- a/lib/pages/wallet_view/sub_widgets/wallet_summary_info.dart +++ b/lib/pages/wallet_view/sub_widgets/wallet_summary_info.dart @@ -10,6 +10,7 @@ import 'dart:async'; import 'dart:io'; +import 'dart:typed_data'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; @@ -19,6 +20,7 @@ import 'package:stackwallet/pages/wallet_view/sub_widgets/wallet_refresh_button. 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/banano/banano_wallet.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'; @@ -101,10 +103,12 @@ class _WalletSummaryInfoState extends ConsumerState { bool isMonkey = true; + final manager = ref.watch(walletsChangeNotifierProvider + .select((value) => value.getManager(widget.walletId))); + final externalCalls = ref.watch( prefsChangeNotifierProvider.select((value) => value.externalCalls)); - final coin = ref.watch(walletsChangeNotifierProvider - .select((value) => value.getManager(widget.walletId).coin)); + final coin = manager.coin; final balance = ref.watch(walletsChangeNotifierProvider .select((value) => value.getManager(widget.walletId).balance)); @@ -141,18 +145,24 @@ class _WalletSummaryInfoState extends ConsumerState { title = _showAvailable ? "Available balance" : "Full balance"; } + List? imageBytes; + + if (coin == Coin.banano) { + imageBytes = (manager.wallet as BananoWallet).getMonkeyImageBytes(); + } + return ConditionalParent( - condition: isMonkey && Coin == Coin.banano, - builder: (child) => Container( - decoration: BoxDecoration( - image: DecorationImage( - alignment: Alignment.centerRight, - image: NetworkImage( - 'https://monkey.banano.cc/api/v1/monkey/$receivingAddress?format=png&size=512', + condition: imageBytes != null, + builder: (child) => Stack( + children: [ + Positioned.fill( + left: 150.0, + child: SvgPicture.memory( + Uint8List.fromList(imageBytes!), ), ), - ), - child: child, + child, + ], ), child: Row( children: [ diff --git a/lib/services/coins/banano/banano_wallet.dart b/lib/services/coins/banano/banano_wallet.dart index 8511f5599..be5d20889 100644 --- a/lib/services/coins/banano/banano_wallet.dart +++ b/lib/services/coins/banano/banano_wallet.dart @@ -4,6 +4,7 @@ import 'dart:convert'; import 'package:http/http.dart' as http; import 'package:isar/isar.dart'; import 'package:nanodart/nanodart.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'; @@ -925,4 +926,19 @@ class BananoWallet extends CoinServiceAPI ); await updateCachedChainHeight(height ?? 0); } + + Future updateMonkeyImageBytes(List bytes) async { + await DB.instance.put( + boxName: _walletId, + key: "monkeyImageBytesKey", + value: bytes, + ); + } + + List? getMonkeyImageBytes() { + return DB.instance.get( + boxName: _walletId, + key: "monkeyImageBytesKey", + ) as List?; + } } From dbcb567d6bcdff7c7905023b2479d9955d6440d4 Mon Sep 17 00:00:00 2001 From: julian Date: Wed, 26 Jul 2023 11:54:46 -0600 Subject: [PATCH 148/169] fix tests --- .../services/coins/firo/firo_wallet_test.dart | 37 +++++++++++-------- .../coins/firo/firo_wallet_test.mocks.dart | 10 +++++ .../transaction_card_test.mocks.dart | 10 +++++ 3 files changed, 42 insertions(+), 15 deletions(-) diff --git a/test/services/coins/firo/firo_wallet_test.dart b/test/services/coins/firo/firo_wallet_test.dart index fcd7bd50e..05b4aeeff 100644 --- a/test/services/coins/firo/firo_wallet_test.dart +++ b/test/services/coins/firo/firo_wallet_test.dart @@ -11,9 +11,7 @@ import 'package:mockito/mockito.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/isar/models/blockchain_data/transaction.dart'; -import 'package:stackwallet/models/isar/models/blockchain_data/utxo.dart'; -import 'package:stackwallet/models/lelantus_coin.dart'; +import 'package:stackwallet/models/isar/models/isar_models.dart'; import 'package:stackwallet/models/lelantus_fee_data.dart'; import 'package:stackwallet/models/paymint/transactions_model.dart' as old; import 'package:stackwallet/services/coins/firo/firo_wallet.dart'; @@ -114,10 +112,7 @@ void main() { final result = await staticProcessRestore(txData, message, currentHeight); expect(result, isA>()); - expect(result["mintIndex"], 8); - expect(result["jindex"], [2, 4, 6]); - expect( - result["_lelantus_coins"], isA>>()); + expect(result["_lelantus_coins"], isA>()); expect(result["newTxMap"], isA>()); }); @@ -532,18 +527,10 @@ void main() { group("FiroWallet service class functions that depend on shared storage", () { const testWalletId = "testWalletID"; const testWalletName = "Test Wallet"; - bool hiveAdaptersRegistered = false; setUp(() async { await setUpTestHive(); - if (!hiveAdaptersRegistered) { - hiveAdaptersRegistered = true; - - // Registering Lelantus Model Adapters - Hive.registerAdapter(LelantusCoinAdapter()); - } - final wallets = await Hive.openBox('wallets'); await wallets.put('currentWalletName', testWalletName); }); @@ -1204,13 +1191,33 @@ void main() { txHash: BuildMintTxTestParams.utxoInfo["txid"] as String, coin: Coin.firo, )).thenAnswer((_) async => BuildMintTxTestParams.cachedClientResponse); + when(cachedClient.getAnonymitySet( + groupId: "1", + coin: Coin.firo, + )).thenAnswer( + (_) async => GetAnonymitySetSampleData.data, + ); + when(cachedClient.getAnonymitySet( + groupId: "2", + coin: Coin.firo, + )).thenAnswer( + (_) async => GetAnonymitySetSampleData.data, + ); when(client.getBlockHeadTip()).thenAnswer( (_) async => {"height": 455873, "hex": "this value not used here"}); + when(client.getLatestCoinId()).thenAnswer((_) async => 2); when(mainDB.getAddress("${testWalletId}buildMintTransaction", any)) .thenAnswer((realInvocation) async => null); + when(mainDB.getHighestUsedMintIndex( + walletId: "${testWalletId}submitHexToNetwork")) + .thenAnswer((_) async => null); + when(mainDB.getHighestUsedMintIndex( + walletId: "testWalletIDbuildMintTransaction")) + .thenAnswer((_) async => null); + final firo = FiroWallet( walletName: testWalletName, walletId: "${testWalletId}buildMintTransaction", diff --git a/test/services/coins/firo/firo_wallet_test.mocks.dart b/test/services/coins/firo/firo_wallet_test.mocks.dart index 95d1750e9..2365f83b1 100644 --- a/test/services/coins/firo/firo_wallet_test.mocks.dart +++ b/test/services/coins/firo/firo_wallet_test.mocks.dart @@ -1151,4 +1151,14 @@ class MockMainDB extends _i1.Mock implements _i9.MainDB { returnValue: _i5.Future.value(), returnValueForMissingStub: _i5.Future.value(), ) as _i5.Future); + @override + _i5.Future getHighestUsedMintIndex({required String? walletId}) => + (super.noSuchMethod( + Invocation.method( + #getHighestUsedMintIndex, + [], + {#walletId: walletId}, + ), + returnValue: _i5.Future.value(), + ) as _i5.Future); } diff --git a/test/widget_tests/transaction_card_test.mocks.dart b/test/widget_tests/transaction_card_test.mocks.dart index 74fa6e89e..35e8a2560 100644 --- a/test/widget_tests/transaction_card_test.mocks.dart +++ b/test/widget_tests/transaction_card_test.mocks.dart @@ -3496,4 +3496,14 @@ class MockMainDB extends _i1.Mock implements _i14.MainDB { returnValue: _i19.Future.value(), returnValueForMissingStub: _i19.Future.value(), ) as _i19.Future); + @override + _i19.Future getHighestUsedMintIndex({required String? walletId}) => + (super.noSuchMethod( + Invocation.method( + #getHighestUsedMintIndex, + [], + {#walletId: walletId}, + ), + returnValue: _i19.Future.value(), + ) as _i19.Future); } From ba4e83968d4282255a08507014fe7fa9e9e5645c Mon Sep 17 00:00:00 2001 From: sneurlax Date: Wed, 26 Jul 2023 13:02:56 -0500 Subject: [PATCH 149/169] show more button on desktop wallet home page if wallet ordinalsinterface --- .../wallet_view/sub_widgets/desktop_wallet_features.dart | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) 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 f116e6656..44b1853db 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 @@ -19,10 +19,10 @@ import 'package:stackwallet/notifications/show_flush_bar.dart'; import 'package:stackwallet/pages/paynym/paynym_claim_view.dart'; import 'package:stackwallet/pages/paynym/paynym_home_view.dart'; import 'package:stackwallet/pages_desktop_specific/coin_control/desktop_coin_control_view.dart'; -import 'package:stackwallet/pages_desktop_specific/ordinals/desktop_ordinals_view.dart'; import 'package:stackwallet/pages_desktop_specific/desktop_menu.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/more_features/more_features_dialog.dart'; +import 'package:stackwallet/pages_desktop_specific/ordinals/desktop_ordinals_view.dart'; import 'package:stackwallet/providers/desktop/current_desktop_menu_item.dart'; import 'package:stackwallet/providers/global/paynym_api_provider.dart'; import 'package:stackwallet/providers/providers.dart'; @@ -341,7 +341,8 @@ class _DesktopWalletFeaturesState extends ConsumerState { )) || manager.coin == Coin.firo || manager.coin == Coin.firoTestNet || - manager.hasWhirlpoolSupport; + manager.hasWhirlpoolSupport || + manager.hasOrdinalsSupport; return Row( children: [ From 032061fa1912e536fc72dae0691f9bcd890d8415 Mon Sep 17 00:00:00 2001 From: ryleedavis Date: Wed, 26 Jul 2023 12:03:01 -0600 Subject: [PATCH 150/169] changed generate monkey structure --- lib/pages/monkey/monkey_loaded_view.dart | 540 ++++++++++++----------- lib/pages/monkey/monkey_view.dart | 296 ++++++++++--- lib/route_generator.dart | 29 +- 3 files changed, 520 insertions(+), 345 deletions(-) diff --git a/lib/pages/monkey/monkey_loaded_view.dart b/lib/pages/monkey/monkey_loaded_view.dart index 32570e6a7..93c28b39a 100644 --- a/lib/pages/monkey/monkey_loaded_view.dart +++ b/lib/pages/monkey/monkey_loaded_view.dart @@ -1,265 +1,275 @@ -import 'dart:io'; - -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:flutter_svg/svg.dart'; -import 'package:http/http.dart' as http; -import 'package:path_provider/path_provider.dart'; -import 'package:permission_handler/permission_handler.dart'; -import 'package:stackwallet/pages/wallet_view/wallet_view.dart'; -import 'package:stackwallet/providers/global/wallets_provider.dart'; -import 'package:stackwallet/services/coins/manager.dart'; -import 'package:stackwallet/themes/stack_colors.dart'; -import 'package:stackwallet/utilities/assets.dart'; -import 'package:stackwallet/utilities/enums/coin_enum.dart'; -import 'package:stackwallet/utilities/text_styles.dart'; -import 'package:stackwallet/widgets/background.dart'; -import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; -import 'package:stackwallet/widgets/desktop/secondary_button.dart'; - -class MonkeyLoadedView extends ConsumerStatefulWidget { - const MonkeyLoadedView({ - Key? key, - required this.walletId, - required this.managerProvider, - }) : super(key: key); - - static const String routeName = "/hasMonkey"; - static const double navBarHeight = 65.0; - - final String walletId; - final ChangeNotifierProvider managerProvider; - - @override - ConsumerState createState() => _MonkeyLoadedViewState(); -} - -class _MonkeyLoadedViewState extends ConsumerState { - late final String walletId; - late final ChangeNotifierProvider managerProvider; - - String receivingAddress = ""; - - void getMonkeySVG(String address) async { - if (address.isEmpty) { - //address shouldn't be empty - return; - } - - final http.Response response = await http - .get(Uri.parse('https://monkey.banano.cc/api/v1/monkey/$address')); - - if (response.statusCode == 200) { - final decodedResponse = response.bodyBytes; - Directory directory = await getApplicationDocumentsDirectory(); - late Directory sampleFolder; - - if (Platform.isAndroid) { - directory = Directory("/storage/emulated/0/"); - sampleFolder = Directory('${directory!.path}Documents'); - } else if (Platform.isIOS) { - sampleFolder = Directory(directory!.path); - } else if (Platform.isLinux) { - sampleFolder = Directory('${directory!.path}Documents'); - } else if (Platform.isWindows) { - sampleFolder = Directory('${directory!.path}Documents'); - } else if (Platform.isMacOS) { - sampleFolder = Directory('${directory!.path}Documents'); - } - - try { - if (!sampleFolder.existsSync()) { - sampleFolder.createSync(recursive: true); - } - } catch (e, s) { - // todo: come back to this - debugPrint("$e $s"); - } - - final docPath = sampleFolder.path; - final filePath = "$docPath/monkey.svg"; - - File imgFile = File(filePath); - await imgFile.writeAsBytes(decodedResponse); - } else { - throw Exception("Failed to get MonKey"); - } - } - - void getMonkeyPNG(String address) async { - if (address.isEmpty) { - //address shouldn't be empty - return; - } - - final http.Response response = await http.get(Uri.parse( - 'https://monkey.banano.cc/api/v1/monkey/${address}?format=png&size=512&background=false')); - - if (response.statusCode == 200) { - if (Platform.isAndroid) { - await Permission.storage.request(); - } - - final decodedResponse = response.bodyBytes; - Directory directory = await getApplicationDocumentsDirectory(); - late Directory sampleFolder; - - if (Platform.isAndroid) { - directory = Directory("/storage/emulated/0/"); - sampleFolder = Directory('${directory!.path}Documents'); - } else if (Platform.isIOS) { - sampleFolder = Directory(directory!.path); - } else if (Platform.isLinux) { - sampleFolder = Directory('${directory!.path}Documents'); - } else if (Platform.isWindows) { - sampleFolder = Directory('${directory!.path}Documents'); - } else if (Platform.isMacOS) { - sampleFolder = Directory('${directory!.path}Documents'); - } - - try { - if (!sampleFolder.existsSync()) { - sampleFolder.createSync(recursive: true); - } - } catch (e, s) { - // todo: come back to this - debugPrint("$e $s"); - } - - final docPath = sampleFolder.path; - final filePath = "$docPath/monkey.png"; - - File imgFile = File(filePath); - await imgFile.writeAsBytes(decodedResponse); - } else { - throw Exception("Failed to get MonKey"); - } - } - - @override - void initState() { - walletId = widget.walletId; - managerProvider = widget.managerProvider; - - WidgetsBinding.instance.addPostFrameCallback((timeStamp) async { - final address = await ref - .read(walletsChangeNotifierProvider) - .getManager(walletId) - .currentReceivingAddress; - setState(() { - receivingAddress = address; - }); - }); - - super.initState(); - } - - @override - void dispose() { - super.dispose(); - } - - @override - Widget build(BuildContext context) { - final Coin coin = ref.watch(managerProvider.select((value) => value.coin)); - - return Background( - child: Stack( - children: [ - Scaffold( - appBar: AppBar( - leading: AppBarBackButton( - onPressed: () { - Navigator.of(context).popUntil( - ModalRoute.withName(WalletView.routeName), - ); - }, - ), - title: Text( - "MonKey", - style: STextStyles.navBarTitle(context), - ), - actions: [ - AspectRatio( - aspectRatio: 1, - child: AppBarIconButton( - icon: SvgPicture.asset(Assets.svg.circleQuestion), - onPressed: () { - showDialog( - context: context, - useSafeArea: false, - barrierDismissible: true, - builder: (context) { - return Dialog( - child: Material( - borderRadius: BorderRadius.circular( - 20, - ), - child: Container( - height: 200, - decoration: BoxDecoration( - color: Theme.of(context) - .extension()! - .popupBG, - borderRadius: BorderRadius.circular( - 20, - ), - ), - child: Column( - children: [ - Center( - child: Text( - "Help", - style: STextStyles.pageTitleH2( - context), - ), - ) - ], - ), - ), - ), - ); - }); - }), - ) - ], - ), - body: Column( - children: [ - const Spacer( - flex: 1, - ), - Image.network( - 'https://monkey.banano.cc/api/v1/monkey/$receivingAddress?format=png&size=512', - ), - const Spacer( - flex: 1, - ), - Padding( - padding: const EdgeInsets.all(16.0), - child: Column( - children: [ - SecondaryButton( - label: "Download as SVG", - onPressed: () async { - getMonkeySVG(receivingAddress); - }, - ), - const SizedBox(height: 12), - SecondaryButton( - label: "Download as PNG", - onPressed: () { - getMonkeyPNG(receivingAddress); - }, - ), - ], - ), - ), - ], - ), - ), - ], - ), - ); - } -} +// import 'dart:io'; +// import 'dart:typed_data'; +// +// import 'package:flutter/material.dart'; +// import 'package:flutter_riverpod/flutter_riverpod.dart'; +// import 'package:flutter_svg/svg.dart'; +// import 'package:http/http.dart' as http; +// import 'package:path_provider/path_provider.dart'; +// import 'package:permission_handler/permission_handler.dart'; +// import 'package:stackwallet/pages/wallet_view/wallet_view.dart'; +// import 'package:stackwallet/providers/global/wallets_provider.dart'; +// import 'package:stackwallet/services/coins/banano/banano_wallet.dart'; +// import 'package:stackwallet/services/coins/manager.dart'; +// import 'package:stackwallet/themes/stack_colors.dart'; +// import 'package:stackwallet/utilities/assets.dart'; +// import 'package:stackwallet/utilities/enums/coin_enum.dart'; +// import 'package:stackwallet/utilities/text_styles.dart'; +// import 'package:stackwallet/widgets/background.dart'; +// import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; +// import 'package:stackwallet/widgets/desktop/secondary_button.dart'; +// +// class MonkeyLoadedView extends ConsumerStatefulWidget { +// const MonkeyLoadedView({ +// Key? key, +// required this.walletId, +// required this.managerProvider, +// }) : super(key: key); +// +// static const String routeName = "/hasMonkey"; +// static const double navBarHeight = 65.0; +// +// final String walletId; +// final ChangeNotifierProvider managerProvider; +// +// @override +// ConsumerState createState() => _MonkeyLoadedViewState(); +// } +// +// class _MonkeyLoadedViewState extends ConsumerState { +// late final String walletId; +// late final ChangeNotifierProvider managerProvider; +// +// String receivingAddress = ""; +// +// void getMonkeySVG(String address) async { +// if (address.isEmpty) { +// //address shouldn't be empty +// return; +// } +// +// final http.Response response = await http +// .get(Uri.parse('https://monkey.banano.cc/api/v1/monkey/$address')); +// +// if (response.statusCode == 200) { +// final decodedResponse = response.bodyBytes; +// Directory directory = await getApplicationDocumentsDirectory(); +// late Directory sampleFolder; +// +// if (Platform.isAndroid) { +// directory = Directory("/storage/emulated/0/"); +// sampleFolder = Directory('${directory!.path}Documents'); +// } else if (Platform.isIOS) { +// sampleFolder = Directory(directory!.path); +// } else if (Platform.isLinux) { +// sampleFolder = Directory('${directory!.path}Documents'); +// } else if (Platform.isWindows) { +// sampleFolder = Directory('${directory!.path}Documents'); +// } else if (Platform.isMacOS) { +// sampleFolder = Directory('${directory!.path}Documents'); +// } +// +// try { +// if (!sampleFolder.existsSync()) { +// sampleFolder.createSync(recursive: true); +// } +// } catch (e, s) { +// // todo: come back to this +// debugPrint("$e $s"); +// } +// +// final docPath = sampleFolder.path; +// final filePath = "$docPath/monkey.svg"; +// +// File imgFile = File(filePath); +// await imgFile.writeAsBytes(decodedResponse); +// } else { +// throw Exception("Failed to get MonKey"); +// } +// } +// +// void getMonkeyPNG(String address) async { +// if (address.isEmpty) { +// //address shouldn't be empty +// return; +// } +// +// final http.Response response = await http.get(Uri.parse( +// 'https://monkey.banano.cc/api/v1/monkey/${address}?format=png&size=512&background=false')); +// +// if (response.statusCode == 200) { +// if (Platform.isAndroid) { +// await Permission.storage.request(); +// } +// +// final decodedResponse = response.bodyBytes; +// Directory directory = await getApplicationDocumentsDirectory(); +// late Directory sampleFolder; +// +// if (Platform.isAndroid) { +// directory = Directory("/storage/emulated/0/"); +// sampleFolder = Directory('${directory!.path}Documents'); +// } else if (Platform.isIOS) { +// sampleFolder = Directory(directory!.path); +// } else if (Platform.isLinux) { +// sampleFolder = Directory('${directory!.path}Documents'); +// } else if (Platform.isWindows) { +// sampleFolder = Directory('${directory!.path}Documents'); +// } else if (Platform.isMacOS) { +// sampleFolder = Directory('${directory!.path}Documents'); +// } +// +// try { +// if (!sampleFolder.existsSync()) { +// sampleFolder.createSync(recursive: true); +// } +// } catch (e, s) { +// // todo: come back to this +// debugPrint("$e $s"); +// } +// +// final docPath = sampleFolder.path; +// final filePath = "$docPath/monkey.png"; +// +// File imgFile = File(filePath); +// await imgFile.writeAsBytes(decodedResponse); +// } else { +// throw Exception("Failed to get MonKey"); +// } +// } +// +// @override +// void initState() { +// walletId = widget.walletId; +// managerProvider = widget.managerProvider; +// +// WidgetsBinding.instance.addPostFrameCallback((timeStamp) async { +// final address = await ref +// .read(walletsChangeNotifierProvider) +// .getManager(walletId) +// .currentReceivingAddress; +// setState(() { +// receivingAddress = address; +// }); +// }); +// +// super.initState(); +// } +// +// @override +// void dispose() { +// super.dispose(); +// } +// +// @override +// Widget build(BuildContext context) { +// final Coin coin = ref.watch(managerProvider.select((value) => value.coin)); +// final manager = ref.watch(walletsChangeNotifierProvider +// .select((value) => value.getManager(widget.walletId))); +// +// List? imageBytes; +// imageBytes = (manager.wallet as BananoWallet).getMonkeyImageBytes(); +// +// return Background( +// child: Stack( +// children: [ +// Scaffold( +// appBar: AppBar( +// leading: AppBarBackButton( +// onPressed: () { +// Navigator.of(context).popUntil( +// ModalRoute.withName(WalletView.routeName), +// ); +// }, +// ), +// title: Text( +// "MonKey", +// style: STextStyles.navBarTitle(context), +// ), +// actions: [ +// AspectRatio( +// aspectRatio: 1, +// child: AppBarIconButton( +// icon: SvgPicture.asset(Assets.svg.circleQuestion), +// onPressed: () { +// showDialog( +// context: context, +// useSafeArea: false, +// barrierDismissible: true, +// builder: (context) { +// return Dialog( +// child: Material( +// borderRadius: BorderRadius.circular( +// 20, +// ), +// child: Container( +// height: 200, +// decoration: BoxDecoration( +// color: Theme.of(context) +// .extension()! +// .popupBG, +// borderRadius: BorderRadius.circular( +// 20, +// ), +// ), +// child: Column( +// children: [ +// Center( +// child: Text( +// "Help", +// style: STextStyles.pageTitleH2( +// context), +// ), +// ) +// ], +// ), +// ), +// ), +// ); +// }); +// }), +// ) +// ], +// ), +// body: Column( +// children: [ +// const Spacer( +// flex: 1, +// ), +// if (imageBytes != null) +// Container( +// child: SvgPicture.memory(Uint8List.fromList(imageBytes!)), +// width: 300, +// height: 300, +// ), +// const Spacer( +// flex: 1, +// ), +// Padding( +// padding: const EdgeInsets.all(16.0), +// child: Column( +// children: [ +// SecondaryButton( +// label: "Download as SVG", +// onPressed: () async { +// getMonkeySVG(receivingAddress); +// }, +// ), +// const SizedBox(height: 12), +// SecondaryButton( +// label: "Download as PNG", +// onPressed: () { +// getMonkeyPNG(receivingAddress); +// }, +// ), +// ], +// ), +// ), +// ], +// ), +// ), +// ], +// ), +// ); +// } +// } diff --git a/lib/pages/monkey/monkey_view.dart b/lib/pages/monkey/monkey_view.dart index 17758b9f7..b631b0703 100644 --- a/lib/pages/monkey/monkey_view.dart +++ b/lib/pages/monkey/monkey_view.dart @@ -1,11 +1,16 @@ import 'dart:io'; +import 'dart:typed_data'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/svg.dart'; -import 'package:stackwallet/pages/monkey/monkey_loaded_view.dart'; +import 'package:http/http.dart' as http; +import 'package:path_provider/path_provider.dart'; +import 'package:permission_handler/permission_handler.dart'; import 'package:stackwallet/pages/monkey/sub_widgets/fetch_monkey_dialog.dart'; +import 'package:stackwallet/pages/wallet_view/wallet_view.dart'; import 'package:stackwallet/providers/global/wallets_provider.dart'; +import 'package:stackwallet/services/coins/banano/banano_wallet.dart'; import 'package:stackwallet/services/coins/manager.dart'; import 'package:stackwallet/themes/coin_icon_provider.dart'; import 'package:stackwallet/themes/stack_colors.dart'; @@ -13,9 +18,10 @@ import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:stackwallet/utilities/text_styles.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/primary_button.dart'; -import 'package:tuple/tuple.dart'; +import 'package:stackwallet/widgets/desktop/secondary_button.dart'; class MonkeyView extends ConsumerStatefulWidget { const MonkeyView({ @@ -40,6 +46,123 @@ class _MonkeyViewState extends ConsumerState { String receivingAddress = ""; + void getMonkeyImage(String address) async { + if (address.isEmpty) { + //address shouldn't be empty + return; + } + + final manager = ref.watch(walletsChangeNotifierProvider + .select((value) => value.getManager(walletId))); + + final http.Response response = await http + .get(Uri.parse('https://monkey.banano.cc/api/v1/monkey/$address')); + + if (response.statusCode == 200) { + final decodedResponse = response.bodyBytes; + await (manager.wallet as BananoWallet) + .updateMonkeyImageBytes(decodedResponse); + } else { + throw Exception("Failed to get MonKey"); + } + } + + void getMonkeySVG(String address) async { + if (address.isEmpty) { + //address shouldn't be empty + return; + } + + final http.Response response = await http + .get(Uri.parse('https://monkey.banano.cc/api/v1/monkey/$address')); + + if (response.statusCode == 200) { + final decodedResponse = response.bodyBytes; + Directory directory = await getApplicationDocumentsDirectory(); + late Directory sampleFolder; + + if (Platform.isAndroid) { + directory = Directory("/storage/emulated/0/"); + sampleFolder = Directory('${directory!.path}Documents'); + } else if (Platform.isIOS) { + sampleFolder = Directory(directory!.path); + } else if (Platform.isLinux) { + sampleFolder = Directory('${directory!.path}Documents'); + } else if (Platform.isWindows) { + sampleFolder = Directory('${directory!.path}Documents'); + } else if (Platform.isMacOS) { + sampleFolder = Directory('${directory!.path}Documents'); + } + + try { + if (!sampleFolder.existsSync()) { + sampleFolder.createSync(recursive: true); + } + } catch (e, s) { + // todo: come back to this + debugPrint("$e $s"); + } + + final docPath = sampleFolder.path; + final filePath = "$docPath/monkey.svg"; + + File imgFile = File(filePath); + await imgFile.writeAsBytes(decodedResponse); + } else { + throw Exception("Failed to get MonKey"); + } + } + + void getMonkeyPNG(String address) async { + if (address.isEmpty) { + //address shouldn't be empty + return; + } + + final http.Response response = await http.get(Uri.parse( + 'https://monkey.banano.cc/api/v1/monkey/${address}?format=png&size=512&background=false')); + + if (response.statusCode == 200) { + if (Platform.isAndroid) { + await Permission.storage.request(); + } + + final decodedResponse = response.bodyBytes; + Directory directory = await getApplicationDocumentsDirectory(); + late Directory sampleFolder; + + if (Platform.isAndroid) { + directory = Directory("/storage/emulated/0/"); + sampleFolder = Directory('${directory!.path}Documents'); + } else if (Platform.isIOS) { + sampleFolder = Directory(directory!.path); + } else if (Platform.isLinux) { + sampleFolder = Directory('${directory!.path}Documents'); + } else if (Platform.isWindows) { + sampleFolder = Directory('${directory!.path}Documents'); + } else if (Platform.isMacOS) { + sampleFolder = Directory('${directory!.path}Documents'); + } + + try { + if (!sampleFolder.existsSync()) { + sampleFolder.createSync(recursive: true); + } + } catch (e, s) { + // todo: come back to this + debugPrint("$e $s"); + } + + final docPath = sampleFolder.path; + final filePath = "$docPath/monkey.png"; + + File imgFile = File(filePath); + await imgFile.writeAsBytes(decodedResponse); + } else { + throw Exception("Failed to get MonKey"); + } + } + @override void initState() { walletId = widget.walletId; @@ -66,6 +189,11 @@ class _MonkeyViewState extends ConsumerState { @override Widget build(BuildContext context) { final Coin coin = ref.watch(managerProvider.select((value) => value.coin)); + final manager = ref.watch(walletsChangeNotifierProvider + .select((value) => value.getManager(widget.walletId))); + + List? imageBytes; + imageBytes = (manager.wallet as BananoWallet).getMonkeyImageBytes(); return Background( child: Stack( @@ -123,76 +251,114 @@ class _MonkeyViewState extends ConsumerState { ); }); }), - ) + ), ], ), - body: Column( - children: [ - const Spacer( - flex: 4, - ), - Center( - child: Column( - children: [ - Opacity( - opacity: 0.2, - child: SvgPicture.file( - File( - ref.watch(coinIconProvider(coin)), + body: ConditionalParent( + condition: imageBytes != null, + builder: (child) => Column( + children: [ + const Spacer( + flex: 1, + ), + if (imageBytes != null) + Container( + child: SvgPicture.memory(Uint8List.fromList(imageBytes!)), + width: 300, + height: 300, + ), + const Spacer( + flex: 1, + ), + Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + children: [ + SecondaryButton( + label: "Download as SVG", + onPressed: () async { + getMonkeySVG(receivingAddress); + }, + ), + const SizedBox(height: 12), + SecondaryButton( + label: "Download as PNG", + onPressed: () { + getMonkeyPNG(receivingAddress); + }, + ), + ], + ), + ), + // child, + ], + ), + child: Column( + children: [ + const Spacer( + flex: 4, + ), + Center( + child: Column( + children: [ + Opacity( + opacity: 0.2, + child: SvgPicture.file( + File( + ref.watch(coinIconProvider(coin)), + ), + width: 200, + height: 200, ), - width: 200, - height: 200, ), - ), - const SizedBox( - height: 40, - ), - Text( - "You do not have a MonKey yet. \nFetch yours now!", - style: STextStyles.smallMed14(context).copyWith( - color: Theme.of(context) - .extension()! - .textDark3, + const SizedBox( + height: 40, ), - textAlign: TextAlign.center, - ), - ], + Text( + "You do not have a MonKey yet. \nFetch yours now!", + style: STextStyles.smallMed14(context).copyWith( + color: Theme.of(context) + .extension()! + .textDark3, + ), + textAlign: TextAlign.center, + ), + ], + ), ), - ), - const Spacer( - flex: 6, - ), - Padding( - padding: const EdgeInsets.all(16.0), - child: PrimaryButton( - label: "Fetch MonKey", - onPressed: () async { - showDialog( - context: context, - useSafeArea: false, - barrierDismissible: false, - builder: (context) { - return FetchMonkeyDialog( - onCancel: () async { - Navigator.of(context).pop(); - }, - ); - }, - ); - - await Future.delayed(const Duration(seconds: 2)); - - Navigator.of(context).pushNamed( - MonkeyLoadedView.routeName, - arguments: Tuple2( - widget.walletId, - widget.managerProvider, - ), - ); - }, + const Spacer( + flex: 6, ), - ), - ], + Padding( + padding: const EdgeInsets.all(16.0), + child: PrimaryButton( + label: "Fetch MonKey", + onPressed: () async { + getMonkeyImage(receivingAddress); + + showDialog( + context: context, + useSafeArea: false, + barrierDismissible: false, + builder: (context) { + return FetchMonkeyDialog( + onCancel: () async { + Navigator.of(context).pop(); + }, + ); + }, + ); + + await Future.delayed(const Duration(seconds: 2)); + + Navigator.of(context).popUntil( + ModalRoute.withName(WalletView.routeName), + ); + }, + ), + ), + ], + ), ), ), ], diff --git a/lib/route_generator.dart b/lib/route_generator.dart index b02638082..64c2c86d4 100644 --- a/lib/route_generator.dart +++ b/lib/route_generator.dart @@ -56,7 +56,6 @@ import 'package:stackwallet/pages/generic/single_field_edit_view.dart'; import 'package:stackwallet/pages/home_view/home_view.dart'; import 'package:stackwallet/pages/intro_view.dart'; import 'package:stackwallet/pages/manage_favorites_view/manage_favorites_view.dart'; -import 'package:stackwallet/pages/monkey/monkey_loaded_view.dart'; import 'package:stackwallet/pages/monkey/monkey_view.dart'; import 'package:stackwallet/pages/notification_views/notifications_view.dart'; import 'package:stackwallet/pages/paynym/add_new_paynym_follow_view.dart'; @@ -392,20 +391,20 @@ class RouteGenerator { } return _routeError("${settings.name} invalid args: ${args.toString()}"); - case MonkeyLoadedView.routeName: - if (args is Tuple2>) { - return getRoute( - shouldUseMaterialRoute: useMaterialPageRoute, - builder: (_) => MonkeyLoadedView( - walletId: args.item1, - managerProvider: args.item2, - ), - settings: RouteSettings( - name: settings.name, - ), - ); - } - return _routeError("${settings.name} invalid args: ${args.toString()}"); + // case MonkeyLoadedView.routeName: + // if (args is Tuple2>) { + // return getRoute( + // shouldUseMaterialRoute: useMaterialPageRoute, + // builder: (_) => MonkeyLoadedView( + // walletId: args.item1, + // managerProvider: args.item2, + // ), + // settings: RouteSettings( + // name: settings.name, + // ), + // ); + // } + // return _routeError("${settings.name} invalid args: ${args.toString()}"); case CoinControlView.routeName: if (args is Tuple2) { From 86d3258910f3d138d3859159d344a6a888258165 Mon Sep 17 00:00:00 2001 From: ryleedavis Date: Wed, 26 Jul 2023 13:18:18 -0600 Subject: [PATCH 151/169] Monkey help message --- crypto_plugins/flutter_libmonero | 2 +- lib/pages/monkey/monkey_view.dart | 33 +++--------------- lib/pages/wallet_view/wallet_view.dart | 46 +++++++++++--------------- 3 files changed, 25 insertions(+), 56 deletions(-) diff --git a/crypto_plugins/flutter_libmonero b/crypto_plugins/flutter_libmonero index e48952185..407425c9f 160000 --- a/crypto_plugins/flutter_libmonero +++ b/crypto_plugins/flutter_libmonero @@ -1 +1 @@ -Subproject commit e48952185556a10f182184fd572bcb04365f5831 +Subproject commit 407425c9fcf7a30c81f1345246c7225bc18b5cd5 diff --git a/lib/pages/monkey/monkey_view.dart b/lib/pages/monkey/monkey_view.dart index b631b0703..075191235 100644 --- a/lib/pages/monkey/monkey_view.dart +++ b/lib/pages/monkey/monkey_view.dart @@ -22,6 +22,7 @@ import 'package:stackwallet/widgets/conditional_parent.dart'; import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_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 MonkeyView extends ConsumerStatefulWidget { const MonkeyView({ @@ -220,34 +221,10 @@ class _MonkeyViewState extends ConsumerState { useSafeArea: false, barrierDismissible: true, builder: (context) { - return Dialog( - child: Material( - borderRadius: BorderRadius.circular( - 20, - ), - child: Container( - height: 200, - decoration: BoxDecoration( - color: Theme.of(context) - .extension()! - .popupBG, - borderRadius: BorderRadius.circular( - 20, - ), - ), - child: Column( - children: [ - Center( - child: Text( - "Help", - style: STextStyles.pageTitleH2( - context), - ), - ) - ], - ), - ), - ), + return const StackOkDialog( + title: "About MonKeys", + message: + "A MonKey is a visual representation of your Banano address.", ); }); }), diff --git a/lib/pages/wallet_view/wallet_view.dart b/lib/pages/wallet_view/wallet_view.dart index 0dcfe18dc..b2d5de8e6 100644 --- a/lib/pages/wallet_view/wallet_view.dart +++ b/lib/pages/wallet_view/wallet_view.dart @@ -925,33 +925,25 @@ class _WalletViewState extends ConsumerState { ); }, ), - if (ref.watch( - walletsChangeNotifierProvider.select( - (value) => value - .getManager(widget.walletId) - .hasCoinControlSupport, - ), - ) && - ref.watch( - prefsChangeNotifierProvider.select( - (value) => value.enableCoinControl, - ), - )) - WalletNavigationBarItemData( - icon: SvgPicture.asset(Assets.svg.monkey, - height: 20, - width: 20, - color: Theme.of(context).extension()!.bottomNavIconIcon,), - label: "MonKey", - onTap: () { - Navigator.of(context).pushNamed( - MonkeyView.routeName, - arguments: Tuple2( - widget.walletId, - widget.managerProvider, - ), - ); - }), + WalletNavigationBarItemData( + icon: SvgPicture.asset( + Assets.svg.monkey, + height: 20, + width: 20, + color: Theme.of(context) + .extension()! + .bottomNavIconIcon, + ), + label: "MonKey", + onTap: () { + Navigator.of(context).pushNamed( + MonkeyView.routeName, + arguments: Tuple2( + widget.walletId, + widget.managerProvider, + ), + ); + }), if (ref.watch( walletsChangeNotifierProvider.select( (value) => value From 3cb8a6a173d834e633969ab0f4035918b0aed72f Mon Sep 17 00:00:00 2001 From: ryleedavis Date: Wed, 26 Jul 2023 14:12:06 -0600 Subject: [PATCH 152/169] WIP: adding monkey to desktop --- .../sub_widgets/desktop_wallet_features.dart | 20 ++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) 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 aa5922959..1ee1b664b 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 @@ -16,6 +16,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:flutter_svg/svg.dart'; import 'package:stackwallet/notifications/show_flush_bar.dart'; +import 'package:stackwallet/pages/monkey/monkey_view.dart'; import 'package:stackwallet/pages/paynym/paynym_claim_view.dart'; import 'package:stackwallet/pages/paynym/paynym_home_view.dart'; import 'package:stackwallet/pages_desktop_specific/coin_control/desktop_coin_control_view.dart'; @@ -41,6 +42,7 @@ import 'package:stackwallet/widgets/desktop/desktop_dialog.dart'; import 'package:stackwallet/widgets/desktop/primary_button.dart'; import 'package:stackwallet/widgets/desktop/secondary_button.dart'; import 'package:stackwallet/widgets/loading_indicator.dart'; +import 'package:tuple/tuple.dart'; class DesktopWalletFeatures extends ConsumerStatefulWidget { const DesktopWalletFeatures({ @@ -80,6 +82,7 @@ class _DesktopWalletFeaturesState extends ConsumerState { onCoinControlPressed: _onCoinControlPressed, onAnonymizeAllPressed: _onAnonymizeAllPressed, onWhirlpoolPressed: _onWhirlpoolPressed, + onMonkeyPressed: _onMonkeyPressed, ), ); } @@ -313,6 +316,21 @@ class _DesktopWalletFeaturesState extends ConsumerState { } } + Future _onMonkeyPressed() async { + Navigator.of(context, rootNavigator: true).pop(); + final managerProvider = ref + .read(walletsChangeNotifierProvider) + .getManagerProvider(widget.walletId); + + await (Navigator.of(context).pushNamed( + MonkeyView.routeName, + arguments: Tuple2( + widget.walletId, + managerProvider, + ), + )); + } + @override Widget build(BuildContext context) { final manager = ref.watch( @@ -330,8 +348,8 @@ class _DesktopWalletFeaturesState extends ConsumerState { )) || manager.coin == Coin.firo || manager.coin == Coin.firoTestNet || + manager.coin == Coin.banano || manager.hasWhirlpoolSupport; - return Row( children: [ if (Constants.enableExchange) From 5668b045e5ec63a72526263eb408d996758ab21e Mon Sep 17 00:00:00 2001 From: ryleedavis Date: Wed, 26 Jul 2023 14:12:19 -0600 Subject: [PATCH 153/169] WIP: adding monkey to desktop --- .../sub_widgets/more_features/more_features_dialog.dart | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/more_features/more_features_dialog.dart b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/more_features/more_features_dialog.dart index 4c8f47c25..3aaa6b5df 100644 --- a/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/more_features/more_features_dialog.dart +++ b/lib/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/more_features/more_features_dialog.dart @@ -29,6 +29,7 @@ class MoreFeaturesDialog extends ConsumerStatefulWidget { required this.onCoinControlPressed, required this.onAnonymizeAllPressed, required this.onWhirlpoolPressed, + required this.onMonkeyPressed, }) : super(key: key); final String walletId; @@ -36,6 +37,7 @@ class MoreFeaturesDialog extends ConsumerStatefulWidget { final VoidCallback? onCoinControlPressed; final VoidCallback? onAnonymizeAllPressed; final VoidCallback? onWhirlpoolPressed; + final VoidCallback? onMonkeyPressed; @override ConsumerState createState() => _MoreFeaturesDialogState(); @@ -103,6 +105,13 @@ class _MoreFeaturesDialogState extends ConsumerState { iconAsset: Assets.svg.robotHead, onPressed: () => widget.onPaynymPressed?.call(), ), + if (manager.coin == Coin.banano) + _MoreFeaturesItem( + label: "MonKey", + detail: "Generate Banano MonKey", + iconAsset: Assets.svg.monkey, + onPressed: () => widget.onMonkeyPressed?.call(), + ), const SizedBox( height: 28, ), From 36747ca479da7ad04b35e2fd7434ce931255576d Mon Sep 17 00:00:00 2001 From: julian Date: Wed, 26 Jul 2023 14:50:38 -0600 Subject: [PATCH 154/169] another bandaid fix for hive box isn't open --- lib/db/hive/db.dart | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/lib/db/hive/db.dart b/lib/db/hive/db.dart index 25efcdb3e..9d3f73abd 100644 --- a/lib/db/hive/db.dart +++ b/lib/db/hive/db.dart @@ -274,8 +274,15 @@ class DB { {required dynamic key, required String boxName}) async => await mutex.protect(() async => await Hive.box(boxName).delete(key)); - Future deleteAll({required String boxName}) async => - await mutex.protect(() async => await Hive.box(boxName).clear()); + Future deleteAll({required String boxName}) async { + await mutex.protect(() async { + Box box = Hive.box(boxName); + if (!box.isOpen) { + box = await Hive.openBox(boxName); + } + await box.clear(); + }); + } Future deleteBoxFromDisk({required String boxName}) async => await mutex.protect(() async => await Hive.deleteBoxFromDisk(boxName)); From 66e1db9b52021689ce208b86c593c9cf96d0daa1 Mon Sep 17 00:00:00 2001 From: ryleedavis Date: Wed, 26 Jul 2023 15:10:53 -0600 Subject: [PATCH 155/169] WIP: scaling monkey view for desktop --- lib/pages/monkey/monkey_view.dart | 315 ++++++++++++++++++++---------- 1 file changed, 207 insertions(+), 108 deletions(-) diff --git a/lib/pages/monkey/monkey_view.dart b/lib/pages/monkey/monkey_view.dart index 075191235..83901fbca 100644 --- a/lib/pages/monkey/monkey_view.dart +++ b/lib/pages/monkey/monkey_view.dart @@ -9,6 +9,7 @@ import 'package:path_provider/path_provider.dart'; import 'package:permission_handler/permission_handler.dart'; import 'package:stackwallet/pages/monkey/sub_widgets/fetch_monkey_dialog.dart'; 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/global/wallets_provider.dart'; import 'package:stackwallet/services/coins/banano/banano_wallet.dart'; import 'package:stackwallet/services/coins/manager.dart'; @@ -17,6 +18,7 @@ import 'package:stackwallet/themes/stack_colors.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:stackwallet/utilities/text_styles.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'; @@ -24,6 +26,9 @@ import 'package:stackwallet/widgets/desktop/primary_button.dart'; import 'package:stackwallet/widgets/desktop/secondary_button.dart'; import 'package:stackwallet/widgets/stack_dialog.dart'; +import '../../widgets/desktop/desktop_app_bar.dart'; +import '../../widgets/desktop/desktop_scaffold.dart'; + class MonkeyView extends ConsumerStatefulWidget { const MonkeyView({ Key? key, @@ -193,25 +198,57 @@ class _MonkeyViewState extends ConsumerState { final manager = ref.watch(walletsChangeNotifierProvider .select((value) => value.getManager(widget.walletId))); + final bool isDesktop = Util.isDesktop; + List? imageBytes; imageBytes = (manager.wallet as BananoWallet).getMonkeyImageBytes(); + //edit for desktop return Background( child: Stack( children: [ - Scaffold( - appBar: AppBar( - leading: AppBarBackButton( - onPressed: () { - Navigator.of(context).pop(); - }, - ), - title: Text( - "MonKey", - style: STextStyles.navBarTitle(context), - ), - actions: [ - AspectRatio( + ConditionalParent( + condition: isDesktop, + builder: (child) => DesktopScaffold( + appBar: DesktopAppBar( + background: Theme.of(context).extension()!.popupBG, + leading: Expanded( + child: Row( + children: [ + const SizedBox( + width: 32, + ), + AppBarIconButton( + size: 32, + color: Theme.of(context) + .extension()! + .textFieldDefaultBG, + shadows: const [], + icon: SvgPicture.asset( + Assets.svg.arrowLeft, + width: 18, + height: 18, + color: Theme.of(context) + .extension()! + .topNavIconPrimary, + ), + onPressed: () { + if (mounted) { + Navigator.of(context).pop(); + } + }, + ), + const SizedBox( + width: 15, + ), + Text( + "MonKey", + style: STextStyles.navBarTitle(context), + ), + ], + ), + ), + trailing: AspectRatio( aspectRatio: 1, child: AppBarIconButton( icon: SvgPicture.asset(Assets.svg.circleQuestion), @@ -221,7 +258,7 @@ class _MonkeyViewState extends ConsumerState { useSafeArea: false, barrierDismissible: true, builder: (context) { - return const StackOkDialog( + return const StackDialog( title: "About MonKeys", message: "A MonKey is a visual representation of your Banano address.", @@ -229,112 +266,174 @@ class _MonkeyViewState extends ConsumerState { }); }), ), - ], - ), - body: ConditionalParent( - condition: imageBytes != null, - builder: (child) => Column( - children: [ - const Spacer( - flex: 1, - ), - if (imageBytes != null) - Container( - child: SvgPicture.memory(Uint8List.fromList(imageBytes!)), - width: 300, - height: 300, - ), - const Spacer( - flex: 1, - ), - Padding( - padding: const EdgeInsets.all(16.0), - child: Column( - children: [ - SecondaryButton( - label: "Download as SVG", - onPressed: () async { - getMonkeySVG(receivingAddress); - }, - ), - const SizedBox(height: 12), - SecondaryButton( - label: "Download as PNG", - onPressed: () { - getMonkeyPNG(receivingAddress); - }, - ), - ], - ), - ), - // child, - ], + useSpacers: false, + isCompactHeight: true, ), - child: Column( - children: [ - const Spacer( - flex: 4, + body: child, + ), + child: ConditionalParent( + condition: !isDesktop, + builder: (child) => Scaffold( + appBar: AppBar( + leading: AppBarBackButton( + onPressed: () { + Navigator.of(context).pop(); + }, ), - Center( - child: Column( - children: [ - Opacity( - opacity: 0.2, - child: SvgPicture.file( - File( - ref.watch(coinIconProvider(coin)), - ), - width: 200, - height: 200, - ), - ), - const SizedBox( - height: 40, - ), - Text( - "You do not have a MonKey yet. \nFetch yours now!", - style: STextStyles.smallMed14(context).copyWith( + title: Text( + "MonKey", + style: STextStyles.navBarTitle(context), + ), + actions: [ + AspectRatio( + aspectRatio: 1, + child: AppBarIconButton( + icon: SvgPicture.asset( + Assets.svg.circleQuestion, color: Theme.of(context) .extension()! - .textDark3, + .infoItemText, ), - textAlign: TextAlign.center, - ), - ], + onPressed: () { + showDialog( + context: context, + useSafeArea: false, + barrierDismissible: true, + builder: (context) { + return const StackOkDialog( + title: "About MonKeys", + message: + "A MonKey is a visual representation of your Banano address.", + ); + }); + }), ), + ], + ), + body: child, + ), + child: ConditionalParent( + condition: isDesktop, + builder: (child) => SizedBox( + width: 300, + height: 300, + child: child, + ), + child: ConditionalParent( + condition: imageBytes != null, + builder: (child) => Column( + children: [ + const Spacer( + flex: 1, + ), + if (imageBytes != null) + Container( + child: SvgPicture.memory( + Uint8List.fromList(imageBytes!)), + width: 300, + height: 300, + ), + const Spacer( + flex: 1, + ), + Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + children: [ + SecondaryButton( + label: "Download as SVG", + onPressed: () async { + getMonkeySVG(receivingAddress); + }, + ), + const SizedBox(height: 12), + SecondaryButton( + label: "Download as PNG", + onPressed: () { + getMonkeyPNG(receivingAddress); + }, + ), + ], + ), + ), + // child, + ], ), - const Spacer( - flex: 6, - ), - Padding( - padding: const EdgeInsets.all(16.0), - child: PrimaryButton( - label: "Fetch MonKey", - onPressed: () async { - getMonkeyImage(receivingAddress); + child: Column( + children: [ + const Spacer( + flex: 4, + ), + Center( + child: Column( + children: [ + Opacity( + opacity: 0.2, + child: SvgPicture.file( + File( + ref.watch(coinIconProvider(coin)), + ), + width: 200, + height: 200, + ), + ), + const SizedBox( + height: 40, + ), + Text( + "You do not have a MonKey yet. \nFetch yours now!", + style: STextStyles.smallMed14(context).copyWith( + color: Theme.of(context) + .extension()! + .textDark3, + ), + textAlign: TextAlign.center, + ), + ], + ), + ), + const Spacer( + flex: 6, + ), + Padding( + padding: const EdgeInsets.all(16.0), + child: PrimaryButton( + label: "Fetch MonKey", + onPressed: () async { + getMonkeyImage(receivingAddress); - showDialog( - context: context, - useSafeArea: false, - barrierDismissible: false, - builder: (context) { - return FetchMonkeyDialog( - onCancel: () async { - Navigator.of(context).pop(); + showDialog( + context: context, + useSafeArea: false, + barrierDismissible: false, + builder: (context) { + return FetchMonkeyDialog( + onCancel: () async { + Navigator.of(context).pop(); + }, + ); }, ); + + await Future.delayed( + const Duration(seconds: 2)); + + if (isDesktop) { + Navigator.of(context).popUntil( + ModalRoute.withName( + DesktopWalletView.routeName), + ); + } else { + Navigator.of(context).popUntil( + ModalRoute.withName(WalletView.routeName), + ); + } }, - ); - - await Future.delayed(const Duration(seconds: 2)); - - Navigator.of(context).popUntil( - ModalRoute.withName(WalletView.routeName), - ); - }, - ), + ), + ), + ], ), - ], + ), ), ), ), From b2aa4272b445433bff9dd5232a675d4934e616ce Mon Sep 17 00:00:00 2001 From: julian Date: Wed, 26 Jul 2023 15:32:00 -0600 Subject: [PATCH 156/169] reorg and loading indicator --- lib/pages/monkey/monkey_view.dart | 66 ++++++++----------- .../sub_widgets/desktop_wallet_features.dart | 9 +-- lib/route_generator.dart | 5 +- 3 files changed, 29 insertions(+), 51 deletions(-) diff --git a/lib/pages/monkey/monkey_view.dart b/lib/pages/monkey/monkey_view.dart index 83901fbca..ce4e951d3 100644 --- a/lib/pages/monkey/monkey_view.dart +++ b/lib/pages/monkey/monkey_view.dart @@ -7,40 +7,34 @@ import 'package:flutter_svg/svg.dart'; import 'package:http/http.dart' as http; import 'package:path_provider/path_provider.dart'; import 'package:permission_handler/permission_handler.dart'; -import 'package:stackwallet/pages/monkey/sub_widgets/fetch_monkey_dialog.dart'; -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/global/wallets_provider.dart'; import 'package:stackwallet/services/coins/banano/banano_wallet.dart'; -import 'package:stackwallet/services/coins/manager.dart'; import 'package:stackwallet/themes/coin_icon_provider.dart'; import 'package:stackwallet/themes/stack_colors.dart'; import 'package:stackwallet/utilities/assets.dart'; import 'package:stackwallet/utilities/enums/coin_enum.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/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/desktop/secondary_button.dart'; import 'package:stackwallet/widgets/stack_dialog.dart'; -import '../../widgets/desktop/desktop_app_bar.dart'; -import '../../widgets/desktop/desktop_scaffold.dart'; - class MonkeyView extends ConsumerStatefulWidget { const MonkeyView({ Key? key, required this.walletId, - required this.managerProvider, }) : super(key: key); static const String routeName = "/monkey"; static const double navBarHeight = 65.0; final String walletId; - final ChangeNotifierProvider managerProvider; @override ConsumerState createState() => _MonkeyViewState(); @@ -48,23 +42,21 @@ class MonkeyView extends ConsumerStatefulWidget { class _MonkeyViewState extends ConsumerState { late final String walletId; - late final ChangeNotifierProvider managerProvider; String receivingAddress = ""; - void getMonkeyImage(String address) async { + Future getMonkeyImage(String address) async { if (address.isEmpty) { //address shouldn't be empty return; } - final manager = ref.watch(walletsChangeNotifierProvider - .select((value) => value.getManager(walletId))); - final http.Response response = await http .get(Uri.parse('https://monkey.banano.cc/api/v1/monkey/$address')); if (response.statusCode == 200) { + final manager = + ref.read(walletsChangeNotifierProvider).getManager(walletId); final decodedResponse = response.bodyBytes; await (manager.wallet as BananoWallet) .updateMonkeyImageBytes(decodedResponse); @@ -172,7 +164,6 @@ class _MonkeyViewState extends ConsumerState { @override void initState() { walletId = widget.walletId; - managerProvider = widget.managerProvider; WidgetsBinding.instance.addPostFrameCallback((timeStamp) async { final address = await ref @@ -194,9 +185,9 @@ class _MonkeyViewState extends ConsumerState { @override Widget build(BuildContext context) { - final Coin coin = ref.watch(managerProvider.select((value) => value.coin)); final manager = ref.watch(walletsChangeNotifierProvider .select((value) => value.getManager(widget.walletId))); + final Coin coin = manager.coin; final bool isDesktop = Util.isDesktop; @@ -400,34 +391,29 @@ class _MonkeyViewState extends ConsumerState { child: PrimaryButton( label: "Fetch MonKey", onPressed: () async { - getMonkeyImage(receivingAddress); + final future = Future.wait([ + getMonkeyImage(receivingAddress), + Future.delayed(const Duration(seconds: 2)), + ]); - showDialog( + await showLoading( + whileFuture: future, context: context, - useSafeArea: false, - barrierDismissible: false, - builder: (context) { - return FetchMonkeyDialog( - onCancel: () async { - Navigator.of(context).pop(); - }, - ); - }, + isDesktop: Util.isDesktop, + message: "Fetching MonKey", + subMessage: "We are fetching your MonKey", ); - await Future.delayed( - const Duration(seconds: 2)); - - if (isDesktop) { - Navigator.of(context).popUntil( - ModalRoute.withName( - DesktopWalletView.routeName), - ); - } else { - Navigator.of(context).popUntil( - ModalRoute.withName(WalletView.routeName), - ); - } + // if (isDesktop) { + // Navigator.of(context).popUntil( + // ModalRoute.withName( + // DesktopWalletView.routeName), + // ); + // } else { + // Navigator.of(context).popUntil( + // ModalRoute.withName(WalletView.routeName), + // ); + // } }, ), ), 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 1ee1b664b..afeec5094 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 @@ -42,7 +42,6 @@ import 'package:stackwallet/widgets/desktop/desktop_dialog.dart'; import 'package:stackwallet/widgets/desktop/primary_button.dart'; import 'package:stackwallet/widgets/desktop/secondary_button.dart'; import 'package:stackwallet/widgets/loading_indicator.dart'; -import 'package:tuple/tuple.dart'; class DesktopWalletFeatures extends ConsumerStatefulWidget { const DesktopWalletFeatures({ @@ -318,16 +317,10 @@ class _DesktopWalletFeaturesState extends ConsumerState { Future _onMonkeyPressed() async { Navigator.of(context, rootNavigator: true).pop(); - final managerProvider = ref - .read(walletsChangeNotifierProvider) - .getManagerProvider(widget.walletId); await (Navigator.of(context).pushNamed( MonkeyView.routeName, - arguments: Tuple2( - widget.walletId, - managerProvider, - ), + arguments: widget.walletId, )); } diff --git a/lib/route_generator.dart b/lib/route_generator.dart index 368d23397..edd3b15fc 100644 --- a/lib/route_generator.dart +++ b/lib/route_generator.dart @@ -378,12 +378,11 @@ class RouteGenerator { return _routeError("${settings.name} invalid args: ${args.toString()}"); case MonkeyView.routeName: - if (args is Tuple2>) { + if (args is String) { return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, builder: (_) => MonkeyView( - walletId: args.item1, - managerProvider: args.item2, + walletId: args, ), settings: RouteSettings( name: settings.name, From 4e9ba505723970d216cbbc6f2392cfaad59cd9bd Mon Sep 17 00:00:00 2001 From: ryleedavis Date: Wed, 26 Jul 2023 15:37:10 -0600 Subject: [PATCH 157/169] Desktop what is monkey button --- lib/pages/monkey/monkey_view.dart | 40 +++++++++++++++++++++++-------- 1 file changed, 30 insertions(+), 10 deletions(-) diff --git a/lib/pages/monkey/monkey_view.dart b/lib/pages/monkey/monkey_view.dart index ce4e951d3..07466fe1b 100644 --- a/lib/pages/monkey/monkey_view.dart +++ b/lib/pages/monkey/monkey_view.dart @@ -239,11 +239,12 @@ class _MonkeyViewState extends ConsumerState { ], ), ), - trailing: AspectRatio( - aspectRatio: 1, - child: AppBarIconButton( - icon: SvgPicture.asset(Assets.svg.circleQuestion), - onPressed: () { + trailing: Padding( + padding: const EdgeInsets.all(8.0), + child: MouseRegion( + cursor: SystemMouseCursors.click, + child: GestureDetector( + onTap: () { showDialog( context: context, useSafeArea: false, @@ -255,7 +256,30 @@ class _MonkeyViewState extends ConsumerState { "A MonKey is a visual representation of your Banano address.", ); }); - }), + }, + child: Row( + children: [ + SvgPicture.asset( + Assets.svg.circleQuestion, + color: Colors.blue[800], + ), + SizedBox( + width: 6, + ), + Padding( + padding: const EdgeInsets.only(bottom: 8.0), + child: Text( + "What is MonKey?", + style: STextStyles.desktopTextSmall(context) + .copyWith( + color: Colors.blue[800], + ), + ), + ), + ], + ), + ), + ), ), useSpacers: false, isCompactHeight: true, @@ -281,9 +305,6 @@ class _MonkeyViewState extends ConsumerState { child: AppBarIconButton( icon: SvgPicture.asset( Assets.svg.circleQuestion, - color: Theme.of(context) - .extension()! - .infoItemText, ), onPressed: () { showDialog( @@ -403,7 +424,6 @@ class _MonkeyViewState extends ConsumerState { message: "Fetching MonKey", subMessage: "We are fetching your MonKey", ); - // if (isDesktop) { // Navigator.of(context).popUntil( // ModalRoute.withName( From 0175939d6383526b55f8bb80e84ef7ff6a34d363 Mon Sep 17 00:00:00 2001 From: julian Date: Wed, 26 Jul 2023 15:52:52 -0600 Subject: [PATCH 158/169] download/save image on linux --- lib/pages/monkey/monkey_view.dart | 170 +++++++++++++++++------------- 1 file changed, 96 insertions(+), 74 deletions(-) diff --git a/lib/pages/monkey/monkey_view.dart b/lib/pages/monkey/monkey_view.dart index ce4e951d3..56cd8cf1d 100644 --- a/lib/pages/monkey/monkey_view.dart +++ b/lib/pages/monkey/monkey_view.dart @@ -65,60 +65,73 @@ class _MonkeyViewState extends ConsumerState { } } - void getMonkeySVG(String address) async { - if (address.isEmpty) { - //address shouldn't be empty - return; - } + // void getMonkeySVG(String address) async { + // if (address.isEmpty) { + // //address shouldn't be empty + // return; + // } + // + // final http.Response response = await http + // .get(Uri.parse('https://monkey.banano.cc/api/v1/monkey/$address')); + // + // if (response.statusCode == 200) { + // final decodedResponse = response.bodyBytes; + // Directory directory = await getApplicationDocumentsDirectory(); + // late Directory sampleFolder; + // + // if (Platform.isAndroid) { + // directory = Directory("/storage/emulated/0/"); + // sampleFolder = Directory('${directory!.path}Documents'); + // } else if (Platform.isIOS) { + // sampleFolder = Directory(directory!.path); + // } else if (Platform.isLinux) { + // sampleFolder = Directory('${directory!.path}Documents'); + // } else if (Platform.isWindows) { + // sampleFolder = Directory('${directory!.path}Documents'); + // } else if (Platform.isMacOS) { + // sampleFolder = Directory('${directory!.path}Documents'); + // } + // + // try { + // if (!sampleFolder.existsSync()) { + // sampleFolder.createSync(recursive: true); + // } + // } catch (e, s) { + // // todo: come back to this + // debugPrint("$e $s"); + // } + // + // final docPath = sampleFolder.path; + // final filePath = "$docPath/monkey.svg"; + // + // File imgFile = File(filePath); + // await imgFile.writeAsBytes(decodedResponse); + // } else { + // throw Exception("Failed to get MonKey"); + // } + // } - final http.Response response = await http - .get(Uri.parse('https://monkey.banano.cc/api/v1/monkey/$address')); - - if (response.statusCode == 200) { - final decodedResponse = response.bodyBytes; - Directory directory = await getApplicationDocumentsDirectory(); - late Directory sampleFolder; - - if (Platform.isAndroid) { - directory = Directory("/storage/emulated/0/"); - sampleFolder = Directory('${directory!.path}Documents'); - } else if (Platform.isIOS) { - sampleFolder = Directory(directory!.path); - } else if (Platform.isLinux) { - sampleFolder = Directory('${directory!.path}Documents'); - } else if (Platform.isWindows) { - sampleFolder = Directory('${directory!.path}Documents'); - } else if (Platform.isMacOS) { - sampleFolder = Directory('${directory!.path}Documents'); - } - - try { - if (!sampleFolder.existsSync()) { - sampleFolder.createSync(recursive: true); - } - } catch (e, s) { - // todo: come back to this - debugPrint("$e $s"); - } - - final docPath = sampleFolder.path; - final filePath = "$docPath/monkey.svg"; - - File imgFile = File(filePath); - await imgFile.writeAsBytes(decodedResponse); - } else { - throw Exception("Failed to get MonKey"); + Future getDocsDir() async { + try { + return await getApplicationDocumentsDirectory(); + } catch (_) { + return null; } } - void getMonkeyPNG(String address) async { + Future downloadMonkey(String address, bool isPNG) async { if (address.isEmpty) { //address shouldn't be empty return; } - final http.Response response = await http.get(Uri.parse( - 'https://monkey.banano.cc/api/v1/monkey/${address}?format=png&size=512&background=false')); + String url = "https://monkey.banano.cc/api/v1/monkey/$address"; + + if (isPNG) { + url += '?format=png&size=512&background=false'; + } + + final http.Response response = await http.get(Uri.parse(url)); if (response.statusCode == 200) { if (Platform.isAndroid) { @@ -126,33 +139,30 @@ class _MonkeyViewState extends ConsumerState { } final decodedResponse = response.bodyBytes; - Directory directory = await getApplicationDocumentsDirectory(); - late Directory sampleFolder; + final Directory? sampleFolder = await getDocsDir(); - if (Platform.isAndroid) { - directory = Directory("/storage/emulated/0/"); - sampleFolder = Directory('${directory!.path}Documents'); - } else if (Platform.isIOS) { - sampleFolder = Directory(directory!.path); - } else if (Platform.isLinux) { - sampleFolder = Directory('${directory!.path}Documents'); - } else if (Platform.isWindows) { - sampleFolder = Directory('${directory!.path}Documents'); - } else if (Platform.isMacOS) { - sampleFolder = Directory('${directory!.path}Documents'); + print("PATH: ${sampleFolder?.path}"); + + if (sampleFolder == null) { + print("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"); + return; } - try { - if (!sampleFolder.existsSync()) { - sampleFolder.createSync(recursive: true); - } - } catch (e, s) { - // todo: come back to this - debugPrint("$e $s"); - } + // try { + // if (!sampleFolder.existsSync()) { + // sampleFolder.createSync(recursive: true); + // } + // } catch (e, s) { + // // todo: come back to this + // debugPrint("$e $s"); + // } final docPath = sampleFolder.path; - final filePath = "$docPath/monkey.png"; + String filePath = "$docPath/monkey_$address"; + + filePath += isPNG ? ".png" : ".svg"; + + // todo check if monkey.png exists File imgFile = File(filePath); await imgFile.writeAsBytes(decodedResponse); @@ -318,11 +328,11 @@ class _MonkeyViewState extends ConsumerState { flex: 1, ), if (imageBytes != null) - Container( - child: SvgPicture.memory( - Uint8List.fromList(imageBytes!)), + SizedBox( width: 300, height: 300, + child: + SvgPicture.memory(Uint8List.fromList(imageBytes)), ), const Spacer( flex: 1, @@ -332,16 +342,28 @@ class _MonkeyViewState extends ConsumerState { child: Column( children: [ SecondaryButton( - label: "Download as SVG", + label: "Save as SVG", onPressed: () async { - getMonkeySVG(receivingAddress); + await showLoading( + whileFuture: + downloadMonkey(receivingAddress, false), + context: context, + isDesktop: Util.isDesktop, + message: "Saving MonKey svg", + ); }, ), const SizedBox(height: 12), SecondaryButton( label: "Download as PNG", - onPressed: () { - getMonkeyPNG(receivingAddress); + onPressed: () async { + await showLoading( + whileFuture: + downloadMonkey(receivingAddress, true), + context: context, + isDesktop: Util.isDesktop, + message: "Downloading MonKey png", + ); }, ), ], From 39882b0d46bf37d15f05ceb84f60da41aaa1997b Mon Sep 17 00:00:00 2001 From: ryleedavis Date: Wed, 26 Jul 2023 16:07:54 -0600 Subject: [PATCH 159/169] monkey icon in app bar --- lib/pages/monkey/monkey_view.dart | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/pages/monkey/monkey_view.dart b/lib/pages/monkey/monkey_view.dart index 07466fe1b..940b41fa4 100644 --- a/lib/pages/monkey/monkey_view.dart +++ b/lib/pages/monkey/monkey_view.dart @@ -232,6 +232,10 @@ class _MonkeyViewState extends ConsumerState { const SizedBox( width: 15, ), + SvgPicture.asset(Assets.svg.monkey), + const SizedBox( + width: 12, + ), Text( "MonKey", style: STextStyles.navBarTitle(context), From 9d2b315bd9a6af38a47addd5f3c3853e79ce5401 Mon Sep 17 00:00:00 2001 From: julian Date: Wed, 26 Jul 2023 16:08:01 -0600 Subject: [PATCH 160/169] desktop layout --- lib/pages/monkey/monkey_view.dart | 428 +++++++++++++++--------------- 1 file changed, 219 insertions(+), 209 deletions(-) diff --git a/lib/pages/monkey/monkey_view.dart b/lib/pages/monkey/monkey_view.dart index 56cd8cf1d..02947a5ce 100644 --- a/lib/pages/monkey/monkey_view.dart +++ b/lib/pages/monkey/monkey_view.dart @@ -206,60 +206,100 @@ class _MonkeyViewState extends ConsumerState { //edit for desktop return Background( - child: Stack( - children: [ - ConditionalParent( - condition: isDesktop, - builder: (child) => DesktopScaffold( - appBar: DesktopAppBar( - background: Theme.of(context).extension()!.popupBG, - leading: Expanded( - child: Row( - children: [ - const SizedBox( - width: 32, - ), - AppBarIconButton( - size: 32, - color: Theme.of(context) - .extension()! - .textFieldDefaultBG, - shadows: const [], - icon: SvgPicture.asset( - Assets.svg.arrowLeft, - width: 18, - height: 18, - color: Theme.of(context) - .extension()! - .topNavIconPrimary, - ), - onPressed: () { - if (mounted) { - Navigator.of(context).pop(); - } - }, - ), - const SizedBox( - width: 15, - ), - Text( - "MonKey", - style: STextStyles.navBarTitle(context), - ), - ], + child: ConditionalParent( + condition: isDesktop, + builder: (child) => DesktopScaffold( + appBar: DesktopAppBar( + background: Theme.of(context).extension()!.popupBG, + leading: Expanded( + child: Row( + children: [ + const SizedBox( + width: 32, ), - ), - trailing: AspectRatio( + AppBarIconButton( + size: 32, + color: Theme.of(context) + .extension()! + .textFieldDefaultBG, + shadows: const [], + icon: SvgPicture.asset( + Assets.svg.arrowLeft, + width: 18, + height: 18, + color: Theme.of(context) + .extension()! + .topNavIconPrimary, + ), + onPressed: () { + if (mounted) { + Navigator.of(context).pop(); + } + }, + ), + const SizedBox( + width: 15, + ), + Text( + "MonKey", + style: STextStyles.navBarTitle(context), + ), + ], + ), + ), + trailing: AspectRatio( + aspectRatio: 1, + child: AppBarIconButton( + icon: SvgPicture.asset(Assets.svg.circleQuestion), + onPressed: () { + showDialog( + context: context, + useSafeArea: false, + barrierDismissible: true, + builder: (context) { + return const StackDialog( + title: "About MonKeys", + message: + "A MonKey is a visual representation of your Banano address.", + ); + }); + }), + ), + useSpacers: false, + isCompactHeight: true, + ), + body: child, + ), + child: ConditionalParent( + condition: !isDesktop, + builder: (child) => Scaffold( + appBar: AppBar( + leading: AppBarBackButton( + onPressed: () { + Navigator.of(context).pop(); + }, + ), + title: Text( + "MonKey", + style: STextStyles.navBarTitle(context), + ), + actions: [ + AspectRatio( aspectRatio: 1, child: AppBarIconButton( - icon: SvgPicture.asset(Assets.svg.circleQuestion), + icon: SvgPicture.asset( + Assets.svg.circleQuestion, + color: Theme.of(context) + .extension()! + .infoItemText, + ), onPressed: () { showDialog( context: context, useSafeArea: false, barrierDismissible: true, builder: (context) { - return const StackDialog( + return const StackOkDialog( title: "About MonKeys", message: "A MonKey is a visual representation of your Banano address.", @@ -267,185 +307,155 @@ class _MonkeyViewState extends ConsumerState { }); }), ), - useSpacers: false, - isCompactHeight: true, - ), - body: child, + ], + ), + body: child, + ), + child: ConditionalParent( + condition: isDesktop, + builder: (child) => SizedBox( + width: 318, + child: child, ), child: ConditionalParent( - condition: !isDesktop, - builder: (child) => Scaffold( - appBar: AppBar( - leading: AppBarBackButton( - onPressed: () { - Navigator.of(context).pop(); - }, - ), - title: Text( - "MonKey", - style: STextStyles.navBarTitle(context), - ), - actions: [ - AspectRatio( - aspectRatio: 1, - child: AppBarIconButton( - icon: SvgPicture.asset( - Assets.svg.circleQuestion, - color: Theme.of(context) - .extension()! - .infoItemText, - ), - onPressed: () { - showDialog( - context: context, - useSafeArea: false, - barrierDismissible: true, - builder: (context) { - return const StackOkDialog( - title: "About MonKeys", - message: - "A MonKey is a visual representation of your Banano address.", - ); - }); - }), + condition: imageBytes != null, + builder: (_) => Column( + children: [ + isDesktop + ? const SizedBox( + height: 50, + ) + : const Spacer( + flex: 1, + ), + if (imageBytes != null) + SizedBox( + width: 300, + height: 300, + child: SvgPicture.memory(Uint8List.fromList(imageBytes)), ), - ], - ), - body: child, - ), - child: ConditionalParent( - condition: isDesktop, - builder: (child) => SizedBox( - width: 300, - height: 300, - child: child, - ), - child: ConditionalParent( - condition: imageBytes != null, - builder: (child) => Column( - children: [ - const Spacer( - flex: 1, - ), - if (imageBytes != null) - SizedBox( - width: 300, - height: 300, - child: - SvgPicture.memory(Uint8List.fromList(imageBytes)), + isDesktop + ? const SizedBox( + height: 50, + ) + : const Spacer( + flex: 1, ), - const Spacer( - flex: 1, - ), - Padding( - padding: const EdgeInsets.all(16.0), - child: Column( - children: [ - SecondaryButton( - label: "Save as SVG", - onPressed: () async { - await showLoading( - whileFuture: - downloadMonkey(receivingAddress, false), - context: context, - isDesktop: Util.isDesktop, - message: "Saving MonKey svg", - ); - }, - ), - const SizedBox(height: 12), - SecondaryButton( - label: "Download as PNG", - onPressed: () async { - await showLoading( - whileFuture: - downloadMonkey(receivingAddress, true), - context: context, - isDesktop: Util.isDesktop, - message: "Downloading MonKey png", - ); - }, - ), - ], - ), - ), - // child, - ], - ), - child: Column( - children: [ - const Spacer( - flex: 4, - ), - Center( - child: Column( - children: [ - Opacity( - opacity: 0.2, - child: SvgPicture.file( - File( - ref.watch(coinIconProvider(coin)), - ), - width: 200, - height: 200, - ), - ), - const SizedBox( - height: 40, - ), - Text( - "You do not have a MonKey yet. \nFetch yours now!", - style: STextStyles.smallMed14(context).copyWith( - color: Theme.of(context) - .extension()! - .textDark3, - ), - textAlign: TextAlign.center, - ), - ], - ), - ), - const Spacer( - flex: 6, - ), - Padding( - padding: const EdgeInsets.all(16.0), - child: PrimaryButton( - label: "Fetch MonKey", + Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + children: [ + SecondaryButton( + label: "Save as SVG", onPressed: () async { - final future = Future.wait([ - getMonkeyImage(receivingAddress), - Future.delayed(const Duration(seconds: 2)), - ]); - await showLoading( - whileFuture: future, + whileFuture: + downloadMonkey(receivingAddress, false), context: context, isDesktop: Util.isDesktop, - message: "Fetching MonKey", - subMessage: "We are fetching your MonKey", + message: "Saving MonKey svg", ); - - // if (isDesktop) { - // Navigator.of(context).popUntil( - // ModalRoute.withName( - // DesktopWalletView.routeName), - // ); - // } else { - // Navigator.of(context).popUntil( - // ModalRoute.withName(WalletView.routeName), - // ); - // } }, ), - ), - ], + const SizedBox(height: 12), + SecondaryButton( + label: "Download as PNG", + onPressed: () async { + await showLoading( + whileFuture: + downloadMonkey(receivingAddress, true), + context: context, + isDesktop: Util.isDesktop, + message: "Downloading MonKey png", + ); + }, + ), + ], + ), ), - ), + // child, + ], + ), + child: Column( + children: [ + isDesktop + ? const SizedBox( + height: 100, + ) + : const Spacer( + flex: 4, + ), + Center( + child: Column( + children: [ + Opacity( + opacity: 0.2, + child: SvgPicture.file( + File( + ref.watch(coinIconProvider(coin)), + ), + width: 200, + height: 200, + ), + ), + const SizedBox( + height: 70, + ), + Text( + "You do not have a MonKey yet. \nFetch yours now!", + style: STextStyles.smallMed14(context).copyWith( + color: Theme.of(context) + .extension()! + .textDark3, + ), + textAlign: TextAlign.center, + ), + ], + ), + ), + isDesktop + ? const SizedBox( + height: 50, + ) + : const Spacer( + flex: 6, + ), + Padding( + padding: const EdgeInsets.all(16.0), + child: PrimaryButton( + label: "Fetch MonKey", + onPressed: () async { + final future = Future.wait([ + getMonkeyImage(receivingAddress), + Future.delayed(const Duration(seconds: 2)), + ]); + + await showLoading( + whileFuture: future, + context: context, + isDesktop: Util.isDesktop, + message: "Fetching MonKey", + subMessage: "We are fetching your MonKey", + ); + + // if (isDesktop) { + // Navigator.of(context).popUntil( + // ModalRoute.withName( + // DesktopWalletView.routeName), + // ); + // } else { + // Navigator.of(context).popUntil( + // ModalRoute.withName(WalletView.routeName), + // ); + // } + }, + ), + ), + ], ), ), ), - ], + ), ), ); } From 2f5a18b61574298b223d5437d19dd4b7bc043db2 Mon Sep 17 00:00:00 2001 From: julian Date: Wed, 26 Jul 2023 16:10:27 -0600 Subject: [PATCH 161/169] merge conflict clean things --- lib/pages/monkey/monkey_view.dart | 50 +++++++++++++++---------------- 1 file changed, 25 insertions(+), 25 deletions(-) diff --git a/lib/pages/monkey/monkey_view.dart b/lib/pages/monkey/monkey_view.dart index c34d4ed95..1dff1fd4d 100644 --- a/lib/pages/monkey/monkey_view.dart +++ b/lib/pages/monkey/monkey_view.dart @@ -241,9 +241,10 @@ class _MonkeyViewState extends ConsumerState { width: 15, ), SvgPicture.asset(Assets.svg.monkey), - const SizedBox( - width: 12, - ),Text( + const SizedBox( + width: 12, + ), + Text( "MonKey", style: STextStyles.navBarTitle(context), ), @@ -253,8 +254,8 @@ class _MonkeyViewState extends ConsumerState { trailing: Padding( padding: const EdgeInsets.all(8.0), child: MouseRegion( - cursor: SystemMouseCursors.click, - child: GestureDetector( + cursor: SystemMouseCursors.click, + child: GestureDetector( onTap: () { showDialog( context: context, @@ -268,28 +269,28 @@ class _MonkeyViewState extends ConsumerState { ); }); }, - child: Row( - children: [ - SvgPicture.asset( - Assets.svg.circleQuestion, + child: Row( + children: [ + SvgPicture.asset( + Assets.svg.circleQuestion, + color: Colors.blue[800], + ), + const SizedBox( + width: 6, + ), + Padding( + padding: const EdgeInsets.only(bottom: 8.0), + child: Text( + "What is MonKey?", + style: STextStyles.desktopTextSmall(context).copyWith( color: Colors.blue[800], ), - SizedBox( - width: 6, - ), - Padding( - padding: const EdgeInsets.only(bottom: 8.0), - child: Text( - "What is MonKey?", - style: STextStyles.desktopTextSmall(context) - .copyWith( - color: Colors.blue[800], - ), - ), - ), - ], + ), ), - ),), + ], + ), + ), + ), ), useSpacers: false, isCompactHeight: true, @@ -315,7 +316,6 @@ class _MonkeyViewState extends ConsumerState { child: AppBarIconButton( icon: SvgPicture.asset( Assets.svg.circleQuestion, - ), onPressed: () { showDialog( From d8e6f3cac691443f4167205a35b8747450282947 Mon Sep 17 00:00:00 2001 From: julian Date: Wed, 26 Jul 2023 16:15:29 -0600 Subject: [PATCH 162/169] fix mobile nav error --- lib/pages/wallet_view/wallet_view.dart | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/lib/pages/wallet_view/wallet_view.dart b/lib/pages/wallet_view/wallet_view.dart index b2d5de8e6..b3552807f 100644 --- a/lib/pages/wallet_view/wallet_view.dart +++ b/lib/pages/wallet_view/wallet_view.dart @@ -938,10 +938,7 @@ class _WalletViewState extends ConsumerState { onTap: () { Navigator.of(context).pushNamed( MonkeyView.routeName, - arguments: Tuple2( - widget.walletId, - widget.managerProvider, - ), + arguments: widget.walletId, ); }), if (ref.watch( From 8754142694e81e958b25c8e010cfb36661db434b Mon Sep 17 00:00:00 2001 From: ryleedavis Date: Wed, 26 Jul 2023 16:21:20 -0600 Subject: [PATCH 163/169] android file path --- lib/pages/monkey/monkey_view.dart | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/pages/monkey/monkey_view.dart b/lib/pages/monkey/monkey_view.dart index 1dff1fd4d..554d6d9e0 100644 --- a/lib/pages/monkey/monkey_view.dart +++ b/lib/pages/monkey/monkey_view.dart @@ -113,6 +113,10 @@ class _MonkeyViewState extends ConsumerState { Future getDocsDir() async { try { + if (Platform.isAndroid) { + return Directory("/storage/emulated/0/"); + } + return await getApplicationDocumentsDirectory(); } catch (_) { return null; From 04b9cddb1994b23208b108f0f73f130298b1c5ab Mon Sep 17 00:00:00 2001 From: ryleedavis Date: Wed, 26 Jul 2023 16:23:34 -0600 Subject: [PATCH 164/169] fixed path --- lib/pages/monkey/monkey_view.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pages/monkey/monkey_view.dart b/lib/pages/monkey/monkey_view.dart index 554d6d9e0..af2fd91fd 100644 --- a/lib/pages/monkey/monkey_view.dart +++ b/lib/pages/monkey/monkey_view.dart @@ -114,7 +114,7 @@ class _MonkeyViewState extends ConsumerState { Future getDocsDir() async { try { if (Platform.isAndroid) { - return Directory("/storage/emulated/0/"); + return Directory("/storage/emulated/0/Documents"); } return await getApplicationDocumentsDirectory(); From 7390c498c732f336b7c047827e8c17eb616d1f64 Mon Sep 17 00:00:00 2001 From: julian Date: Wed, 26 Jul 2023 16:29:09 -0600 Subject: [PATCH 165/169] set state on download --- lib/pages/monkey/monkey_view.dart | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/lib/pages/monkey/monkey_view.dart b/lib/pages/monkey/monkey_view.dart index af2fd91fd..2ad4b18eb 100644 --- a/lib/pages/monkey/monkey_view.dart +++ b/lib/pages/monkey/monkey_view.dart @@ -42,6 +42,7 @@ class MonkeyView extends ConsumerStatefulWidget { class _MonkeyViewState extends ConsumerState { late final String walletId; + List? imageBytes; String receivingAddress = ""; @@ -205,8 +206,7 @@ class _MonkeyViewState extends ConsumerState { final bool isDesktop = Util.isDesktop; - List? imageBytes; - imageBytes = (manager.wallet as BananoWallet).getMonkeyImageBytes(); + imageBytes ??= (manager.wallet as BananoWallet).getMonkeyImageBytes(); //edit for desktop return Background( @@ -360,7 +360,7 @@ class _MonkeyViewState extends ConsumerState { SizedBox( width: 300, height: 300, - child: SvgPicture.memory(Uint8List.fromList(imageBytes)), + child: SvgPicture.memory(Uint8List.fromList(imageBytes!)), ), isDesktop ? const SizedBox( @@ -466,6 +466,13 @@ class _MonkeyViewState extends ConsumerState { subMessage: "We are fetching your MonKey", ); + imageBytes = (manager.wallet as BananoWallet) + .getMonkeyImageBytes(); + + if (imageBytes != null) { + setState(() {}); + } + // if (isDesktop) { // Navigator.of(context).popUntil( // ModalRoute.withName( From e52072649594896012f797805848f985d355b3ec Mon Sep 17 00:00:00 2001 From: julian Date: Wed, 26 Jul 2023 14:50:38 -0600 Subject: [PATCH 166/169] fix the fix --- lib/db/hive/db.dart | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/lib/db/hive/db.dart b/lib/db/hive/db.dart index 9d3f73abd..2fb82c806 100644 --- a/lib/db/hive/db.dart +++ b/lib/db/hive/db.dart @@ -276,10 +276,7 @@ class DB { Future deleteAll({required String boxName}) async { await mutex.protect(() async { - Box box = Hive.box(boxName); - if (!box.isOpen) { - box = await Hive.openBox(boxName); - } + final box = await Hive.openBox(boxName); await box.clear(); }); } From 4d870963245e00e6486129c94b522e58617e2d72 Mon Sep 17 00:00:00 2001 From: julian Date: Wed, 26 Jul 2023 17:01:24 -0600 Subject: [PATCH 167/169] temp disable some ordinals stuff --- lib/pages/ordinals/ordinals_view.dart | 177 +++++++++--------- .../desktop_ordinal_details_view.dart | 35 ++-- .../ordinals/desktop_ordinals_view.dart | 113 +++++------ 3 files changed, 160 insertions(+), 165 deletions(-) diff --git a/lib/pages/ordinals/ordinals_view.dart b/lib/pages/ordinals/ordinals_view.dart index 059c81c41..45b30655a 100644 --- a/lib/pages/ordinals/ordinals_view.dart +++ b/lib/pages/ordinals/ordinals_view.dart @@ -11,21 +11,15 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/svg.dart'; -import 'package:stackwallet/pages/ordinals/ordinals_filter_view.dart'; import 'package:stackwallet/pages/ordinals/widgets/ordinals_list.dart'; import 'package:stackwallet/providers/global/wallets_provider.dart'; import 'package:stackwallet/services/mixins/ordinals_interface.dart'; import 'package:stackwallet/themes/stack_colors.dart'; import 'package:stackwallet/utilities/assets.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/background.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 OrdinalsView extends ConsumerStatefulWidget { const OrdinalsView({ @@ -93,38 +87,39 @@ class _OrdinalsViewState extends ConsumerState { onPressed: () async { // show loading for a minimum of 2 seconds on refreshing await showLoading( - whileFuture: Future.wait([ - Future.delayed(const Duration(seconds: 2)), - (ref - .read(walletsChangeNotifierProvider) - .getManager(widget.walletId) - .wallet as OrdinalsInterface) - .refreshInscriptions() - ]), - context: context, - message: "Refreshing..."); - }, - ), - ), - AspectRatio( - aspectRatio: 1, - child: AppBarIconButton( - size: 36, - icon: SvgPicture.asset( - Assets.svg.filter, - width: 20, - height: 20, - color: Theme.of(context) - .extension()! - .topNavIconPrimary, - ), - onPressed: () { - Navigator.of(context).pushNamed( - OrdinalsFilterView.routeName, + whileFuture: Future.wait([ + Future.delayed(const Duration(seconds: 2)), + (ref + .read(walletsChangeNotifierProvider) + .getManager(widget.walletId) + .wallet as OrdinalsInterface) + .refreshInscriptions() + ]), + context: context, + message: "Refreshing...", ); }, ), ), + // AspectRatio( + // aspectRatio: 1, + // child: AppBarIconButton( + // size: 36, + // icon: SvgPicture.asset( + // Assets.svg.filter, + // width: 20, + // height: 20, + // color: Theme.of(context) + // .extension()! + // .topNavIconPrimary, + // ), + // onPressed: () { + // Navigator.of(context).pushNamed( + // OrdinalsFilterView.routeName, + // ); + // }, + // ), + // ), ], ), body: Padding( @@ -135,63 +130,63 @@ class _OrdinalsViewState extends ConsumerState { ), child: Column( children: [ - ClipRRect( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), - child: TextField( - autocorrect: Util.isDesktop ? false : true, - enableSuggestions: Util.isDesktop ? false : true, - controller: searchController, - focusNode: searchFocus, - onChanged: (value) { - setState(() { - _searchTerm = value; - }); - }, - style: STextStyles.field(context), - decoration: standardInputDecoration( - "Search", - searchFocus, - context, - ).copyWith( - prefixIcon: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 10, - vertical: 16, - ), - child: SvgPicture.asset( - Assets.svg.search, - width: 16, - height: 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 = ""; - _searchTerm = ""; - }); - }, - ), - ], - ), - ), - ) - : null, - ), - ), - ), - const SizedBox( - height: 16, - ), + // ClipRRect( + // borderRadius: BorderRadius.circular( + // Constants.size.circularBorderRadius, + // ), + // child: TextField( + // autocorrect: Util.isDesktop ? false : true, + // enableSuggestions: Util.isDesktop ? false : true, + // controller: searchController, + // focusNode: searchFocus, + // onChanged: (value) { + // setState(() { + // _searchTerm = value; + // }); + // }, + // style: STextStyles.field(context), + // decoration: standardInputDecoration( + // "Search", + // searchFocus, + // context, + // ).copyWith( + // prefixIcon: Padding( + // padding: const EdgeInsets.symmetric( + // horizontal: 10, + // vertical: 16, + // ), + // child: SvgPicture.asset( + // Assets.svg.search, + // width: 16, + // height: 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 = ""; + // _searchTerm = ""; + // }); + // }, + // ), + // ], + // ), + // ), + // ) + // : null, + // ), + // ), + // ), + // const SizedBox( + // height: 16, + // ), Expanded( child: OrdinalsList( walletId: widget.walletId, diff --git a/lib/pages_desktop_specific/ordinals/desktop_ordinal_details_view.dart b/lib/pages_desktop_specific/ordinals/desktop_ordinal_details_view.dart index cb76e036d..2f89b7cd6 100644 --- a/lib/pages_desktop_specific/ordinals/desktop_ordinal_details_view.dart +++ b/lib/pages_desktop_specific/ordinals/desktop_ordinal_details_view.dart @@ -19,7 +19,6 @@ import 'package:stackwallet/utilities/text_styles.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/secondary_button.dart'; import 'package:stackwallet/widgets/rounded_white_container.dart'; class DesktopOrdinalDetailsView extends ConsumerStatefulWidget { @@ -175,23 +174,23 @@ class _DesktopOrdinalDetailsViewState // const SizedBox( // width: 16, // ), - SecondaryButton( - width: 150, - label: "Download", - icon: SvgPicture.asset( - Assets.svg.arrowDown, - width: 13, - height: 18, - color: Theme.of(context) - .extension()! - .buttonTextSecondary, - ), - buttonHeight: ButtonHeight.l, - iconSpacing: 8, - onPressed: () { - // TODO: save and download image to device - }, - ), + // SecondaryButton( + // width: 150, + // label: "Download", + // icon: SvgPicture.asset( + // Assets.svg.arrowDown, + // width: 13, + // height: 18, + // color: Theme.of(context) + // .extension()! + // .buttonTextSecondary, + // ), + // buttonHeight: ButtonHeight.l, + // iconSpacing: 8, + // onPressed: () { + // // TODO: save and download image to device + // }, + // ), ], ), ), diff --git a/lib/pages_desktop_specific/ordinals/desktop_ordinals_view.dart b/lib/pages_desktop_specific/ordinals/desktop_ordinals_view.dart index 414ece75d..ec2748560 100644 --- a/lib/pages_desktop_specific/ordinals/desktop_ordinals_view.dart +++ b/lib/pages_desktop_specific/ordinals/desktop_ordinals_view.dart @@ -121,62 +121,63 @@ class _DesktopOrdinals extends ConsumerState { children: [ Row( children: [ - Expanded( - child: ClipRRect( - borderRadius: BorderRadius.circular( - Constants.size.circularBorderRadius, - ), - child: TextField( - autocorrect: Util.isDesktop ? false : true, - enableSuggestions: Util.isDesktop ? false : true, - controller: searchController, - focusNode: searchFocusNode, - onChanged: (value) { - setState(() { - _searchTerm = value; - }); - }, - style: STextStyles.field(context), - decoration: standardInputDecoration( - "Search", - searchFocusNode, - context, - ).copyWith( - prefixIcon: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 10, - vertical: 20, - ), - child: SvgPicture.asset( - Assets.svg.search, - width: 16, - height: 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 = ""; - _searchTerm = ""; - }); - }, - ), - ], - ), - ), - ) - : null, - ), - ), - ), - ), + const Spacer(), + // Expanded( + // child: ClipRRect( + // borderRadius: BorderRadius.circular( + // Constants.size.circularBorderRadius, + // ), + // child: TextField( + // autocorrect: Util.isDesktop ? false : true, + // enableSuggestions: Util.isDesktop ? false : true, + // controller: searchController, + // focusNode: searchFocusNode, + // onChanged: (value) { + // setState(() { + // _searchTerm = value; + // }); + // }, + // style: STextStyles.field(context), + // decoration: standardInputDecoration( + // "Search", + // searchFocusNode, + // context, + // ).copyWith( + // prefixIcon: Padding( + // padding: const EdgeInsets.symmetric( + // horizontal: 10, + // vertical: 20, + // ), + // child: SvgPicture.asset( + // Assets.svg.search, + // width: 16, + // height: 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 = ""; + // _searchTerm = ""; + // }); + // }, + // ), + // ], + // ), + // ), + // ) + // : null, + // ), + // ), + // ), + // ), // const SizedBox( // width: 16, // ), From f51cf2e395772a630ffd5d70a04f2dfbec274ac2 Mon Sep 17 00:00:00 2001 From: julian Date: Wed, 26 Jul 2023 17:15:11 -0600 Subject: [PATCH 168/169] extra temp check on firo --- lib/services/coins/firo/firo_wallet.dart | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/lib/services/coins/firo/firo_wallet.dart b/lib/services/coins/firo/firo_wallet.dart index 079b96ff4..573992318 100644 --- a/lib/services/coins/firo/firo_wallet.dart +++ b/lib/services/coins/firo/firo_wallet.dart @@ -1169,6 +1169,11 @@ class FiroWallet extends CoinServiceAPI required Amount amount, Map? args, }) async { + if (amount.raw > BigInt.from(MINT_LIMIT)) { + throw Exception( + "Lelantus sends of more than 5001 are currently disabled"); + } + try { // check for send all bool isSendAll = false; @@ -2510,6 +2515,11 @@ class FiroWallet extends CoinServiceAPI } Future>> createMintsFromAmount(int total) async { + if (total > MINT_LIMIT) { + throw Exception( + "Lelantus mints of more than 5001 are currently disabled"); + } + int tmpTotal = total; int counter = 0; final lastUsedIndex = await db.getHighestUsedMintIndex(walletId: walletId); From 703863cab83a2cc7ae784ec0f726035c5f711cf3 Mon Sep 17 00:00:00 2001 From: julian Date: Wed, 26 Jul 2023 17:21:36 -0600 Subject: [PATCH 169/169] reorder firo balance sheet on mobile --- .../wallet_balance_toggle_sheet.dart | 52 ++++++++++--------- 1 file changed, 27 insertions(+), 25 deletions(-) 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 4b2ee7a3b..029cd7aa3 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 @@ -121,7 +121,8 @@ class WalletBalanceToggleSheet extends ConsumerWidget { height: 24, ), BalanceSelector( - title: "Available balance", + title: + "Available${balanceSecondary != null ? " public" : ""} balance", coin: coin, balance: balance.spendable, onPressed: () { @@ -141,6 +142,31 @@ class WalletBalanceToggleSheet extends ConsumerWidget { value: _BalanceType.available, groupValue: _bal, ), + const SizedBox( + height: 12, + ), + BalanceSelector( + title: + "Full${balanceSecondary != null ? " public" : ""} balance", + coin: coin, + balance: balance.total, + onPressed: () { + ref.read(walletBalanceToggleStateProvider.state).state = + WalletBalanceToggleState.full; + ref.read(publicPrivateBalanceStateProvider.state).state = + "Public"; + Navigator.of(context).pop(); + }, + onChanged: (_) { + ref.read(walletBalanceToggleStateProvider.state).state = + WalletBalanceToggleState.full; + ref.read(publicPrivateBalanceStateProvider.state).state = + "Public"; + Navigator.of(context).pop(); + }, + value: _BalanceType.full, + groupValue: _bal, + ), if (balanceSecondary != null) const SizedBox( height: 12, @@ -167,30 +193,6 @@ class WalletBalanceToggleSheet extends ConsumerWidget { value: _BalanceType.privateAvailable, groupValue: _bal, ), - const SizedBox( - height: 12, - ), - BalanceSelector( - title: "Full balance", - coin: coin, - balance: balance.total, - onPressed: () { - ref.read(walletBalanceToggleStateProvider.state).state = - WalletBalanceToggleState.full; - ref.read(publicPrivateBalanceStateProvider.state).state = - "Public"; - Navigator.of(context).pop(); - }, - onChanged: (_) { - ref.read(walletBalanceToggleStateProvider.state).state = - WalletBalanceToggleState.full; - ref.read(publicPrivateBalanceStateProvider.state).state = - "Public"; - Navigator.of(context).pop(); - }, - value: _BalanceType.full, - groupValue: _bal, - ), if (balanceSecondary != null) const SizedBox( height: 12,