WIP: CW-665 Implement AirGapped Monero Transactions (#1535)

* replace qr scanner with fast_scanner

* bump java version

* fix qr code scanning

* add flashlight and camera switch

* airgap work

* commitTransactionUR

* bump fast_scanner, fix configure script

* add option to restore wallet from NERO qr format

* fixes to the flow and logic
use gsed or otherwise it fails?

* remove Expanded() to fix URQR on release builds

* cache key to allow app updates

* rename cache key

* [skip ci] cache key.jks in cache_dependencies

* update fast_scanner to work on ios, with light mlkit dependency

* ui fixes

* error handling fix

* update fast_scanner to drop iOS dependency

* changes from review

* Update lib/entities/qr_scanner.dart

* changes from review

* remove useless commit

* don't set state multiple times
remove return Container() for non monero wallets

* return on fail
don't handle empty qr codes

* set node as trusted
display primary address in seed screen

* fix wow and haven

* migrate node to trusted

* - update trusted node for existing users
- update locales
- fix conflicts
- move menu item

---------

Co-authored-by: Omar Hatem <omarh.ismail1@gmail.com>
This commit is contained in:
cyan
2024-11-09 20:59:47 +02:00
committed by GitHub
co-authored by Omar Hatem
parent 8e12fb1ad9
commit cea3084bb3
84 changed files with 1318 additions and 141 deletions
@@ -347,8 +347,8 @@ class CakePayBuyCardDetailPage extends BasePage {
rightButtonText: S.of(popupContext).send,
leftButtonText: S.of(popupContext).cancel,
actionRightButton: () async {
Navigator.of(popupContext).pop();
await cakePayPurchaseViewModel.sendViewModel.commitTransaction();
Navigator.of(context).pop();
await cakePayPurchaseViewModel.sendViewModel.commitTransaction(context);
},
actionLeftButton: () => Navigator.of(popupContext).pop()));
},
@@ -101,6 +101,9 @@ class MenuWidgetState extends State<MenuWidget> {
if (!widget.dashboardViewModel.hasSilentPayments) {
items.removeWhere((element) => element.name(context) == S.of(context).silent_payments_settings);
}
if (!widget.dashboardViewModel.isMoneroViewOnly) {
items.removeWhere((element) => element.name(context) == S.of(context).export_outputs);
}
if (!widget.dashboardViewModel.hasMweb) {
items.removeWhere((element) => element.name(context) == S.of(context).litecoin_mweb_settings);
}
@@ -189,7 +192,6 @@ class MenuWidgetState extends State<MenuWidget> {
index--;
final item = items[index];
final isLastTile = index == itemCount - 1;
return SettingActionButton(
@@ -277,7 +277,7 @@ class ExchangeTradeState extends State<ExchangeTradeForm> {
actionRightButton: () async {
Navigator.of(popupContext).pop();
await widget.exchangeTradeViewModel.sendViewModel
.commitTransaction();
.commitTransaction(context);
transactionStatePopup();
},
actionLeftButton: () => Navigator.of(popupContext).pop(),
+1 -1
View File
@@ -498,7 +498,7 @@ class SendPage extends BasePage {
ValueKey('send_page_confirm_sending_dialog_cancel_button_key'),
actionRightButton: () async {
Navigator.of(_dialogContext).pop();
sendViewModel.commitTransaction();
sendViewModel.commitTransaction(context);
await showPopUp<void>(
context: context,
builder: (BuildContext _dialogContext) {
@@ -4,6 +4,7 @@ import 'package:cake_wallet/src/widgets/setting_action_button.dart';
import 'package:cake_wallet/src/widgets/setting_actions.dart';
import 'package:cake_wallet/typography.dart';
import 'package:cake_wallet/view_model/dashboard/dashboard_view_model.dart';
import 'package:cw_core/wallet_type.dart';
import 'package:flutter/material.dart';
import 'package:cake_wallet/router.dart' as Router;
import 'package:cake_wallet/themes/extensions/menu_theme.dart';
@@ -60,8 +61,10 @@ class _DesktopSettingsPageState extends State<DesktopSettingsPage> {
return Container();
}
if (!widget.dashboardViewModel.hasMweb &&
item.name(context) == S.of(context).litecoin_mweb_settings) {
if ((!widget.dashboardViewModel.isMoneroViewOnly &&
item.name(context) == S.of(context).export_outputs) ||
(!widget.dashboardViewModel.hasMweb &&
item.name(context) == S.of(context).litecoin_mweb_settings)) {
return Container();
}
@@ -168,7 +168,7 @@ class RBFDetailsPage extends BasePage {
leftButtonText: S.of(popupContext).cancel,
actionRightButton: () async {
Navigator.of(popupContext).pop();
await transactionDetailsViewModel.sendViewModel.commitTransaction();
await transactionDetailsViewModel.sendViewModel.commitTransaction(context);
try {
Navigator.of(popupContext).pop();
} catch (_) {}
+184
View File
@@ -0,0 +1,184 @@
import 'dart:async';
import 'package:cake_wallet/di.dart';
import 'package:cake_wallet/entities/qr_scanner.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/monero/monero.dart';
import 'package:cake_wallet/src/screens/base_page.dart';
import 'package:cake_wallet/src/screens/receive/widgets/qr_image.dart';
import 'package:cake_wallet/src/screens/send/widgets/confirm_sending_alert.dart';
import 'package:cake_wallet/src/widgets/alert_with_one_action.dart';
import 'package:cake_wallet/src/widgets/primary_button.dart';
import 'package:cake_wallet/utils/show_pop_up.dart';
import 'package:cake_wallet/view_model/animated_ur_model.dart';
import 'package:cake_wallet/view_model/dashboard/wallet_balance.dart';
import 'package:cw_core/balance.dart';
import 'package:cw_core/transaction_history.dart';
import 'package:cw_core/transaction_info.dart';
import 'package:cw_core/wallet_base.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
// ur:xmr-txunsigned - unsigned transaction
// should show a scanner afterwards.
class AnimatedURPage extends BasePage {
final bool isAll;
AnimatedURPage(this.animatedURmodel, {required String urQr, this.isAll = false}) {
if (urQr == "export-outputs") {
this.urQr = monero!.exportOutputsUR(animatedURmodel.wallet, false);
} else if (urQr == "export-outputs-all") {
this.urQr = monero!.exportOutputsUR(animatedURmodel.wallet, true);
} else {
this.urQr = urQr;
}
}
late String urQr;
final AnimatedURModel animatedURmodel;
String get urQrType {
final first = urQr.trim().split("\n")[0];
return first.split('/')[0];
}
@override
Widget body(BuildContext context) {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
Padding(
padding: const EdgeInsets.only(top: 64.0),
child: URQR(
frames: urQr.trim().split("\n"),
),
),
SizedBox(height: 32),
if (urQrType == "ur:xmr-txunsigned" || urQrType == "ur:xmr-output")
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: SizedBox(
width: double.maxFinite,
child: PrimaryButton(
onPressed: () => _continue(context),
text: "Continue",
color: Theme.of(context).primaryColor,
textColor: Colors.white,
),
),
),
SizedBox(height: 32),
if (urQrType == "ur:xmr-output" && !isAll) Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: SizedBox(
width: double.maxFinite,
child: PrimaryButton(
onPressed: () => _exportAll(context),
text: "Export all",
color: Theme.of(context).colorScheme.secondary,
textColor: Colors.white,
),
),
),
],
);
}
void _exportAll(BuildContext context) {
Navigator.of(context).pushReplacement(
MaterialPageRoute(
builder: (context) {
return AnimatedURPage(animatedURmodel, urQr: "export-outputs-all", isAll: true);
},
),
);
}
Future<void> _continue(BuildContext context) async {
try {
switch (urQrType) {
case "ur:xmr-txunsigned": // ur:xmr-txsigned
final ur = await presentQRScanner(context);
final result = await monero!.commitTransactionUR(animatedURmodel.wallet, ur);
if (result) {
Navigator.of(context).pop(true);
}
break;
case "ur:xmr-output": // xmr-keyimage
final ur = await presentQRScanner(context);
final result = await monero!.importKeyImagesUR(animatedURmodel.wallet, ur);
if (result) {
Navigator.of(context).pop(true);
}
break;
default:
throw UnimplementedError("unable to handle UR: ${urQrType}");
}
} catch (e) {
await showPopUp<void>(
context: context,
builder: (BuildContext context) {
return AlertWithOneAction(
alertTitle: S.of(context).error,
alertContent: e.toString(),
buttonText: S.of(context).ok,
buttonAction: () => Navigator.pop(context, true));
});
}
}
}
class URQR extends StatefulWidget {
URQR({super.key, required this.frames});
List<String> frames;
@override
// ignore: library_private_types_in_public_api
_URQRState createState() => _URQRState();
}
const urFrameTime = 1000 ~/ 5;
class _URQRState extends State<URQR> {
Timer? t;
int frame = 0;
@override
void initState() {
super.initState();
setState(() {
t = Timer.periodic(const Duration(milliseconds: urFrameTime), (timer) {
_nextFrame();
});
});
}
void _nextFrame() {
setState(() {
frame++;
});
}
@override
void dispose() {
t?.cancel();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Center(
child: QrImage(
data: widget.frames[frame % widget.frames.length], version: -1,
size: 400,
),
),
],
);
}
}
@@ -65,7 +65,7 @@ class WCPairingsWidget extends BasePage {
bool isCameraPermissionGranted =
await PermissionHandler.checkPermission(Permission.camera, context);
if (!isCameraPermissionGranted) return;
uri = await presentQRScanner();
uri = await presentQRScanner(context);
} else {
uri = await _showEnterWalletConnectURIPopUp(context);
}
+1 -1
View File
@@ -231,7 +231,7 @@ class AddressTextField extends StatelessWidget {
bool isCameraPermissionGranted =
await PermissionHandler.checkPermission(Permission.camera, context);
if (!isCameraPermissionGranted) return;
final code = await presentQRScanner();
final code = await presentQRScanner(context);
if (code.isEmpty) {
return;
}
+11
View File
@@ -21,6 +21,7 @@ class SettingActions {
addressBookSettingAction,
silentPaymentsSettingAction,
litecoinMwebSettingAction,
exportOutputsAction,
securityBackupSettingAction,
privacySettingAction,
displaySettingAction,
@@ -50,6 +51,16 @@ class SettingActions {
},
);
static SettingActions exportOutputsAction = SettingActions._(
key: ValueKey('dashboard_page_menu_widget_export_outputs_settings_button_key'),
name: (context) => S.of(context).export_outputs,
image: 'assets/images/monero_menu.png',
onTap: (BuildContext context) {
Navigator.pop(context);
Navigator.of(context).pushNamed(Routes.urqrAnimatedPage, arguments: 'export-outputs');
},
);
static SettingActions litecoinMwebSettingAction = SettingActions._(
key: ValueKey('dashboard_page_menu_widget_litecoin_mweb_settings_button_key'),
name: (context) => S.of(context).litecoin_mweb_settings,