From 21e95e42d37882cf02b0cec4e1575ffbf930c4fc Mon Sep 17 00:00:00 2001 From: Peter Date: Wed, 31 Jan 2024 21:36:56 +0000 Subject: [PATCH 1/6] Add cktap_protocol library - Allows us to communicate with Satscards --- README.md | 8 +- android/app/build.gradle | 1 + .../Breez/BreezLib/NativeMethods.swift | 4 + ios/Podfile.lock | 46 +- ios/Runner.xcodeproj/project.pbxproj | 2 + ios/Runner/Info.plist | 610 +++++++++--------- ios/Runner/Runner.entitlements | 2 +- lib/routes/charge/pos_payment_dialog.dart | 2 +- lib/services/nfc.dart | 85 ++- pubspec.lock | 129 ++-- pubspec.yaml | 23 +- 11 files changed, 516 insertions(+), 396 deletions(-) diff --git a/README.md b/README.md index 738539df5..e06a0c992 100644 --- a/README.md +++ b/README.md @@ -89,7 +89,13 @@ To learn more about it, please read [Introducing Breez](https://doc.breez.techno ### Prerequisites -Make sure you have Flutter 3 installed on your system before continuing the setup process. +Make sure you have the following installed: +1. Flutter 3.7.12, newer versions won't work. +2. [cktap-protocol-flutter](https://github.com/PeteClubSeven/cktap-protocol-flutter/tree/release/breez) dependencies + - macOS 13 or newer + - `brew install cmake` + - macOS 12 or older + - `brew install cmake coreutils` ### Setting up for Android diff --git a/android/app/build.gradle b/android/app/build.gradle index ddbccb1a4..5cddfc3e5 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -16,6 +16,7 @@ apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" android { compileSdkVersion 33 + ndkVersion "21.4.7075529" def keystoreProperties = new Properties() def keystorePropertiesFile = rootProject.file('key.properties') diff --git a/ios/Plugins/Breez/BreezLib/NativeMethods.swift b/ios/Plugins/Breez/BreezLib/NativeMethods.swift index d3c183742..de77b6331 100644 --- a/ios/Plugins/Breez/BreezLib/NativeMethods.swift +++ b/ios/Plugins/Breez/BreezLib/NativeMethods.swift @@ -98,6 +98,10 @@ fileprivate let calls : [String:BindingExecutor] = [ "resetClosedChannelChainInfo": SingleArgBindingExecutor(f: BindingsResetClosedChannelChainInfo), "setNonBlockingUnconfirmedSwaps": EmptyArgsBindingExecutor(f: BindingsSetNonBlockingUnconfirmedSwaps), + "getAddressInfo": SingleArgBindingExecutor(f: BindingsGetAddressInfo), + "createSlotSweepTransactions": SingleArgBindingExecutor(f: BindingsCreateSlotSweepTransactions), + "signSlotSweepTransaction": SingleArgBindingExecutor(f: BindingsSignSlotSweepTransaction), + //jobs // FOUNDATION_EXPORT id BindingsNewClosedChannelsJob(NSString* workingDir, NSError** error); // FOUNDATION_EXPORT id BindingsNewSyncJob(NSString* workingDir, NSError** error); diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 7a9f8134a..c0dadbfb2 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -11,6 +11,8 @@ PODS: - Flutter - audio_session (0.0.1): - Flutter + - cktap_protocol (0.0.1): + - Flutter - clipboard_watcher (0.0.1): - Flutter - connectivity_plus (0.0.1): @@ -70,27 +72,27 @@ PODS: - Firebase/Messaging (10.18.0): - Firebase/CoreOnly - FirebaseMessaging (~> 10.18.0) - - firebase_core (2.24.0): + - firebase_core (2.24.2): - Firebase/CoreOnly (= 10.18.0) - Flutter - - firebase_database (10.3.6): + - firebase_database (10.4.0): - Firebase/Database (= 10.18.0) - firebase_core - Flutter - - firebase_dynamic_links (5.4.6): + - firebase_dynamic_links (5.4.8): - Firebase/DynamicLinks (= 10.18.0) - firebase_core - Flutter - - firebase_messaging (14.7.6): + - firebase_messaging (14.7.10): - Firebase/Messaging (= 10.18.0) - firebase_core - Flutter - - FirebaseAppCheckInterop (10.18.0) + - FirebaseAppCheckInterop (10.20.0) - FirebaseCore (10.18.0): - FirebaseCoreInternal (~> 10.0) - GoogleUtilities/Environment (~> 7.12) - GoogleUtilities/Logger (~> 7.12) - - FirebaseCoreInternal (10.18.0): + - FirebaseCoreInternal (10.20.0): - "GoogleUtilities/NSData+zlib (~> 7.8)" - FirebaseDatabase (10.18.0): - FirebaseAppCheckInterop (~> 10.17) @@ -99,7 +101,7 @@ PODS: - leveldb-library (~> 1.22) - FirebaseDynamicLinks (10.18.0): - FirebaseCore (~> 10.0) - - FirebaseInstallations (10.18.0): + - FirebaseInstallations (10.20.0): - FirebaseCore (~> 10.0) - GoogleUtilities/Environment (~> 7.8) - GoogleUtilities/UserDefaults (~> 7.8) @@ -113,7 +115,7 @@ PODS: - GoogleUtilities/Reachability (~> 7.8) - GoogleUtilities/UserDefaults (~> 7.8) - nanopb (< 2.30910.0, >= 2.30908.0) - - FirebaseSharedSwift (10.18.0) + - FirebaseSharedSwift (10.20.0) - Flutter (1.0.0) - flutter_downloader (0.0.1): - Flutter @@ -227,9 +229,9 @@ PODS: - Flutter - PromisesObjC (2.3.1) - ReachabilitySwift (5.0.0) - - SDWebImage (5.18.5): - - SDWebImage/Core (= 5.18.5) - - SDWebImage/Core (5.18.5) + - SDWebImage (5.18.10): + - SDWebImage/Core (= 5.18.10) + - SDWebImage/Core (5.18.10) - share_plus (0.0.1): - Flutter - shared_preferences_foundation (0.0.1): @@ -254,6 +256,7 @@ DEPENDENCIES: - app_settings (from `.symlinks/plugins/app_settings/ios`) - audio_service (from `.symlinks/plugins/audio_service/ios`) - audio_session (from `.symlinks/plugins/audio_session/ios`) + - cktap_protocol (from `.symlinks/plugins/cktap_protocol/ios`) - clipboard_watcher (from `.symlinks/plugins/clipboard_watcher/ios`) - connectivity_plus (from `.symlinks/plugins/connectivity_plus/ios`) - device_info_plus (from `.symlinks/plugins/device_info_plus/ios`) @@ -333,6 +336,8 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/audio_service/ios" audio_session: :path: ".symlinks/plugins/audio_session/ios" + cktap_protocol: + :path: ".symlinks/plugins/cktap_protocol/ios" clipboard_watcher: :path: ".symlinks/plugins/clipboard_watcher/ios" connectivity_plus: @@ -407,6 +412,7 @@ SPEC CHECKSUMS: AppAuth: 3bb1d1cd9340bd09f5ed189fb00b1cc28e1e8570 audio_service: f509d65da41b9521a61f1c404dd58651f265a567 audio_session: 4f3e461722055d21515cf3261b64c973c062f345 + cktap_protocol: f0cb9c355c1c41d44b9daef7bf5ab18ba7ea6346 clipboard_watcher: 86fb70421aca6f4944e0591a8292605da7784666 connectivity_plus: 07c49e96d7fc92bc9920617b83238c4d178b446a device_info_plus: c6fb39579d0f423935b0c9ce7ee2f44b71b9fce6 @@ -416,18 +422,18 @@ SPEC CHECKSUMS: ffmpeg_kit_flutter_https_gpl: 066ec3b6a89759f1ffb84860e75534c3d3a1b172 file_picker: ce3938a0df3cc1ef404671531facef740d03f920 Firebase: 414ad272f8d02dfbf12662a9d43f4bba9bec2a06 - firebase_core: f802c5c1f6caff9b8d38b591a36e7b25f8878936 - firebase_database: 18b34a683c878f836ac5a403ce29242065d5acd0 - firebase_dynamic_links: 212953c972f7e952c853f69896dc4da0fa5cb06e - firebase_messaging: ca2fc59ddd52ef032d80f1a717d12eae8fa0e994 - FirebaseAppCheckInterop: 3cd914842ba46f4304050874cd284de82f154ffd + firebase_core: 0af4a2b24f62071f9bf283691c0ee41556dcb3f5 + firebase_database: 5d420ac53c48f3394445c8b83c530a42d149c3d4 + firebase_dynamic_links: b626a11f5eb02033981ae377377c3f297eb4c1b0 + firebase_messaging: 90e8a6db84b6e1e876cebce4f30f01dc495e7014 + FirebaseAppCheckInterop: e81bdb1cdb82f8e0cef353ba5018a8402682032c FirebaseCore: 2322423314d92f946219c8791674d2f3345b598f - FirebaseCoreInternal: 8eb002e564b533bdcf1ba011f33f2b5c10e2ed4a + FirebaseCoreInternal: efeeb171ac02d623bdaefe121539939821e10811 FirebaseDatabase: ac770bf7525ff0340b105166037036c0e46c2c7e FirebaseDynamicLinks: c37307441c53838d66a9650dabca9e0459502527 - FirebaseInstallations: e842042ec6ac1fd2e37d7706363ebe7f662afea4 + FirebaseInstallations: 558b1da7d65afeb996fd5c814332f013234ece4e FirebaseMessaging: 9bc34a98d2e0237e1b121915120d4d48ddcf301e - FirebaseSharedSwift: 62e248642c0582324d0390706cadd314687c116b + FirebaseSharedSwift: 2fbf73618288b7a36b2014b957745dcdd781389e Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854 flutter_downloader: b7301ae057deadd4b1650dc7c05375f10ff12c39 flutter_keyboard_visibility: 0339d06371254c3eb25eeb90ba8d17dca8f9c069 @@ -462,7 +468,7 @@ SPEC CHECKSUMS: printing: 233e1b73bd1f4a05615548e9b5a324c98588640b PromisesObjC: c50d2056b5253dadbd6c2bea79b0674bd5a52fa4 ReachabilitySwift: 985039c6f7b23a1da463388634119492ff86c825 - SDWebImage: 7ac2b7ddc5e8484c79aa90fc4e30b149d6a2c88f + SDWebImage: fc8f2d48bbfd72ef39d70e981bd24a3f3be53fec share_plus: c3fef564749587fc939ef86ffb283ceac0baf9f5 shared_preferences_foundation: 5b919d13b803cadd15ed2dc053125c68730e5126 sqflite: 31f7eba61e3074736dff8807a9b41581e4f7f15a diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 850f453d7..30eec30cf 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -333,6 +333,7 @@ "${BUILT_PRODUCTS_DIR}/app_settings/app_settings.framework", "${BUILT_PRODUCTS_DIR}/audio_service/audio_service.framework", "${BUILT_PRODUCTS_DIR}/audio_session/audio_session.framework", + "${BUILT_PRODUCTS_DIR}/cktap_protocol/cktap_protocol.framework", "${BUILT_PRODUCTS_DIR}/clipboard_watcher/clipboard_watcher.framework", "${BUILT_PRODUCTS_DIR}/connectivity_plus/connectivity_plus.framework", "${BUILT_PRODUCTS_DIR}/device_info_plus/device_info_plus.framework", @@ -398,6 +399,7 @@ "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/app_settings.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/audio_service.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/audio_session.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/cktap_protocol.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/clipboard_watcher.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/connectivity_plus.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/device_info_plus.framework", diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist index 77242efe1..76ba66e4e 100644 --- a/ios/Runner/Info.plist +++ b/ios/Runner/Info.plist @@ -2,8 +2,18 @@ - NFCReaderUsageDescription - NFC is used to read payment requests + CADisableMinimumFrameDurationOnPhone + + CFBundleDevelopmentRegion + en + CFBundleDisplayName + Breez + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 CFBundleLocalizations en @@ -19,16 +29,6 @@ sk sv - CFBundleDevelopmentRegion - en - CFBundleDisplayName - Breez - CFBundleExecutable - $(EXECUTABLE_NAME) - CFBundleIdentifier - $(PRODUCT_BUNDLE_IDENTIFIER) - CFBundleInfoDictionaryVersion - 6.0 CFBundleName breez CFBundlePackageType @@ -79,6 +79,8 @@ LSRequiresIPhoneOS + NFCReaderUsageDescription + NFC is used to read payment requests NSAppTransportSecurity NSAllowsArbitraryLoads @@ -113,6 +115,8 @@ Microphone is used in podcasts NSPhotoLibraryUsageDescription Required for selecting user avatar image. + UIApplicationSupportsIndirectInputEvents + UIBackgroundModes audio @@ -140,298 +144,294 @@ UIViewControllerBasedStatusBarAppearance - com.apple.developer.nfc.readersession.felica.systemcodes - - 12FC - 0003 - 04D1 - 8008 - 80DE - 865E - 8592 - 8B5D - 8FC1 - FE00 - - com.apple.developer.nfc.readersession.iso7816.select-identifiers - - A0000002310100000000000000000000 - A0000002310200000000000000000000 - A0000002480300000000000000000000 - A00000006510 - A0000000651010 - 315041592E5359532E4444463031 - 325041592E5359532E4444463031 - 44464D46412E44466172653234313031 - A00000000101 - A000000003000000 - A00000000300037561 - A00000000305076010 - A0000000031010 - A000000003101001 - A000000003101002 - A0000000032010 - A0000000032020 - A0000000033010 - A0000000034010 - A0000000035010 - A000000003534441 - A0000000035350 - A000000003535041 - A0000000036010 - A0000000036020 - A0000000038002 - A0000000038010 - A0000000039010 - A000000003999910 - A0000000040000 - A00000000401 - A0000000041010 - A00000000410101213 - A00000000410101215 - A0000000041010BB5449435301 - A0000000042010 - A0000000042203 - A0000000043010 - A0000000043060 - A000000004306001 - A0000000044010 - A0000000045010 - A0000000045555 - A0000000046000 - A0000000048002 - A0000000049999 - A0000000050001 - A0000000050002 - A0000000090001FF44FF1289 - A0000000101030 - A00000001800 - A0000000181001 - A000000018434D - A000000018434D00 - A00000002401 - A000000025 - A0000000250000 - A00000002501 - A000000025010104 - A000000025010402 - A000000025010701 - A000000025010801 - A0000000291010 - A00000002945087510100000 - A00000002949034010100001 - A00000002949282010100000 - A000000029564182 - A00000003029057000AD13100101FF - A0000000308000000000280101 - A0000000421010 - A0000000422010 - A0000000423010 - A0000000424010 - A0000000425010 - A0000000426010 - A00000005945430100 - A000000063504B43532D3135 - A0000000635741502D57494D - A00000006510 - A0000000651010 - A00000006900 - A000000077010000021000000000003B - A0000000790100 - A0000000790101 - A0000000790102 - A00000007901F0 - A00000007901F1 - A00000007901F2 - A0000000790200 - A0000000790201 - A00000007902FB - A00000007902FD - A00000007902FE - A0000000790300 - A0000000791201 - A0000000791202 - A0000000871002FF49FF0589 - A00000008810200105C100 - A000000088102201034221 - A000000088102201034321 - A0000000960200 - A000000098 - A0000000980840 - A0000000980848 - A0000001110101 - A0000001160300 - A0000001166010 - A0000001166030 - A0000001169000 - A000000116A001 - A000000116DB00 - A000000118010000 - A000000118020000 - A000000118030000 - A000000118040000 - A0000001184543 - A000000118454E - A0000001211010 - A0000001320001 - A0000001408001 - A0000001410001 - A0000001510000 - A00000015153504341534400 - A0000001523010 - A0000001524010 - A0000001544442 - A0000001570010 - A0000001570020 - A0000001570021 - A0000001570022 - A0000001570023 - A0000001570030 - A0000001570031 - A0000001570040 - A0000001570050 - A0000001570051 - A0000001570100 - A0000001570104 - A0000001570109 - A000000157010A - A000000157010B - A000000157010C - A000000157010D - A0000001574443 - A0000001574444 - A000000167413000FF - A000000167413001 - A000000172950001 - A000000177504B43532D3135 - A0000001850002 - A0000001884443 - A0000002040000 - A0000002281010 - A0000002282010 - A00000022820101010 - A0000002310100000000000000000000 - A0000002310200000000000000000000 - A0000002480300000000000000000000 - A0000002471001 - A0000002472001 - A0000002771010 - A00000030600000000000000 - A000000308000010000100 - A00000031510100528 - A0000003156020 - A00000032301 - A0000003230101 - A0000003241010 - A000000333010101 - A000000333010102 - A000000333010103 - A000000333010106 - A000000333010108 - A000000337301000 - A000000337101000 - A000000337102000 - A000000337101001 - A000000337102001 - A000000337601001 - A0000003591010 - A0000003591010028001 - A00000035910100380 - A0000003660001 - A0000003660002 - A0000003710001 - A00000038410 - A00000038420 - A0000003964D66344D0002 - A00000039742544659 - A0000003974349445F0100 - A0000004271010 - A0000004320001 - A0000004360100 - A0000004391010 - A0000004540010 - A0000004540011 - A0000004762010 - A0000004763030 - A0000004766C - A000000476A010 - A000000476A110 - A000000485 - A0000005241010 - A0000005271002 - A000000527200101 - A000000527210101 - A0000005591010FFFFFFFF8900000100 - A0000005591010FFFFFFFF8900000200 - A0000005591010FFFFFFFF8900000D00 - A0000005591010FFFFFFFF8900000E00 - A0000005591010FFFFFFFF8900000F00 - A0000005591010FFFFFFFF8900001000 - A00000061700 - A0000006200620 - A0000006581010 - A0000006581011 - A0000006582010 - A0000006723010 - A0000006723020 - A0000007705850 - A0000007790000 - B012345678 - D040000001000002 - D040000002000002 - D040000003000002 - D040000004000002 - D04000000B000002 - D04000000C000002 - D04000000D000002 - D040000013000001 - D040000013000001 - D040000013000002 - D040000013000002 - D040000014000001 - D040000015000001 - D040000015000001 - D0400000190001 - D0400000190002 - D0400000190003 - D0400000190004 - D0400000190010 - D268000001 - D276000005 - D276000005AA040360010410 - D276000005AA0503E00401 - D276000005AA0503E00501 - D276000005AA0503E0050101 - D276000005AB0503E0040101 - D27600002200000001 - D27600002200000002 - D27600002200000060 - D276000025 - D27600002545410100 - D27600002545500100 - D27600002547410100 - D276000060 - D2760000850101 - D276000118 - D2760001180101 - D27600012401 - D276000124010101FFFF000000010000 - D2760001240102000000000000010000 - D27600012402 - D2760001240200010000000000000000 - D4100000011010 - D5280050218002 - D5780000021010 - D7560000010101 - D7560000300101 - E80704007F00070302 - E82881C11702 - E828BD080F - F0000000030001 - - CADisableMinimumFrameDurationOnPhone - - UIApplicationSupportsIndirectInputEvents - + com.apple.developer.nfc.readersession.felica.systemcodes + + 12FC + 0003 + 04D1 + 8008 + 80DE + 865E + 8592 + 8B5D + 8FC1 + FE00 + + com.apple.developer.nfc.readersession.iso7816.select-identifiers + + A0000002310100000000000000000000 + A0000002310200000000000000000000 + A0000002480300000000000000000000 + A00000006510 + A0000000651010 + 315041592E5359532E4444463031 + 325041592E5359532E4444463031 + 44464D46412E44466172653234313031 + A00000000101 + A000000003000000 + A00000000300037561 + A00000000305076010 + A0000000031010 + A000000003101001 + A000000003101002 + A0000000032010 + A0000000032020 + A0000000033010 + A0000000034010 + A0000000035010 + A000000003534441 + A0000000035350 + A000000003535041 + A0000000036010 + A0000000036020 + A0000000038002 + A0000000038010 + A0000000039010 + A000000003999910 + A0000000040000 + A00000000401 + A0000000041010 + A00000000410101213 + A00000000410101215 + A0000000041010BB5449435301 + A0000000042010 + A0000000042203 + A0000000043010 + A0000000043060 + A000000004306001 + A0000000044010 + A0000000045010 + A0000000045555 + A0000000046000 + A0000000048002 + A0000000049999 + A0000000050001 + A0000000050002 + A0000000090001FF44FF1289 + A0000000101030 + A00000001800 + A0000000181001 + A000000018434D + A000000018434D00 + A00000002401 + A000000025 + A0000000250000 + A00000002501 + A000000025010104 + A000000025010402 + A000000025010701 + A000000025010801 + A0000000291010 + A00000002945087510100000 + A00000002949034010100001 + A00000002949282010100000 + A000000029564182 + A00000003029057000AD13100101FF + A0000000308000000000280101 + A0000000421010 + A0000000422010 + A0000000423010 + A0000000424010 + A0000000425010 + A0000000426010 + A00000005945430100 + A000000063504B43532D3135 + A0000000635741502D57494D + A00000006510 + A0000000651010 + A00000006900 + A000000077010000021000000000003B + A0000000790100 + A0000000790101 + A0000000790102 + A00000007901F0 + A00000007901F1 + A00000007901F2 + A0000000790200 + A0000000790201 + A00000007902FB + A00000007902FD + A00000007902FE + A0000000790300 + A0000000791201 + A0000000791202 + A0000000871002FF49FF0589 + A00000008810200105C100 + A000000088102201034221 + A000000088102201034321 + A0000000960200 + A000000098 + A0000000980840 + A0000000980848 + A0000001110101 + A0000001160300 + A0000001166010 + A0000001166030 + A0000001169000 + A000000116A001 + A000000116DB00 + A000000118010000 + A000000118020000 + A000000118030000 + A000000118040000 + A0000001184543 + A000000118454E + A0000001211010 + A0000001320001 + A0000001408001 + A0000001410001 + A0000001510000 + A00000015153504341534400 + A0000001523010 + A0000001524010 + A0000001544442 + A0000001570010 + A0000001570020 + A0000001570021 + A0000001570022 + A0000001570023 + A0000001570030 + A0000001570031 + A0000001570040 + A0000001570050 + A0000001570051 + A0000001570100 + A0000001570104 + A0000001570109 + A000000157010A + A000000157010B + A000000157010C + A000000157010D + A0000001574443 + A0000001574444 + A000000167413000FF + A000000167413001 + A000000172950001 + A000000177504B43532D3135 + A0000001850002 + A0000001884443 + A0000002040000 + A0000002281010 + A0000002282010 + A00000022820101010 + A0000002310100000000000000000000 + A0000002310200000000000000000000 + A0000002480300000000000000000000 + A0000002471001 + A0000002472001 + A0000002771010 + A00000030600000000000000 + A000000308000010000100 + A00000031510100528 + A0000003156020 + A00000032301 + A0000003230101 + A0000003241010 + A000000333010101 + A000000333010102 + A000000333010103 + A000000333010106 + A000000333010108 + A000000337301000 + A000000337101000 + A000000337102000 + A000000337101001 + A000000337102001 + A000000337601001 + A0000003591010 + A0000003591010028001 + A00000035910100380 + A0000003660001 + A0000003660002 + A0000003710001 + A00000038410 + A00000038420 + A0000003964D66344D0002 + A00000039742544659 + A0000003974349445F0100 + A0000004271010 + A0000004320001 + A0000004360100 + A0000004391010 + A0000004540010 + A0000004540011 + A0000004762010 + A0000004763030 + A0000004766C + A000000476A010 + A000000476A110 + A000000485 + A0000005241010 + A0000005271002 + A000000527200101 + A000000527210101 + A0000005591010FFFFFFFF8900000100 + A0000005591010FFFFFFFF8900000200 + A0000005591010FFFFFFFF8900000D00 + A0000005591010FFFFFFFF8900000E00 + A0000005591010FFFFFFFF8900000F00 + A0000005591010FFFFFFFF8900001000 + A00000061700 + A0000006200620 + A0000006581010 + A0000006581011 + A0000006582010 + A0000006723010 + A0000006723020 + A0000007705850 + A0000007790000 + B012345678 + D040000001000002 + D040000002000002 + D040000003000002 + D040000004000002 + D04000000B000002 + D04000000C000002 + D04000000D000002 + D040000013000001 + D040000013000001 + D040000013000002 + D040000013000002 + D040000014000001 + D040000015000001 + D040000015000001 + D0400000190001 + D0400000190002 + D0400000190003 + D0400000190004 + D0400000190010 + D268000001 + D276000005 + D276000005AA040360010410 + D276000005AA0503E00401 + D276000005AA0503E00501 + D276000005AA0503E0050101 + D276000005AB0503E0040101 + D27600002200000001 + D27600002200000002 + D27600002200000060 + D276000025 + D27600002545410100 + D27600002545500100 + D27600002547410100 + D276000060 + D2760000850101 + D276000118 + D2760001180101 + D27600012401 + D276000124010101FFFF000000010000 + D2760001240102000000000000010000 + D27600012402 + D2760001240200010000000000000000 + D4100000011010 + D5280050218002 + D5780000021010 + D7560000010101 + D7560000300101 + E80704007F00070302 + E82881C11702 + E828BD080F + F0000000030001 + diff --git a/ios/Runner/Runner.entitlements b/ios/Runner/Runner.entitlements index 13cf5bf69..a940f1336 100644 --- a/ios/Runner/Runner.entitlements +++ b/ios/Runner/Runner.entitlements @@ -20,7 +20,7 @@ com.apple.developer.nfc.readersession.formats - NDEF + NDEF TAG com.apple.developer.ubiquity-container-identifiers diff --git a/lib/routes/charge/pos_payment_dialog.dart b/lib/routes/charge/pos_payment_dialog.dart index 1c524da5f..848899645 100644 --- a/lib/routes/charge/pos_payment_dialog.dart +++ b/lib/routes/charge/pos_payment_dialog.dart @@ -166,7 +166,7 @@ class PosPaymentDialogState extends State { onPressed: Platform.isAndroid ? null : () { - ServiceInjector().nfc.starSession(autoClose: true); + ServiceInjector().nfc.startSession(autoClose: true, lnurlOnly: true); }, ), IconButton( diff --git a/lib/services/nfc.dart b/lib/services/nfc.dart index 24444cd4d..1ede95db8 100644 --- a/lib/services/nfc.dart +++ b/lib/services/nfc.dart @@ -4,14 +4,24 @@ import 'dart:io'; import 'package:breez/services/device.dart'; import 'package:breez/services/injector.dart'; import 'package:breez/services/supported_schemes.dart'; +import 'package:cktap_protocol/cktap_protocol.dart'; import 'package:flutter/services.dart'; import 'package:logging/logging.dart'; import 'package:nfc_manager/nfc_manager.dart'; final _log = Logger("NFC"); +typedef SatscardTagCallback = Future Function(NfcTag tag); + class NFCService { static const _platform = MethodChannel('com.breez.client/nfc'); + + /// Instead of using a stream controller we need a direct callback because + /// once the nfc_manager callback returns the given tag is erased and can't be + /// used for transmission anymore. The upcoming version 4.0 should avoid this + /// by re-architecting the entire plugin + SatscardTagCallback onSatscardTag; + final StreamController _lnLinkController = StreamController.broadcast(); StreamSubscription _lnLinkListener; @@ -21,6 +31,10 @@ class NFCService { return _lnLinkController.stream; } + /// A cache of whether NFC capabilities are available on this device + bool _isAvailable = false; + bool get isAvailable => _isAvailable; + NFCService() { if (Platform.isAndroid) { int fnCalls = 0; @@ -38,8 +52,26 @@ class NFCService { _listenLnLinks(); } - starSession({bool autoClose}) { - _startNFCSession(autoClose: autoClose); + startSession({ + bool autoClose = false, + String iosAlert, + bool lnurlOnly = false, + bool satscardOnly = false, + }) { + _startNFCSession( + autoClose: autoClose, + iosAlert: iosAlert, + lnurlOnly: lnurlOnly, + satscardOnly: satscardOnly); + } + + stopSession({String iosAlert, String iosError}) { + NfcManager.instance + .stopSession(alertMessage: iosAlert, errorMessage: iosError); + } + + updateAlert(String iosAlert) { + NfcManager.instance.updateSession(iosAlert); } _checkNfcStartedWith() async { @@ -54,9 +86,9 @@ class NFCService { _listenLnLinks() async { // Check availability _log.info("check if nfc available"); - bool isAvailable = await NfcManager.instance.isAvailable(); - _log.info("nfc available $isAvailable"); - if (isAvailable && Platform.isAndroid) { + _isAvailable = await NfcManager.instance.isAvailable(); + _log.info("nfc available $_isAvailable"); + if (_isAvailable && Platform.isAndroid) { _startNFCSession(); ServiceInjector().device.eventStream.distinct().listen((event) { switch (event) { @@ -72,19 +104,24 @@ class NFCService { } } - _startNFCSession({bool autoClose = false}) async { + _startNFCSession({ + bool autoClose = false, + String iosAlert, + bool lnurlOnly = false, + bool satscardOnly = false, + }) async { await NfcManager.instance.stopSession(); NfcManager.instance.startSession( + alertMessage: iosAlert, onDiscovered: (NfcTag tag) async { var ndef = Ndef.from(tag); _log.info("tag data: ${tag.data.toString()}"); if (ndef != null) { for (var rec in ndef.cachedMessage.records) { - String payload = String.fromCharCodes(rec.payload); - final link = extractPayloadLink(payload); - if (link != null) { - _log.info("nfc broadcasting link: $link"); - _lnLinkController.add(link); + final payload = String.fromCharCodes(rec.payload); + final wasHandled = await _handlePayload(payload, tag, + lnurlOnly: lnurlOnly, satscardOnly: satscardOnly); + if (wasHandled) { if (autoClose) { NfcManager.instance.stopSession(); } @@ -97,6 +134,32 @@ class NFCService { ); } + Future _handlePayload(String payload, NfcTag tag, + {bool lnurlOnly, bool satscardOnly}) async { + final checkAll = !(lnurlOnly || satscardOnly); + if (checkAll || lnurlOnly) { + final link = extractPayloadLink(payload); + if (link != null) { + _log.info("nfc broadcasting link: $link"); + _lnLinkController.add(link); + return true; + } + } + if (checkAll || satscardOnly) { + if (CKTap.isLikelySatscard(payload)) { + if (onSatscardTag != null) { + _log.info("nfc broadcasting possible satscard: $payload"); + await onSatscardTag(tag); + } else { + _log.warning( + "nfc encountered Satscard but no callback was registered: $payload"); + } + return true; + } + } + return false; + } + close() { _lnLinkListener?.cancel(); _checkNfcStartedWithTimer?.cancel(); diff --git a/pubspec.lock b/pubspec.lock index bf9d30f49..a93c9ba19 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -13,10 +13,10 @@ packages: dependency: transitive description: name: _flutterfire_internals - sha256: eb0ac20f704799b986049fbb3c1c16421eca319a1b872378d669513e12452ba5 + sha256: f5628cd9c92ed11083f425fd1f8f1bc60ecdda458c81d73b143aeda036c35fe7 url: "https://pub.dev" source: hosted - version: "1.3.14" + version: "1.3.16" analyzer: dependency: transitive description: @@ -54,10 +54,10 @@ packages: dependency: "direct main" description: name: archive - sha256: "7b875fd4a20b165a3084bd2d210439b22ebc653f21cea4842729c0c30c82596b" + sha256: "22600aa1e926be775fa5fe7e6894e7fb3df9efda8891c73f70fb3262399a432d" url: "https://pub.dev" source: hosted - version: "3.4.9" + version: "3.4.10" args: dependency: transitive description: @@ -126,10 +126,10 @@ packages: dependency: transitive description: name: barcode - sha256: "789f898eef0bd88312470bdb2cc996f895ad7dd5f89e9adde84b204546a90b45" + sha256: "91b143666f7bb13636f716b6d4e412e372ab15ff7969799af8c9e30a382e9385" url: "https://pub.dev" source: hosted - version: "2.2.4" + version: "2.2.6" base58check: dependency: transitive description: @@ -190,9 +190,9 @@ packages: dependency: "direct main" description: path: "." - ref: cab51c7c70339e5bfdc822df01dc0785498778f4 - resolved-ref: cab51c7c70339e5bfdc822df01dc0785498778f4 - url: "https://github.com/breez/Breez-Translations.git" + ref: "866d5599f7d5084658f1c72ccc19bda392e0900f" + resolved-ref: "866d5599f7d5084658f1c72ccc19bda392e0900f" + url: "https://github.com/breez/Breez-Translations" source: git version: "1.0.0" bs58check: @@ -283,6 +283,32 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.3" + cktap_protocol: + dependency: "direct main" + description: + path: "." + ref: "release/breez" + resolved-ref: "35baa6a4d1cc838f79aa2ce8079c2424a420b82d" + url: "https://github.com/PeteClubSeven/cktap-protocol-flutter.git" + source: git + version: "0.1.0" + cktap_transport: + dependency: transitive + description: + name: cktap_transport + sha256: fa863aa881ed0796926832ddd483b1b7fd4f8e2afcefa70b4ea910a81bbe2967 + url: "https://pub.dev" + source: hosted + version: "0.1.1" + cktap_transport_nfc_manager: + dependency: "direct main" + description: + path: "." + ref: "release/v3" + resolved-ref: e1f0c6a1baec7505d51df964050fa5cee7ecbf97 + url: "https://github.com/PeteClubSeven/cktap-transport-nfc-manager.git" + source: git + version: "0.1.2" cli_util: dependency: transitive description: @@ -423,10 +449,10 @@ packages: dependency: "direct main" description: name: device_info_plus - sha256: "0042cb3b2a76413ea5f8a2b40cec2a33e01d0c937e91f0f7c211fde4f7739ba6" + sha256: "77f757b789ff68e4eaf9c56d1752309bd9f7ad557cb105b938a7f8eb89e59110" url: "https://pub.dev" source: hosted - version: "9.1.1" + version: "9.1.2" device_info_plus_platform_interface: dependency: transitive description: @@ -567,10 +593,10 @@ packages: dependency: "direct main" description: name: firebase_core - sha256: d301561d614487688d797717bef013a264c517d1d09e4c5c1325c3a64c835efb + sha256: "96607c0e829a581c2a483c658f04e8b159964c3bae2730f73297070bc85d40bb" url: "https://pub.dev" source: hosted - version: "2.24.0" + version: "2.24.2" firebase_core_platform_interface: dependency: "direct main" description: @@ -583,74 +609,74 @@ packages: dependency: transitive description: name: firebase_core_web - sha256: "10159d9ee42c79f4548971d92f3f0fcd5791f6738cda3583a4e3b2c8b244c018" + sha256: d585bdf3c656c3f7821ba1bd44da5f13365d22fcecaf5eb75c4295246aaa83c0 url: "https://pub.dev" source: hosted - version: "2.9.0" + version: "2.10.0" firebase_database: dependency: "direct main" description: name: firebase_database - sha256: "373b54cc0a890f4f1547380d31232a718aa5e4ded1a8819e6c4709d7df8286e6" + sha256: "8568ad41f9312ab1f162f70c1e3e7cb7420b8bc8d07e4d543e575bb0cb41f8a5" url: "https://pub.dev" source: hosted - version: "10.3.6" + version: "10.4.0" firebase_database_platform_interface: dependency: transitive description: name: firebase_database_platform_interface - sha256: "2e3edb4552848585aa0031c0d493699305b40da756ebe507686ca6bbe96369eb" + sha256: "4366ade2390f8799a317bb13af29c2a1fdfc84f4d04372094756b86a6cbfd305" url: "https://pub.dev" source: hosted - version: "0.2.5+14" + version: "0.2.5+16" firebase_database_web: dependency: transitive description: name: firebase_database_web - sha256: "55ec085db984291668c232d8760b82c86c33f5e4a459721a2df0438c2ece5859" + sha256: "4920a83b917493b37fd408cbb01c289ef8a422d9ed48982f908a9850290262f9" url: "https://pub.dev" source: hosted - version: "0.2.3+14" + version: "0.2.3+16" firebase_dynamic_links: dependency: "direct main" description: name: firebase_dynamic_links - sha256: "300bd7e1c2dafe7e90cfe7f0890e1371bd30f37043bf64cde406b53ecd1dd4af" + sha256: b0522806658428803aeb5e7be0b22a29acb8f8697a8909c36965feaeb1f655bd url: "https://pub.dev" source: hosted - version: "5.4.6" + version: "5.4.8" firebase_dynamic_links_platform_interface: dependency: transitive description: name: firebase_dynamic_links_platform_interface - sha256: "1d1d2d90a2edb1a67aad66a44ffc92ba43b4550631c62b4fd988dff771d91edd" + sha256: "8b90384d8f85c7211f2b5e2d9d5ae98bd08091f116ef2bd1a74b33574efacc61" url: "https://pub.dev" source: hosted - version: "0.2.6+14" + version: "0.2.6+16" firebase_messaging: dependency: "direct main" description: name: firebase_messaging - sha256: "260064e1b512a9e1970b5964d645eba888208ca3de42459c38e484c8ecdc37a9" + sha256: "980259425fa5e2afc03e533f33723335731d21a56fd255611083bceebf4373a8" url: "https://pub.dev" source: hosted - version: "14.7.6" + version: "14.7.10" firebase_messaging_platform_interface: dependency: transitive description: name: firebase_messaging_platform_interface - sha256: "81fb8c983356dd75ee660f276c918380325df7a1ab1e981ede911809e9ddff30" + sha256: "54e283a0e41d81d854636ad0dad73066adc53407a60a7c3189c9656e2f1b6107" url: "https://pub.dev" source: hosted - version: "4.5.15" + version: "4.5.18" firebase_messaging_web: dependency: transitive description: name: firebase_messaging_web - sha256: "1c5d9b6cf929ab471300143059d1641a26b73c9c24adb5266e241aea23c090aa" + sha256: "90dc7ed885e90a24bb0e56d661d4d2b5f84429697fd2cbb9e5890a0ca370e6f4" url: "https://pub.dev" source: hosted - version: "3.5.15" + version: "3.5.18" fixnum: dependency: "direct main" description: @@ -1005,10 +1031,10 @@ packages: dependency: "direct main" description: name: image - sha256: "028f61960d56f26414eb616b48b04eb37d700cbe477b7fb09bf1d7ce57fd9271" + sha256: "4c68bfd5ae83e700b5204c1e74451e7bf3cf750e6843c6e158289cf56bda018e" url: "https://pub.dev" source: hosted - version: "4.1.3" + version: "4.1.7" image_cropper: dependency: "direct main" description: @@ -1238,10 +1264,10 @@ packages: dependency: "direct main" description: name: meta - sha256: d584fa6707a52763a52446f02cc621b077888fb63b93bbcb1143a7be5a0c0c04 + sha256: "7687075e408b093f36e6bbf6c91878cc0d4cd10f409506f7bc996f68220b9136" url: "https://pub.dev" source: hosted - version: "1.11.0" + version: "1.12.0" mime: dependency: transitive description: @@ -1294,10 +1320,11 @@ packages: nfc_manager: dependency: "direct main" description: - name: nfc_manager - sha256: d6a4cc6a8a37119b1e8cc242392c0c87623d319cf1423f6a90cb998a52970baf - url: "https://pub.dev" - source: hosted + path: "." + ref: "release/v3" + resolved-ref: eb09b876bc1c7d5e88ba778dccb4a279c7aeac56 + url: "https://github.com/PeteClubSeven/flutter-nfc-manager" + source: git version: "3.3.0" nm: dependency: transitive @@ -1617,10 +1644,10 @@ packages: dependency: "direct main" description: name: share_plus - sha256: f74fc3f1cbd99f39760182e176802f693fa0ec9625c045561cfad54681ea93dd + sha256: "3ef39599b00059db0990ca2e30fca0a29d8b37aae924d60063f8e0184cf20900" url: "https://pub.dev" source: hosted - version: "7.2.1" + version: "7.2.2" share_plus_platform_interface: dependency: transitive description: @@ -1902,10 +1929,10 @@ packages: dependency: "direct main" description: name: timeago - sha256: c44b80cbc6b44627c00d76960f2af571f6f50e5dbedef4d9215d455e4335165b + sha256: d3204eb4c788214883380253da7f23485320a58c11d145babc82ad16bf4e7764 url: "https://pub.dev" source: hosted - version: "3.6.0" + version: "3.6.1" timing: dependency: transitive description: @@ -2022,10 +2049,10 @@ packages: dependency: "direct main" description: name: uuid - sha256: df5a4d8f22ee4ccd77f8839ac7cb274ebc11ef9adcce8b92be14b797fe889921 + sha256: "22c94e5ad1e75f9934b766b53c742572ee2677c56bc871d850a57dad0f82127f" url: "https://pub.dev" source: hosted - version: "4.2.1" + version: "4.2.2" validators: dependency: "direct main" description: @@ -2038,26 +2065,26 @@ packages: dependency: transitive description: name: vector_graphics - sha256: "0f0c746dd2d6254a0057218ff980fc7f5670fd0fcf5e4db38a490d31eed4ad43" + sha256: "4ac59808bbfca6da38c99f415ff2d3a5d7ca0a6b4809c71d9cf30fba5daf9752" url: "https://pub.dev" source: hosted - version: "1.1.9+1" + version: "1.1.10+1" vector_graphics_codec: dependency: transitive description: name: vector_graphics_codec - sha256: "0edf6d630d1bfd5589114138ed8fada3234deacc37966bec033d3047c29248b7" + sha256: f3247e7ab0ec77dc759263e68394990edc608fb2b480b80db8aa86ed09279e33 url: "https://pub.dev" source: hosted - version: "1.1.9+1" + version: "1.1.10+1" vector_graphics_compiler: dependency: transitive description: name: vector_graphics_compiler - sha256: d24333727332d9bd20990f1483af4e09abdb9b1fc7c3db940b56ab5c42790c26 + sha256: "18489bdd8850de3dd7ca8a34e0c446f719ec63e2bab2e7a8cc66a9028dd76c5a" url: "https://pub.dev" source: hosted - version: "1.1.9+1" + version: "1.1.10+1" vector_math: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 0d861ee46..0613dab91 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -16,8 +16,16 @@ dependencies: bip39: ^1.0.6 breez_translations: git: - url: https://github.com/breez/Breez-Translations.git - ref: cab51c7c70339e5bfdc822df01dc0785498778f4 + url: https://github.com/breez/Breez-Translations + ref: 866d5599f7d5084658f1c72ccc19bda392e0900f + cktap_protocol: + git: + url: https://github.com/PeteClubSeven/cktap-protocol-flutter.git + ref: release/breez + cktap_transport_nfc_manager: + git: + url: https://github.com/PeteClubSeven/cktap-transport-nfc-manager.git + ref: release/v3 clipboard_watcher: ^0.2.0 collection: ^1.18.0 confetti: ^0.7.0 @@ -60,10 +68,13 @@ dependencies: md5_file_checksum: git: url: https://github.com/breez/md5_file_checksum.git - nfc_manager: ^3.3.0 + nfc_manager: + git: + url: https://github.com/PeteClubSeven/flutter-nfc-manager + ref: release/v3 nostr_tools: <1.0.8 # Requires Flutter 3.10(depends on http ^1.1.0) package_info_plus: <5.0.1 # Requires Flutter 3.10 - path: ^1.8.3 + path: <1.9.0 # Requires Flutter 3.10 path_provider: ^2.1.1 path_provider_platform_interface: ^2.1.1 pdf: ^3.10.7 @@ -72,7 +83,7 @@ dependencies: protobuf: <3.0.0 # Requires Flutter 3.10 provider: ^6.1.1 meta: ^1.11.0 - mobile_scanner: ^3.5.5 + mobile_scanner: <3.5.6 # Requires Android SDK 34 # qr_flutter has already fixed the build issue with the qr package but did not publish an updated # version, they will publish as 4.0.1 for now they recommend to use the master but instead of that # we are using an specific commit to avoid unexpected behaviour in future builds. @@ -123,7 +134,7 @@ dependency_overrides: collection: ^1.18.0 intl: ^0.18.1 meta: ^1.11.0 - path: ^1.8.3 + path: <1.9.0 # Requires Flutter 3.10 flutter_downloader: git: url: https://github.com/breez/flutter_downloader.git From 45d0bb90723373da5eadcac237aff19a55d72133 Mon Sep 17 00:00:00 2001 From: Peter Date: Wed, 31 Jan 2024 21:39:48 +0000 Subject: [PATCH 2/6] Tweak existing widgets for Satscard implementation --- lib/routes/add_funds/address_widget.dart | 24 +++++++++++++------ .../deposit_to_btc_address_page.dart | 2 +- lib/widgets/circular_progress.dart | 18 ++++++++++---- lib/widgets/fee_chooser.dart | 8 ++++++- 4 files changed, 38 insertions(+), 14 deletions(-) diff --git a/lib/routes/add_funds/address_widget.dart b/lib/routes/add_funds/address_widget.dart index e2ccfab9d..9c20f20ee 100644 --- a/lib/routes/add_funds/address_widget.dart +++ b/lib/routes/add_funds/address_widget.dart @@ -10,11 +10,13 @@ import 'package:share_plus/share_plus.dart'; class AddressWidget extends StatelessWidget { final String address; final String backupJson; + final bool isGeneric; const AddressWidget( - this.address, + this.address, { this.backupJson, - ); + this.isGeneric = false, + }); @override Widget build(BuildContext context) { @@ -32,7 +34,9 @@ class AddressWidget extends StatelessWidget { mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( - texts.invoice_btc_address_deposit_address, + (isGeneric + ? texts.invoice_btc_address_generic_address + : texts.invoice_btc_address_deposit_address), style: theme.FieldTextStyle.labelStyle, ), Row( @@ -46,6 +50,8 @@ class AddressWidget extends StatelessWidget { : Column( children: [ GestureDetector( + onLongPress: + isGeneric ? null : () => _showAlertDialog(context), child: Container( margin: const EdgeInsets.only(top: 32.0, bottom: 16.0), padding: const EdgeInsets.all(8.6), @@ -54,7 +60,6 @@ class AddressWidget extends StatelessWidget { size: 180.0, ), ), - onLongPress: () => _showAlertDialog(context), ), Container( padding: const EdgeInsets.only(top: 16.0), @@ -63,8 +68,11 @@ class AddressWidget extends StatelessWidget { ServiceInjector().device.setClipboardText(address); showFlushbar( context, - message: texts - .invoice_btc_address_deposit_address_copied, + message: (isGeneric + ? texts + .invoice_btc_address_generic_address_copied + : texts + .invoice_btc_address_deposit_address_copied), ); }, child: Text( @@ -124,7 +132,9 @@ class AddressWidget extends StatelessWidget { ServiceInjector().device.setClipboardText(address); showFlushbar( context, - message: texts.invoice_btc_address_deposit_address_copied, + message: isGeneric + ? texts.invoice_btc_address_generic_address_copied + : texts.invoice_btc_address_deposit_address_copied, ); }, ); diff --git a/lib/routes/add_funds/deposit_to_btc_address_page.dart b/lib/routes/add_funds/deposit_to_btc_address_page.dart index b777550e3..d413dcdcb 100644 --- a/lib/routes/add_funds/deposit_to_btc_address_page.dart +++ b/lib/routes/add_funds/deposit_to_btc_address_page.dart @@ -126,7 +126,7 @@ class DepositToBTCAddressPageState extends State { child: Column( mainAxisSize: MainAxisSize.min, children: [ - AddressWidget(response?.address, response?.backupJson), + AddressWidget(response?.address, backupJson: response?.backupJson), response == null || lspInfo == null ? const SizedBox() : WarningBox( diff --git a/lib/widgets/circular_progress.dart b/lib/widgets/circular_progress.dart index ab1b00c62..d29a9bf09 100644 --- a/lib/widgets/circular_progress.dart +++ b/lib/widgets/circular_progress.dart @@ -8,16 +8,24 @@ class CircularProgress extends StatelessWidget { final String title; final double size; final Color color; + final MainAxisAlignment mainAxisAlignment; + final CrossAxisAlignment crossAxisAlignment; - const CircularProgress( - {Key key, this.value, this.title, this.size, this.color}) - : super(key: key); + const CircularProgress({ + Key key, + this.value, + this.title, + this.size, + this.color, + this.mainAxisAlignment = MainAxisAlignment.center, + this.crossAxisAlignment = CrossAxisAlignment.center, + }) : super(key: key); @override Widget build(BuildContext context) { return Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: mainAxisAlignment, + crossAxisAlignment: crossAxisAlignment, children: [ Stack( alignment: Alignment.center, diff --git a/lib/widgets/fee_chooser.dart b/lib/widgets/fee_chooser.dart index 085acfd8d..b383bf42e 100644 --- a/lib/widgets/fee_chooser.dart +++ b/lib/widgets/fee_chooser.dart @@ -106,7 +106,13 @@ class FeeChooser extends StatelessWidget { border: border, ), child: TextButton( - onPressed: disabled ? null : () => onSelect(index), + onPressed: disabled + ? null + : () { + if (onSelect != null) { + onSelect(index); + } + }, child: Text( text, style: themeData.textTheme.labelLarge.copyWith( From 3bcc01a533a70a3cfe2d830dee71aa72d60a5cd1 Mon Sep 17 00:00:00 2001 From: Peter Date: Wed, 31 Jan 2024 21:41:55 +0000 Subject: [PATCH 3/6] Add SatscardBloc - Performs all necessary Satscard NFC and sweeping actions --- lib/bloc/app_blocs.dart | 5 + .../satscard/detected_satscard_status.dart | 47 ++ lib/bloc/satscard/satscard_actions.dart | 53 ++ lib/bloc/satscard/satscard_bloc.dart | 257 ++++++++++ lib/bloc/satscard/satscard_op_status.dart | 81 +++ lib/home_page.dart | 3 + lib/services/breezlib/breez_bridge.dart | 27 + lib/services/breezlib/data/messages.pb.dart | 472 ++++++++++++++++++ .../breezlib/data/messages.pbjson.dart | 74 +++ lib/user_app.dart | 3 + 10 files changed, 1022 insertions(+) create mode 100644 lib/bloc/satscard/detected_satscard_status.dart create mode 100644 lib/bloc/satscard/satscard_actions.dart create mode 100644 lib/bloc/satscard/satscard_bloc.dart create mode 100644 lib/bloc/satscard/satscard_op_status.dart diff --git a/lib/bloc/app_blocs.dart b/lib/bloc/app_blocs.dart index 5e48cfb60..1b9385086 100644 --- a/lib/bloc/app_blocs.dart +++ b/lib/bloc/app_blocs.dart @@ -10,6 +10,7 @@ import 'package:breez/bloc/podcast_history/podcast_history_bloc.dart'; import 'package:breez/bloc/pos_catalog/bloc.dart'; import 'package:breez/bloc/pos_catalog/sqlite/repository.dart'; import 'package:breez/bloc/reverse_swap/reverse_swap_bloc.dart'; +import 'package:breez/bloc/satscard/satscard_bloc.dart'; import 'package:breez/bloc/tor/bloc.dart'; import 'package:breez/bloc/user_profile/user_profile_bloc.dart'; @@ -33,6 +34,7 @@ class AppBlocs { final PosCatalogBloc posCatalogBloc; final PaymentOptionsBloc paymentOptionsBloc; final ReverseSwapBloc reverseSwapBloc; + final SatscardBloc satscardBloc; final Map _blocsByType; final PodcastHistoryBloc podCastHistoryBloc; @@ -84,6 +86,7 @@ class AppBlocs { paymentOptionsBloc, ), blocsByType); + SatscardBloc satscardBloc = _registerBloc(SatscardBloc(), blocsByType); PosCatalogBloc posCatalogBloc = _registerBloc( PosCatalogBloc( accountBloc.accountStream, @@ -114,6 +117,7 @@ class AppBlocs { fastbitcoinsBloc, lspBloc, reverseSwapBloc, + satscardBloc, lnurlBloc, posCatalogBloc, paymentOptionsBloc, @@ -133,6 +137,7 @@ class AppBlocs { this.fastbitcoinsBloc, this.lspBloc, this.reverseSwapBloc, + this.satscardBloc, this.lnurlBloc, this.posCatalogBloc, this.paymentOptionsBloc, diff --git a/lib/bloc/satscard/detected_satscard_status.dart b/lib/bloc/satscard/detected_satscard_status.dart new file mode 100644 index 000000000..ac5a2eb5e --- /dev/null +++ b/lib/bloc/satscard/detected_satscard_status.dart @@ -0,0 +1,47 @@ +import 'package:cktap_protocol/cktapcard.dart'; + +abstract class DetectedSatscardStatus { + const DetectedSatscardStatus(); + + factory DetectedSatscardStatus.sweepable(Satscard card, Slot slot) => + DetectedSweepableSatscardStatus(card, slot); + + factory DetectedSatscardStatus.unused(Satscard card) => + DetectedUnusedSatscardStatus(card); + + factory DetectedSatscardStatus.usedUp(Satscard card) => + DetectedUsedUpSatscardStatus(card); + + factory DetectedSatscardStatus.nfcError() => + DetectedNoSatscardStatus(); + + factory DetectedSatscardStatus.unknownError(String message) => + DetectedInvalidSatscardStatus(message); +} + +class DetectedInvalidSatscardStatus extends DetectedSatscardStatus { + final String message; + + DetectedInvalidSatscardStatus(this.message); +} + +class DetectedNoSatscardStatus extends DetectedSatscardStatus {} + +class DetectedSweepableSatscardStatus extends DetectedSatscardStatus { + final Satscard card; + final Slot slot; + + DetectedSweepableSatscardStatus(this.card, this.slot); +} + +class DetectedUnusedSatscardStatus extends DetectedSatscardStatus { + final Satscard card; + + DetectedUnusedSatscardStatus(this.card); +} + +class DetectedUsedUpSatscardStatus extends DetectedSatscardStatus { + final Satscard card; + + DetectedUsedUpSatscardStatus(this.card); +} diff --git a/lib/bloc/satscard/satscard_actions.dart b/lib/bloc/satscard/satscard_actions.dart new file mode 100644 index 000000000..6dbd5ab5a --- /dev/null +++ b/lib/bloc/satscard/satscard_actions.dart @@ -0,0 +1,53 @@ +import 'dart:typed_data'; + +import 'package:breez/bloc/async_action.dart'; +import 'package:breez/services/breezlib/data/messages.pb.dart'; +import 'package:cktap_protocol/satscard.dart'; + +class CreateSlotSweepTransactions extends AsyncAction { + final AddressInfo slotInfo; + final String recipient; + + CreateSlotSweepTransactions(this.slotInfo, this.recipient); +} + +class DisableListening extends AsyncAction {} + +class EnableListening extends AsyncAction {} + +class GetAddressInfo extends AsyncAction { + final String address; + + GetAddressInfo(this.address); +} + +class GetSlot extends AsyncAction { + final Satscard satscard; + final int index; + final String spendCode; + + GetSlot(this.satscard, this.index, this.spendCode); +} + +class InitializeSlot extends AsyncAction { + final Satscard satscard; + final String spendCode; + final String chainCode; + + InitializeSlot(this.satscard, this.spendCode, this.chainCode); +} + +class SignSlotSweepTransaction extends AsyncAction { + final AddressInfo addressInfo; + final Uint8List privateKey; + final RawSlotSweepTransaction transaction; + + SignSlotSweepTransaction(this.addressInfo, this.transaction, this.privateKey); +} + +class UnsealSlot extends AsyncAction { + final Satscard satscard; + final String spendCode; + + UnsealSlot(this.satscard, this.spendCode); +} diff --git a/lib/bloc/satscard/satscard_bloc.dart b/lib/bloc/satscard/satscard_bloc.dart new file mode 100644 index 000000000..9b64bab2a --- /dev/null +++ b/lib/bloc/satscard/satscard_bloc.dart @@ -0,0 +1,257 @@ +import 'dart:async'; + +import 'package:breez/bloc/async_actions_handler.dart'; +import 'package:breez/bloc/satscard/satscard_actions.dart'; +import 'package:breez/bloc/satscard/detected_satscard_status.dart'; +import 'package:breez/bloc/satscard/satscard_op_status.dart'; +import 'package:breez/services/breezlib/breez_bridge.dart'; +import 'package:breez/services/injector.dart'; +import 'package:breez/services/nfc.dart'; +import 'package:cktap_protocol/cktapcard.dart'; +import 'package:cktap_protocol/exceptions.dart'; +import 'package:cktap_transport_nfc_manager/cktap_transport_nfc_manager.dart'; +import 'package:logging/logging.dart'; +import 'package:nfc_manager/nfc_manager.dart'; + +final _log = Logger("SatscardBloc"); + +class SatscardBloc with AsyncActionsHandler { + final _detectedController = StreamController(); + Stream get detectedStream => + _detectedController.stream; + + final _operationController = StreamController.broadcast(); + Stream get operationStream => _operationController.stream; + + final BreezBridge _breezLib; + final NFCService _nfc; + + SatscardBloc() + : _breezLib = ServiceInjector().breezBridge, + _nfc = ServiceInjector().nfc { + registerAsyncHandlers({ + CreateSlotSweepTransactions: _createSlotSweepTransactions, + DisableListening: _disableListening, + EnableListening: _enableListening, + GetAddressInfo: _getAddressInfo, + GetSlot: _getSlot, + InitializeSlot: _initializeSlot, + SignSlotSweepTransaction: _signSlotSweepTransaction, + UnsealSlot: _unsealSlot, + }); + listenActions(); + } + + @override + Future dispose() async { + await _detectedController.close(); + await _operationController.close(); + } + + void _listenSatscards() { + _log.info("_listenSatscards() registered with NFCService"); + _nfc.onSatscardTag = (tag) async { + try { + _log.info("Attempting to read Satscard with the following tag: $tag"); + final transport = NfcManagerTransport(tag); + final card = await Satscard.fromTransport(transport); + if (card.isUsedUp) { + _log.info("Found Satscard with no unused slots: $card"); + _detectedController.add(DetectedSatscardStatus.usedUp(card)); + return; + } + var slot = await card.getActiveSlot(); + switch (slot.status) { + case SlotStatus.unused: + _log.info("Uninitialized active slot found on Satscard: $card"); + _detectedController.add(DetectedSatscardStatus.unused(card)); + break; + case SlotStatus.sealed: + case SlotStatus.unsealed: + _log.info("Sweepable Satscard found: $card"); + _log.info("Active slot: $slot"); + _detectedController + .add(DetectedSatscardStatus.sweepable(card, slot)); + break; + default: + _log.severe( + "Satscard detected with unknown slot status: ${slot.status}"); + break; + } + } on NfcCommunicationException catch (e) { + _log.warning( + "Reading a satscard failed due to a communication error: $e"); + _detectedController.add(DetectedSatscardStatus.nfcError()); + } catch (e, s) { + _log.severe("Reading a satscard failed with an unexpected error", e, s); + _detectedController + .add(DetectedSatscardStatus.unknownError(e.toString())); + } + }; + } + + Future _createSlotSweepTransactions( + CreateSlotSweepTransactions action) async { + _log.info( + "_createSlotSweepTransaction() called for ${action.slotInfo.address} to ${action.recipient}"); + return _breezLib + .createSlotSweepTransactions(action.slotInfo, action.recipient) + .then((result) => action.resolve(result)); + } + + Future _disableListening(DisableListening action) async { + _log.info("_disableListening() called"); + _nfc.onSatscardTag = (tag) async => _log + .info("Ignoring Satscard tag due to listening being disabled: $tag"); + action.resolve(true); + } + + Future _enableListening(EnableListening action) async { + _log.info("_enableListening() called"); + _listenSatscards(); + action.resolve(true); + } + + Future _getAddressInfo(GetAddressInfo action) async { + _log.info("_getAddressInfo() called for: ${action.address}"); + return _breezLib + .getAddressInfo(action.address) + .then((result) => action.resolve(result)); + } + + Future _getSlot(GetSlot action) async { + _log.info("getSlot() registered with NFCService"); + _nfc.onSatscardTag = (tag) async { + await _performSatscardOperation(tag, action.satscard, + func: (card, activeSlot, transport) async { + _log.info("Getting slot ${action.index} of card ${card.ident}"); + + final slot = await card.getSlot(transport, action.index, + spendCode: action.spendCode); + _operationController.add(SatscardOpStatus.success(card, slot)); + }); + }; + action.resolve(true); + } + + Future _initializeSlot(InitializeSlot action) async { + _log.info("_initializeSlot() registered with NFCService"); + _nfc.onSatscardTag = (tag) async { + await _performSatscardOperation(tag, action.satscard, + expectedStatus: SlotStatus.unused, + func: (card, activeSlot, transport) async { + _log.info("Initializing active slot of card ${card.ident}"); + + final slot = await card.newSlot(transport, action.spendCode, + chainCode: action.chainCode); + _operationController.add(SatscardOpStatus.success(card, slot)); + }); + }; + action.resolve(true); + } + + Future _signSlotSweepTransaction( + SignSlotSweepTransaction action) async { + _log.info( + "_signSlotSweepTransaction() called for ${action.addressInfo.address} with a balance of ${action.addressInfo.confirmedBalance} sats"); + return _breezLib + .signSlotSweepTransaction( + action.addressInfo, action.transaction, action.privateKey) + .then((value) => action.resolve(value)); + } + + Future _unsealSlot(UnsealSlot action) async { + _log.info("_unsealSlot() registered with NFCService"); + _nfc.onSatscardTag = (tag) async { + await _performSatscardOperation(tag, action.satscard, + expectedStatus: SlotStatus.sealed, + func: (card, activeSlot, transport) async { + _log.info("Unsealing active slot of card ${card.ident}"); + + final unsealedSlot = await card.unseal(transport, action.spendCode); + _operationController.add(SatscardOpStatus.success(card, unsealedSlot)); + }); + }; + action.resolve(true); + } + + Future _performSatscardOperation( + NfcTag tag, + Satscard expectedCard, { + SlotStatus expectedStatus, + Function(Satscard, Slot, Transport) func, + }) async { + try { + _operationController.add(SatscardOpStatus.inProgress()); + final transport = NfcManagerTransport(tag); + final card = await _createValidatedCard(expectedCard, transport); + if (card == null) { + return; + } + final activeSlot = await _getValidatedActiveSlot( + card, expectedCard.activeSlotIndex, expectedStatus); + if (activeSlot == null) { + return; + } + final authDelay = await _processAuthDelay(card, transport); + if (authDelay != 0) { + return; + } + + // We have a valid card and can perform the operation now! + _operationController.add(SatscardOpStatus.inProgress()); + await func(card, activeSlot, transport); + } on NfcCommunicationException catch (e) { + _log.warning("Slot operation failed due to a communication error: $e"); + _operationController.add(SatscardOpStatus.nfcError()); + } on TapProtoException catch (e, s) { + _log.severe("Slot operation failed due to a protocol error", e, s); + _operationController.add(SatscardOpStatus.protocolError(e)); + } catch (e, s) { + _log.severe("Slot operation failed with an unexpected error", e, s); + _operationController.add(SatscardOpStatus.unexpectedError(e.toString())); + } + } + + Future _createValidatedCard( + Satscard expected, Transport transport) async { + final card = await Satscard.fromTransport(transport); + if (card.ident != expected.ident) { + _operationController.add(SatscardOpStatus.incorrectCard()); + return null; + } + return card; + } + + Future _getValidatedActiveSlot( + Satscard card, int expectedIndex, SlotStatus expectedStatus) async { + if (card.activeSlotIndex != expectedIndex) { + _operationController.add(SatscardOpStatus.staleCard()); + return null; + } + final activeSlot = await card.getActiveSlot(); + if (expectedStatus != null && activeSlot.status != expectedStatus) { + _operationController.add(SatscardOpStatus.staleCard()); + return null; + } + return activeSlot; + } + + Future _processAuthDelay(Satscard card, Transport transport) async { + final initialDelay = card.authDelay; + if (initialDelay > 0) { + _operationController + .add(SatscardOpStatus.waiting(initialDelay, initialDelay)); + while (card.authDelay > 0) { + final response = await card.wait(transport); + if (!response.success) { + throw Exception( + "Unexpected failure while awaiting the authentication delay"); + } + _operationController + .add(SatscardOpStatus.waiting(card.authDelay, initialDelay)); + } + } + return card.authDelay; + } +} diff --git a/lib/bloc/satscard/satscard_op_status.dart b/lib/bloc/satscard/satscard_op_status.dart new file mode 100644 index 000000000..31e5f82b9 --- /dev/null +++ b/lib/bloc/satscard/satscard_op_status.dart @@ -0,0 +1,81 @@ +import 'package:cktap_protocol/cktapcard.dart'; +import 'package:cktap_protocol/exceptions.dart'; + +abstract class SatscardOpStatus { + const SatscardOpStatus(); + + // Progress states + + factory SatscardOpStatus.inProgress() => const SatscardOpStatusInProgress(); + factory SatscardOpStatus.waiting( + int currentAuthDelay, int initialAuthDelay) => + SatscardOpStatusWaiting(currentAuthDelay, initialAuthDelay); + + // Success states + + factory SatscardOpStatus.success(Satscard card, Slot slot) => + SatscardOpStatusSuccess(card, slot); + + // Failure states + + factory SatscardOpStatus.incorrectCard() => + const SatscardOpStatusIncorrectCard(); + factory SatscardOpStatus.nfcError() => const SatscardOpStatusNfcError(); + factory SatscardOpStatus.protocolError(TapProtoException e) { + switch (e.code) { + case TapProtoExceptionCode.BAD_AUTH: + return const SatscardOpStatusBadAuth(); + default: + return SatscardOpStatusProtocolError(e); + } + } + factory SatscardOpStatus.staleCard() => const SatscardOpStatusStaleCard(); + factory SatscardOpStatus.unexpectedError(String message) => + SatscardOpStatusUnexpectedError(message); +} + +class SatscardOpStatusInProgress extends SatscardOpStatus { + const SatscardOpStatusInProgress(); +} + +class SatscardOpStatusWaiting extends SatscardOpStatus { + final int currentAuthDelay; + final int initialAuthDelay; + + const SatscardOpStatusWaiting(this.currentAuthDelay, this.initialAuthDelay); +} + +class SatscardOpStatusSuccess extends SatscardOpStatus { + final Satscard card; + final Slot slot; + + const SatscardOpStatusSuccess(this.card, this.slot); +} + +class SatscardOpStatusBadAuth extends SatscardOpStatus { + const SatscardOpStatusBadAuth(); +} + +class SatscardOpStatusIncorrectCard extends SatscardOpStatus { + const SatscardOpStatusIncorrectCard(); +} + +class SatscardOpStatusNfcError extends SatscardOpStatus { + const SatscardOpStatusNfcError(); +} + +class SatscardOpStatusProtocolError extends SatscardOpStatus { + final TapProtoException e; + + const SatscardOpStatusProtocolError(this.e); +} + +class SatscardOpStatusStaleCard extends SatscardOpStatus { + const SatscardOpStatusStaleCard(); +} + +class SatscardOpStatusUnexpectedError extends SatscardOpStatus { + final String message; + + const SatscardOpStatusUnexpectedError(this.message); +} diff --git a/lib/home_page.dart b/lib/home_page.dart index 0e2d29ed1..afb3b5d38 100644 --- a/lib/home_page.dart +++ b/lib/home_page.dart @@ -15,6 +15,7 @@ import 'package:breez/bloc/invoice/invoice_bloc.dart'; import 'package:breez/bloc/lnurl/lnurl_bloc.dart'; import 'package:breez/bloc/lsp/lsp_bloc.dart'; import 'package:breez/bloc/reverse_swap/reverse_swap_bloc.dart'; +import 'package:breez/bloc/satscard/satscard_bloc.dart'; import 'package:breez/bloc/user_profile/breez_user_model.dart'; import 'package:breez/bloc/user_profile/user_profile_bloc.dart'; import 'package:breez/handlers/check_channel_connection_handler.dart'; @@ -69,6 +70,7 @@ class Home extends StatefulWidget { final BackupBloc backupBloc; final LSPBloc lspBloc; final ReverseSwapBloc reverseSwapBloc; + final SatscardBloc satscardBloc; final LNUrlBloc lnurlBloc; Home( @@ -79,6 +81,7 @@ class Home extends StatefulWidget { this.backupBloc, this.lspBloc, this.reverseSwapBloc, + this.satscardBloc, this.lnurlBloc, ); diff --git a/lib/services/breezlib/breez_bridge.dart b/lib/services/breezlib/breez_bridge.dart index 3f6ea3834..af1f16b70 100644 --- a/lib/services/breezlib/breez_bridge.dart +++ b/lib/services/breezlib/breez_bridge.dart @@ -924,6 +924,33 @@ class BreezBridge { }); } + Future getAddressInfo(String address) { + return _invokeMethodWhenReady( + "getAddressInfo", {"argument": address}) + .then((res) => AddressInfo()..mergeFromBuffer(res ?? [])); + } + + Future createSlotSweepTransactions( + AddressInfo slot, String recipient) { + var request = CreateSlotSweepRequest() + ..slot = slot + ..recipient = recipient; + return _invokeMethodWhenReady("createSlotSweepTransactions", { + "argument": request.writeToBuffer() + }).then((res) => CreateSlotSweepResponse()..mergeFromBuffer(res ?? [])); + } + + Future signSlotSweepTransaction(AddressInfo info, + RawSlotSweepTransaction transaction, Uint8List privateKey) { + var request = SignSlotSweepRequest() + ..addressInfo = info + ..transaction = transaction + ..privateKey = privateKey; + return _invokeMethodWhenReady( + "signSlotSweepTransaction", {"argument": request.writeToBuffer()}) + .then((res) => TransactionDetails()..mergeFromBuffer(res ?? [])); + } + Future _invokeMethodWhenReady(String methodName, [dynamic arguments]) { if (methodName != "log") { _log.info("before invoking method $methodName"); diff --git a/lib/services/breezlib/data/messages.pb.dart b/lib/services/breezlib/data/messages.pb.dart index 356a084e8..61fe3e10b 100644 --- a/lib/services/breezlib/data/messages.pb.dart +++ b/lib/services/breezlib/data/messages.pb.dart @@ -6065,3 +6065,475 @@ class TorConfig extends $pb.GeneratedMessage { void clearSocks() => clearField(3); } +class Utxo extends $pb.GeneratedMessage { + static final $pb.BuilderInfo _i = $pb.BuilderInfo(const $core.bool.fromEnvironment('protobuf.omit_message_names') ? '' : 'Utxo', package: const $pb.PackageName(const $core.bool.fromEnvironment('protobuf.omit_message_names') ? '' : 'data'), createEmptyInstance: create) + ..aOS(1, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'txid') + ..a<$core.int>(2, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'vout', $pb.PbFieldType.OU3) + ..aInt64(3, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'value') + ..aOB(4, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'isConfirmed', protoName: 'isConfirmed') + ..hasRequiredFields = false + ; + + Utxo._() : super(); + factory Utxo({ + $core.String? txid, + $core.int? vout, + $fixnum.Int64? value, + $core.bool? isConfirmed, + }) { + final _result = create(); + if (txid != null) { + _result.txid = txid; + } + if (vout != null) { + _result.vout = vout; + } + if (value != null) { + _result.value = value; + } + if (isConfirmed != null) { + _result.isConfirmed = isConfirmed; + } + return _result; + } + factory Utxo.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory Utxo.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + Utxo clone() => Utxo()..mergeFromMessage(this); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + Utxo copyWith(void Function(Utxo) updates) => super.copyWith((message) => updates(message as Utxo)) as Utxo; // ignore: deprecated_member_use + $pb.BuilderInfo get info_ => _i; + @$core.pragma('dart2js:noInline') + static Utxo create() => Utxo._(); + Utxo createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static Utxo getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static Utxo? _defaultInstance; + + @$pb.TagNumber(1) + $core.String get txid => $_getSZ(0); + @$pb.TagNumber(1) + set txid($core.String v) { $_setString(0, v); } + @$pb.TagNumber(1) + $core.bool hasTxid() => $_has(0); + @$pb.TagNumber(1) + void clearTxid() => clearField(1); + + @$pb.TagNumber(2) + $core.int get vout => $_getIZ(1); + @$pb.TagNumber(2) + set vout($core.int v) { $_setUnsignedInt32(1, v); } + @$pb.TagNumber(2) + $core.bool hasVout() => $_has(1); + @$pb.TagNumber(2) + void clearVout() => clearField(2); + + @$pb.TagNumber(3) + $fixnum.Int64 get value => $_getI64(2); + @$pb.TagNumber(3) + set value($fixnum.Int64 v) { $_setInt64(2, v); } + @$pb.TagNumber(3) + $core.bool hasValue() => $_has(2); + @$pb.TagNumber(3) + void clearValue() => clearField(3); + + @$pb.TagNumber(4) + $core.bool get isConfirmed => $_getBF(3); + @$pb.TagNumber(4) + set isConfirmed($core.bool v) { $_setBool(3, v); } + @$pb.TagNumber(4) + $core.bool hasIsConfirmed() => $_has(3); + @$pb.TagNumber(4) + void clearIsConfirmed() => clearField(4); +} + +class AddressInfo extends $pb.GeneratedMessage { + static final $pb.BuilderInfo _i = $pb.BuilderInfo(const $core.bool.fromEnvironment('protobuf.omit_message_names') ? '' : 'AddressInfo', package: const $pb.PackageName(const $core.bool.fromEnvironment('protobuf.omit_message_names') ? '' : 'data'), createEmptyInstance: create) + ..aOS(1, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'address') + ..aInt64(2, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'confirmedBalance', protoName: 'confirmedBalance') + ..aInt64(3, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'unconfirmedBalance', protoName: 'unconfirmedBalance') + ..pc(4, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'utxos', $pb.PbFieldType.PM, subBuilder: Utxo.create) + ..hasRequiredFields = false + ; + + AddressInfo._() : super(); + factory AddressInfo({ + $core.String? address, + $fixnum.Int64? confirmedBalance, + $fixnum.Int64? unconfirmedBalance, + $core.Iterable? utxos, + }) { + final _result = create(); + if (address != null) { + _result.address = address; + } + if (confirmedBalance != null) { + _result.confirmedBalance = confirmedBalance; + } + if (unconfirmedBalance != null) { + _result.unconfirmedBalance = unconfirmedBalance; + } + if (utxos != null) { + _result.utxos.addAll(utxos); + } + return _result; + } + factory AddressInfo.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory AddressInfo.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + AddressInfo clone() => AddressInfo()..mergeFromMessage(this); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + AddressInfo copyWith(void Function(AddressInfo) updates) => super.copyWith((message) => updates(message as AddressInfo)) as AddressInfo; // ignore: deprecated_member_use + $pb.BuilderInfo get info_ => _i; + @$core.pragma('dart2js:noInline') + static AddressInfo create() => AddressInfo._(); + AddressInfo createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static AddressInfo getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static AddressInfo? _defaultInstance; + + @$pb.TagNumber(1) + $core.String get address => $_getSZ(0); + @$pb.TagNumber(1) + set address($core.String v) { $_setString(0, v); } + @$pb.TagNumber(1) + $core.bool hasAddress() => $_has(0); + @$pb.TagNumber(1) + void clearAddress() => clearField(1); + + @$pb.TagNumber(2) + $fixnum.Int64 get confirmedBalance => $_getI64(1); + @$pb.TagNumber(2) + set confirmedBalance($fixnum.Int64 v) { $_setInt64(1, v); } + @$pb.TagNumber(2) + $core.bool hasConfirmedBalance() => $_has(1); + @$pb.TagNumber(2) + void clearConfirmedBalance() => clearField(2); + + @$pb.TagNumber(3) + $fixnum.Int64 get unconfirmedBalance => $_getI64(2); + @$pb.TagNumber(3) + set unconfirmedBalance($fixnum.Int64 v) { $_setInt64(2, v); } + @$pb.TagNumber(3) + $core.bool hasUnconfirmedBalance() => $_has(2); + @$pb.TagNumber(3) + void clearUnconfirmedBalance() => clearField(3); + + @$pb.TagNumber(4) + $core.List get utxos => $_getList(3); +} + +class CreateSlotSweepRequest extends $pb.GeneratedMessage { + static final $pb.BuilderInfo _i = $pb.BuilderInfo(const $core.bool.fromEnvironment('protobuf.omit_message_names') ? '' : 'CreateSlotSweepRequest', package: const $pb.PackageName(const $core.bool.fromEnvironment('protobuf.omit_message_names') ? '' : 'data'), createEmptyInstance: create) + ..aOM(1, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'slot', subBuilder: AddressInfo.create) + ..aOS(2, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'recipient') + ..hasRequiredFields = false + ; + + CreateSlotSweepRequest._() : super(); + factory CreateSlotSweepRequest({ + AddressInfo? slot, + $core.String? recipient, + }) { + final _result = create(); + if (slot != null) { + _result.slot = slot; + } + if (recipient != null) { + _result.recipient = recipient; + } + return _result; + } + factory CreateSlotSweepRequest.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory CreateSlotSweepRequest.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + CreateSlotSweepRequest clone() => CreateSlotSweepRequest()..mergeFromMessage(this); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + CreateSlotSweepRequest copyWith(void Function(CreateSlotSweepRequest) updates) => super.copyWith((message) => updates(message as CreateSlotSweepRequest)) as CreateSlotSweepRequest; // ignore: deprecated_member_use + $pb.BuilderInfo get info_ => _i; + @$core.pragma('dart2js:noInline') + static CreateSlotSweepRequest create() => CreateSlotSweepRequest._(); + CreateSlotSweepRequest createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static CreateSlotSweepRequest getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static CreateSlotSweepRequest? _defaultInstance; + + @$pb.TagNumber(1) + AddressInfo get slot => $_getN(0); + @$pb.TagNumber(1) + set slot(AddressInfo v) { setField(1, v); } + @$pb.TagNumber(1) + $core.bool hasSlot() => $_has(0); + @$pb.TagNumber(1) + void clearSlot() => clearField(1); + @$pb.TagNumber(1) + AddressInfo ensureSlot() => $_ensure(0); + + @$pb.TagNumber(2) + $core.String get recipient => $_getSZ(1); + @$pb.TagNumber(2) + set recipient($core.String v) { $_setString(1, v); } + @$pb.TagNumber(2) + $core.bool hasRecipient() => $_has(1); + @$pb.TagNumber(2) + void clearRecipient() => clearField(2); +} + +class RawSlotSweepTransaction extends $pb.GeneratedMessage { + static final $pb.BuilderInfo _i = $pb.BuilderInfo(const $core.bool.fromEnvironment('protobuf.omit_message_names') ? '' : 'RawSlotSweepTransaction', package: const $pb.PackageName(const $core.bool.fromEnvironment('protobuf.omit_message_names') ? '' : 'data'), createEmptyInstance: create) + ..a<$core.List<$core.int>>(1, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'msgTx', $pb.PbFieldType.OY, protoName: 'msgTx') + ..aInt64(2, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'input') + ..aInt64(3, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'output') + ..a<$core.double>(4, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'vSize', $pb.PbFieldType.OD, protoName: 'vSize') + ..aInt64(5, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'fees') + ..a<$core.int>(6, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'targetConfirmations', $pb.PbFieldType.O3, protoName: 'targetConfirmations') + ..hasRequiredFields = false + ; + + RawSlotSweepTransaction._() : super(); + factory RawSlotSweepTransaction({ + $core.List<$core.int>? msgTx, + $fixnum.Int64? input, + $fixnum.Int64? output, + $core.double? vSize, + $fixnum.Int64? fees, + $core.int? targetConfirmations, + }) { + final _result = create(); + if (msgTx != null) { + _result.msgTx = msgTx; + } + if (input != null) { + _result.input = input; + } + if (output != null) { + _result.output = output; + } + if (vSize != null) { + _result.vSize = vSize; + } + if (fees != null) { + _result.fees = fees; + } + if (targetConfirmations != null) { + _result.targetConfirmations = targetConfirmations; + } + return _result; + } + factory RawSlotSweepTransaction.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory RawSlotSweepTransaction.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + RawSlotSweepTransaction clone() => RawSlotSweepTransaction()..mergeFromMessage(this); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + RawSlotSweepTransaction copyWith(void Function(RawSlotSweepTransaction) updates) => super.copyWith((message) => updates(message as RawSlotSweepTransaction)) as RawSlotSweepTransaction; // ignore: deprecated_member_use + $pb.BuilderInfo get info_ => _i; + @$core.pragma('dart2js:noInline') + static RawSlotSweepTransaction create() => RawSlotSweepTransaction._(); + RawSlotSweepTransaction createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static RawSlotSweepTransaction getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static RawSlotSweepTransaction? _defaultInstance; + + @$pb.TagNumber(1) + $core.List<$core.int> get msgTx => $_getN(0); + @$pb.TagNumber(1) + set msgTx($core.List<$core.int> v) { $_setBytes(0, v); } + @$pb.TagNumber(1) + $core.bool hasMsgTx() => $_has(0); + @$pb.TagNumber(1) + void clearMsgTx() => clearField(1); + + @$pb.TagNumber(2) + $fixnum.Int64 get input => $_getI64(1); + @$pb.TagNumber(2) + set input($fixnum.Int64 v) { $_setInt64(1, v); } + @$pb.TagNumber(2) + $core.bool hasInput() => $_has(1); + @$pb.TagNumber(2) + void clearInput() => clearField(2); + + @$pb.TagNumber(3) + $fixnum.Int64 get output => $_getI64(2); + @$pb.TagNumber(3) + set output($fixnum.Int64 v) { $_setInt64(2, v); } + @$pb.TagNumber(3) + $core.bool hasOutput() => $_has(2); + @$pb.TagNumber(3) + void clearOutput() => clearField(3); + + @$pb.TagNumber(4) + $core.double get vSize => $_getN(3); + @$pb.TagNumber(4) + set vSize($core.double v) { $_setDouble(3, v); } + @$pb.TagNumber(4) + $core.bool hasVSize() => $_has(3); + @$pb.TagNumber(4) + void clearVSize() => clearField(4); + + @$pb.TagNumber(5) + $fixnum.Int64 get fees => $_getI64(4); + @$pb.TagNumber(5) + set fees($fixnum.Int64 v) { $_setInt64(4, v); } + @$pb.TagNumber(5) + $core.bool hasFees() => $_has(4); + @$pb.TagNumber(5) + void clearFees() => clearField(5); + + @$pb.TagNumber(6) + $core.int get targetConfirmations => $_getIZ(5); + @$pb.TagNumber(6) + set targetConfirmations($core.int v) { $_setSignedInt32(5, v); } + @$pb.TagNumber(6) + $core.bool hasTargetConfirmations() => $_has(5); + @$pb.TagNumber(6) + void clearTargetConfirmations() => clearField(6); +} + +class CreateSlotSweepResponse extends $pb.GeneratedMessage { + static final $pb.BuilderInfo _i = $pb.BuilderInfo(const $core.bool.fromEnvironment('protobuf.omit_message_names') ? '' : 'CreateSlotSweepResponse', package: const $pb.PackageName(const $core.bool.fromEnvironment('protobuf.omit_message_names') ? '' : 'data'), createEmptyInstance: create) + ..pc(1, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'txs', $pb.PbFieldType.PM, subBuilder: RawSlotSweepTransaction.create) + ..hasRequiredFields = false + ; + + CreateSlotSweepResponse._() : super(); + factory CreateSlotSweepResponse({ + $core.Iterable? txs, + }) { + final _result = create(); + if (txs != null) { + _result.txs.addAll(txs); + } + return _result; + } + factory CreateSlotSweepResponse.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory CreateSlotSweepResponse.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + CreateSlotSweepResponse clone() => CreateSlotSweepResponse()..mergeFromMessage(this); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + CreateSlotSweepResponse copyWith(void Function(CreateSlotSweepResponse) updates) => super.copyWith((message) => updates(message as CreateSlotSweepResponse)) as CreateSlotSweepResponse; // ignore: deprecated_member_use + $pb.BuilderInfo get info_ => _i; + @$core.pragma('dart2js:noInline') + static CreateSlotSweepResponse create() => CreateSlotSweepResponse._(); + CreateSlotSweepResponse createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static CreateSlotSweepResponse getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static CreateSlotSweepResponse? _defaultInstance; + + @$pb.TagNumber(1) + $core.List get txs => $_getList(0); +} + +class SignSlotSweepRequest extends $pb.GeneratedMessage { + static final $pb.BuilderInfo _i = $pb.BuilderInfo(const $core.bool.fromEnvironment('protobuf.omit_message_names') ? '' : 'SignSlotSweepRequest', package: const $pb.PackageName(const $core.bool.fromEnvironment('protobuf.omit_message_names') ? '' : 'data'), createEmptyInstance: create) + ..aOM(1, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'addressInfo', protoName: 'addressInfo', subBuilder: AddressInfo.create) + ..aOM(2, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'transaction', subBuilder: RawSlotSweepTransaction.create) + ..a<$core.List<$core.int>>(3, const $core.bool.fromEnvironment('protobuf.omit_field_names') ? '' : 'privateKey', $pb.PbFieldType.OY, protoName: 'privateKey') + ..hasRequiredFields = false + ; + + SignSlotSweepRequest._() : super(); + factory SignSlotSweepRequest({ + AddressInfo? addressInfo, + RawSlotSweepTransaction? transaction, + $core.List<$core.int>? privateKey, + }) { + final _result = create(); + if (addressInfo != null) { + _result.addressInfo = addressInfo; + } + if (transaction != null) { + _result.transaction = transaction; + } + if (privateKey != null) { + _result.privateKey = privateKey; + } + return _result; + } + factory SignSlotSweepRequest.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory SignSlotSweepRequest.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + SignSlotSweepRequest clone() => SignSlotSweepRequest()..mergeFromMessage(this); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + SignSlotSweepRequest copyWith(void Function(SignSlotSweepRequest) updates) => super.copyWith((message) => updates(message as SignSlotSweepRequest)) as SignSlotSweepRequest; // ignore: deprecated_member_use + $pb.BuilderInfo get info_ => _i; + @$core.pragma('dart2js:noInline') + static SignSlotSweepRequest create() => SignSlotSweepRequest._(); + SignSlotSweepRequest createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static SignSlotSweepRequest getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static SignSlotSweepRequest? _defaultInstance; + + @$pb.TagNumber(1) + AddressInfo get addressInfo => $_getN(0); + @$pb.TagNumber(1) + set addressInfo(AddressInfo v) { setField(1, v); } + @$pb.TagNumber(1) + $core.bool hasAddressInfo() => $_has(0); + @$pb.TagNumber(1) + void clearAddressInfo() => clearField(1); + @$pb.TagNumber(1) + AddressInfo ensureAddressInfo() => $_ensure(0); + + @$pb.TagNumber(2) + RawSlotSweepTransaction get transaction => $_getN(1); + @$pb.TagNumber(2) + set transaction(RawSlotSweepTransaction v) { setField(2, v); } + @$pb.TagNumber(2) + $core.bool hasTransaction() => $_has(1); + @$pb.TagNumber(2) + void clearTransaction() => clearField(2); + @$pb.TagNumber(2) + RawSlotSweepTransaction ensureTransaction() => $_ensure(1); + + @$pb.TagNumber(3) + $core.List<$core.int> get privateKey => $_getN(2); + @$pb.TagNumber(3) + set privateKey($core.List<$core.int> v) { $_setBytes(2, v); } + @$pb.TagNumber(3) + $core.bool hasPrivateKey() => $_has(2); + @$pb.TagNumber(3) + void clearPrivateKey() => clearField(3); +} + diff --git a/lib/services/breezlib/data/messages.pbjson.dart b/lib/services/breezlib/data/messages.pbjson.dart index d22f8bec0..1d29b5ab7 100644 --- a/lib/services/breezlib/data/messages.pbjson.dart +++ b/lib/services/breezlib/data/messages.pbjson.dart @@ -1073,3 +1073,77 @@ const TorConfig$json = const { /// Descriptor for `TorConfig`. Decode as a `google.protobuf.DescriptorProto`. final $typed_data.Uint8List torConfigDescriptor = $convert.base64Decode('CglUb3JDb25maWcSGAoHY29udHJvbBgBIAEoCVIHY29udHJvbBISCgRodHRwGAIgASgJUgRodHRwEhQKBXNvY2tzGAMgASgJUgVzb2Nrcw=='); +@$core.Deprecated('Use utxoDescriptor instead') +const Utxo$json = const { + '1': 'Utxo', + '2': const [ + const {'1': 'txid', '3': 1, '4': 1, '5': 9, '10': 'txid'}, + const {'1': 'vout', '3': 2, '4': 1, '5': 13, '10': 'vout'}, + const {'1': 'value', '3': 3, '4': 1, '5': 3, '10': 'value'}, + const {'1': 'isConfirmed', '3': 4, '4': 1, '5': 8, '10': 'isConfirmed'}, + ], +}; + +/// Descriptor for `Utxo`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List utxoDescriptor = $convert.base64Decode('CgRVdHhvEhIKBHR4aWQYASABKAlSBHR4aWQSEgoEdm91dBgCIAEoDVIEdm91dBIUCgV2YWx1ZRgDIAEoA1IFdmFsdWUSIAoLaXNDb25maXJtZWQYBCABKAhSC2lzQ29uZmlybWVk'); +@$core.Deprecated('Use addressInfoDescriptor instead') +const AddressInfo$json = const { + '1': 'AddressInfo', + '2': const [ + const {'1': 'address', '3': 1, '4': 1, '5': 9, '10': 'address'}, + const {'1': 'confirmedBalance', '3': 2, '4': 1, '5': 3, '10': 'confirmedBalance'}, + const {'1': 'unconfirmedBalance', '3': 3, '4': 1, '5': 3, '10': 'unconfirmedBalance'}, + const {'1': 'utxos', '3': 4, '4': 3, '5': 11, '6': '.data.Utxo', '10': 'utxos'}, + ], +}; + +/// Descriptor for `AddressInfo`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List addressInfoDescriptor = $convert.base64Decode('CgtBZGRyZXNzSW5mbxIYCgdhZGRyZXNzGAEgASgJUgdhZGRyZXNzEioKEGNvbmZpcm1lZEJhbGFuY2UYAiABKANSEGNvbmZpcm1lZEJhbGFuY2USLgoSdW5jb25maXJtZWRCYWxhbmNlGAMgASgDUhJ1bmNvbmZpcm1lZEJhbGFuY2USIAoFdXR4b3MYBCADKAsyCi5kYXRhLlV0eG9SBXV0eG9z'); +@$core.Deprecated('Use createSlotSweepRequestDescriptor instead') +const CreateSlotSweepRequest$json = const { + '1': 'CreateSlotSweepRequest', + '2': const [ + const {'1': 'slot', '3': 1, '4': 1, '5': 11, '6': '.data.AddressInfo', '10': 'slot'}, + const {'1': 'recipient', '3': 2, '4': 1, '5': 9, '10': 'recipient'}, + ], +}; + +/// Descriptor for `CreateSlotSweepRequest`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List createSlotSweepRequestDescriptor = $convert.base64Decode('ChZDcmVhdGVTbG90U3dlZXBSZXF1ZXN0EiUKBHNsb3QYASABKAsyES5kYXRhLkFkZHJlc3NJbmZvUgRzbG90EhwKCXJlY2lwaWVudBgCIAEoCVIJcmVjaXBpZW50'); +@$core.Deprecated('Use rawSlotSweepTransactionDescriptor instead') +const RawSlotSweepTransaction$json = const { + '1': 'RawSlotSweepTransaction', + '2': const [ + const {'1': 'msgTx', '3': 1, '4': 1, '5': 12, '10': 'msgTx'}, + const {'1': 'input', '3': 2, '4': 1, '5': 3, '10': 'input'}, + const {'1': 'output', '3': 3, '4': 1, '5': 3, '10': 'output'}, + const {'1': 'vSize', '3': 4, '4': 1, '5': 1, '10': 'vSize'}, + const {'1': 'fees', '3': 5, '4': 1, '5': 3, '10': 'fees'}, + const {'1': 'targetConfirmations', '3': 6, '4': 1, '5': 5, '10': 'targetConfirmations'}, + ], +}; + +/// Descriptor for `RawSlotSweepTransaction`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List rawSlotSweepTransactionDescriptor = $convert.base64Decode('ChdSYXdTbG90U3dlZXBUcmFuc2FjdGlvbhIUCgVtc2dUeBgBIAEoDFIFbXNnVHgSFAoFaW5wdXQYAiABKANSBWlucHV0EhYKBm91dHB1dBgDIAEoA1IGb3V0cHV0EhQKBXZTaXplGAQgASgBUgV2U2l6ZRISCgRmZWVzGAUgASgDUgRmZWVzEjAKE3RhcmdldENvbmZpcm1hdGlvbnMYBiABKAVSE3RhcmdldENvbmZpcm1hdGlvbnM='); +@$core.Deprecated('Use createSlotSweepResponseDescriptor instead') +const CreateSlotSweepResponse$json = const { + '1': 'CreateSlotSweepResponse', + '2': const [ + const {'1': 'txs', '3': 1, '4': 3, '5': 11, '6': '.data.RawSlotSweepTransaction', '10': 'txs'}, + ], +}; + +/// Descriptor for `CreateSlotSweepResponse`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List createSlotSweepResponseDescriptor = $convert.base64Decode('ChdDcmVhdGVTbG90U3dlZXBSZXNwb25zZRIvCgN0eHMYASADKAsyHS5kYXRhLlJhd1Nsb3RTd2VlcFRyYW5zYWN0aW9uUgN0eHM='); +@$core.Deprecated('Use signSlotSweepRequestDescriptor instead') +const SignSlotSweepRequest$json = const { + '1': 'SignSlotSweepRequest', + '2': const [ + const {'1': 'addressInfo', '3': 1, '4': 1, '5': 11, '6': '.data.AddressInfo', '10': 'addressInfo'}, + const {'1': 'transaction', '3': 2, '4': 1, '5': 11, '6': '.data.RawSlotSweepTransaction', '10': 'transaction'}, + const {'1': 'privateKey', '3': 3, '4': 1, '5': 12, '10': 'privateKey'}, + ], +}; + +/// Descriptor for `SignSlotSweepRequest`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List signSlotSweepRequestDescriptor = $convert.base64Decode('ChRTaWduU2xvdFN3ZWVwUmVxdWVzdBIzCgthZGRyZXNzSW5mbxgBIAEoCzIRLmRhdGEuQWRkcmVzc0luZm9SC2FkZHJlc3NJbmZvEj8KC3RyYW5zYWN0aW9uGAIgASgLMh0uZGF0YS5SYXdTbG90U3dlZXBUcmFuc2FjdGlvblILdHJhbnNhY3Rpb24SHgoKcHJpdmF0ZUtleRgDIAEoDFIKcHJpdmF0ZUtleQ=='); diff --git a/lib/user_app.dart b/lib/user_app.dart index 6574c09ec..d68538322 100644 --- a/lib/user_app.dart +++ b/lib/user_app.dart @@ -10,6 +10,7 @@ import 'package:breez/bloc/lsp/lsp_bloc.dart'; import 'package:breez/bloc/pos_catalog/bloc.dart'; import 'package:breez/bloc/pos_catalog/model.dart'; import 'package:breez/bloc/reverse_swap/reverse_swap_bloc.dart'; +import 'package:breez/bloc/satscard/satscard_bloc.dart'; import 'package:breez/bloc/user_profile/breez_user_model.dart'; import 'package:breez/bloc/user_profile/user_actions.dart'; import 'package:breez/bloc/user_profile/user_profile_bloc.dart'; @@ -76,6 +77,7 @@ class UserApp extends StatelessWidget { var connectPayBloc = AppBlocsProvider.of(context); var lspBloc = AppBlocsProvider.of(context); var reverseSwapBloc = AppBlocsProvider.of(context); + var satscardBloc = AppBlocsProvider.of(context); var lnurlBloc = AppBlocsProvider.of(context); var posCatalogBloc = AppBlocsProvider.of(context); @@ -205,6 +207,7 @@ class UserApp extends StatelessWidget { backupBloc, lspBloc, reverseSwapBloc, + satscardBloc, lnurlBloc, ), settings: settings, From dd4f996dacde16504c4827dcb8e36408f7b15ba1 Mon Sep 17 00:00:00 2001 From: Peter Date: Wed, 31 Jan 2024 21:43:17 +0000 Subject: [PATCH 4/6] Add "Sweep Satscard" option to receive list - Opens the NFC prompt on iOS so Satscards can be scanned - Android doesn't need this because we are always scanning for NFC devices in the background --- lib/routes/home/bottom_actions_bar.dart | 61 +++++++++++++++++++++++-- 1 file changed, 57 insertions(+), 4 deletions(-) diff --git a/lib/routes/home/bottom_actions_bar.dart b/lib/routes/home/bottom_actions_bar.dart index b5c9481c1..65b1fd665 100644 --- a/lib/routes/home/bottom_actions_bar.dart +++ b/lib/routes/home/bottom_actions_bar.dart @@ -1,4 +1,5 @@ import 'dart:async'; +import 'dart:io'; import 'package:auto_size_text/auto_size_text.dart'; import 'package:breez/bloc/account/account_bloc.dart'; @@ -12,6 +13,7 @@ import 'package:breez/bloc/lnurl/lnurl_bloc.dart'; import 'package:breez/bloc/lsp/lsp_bloc.dart'; import 'package:breez/bloc/lsp/lsp_model.dart'; import 'package:breez/routes/spontaneous_payment/spontaneous_payment_page.dart'; +import 'package:breez/services/injector.dart'; import 'package:breez/theme_data.dart' as theme; import 'package:breez/utils/dynamic_fees.dart'; import 'package:breez/utils/exceptions.dart'; @@ -25,6 +27,7 @@ import 'package:breez/widgets/warning_box.dart'; import 'package:breez_translations/breez_translations_locales.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:flutter_svg/svg.dart'; class BottomActionsBar extends StatefulWidget { final GlobalKey firstPaymentItemKey; @@ -293,22 +296,37 @@ class _Action extends StatelessWidget { class _ActionImage extends StatelessWidget { final String iconAssetPath; + final String svgAssetPath; final bool enabled; const _ActionImage({ Key key, this.iconAssetPath, + this.svgAssetPath, this.enabled = true, }) : super(key: key); @override Widget build(BuildContext context) { + final color = enabled ? Colors.white : Theme.of(context).disabledColor; + const fit = BoxFit.contain; + const height = 24.0; + const width = 24.0; + if (svgAssetPath != null && svgAssetPath.isNotEmpty) { + return SvgPicture.asset( + svgAssetPath, + colorFilter: ColorFilter.mode(color, BlendMode.srcATop), + fit: BoxFit.contain, + width: width, + height: height, + ); + } return Image( image: AssetImage(iconAssetPath), - color: enabled ? Colors.white : Theme.of(context).disabledColor, - fit: BoxFit.contain, - width: 24.0, - height: 24.0, + color: color, + fit: fit, + width: width, + height: height, ); } } @@ -415,6 +433,41 @@ Future showReceiveOptions( }, ).toList(); + // We need an option to scan Satscards on iOS. Unnecessary on Android due to background scanning + final nfc = ServiceInjector().nfc; + if (Platform.isIOS && nfc.isAvailable) { + children.add( + Column( + children: [ + Divider( + height: 0.0, + color: themeData.dividerColor.withOpacity(0.2), + indent: 72.0, + ), + ListTile( + leading: const _ActionImage( + enabled: true, + svgAssetPath: "src/icon/nfc.svg", + ), + title: Text( + texts.bottom_action_bar_sweep_satscard, + style: theme.bottomSheetTextStyle, + ), + onTap: () { + Navigator.of(context).pop(); + ServiceInjector().nfc.startSession( + autoClose: false, + satscardOnly: true, + iosAlert: texts + .bottom_action_bar_sweep_satscard_nfc_prompt, + ); + }, + ), + ], + ), + ); + } + return Column( mainAxisSize: MainAxisSize.min, children: [ From b55ecf1abebae6f62fe9334f56c4286831d3d4fa Mon Sep 17 00:00:00 2001 From: Peter Date: Wed, 31 Jan 2024 21:46:23 +0000 Subject: [PATCH 5/6] Add InitializeSatscardPage - Allows the user to prepare the next Satscard slot for usage - Includes the option to specify their own chain code --- lib/home_page.dart | 87 ++++ .../initialize_satscard_page.dart | 123 ++++++ lib/user_app.dart | 8 + lib/widgets/satscard/chain_code_field.dart | 89 ++++ .../satscard/satscard_operation_dialog.dart | 390 ++++++++++++++++++ lib/widgets/satscard/spend_code_field.dart | 91 ++++ 6 files changed, 788 insertions(+) create mode 100644 lib/routes/initialize_satscard/initialize_satscard_page.dart create mode 100644 lib/widgets/satscard/chain_code_field.dart create mode 100644 lib/widgets/satscard/satscard_operation_dialog.dart create mode 100644 lib/widgets/satscard/spend_code_field.dart diff --git a/lib/home_page.dart b/lib/home_page.dart index afb3b5d38..7ebbd3da3 100644 --- a/lib/home_page.dart +++ b/lib/home_page.dart @@ -1,4 +1,5 @@ import 'dart:async'; +import 'dart:io'; import 'package:anytime/bloc/podcast/audio_bloc.dart'; import 'package:anytime/ui/anytime_podcast_app.dart'; @@ -15,6 +16,8 @@ import 'package:breez/bloc/invoice/invoice_bloc.dart'; import 'package:breez/bloc/lnurl/lnurl_bloc.dart'; import 'package:breez/bloc/lsp/lsp_bloc.dart'; import 'package:breez/bloc/reverse_swap/reverse_swap_bloc.dart'; +import 'package:breez/bloc/satscard/detected_satscard_status.dart'; +import 'package:breez/bloc/satscard/satscard_actions.dart'; import 'package:breez/bloc/satscard/satscard_bloc.dart'; import 'package:breez/bloc/user_profile/breez_user_model.dart'; import 'package:breez/bloc/user_profile/user_profile_bloc.dart'; @@ -50,6 +53,8 @@ import 'package:breez/widgets/lost_card_dialog.dart' as lostCard; import 'package:breez/widgets/payment_failed_report_dialog.dart'; import 'package:breez/widgets/route.dart'; import 'package:breez_translations/breez_translations_locales.dart'; +import 'package:breez_translations/generated/breez_translations.dart'; +import 'package:cktap_protocol/satscard.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; @@ -173,6 +178,7 @@ class HomeState extends State with WidgetsBindingObserver { _listenWhitelistPermissionsRequest(); _listenLSPSelectionPrompt(); _listenPaymentResults(); + _listenSatscards(); } @override @@ -527,6 +533,87 @@ class HomeState extends State with WidgetsBindingObserver { ); } + void _listenSatscards() { + widget.satscardBloc.actionsSink.add(EnableListening()); + widget.satscardBloc.detectedStream.listen((status) async { + Navigator.popUntil(context, (route) => route.settings.name == "/"); + final nfc = ServiceInjector().nfc; + final texts = context.texts(); + final themeData = Theme.of(context); + if (_handleSatscardErrors(themeData, texts, status)) { + if (Platform.isIOS) { + nfc.stopSession(iosError: texts.satscard_ios_error_label); + } + return; + } + if (Platform.isIOS) { + nfc.stopSession(iosAlert: texts.satscard_ios_success_label); + } + + // Disable listening until we finish operating on the detected card + widget.satscardBloc.actionsSink.add(DisableListening()); + Future future; + if (status is DetectedSweepableSatscardStatus) { + future = _handleSweepableSatscard(status.card, status.slot); + } else if (status is DetectedUnusedSatscardStatus) { + future = _handleUnusedSatscard(themeData, texts, status.card); + } + + // Pages may choose to enable listening themselves at a later time, e.g. + // InitializeSatscardPage upon successful slot initialization + await future.then((result) { + final shouldEnableListening = result == null || result == true; + if (shouldEnableListening) { + widget.satscardBloc.actionsSink.add(EnableListening()); + } + }); + }); + } + + bool _handleSatscardErrors( + ThemeData themeData, + BreezTranslations texts, + DetectedSatscardStatus status, + ) { + void showPrompt(String title, String body) => promptMessage(context, title, + Text(body, style: themeData.dialogTheme.contentTextStyle)); + + if (status is DetectedNoSatscardStatus) { + showPrompt(texts.satscard_error_nfc_title, texts.satscard_error_nfc_body); + return true; + } else if (status is DetectedInvalidSatscardStatus) { + showPrompt(texts.satscard_error_invalid_title, + texts.satscard_error_invalid_body(status.message)); + return true; + } + if (status is DetectedUsedUpSatscardStatus) { + showPrompt(texts.satscard_error_used_up_title, + texts.satscard_error_used_up_body); + return true; + } + return false; + } + + Future _handleSweepableSatscard(Satscard card, Slot slot) => + Navigator.pushNamed(context, "/satscard_balance", + arguments: {"card": card, "slot": slot}); + + Future _handleUnusedSatscard( + ThemeData themeData, BreezTranslations texts, Satscard card) { + return promptAreYouSure( + context, + texts.satscard_unused_prompt_title, + Text( + texts.satscard_unused_prompt_body, + style: themeData.dialogTheme.contentTextStyle, + ), + okText: texts.satscard_dialog_ok, + cancelText: texts.satscard_dialog_cancel, + ).then((result) => result + ? Navigator.pushNamed(context, "/initialize_satscard", arguments: card) + : null); + } + List _filterItems(List items) { return items.where((c) => !_hiddenRoutes.contains(c.name)).toList(); } diff --git a/lib/routes/initialize_satscard/initialize_satscard_page.dart b/lib/routes/initialize_satscard/initialize_satscard_page.dart new file mode 100644 index 000000000..6536bc7b4 --- /dev/null +++ b/lib/routes/initialize_satscard/initialize_satscard_page.dart @@ -0,0 +1,123 @@ +import 'dart:collection'; + +import 'package:auto_size_text/auto_size_text.dart'; +import 'package:breez/bloc/blocs_provider.dart'; +import 'package:breez/bloc/satscard/satscard_actions.dart'; +import 'package:breez/bloc/satscard/satscard_bloc.dart'; +import 'package:breez/bloc/satscard/satscard_op_status.dart'; +import 'package:breez/services/injector.dart'; +import 'package:breez/theme_data.dart' as theme; +import 'package:breez/utils/min_font_size.dart'; +import 'package:breez/widgets/back_button.dart' as backBtn; +import 'package:breez/widgets/flushbar.dart'; +import 'package:breez/widgets/satscard/chain_code_field.dart'; +import 'package:breez/widgets/satscard/satscard_operation_dialog.dart'; +import 'package:breez/widgets/satscard/spend_code_field.dart'; +import 'package:breez/widgets/single_button_bottom_bar.dart'; +import 'package:breez_translations/breez_translations_locales.dart'; +import 'package:cktap_protocol/cktapcard.dart'; +import 'package:flutter/material.dart'; + +class InitializeSatscardPage extends StatelessWidget { + final _formKey = GlobalKey(); + final _scaffoldKey = GlobalKey(); + final _spendCodeController = TextEditingController(); + final _chainCodeController = TextEditingController(); + final _spendCodeFocusNode = FocusNode(); + final _chainCodeFocusNode = FocusNode(); + final _incorrectCodes = + HashSet(equals: (a, b) => a == b, hashCode: (a) => a.hashCode); + + final Satscard _card; + final String _activeSlot; + + InitializeSatscardPage(this._card) + : _activeSlot = (_card.activeSlotIndex + 1).toString(); + + @override + Widget build(BuildContext context) { + final texts = context.texts(); + return Scaffold( + key: _scaffoldKey, + appBar: AppBar( + leading: const backBtn.BackButton(), + title: Text(texts.satscard_initialize_title(_activeSlot)), + ), + bottomNavigationBar: SingleButtonBottomBar( + stickToBottom: true, + text: texts.satscard_initialize_button_label, + onPressed: () { + if (_formKey.currentState.validate()) { + final bloc = AppBlocsProvider.of(context); + final action = InitializeSlot( + _card, _spendCodeController.text, _chainCodeController.text); + bloc.actionsSink.add(action); + + showSatscardOperationDialog(context, bloc, _card.ident) + .then((result) { + if (result is SatscardOpStatusBadAuth) { + _incorrectCodes.add(action.spendCode); + _formKey.currentState.validate(); + } else if (result is SatscardOpStatusSuccess) { + Navigator.of(context).pushReplacementNamed("/satscard_balance", + result: false, + arguments: { + "card": result.card, + "slot": result.slot + }).then((_) => bloc.actionsSink.add(EnableListening())); + } + }); + } + }, + ), + body: Form( + key: _formKey, + child: Padding( + padding: const EdgeInsets.fromLTRB(16.0, 24.0, 16.0, 40.0), + child: Scrollbar( + child: SingleChildScrollView( + child: Column( + mainAxisSize: MainAxisSize.max, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SpendCodeFormField( + context: context, + texts: texts, + focusNode: _spendCodeFocusNode, + controller: _spendCodeController, + style: theme.FieldTextStyle.textStyle, + validatorFn: (code) => _incorrectCodes.contains(code) + ? texts.satscard_spend_code_incorrect_code_hint + : null), + ChainCodeFormField( + context: context, + texts: texts, + focusNode: _chainCodeFocusNode, + controller: _chainCodeController, + style: theme.FieldTextStyle.textStyle, + ), + Container( + padding: const EdgeInsets.only(top: 16.0), + child: GestureDetector( + onTap: () { + ServiceInjector().device.setClipboardText(_card.ident); + showFlushbar(context, + message: texts.satscard_card_id_copied); + }, + child: AutoSizeText( + texts.satscard_card_id_text(_card.ident), + style: theme.textStyle, + maxLines: 1, + minFontSize: MinFontSize(context).minFontSize, + ), + ), + ), + ], + ), + ), + ), + ), + ), + ); + } +} diff --git a/lib/user_app.dart b/lib/user_app.dart index d68538322..3ceaa7be7 100644 --- a/lib/user_app.dart +++ b/lib/user_app.dart @@ -25,6 +25,7 @@ import 'package:breez/routes/dev/dev.dart'; import 'package:breez/routes/fiat_currencies/fiat_currency_settings.dart'; import 'package:breez/routes/get_refund/get_refund_page.dart'; import 'package:breez/routes/initial_walkthrough/initial_walkthrough.dart'; +import 'package:breez/routes/initialize_satscard/initialize_satscard_page.dart'; import 'package:breez/routes/lsp/select_lsp_page.dart'; import 'package:breez/routes/marketplace/marketplace.dart'; import 'package:breez/routes/network/network.dart'; @@ -44,6 +45,7 @@ import 'package:breez/theme_data.dart' as theme; import 'package:breez/widgets/route.dart'; import 'package:breez/widgets/static_loader.dart'; import 'package:breez_translations/breez_translations_locales.dart'; +import 'package:cktap_protocol/cktapcard.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_easyloading/flutter_easyloading.dart'; @@ -367,6 +369,12 @@ class UserApp extends StatelessWidget { builder: (_) => QRScan(), settings: settings, ); + case '/initialize_satscard': + return FadeInRoute ( + builder: (_) => + InitializeSatscardPage(settings.arguments as Satscard), + settings: settings, + ); } assert(false); }, diff --git a/lib/widgets/satscard/chain_code_field.dart b/lib/widgets/satscard/chain_code_field.dart new file mode 100644 index 000000000..acdae4e05 --- /dev/null +++ b/lib/widgets/satscard/chain_code_field.dart @@ -0,0 +1,89 @@ +import 'dart:math'; + +import 'package:breez_translations/generated/breez_translations.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + +class ChainCodeFormField extends TextFormField { + final String Function(String) validatorFn; + final BreezTranslations texts; + + ChainCodeFormField({ + this.validatorFn, + this.texts, + BuildContext context, + TextEditingController controller, + InputDecoration decoration = const InputDecoration(), + bool enabled, + Key key, + FocusNode focusNode, + int maxLength = 64, + int maxLines = 2, + ValueChanged onFieldSubmitted, + FormFieldSetter onSaved, + ValueChanged onChanged, + TextStyle style, + }) : super( + autocorrect: false, + controller: controller, + decoration: InputDecoration( + labelText: texts.satscard_chain_code_label, + ), + enabled: enabled, + enableSuggestions: false, + focusNode: focusNode, + inputFormatters: [ChainCodeFieldFormatter()], + keyboardType: TextInputType.text, + maxLength: maxLength, + maxLines: maxLines, + onChanged: onChanged, + onFieldSubmitted: onFieldSubmitted, + onSaved: onSaved, + style: style, + ); + + @override + FormFieldValidator get validator { + return (value) { + if (value.isNotEmpty && value.length != 64) { + return texts.satscard_chain_code_wrong_hint; + } else { + try { + if (validatorFn != null) { + return validatorFn(value); + } + } catch (_) {} + + return null; + } + }; + } +} + +class ChainCodeFieldFormatter extends TextInputFormatter { + final RegExp _pattern = RegExp(r'[^\da-fA-F]'); + + @override + TextEditingValue formatEditUpdate( + TextEditingValue oldValue, + TextEditingValue newValue, + ) { + const maxLength = 64; + + var text = newValue.text.replaceAll(_pattern, "").toUpperCase(); + var offset = min(min(newValue.selection.start, maxLength), text.length); + if (text.isEmpty) { + return newValue.copyWith( + text: "", + selection: const TextSelection.collapsed(offset: 0), + ); + } else if (text.length > maxLength) { + text = text.substring(0, maxLength); + } + + return newValue.copyWith( + text: text, + selection: TextSelection.collapsed(offset: offset), + ); + } +} diff --git a/lib/widgets/satscard/satscard_operation_dialog.dart b/lib/widgets/satscard/satscard_operation_dialog.dart new file mode 100644 index 000000000..f4b95432c --- /dev/null +++ b/lib/widgets/satscard/satscard_operation_dialog.dart @@ -0,0 +1,390 @@ +import 'dart:async'; +import 'dart:io'; + +import 'package:breez/bloc/satscard/satscard_actions.dart'; +import 'package:breez/bloc/satscard/satscard_bloc.dart'; +import 'package:breez/bloc/satscard/satscard_op_status.dart'; +import 'package:breez/services/injector.dart'; +import 'package:breez/services/nfc.dart'; +import 'package:breez/widgets/circular_progress.dart'; +import 'package:breez_translations/breez_translations_locales.dart'; +import 'package:breez_translations/generated/breez_translations.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; + +Future showSatscardOperationDialog( + BuildContext context, SatscardBloc bloc, String cardId) { + return showDialog( + useRootNavigator: false, + barrierDismissible: false, + context: context, + builder: (_) => SatscardOperationDialog(bloc, cardId), + ); +} + +class SatscardOperationDialog extends StatefulWidget { + final SatscardBloc _bloc; + final String _cardId; + + const SatscardOperationDialog( + this._bloc, + this._cardId, + ); + + @override + State createState() => SatscardOperationDialogState(); +} + +class SatscardOperationDialogState extends State + with SingleTickerProviderStateMixin { + AnimationController _animationController; + Animation _opacityAnimation; + NFCService _nfc; + bool _isClosing; + _BuildInfo _info; + StreamSubscription _operationSubscription; + + static const _iconHeight = 64.0; + + @override + void dispose() { + _animationController?.dispose(); + _operationSubscription?.cancel(); + super.dispose(); + } + + @override + void didChangeDependencies() { + super.didChangeDependencies(); + _isClosing = false; + _animationController = AnimationController( + vsync: this, + duration: const Duration(milliseconds: 500), + ); + _opacityAnimation = Tween( + begin: 0.0, + end: 1.0, + ).animate(CurvedAnimation( + parent: _animationController, + curve: Curves.ease, + )); + _animationController.forward(); + + // We need to initialize NFC on iOS devices + _nfc = ServiceInjector().nfc; + if (Platform.isIOS) { + _startNfcSession(context.texts()); + } + } + + @override + Widget build(BuildContext context) { + final themeData = Theme.of(context); + final texts = context.texts(); + + return StreamBuilder( + stream: widget._bloc.operationStream, + builder: (context, snapshot) { + final data = snapshot.data; + _handleExitConditions(data); + _setBuildInfo(_BuildInfo(data, widget._cardId, texts)); + + return FadeTransition( + opacity: _opacityAnimation, + child: SimpleDialog( + title: _buildTitle(themeData, texts), + titlePadding: const EdgeInsets.fromLTRB(20.0, 22.0, 20.0, 8.0), + contentPadding: const EdgeInsets.fromLTRB(20.0, 0.0, 20.0, 20.0), + children: [ + SizedBox( + width: 310, + height: 250, + child: Padding( + padding: const EdgeInsets.only(top: 60), + child: _buildContentForState(context, data), + ), + ), + _buildCancelButton(themeData, texts), + ], + ), + ); + }); + } + + Widget _buildTitle(ThemeData themeData, BreezTranslations texts) { + final text = Text( + texts.satscard_operation_dialog_title, + style: themeData.dialogTheme.titleTextStyle, + ); + + // We don't need to show an NFC icon on Android as we constantly scan in the background + if (Platform.isIOS) { + return Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded(child: text), + IconButton( + icon: _buildNfcIcon(themeData), + onPressed: () => _startNfcSession(texts), + ), + ], + ); + } else { + return text; + } + } + + Widget _buildContentForState(BuildContext context, SatscardOpStatus status) { + final texts = context.texts(); + final themeData = Theme.of(context); + + // Communicating with the user is handled by the system UI on iOS so we + // don't need to update the dialog box + if (Platform.isIOS) { + return Padding( + padding: const EdgeInsets.only(top: 32.0), + child: Text( + texts.satscard_operation_dialog_content_ios_label, + style: TextStyle(color: themeData.dialogTheme.contentTextStyle.color), + textAlign: TextAlign.center, + ), + ); + } + switch (_info.mode) { + case _PresentMode.prompt: + return _buildTextPrompt( + themeData, _buildNfcIcon(themeData), _info.message); + case _PresentMode.progress: + return _buildProgressIndicator(themeData, _info.message, + value: _info.progress); + case _PresentMode.success: + return _buildTextPrompt( + themeData, _buildSuccessIcon(themeData), _info.message); + default: + return _buildTextPrompt( + themeData, _buildErrorIcon(themeData), _info.message); + } + } + + Widget _buildCancelButton(ThemeData themeData, BreezTranslations texts) { + return TextButton( + onPressed: _isClosing ? null : () => _onFinish(false), + child: Text( + texts.satscard_operation_dialog_cancel_label, + style: themeData.primaryTextTheme.labelLarge.copyWith( + color: _isClosing + ? themeData.primaryTextTheme.labelLarge.color.withOpacity(0.4) + : themeData.primaryTextTheme.labelLarge.color, + ), + ), + ); + } + + Widget _buildProgressIndicator(ThemeData themeData, String title, + {double value}) { + return CircularProgress( + color: themeData.dialogTheme.contentTextStyle.color, + mainAxisAlignment: MainAxisAlignment.start, + size: _iconHeight, + title: title, + value: value, + ); + } + + Widget _buildTextPrompt(ThemeData themeData, Widget image, String label) { + return Column( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + image, + Padding( + padding: const EdgeInsets.only(top: _iconHeight / 5), + child: Text( + label, + style: + TextStyle(color: themeData.dialogTheme.contentTextStyle.color), + textAlign: TextAlign.center, + ), + ), + ], + ); + } + + Widget _buildSuccessIcon(ThemeData themeData) { + return Image( + image: const AssetImage("src/icon/ic_done.png"), + height: _iconHeight, + fit: BoxFit.fitHeight, + color: themeData.primaryColorLight, + ); + } + + Widget _buildNfcIcon(ThemeData themeData) { + double height; + Color color; + if (Platform.isIOS) { + color = themeData.primaryTextTheme.labelLarge.color; + } else { + height = _iconHeight; + color = themeData.primaryColorLight; + } + return SvgPicture.asset( + "src/icon/nfc.svg", + height: height, + fit: BoxFit.fitHeight, + colorFilter: ColorFilter.mode( + color, + BlendMode.srcATop, + ), + ); + } + + Widget _buildErrorIcon(ThemeData themeData) { + return SvgPicture.asset( + "src/icon/warning.svg", + height: _iconHeight, + fit: BoxFit.fitHeight, + colorFilter: ColorFilter.mode( + themeData.colorScheme.error, + BlendMode.srcATop, + ), + ); + } + + void _handleExitConditions(SatscardOpStatus status) { + if (_isClosing == true) { + return; + } else if (status is SatscardOpStatusSuccess) { + _onFinish(status, delay: 1.5); + } else if (status is SatscardOpStatusBadAuth) { + _onFinish(status, delay: 1.5); + } + } + + void _onFinish(T result, {double delay = 0.0}) async { + if (_isClosing) { + return; + } + _isClosing = true; + widget._bloc.actionsSink.add(DisableListening()); + + if (delay > 0.0) { + await Future.delayed(Duration(milliseconds: (1000 * delay).toInt())); + await _animationController.reverse(); + } + if (mounted) { + Navigator.of(context).pop(result); + } + } + + void _setBuildInfo(_BuildInfo info) { + // On iOS we need to update system messages and the NFC status. We make + // sure to do so only when the state has changed + bool hasStateChanged = Platform.isIOS && _info != info; + _info = info; + + if (hasStateChanged) { + switch (info.mode) { + case _PresentMode.prompt: + case _PresentMode.progress: + _nfc.updateAlert(info.message); + break; + case _PresentMode.success: + // Close session with a success message + _nfc.stopSession(iosAlert: info.message); + break; + case _PresentMode.error: + // Close session with an error message + _nfc.stopSession(iosError: info.message); + break; + } + } + } + + void _startNfcSession(BreezTranslations texts) { + _nfc.startSession( + autoClose: false, + satscardOnly: true, + iosAlert: texts + .satscard_operation_dialog_present_satscards_label(widget._cardId), + ); + } +} + +class _BuildInfo { + final _PresentMode mode; + final String message; + final double progress; + + factory _BuildInfo(SatscardOpStatus s, String id, BreezTranslations texts) { + if (s is SatscardOpStatusInProgress) { + return _BuildInfo.progress( + texts.satscard_operation_dialog_in_progress_label, null); + } + if (s is SatscardOpStatusWaiting) { + final nominator = s.initialAuthDelay - s.currentAuthDelay; + final percentage = nominator / s.initialAuthDelay; + return _BuildInfo.progress( + Platform.isIOS + ? texts.satscard_operation_dialog_waiting_ios_label(percentage) + : texts.satscard_operation_dialog_waiting_label, + percentage); + } + if (s is SatscardOpStatusSuccess) { + return _BuildInfo.success(texts.satscard_operation_dialog_success_label); + } + if (s is SatscardOpStatusBadAuth) { + return _BuildInfo.error( + texts.satscard_operation_dialog_incorrect_code_label); + } + if (s is SatscardOpStatusIncorrectCard) { + return _BuildInfo.error( + texts.satscard_operation_dialog_incorrect_card_label(id)); + } + if (s is SatscardOpStatusStaleCard) { + return _BuildInfo.error(texts.satscard_operation_dialog_stale_card_label); + } + if (s is SatscardOpStatusNfcError) { + return _BuildInfo.error(texts.satscard_operation_dialog_nfc_error_label); + } + if (s is SatscardOpStatusProtocolError) { + return _BuildInfo.error( + texts.satscard_operation_dialog_protocol_error_label( + s.e.code, s.e.literal, s.e.message)); + } + if (s is SatscardOpStatusUnexpectedError) { + return _BuildInfo.error( + texts.satscard_operation_dialog_unknown_error_label(s.message)); + } + return _BuildInfo.prompt( + texts.satscard_operation_dialog_present_satscards_label(id)); + } + + _BuildInfo.prompt(this.message) + : mode = _PresentMode.prompt, + progress = null; + _BuildInfo.progress(this.message, this.progress) + : mode = _PresentMode.progress; + _BuildInfo.success(this.message) + : mode = _PresentMode.success, + progress = null; + _BuildInfo.error(this.message) + : mode = _PresentMode.error, + progress = null; + + @override + bool operator ==(Object obj) => + obj is _BuildInfo && + obj.mode == mode && + obj.message == message && + obj.progress == progress; + + @override + int get hashCode => Object.hash(mode, message, progress); +} + +enum _PresentMode { + prompt, + progress, + success, + error, +} diff --git a/lib/widgets/satscard/spend_code_field.dart b/lib/widgets/satscard/spend_code_field.dart new file mode 100644 index 000000000..2353aa5aa --- /dev/null +++ b/lib/widgets/satscard/spend_code_field.dart @@ -0,0 +1,91 @@ +import 'dart:math'; + +import 'package:breez_translations/generated/breez_translations.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + +class SpendCodeFormField extends TextFormField { + final String Function(String) validatorFn; + final BreezTranslations texts; + + SpendCodeFormField({ + this.validatorFn, + this.texts, + BuildContext context, + TextEditingController controller, + bool enabled, + FocusNode focusNode, + int maxLength = 6, + int maxLines = 1, + ValueChanged onFieldSubmitted, + FormFieldSetter onSaved, + ValueChanged onChanged, + TextStyle style, + }) : super( + autocorrect: false, + autofillHints: null, + controller: controller, + decoration: InputDecoration( + labelText: texts.satscard_spend_code_label, + ), + enabled: enabled, + enableSuggestions: false, + focusNode: focusNode, + inputFormatters: [SpendCodeFieldFormatter()], + keyboardType: const TextInputType.numberWithOptions( + signed: false, + decimal: false, + ), + maxLength: maxLength, + maxLines: maxLines, + obscureText: true, + onChanged: onChanged, + onFieldSubmitted: onFieldSubmitted, + onSaved: onSaved, + style: style, + ); + + @override + FormFieldValidator get validator { + return (value) { + if (value.length != 6) { + return texts.satscard_spend_code_incorrect_length_hint; + } + try { + if (validatorFn != null) { + return validatorFn(value); + } + } catch (_) {} + + return null; + }; + } +} + +class SpendCodeFieldFormatter extends TextInputFormatter { + final RegExp _pattern = RegExp(r'\D'); + + @override + TextEditingValue formatEditUpdate( + TextEditingValue oldValue, + TextEditingValue newValue, + ) { + const maxLength = 6; + + var text = newValue.text.replaceAll(_pattern, ""); + var offset = min(min(newValue.selection.start, maxLength), text.length); + if (text.isEmpty) { + return newValue.copyWith( + text: "", + selection: const TextSelection.collapsed(offset: 0), + ); + } else if (text.length > maxLength) { + text = text.substring(0, maxLength); + } + + return newValue.copyWith( + text: text, + selection: TextSelection.collapsed(offset: offset), + ); + } +} From 00ea63ea100992b95c95525f38f7e16256adeafd Mon Sep 17 00:00:00 2001 From: Peter Date: Wed, 31 Jan 2024 21:47:00 +0000 Subject: [PATCH 6/6] Add SatscardBalancePage - Used for displaying the balance of the active Satscard slot and allowing the user to sweep it into their wallet --- .../broadcast_slot_sweep_transaction.dart | 156 ++++++ .../satscard_balance_page.dart | 200 +++++++ .../satscard_balance/slot_balance_page.dart | 229 ++++++++ .../satscard_balance/sweep_slot_page.dart | 488 ++++++++++++++++++ lib/user_app.dart | 11 + 5 files changed, 1084 insertions(+) create mode 100644 lib/routes/satscard_balance/broadcast_slot_sweep_transaction.dart create mode 100644 lib/routes/satscard_balance/satscard_balance_page.dart create mode 100644 lib/routes/satscard_balance/slot_balance_page.dart create mode 100644 lib/routes/satscard_balance/sweep_slot_page.dart diff --git a/lib/routes/satscard_balance/broadcast_slot_sweep_transaction.dart b/lib/routes/satscard_balance/broadcast_slot_sweep_transaction.dart new file mode 100644 index 000000000..000bd1036 --- /dev/null +++ b/lib/routes/satscard_balance/broadcast_slot_sweep_transaction.dart @@ -0,0 +1,156 @@ +import 'dart:typed_data'; + +import 'package:breez/bloc/account/account_actions.dart'; +import 'package:breez/bloc/account/account_bloc.dart'; +import 'package:breez/bloc/blocs_provider.dart'; +import 'package:breez/bloc/satscard/satscard_actions.dart'; +import 'package:breez/bloc/satscard/satscard_bloc.dart'; +import 'package:breez/routes/satscard_balance/satscard_balance_page.dart'; +import 'package:breez/services/breezlib/data/messages.pb.dart'; +import 'package:breez/services/injector.dart'; +import 'package:breez/widgets/back_button.dart' as backBtn; +import 'package:breez/widgets/error_dialog.dart'; +import 'package:breez/widgets/flushbar.dart'; +import 'package:breez/widgets/link_launcher.dart'; +import 'package:breez/widgets/single_button_bottom_bar.dart'; +import 'package:breez_translations/breez_translations_locales.dart'; +import 'package:breez_translations/generated/breez_translations.dart'; +import 'package:flutter/material.dart'; + +class BroadcastSlotSweepTransactionPage extends StatefulWidget { + final Function() onBack; + final Function() onDone; + final AddressInfo Function() getAddressInfo; + final Uint8List Function() getPrivateKey; + final RawSlotSweepTransaction Function() getTransaction; + + const BroadcastSlotSweepTransactionPage( + {@required this.onBack, + @required this.onDone, + @required this.getAddressInfo, + @required this.getPrivateKey, + @required this.getTransaction}); + + @override + State createState() => + BroadcastSlotSweepTransactionPageState(); +} + +class BroadcastSlotSweepTransactionPageState + extends State { + TransactionDetails _signedTransaction; + Future _future; + + @override + void didChangeDependencies() { + super.didChangeDependencies(); + _broadcastTransaction(context); + } + + @override + Widget build(BuildContext context) { + return FutureBuilder( + future: _future, + builder: (context, snapshot) { + final themeData = Theme.of(context); + final texts = context.texts(); + final showError = snapshot.hasError; + return Scaffold( + appBar: AppBar( + title: Text( + texts.satscard_broadcast_title, + style: themeData.appBarTheme.titleTextStyle, + ), + leading: backBtn.BackButton( + onPressed: widget.onBack, + ), + ), + bottomNavigationBar: + !snapshot.hasError || _signedTransaction == null + ? null + : Padding( + padding: const EdgeInsets.only(top: 10), + child: SingleButtonBottomBar( + stickToBottom: true, + text: texts.satscard_balance_button_retry_label, + onPressed: () => _broadcastTransaction(context), + ), + ), + body: showError + ? buildErrorBody( + themeData, _getErrorText(texts, snapshot.error)) + : buildLoaderBody(themeData, _getLoaderText(texts)), + ); + }); + } + + String _getErrorText(BreezTranslations texts, Object error) { + return _signedTransaction == null + ? texts.satscard_broadcast_error_signing(error) + : texts.satscard_broadcast_error_broadcasting(error); + } + + String _getLoaderText(BreezTranslations texts) { + return _signedTransaction == null + ? texts.satscard_broadcast_signing_label + : texts.satscard_broadcast_broadcasting_label; + } + + void _broadcastTransaction(BuildContext context) { + if (!context.mounted) { + return; + } + setState(() { + _future = Future.sync(() { + // Sign the selected transaction if we haven't already + if (_signedTransaction == null) { + final satscardBloc = AppBlocsProvider.of(context); + final info = widget.getAddressInfo(); + final tx = widget.getTransaction(); + final key = widget.getPrivateKey(); + final action = SignSlotSweepTransaction(info, tx, key); + satscardBloc.actionsSink.add(action); + return action.future.then((result) { + if (context.mounted) { + setState(() { + _signedTransaction = result as TransactionDetails; + }); + } + }); + } + }).then((_) { + final accountBloc = AppBlocsProvider.of(context); + final action = PublishTransaction(_signedTransaction.tx); + accountBloc.userActionsSink.add(action); + return action.future; + }).then((_) { + if (context.mounted) { + final texts = context.texts(); + final tx = _signedTransaction; + + widget.onDone(); + promptMessage( + context, + texts.satscard_broadcast_complete_title, + Builder( + builder: (context) => LinkLauncher( + linkName: tx.txHash, + linkAddress: "https://blockstream.info/tx/${tx.txHash}", + onCopy: () { + ServiceInjector().device.setClipboardText(tx.txHash); + showFlushbar( + context, + message: texts.add_funds_transaction_id_copied, + duration: const Duration(seconds: 3), + ); + }, + ), + ), + contentPadding: + const EdgeInsets.symmetric(vertical: 12.0, horizontal: 32.0), + ); + } + }); + }); + } +} diff --git a/lib/routes/satscard_balance/satscard_balance_page.dart b/lib/routes/satscard_balance/satscard_balance_page.dart new file mode 100644 index 000000000..5d61c1bfd --- /dev/null +++ b/lib/routes/satscard_balance/satscard_balance_page.dart @@ -0,0 +1,200 @@ +import 'dart:typed_data'; + +import 'package:auto_size_text/auto_size_text.dart'; +import 'package:breez/bloc/account/account_model.dart'; +import 'package:breez/routes/satscard_balance/broadcast_slot_sweep_transaction.dart'; +import 'package:breez/routes/satscard_balance/slot_balance_page.dart'; +import 'package:breez/routes/satscard_balance/sweep_slot_page.dart'; +import 'package:breez/services/breezlib/data/messages.pb.dart'; +import 'package:breez/services/injector.dart'; +import 'package:breez/theme_data.dart' as theme; +import 'package:breez/utils/min_font_size.dart'; +import 'package:breez/widgets/circular_progress.dart'; +import 'package:breez/widgets/flushbar.dart'; +import 'package:breez/widgets/warning_box.dart'; +import 'package:breez_translations/generated/breez_translations.dart'; +import 'package:cktap_protocol/cktapcard.dart'; +import 'package:fixnum/fixnum.dart'; +import 'package:flutter/material.dart'; + +class SatscardBalancePage extends StatefulWidget { + final Satscard _card; + final Slot _slot; + + const SatscardBalancePage(this._card, this._slot); + + @override + State createState() => SatscardBalancePageState(); +} + +class SatscardBalancePageState extends State { + final _pageController = PageController(); + + AddressInfo _recentAddressInfo; + RawSlotSweepTransaction _selectedTransaction; + Uint8List _slotPrivateKey; + + @override + void dispose() { + _pageController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + body: PageView( + controller: _pageController, + physics: const NeverScrollableScrollPhysics(), + children: [ + SlotBalancePage( + widget._card, + widget._slot, + onBack: () => Navigator.pop(context), + onSweep: (balance) { + _recentAddressInfo = balance; + _pageController.nextPage( + duration: const Duration(milliseconds: 250), + curve: Curves.easeInOut, + ); + }, + ), + SweepSlotPage( + widget._card, + widget._slot, + onBack: () => _pageController.previousPage( + duration: const Duration(milliseconds: 250), + curve: Curves.easeInOut, + ), + onUnsealed: (transaction, privateKey) { + _selectedTransaction = transaction; + _slotPrivateKey = privateKey; + _pageController.nextPage( + duration: const Duration(milliseconds: 250), + curve: Curves.easeInOut, + ); + }, + getAddressInfo: () => _recentAddressInfo, + getCachedPrivateKey: () { + // Allow for unsealed slots + if (_slotPrivateKey != null && _slotPrivateKey.isEmpty) { + return widget._slot.privkey; + } + return _slotPrivateKey; + }, + ), + BroadcastSlotSweepTransactionPage( + onBack: () => _pageController.previousPage( + duration: const Duration(milliseconds: 250), + curve: Curves.easeInOut, + ), + onDone: () => Navigator.of(context).pop(), + getAddressInfo: () => _recentAddressInfo, + getPrivateKey: () => _slotPrivateKey, + getTransaction: () => _selectedTransaction, + ), + ], + ), + ); + } +} + +Widget buildErrorBody(ThemeData themeData, String title) { + return Stack( + children: [ + Positioned.fill( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + buildWarning(themeData, title: title), + ], + ), + ), + ], + ); +} + +Widget buildLoaderBody(ThemeData themeData, String title) { + return Stack( + children: [ + Positioned.fill( + child: buildIndicator(themeData, title: title), + ), + ], + ); +} + +Widget buildIndicator(ThemeData themeData, {String title}) { + return CircularProgress( + size: 64, + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + color: themeData.progressIndicatorTheme.color, + title: title, + ); +} + +ListTile buildSlotPageTextTile( + BuildContext context, + MinFontSize minFont, { + String titleText, + Color titleColor, + String trailingText, + Color trailingColor, + String copyMessage, +}) { + final style = theme.FieldTextStyle.labelStyle + .copyWith(color: titleColor ?? Colors.white); + final trailing = AutoSizeText( + trailingText ?? "", + style: style.copyWith(color: trailingColor ?? Colors.white), + maxLines: 1, + minFontSize: minFont.minFontSize, + stepGranularity: 0.1, + ); + + return ListTile( + visualDensity: const VisualDensity(horizontal: 0, vertical: -4), + title: AutoSizeText( + titleText ?? "", + style: style, + maxLines: 1, + minFontSize: minFont.minFontSize, + stepGranularity: 0.1, + ), + trailing: copyMessage == null + ? trailing + : GestureDetector( + onTap: () { + ServiceInjector().device.setClipboardText(trailingText); + showFlushbar(context, message: copyMessage); + }, + child: trailing, + ), + ); +} + +Widget buildWarning(ThemeData themeData, {String title}) { + return WarningBox( + child: Text( + title, + style: themeData.textTheme.titleLarge, + textAlign: TextAlign.left, + ), + ); +} + +String formatBalanceValue( + BreezTranslations texts, AccountModel acc, Int64 sats) { + if (acc == null) { + return sats.toString(); + } + final satsString = acc.currency.format(sats); + if (acc.fiatCurrency == null || sats <= 0) { + return texts.satscard_balance_value_no_fiat(satsString); + } else { + final fiat = acc.fiatCurrency.format(sats); + return texts.satscard_balance_value_with_fiat(satsString, fiat); + } +} diff --git a/lib/routes/satscard_balance/slot_balance_page.dart b/lib/routes/satscard_balance/slot_balance_page.dart new file mode 100644 index 000000000..40bf4ce07 --- /dev/null +++ b/lib/routes/satscard_balance/slot_balance_page.dart @@ -0,0 +1,229 @@ +import 'package:breez/bloc/account/account_bloc.dart'; +import 'package:breez/bloc/account/account_model.dart'; +import 'package:breez/bloc/blocs_provider.dart'; +import 'package:breez/bloc/satscard/satscard_actions.dart'; +import 'package:breez/bloc/satscard/satscard_bloc.dart'; +import 'package:breez/routes/add_funds/address_widget.dart'; +import 'package:breez/routes/satscard_balance/satscard_balance_page.dart'; +import 'package:breez/services/breezlib/data/messages.pb.dart'; +import 'package:breez/utils/min_font_size.dart'; +import 'package:breez/widgets/back_button.dart' as backBtn; +import 'package:breez/widgets/error_dialog.dart'; +import 'package:breez/widgets/single_button_bottom_bar.dart'; +import 'package:breez_translations/breez_translations_locales.dart'; +import 'package:breez_translations/generated/breez_translations.dart'; +import 'package:cktap_protocol/cktapcard.dart'; +import 'package:collection/collection.dart'; +import 'package:flutter/material.dart'; + +class SlotBalancePage extends StatefulWidget { + final Satscard _card; + final Slot _slot; + final Function() onBack; + final Function(AddressInfo) onSweep; + + const SlotBalancePage(this._card, this._slot, + {@required this.onBack, @required this.onSweep}); + + @override + State createState() => SlotBalancePageState(); +} + +class SlotBalancePageState extends State { + SatscardBloc _satscardBloc; + Future _future; + + @override + void didChangeDependencies() { + super.didChangeDependencies(); + _satscardBloc = AppBlocsProvider.of(context); + _retrieveBalance(); + } + + @override + Widget build(BuildContext context) { + return FutureBuilder( + future: _future, + builder: (context, infoSnapshot) => StreamBuilder( + stream: AppBlocsProvider.of(context).accountStream, + builder: (context, accSnapshot) { + final texts = context.texts(); + final themeData = Theme.of(context); + + final acc = accSnapshot.data; + final info = infoSnapshot.data; + final error = infoSnapshot.error ?? accSnapshot.error; + final showError = error != null; + final showLoader = + !showError && (!infoSnapshot.hasData || !accSnapshot.hasData); + final canSweep = !showError && + !showLoader && + infoSnapshot.data.confirmedBalance > 0; + + return Scaffold( + appBar: AppBar( + leading: backBtn.BackButton(onPressed: widget.onBack), + title: Text(texts.satscard_balance_title), + ), + bottomNavigationBar: Padding( + padding: const EdgeInsets.only(top: 10), + child: SingleButtonBottomBar( + stickToBottom: true, + text: showError + ? texts.satscard_balance_button_retry_label + : texts.satscard_balance_button_label, + onPressed: () => _onButtonPressed( + context, + themeData, + texts, + info, + showError: showError, + canSweep: canSweep, + ), + ), + ), + body: Scrollbar( + child: SingleChildScrollView( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + AddressWidget( + widget._slot.address, + isGeneric: true, + ), + Padding( + padding: const EdgeInsets.fromLTRB(16, 28, 16, 12), + child: _buildBalanceBody( + context, + themeData, + texts, + acc, + info, + error: error, + showError: showError, + showLoader: showLoader, + ), + ), + ], + ), + ), + ), + ); + }, + ), + ); + } + + Widget _buildBalanceBody( + BuildContext context, + ThemeData themeData, + BreezTranslations texts, + AccountModel acc, + AddressInfo info, { + Object error, + bool showError, + bool showLoader, + }) { + if (showError) { + return buildWarning( + themeData, + title: texts.satscard_balance_error_address_info(error.toString()), + ); + } else if (showLoader) { + return Padding( + padding: const EdgeInsets.only(top: 24), + child: buildIndicator( + themeData, + title: info == null + ? texts.satscard_balance_awaiting_balance_label + : texts.satscard_balance_awaiting_account_label, + ), + ); + } + final minFont = MinFontSize(context); + return Column( + children: [ + buildSlotPageTextTile( + context, + minFont, + titleText: texts.satscard_balance_confirmed_label, + trailingText: formatBalanceValue(texts, acc, info.confirmedBalance), + trailingColor: themeData.colorScheme.error, + ), + info.unconfirmedBalance == 0 + ? null + : buildSlotPageTextTile( + context, + minFont, + titleText: texts.satscard_balance_unconfirmed_label, + trailingText: + formatBalanceValue(texts, acc, info.unconfirmedBalance), + trailingColor: Colors.white.withOpacity(0.4), + ), + buildSlotPageTextTile(context, minFont, + titleText: texts.satscard_balance_slot_label, + trailingText: + "${widget._card.activeSlotIndex + 1} / ${widget._card.numSlots}"), + buildSlotPageTextTile(context, minFont, + titleText: texts.satscard_balance_version_label, + trailingText: widget._card.appletVersion), + buildSlotPageTextTile(context, minFont, + titleText: texts.satscard_balance_birth_height_label, + trailingText: widget._card.birthHeight.toString()), + buildSlotPageTextTile(context, minFont, + titleText: texts.satscard_balance_card_id_label, + trailingText: widget._card.ident, + copyMessage: texts.satscard_card_id_copied), + ].whereNotNull().toList(), + ); + } + + void _onButtonPressed( + BuildContext context, + ThemeData themeData, + BreezTranslations texts, + AddressInfo info, { + bool showError, + bool canSweep, + }) async { + if (showError) { + _retrieveBalance(); + return; + } else if (!canSweep) { + promptError( + context, + texts.satscard_balance_warning_no_funds_title, + Text( + texts.satscard_balance_warning_no_funds_body, + style: themeData.dialogTheme.contentTextStyle, + ), + ); + return; + } else if (info.unconfirmedBalance > 0) { + final confirm = await promptAreYouSure( + context, + texts.satscard_balance_warning_unconfirmed_title, + Text( + texts.satscard_balance_warning_unconfirmed_body, + style: themeData.dialogTheme.contentTextStyle, + ), + okText: texts.satscard_dialog_ok, + cancelText: texts.satscard_dialog_cancel, + ); + if (!confirm) { + return; + } + } + if (context.mounted) { + widget.onSweep(info); + } + } + + void _retrieveBalance() { + setState(() { + final action = GetAddressInfo(widget._slot.address); + _satscardBloc.actionsSink.add(action); + _future = action.future.then((result) => result as AddressInfo); + }); + } +} diff --git a/lib/routes/satscard_balance/sweep_slot_page.dart b/lib/routes/satscard_balance/sweep_slot_page.dart new file mode 100644 index 000000000..af3d3dbdc --- /dev/null +++ b/lib/routes/satscard_balance/sweep_slot_page.dart @@ -0,0 +1,488 @@ +import 'dart:collection'; +import 'dart:typed_data'; + +import 'package:breez/bloc/account/account_bloc.dart'; +import 'package:breez/bloc/account/account_model.dart'; +import 'package:breez/bloc/account/add_funds_bloc.dart'; +import 'package:breez/bloc/account/add_funds_model.dart'; +import 'package:breez/bloc/blocs_provider.dart'; +import 'package:breez/bloc/lsp/lsp_bloc.dart'; +import 'package:breez/bloc/lsp/lsp_model.dart'; +import 'package:breez/bloc/satscard/satscard_actions.dart'; +import 'package:breez/bloc/satscard/satscard_bloc.dart'; +import 'package:breez/bloc/satscard/satscard_op_status.dart'; +import 'package:breez/routes/add_funds/conditional_deposit.dart'; +import 'package:breez/routes/satscard_balance/satscard_balance_page.dart'; +import 'package:breez/services/breezlib/data/messages.pb.dart'; +import 'package:breez/theme_data.dart' as theme; +import 'package:breez/utils/min_font_size.dart'; +import 'package:breez/utils/stream_builder_extensions.dart'; +import 'package:breez/widgets/back_button.dart' as backBtn; +import 'package:breez/widgets/fee_chooser.dart'; +import 'package:breez/widgets/satscard/satscard_operation_dialog.dart'; +import 'package:breez/widgets/satscard/spend_code_field.dart'; +import 'package:breez/widgets/single_button_bottom_bar.dart'; +import 'package:breez_translations/breez_translations_locales.dart'; +import 'package:breez_translations/generated/breez_translations.dart'; +import 'package:cktap_protocol/cktapcard.dart'; +import 'package:collection/collection.dart'; +import 'package:fixnum/fixnum.dart'; +import 'package:flutter/material.dart'; + +class SweepSlotPage extends StatefulWidget { + final Satscard _card; + final Slot _slot; + final Function() onBack; + final Function(RawSlotSweepTransaction, Uint8List) onUnsealed; + final AddressInfo Function() getAddressInfo; + final Uint8List Function() getCachedPrivateKey; + + const SweepSlotPage(this._card, this._slot, + {@required this.onBack, + @required this.onUnsealed, + @required this.getAddressInfo, + @required this.getCachedPrivateKey}); + + @override + State createState() => SweepSlotPageState(); +} + +class SweepSlotPageState extends State { + final _formKey = GlobalKey(); + final _spendCodeController = TextEditingController(); + final _spendCodeFocusNode = FocusNode(); + final _incorrectCodes = + HashSet(equals: (a, b) => a == b, hashCode: (a) => a.hashCode); + + AddFundsBloc _addFundsBloc; + SatscardBloc _satscardBloc; + AddressInfo _addressInfo; + Future _transactionFuture; + Object _fundError; + AddFundResponse _fundResponse; + CreateSlotSweepResponse _createResponse; + int _selectedFeeIndex = 1; + + RawSlotSweepTransaction get _transaction => + _createResponse != null ? _createResponse.txs[_selectedFeeIndex] : null; + + @override + void didChangeDependencies() { + super.didChangeDependencies(); + _addFundsBloc = BlocProvider.of(context); + _satscardBloc = AppBlocsProvider.of(context); + _addressInfo = widget.getAddressInfo(); + _requestDepositAddress(); + } + + @override + Widget build(BuildContext context) { + final accountBloc = AppBlocsProvider.of(context); + final lspBloc = AppBlocsProvider.of(context); + final texts = context.texts(); + final displayIndex = widget._card.activeSlotIndex + 1; + return ConditionalDeposit( + title: texts.satscard_sweep_title(displayIndex), + enabledChild: FutureBuilder( + future: _transactionFuture, + builder: (context, futureSnapshot) => StreamBuilder2( + streamA: accountBloc.accountStream, + streamB: lspBloc.lspStatusStream, + builder: (context, accSnapshot, lspSnapshot) { + _createResponse = futureSnapshot.data; + + // Handle logic separately + final texts = context.texts(); + final info = _BuildInfo.create( + texts, + accSnapshot.data, + lspSnapshot.data, + _fundResponse, + _createResponse, + _selectedFeeIndex, + error: futureSnapshot.error ?? + _fundError ?? + lspSnapshot.error ?? + accSnapshot.error, + ); + + return Scaffold( + appBar: AppBar( + leading: backBtn.BackButton( + onPressed: () { + if (_spendCodeFocusNode.hasFocus) { + _spendCodeFocusNode.unfocus(); + } + widget.onBack(); + }, + ), + title: Text(texts.satscard_sweep_title(displayIndex)), + ), + bottomNavigationBar: _buildButton(context, texts, info), + body: _buildBody(context, texts, accSnapshot.data, info), + ); + }, + ), + ), + ); + } + + Widget _buildBody(BuildContext context, BreezTranslations texts, + AccountModel acc, _BuildInfo info) { + final feeOptions = _getFeeOptions(); + final themeData = Theme.of(context); + final minFont = MinFontSize(context); + + if (info.showError) { + return buildErrorBody(themeData, info.outText); + } else if (info.showLoader) { + return buildLoaderBody(themeData, info.outText); + } + return Form( + key: _formKey, + child: Padding( + padding: const EdgeInsets.fromLTRB(16.0, 0.0, 16.0, 10.0), + child: Scrollbar( + child: SingleChildScrollView( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + SpendCodeFormField( + context: context, + texts: texts, + focusNode: _spendCodeFocusNode, + controller: _spendCodeController, + style: theme.FieldTextStyle.textStyle, + validatorFn: (code) { + if (_incorrectCodes.contains(code)) { + return texts.satscard_spend_code_incorrect_code_hint; + } + return null; + }, + ), + Padding( + padding: const EdgeInsets.only(top: 24, bottom: 24), + child: FeeChooser( + economyFee: feeOptions[0], + regularFee: feeOptions[1], + priorityFee: feeOptions[2], + selectedIndex: _selectedFeeIndex, + onSelect: (index) => setState(() { + _selectedFeeIndex = index; + if (_spendCodeFocusNode.hasFocus) { + _spendCodeFocusNode.unfocus(); + } + }), + ), + ), + Container( + decoration: BoxDecoration( + borderRadius: const BorderRadius.all(Radius.circular(5.0)), + border: Border.all( + color: themeData.colorScheme.onSurface.withOpacity(0.4), + ), + ), + child: ListView( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + children: [ + buildSlotPageTextTile( + context, + minFont, + titleText: texts.satscard_sweep_balance_label, + trailingText: + formatBalanceValue(texts, acc, _transaction.input), + trailingColor: themeData.colorScheme.error, + ), + buildSlotPageTextTile( + context, + minFont, + titleText: texts.satscard_sweep_chain_fee_label, + trailingText: texts.satscard_sweep_fee_value( + acc.currency.format(_transaction.fees), + ), + titleColor: Colors.white.withOpacity(0.4), + trailingColor: Colors.white.withOpacity(0.4), + ), + !info.willOpenChannel + ? null + : buildSlotPageTextTile( + context, + minFont, + titleText: texts.satscard_sweep_lsp_fee_label, + trailingText: texts.satscard_sweep_fee_value( + acc.currency.format(info.lspFee), + ), + titleColor: Colors.white.withOpacity(0.4), + trailingColor: Colors.white.withOpacity(0.4), + ), + buildSlotPageTextTile( + context, + minFont, + titleText: texts.satscard_sweep_receive_label, + trailingText: + formatBalanceValue(texts, acc, info.receiveAmount), + trailingColor: themeData.colorScheme.error, + ), + info.canSweep + ? null + : buildSlotPageTextTile( + context, + minFont, + titleText: info.failureLabel, + trailingText: formatBalanceValue( + texts, acc, info.failureAmount), + trailingColor: themeData.colorScheme.error, + ), + ].whereNotNull().toList(), + ), + ), + info.outText.isEmpty + ? const SizedBox.shrink() + : Padding( + padding: const EdgeInsets.only(top: 12), + child: buildWarning( + themeData, + title: info.outText, + ), + ), + ], + ), + ), + ), + ), + ); + } + + Widget _buildButton( + BuildContext context, BreezTranslations texts, _BuildInfo info) { + String text = ""; + Function() onPressed; + + switch (info.buttonAction) { + case _ButtonAction.None: + return null; + + case _ButtonAction.Cancel: + text = texts.satscard_sweep_button_cancel_label; + onPressed = widget.onBack; + break; + + case _ButtonAction.Retry: + text = texts.satscard_balance_button_retry_label; + onPressed = () { + if (_fundResponse == null || _fundResponse.errorMessage.isNotEmpty) { + _requestDepositAddress(); + } else { + _updateFundResponse(_fundResponse); + } + }; + break; + + case _ButtonAction.Sweep: + text = texts.satscard_sweep_button_confirm_label; + onPressed = () { + Uint8List cachedKey = widget.getCachedPrivateKey(); + if (cachedKey != null && cachedKey.isNotEmpty) { + widget.onUnsealed(_transaction, cachedKey); + return; + } + if (_formKey.currentState.validate()) { + final spendCode = _spendCodeController.text; + if (widget._slot.status == SlotStatus.sealed) { + _satscardBloc.actionsSink + .add(UnsealSlot(widget._card, spendCode)); + } else { + _satscardBloc.actionsSink + .add(GetSlot(widget._card, widget._slot.index, spendCode)); + } + if (_spendCodeFocusNode.hasFocus) { + _spendCodeFocusNode.unfocus(); + } + showSatscardOperationDialog( + context, + _satscardBloc, + widget._card.ident, + ).then((r) { + if (r is SatscardOpStatusBadAuth) { + _incorrectCodes.add(spendCode); + _formKey.currentState.validate(); + } + if (r is SatscardOpStatusSuccess) { + widget.onUnsealed(_transaction, r.slot.privkey); + } + }); + } + }; + break; + } + return SingleButtonBottomBar( + stickToBottom: false, + text: text, + onPressed: onPressed, + ); + } + + List _getFeeOptions() { + if (_createResponse == null) { + return List.empty(); + } + return List.generate( + _createResponse.txs.length, + (i) => FeeOption( + _createResponse.txs[i].fees.toInt(), + _createResponse.txs[i].targetConfirmations, + ), + ); + } + + void _requestDepositAddress() { + setState(() => _fundError = null); + + final addFundsAction = AddFundsInfo(true, false); + _addFundsBloc.addFundRequestSink.add(addFundsAction); + _addFundsBloc.addFundResponseStream + .listen((response) => _updateFundResponse(response)); + _addFundsBloc.addFundResponseStream.handleError((e) { + if (context.mounted) { + setState(() => _fundError = e); + } + }); + } + + Future _requestSlotSweepTransactions(String depositAddress) { + final action = CreateSlotSweepTransactions(_addressInfo, depositAddress); + _satscardBloc.actionsSink.add(action); + return action.future; + } + + void _updateFundResponse(AddFundResponse response) { + if (!context.mounted) { + return; + } + setState(() { + _fundResponse = response; + if (_fundResponse == null || _fundResponse.errorMessage.isNotEmpty) { + _transactionFuture = null; + } else { + _transactionFuture = _requestSlotSweepTransactions(response.address); + } + }); + } +} + +enum _ButtonAction { + None, + Cancel, + Retry, + Sweep, +} + +class _BuildInfo { + _ButtonAction buttonAction = _ButtonAction.None; + bool showError = false; + bool showLoader = false; + String outText = ""; + Int64 receiveAmount = Int64.ZERO; + Int64 lspFee = Int64.ZERO; + String failureLabel = ""; + Int64 failureAmount = Int64.ZERO; + + bool get canSweep => failureLabel.isEmpty; + bool get willOpenChannel => lspFee > 0; + + _BuildInfo.sweepable(this.outText, this.receiveAmount, this.lspFee) + : buttonAction = _ButtonAction.Sweep; + + _BuildInfo.failure(this.outText, this.receiveAmount, this.lspFee, + this.failureLabel, this.failureAmount) + : buttonAction = _ButtonAction.Cancel; + + _BuildInfo.error(this.outText) + : buttonAction = _ButtonAction.Retry, + showError = true; + + _BuildInfo.loading(this.outText) + : buttonAction = _ButtonAction.None, + showLoader = true; + + factory _BuildInfo.create( + BreezTranslations texts, + AccountModel acc, + LSPStatus lsp, + AddFundResponse fund, + CreateSlotSweepResponse sweep, + int selectedTxIndex, { + Object error, + }) { + // Handle errors + if (error != null) { + return _BuildInfo.error(sweep == null + ? texts.satscard_sweep_error_create_transactions(error) + : texts.satscard_sweep_error_deposit_address(error)); + } else if (fund != null && fund.errorMessage.isNotEmpty) { + return _BuildInfo.error( + texts.satscard_sweep_error_deposit_address(fund.errorMessage)); + } + + // Handle loading + if (acc == null) { + return _BuildInfo.loading(texts.satscard_balance_awaiting_account_label); + } else if (lsp == null) { + return _BuildInfo.loading(texts.satscard_sweep_awaiting_lsp_label); + } else if (fund == null) { + return _BuildInfo.loading(texts.satscard_sweep_awaiting_deposit_label); + } else if (sweep == null) { + return _BuildInfo.loading(texts.satscard_sweep_awaiting_fees_label); + } + + // Figure out if we need a new channel + final liquidity = acc.connected ? acc.maxInboundLiquidity : Int64.ZERO; + final tx = sweep.txs[selectedTxIndex]; + var receive = tx.output; + var lspFee = Int64.ZERO; + var outText = ""; + if (liquidity < receive) { + final minFee = lsp.currentLSP.cheapestOpeningFeeParams.minMsat ~/ 1000; + final propFee = tx.output * + lsp.currentLSP.cheapestOpeningFeeParams.proportional ~/ + 1000000; + lspFee = minFee > propFee ? minFee : propFee; + receive -= lspFee; + + // Format our warning message + final sats = acc.currency.format(liquidity); + final fee = acc.currency.format(minFee); + final percent = + (lsp.currentLSP.cheapestOpeningFeeParams.proportional / 10000) + .toString(); + outText = acc.maxInboundLiquidity == 0 + ? texts.satscard_sweep_warning_lsp_fee_no_liquidity_label( + fee, percent) + : texts.satscard_sweep_warning_lsp_fee_label(fee, percent, sats); + } + + // Verify the sweep meets all the deposit conditions + final minimum = + fund.minAllowedDeposit > lspFee ? fund.minAllowedDeposit : lspFee; + if (tx.output < minimum) { + return _BuildInfo.failure(texts.satscard_sweep_warning_not_valid, receive, + lspFee, texts.satscard_sweep_balance_too_low_label, minimum); + } else if (tx.output > fund.maxAllowedDeposit) { + return _BuildInfo.failure( + texts.satscard_sweep_warning_not_valid, + receive, + lspFee, + texts.satscard_sweep_balance_too_high_label, + fund.maxAllowedDeposit); + } else if (receive < fund.requiredReserve) { + return _BuildInfo.failure( + texts.satscard_sweep_warning_not_valid, + receive, + lspFee, + texts.satscard_sweep_reserve_not_met_label, + fund.requiredReserve); + } + return _BuildInfo.sweepable(outText, receive, lspFee); + } +} diff --git a/lib/user_app.dart b/lib/user_app.dart index 3ceaa7be7..662377044 100644 --- a/lib/user_app.dart +++ b/lib/user_app.dart @@ -34,6 +34,7 @@ import 'package:breez/routes/payment_options/payment_options_page.dart'; import 'package:breez/routes/podcast/theme.dart'; import 'package:breez/routes/podcast_history/podcast_history.dart'; import 'package:breez/routes/qr_scan.dart'; +import 'package:breez/routes/satscard_balance/satscard_balance_page.dart'; import 'package:breez/routes/security_pin/lock_screen.dart'; import 'package:breez/routes/security_pin/security_and_backup/security_and_backup_page.dart'; import 'package:breez/routes/settings/pos_settings_page.dart'; @@ -375,6 +376,16 @@ class UserApp extends StatelessWidget { InitializeSatscardPage(settings.arguments as Satscard), settings: settings, ); + case '/satscard_balance': + return FadeInRoute ( + builder: (_) { + final arguments = settings.arguments as Map; + final card = arguments["card"] as Satscard; + final slot = arguments["slot"] as Slot; + return SatscardBalancePage(card, slot); + }, + settings: settings, + ); } assert(false); },