mirror of
https://github.com/Such-Software/hash-wallet.git
synced 2026-10-19 05:58:50 +02:00
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:
@@ -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));
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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
@@ -204,7 +204,7 @@ Future<void> initializeAppConfigs() async {
|
||||
transactionDescriptions: transactionDescriptions,
|
||||
secureStorage: secureStorage,
|
||||
anonpayInvoiceInfo: anonpayInvoiceInfo,
|
||||
initialMigrationVersion: 42,
|
||||
initialMigrationVersion: 43,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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 (_) {}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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'),
|
||||
|
||||
Reference in New Issue
Block a user