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
+7
View File
@@ -125,6 +125,7 @@ import 'package:cake_wallet/src/screens/subaddress/address_edit_or_create_page.d
import 'package:cake_wallet/src/screens/support/support_page.dart';
import 'package:cake_wallet/src/screens/support_chat/support_chat_page.dart';
import 'package:cake_wallet/src/screens/support_other_links/support_other_links_page.dart';
import 'package:cake_wallet/src/screens/ur/animated_ur_page.dart';
import 'package:cake_wallet/src/screens/wallet/wallet_edit_page.dart';
import 'package:cake_wallet/src/screens/wallet_connect/wc_connections_listing_view.dart';
import 'package:cake_wallet/src/screens/wallet_unlock/wallet_unlock_arguments.dart';
@@ -134,6 +135,7 @@ import 'package:cake_wallet/utils/device_info.dart';
import 'package:cake_wallet/store/anonpay/anonpay_transactions_store.dart';
import 'package:cake_wallet/utils/payment_request.dart';
import 'package:cake_wallet/utils/responsive_layout_util.dart';
import 'package:cake_wallet/view_model/animated_ur_model.dart';
import 'package:cake_wallet/view_model/dashboard/desktop_sidebar_view_model.dart';
import 'package:cake_wallet/view_model/anon_invoice_page_view_model.dart';
import 'package:cake_wallet/view_model/anonpay_details_view_model.dart';
@@ -903,6 +905,11 @@ Future<void> setup({
getIt.registerFactory(() => WalletKeysViewModel(getIt.get<AppStore>()));
getIt.registerFactory(() => WalletKeysPage(getIt.get<WalletKeysViewModel>()));
getIt.registerFactory(() => AnimatedURModel(getIt.get<AppStore>()));
getIt.registerFactoryParam<AnimatedURPage, String, void>((String urQr, _) =>
AnimatedURPage(getIt.get<AnimatedURModel>(), urQr: urQr));
getIt.registerFactoryParam<ContactViewModel, ContactRecord?, void>(
(ContactRecord? contact, _) => ContactViewModel(_contactSource, contact: contact));
+14 -1
View File
@@ -259,6 +259,10 @@ Future<void> defaultSettingsMigration(
case 42:
updateBtcElectrumNodeToUseSSL(nodes, sharedPreferences);
break;
case 43:
_updateCakeXmrNode(nodes);
break;
default:
break;
}
@@ -273,6 +277,15 @@ Future<void> defaultSettingsMigration(
await sharedPreferences.setInt(PreferencesKey.currentDefaultSettingsMigrationVersion, version);
}
void _updateCakeXmrNode(Box<Node> nodes) {
final node = nodes.values.firstWhereOrNull((element) => element.uriRaw == newCakeWalletMoneroUri);
if (node != null && !node.trusted) {
node.trusted = true;
node.save();
}
}
void updateBtcElectrumNodeToUseSSL(Box<Node> nodes, SharedPreferences sharedPreferences) {
final btcElectrumNode = nodes.values.firstWhereOrNull((element) => element.uriRaw == newCakeWalletBitcoinUri);
@@ -843,7 +856,7 @@ Future<void> changeDefaultMoneroNode(
}
});
final newCakeWalletNode = Node(uri: newCakeWalletMoneroUri, type: WalletType.monero);
final newCakeWalletNode = Node(uri: newCakeWalletMoneroUri, type: WalletType.monero, trusted: true);
await nodeSource.add(newCakeWalletNode);
+365 -4
View File
@@ -1,15 +1,376 @@
import 'package:barcode_scan2/barcode_scan2.dart';
import 'dart:math';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/main.dart';
import 'package:cake_wallet/src/widgets/alert_with_one_action.dart';
import 'package:cake_wallet/src/widgets/alert_with_two_actions.dart';
import 'package:cake_wallet/utils/show_pop_up.dart';
import 'package:fast_scanner/fast_scanner.dart';
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
var isQrScannerShown = false;
Future<String> presentQRScanner() async {
Future<String> presentQRScanner(BuildContext context) async {
isQrScannerShown = true;
try {
final result = await BarcodeScanner.scan();
final result = await Navigator.of(context).push<String>(
MaterialPageRoute(
builder:(context) {
return BarcodeScannerSimple();
},
),
);
isQrScannerShown = false;
return result.rawContent.trim();
return result??'';
} catch (e) {
isQrScannerShown = false;
rethrow;
}
}
// https://github.com/MrCyjaneK/fast_scanner/blob/master/example/lib/barcode_scanner_simple.dart
class BarcodeScannerSimple extends StatefulWidget {
const BarcodeScannerSimple({super.key});
@override
State<BarcodeScannerSimple> createState() => _BarcodeScannerSimpleState();
}
class _BarcodeScannerSimpleState extends State<BarcodeScannerSimple> {
Barcode? _barcode;
bool popped = false;
List<String> urCodes = [];
late var ur = URQRToURQRData(urCodes);
void _handleBarcode(BarcodeCapture barcodes) {
try {
_handleBarcodeInternal(barcodes);
} catch (e) {
showPopUp<void>(
context: context,
builder: (context) {
return AlertWithOneAction(
alertTitle: S.of(context).error,
alertContent: S.of(context).error_dialog_content,
buttonText: 'ok',
buttonAction: () {
Navigator.of(context).pop();
},
);
},
);
print(e);
}
}
void _handleBarcodeInternal(BarcodeCapture barcodes) {
for (final barcode in barcodes.barcodes) {
// don't handle unknown QR codes
if (barcode.rawValue?.trim().isEmpty??false == false) continue;
if (barcode.rawValue!.startsWith("ur:")) {
if (urCodes.contains(barcode.rawValue)) continue;
setState(() {
urCodes.add(barcode.rawValue!);
ur = URQRToURQRData(urCodes);
});
if (ur.progress == 1) {
setState(() {
popped = true;
});
SchedulerBinding.instance.addPostFrameCallback((_) {
Navigator.of(context).pop(ur.inputs.join("\n"));
});
};
}
}
if (urCodes.isNotEmpty) return;
if (mounted) {
setState(() {
_barcode = barcodes.barcodes.firstOrNull;
});
if (_barcode != null && popped != true) {
setState(() {
popped = true;
});
SchedulerBinding.instance.addPostFrameCallback((_) {
Navigator.of(context).pop(_barcode?.rawValue ?? "");
});
}
}
}
final MobileScannerController ctrl = MobileScannerController();
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Scan'),
actions: [
SwitchCameraButton(controller: ctrl),
ToggleFlashlightButton(controller: ctrl),
],
),
backgroundColor: Colors.black,
body: Stack(
children: [
MobileScanner(
onDetect: _handleBarcode,
controller: ctrl,
),
if (ur.inputs.length != 0)
Center(child:
Text(
"${ur.inputs.length}/${ur.count}",
style: Theme.of(context).textTheme.displayLarge?.copyWith(color: Colors.white)
),
),
SizedBox(
child: Center(
child: SizedBox(
width: 250,
height: 250,
child: CustomPaint(
painter: ProgressPainter(
urQrProgress: URQrProgress(
expectedPartCount: ur.count - 1,
processedPartsCount: ur.inputs.length,
receivedPartIndexes: _urParts(),
percentage: ur.progress,
),
),
),
),
),
),
],
),
);
}
List<int> _urParts() {
List<int> l = [];
for (var inp in ur.inputs) {
try {
l.add(int.parse(inp.split("/")[1].split("-")[0]));
} catch (e) {}
}
return l;
}
}
class ToggleFlashlightButton extends StatelessWidget {
const ToggleFlashlightButton({required this.controller, super.key});
final MobileScannerController controller;
@override
Widget build(BuildContext context) {
return ValueListenableBuilder(
valueListenable: controller,
builder: (context, state, child) {
if (!state.isInitialized || !state.isRunning) {
return const SizedBox.shrink();
}
switch (state.torchState) {
case TorchState.auto:
return IconButton(
iconSize: 32.0,
icon: const Icon(Icons.flash_auto),
onPressed: () async {
await controller.toggleTorch();
},
);
case TorchState.off:
return IconButton(
iconSize: 32.0,
icon: const Icon(Icons.flash_off),
onPressed: () async {
await controller.toggleTorch();
},
);
case TorchState.on:
return IconButton(
iconSize: 32.0,
icon: const Icon(Icons.flash_on),
onPressed: () async {
await controller.toggleTorch();
},
);
case TorchState.unavailable:
return const Icon(
Icons.no_flash,
color: Colors.grey,
);
}
},
);
}
}
class SwitchCameraButton extends StatelessWidget {
const SwitchCameraButton({required this.controller, super.key});
final MobileScannerController controller;
@override
Widget build(BuildContext context) {
return ValueListenableBuilder(
valueListenable: controller,
builder: (context, state, child) {
if (!state.isInitialized || !state.isRunning) {
return const SizedBox.shrink();
}
final int? availableCameras = state.availableCameras;
if (availableCameras != null && availableCameras < 2) {
return const SizedBox.shrink();
}
final Widget icon;
switch (state.cameraDirection) {
case CameraFacing.front:
icon = const Icon(Icons.camera_front);
case CameraFacing.back:
icon = const Icon(Icons.camera_rear);
}
return IconButton(
iconSize: 32.0,
icon: icon,
onPressed: () async {
await controller.switchCamera();
},
);
},
);
}
}
class URQRData {
URQRData(
{required this.tag,
required this.str,
required this.progress,
required this.count,
required this.error,
required this.inputs});
final String tag;
final String str;
final double progress;
final int count;
final String error;
final List<String> inputs;
Map<String, dynamic> toJson() {
return {
"tag": tag,
"str": str,
"progress": progress,
"count": count,
"error": error,
"inputs": inputs,
};
}
}
URQRData URQRToURQRData(List<String> urqr_) {
final urqr = urqr_.toSet().toList();
urqr.sort((s1, s2) {
final s1s = s1.split("/");
final s1frameStr = s1s[1].split("-");
final s1curFrame = int.parse(s1frameStr[0]);
final s2s = s2.split("/");
final s2frameStr = s2s[1].split("-");
final s2curFrame = int.parse(s2frameStr[0]);
return s1curFrame - s2curFrame;
});
String tag = '';
int count = 0;
String bw = '';
for (var elm in urqr) {
final s = elm.substring(elm.indexOf(":") + 1); // strip down ur: prefix
final s2 = s.split("/");
tag = s2[0];
final frameStr = s2[1].split("-");
// final curFrame = int.parse(frameStr[0]);
count = int.parse(frameStr[1]);
final byteWords = s2[2];
bw += byteWords;
}
String? error;
return URQRData(
tag: tag,
str: bw,
progress: count == 0 ? 0 : (urqr.length / count),
count: count,
error: error ?? "",
inputs: urqr,
);
}
class ProgressPainter extends CustomPainter {
final URQrProgress urQrProgress;
ProgressPainter({required this.urQrProgress});
@override
void paint(Canvas canvas, Size size) {
final c = Offset(size.width / 2.0, size.height / 2.0);
final radius = size.width * 0.9;
final rect = Rect.fromCenter(center: c, width: radius, height: radius);
const fullAngle = 360.0;
var startAngle = 0.0;
for (int i = 0; i < urQrProgress.expectedPartCount.toInt(); i++) {
var sweepAngle =
(1 / urQrProgress.expectedPartCount) * fullAngle * pi / 180.0;
drawSector(canvas, urQrProgress.receivedPartIndexes.contains(i), rect,
startAngle, sweepAngle);
startAngle += sweepAngle;
}
}
void drawSector(Canvas canvas, bool isActive, Rect rect, double startAngle,
double sweepAngle) {
final paint = Paint()
..style = PaintingStyle.stroke
..strokeWidth = 8
..strokeCap = StrokeCap.round
..strokeJoin = StrokeJoin.round
..color = isActive ? const Color(0xffff6600) : Colors.white70;
canvas.drawArc(rect, startAngle, sweepAngle, false, paint);
}
@override
bool shouldRepaint(covariant ProgressPainter oldDelegate) {
return urQrProgress != oldDelegate.urQrProgress;
}
}
class URQrProgress {
int expectedPartCount;
int processedPartsCount;
List<int> receivedPartIndexes;
double percentage;
URQrProgress({
required this.expectedPartCount,
required this.processedPartsCount,
required this.receivedPartIndexes,
required this.percentage,
});
bool equals(URQrProgress? progress) {
if (progress == null) {
return false;
}
return processedPartsCount == progress.processedPartsCount;
}
}
+1 -1
View File
@@ -204,7 +204,7 @@ Future<void> initializeAppConfigs() async {
transactionDescriptions: transactionDescriptions,
secureStorage: secureStorage,
anonpayInvoiceInfo: anonpayInvoiceInfo,
initialMigrationVersion: 42,
initialMigrationVersion: 43,
);
}
+24
View File
@@ -248,6 +248,7 @@ class CWMonero extends Monero {
final moneroWallet = wallet as MoneroWallet;
final keys = moneroWallet.keys;
return <String, String>{
'primaryAddress': keys.primaryAddress,
'privateSpendKey': keys.privateSpendKey,
'privateViewKey': keys.privateViewKey,
'publicSpendKey': keys.publicSpendKey,
@@ -357,9 +358,32 @@ class CWMonero extends Monero {
Future<int> getCurrentHeight() async {
return monero_wallet_api.getCurrentHeight();
}
@override
bool importKeyImagesUR(Object wallet, String ur) {
final moneroWallet = wallet as MoneroWallet;
return moneroWallet.importKeyImagesUR(ur);
}
@override
Future<bool> commitTransactionUR(Object wallet, String ur) {
final moneroWallet = wallet as MoneroWallet;
return moneroWallet.submitTransactionUR(ur);
}
@override
String exportOutputsUR(Object wallet, bool all) {
final moneroWallet = wallet as MoneroWallet;
return moneroWallet.exportOutputsUR(all);
}
@override
void monerocCheck() {
checkIfMoneroCIsFine();
}
bool isViewOnly() {
return isViewOnlyBySpendKey();
}
}
+4
View File
@@ -96,6 +96,7 @@ import 'package:cake_wallet/src/screens/transaction_details/rbf_details_page.dar
import 'package:cake_wallet/src/screens/transaction_details/transaction_details_page.dart';
import 'package:cake_wallet/src/screens/unspent_coins/unspent_coins_details_page.dart';
import 'package:cake_wallet/src/screens/unspent_coins/unspent_coins_list_page.dart';
import 'package:cake_wallet/src/screens/ur/animated_ur_page.dart';
import 'package:cake_wallet/src/screens/wallet/wallet_edit_page.dart';
import 'package:cake_wallet/src/screens/wallet_connect/wc_connections_listing_view.dart';
import 'package:cake_wallet/src/screens/wallet_keys/wallet_keys_page.dart';
@@ -732,6 +733,9 @@ Route<dynamic> createRoute(RouteSettings settings) {
case Routes.setup2faInfoPage:
return MaterialPageRoute<void>(builder: (_) => getIt.get<Setup2FAInfoPage>());
case Routes.urqrAnimatedPage:
return MaterialPageRoute<void>(builder: (_) => getIt.get<AnimatedURPage>(param1: settings.arguments));
case Routes.homeSettings:
return CupertinoPageRoute<void>(
builder: (_) => getIt.get<HomeSettingsPage>(param1: settings.arguments),
+1
View File
@@ -108,6 +108,7 @@ class Routes {
static const signPage = '/sign_page';
static const connectDevices = '/device/connect';
static const urqrAnimatedPage = '/urqr/animated_page';
static const walletGroupsDisplayPage = '/wallet_groups_display_page';
static const walletGroupDescription = '/wallet_group_description';
}
@@ -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,
+10
View File
@@ -0,0 +1,10 @@
import 'package:cake_wallet/store/app_store.dart';
import 'package:cw_core/wallet_base.dart';
import 'package:mobx/mobx.dart';
class AnimatedURModel with Store {
AnimatedURModel(this.appStore)
: wallet = appStore.wallet!;
final AppStore appStore;
final WalletBase wallet;
}
@@ -393,6 +393,12 @@ abstract class DashboardViewModelBase with Store {
wallet.type == WalletType.wownero ||
wallet.type == WalletType.haven;
@computed
bool get isMoneroViewOnly {
if (wallet.type != WalletType.monero) return false;
return monero!.isViewOnly();
}
@computed
String? get getMoneroError {
if (wallet.type != WalletType.monero) return null;
@@ -213,7 +213,7 @@ abstract class NodeCreateOrEditViewModelBase with Store {
bool isCameraPermissionGranted =
await PermissionHandler.checkPermission(Permission.camera, context);
if (!isCameraPermissionGranted) return;
String code = await presentQRScanner();
String code = await presentQRScanner(context);
if (code.isEmpty) {
throw Exception('Unexpected scan QR code value: value is empty');
+13 -1
View File
@@ -1,3 +1,5 @@
import 'dart:convert';
import 'package:cake_wallet/view_model/restore/restore_mode.dart';
import 'package:cw_core/wallet_type.dart';
@@ -32,6 +34,16 @@ class RestoredWallet {
final String? privateKey;
factory RestoredWallet.fromKey(Map<String, dynamic> json) {
try {
final codeParsed = jsonDecode(json['raw_qr'].toString());
if (codeParsed["version"] == 0) {
json['address'] = codeParsed["primaryAddress"];
json['view_key'] = codeParsed["privateViewKey"];
json['height'] = codeParsed["restoreHeight"].toString();
}
} catch (e) {
// fine, we don't care, it is only for monero anyway
}
final height = json['height'] as String?;
return RestoredWallet(
restoreMode: json['mode'] as WalletRestoreMode,
@@ -39,7 +51,7 @@ class RestoredWallet {
address: json['address'] as String?,
spendKey: json['spend_key'] as String?,
viewKey: json['view_key'] as String?,
height: height != null ? int.parse(height) : 0,
height: height != null ? int.tryParse(height)??0 : 0,
privateKey: json['private_key'] as String?,
);
}
@@ -1,3 +1,5 @@
import 'dart:convert';
import 'package:cake_wallet/core/seed_validator.dart';
import 'package:cake_wallet/entities/parse_address_from_domain.dart';
import 'package:cake_wallet/entities/qr_scanner.dart';
@@ -48,6 +50,17 @@ class WalletRestoreFromQRCode {
final extracted = sortedKeys.firstWhereOrNull((key) => code.toLowerCase().contains(key));
if (extracted == null) {
// Special case for view-only monero wallet
final codeParsed = json.decode(code);
if (codeParsed["version"] == 0 &&
codeParsed["primaryAddress"] != null &&
codeParsed["privateViewKey"] != null &&
codeParsed["restoreHeight"] != null) {
return WalletType.monero;
}
}
return _walletTypeMap[extracted];
}
@@ -75,7 +88,7 @@ class WalletRestoreFromQRCode {
}
static Future<RestoredWallet> scanQRCodeForRestoring(BuildContext context) async {
String code = await presentQRScanner();
String code = await presentQRScanner(context);
if (code.isEmpty) throw Exception('Unexpected scan QR code value: value is empty');
WalletType? walletType;
@@ -109,7 +122,7 @@ class WalletRestoreFromQRCode {
queryParameters['address'] = _extractAddressFromUrl(code, walletType!);
}
Map<String, dynamic> credentials = {'type': walletType, ...queryParameters};
Map<String, dynamic> credentials = {'type': walletType, ...queryParameters, 'raw_qr': code};
credentials['mode'] = _determineWalletRestoreMode(credentials);
@@ -205,6 +218,17 @@ class WalletRestoreFromQRCode {
return WalletRestoreMode.keys;
}
if (type == WalletType.monero) {
final codeParsed = json.decode(credentials['raw_qr'].toString());
if (codeParsed["version"] != 0) throw UnimplementedError("Found view-only restore with unsupported version");
if (codeParsed["primaryAddress"] == null ||
codeParsed["privateViewKey"] == null ||
codeParsed["restoreHeight"] == null) {
throw UnimplementedError("Missing one or more attributes in the JSON");
}
return WalletRestoreMode.keys;
}
throw Exception('Unexpected restore mode: restore params are invalid');
}
}
+16 -2
View File
@@ -1,3 +1,4 @@
import 'package:cake_wallet/di.dart';
import 'package:cake_wallet/entities/contact.dart';
import 'package:cake_wallet/entities/priority_for_wallet_type.dart';
import 'package:cake_wallet/entities/transaction_description.dart';
@@ -10,7 +11,9 @@ import 'package:cake_wallet/entities/contact_record.dart';
import 'package:cake_wallet/entities/wallet_contact.dart';
import 'package:cake_wallet/polygon/polygon.dart';
import 'package:cake_wallet/reactions/wallet_connect.dart';
import 'package:cake_wallet/routes.dart';
import 'package:cake_wallet/solana/solana.dart';
import 'package:cake_wallet/src/screens/ur/animated_ur_page.dart';
import 'package:cake_wallet/store/app_store.dart';
import 'package:cake_wallet/tron/tron.dart';
import 'package:cake_wallet/view_model/contact_list/contact_list_view_model.dart';
@@ -23,6 +26,7 @@ import 'package:cw_core/transaction_priority.dart';
import 'package:cw_core/unspent_coin_type.dart';
import 'package:cake_wallet/view_model/send/output.dart';
import 'package:cake_wallet/view_model/send/send_template_view_model.dart';
import 'package:flutter/material.dart';
import 'package:hive/hive.dart';
import 'package:mobx/mobx.dart';
import 'package:cake_wallet/entities/template.dart';
@@ -457,7 +461,7 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
}
@action
Future<void> commitTransaction() async {
Future<void> commitTransaction(BuildContext context) async {
if (pendingTransaction == null) {
throw Exception("Pending transaction doesn't exist. It should not be happened.");
}
@@ -476,7 +480,17 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
try {
state = TransactionCommitting();
await pendingTransaction!.commit();
if (pendingTransaction!.shouldCommitUR()) {
final urstr = await pendingTransaction!.commitUR();
final result = await Navigator.of(context).pushNamed(Routes.urqrAnimatedPage, arguments: urstr);
if (result == null) {
state = FailureState("Canceled by user");
return;
}
} else {
await pendingTransaction!.commit();
}
if (walletType == WalletType.nano) {
nano!.updateTransactions(wallet);
@@ -72,6 +72,10 @@ abstract class WalletKeysViewModelBase with Store {
final keys = monero!.getKeys(_appStore.wallet!);
items.addAll([
if (keys['primaryAddress'] != null)
StandartListItem(
title: S.current.primary_address,
value: keys['primaryAddress']!),
if (keys['publicSpendKey'] != null)
StandartListItem(
key: ValueKey('${_walletName}_wallet_public_spend_key_item_key'),
@@ -131,6 +135,10 @@ abstract class WalletKeysViewModelBase with Store {
final keys = haven!.getKeys(_appStore.wallet!);
items.addAll([
if (keys['primaryAddress'] != null)
StandartListItem(
title: S.current.primary_address,
value: keys['primaryAddress']!),
if (keys['publicSpendKey'] != null)
StandartListItem(
key: ValueKey('${_walletName}_wallet_public_spend_key_item_key'),
@@ -168,6 +176,10 @@ abstract class WalletKeysViewModelBase with Store {
final keys = wownero!.getKeys(_appStore.wallet!);
items.addAll([
if (keys['primaryAddress'] != null)
StandartListItem(
title: S.current.primary_address,
value: keys['primaryAddress']!),
if (keys['publicSpendKey'] != null)
StandartListItem(
key: ValueKey('${_walletName}_wallet_public_spend_key_item_key'),