mirror of
https://github.com/Such-Software/hash-wallet.git
synced 2026-10-21 06:18:42 +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:
@@ -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