Sp enhancements (#1672)

* fix: enhance regex, fix multiline

* feat: improve scan msg, fix missing txs, use date api

* feat: node fixes, enhance send modal, TX list tag & filter, refactors

* fix: continuous scanning

* fix: missing close

* fix: resubscribe tweaks

* feat: use mempool api setting toggle

* handle any failure of height API and fallback to the old method [skip ci]

---------

Co-authored-by: OmarHatem <omarh.ismail1@gmail.com>
This commit is contained in:
Rafael
2024-09-20 17:24:25 +03:00
committed by GitHub
co-authored by OmarHatem
parent b6bc3df622
commit 3a391f10a3
61 changed files with 872 additions and 531 deletions
+46 -2
View File
@@ -361,7 +361,7 @@ class CWBitcoin extends Bitcoin {
continue;
}
final sh = scriptHash(address, network: network);
final sh = BitcoinAddressUtils.scriptHash(address, network: network);
final history = await electrumClient.getHistory(sh);
final balance = await electrumClient.getBalance(sh);
@@ -522,7 +522,20 @@ class CWBitcoin extends Bitcoin {
}
@override
int getHeightByDate({required DateTime date}) => getBitcoinHeightByDate(date: date);
Future<bool> checkIfMempoolAPIIsEnabled(Object wallet) async {
final bitcoinWallet = wallet as ElectrumWallet;
return await bitcoinWallet.checkIfMempoolAPIIsEnabled();
}
@override
Future<int> getHeightByDate({required DateTime date, bool? bitcoinMempoolAPIEnabled}) async {
if (bitcoinMempoolAPIEnabled ?? false) {
try {
return await getBitcoinHeightByDateAPI(date: date);
} catch (_) {}
}
return await getBitcoinHeightByDate(date: date);
}
@override
Future<void> rescan(Object wallet, {required int height, bool? doSingleScan}) async {
@@ -547,4 +560,35 @@ class CWBitcoin extends Bitcoin {
final bitcoinWallet = wallet as ElectrumWallet;
await bitcoinWallet.updateFeeRates();
}
@override
List<Output> updateOutputs(PendingTransaction pendingTransaction, List<Output> outputs) {
final pendingTx = pendingTransaction as PendingBitcoinTransaction;
if (!pendingTx.hasSilentPayment) {
return outputs;
}
final updatedOutputs = outputs.map((output) {
try {
final pendingOut = pendingTx!.outputs[outputs.indexOf(output)];
final updatedOutput = output;
updatedOutput.stealthAddress = P2trAddress.fromScriptPubkey(script: pendingOut.scriptPubKey)
.toAddress(BitcoinNetwork.mainnet);
return updatedOutput;
} catch (_) {}
return output;
}).toList();
return updatedOutputs;
}
@override
bool txIsReceivedSilentPayment(TransactionInfo txInfo) {
final tx = txInfo as ElectrumTransactionInfo;
return tx.isReceivedSilentPayment;
}
}
+66 -57
View File
@@ -5,32 +5,40 @@ import 'package:cake_wallet/solana/solana.dart';
import 'package:cw_core/crypto_currency.dart';
import 'package:cw_core/erc20_token.dart';
const BEFORE_REGEX = '(^|\s)';
const AFTER_REGEX = '(\$|\s)';
class AddressValidator extends TextValidator {
AddressValidator({required CryptoCurrency type})
: super(
errorMessage: S.current.error_text_address,
useAdditionalValidation: type == CryptoCurrency.btc
? (String txt) => validateAddress(address: txt, network: BitcoinNetwork.mainnet)
? (String txt) => BitcoinAddressUtils.validateAddress(
address: txt,
network: BitcoinNetwork.mainnet,
)
: null,
pattern: getPattern(type),
length: getLength(type));
static String getPattern(CryptoCurrency type) {
var pattern = "";
if (type is Erc20Token) {
return '0x[0-9a-zA-Z]';
pattern = '0x[0-9a-zA-Z]';
}
switch (type) {
case CryptoCurrency.xmr:
return '^4[0-9a-zA-Z]{94}\$|^8[0-9a-zA-Z]{94}\$|^[0-9a-zA-Z]{106}\$';
pattern = '4[0-9a-zA-Z]{94}|8[0-9a-zA-Z]{94}|[0-9a-zA-Z]{106}';
case CryptoCurrency.ada:
return '^[0-9a-zA-Z]{59}\$|^[0-9a-zA-Z]{92}\$|^[0-9a-zA-Z]{104}\$'
'|^[0-9a-zA-Z]{105}\$|^addr1[0-9a-zA-Z]{98}\$';
pattern = '[0-9a-zA-Z]{59}|[0-9a-zA-Z]{92}|[0-9a-zA-Z]{104}'
'|[0-9a-zA-Z]{105}|addr1[0-9a-zA-Z]{98}';
case CryptoCurrency.btc:
return '^${P2pkhAddress.regex.pattern}\$|^${P2shAddress.regex.pattern}\$|^${P2wpkhAddress.regex.pattern}\$|${P2trAddress.regex.pattern}\$|^${P2wshAddress.regex.pattern}\$|^${SilentPaymentAddress.regex.pattern}\$';
pattern =
'${P2pkhAddress.regex.pattern}|${P2shAddress.regex.pattern}|${P2wpkhAddress.regex.pattern}|${P2trAddress.regex.pattern}|${P2wshAddress.regex.pattern}|${SilentPaymentAddress.regex.pattern}';
case CryptoCurrency.nano:
return '[0-9a-zA-Z_]';
pattern = '[0-9a-zA-Z_]';
case CryptoCurrency.banano:
return '[0-9a-zA-Z_]';
pattern = '[0-9a-zA-Z_]';
case CryptoCurrency.usdc:
case CryptoCurrency.usdcpoly:
case CryptoCurrency.usdtPoly:
@@ -66,11 +74,11 @@ class AddressValidator extends TextValidator {
case CryptoCurrency.dydx:
case CryptoCurrency.steth:
case CryptoCurrency.shib:
return '0x[0-9a-zA-Z]';
pattern = '0x[0-9a-zA-Z]';
case CryptoCurrency.xrp:
return '^[0-9a-zA-Z]{34}\$|^X[0-9a-zA-Z]{46}\$';
pattern = '[0-9a-zA-Z]{34}|X[0-9a-zA-Z]{46}';
case CryptoCurrency.xhv:
return '^hvx|hvi|hvs[0-9a-zA-Z]';
pattern = 'hvx|hvi|hvs[0-9a-zA-Z]';
case CryptoCurrency.xag:
case CryptoCurrency.xau:
case CryptoCurrency.xaud:
@@ -92,40 +100,43 @@ class AddressValidator extends TextValidator {
case CryptoCurrency.dash:
case CryptoCurrency.eos:
case CryptoCurrency.wow:
return '[0-9a-zA-Z]';
pattern = '[0-9a-zA-Z]';
case CryptoCurrency.bch:
return '^(?!bitcoincash:)[0-9a-zA-Z]*\$|^(?!bitcoincash:)q|p[0-9a-zA-Z]{41}\$|^(?!bitcoincash:)q|p[0-9a-zA-Z]{42}\$|^bitcoincash:q|p[0-9a-zA-Z]{41}\$|^bitcoincash:q|p[0-9a-zA-Z]{42}\$';
pattern =
'(?!bitcoincash:)[0-9a-zA-Z]*|(?!bitcoincash:)q|p[0-9a-zA-Z]{41}|(?!bitcoincash:)q|p[0-9a-zA-Z]{42}|bitcoincash:q|p[0-9a-zA-Z]{41}|bitcoincash:q|p[0-9a-zA-Z]{42}';
case CryptoCurrency.bnb:
return '[0-9a-zA-Z]';
pattern = '[0-9a-zA-Z]';
case CryptoCurrency.ltc:
return '^(?!(ltc|LTC)1)[0-9a-zA-Z]*\$|(^LTC1[A-Z0-9]*\$)|(^ltc1[a-z0-9]*\$)';
pattern = '(?!(ltc|LTC)1)[0-9a-zA-Z]*|(LTC1[A-Z0-9]*)|(ltc1[a-z0-9]*)';
case CryptoCurrency.hbar:
return '[0-9a-zA-Z.]';
pattern = '[0-9a-zA-Z.]';
case CryptoCurrency.zaddr:
return '^zs[0-9a-zA-Z]{75}';
pattern = 'zs[0-9a-zA-Z]{75}';
case CryptoCurrency.zec:
return '^t1[0-9a-zA-Z]{33}\$|^t3[0-9a-zA-Z]{33}\$';
pattern = 't1[0-9a-zA-Z]{33}|t3[0-9a-zA-Z]{33}';
case CryptoCurrency.dcr:
return 'D[ksecS]([0-9a-zA-Z])+';
pattern = 'D[ksecS]([0-9a-zA-Z])+';
case CryptoCurrency.rvn:
return '[Rr]([1-9a-km-zA-HJ-NP-Z]){33}';
pattern = '[Rr]([1-9a-km-zA-HJ-NP-Z]){33}';
case CryptoCurrency.near:
return '[0-9a-f]{64}';
pattern = '[0-9a-f]{64}';
case CryptoCurrency.rune:
return 'thor1[0-9a-z]{38}';
pattern = 'thor1[0-9a-z]{38}';
case CryptoCurrency.scrt:
return 'secret1[0-9a-z]{38}';
pattern = 'secret1[0-9a-z]{38}';
case CryptoCurrency.stx:
return 'S[MP][0-9a-zA-Z]+';
pattern = 'S[MP][0-9a-zA-Z]+';
case CryptoCurrency.kmd:
return 'R[0-9a-zA-Z]{33}';
pattern = 'R[0-9a-zA-Z]{33}';
case CryptoCurrency.pivx:
return 'D([1-9a-km-zA-HJ-NP-Z]){33}';
pattern = 'D([1-9a-km-zA-HJ-NP-Z]){33}';
case CryptoCurrency.btcln:
return '^(lnbc|LNBC)([0-9]{1,}[a-zA-Z0-9]+)';
pattern = '(lnbc|LNBC)([0-9]{1,}[a-zA-Z0-9]+)';
default:
return '[0-9a-zA-Z]';
pattern = '[0-9a-zA-Z]';
}
return '$BEFORE_REGEX$pattern$AFTER_REGEX';
}
static List<int>? getLength(CryptoCurrency type) {
@@ -266,56 +277,54 @@ class AddressValidator extends TextValidator {
}
static String? getAddressFromStringPattern(CryptoCurrency type) {
String? pattern = null;
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]|\$)';
pattern = '4[0-9a-zA-Z]{94}'
'|8[0-9a-zA-Z]{94}'
'|[0-9a-zA-Z]{106}';
case CryptoCurrency.btc:
return '([^0-9a-zA-Z]|^)([1mn][a-km-zA-HJ-NP-Z1-9]{25,34})([^0-9a-zA-Z]|\$)' //P2pkhAddress type
'|([^0-9a-zA-Z]|^)([23][a-km-zA-HJ-NP-Z1-9]{25,34})([^0-9a-zA-Z]|\$)' //P2shAddress type
'|([^0-9a-zA-Z]|^)((bc|tb)1q[ac-hj-np-z02-9]{25,39})([^0-9a-zA-Z]|\$)' //P2wpkhAddress type
'|([^0-9a-zA-Z]|^)((bc|tb)1q[ac-hj-np-z02-9]{40,80})([^0-9a-zA-Z]|\$)' //P2wshAddress type
'|([^0-9a-zA-Z]|^)((bc|tb)1p([ac-hj-np-z02-9]{39}|[ac-hj-np-z02-9]{59}|[ac-hj-np-z02-9]{8,89}))([^0-9a-zA-Z]|\$)' //P2trAddress type
'|${SilentPaymentAddress.regex.pattern}\$';
pattern =
'${P2pkhAddress.regex.pattern}|${P2shAddress.regex.pattern}|${P2wpkhAddress.regex.pattern}|${P2trAddress.regex.pattern}|${P2wshAddress.regex.pattern}|${SilentPaymentAddress.regex.pattern}';
case CryptoCurrency.ltc:
return '([^0-9a-zA-Z]|^)^L[a-zA-Z0-9]{26,33}([^0-9a-zA-Z]|\$)'
'|([^0-9a-zA-Z]|^)[LM][a-km-zA-HJ-NP-Z1-9]{26,33}([^0-9a-zA-Z]|\$)'
'|([^0-9a-zA-Z]|^)ltc[a-zA-Z0-9]{26,45}([^0-9a-zA-Z]|\$)';
pattern = '^L[a-zA-Z0-9]{26,33}'
'|[LM][a-km-zA-HJ-NP-Z1-9]{26,33}'
'|ltc[a-zA-Z0-9]{26,45}';
case CryptoCurrency.eth:
return '0x[0-9a-zA-Z]{42}';
pattern = '0x[0-9a-zA-Z]{42}';
case CryptoCurrency.maticpoly:
return '0x[0-9a-zA-Z]{42}';
pattern = '0x[0-9a-zA-Z]{42}';
case CryptoCurrency.nano:
return 'nano_[0-9a-zA-Z]{60}';
pattern = 'nano_[0-9a-zA-Z]{60}';
case CryptoCurrency.banano:
return 'ban_[0-9a-zA-Z]{60}';
pattern = 'ban_[0-9a-zA-Z]{60}';
case CryptoCurrency.bch:
return 'bitcoincash:q[0-9a-zA-Z]{41}([^0-9a-zA-Z]|\$)'
'|bitcoincash:q[0-9a-zA-Z]{42}([^0-9a-zA-Z]|\$)'
'|([^0-9a-zA-Z]|^)q[0-9a-zA-Z]{41}([^0-9a-zA-Z]|\$)'
'|([^0-9a-zA-Z]|^)q[0-9a-zA-Z]{42}([^0-9a-zA-Z]|\$)';
pattern = '(bitcoincash:)?q[0-9a-zA-Z]{41,42}';
case CryptoCurrency.sol:
return '([^0-9a-zA-Z]|^)[1-9A-HJ-NP-Za-km-z]{43,44}([^0-9a-zA-Z]|\$)';
pattern = '[1-9A-HJ-NP-Za-km-z]{43,44}';
case CryptoCurrency.trx:
return '(T|t)[1-9A-HJ-NP-Za-km-z]{33}';
pattern = '(T|t)[1-9A-HJ-NP-Za-km-z]{33}';
default:
if (type.tag == CryptoCurrency.eth.title) {
return '0x[0-9a-zA-Z]{42}';
pattern = '0x[0-9a-zA-Z]{42}';
}
if (type.tag == CryptoCurrency.maticpoly.tag) {
return '0x[0-9a-zA-Z]{42}';
pattern = '0x[0-9a-zA-Z]{42}';
}
if (type.tag == CryptoCurrency.sol.title) {
return '([^0-9a-zA-Z]|^)[1-9A-HJ-NP-Za-km-z]{43,44}([^0-9a-zA-Z]|\$)';
pattern = '[1-9A-HJ-NP-Za-km-z]{43,44}';
}
if (type.tag == CryptoCurrency.trx.title) {
return '(T|t)[1-9A-HJ-NP-Za-km-z]{33}';
pattern = '(T|t)[1-9A-HJ-NP-Za-km-z]{33}';
}
return null;
}
if (pattern != null) {
return "$BEFORE_REGEX$pattern$AFTER_REGEX";
}
return null;
}
}
+5 -1
View File
@@ -53,7 +53,11 @@ String syncStatusTitle(SyncStatus syncStatus) {
}
if (syncStatus is StartingScanSyncStatus) {
return S.current.sync_status_starting_scan;
return S.current.sync_status_starting_scan(syncStatus.beginHeight.toString());
}
if (syncStatus is AttemptingScanSyncStatus) {
return S.current.sync_status_attempting_scan;
}
return '';
+2 -3
View File
@@ -51,7 +51,7 @@ class AddressResolver {
throw Exception('Unexpected token: $type for getAddressFromStringPattern');
}
final match = RegExp(addressPattern).firstMatch(raw);
final match = RegExp(addressPattern, multiLine: true).firstMatch(raw);
return match?.group(0)?.replaceAllMapped(RegExp('[^0-9a-zA-Z]|bitcoincash:|nano_|ban_'),
(Match match) {
String group = match.group(0)!;
@@ -213,8 +213,7 @@ class AddressResolver {
await NostrProfileHandler.processRelays(context, nostrProfile!, text);
if (nostrUserData != null) {
String? addressFromBio = extractAddressByType(
raw: nostrUserData.about, type: currency);
String? addressFromBio = extractAddressByType(raw: nostrUserData.about, type: currency);
if (addressFromBio != null) {
return ParsedAddress.nostrAddress(
address: addressFromBio,
@@ -1,3 +1,4 @@
import 'package:cake_wallet/bitcoin/bitcoin.dart';
import 'package:cake_wallet/src/screens/dashboard/widgets/anonpay_transaction_row.dart';
import 'package:cake_wallet/src/screens/dashboard/widgets/order_row.dart';
import 'package:cake_wallet/themes/extensions/placeholder_theme.dart';
@@ -52,7 +53,7 @@ class TransactionsPage extends StatelessWidget {
try {
final uri = Uri.parse(
"https://guides.cakewallet.com/docs/FAQ/why_are_my_funds_not_appearing/");
launchUrl(uri, mode: LaunchMode.externalApplication);
launchUrl(uri, mode: LaunchMode.externalApplication);
} catch (_) {}
},
title: S.of(context).syncing_wallet_alert_title,
@@ -84,7 +85,7 @@ class TransactionsPage extends StatelessWidget {
final transaction = item.transaction;
final transactionType = dashboardViewModel.type == WalletType.ethereum &&
transaction.evmSignatureName == 'approval'
transaction.evmSignatureName == 'approval'
? ' (${transaction.evmSignatureName})'
: '';
@@ -100,8 +101,11 @@ class TransactionsPage extends StatelessWidget {
? ''
: item.formattedFiatAmount,
isPending: transaction.isPending,
title: item.formattedTitle +
item.formattedStatus + ' $transactionType',
title:
item.formattedTitle + item.formattedStatus + ' $transactionType',
isReceivedSilentPayment:
dashboardViewModel.type == WalletType.bitcoin &&
bitcoin!.txIsReceivedSilentPayment(transaction),
),
);
}
@@ -1,3 +1,4 @@
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/themes/extensions/cake_text_theme.dart';
import 'package:flutter/material.dart';
import 'package:cw_core/transaction_direction.dart';
@@ -5,14 +6,16 @@ import 'package:cake_wallet/themes/extensions/dashboard_page_theme.dart';
import 'package:cake_wallet/themes/extensions/transaction_trade_theme.dart';
class TransactionRow extends StatelessWidget {
TransactionRow(
{required this.direction,
required this.formattedDate,
required this.formattedAmount,
required this.formattedFiatAmount,
required this.isPending,
required this.title,
required this.onTap});
TransactionRow({
required this.direction,
required this.formattedDate,
required this.formattedAmount,
required this.formattedFiatAmount,
required this.isPending,
required this.isReceivedSilentPayment,
required this.title,
required this.onTap,
});
final VoidCallback onTap;
final TransactionDirection direction;
@@ -20,6 +23,7 @@ class TransactionRow extends StatelessWidget {
final String formattedAmount;
final String formattedFiatAmount;
final bool isPending;
final bool isReceivedSilentPayment;
final String title;
@override
@@ -38,50 +42,80 @@ class TransactionRow extends StatelessWidget {
width: 36,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: Theme.of(context).extension<TransactionTradeTheme>()!.rowsColor
),
child: Image.asset(
direction == TransactionDirection.incoming
? 'assets/images/down_arrow.png'
: 'assets/images/up_arrow.png'),
color: Theme.of(context).extension<TransactionTradeTheme>()!.rowsColor),
child: Image.asset(direction == TransactionDirection.incoming
? 'assets/images/down_arrow.png'
: 'assets/images/up_arrow.png'),
),
SizedBox(width: 12),
Expanded(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
Text(title,
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
color: Theme.of(context).extension<DashboardPageTheme>()!.textColor)),
Text(formattedAmount,
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
color: Theme.of(context).extension<DashboardPageTheme>()!.textColor))
]),
SizedBox(height: 5),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
Text(formattedDate,
style: TextStyle(
fontSize: 14,
color: Theme.of(context).extension<CakeTextTheme>()!.dateSectionRowColor)),
Text(formattedFiatAmount,
style: TextStyle(
fontSize: 14,
color: Theme.of(context).extension<CakeTextTheme>()!.dateSectionRowColor))
])
],
)
)
mainAxisSize: MainAxisSize.min,
children: [
Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: <Widget>[
Row(
children: [
Text(title,
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
color: Theme.of(context).extension<DashboardPageTheme>()!.textColor,
)),
if (isReceivedSilentPayment) TxTag(tag: S.of(context).silent_payment),
],
),
Text(formattedAmount,
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
color: Theme.of(context).extension<DashboardPageTheme>()!.textColor))
]),
SizedBox(height: 5),
Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: <Widget>[
Text(formattedDate,
style: TextStyle(
fontSize: 14,
color:
Theme.of(context).extension<CakeTextTheme>()!.dateSectionRowColor)),
Text(formattedFiatAmount,
style: TextStyle(
fontSize: 14,
color:
Theme.of(context).extension<CakeTextTheme>()!.dateSectionRowColor))
])
],
))
],
),
));
}
}
// A tag to add context to a transaction
// example use: differ silent payments from regular txs
class TxTag extends StatelessWidget {
TxTag({required this.tag});
final String tag;
@override
Widget build(BuildContext context) {
return Container(
height: 17,
padding: EdgeInsets.only(left: 6, right: 6),
decoration: BoxDecoration(
borderRadius: BorderRadius.all(Radius.circular(8.5)),
color: Theme.of(context).extension<TransactionTradeTheme>()!.rowsColor,
),
alignment: Alignment.center,
child: Text(
tag.toLowerCase(),
style: TextStyle(
color: Theme.of(context).extension<DashboardPageTheme>()!.textColor,
fontSize: 9,
fontWeight: FontWeight.w600,
),
),
);
}
}
+1
View File
@@ -37,6 +37,7 @@ class RescanPage extends BasePage {
toggleSingleScan: () =>
_rescanViewModel.doSingleScan = !_rescanViewModel.doSingleScan,
walletType: _rescanViewModel.wallet.type,
bitcoinMempoolAPIEnabled: _rescanViewModel.isBitcoinMempoolAPIEnabled,
)),
Observer(
builder: (_) => LoadingPrimaryButton(
+25 -25
View File
@@ -68,11 +68,11 @@ class SendPage extends BasePage {
@override
Function(BuildContext)? get pushToNextWidget => (context) {
FocusScopeNode currentFocus = FocusScope.of(context);
if (!currentFocus.hasPrimaryFocus) {
currentFocus.focusedChild?.unfocus();
}
};
FocusScopeNode currentFocus = FocusScope.of(context);
if (!currentFocus.hasPrimaryFocus) {
currentFocus.focusedChild?.unfocus();
}
};
@override
Widget? leading(BuildContext context) {
@@ -212,26 +212,25 @@ class SendPage extends BasePage {
final count = sendViewModel.outputs.length;
return count > 1
? Semantics (
label: 'Page Indicator',
hint: 'Swipe to change receiver',
excludeSemantics: true,
child:
SmoothPageIndicator(
controller: controller,
count: count,
effect: ScrollingDotsEffect(
spacing: 6.0,
radius: 6.0,
dotWidth: 6.0,
dotHeight: 6.0,
dotColor: Theme.of(context)
.extension<SendPageTheme>()!
.indicatorDotColor,
activeDotColor: Theme.of(context)
.extension<SendPageTheme>()!
.templateBackgroundColor),
))
? Semantics(
label: 'Page Indicator',
hint: 'Swipe to change receiver',
excludeSemantics: true,
child: SmoothPageIndicator(
controller: controller,
count: count,
effect: ScrollingDotsEffect(
spacing: 6.0,
radius: 6.0,
dotWidth: 6.0,
dotHeight: 6.0,
dotColor: Theme.of(context)
.extension<SendPageTheme>()!
.indicatorDotColor,
activeDotColor: Theme.of(context)
.extension<SendPageTheme>()!
.templateBackgroundColor),
))
: Offstage();
},
),
@@ -478,6 +477,7 @@ class SendPage extends BasePage {
feeValue: sendViewModel.pendingTransaction!.feeFormatted,
feeFiatAmount: sendViewModel.pendingTransactionFeeFiatAmountFormatted,
outputs: sendViewModel.outputs,
change: sendViewModel.pendingTransaction!.change,
rightButtonText: S.of(_dialogContext).send,
leftButtonText: S.of(_dialogContext).cancel,
actionRightButton: () async {
@@ -1,6 +1,7 @@
import 'package:cake_wallet/themes/extensions/cake_text_theme.dart';
import 'package:cake_wallet/palette.dart';
import 'package:cake_wallet/view_model/send/output.dart';
import 'package:cw_core/pending_transaction.dart';
import 'package:flutter/material.dart';
import 'package:cake_wallet/src/widgets/base_alert_dialog.dart';
import 'package:cake_wallet/generated/i18n.dart';
@@ -21,6 +22,7 @@ class ConfirmSendingAlert extends BaseAlertDialog {
required this.feeValue,
required this.feeFiatAmount,
required this.outputs,
this.change,
required this.leftButtonText,
required this.rightButtonText,
required this.actionLeftButton,
@@ -44,6 +46,7 @@ class ConfirmSendingAlert extends BaseAlertDialog {
final String feeValue;
final String feeFiatAmount;
final List<Output> outputs;
final PendingChange? change;
final String leftButtonText;
final String rightButtonText;
final VoidCallback actionLeftButton;
@@ -101,6 +104,7 @@ class ConfirmSendingAlert extends BaseAlertDialog {
feeValue: feeValue,
feeFiatAmount: feeFiatAmount,
outputs: outputs,
change: change,
onDispose: onDispose);
}
@@ -117,6 +121,7 @@ class ConfirmSendingAlertContent extends StatefulWidget {
required this.feeValue,
required this.feeFiatAmount,
required this.outputs,
this.change,
required this.onDispose}) {}
final String? paymentId;
@@ -130,6 +135,7 @@ class ConfirmSendingAlertContent extends StatefulWidget {
final String feeValue;
final String feeFiatAmount;
final List<Output> outputs;
final PendingChange? change;
final Function? onDispose;
@override
@@ -145,6 +151,7 @@ class ConfirmSendingAlertContent extends StatefulWidget {
feeValue: feeValue,
feeFiatAmount: feeFiatAmount,
outputs: outputs,
change: change,
onDispose: onDispose);
}
@@ -161,6 +168,7 @@ class ConfirmSendingAlertContentState extends State<ConfirmSendingAlertContent>
required this.feeValue,
required this.feeFiatAmount,
required this.outputs,
this.change,
this.onDispose})
: recipientTitle = '' {
recipientTitle = outputs.length > 1
@@ -179,6 +187,7 @@ class ConfirmSendingAlertContentState extends State<ConfirmSendingAlertContent>
final String feeValue;
final String feeFiatAmount;
final List<Output> outputs;
final PendingChange? change;
final Function? onDispose;
final double backgroundHeight = 160;
@@ -391,100 +400,57 @@ class ConfirmSendingAlertContentState extends State<ConfirmSendingAlertContent>
decoration: TextDecoration.none,
),
),
outputs.length > 1
? ListView.builder(
padding: EdgeInsets.only(top: 0),
shrinkWrap: true,
physics: NeverScrollableScrollPhysics(),
itemCount: outputs.length,
itemBuilder: (context, index) {
final item = outputs[index];
final _address =
item.isParsedAddress ? item.extractedAddress : item.address;
final _amount = item.cryptoAmount.replaceAll(',', '.');
ListView.builder(
padding: EdgeInsets.only(top: 0),
shrinkWrap: true,
physics: NeverScrollableScrollPhysics(),
itemCount: outputs.length,
itemBuilder: (context, index) {
final item = outputs[index];
final _address =
item.isParsedAddress ? item.extractedAddress : item.address;
final _amount = item.cryptoAmount.replaceAll(',', '.');
return Column(
children: [
if (item.isParsedAddress)
Padding(
padding: EdgeInsets.only(top: 8),
child: Text(
item.parsedAddress.name,
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
fontFamily: 'Lato',
color: PaletteDark.pigeonBlue,
decoration: TextDecoration.none,
),
)),
Padding(
padding: EdgeInsets.only(top: 8),
child: Text(
_address,
style: TextStyle(
fontSize: 10,
fontWeight: FontWeight.w600,
fontFamily: 'Lato',
color: PaletteDark.pigeonBlue,
decoration: TextDecoration.none,
),
)),
Padding(
padding: EdgeInsets.only(top: 8),
child: Row(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.end,
children: [
Text(
_amount,
style: TextStyle(
fontSize: 10,
fontWeight: FontWeight.w600,
fontFamily: 'Lato',
color: PaletteDark.pigeonBlue,
decoration: TextDecoration.none,
),
)
],
))
],
);
})
: Column(children: [
if (outputs.first.isParsedAddress)
Padding(
padding: EdgeInsets.only(top: 8),
child: Text(
outputs.first.parsedAddress.name,
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
fontFamily: 'Lato',
color: PaletteDark.pigeonBlue,
decoration: TextDecoration.none,
),
)),
Padding(
padding: EdgeInsets.only(top: 8),
child: Text(
outputs.first.isParsedAddress
? outputs.first.extractedAddress
: outputs.first.address,
style: TextStyle(
fontSize: 10,
fontWeight: FontWeight.w600,
fontFamily: 'Lato',
color: PaletteDark.pigeonBlue,
decoration: TextDecoration.none,
),
)),
])
return Column(
children: [
if (item.isParsedAddress)
AddressText(text: item.parsedAddress.name),
AddressText(text: _address, fontSize: 10),
if (stealthAddressText(item.stealthAddress) != null)
AddressText(
text: stealthAddressText(item.stealthAddress)!, fontSize: 10),
AmountText(text: _amount),
],
);
},
)
],
),
)
),
if (change != null)
Padding(
padding: EdgeInsets.only(top: 16),
child: Column(
children: [
Text(
S.of(context).send_change_to_you,
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.normal,
fontFamily: 'Lato',
color: Theme.of(context).extension<CakeTextTheme>()!.titleColor,
decoration: TextDecoration.none,
),
),
Column(
children: [
AddressText(text: change!.address, fontSize: 10),
AmountText(text: change!.amount),
],
)
],
),
)
],
))),
if (showScrollbar)
@@ -539,3 +505,78 @@ class ExpirationTimeWidget extends StatelessWidget {
);
}
}
class AddressText extends StatelessWidget {
final String text;
final double fontSize;
final FontWeight fontWeight;
final TextAlign? textAlign;
const AddressText({
required this.text,
this.fontSize = 14,
this.fontWeight = FontWeight.w600,
this.textAlign,
});
@override
Widget build(BuildContext context) {
return Padding(
padding: EdgeInsets.only(top: 8),
child: Text(
text,
style: TextStyle(
fontSize: fontSize,
fontWeight: fontWeight,
fontFamily: 'Lato',
color: PaletteDark.pigeonBlue,
decoration: TextDecoration.none,
),
),
);
}
}
class AmountText extends StatelessWidget {
final String text;
final double fontSize;
final FontWeight fontWeight;
final TextAlign? textAlign;
const AmountText({
required this.text,
this.fontSize = 10,
this.fontWeight = FontWeight.w600,
this.textAlign,
});
@override
Widget build(BuildContext context) {
return Padding(
padding: EdgeInsets.only(top: 8),
child: Row(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.end,
children: [
Text(
text,
style: TextStyle(
fontSize: fontSize,
fontWeight: fontWeight,
fontFamily: 'Lato',
color: PaletteDark.pigeonBlue,
decoration: TextDecoration.none,
),
)
],
));
}
}
String? stealthAddressText(String? stealthAddress) {
if (stealthAddress == null) {
return null;
}
return stealthAddress.isNotEmpty ? "-> $stealthAddress" : null;
}
+13 -13
View File
@@ -58,8 +58,8 @@ class PrivacyPage extends BasePage {
if (_privacySettingsViewModel.isAutoGenerateSubaddressesVisible)
SettingsSwitcherCell(
title: _privacySettingsViewModel.isMoneroWallet
? S.current.auto_generate_subaddresses
: S.current.auto_generate_addresses,
? S.current.auto_generate_subaddresses
: S.current.auto_generate_addresses,
value: _privacySettingsViewModel.isAutoGenerateSubaddressesEnabled,
onValueChange: (BuildContext _, bool value) {
_privacySettingsViewModel.setAutoGenerateSubaddresses(value);
@@ -115,21 +115,21 @@ class PrivacyPage extends BasePage {
),
if (_privacySettingsViewModel.canUseMempoolFeeAPI)
SettingsSwitcherCell(
title: S.current.live_fee_rates,
title: S.current.enable_mempool_api,
value: _privacySettingsViewModel.useMempoolFeeAPI,
onValueChange: (BuildContext _, bool isEnabled) async {
if (!isEnabled) {
final bool confirmation = await showPopUp<bool>(
context: context,
builder: (BuildContext context) {
return AlertWithTwoActions(
alertTitle: S.of(context).warning,
alertContent: S.of(context).disable_fee_api_warning,
rightButtonText: S.of(context).confirm,
leftButtonText: S.of(context).cancel,
actionRightButton: () => Navigator.of(context).pop(true),
actionLeftButton: () => Navigator.of(context).pop(false));
}) ??
context: context,
builder: (BuildContext context) {
return AlertWithTwoActions(
alertTitle: S.of(context).warning,
alertContent: S.of(context).disable_fee_api_warning,
rightButtonText: S.of(context).confirm,
leftButtonText: S.of(context).cancel,
actionRightButton: () => Navigator.of(context).pop(true),
actionLeftButton: () => Navigator.of(context).pop(false));
}) ??
false;
if (confirmation) {
_privacySettingsViewModel.setUseMempoolFeeAPI(isEnabled);
+11 -3
View File
@@ -20,6 +20,7 @@ class BlockchainHeightWidget extends StatefulWidget {
this.isSilentPaymentsScan = false,
this.toggleSingleScan,
this.doSingleScan = false,
this.bitcoinMempoolAPIEnabled,
required this.walletType,
}) : super(key: key);
@@ -29,6 +30,7 @@ class BlockchainHeightWidget extends StatefulWidget {
final bool hasDatePicker;
final bool isSilentPaymentsScan;
final bool doSingleScan;
final Future<bool>? bitcoinMempoolAPIEnabled;
final Function()? toggleSingleScan;
final WalletType walletType;
@@ -79,7 +81,8 @@ class BlockchainHeightState extends State<BlockchainHeightWidget> {
child: BaseTextFormField(
focusNode: widget.focusNode,
controller: restoreHeightController,
keyboardType: TextInputType.numberWithOptions(signed: false, decimal: false),
keyboardType:
TextInputType.numberWithOptions(signed: false, decimal: false),
hintText: widget.isSilentPaymentsScan
? S.of(context).silent_payments_scan_from_height
: S.of(context).widgets_restore_from_blockheight,
@@ -146,7 +149,9 @@ class BlockchainHeightState extends State<BlockchainHeightWidget> {
: S.of(context).restore_from_date_or_blockheight,
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 12, fontWeight: FontWeight.normal, color: Theme.of(context).hintColor),
fontSize: 12,
fontWeight: FontWeight.normal,
color: Theme.of(context).hintColor),
),
)
]
@@ -166,7 +171,10 @@ class BlockchainHeightState extends State<BlockchainHeightWidget> {
if (date != null) {
int height;
if (widget.isSilentPaymentsScan) {
height = bitcoin!.getHeightByDate(date: date);
height = await bitcoin!.getHeightByDate(
date: date,
bitcoinMempoolAPIEnabled: await widget.bitcoinMempoolAPIEnabled,
);
} else {
if (widget.walletType == WalletType.monero) {
height = monero!.getHeightByDate(date: date);
@@ -1,3 +1,4 @@
import 'package:cake_wallet/bitcoin/bitcoin.dart';
import 'package:cake_wallet/view_model/dashboard/action_list_item.dart';
import 'package:cake_wallet/view_model/dashboard/anonpay_transaction_list_item.dart';
import 'package:mobx/mobx.dart';
@@ -6,12 +7,13 @@ import 'package:cake_wallet/view_model/dashboard/transaction_list_item.dart';
part 'transaction_filter_store.g.dart';
class TransactionFilterStore = TransactionFilterStoreBase
with _$TransactionFilterStore;
class TransactionFilterStore = TransactionFilterStoreBase with _$TransactionFilterStore;
abstract class TransactionFilterStoreBase with Store {
TransactionFilterStoreBase() : displayIncoming = true,
displayOutgoing = true;
TransactionFilterStoreBase()
: displayIncoming = true,
displayOutgoing = true,
displaySilentPayments = true;
@observable
bool displayIncoming;
@@ -19,6 +21,9 @@ abstract class TransactionFilterStoreBase with Store {
@observable
bool displayOutgoing;
@observable
bool displaySilentPayments;
@observable
DateTime? startDate;
@@ -26,31 +31,36 @@ abstract class TransactionFilterStoreBase with Store {
DateTime? endDate;
@computed
bool get displayAll => displayIncoming && displayOutgoing;
bool get displayAll => displayIncoming && displayOutgoing && displaySilentPayments;
@action
void toggleAll() {
if (displayAll) {
displayOutgoing = false;
displayIncoming = false;
displaySilentPayments = false;
} else {
displayOutgoing = true;
displayIncoming = true;
displaySilentPayments = true;
}
}
@action
void toggleIncoming() {
displayIncoming = !displayIncoming;
}
@action
void toggleOutgoing() {
displayOutgoing = !displayOutgoing;
}
@action
void toggleSilentPayments() {
displaySilentPayments = !displaySilentPayments;
}
@action
void changeStartDate(DateTime date) => startDate = date;
@@ -59,34 +69,33 @@ abstract class TransactionFilterStoreBase with Store {
List<ActionListItem> filtered({required List<ActionListItem> transactions}) {
var _transactions = <ActionListItem>[];
final needToFilter = !displayAll ||
(startDate != null && endDate != null);
final needToFilter = !displayAll || (startDate != null && endDate != null);
if (needToFilter) {
_transactions = transactions.where((item) {
var allowed = true;
if (allowed && startDate != null && endDate != null) {
if(item is TransactionListItem){
allowed = (startDate?.isBefore(item.transaction.date) ?? false)
&& (endDate?.isAfter(item.transaction.date) ?? false);
}else if(item is AnonpayTransactionListItem){
allowed = (startDate?.isBefore(item.transaction.createdAt) ?? false)
&& (endDate?.isAfter(item.transaction.createdAt) ?? false);
}
if (item is TransactionListItem) {
allowed = (startDate?.isBefore(item.transaction.date) ?? false) &&
(endDate?.isAfter(item.transaction.date) ?? false);
} else if (item is AnonpayTransactionListItem) {
allowed = (startDate?.isBefore(item.transaction.createdAt) ?? false) &&
(endDate?.isAfter(item.transaction.createdAt) ?? false);
}
}
if (allowed && (!displayAll)) {
if(item is TransactionListItem){
allowed = (displayOutgoing &&
item.transaction.direction ==
TransactionDirection.outgoing) ||
(displayIncoming &&
item.transaction.direction == TransactionDirection.incoming);
} else if(item is AnonpayTransactionListItem){
if (item is TransactionListItem) {
allowed =
(displayOutgoing && item.transaction.direction == TransactionDirection.outgoing) ||
(displayIncoming &&
item.transaction.direction == TransactionDirection.incoming &&
!bitcoin!.txIsReceivedSilentPayment(item.transaction)) ||
(displaySilentPayments && bitcoin!.txIsReceivedSilentPayment(item.transaction));
} else if (item is AnonpayTransactionListItem) {
allowed = displayIncoming;
}
}
return allowed;
@@ -97,4 +106,4 @@ abstract class TransactionFilterStoreBase with Store {
return _transactions;
}
}
}
@@ -88,6 +88,11 @@ abstract class DashboardViewModelBase with Store {
value: () => transactionFilterStore.displayOutgoing,
caption: S.current.outgoing,
onChanged: transactionFilterStore.toggleOutgoing),
FilterItem(
value: () => transactionFilterStore.displaySilentPayments,
caption: S.current.silent_payments,
onChanged: transactionFilterStore.toggleSilentPayments,
),
// FilterItem(
// value: () => false,
// caption: S.current.transactions_by_date,
@@ -376,12 +381,15 @@ abstract class DashboardViewModelBase with Store {
// to not cause work duplication, this will do the job as well, it will be slightly less precise
// about what happened - but still enough.
// if (keys['privateSpendKey'] == List.generate(64, (index) => "0").join("")) "Private spend key is 0",
if (keys['privateViewKey'] == List.generate(64, (index) => "0").join("")) "private view key is 0",
if (keys['privateViewKey'] == List.generate(64, (index) => "0").join(""))
"private view key is 0",
// if (keys['publicSpendKey'] == List.generate(64, (index) => "0").join("")) "public spend key is 0",
if (keys['publicViewKey'] == List.generate(64, (index) => "0").join("")) "public view key is 0",
if (keys['publicViewKey'] == List.generate(64, (index) => "0").join(""))
"public view key is 0",
// if (wallet.seed == null) "wallet seed is null",
// if (wallet.seed == "") "wallet seed is empty",
if (monero!.getSubaddressList(wallet).getAll(wallet)[0].address == "41d7FXjswpK1111111111111111111111111111111111111111111111111111111111111111111111111111112KhNi4")
if (monero!.getSubaddressList(wallet).getAll(wallet)[0].address ==
"41d7FXjswpK1111111111111111111111111111111111111111111111111111111111111111111111111111112KhNi4")
"primary address is invalid, you won't be able to receive / spend funds",
];
return errors;
+4
View File
@@ -29,6 +29,10 @@ abstract class RescanViewModelBase with Store {
@computed
bool get isSilentPaymentsScan => wallet.type == WalletType.bitcoin;
@computed
Future<bool> get isBitcoinMempoolAPIEnabled async =>
wallet.type == WalletType.bitcoin && await bitcoin!.checkIfMempoolAPIIsEnabled(wallet);
@action
Future<void> rescanCurrentWallet({required int restoreHeight}) async {
state = RescanWalletState.rescaning;
+11 -8
View File
@@ -79,6 +79,9 @@ abstract class OutputBase with Store {
bool get isParsedAddress =>
parsedAddress.parseFrom != ParseFrom.notParsed && parsedAddress.name.isNotEmpty;
@observable
String? stealthAddress;
@computed
int get formattedCryptoAmount {
int amount = 0;
@@ -134,9 +137,8 @@ abstract class OutputBase with Store {
final trc20EstimatedFee = tron!.getTronTRC20EstimatedFee(_wallet) ?? 0;
return double.parse(trc20EstimatedFee.toString());
}
}
if (_wallet.type == WalletType.solana) {
return solana!.getEstimateFees(_wallet) ?? 0.0;
}
@@ -145,16 +147,16 @@ abstract class OutputBase with Store {
_settingsStore.priority[_wallet.type]!, formattedCryptoAmount);
if (_wallet.type == WalletType.bitcoin) {
if (_settingsStore.priority[_wallet.type] == bitcoin!.getBitcoinTransactionPriorityCustom()) {
fee = bitcoin!.getEstimatedFeeWithFeeRate(_wallet,
_settingsStore.customBitcoinFeeRate,formattedCryptoAmount);
if (_settingsStore.priority[_wallet.type] ==
bitcoin!.getBitcoinTransactionPriorityCustom()) {
fee = bitcoin!.getEstimatedFeeWithFeeRate(
_wallet, _settingsStore.customBitcoinFeeRate, formattedCryptoAmount);
}
return bitcoin!.formatterBitcoinAmountToDouble(amount: fee);
}
if (_wallet.type == WalletType.litecoin ||
_wallet.type == WalletType.bitcoinCash) {
if (_wallet.type == WalletType.litecoin || _wallet.type == WalletType.bitcoinCash) {
return bitcoin!.formatterBitcoinAmountToDouble(amount: fee);
}
@@ -249,7 +251,8 @@ abstract class OutputBase with Store {
try {
final fiat = calculateFiatAmount(
price: _fiatConversationStore.prices[cryptoCurrencyHandler()]!,
cryptoAmount: sendAll ? cryptoFullBalance.replaceAll(",", ".") : cryptoAmount.replaceAll(',', '.'));
cryptoAmount:
sendAll ? cryptoFullBalance.replaceAll(",", ".") : cryptoAmount.replaceAll(',', '.'));
if (fiatAmount != fiat) {
fiatAmount = fiat;
}
+9 -2
View File
@@ -375,6 +375,15 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
throw Exception("THORChain does not support Taproot addresses");
}
}
if (wallet.type == WalletType.bitcoin) {
final updatedOutputs = bitcoin!.updateOutputs(pendingTransaction!, outputs);
if (outputs.length == updatedOutputs.length) {
outputs = ObservableList.of(updatedOutputs);
}
}
state = ExecutedSuccessfullyState();
return pendingTransaction;
} catch (e) {
@@ -414,8 +423,6 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
}
Future<void> _executeReplaceByFee(TransactionInfo tx, String newFee) async {
clearOutputs();
final output = outputs.first;
output.address = tx.outputAddresses?.first ?? '';