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
+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'),