* feat: rebase btc-addr-types, migrate to bitcoin_base

* feat: allow scanning elect-rs using get_tweaks

* feat: scanning and adding addresses working with getTweaks, add btc SP address type

* chore: pubspec.lock

* chore: pubspec.lock

* fix: scan when switching, fix multiple unspents in same tx

* fix: initial scan

* fix: initial scan

* fix: scanning issues

* fix: sync, storing silent unspents

* chore: deps

* fix: label issues, clear spent utxo

* chore: deps

* fix: build

* fix: missing types

* feat: new electrs API & changes, fixes for last block scanning

* feat: Scan Silent Payments homepage toggle

* chore: build configure

* feat: generic fixes, testnet UI improvements, useSSL on bitcoin nodes

* fix: invalid Object in sendData

* feat: improve addresses page & address book displays

* feat: silent payments labeled addresses disclaimer

* fix: missing i18n

* chore: print

* feat: single block scan, rescan by date working for btc mainnet

* feat: new cake features page replace market page, move sp scan toggle, auto switch node pop up alert

* feat: delete silent addresses

* fix: red dot in non ssl nodes

* fix: inconsistent connection states, fix tx history

* fix: tx & balance displays, cpfp sending

* feat: new rust lib

* chore: node path

* fix: check node based on network

* fix: missing txcount from addresses

* style: padding in feature page cards

* fix: restore not getting all wallet addresses by type

* fix: auto switch node broken

* fix: silent payment txs not being restored

* feat: change scanning to subscription model, sync improvements

* fix: scan re-subscription

* fix: default nodes

* fix: improve scanning by date, fix single block scan

* refactor: common function for input tx selection

* fix: nodes & build

* fix: send all with multiple outs

* refactor: unchanged file

* Update pr_test_build.yml

* chore: upgrade

* chore: merge changes

* refactor: unchanged files [skip ci]

* fix: scan fixes, add date, allow sending while scanning

* feat: sync fixes, sp settings

* feat: fix resyncing

* fix: date from height logic, status disconnected & chain tip get

* fix: params

* feat: electrum migration if using cake electrum

* fix nodes
update versions

* re-enable tron

* update sp_scanner to work on iOS [skip ci]

* fix: wrong socket for old electrum nodes

* Fix unchecked wallet type call

* fix: double balance

* feat: node domain

* fix: menu name

* fix: update tip on set scanning

* fix: connection switching back and forth

* feat: check if node is electrs, and supports sp

* chore: fix build

* minor enhancements

* fixes and enhancements

* solve conflicts with main

* fix: status toggle

* minor enhancement

* Monero.com fixes

* update sp_scanner to include windows and linux

---------

Co-authored-by: Omar Hatem <omarh.ismail1@gmail.com>
This commit is contained in:
Rafael
2024-05-29 17:43:48 +03:00
committed by GitHub
co-authored by Omar Hatem
parent faa49d21e8
commit 96b9b60f50
122 changed files with 3889 additions and 1252 deletions
@@ -2,6 +2,7 @@ import 'dart:async';
import 'package:cake_wallet/entities/auto_generate_subaddress_status.dart';
import 'package:cake_wallet/entities/contact_base.dart';
import 'package:cake_wallet/entities/wallet_contact.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/store/settings_store.dart';
import 'package:cw_core/wallet_info.dart';
import 'package:cw_core/wallet_type.dart';
@@ -40,16 +41,19 @@ abstract class ContactListViewModelBase with Store {
});
} else if (info.addresses?.isNotEmpty == true) {
info.addresses!.forEach((address, label) {
if (label.isEmpty) {
return;
}
final name = _createName(info.name, label);
walletContacts.add(WalletContact(
address,
name,
walletTypeToCryptoCurrency(info.type),
walletTypeToCryptoCurrency(info.type,
isTestnet:
info.network == null ? false : info.network!.toLowerCase().contains("testnet")),
));
// Only one contact address per wallet
return;
});
} else if (info.address != null) {
} else {
walletContacts.add(WalletContact(
info.address,
info.name,
@@ -64,7 +68,9 @@ abstract class ContactListViewModelBase with Store {
}
String _createName(String walletName, String label) {
return label.isNotEmpty ? '$walletName ($label)' : walletName;
return label.isNotEmpty
? '$walletName (${label.replaceAll(RegExp(r'active', caseSensitive: false), S.current.active).replaceAll(RegExp(r'silent payments', caseSensitive: false), S.current.silent_payments)})'
: walletName;
}
final bool isAutoGenerateEnabled;
@@ -60,6 +60,9 @@ abstract class BalanceViewModelBase with Store {
@observable
WalletBase<Balance, TransactionHistoryBase<TransactionInfo>, TransactionInfo> wallet;
@computed
bool get hasSilentPayments => wallet.type == WalletType.bitcoin;
@computed
double get price {
final price = fiatConvertationStore.prices[appStore.wallet!.currency];
@@ -0,0 +1,16 @@
import 'package:cake_wallet/ionia/ionia_service.dart';
import 'package:mobx/mobx.dart';
part 'cake_features_view_model.g.dart';
class CakeFeaturesViewModel = CakeFeaturesViewModelBase with _$CakeFeaturesViewModel;
abstract class CakeFeaturesViewModelBase with Store {
final IoniaService _ioniaService;
CakeFeaturesViewModelBase(this._ioniaService);
Future<bool> isIoniaUserAuthenticated() async {
return await _ioniaService.isLogined();
}
}
@@ -45,6 +45,7 @@ import 'package:cw_core/wallet_type.dart';
import 'package:eth_sig_util/util/utils.dart';
import 'package:flutter/services.dart';
import 'package:mobx/mobx.dart';
import 'package:cake_wallet/bitcoin/bitcoin.dart';
import 'package:http/http.dart' as http;
import 'package:shared_preferences/shared_preferences.dart';
@@ -201,6 +202,14 @@ abstract class DashboardViewModelBase with Store {
return true;
});
if (hasSilentPayments) {
silentPaymentsScanningActive = bitcoin!.getScanningActive(wallet);
reaction((_) => wallet.syncStatus, (SyncStatus syncStatus) {
silentPaymentsScanningActive = bitcoin!.getScanningActive(wallet);
});
}
}
@observable
@@ -287,11 +296,36 @@ abstract class DashboardViewModelBase with Store {
@observable
WalletBase<Balance, TransactionHistoryBase<TransactionInfo>, TransactionInfo> wallet;
bool get hasRescan => wallet.type == WalletType.monero || wallet.type == WalletType.haven;
@computed
bool get isTestnet => wallet.type == WalletType.bitcoin && bitcoin!.isTestnet(wallet);
@computed
bool get hasRescan =>
wallet.type == WalletType.bitcoin ||
wallet.type == WalletType.monero ||
wallet.type == WalletType.haven;
@computed
bool get hasSilentPayments => wallet.type == WalletType.bitcoin;
@computed
bool get showSilentPaymentsCard => hasSilentPayments && settingsStore.silentPaymentsCardDisplay;
final KeyService keyService;
final SharedPreferences sharedPreferences;
@observable
bool silentPaymentsScanningActive = false;
@action
void setSilentPaymentsScanning(bool active) {
silentPaymentsScanningActive = active;
if (hasSilentPayments) {
bitcoin!.setScanningActive(wallet, active);
}
}
BalanceViewModel balanceViewModel;
AppStore appStore;
@@ -1,17 +0,0 @@
import 'package:cake_wallet/ionia/ionia_service.dart';
import 'package:mobx/mobx.dart';
part 'market_place_view_model.g.dart';
class MarketPlaceViewModel = MarketPlaceViewModelBase with _$MarketPlaceViewModel;
abstract class MarketPlaceViewModelBase with Store {
final IoniaService _ioniaService;
MarketPlaceViewModelBase(this._ioniaService);
Future<bool> isIoniaUserAuthenticated() async {
return await _ioniaService.isLogined();
}
}
+20 -7
View File
@@ -1,4 +1,6 @@
import 'package:cake_wallet/bitcoin/bitcoin.dart';
import 'package:cw_core/wallet_base.dart';
import 'package:cw_core/wallet_type.dart';
import 'package:mobx/mobx.dart';
part 'rescan_view_model.g.dart';
@@ -8,11 +10,12 @@ class RescanViewModel = RescanViewModelBase with _$RescanViewModel;
enum RescanWalletState { rescaning, none }
abstract class RescanViewModelBase with Store {
RescanViewModelBase(this._wallet)
: state = RescanWalletState.none,
isButtonEnabled = false;
RescanViewModelBase(this.wallet)
: state = RescanWalletState.none,
isButtonEnabled = false,
doSingleScan = false;
final WalletBase _wallet;
final WalletBase wallet;
@observable
RescanWalletState state;
@@ -20,11 +23,21 @@ abstract class RescanViewModelBase with Store {
@observable
bool isButtonEnabled;
@observable
bool doSingleScan;
@computed
bool get isSilentPaymentsScan => wallet.type == WalletType.bitcoin;
@action
Future<void> rescanCurrentWallet({required int restoreHeight}) async {
state = RescanWalletState.rescaning;
await _wallet.rescan(height: restoreHeight);
_wallet.transactionHistory.clear();
if (wallet.type != WalletType.bitcoin) {
wallet.rescan(height: restoreHeight);
wallet.transactionHistory.clear();
} else {
bitcoin!.rescan(wallet, height: restoreHeight, doSingleScan: doSingleScan);
}
state = RescanWalletState.none;
}
}
}
+14 -1
View File
@@ -229,7 +229,10 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
isFiatDisabled ? '' : pendingTransactionFeeFiatAmount + ' ' + fiat.title;
@computed
bool get isReadyForSend => wallet.syncStatus is SyncedSyncStatus;
bool get isReadyForSend =>
wallet.syncStatus is SyncedSyncStatus ||
// If silent payments scanning, can still send payments
(wallet.type == WalletType.bitcoin && wallet.syncStatus is SyncingSyncStatus);
@computed
List<Template> get templates => sendTemplateViewModel.templates
@@ -599,6 +602,10 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
walletType == WalletType.litecoin ||
walletType == WalletType.bitcoinCash) {
if (error is TransactionWrongBalanceException) {
if (error.amount != null)
return S.current
.tx_wrong_balance_with_amount_exception(currency.toString(), error.amount.toString());
return S.current.tx_wrong_balance_exception(currency.toString());
}
if (error is TransactionNoInputsException) {
@@ -625,9 +632,15 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
if (error is TransactionCommitFailedVoutNegative) {
return S.current.tx_rejected_vout_negative;
}
if (error is TransactionCommitFailedBIP68Final) {
return S.current.tx_rejected_bip68_final;
}
if (error is TransactionNoDustOnChangeException) {
return S.current.tx_commit_exception_no_dust_on_change(error.min, error.max);
}
if (error is TransactionInputNotSupported) {
return S.current.tx_invalid_input;
}
}
return errorMessage;
@@ -0,0 +1,33 @@
import 'package:cake_wallet/bitcoin/bitcoin.dart';
import 'package:cake_wallet/store/settings_store.dart';
import 'package:cw_core/wallet_base.dart';
import 'package:mobx/mobx.dart';
part 'silent_payments_settings_view_model.g.dart';
class SilentPaymentsSettingsViewModel = SilentPaymentsSettingsViewModelBase
with _$SilentPaymentsSettingsViewModel;
abstract class SilentPaymentsSettingsViewModelBase with Store {
SilentPaymentsSettingsViewModelBase(this._settingsStore, this._wallet);
final SettingsStore _settingsStore;
final WalletBase _wallet;
@computed
bool get silentPaymentsCardDisplay => _settingsStore.silentPaymentsCardDisplay;
@computed
bool get silentPaymentsAlwaysScan => _settingsStore.silentPaymentsAlwaysScan;
@action
void setSilentPaymentsCardDisplay(bool value) {
_settingsStore.silentPaymentsCardDisplay = value;
}
@action
void setSilentPaymentsAlwaysScan(bool value) {
_settingsStore.silentPaymentsAlwaysScan = value;
if (value) bitcoin!.setScanningActive(_wallet, true);
}
}
@@ -15,7 +15,8 @@ abstract class UnspentCoinsItemBase with Store {
required this.isChange,
required this.amountRaw,
required this.vout,
required this.keyImage
required this.keyImage,
required this.isSilentPayment,
});
@observable
@@ -47,4 +48,7 @@ abstract class UnspentCoinsItemBase with Store {
@observable
String? keyImage;
@observable
bool isSilentPayment;
}
@@ -1,10 +1,12 @@
import 'package:cake_wallet/bitcoin/bitcoin.dart';
import 'package:cake_wallet/monero/monero.dart';
import 'package:cake_wallet/utils/exception_handler.dart';
import 'package:cake_wallet/view_model/unspent_coins/unspent_coins_item.dart';
import 'package:cw_core/unspent_coins_info.dart';
import 'package:cw_core/unspent_transaction_output.dart';
import 'package:cw_core/wallet_base.dart';
import 'package:cw_core/wallet_type.dart';
import 'package:flutter/cupertino.dart';
import 'package:hive/hive.dart';
import 'package:mobx/mobx.dart';
@@ -86,22 +88,33 @@ abstract class UnspentCoinsListViewModelBase with Store {
@action
void _updateUnspentCoinsInfo() {
_items.clear();
_items.addAll(_getUnspents().map((elem) {
final info =
getUnspentCoinInfo(elem.hash, elem.address, elem.value, elem.vout, elem.keyImage);
return UnspentCoinsItem(
address: elem.address,
amount: '${formatAmountToString(elem.value)} ${wallet.currency.title}',
hash: elem.hash,
isFrozen: info.isFrozen,
note: info.note,
isSending: info.isSending,
amountRaw: elem.value,
vout: elem.vout,
keyImage: elem.keyImage,
isChange: elem.isChange,
);
}));
List<UnspentCoinsItem> unspents = [];
_getUnspents().forEach((elem) {
try {
final info =
getUnspentCoinInfo(elem.hash, elem.address, elem.value, elem.vout, elem.keyImage);
unspents.add(UnspentCoinsItem(
address: elem.address,
amount: '${formatAmountToString(elem.value)} ${wallet.currency.title}',
hash: elem.hash,
isFrozen: info.isFrozen,
note: info.note,
isSending: info.isSending,
amountRaw: elem.value,
vout: elem.vout,
keyImage: elem.keyImage,
isChange: elem.isChange,
isSilentPayment: info.isSilentPayment ?? false,
));
} catch (e, s) {
print(s);
print(e.toString());
ExceptionHandler.onError(FlutterErrorDetails(exception: e, stack: s));
}
});
_items.addAll(unspents);
}
}
@@ -1,3 +1,6 @@
import 'package:cake_wallet/utils/list_item.dart';
class WalletAddressListHeader extends ListItem {}
class WalletAddressListHeader extends ListItem {
final String? title;
WalletAddressListHeader({this.title});
}
@@ -8,8 +8,10 @@ class WalletAddressListItem extends ListItem {
this.name,
this.txCount,
this.balance,
this.isChange = false})
: super();
this.isChange = false,
// Address that is only ever used once, shouldn't be used to receive funds, copy and paste, share etc
this.isOneTimeReceiveAddress = false,
}) : super();
final int? id;
final bool isPrimary;
@@ -18,7 +20,8 @@ class WalletAddressListItem extends ListItem {
final int? txCount;
final String? balance;
final bool isChange;
final bool? isOneTimeReceiveAddress;
@override
String toString() => name ?? address;
}
}
@@ -334,20 +334,55 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo
}
if (isElectrumWallet) {
final addressItems = bitcoin!.getSubAddresses(wallet).map((subaddress) {
final isPrimary = subaddress.id == 0;
if (bitcoin!.hasSelectedSilentPayments(wallet)) {
final addressItems = bitcoin!.getSilentPaymentAddresses(wallet).map((address) {
final isPrimary = address.id == 0;
return WalletAddressListItem(
id: subaddress.id,
return WalletAddressListItem(
id: address.id,
isPrimary: isPrimary,
name: subaddress.name,
address: subaddress.address,
txCount: subaddress.txCount,
name: address.name,
address: address.address,
txCount: address.txCount,
balance: AmountConverter.amountIntToString(
walletTypeToCryptoCurrency(type), subaddress.balance),
isChange: subaddress.isChange);
});
addressList.addAll(addressItems);
walletTypeToCryptoCurrency(type), address.balance),
isChange: address.isChange,
);
});
addressList.addAll(addressItems);
addressList.add(WalletAddressListHeader(title: S.current.received));
final receivedAddressItems =
bitcoin!.getSilentPaymentReceivedAddresses(wallet).map((address) {
return WalletAddressListItem(
id: address.id,
isPrimary: false,
name: address.name,
address: address.address,
txCount: address.txCount,
balance: AmountConverter.amountIntToString(
walletTypeToCryptoCurrency(type), address.balance),
isChange: address.isChange,
isOneTimeReceiveAddress: true,
);
});
addressList.addAll(receivedAddressItems);
} else {
final addressItems = bitcoin!.getSubAddresses(wallet).map((subaddress) {
final isPrimary = subaddress.id == 0;
return WalletAddressListItem(
id: subaddress.id,
isPrimary: isPrimary,
name: subaddress.name,
address: subaddress.address,
txCount: subaddress.txCount,
balance: AmountConverter.amountIntToString(
walletTypeToCryptoCurrency(type), subaddress.balance),
isChange: subaddress.isChange);
});
addressList.addAll(addressItems);
}
}
if (wallet.type == WalletType.ethereum) {
@@ -416,9 +451,14 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo
wallet.type == WalletType.litecoin ||
wallet.type == WalletType.bitcoinCash;
@computed
bool get isSilentPayments =>
wallet.type == WalletType.bitcoin && bitcoin!.hasSelectedSilentPayments(wallet);
@computed
bool get isAutoGenerateSubaddressEnabled =>
_settingsStore.autoGenerateSubaddressStatus != AutoGenerateSubaddressStatus.disabled;
_settingsStore.autoGenerateSubaddressStatus != AutoGenerateSubaddressStatus.disabled &&
!isSilentPayments;
List<ListItem> _baseItems;
@@ -478,4 +518,11 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo
amount = '';
}
}
@action
void deleteAddress(ListItem item) {
if (wallet.type == WalletType.bitcoin && item is WalletAddressListItem) {
bitcoin!.deleteSilentPaymentAddress(wallet, item.address);
}
}
}
@@ -7,6 +7,7 @@ class WalletListItem {
required this.key,
this.isCurrent = false,
this.isEnabled = true,
this.isTestnet = false,
});
final String name;
@@ -14,4 +15,5 @@ class WalletListItem {
final bool isCurrent;
final dynamic key;
final bool isEnabled;
final bool isTestnet;
}
@@ -61,6 +61,7 @@ abstract class WalletListViewModelBase with Store {
key: info.key,
isCurrent: info.name == _appStore.wallet?.name && info.type == _appStore.wallet?.type,
isEnabled: availableWalletTypes.contains(info.type),
isTestnet: info.network?.toLowerCase().contains('testnet') ?? false,
),
),
);