From f48023b60cb95044c834fad95cf0395bee1e0943 Mon Sep 17 00:00:00 2001 From: OmarHatem Date: Mon, 10 Jun 2024 00:28:52 +0200 Subject: [PATCH] finalizing wownero --- android/app/build.gradle | 18 +++---- android/app/src/main/AndroidManifestBase.xml | 3 ++ assets/bitcoin_electrum_server_list.yml | 8 ++- how_to_add_new_wallet_type.md | 2 +- ios/Runner/InfoBase.plist | 22 +++++++- lib/core/address_validator.dart | 2 + lib/entities/default_settings_migration.dart | 41 +++++++++++++-- lib/entities/node_list.dart | 17 +++++++ lib/main.dart | 4 +- .../dashboard/balance_view_model.dart | 2 + .../restore/restore_from_qr_vm.dart | 9 ++++ .../restore/wallet_restore_from_qr_code.dart | 7 ++- .../transaction_details_view_model.dart | 51 +++++++++++++++++-- .../wallet_address_list_view_model.dart | 35 +++++++++++++ lib/view_model/wallet_restore_view_model.dart | 9 ++-- 15 files changed, 205 insertions(+), 25 deletions(-) diff --git a/android/app/build.gradle b/android/app/build.gradle index 5e27aeb9e..70e9f299e 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -61,17 +61,17 @@ android { } } - signingConfigs { - release { - keyAlias keystoreProperties['keyAlias'] - keyPassword keystoreProperties['keyPassword'] - storeFile file(keystoreProperties['storeFile']) - storePassword keystoreProperties['storePassword'] - } - } +// signingConfigs { +// release { +// keyAlias keystoreProperties['keyAlias'] +// keyPassword keystoreProperties['keyPassword'] +// storeFile file(keystoreProperties['storeFile']) +// storePassword keystoreProperties['storePassword'] +// } +// } buildTypes { release { - signingConfig signingConfigs.release +// signingConfig signingConfigs.release shrinkResources false minifyEnabled false diff --git a/android/app/src/main/AndroidManifestBase.xml b/android/app/src/main/AndroidManifestBase.xml index 0c0c74578..b03c8a925 100644 --- a/android/app/src/main/AndroidManifestBase.xml +++ b/android/app/src/main/AndroidManifestBase.xml @@ -91,6 +91,9 @@ + + + diff --git a/assets/bitcoin_electrum_server_list.yml b/assets/bitcoin_electrum_server_list.yml index 2b6649271..8b734a7bb 100644 --- a/assets/bitcoin_electrum_server_list.yml +++ b/assets/bitcoin_electrum_server_list.yml @@ -1,2 +1,8 @@ - - uri: electrum.cakewallet.com:50002 \ No newline at end of file + uri: electrum.cakewallet.com:50002 + useSSL: true +- + uri: btc-electrum.cakewallet.com:50002 + isDefault: true +- + uri: electrs.cakewallet.com:50001 diff --git a/how_to_add_new_wallet_type.md b/how_to_add_new_wallet_type.md index f51428d44..917e87cf4 100644 --- a/how_to_add_new_wallet_type.md +++ b/how_to_add_new_wallet_type.md @@ -214,7 +214,7 @@ Now you can run the codebase and successfully create a wallet for type walletX s **Restore Wallet** - Go to `lib/core/seed_validator.dart` - In the `getWordList` method, add a case to handle `WalletType.walletx` which would return the word list to be used to validate the passed in seeds. -- Next, go to `lib/restore_view_model.dart` +- Next, go to `lib/wallet_restore_view_model.dart` - Modify the `hasRestoreFromPrivateKey` to reflect if walletx supports restore from Key - Add a switch case to handle the various restore modes that walletX supports - Modify the `getCredential` method to handle the restore flows for `WalletType.walletx` diff --git a/ios/Runner/InfoBase.plist b/ios/Runner/InfoBase.plist index 83e60b542..aec00022b 100644 --- a/ios/Runner/InfoBase.plist +++ b/ios/Runner/InfoBase.plist @@ -200,7 +200,7 @@ solana-wallet - + CFBundleTypeRole Viewer CFBundleURLName @@ -220,6 +220,26 @@ tron-wallet + + CFBundleTypeRole + Viewer + CFBundleURLName + wownero + CFBundleURLSchemes + + wownero + + + + CFBundleTypeRole + Viewer + CFBundleURLName + wownero-wallet + CFBundleURLSchemes + + wownero-wallet + + CFBundleVersion $(CURRENT_PROJECT_VERSION) diff --git a/lib/core/address_validator.dart b/lib/core/address_validator.dart index fe6629f51..ee7e7e54a 100644 --- a/lib/core/address_validator.dart +++ b/lib/core/address_validator.dart @@ -139,6 +139,7 @@ class AddressValidator extends TextValidator { switch (type) { case CryptoCurrency.xmr: + case CryptoCurrency.wow: return null; case CryptoCurrency.ada: return null; @@ -266,6 +267,7 @@ class AddressValidator extends TextValidator { static String? getAddressFromStringPattern(CryptoCurrency type) { switch (type) { case CryptoCurrency.xmr: + case CryptoCurrency.wow: return '([^0-9a-zA-Z]|^)4[0-9a-zA-Z]{94}([^0-9a-zA-Z]|\$)' '|([^0-9a-zA-Z]|^)8[0-9a-zA-Z]{94}([^0-9a-zA-Z]|\$)' '|([^0-9a-zA-Z]|^)[0-9a-zA-Z]{106}([^0-9a-zA-Z]|\$)'; diff --git a/lib/entities/default_settings_migration.dart b/lib/entities/default_settings_migration.dart index f3680084b..a8ec55c12 100644 --- a/lib/entities/default_settings_migration.dart +++ b/lib/entities/default_settings_migration.dart @@ -229,6 +229,10 @@ Future defaultSettingsMigration( case 34: await _addElectRsNode(nodes, sharedPreferences); break; + case 36: + await addWowneroNodeList(nodes: nodes); + await changeWowneroCurrentNodeToDefault(sharedPreferences: sharedPreferences, nodes: nodes); + break; default: break; } @@ -488,9 +492,23 @@ Node? getTronDefaultNode({required Box nodes}) { nodes.values.firstWhereOrNull((node) => node.type == WalletType.tron); } -Node? getWowneroDefaultNode({required Box nodes}) { - return nodes.values.firstWhereOrNull((Node node) => node.uriRaw == wowneroDefaultNodeUri) ?? - nodes.values.firstWhereOrNull((node) => node.type == WalletType.wownero); +Node getWowneroDefaultNode({required Box nodes}) { + final timeZone = DateTime.now().timeZoneOffset.inHours; + var nodeUri = ''; + + if (timeZone >= 1) { + // Eurasia + nodeUri = 'node2.monerodevs.org.lol:34568'; + } else if (timeZone <= -4) { + // America + nodeUri = 'node3.monerodevs.org:34568'; + } + + try { + return nodes.values.firstWhere((Node node) => node.uriRaw == nodeUri); + } catch (_) { + return nodes.values.first; + } } Future insecureStorageMigration({ @@ -1021,6 +1039,23 @@ Future changeEthereumCurrentNodeToDefault( await sharedPreferences.setInt(PreferencesKey.currentEthereumNodeIdKey, nodeId); } +Future addWowneroNodeList({required Box nodes}) async { + final nodeList = await loadDefaultWowneroNodes(); + for (var node in nodeList) { + if (nodes.values.firstWhereOrNull((element) => element.uriRaw == node.uriRaw) == null) { + await nodes.add(node); + } + } +} + +Future changeWowneroCurrentNodeToDefault( + {required SharedPreferences sharedPreferences, required Box nodes}) async { + final node = getWowneroDefaultNode(nodes: nodes); + final nodeId = node?.key as int? ?? 0; + + await sharedPreferences.setInt(PreferencesKey.currentWowneroNodeIdKey, nodeId); +} + Future addNanoNodeList({required Box nodes}) async { final nodeList = await loadDefaultNanoNodes(); for (var node in nodeList) { diff --git a/lib/entities/node_list.dart b/lib/entities/node_list.dart index c1211d2fe..85e37a7bc 100644 --- a/lib/entities/node_list.dart +++ b/lib/entities/node_list.dart @@ -183,6 +183,23 @@ Future> loadDefaultTronNodes() async { return nodes; } +Future> loadDefaultWowneroNodes() async { + final nodesRaw = await rootBundle.loadString('assets/wownero_node_list.yml'); + final loadedNodes = loadYaml(nodesRaw) as YamlList; + final nodes = []; + + for (final raw in loadedNodes) { + if (raw is Map) { + final node = Node.fromMap(Map.from(raw)); + + node.type = WalletType.wownero; + nodes.add(node); + } + } + + return nodes; +} + Future resetToDefault(Box nodeSource) async { final moneroNodes = await loadDefaultNodes(); final bitcoinElectrumServerList = await loadBitcoinElectrumServerList(); diff --git a/lib/main.dart b/lib/main.dart index 3618e40d3..03ca018d3 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,5 +1,4 @@ import 'dart:async'; -import 'dart:io' show Platform; import 'package:cake_wallet/anonpay/anonpay_invoice_info.dart'; import 'package:cake_wallet/buy/order.dart'; import 'package:cake_wallet/core/auth_service.dart'; @@ -42,7 +41,6 @@ import 'package:hive/hive.dart'; import 'package:cw_core/root_dir.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:cw_core/window_size.dart'; -import 'package:monero/monero.dart' as monero_dart; final navigatorKey = GlobalKey(); final rootKey = GlobalKey(); @@ -205,7 +203,7 @@ Future initializeAppConfigs() async { transactionDescriptions: transactionDescriptions, secureStorage: secureStorage, anonpayInvoiceInfo: anonpayInvoiceInfo, - initialMigrationVersion: 34, + initialMigrationVersion: 36, ); } diff --git a/lib/view_model/dashboard/balance_view_model.dart b/lib/view_model/dashboard/balance_view_model.dart index 5ae532bb6..c8acb9c2c 100644 --- a/lib/view_model/dashboard/balance_view_model.dart +++ b/lib/view_model/dashboard/balance_view_model.dart @@ -125,6 +125,7 @@ abstract class BalanceViewModelBase with Store { String get availableBalanceLabel { switch (wallet.type) { case WalletType.monero: + case WalletType.wownero: case WalletType.haven: case WalletType.ethereum: case WalletType.polygon: @@ -142,6 +143,7 @@ abstract class BalanceViewModelBase with Store { String get additionalBalanceLabel { switch (wallet.type) { case WalletType.monero: + case WalletType.wownero: case WalletType.haven: case WalletType.ethereum: case WalletType.polygon: diff --git a/lib/view_model/restore/restore_from_qr_vm.dart b/lib/view_model/restore/restore_from_qr_vm.dart index c92612580..0ddfb56ce 100644 --- a/lib/view_model/restore/restore_from_qr_vm.dart +++ b/lib/view_model/restore/restore_from_qr_vm.dart @@ -75,6 +75,15 @@ abstract class WalletRestorationFromQRVMBase extends WalletCreationVM with Store viewKey: restoreWallet.viewKey ?? '', spendKey: restoreWallet.spendKey ?? '', height: restoreWallet.height ?? 0); + case WalletType.wownero: + return wownero!.createWowneroRestoreWalletFromKeysCredentials( + name: name, + password: password, + language: 'English', + address: restoreWallet.address ?? '', + viewKey: restoreWallet.viewKey ?? '', + spendKey: restoreWallet.spendKey ?? '', + height: restoreWallet.height ?? 0); case WalletType.bitcoin: case WalletType.litecoin: return bitcoin!.createBitcoinRestoreWalletFromWIFCredentials( diff --git a/lib/view_model/restore/wallet_restore_from_qr_code.dart b/lib/view_model/restore/wallet_restore_from_qr_code.dart index 09b5c9d96..37a0ad162 100644 --- a/lib/view_model/restore/wallet_restore_from_qr_code.dart +++ b/lib/view_model/restore/wallet_restore_from_qr_code.dart @@ -36,6 +36,9 @@ class WalletRestoreFromQRCode { 'tron': WalletType.tron, 'tron-wallet': WalletType.tron, 'tron_wallet': WalletType.tron, + 'wownero': WalletType.wownero, + 'wownero-wallet': WalletType.wownero, + 'wownero_wallet': WalletType.wownero, }; static bool _containsAssetSpecifier(String code) => _extractWalletType(code) != null; @@ -57,7 +60,9 @@ class WalletRestoreFromQRCode { RegExp _getPattern(int wordCount) => RegExp(r'(?<=\W|^)((?:\w+\s+){' + (wordCount - 1).toString() + r'}\w+)(?=\W|$)'); - List patternCounts = walletType == WalletType.monero ? [25, 16, 14, 13] : [24, 18, 12]; + List patternCounts = walletType == WalletType.monero || walletType == WalletType.wownero + ? [25, 16, 14, 13] + : [24, 18, 12]; for (final count in patternCounts) { final pattern = _getPattern(count); diff --git a/lib/view_model/transaction_details_view_model.dart b/lib/view_model/transaction_details_view_model.dart index a8ff6afa9..5b7a1a8db 100644 --- a/lib/view_model/transaction_details_view_model.dart +++ b/lib/view_model/transaction_details_view_model.dart @@ -1,4 +1,5 @@ import 'package:cake_wallet/tron/tron.dart'; +import 'package:cake_wallet/wownero/wownero.dart'; import 'package:cw_core/wallet_base.dart'; import 'package:cw_core/transaction_info.dart'; import 'package:cw_core/wallet_type.dart'; @@ -76,7 +77,7 @@ abstract class TransactionDetailsViewModelBase with Store { _addTronListItems(tx, dateFormat); break; case WalletType.wownero: - // _addWowneroListItems(tx, dateFormat); + _addWowneroListItems(tx, dateFormat); break; default: break; @@ -169,7 +170,9 @@ abstract class TransactionDetailsViewModelBase with Store { return 'https://solscan.io/tx/${txId}'; case WalletType.tron: return 'https://tronscan.org/#/transaction/${txId}'; - default: + case WalletType.wownero: + return 'https://explore.wownero.com/tx/${txId}'; + case WalletType.none: return ''; } } @@ -197,7 +200,9 @@ abstract class TransactionDetailsViewModelBase with Store { return S.current.view_transaction_on + 'solscan.io'; case WalletType.tron: return S.current.view_transaction_on + 'tronscan.org'; - default: + case WalletType.wownero: + return S.current.view_transaction_on + 'Wownero.com'; + case WalletType.none: return ''; } } @@ -442,4 +447,44 @@ abstract class TransactionDetailsViewModelBase with Store { String get pendingTransactionFeeFiatAmountFormatted => sendViewModel.isFiatDisabled ? '' : sendViewModel.pendingTransactionFeeFiatAmount + ' ' + sendViewModel.fiat.title; + + void _addWowneroListItems(TransactionInfo tx, DateFormat dateFormat) { + final key = tx.additionalInfo['key'] as String?; + final accountIndex = tx.additionalInfo['accountIndex'] as int; + final addressIndex = tx.additionalInfo['addressIndex'] as int; + final feeFormatted = tx.feeFormatted(); + final _items = [ + StandartListItem(title: S.current.transaction_details_transaction_id, value: tx.id), + StandartListItem( + title: S.current.transaction_details_date, value: dateFormat.format(tx.date)), + StandartListItem(title: S.current.transaction_details_height, value: '${tx.height}'), + StandartListItem(title: S.current.transaction_details_amount, value: tx.amountFormatted()), + if (feeFormatted != null) + StandartListItem(title: S.current.transaction_details_fee, value: feeFormatted), + if (key?.isNotEmpty ?? false) StandartListItem(title: S.current.transaction_key, value: key!), + ]; + + if (tx.direction == TransactionDirection.incoming) { + try { + final address = wownero!.getTransactionAddress(wallet, accountIndex, addressIndex); + final label = wownero!.getSubaddressLabel(wallet, accountIndex, addressIndex); + + if (address.isNotEmpty) { + isRecipientAddressShown = true; + _items.add(StandartListItem( + title: S.current.transaction_details_recipient_address, + value: address, + )); + } + + if (label.isNotEmpty) { + _items.add(StandartListItem(title: S.current.address_label, value: label)); + } + } catch (e) { + print(e.toString()); + } + } + + items.addAll(_items); + } } diff --git a/lib/view_model/wallet_address_list/wallet_address_list_view_model.dart b/lib/view_model/wallet_address_list/wallet_address_list_view_model.dart index 6b59c9033..dd7f02407 100644 --- a/lib/view_model/wallet_address_list/wallet_address_list_view_model.dart +++ b/lib/view_model/wallet_address_list/wallet_address_list_view_model.dart @@ -17,6 +17,7 @@ import 'package:cake_wallet/utils/list_item.dart'; import 'package:cake_wallet/view_model/wallet_address_list/wallet_account_list_header.dart'; import 'package:cake_wallet/view_model/wallet_address_list/wallet_address_list_header.dart'; import 'package:cake_wallet/view_model/wallet_address_list/wallet_address_list_item.dart'; +import 'package:cake_wallet/wownero/wownero.dart'; import 'package:cw_core/amount_converter.dart'; import 'package:cw_core/currency.dart'; import 'package:cw_core/wallet_type.dart'; @@ -191,6 +192,22 @@ class TronURI extends PaymentURI { } } +class WowneroURI extends PaymentURI { + WowneroURI({required String amount, required String address}) + : super(amount: amount, address: address); + + @override + String toString() { + var base = 'wownero:' + address; + + if (amount.isNotEmpty) { + base += '?tx_amount=${amount.replaceAll(',', '.')}'; + } + + return base; + } +} + abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewModel with Store { WalletAddressListViewModelBase({ required AppStore appStore, @@ -293,6 +310,10 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo return TronURI(amount: amount, address: address.address); } + if (wallet.type == WalletType.wownero) { + return WowneroURI(amount: amount, address: address.address); + } + throw Exception('Unexpected type: ${type.toString()}'); } @@ -409,6 +430,20 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo addressList.add(WalletAddressListItem(isPrimary: true, name: null, address: primaryAddress)); } + if (wallet.type == WalletType.wownero) { + final primaryAddress = wownero!.getSubaddressList(wallet).subaddresses.first; + final addressItems = wownero!.getSubaddressList(wallet).subaddresses.map((subaddress) { + final isPrimary = subaddress == primaryAddress; + + return WalletAddressListItem( + id: subaddress.id, + isPrimary: isPrimary, + name: subaddress.label, + address: subaddress.address); + }); + addressList.addAll(addressItems); + } + if (searchText.isNotEmpty) { return ObservableList.of(addressList.where((item) { if (item is WalletAddressListItem) { diff --git a/lib/view_model/wallet_restore_view_model.dart b/lib/view_model/wallet_restore_view_model.dart index 22e86ed95..41d17e99d 100644 --- a/lib/view_model/wallet_restore_view_model.dart +++ b/lib/view_model/wallet_restore_view_model.dart @@ -29,8 +29,10 @@ abstract class WalletRestoreViewModelBase extends WalletCreationVM with Store { WalletRestoreViewModelBase(AppStore appStore, WalletCreationService walletCreationService, Box walletInfoSource, {required WalletType type}) - : hasSeedLanguageSelector = type == WalletType.monero || type == WalletType.haven, - hasBlockchainHeightLanguageSelector = type == WalletType.monero || type == WalletType.haven, + : hasSeedLanguageSelector = + type == WalletType.monero || type == WalletType.haven || type == WalletType.wownero, + hasBlockchainHeightLanguageSelector = + type == WalletType.monero || type == WalletType.haven || type == WalletType.wownero, hasRestoreFromPrivateKey = type == WalletType.ethereum || type == WalletType.polygon || type == WalletType.nano || @@ -112,6 +114,7 @@ abstract class WalletRestoreViewModelBase extends WalletCreationVM with Store { return bitcoinCash!.createBitcoinCashRestoreWalletFromSeedCredentials( name: name, mnemonic: seed, password: password); case WalletType.nano: + case WalletType.banano: return nano!.createNanoRestoreWalletFromSeedCredentials( name: name, mnemonic: seed, @@ -143,7 +146,7 @@ abstract class WalletRestoreViewModelBase extends WalletCreationVM with Store { password: password, height: height, ); - default: + case WalletType.none: break; } }