implement-payjoin (#1949)

* Initial Payjoin

* Initial Payjoin

* More payjoin stuff

* Minor fixes

* Minor fixes

* Minor cleanup

* Minor cleanup

* Minor cleanup

* Minor cleanup

* Minor cleanup

* Minor cleanup

* Fix minor bug causes by data inconsistency in the btc utxos

* Minor cleanup

* Minor cleanup

* Minor cleanup

* Minor cleanup

* Initial Payjoin

* Initial Payjoin

* More payjoin stuff

* Minor fixes

* Minor fixes

* Minor cleanup

* Minor cleanup

* Minor cleanup

* Minor cleanup

* Minor cleanup

* Minor cleanup

* Fix minor bug causes by data inconsistency in the btc utxos

* Minor cleanup

* Minor cleanup

* Minor cleanup

* Minor cleanup

* Fix Rebase issues

* Move PJ Receiver to isolate

* Add Payjoin Setting

* Payjoin Sender are now isolated

* Added Payjoin sessions to tx overview. Fix Fee issue with payjoin

* Clean up code

* Fix taproot for payjoin

* Fix CI Errors

* Add Payjoin UI elements and details page

* Add Payjoin UI elements and details page

* Fix Translations

* feat: Detect Payjoin URIs in pasted text and show to the User sending Payjoin

* feat: rename pjUri to payjoinURI for more code clarity

* Update res/values/strings_pl.arb

Co-authored-by: cyan <cyjan@mrcyjanek.net>

* Update cw_bitcoin/lib/payjoin/manager.dart

Co-authored-by: cyan <cyjan@mrcyjanek.net>

* Update cw_bitcoin/lib/payjoin/manager.dart

Co-authored-by: cyan <cyjan@mrcyjanek.net>

* feat: Disable Payjoin per default

* feat: Disable Payjoin fully if disabled or no Inputs available

* feat: Resume Payjoin if app comes back to foreground

* chore: Revert overly aggressive code formats

* feat: show correct Payjoin amount for receivers

* feat: Improved payjoin status

* feat: Show payjoin errors on payjoin details screen

* deps: update flutter to 3.27.4

* feat: Revert localisations

* bug: Remove duplicate transaction id on payjoin details

* style: remove double await in payjoin sender

* refactor(cw_bitcoin): Refactor method signatures and convert constructor to factory

* refactor(cw_bitcoin): Refactor wallet service and PSBT signer for cleaner code

Removed unnecessary `CakeHive` dependency and refactored `BitcoinWallet` initialization to use `payjoinSessionSource`. Improved code readability in `PsbtSigner` by reformatting lines and simplifying constructor methods for `UtxoWithPrivateKey`.

* fix: Resume Payjoin Sessions and load PJUri after sleep

* feat: Add "Copy Payjoin URL button" to receive screen

* fix: Add "Payjoin enabled"-Box below QR Code on the receive screen

* fix: Set payjoin_enabled color to black independent of the theme

* refactor: Payjoin session management and cleanup unused code.

---------

Co-authored-by: Omar Hatem <omarh.ismail1@gmail.com>
Co-authored-by: cyan <cyjan@mrcyjanek.net>
This commit is contained in:
Konstantin Ullrich
2025-05-12 20:33:14 +03:00
committed by GitHub
co-authored by cyan Omar Hatem
parent 4a08e18f00
commit 82e3ebf4fa
84 changed files with 2622 additions and 198 deletions
+62 -20
View File
@@ -109,26 +109,29 @@ class CWBitcoin extends Bitcoin {
required TransactionPriority priority,
int? feeRate,
UnspentCoinType coinTypeToSpendFrom = UnspentCoinType.any,
String? payjoinUri,
}) {
final bitcoinFeeRate =
priority == BitcoinTransactionPriority.custom && feeRate != null ? feeRate : null;
priority == BitcoinTransactionPriority.custom && feeRate != null
? feeRate
: null;
return BitcoinTransactionCredentials(
outputs
.map((out) => OutputInfo(
fiatAmount: out.fiatAmount,
cryptoAmount: out.cryptoAmount,
address: out.address,
note: out.note,
sendAll: out.sendAll,
extractedAddress: out.extractedAddress,
isParsedAddress: out.isParsedAddress,
formattedCryptoAmount: out.formattedCryptoAmount,
memo: out.memo))
.toList(),
priority: priority as BitcoinTransactionPriority,
feeRate: bitcoinFeeRate,
coinTypeToSpendFrom: coinTypeToSpendFrom,
);
outputs
.map((out) => OutputInfo(
fiatAmount: out.fiatAmount,
cryptoAmount: out.cryptoAmount,
address: out.address,
note: out.note,
sendAll: out.sendAll,
extractedAddress: out.extractedAddress,
isParsedAddress: out.isParsedAddress,
formattedCryptoAmount: out.formattedCryptoAmount,
memo: out.memo))
.toList(),
priority: priority as BitcoinTransactionPriority,
feeRate: bitcoinFeeRate,
coinTypeToSpendFrom: coinTypeToSpendFrom,
payjoinUri: payjoinUri);
}
@override
@@ -224,9 +227,14 @@ class CWBitcoin extends Bitcoin {
await bitcoinWallet.updateAllUnspents();
}
WalletService createBitcoinWalletService(Box<WalletInfo> walletInfoSource,
Box<UnspentCoinsInfo> unspentCoinSource, bool alwaysScan, bool isDirect) {
return BitcoinWalletService(walletInfoSource, unspentCoinSource, alwaysScan, isDirect);
WalletService createBitcoinWalletService(
Box<WalletInfo> walletInfoSource,
Box<UnspentCoinsInfo> unspentCoinSource,
Box<PayjoinSession> payjoinSessionSource,
bool alwaysScan,
bool isDirect) {
return BitcoinWalletService(walletInfoSource, unspentCoinSource,
payjoinSessionSource, alwaysScan, isDirect);
}
WalletService createLitecoinWalletService(Box<WalletInfo> walletInfoSource,
@@ -550,6 +558,10 @@ class CWBitcoin extends Bitcoin {
return option is BitcoinReceivePageOption;
}
@override
bool isPayjoinAvailable(Object wallet) =>
(wallet is BitcoinWallet) && (wallet as BitcoinWallet).isPayjoinAvailable;
@override
BitcoinAddressType getOptionToType(ReceivePageOption option) {
return (option as BitcoinReceivePageOption).toType();
@@ -706,4 +718,34 @@ class CWBitcoin extends Bitcoin {
return null;
}
}
@override
String getPayjoinEndpoint(Object wallet) {
final _wallet = wallet as ElectrumWallet;
if (!isPayjoinAvailable(wallet)) return '';
return (_wallet.walletAddresses as BitcoinWalletAddresses).payjoinEndpoint ?? '';
}
@override
void updatePayjoinState(Object wallet, bool value) {
final _wallet = wallet as ElectrumWallet;
if (value) {
(_wallet.walletAddresses as BitcoinWalletAddresses).initPayjoin();
} else {
stopPayjoinSessions(wallet);
}
}
@override
void resumePayjoinSessions(Object wallet) {
final _wallet = wallet as ElectrumWallet;
(_wallet.walletAddresses as BitcoinWalletAddresses).initPayjoin();
}
@override
void stopPayjoinSessions(Object wallet) {
final _wallet = wallet as ElectrumWallet;
(_wallet.walletAddresses as BitcoinWalletAddresses).payjoinManager.cleanupSessions();
(_wallet.walletAddresses as BitcoinWalletAddresses).currentPayjoinReceiver = null;
}
}
+25
View File
@@ -51,6 +51,7 @@ import 'package:cake_wallet/entities/wallet_manager.dart';
import 'package:cake_wallet/src/screens/buy/buy_sell_options_page.dart';
import 'package:cake_wallet/src/screens/buy/payment_method_options_page.dart';
import 'package:cake_wallet/src/screens/exchange_trade/exchange_trade_external_send_page.dart';
import 'package:cake_wallet/src/screens/payjoin_details/payjoin_details_page.dart';
import 'package:cake_wallet/src/screens/receive/address_list_page.dart';
import 'package:cake_wallet/src/screens/seed/seed_verification/seed_verification_page.dart';
import 'package:cake_wallet/src/screens/send/transaction_success_info_page.dart';
@@ -58,7 +59,10 @@ import 'package:cake_wallet/src/screens/wallet_list/wallet_list_page.dart';
import 'package:cake_wallet/src/screens/settings/mweb_logs_page.dart';
import 'package:cake_wallet/src/screens/settings/mweb_node_page.dart';
import 'package:cake_wallet/src/screens/welcome/welcome_page.dart';
import 'package:cake_wallet/store/dashboard/payjoin_transactions_store.dart';
import 'package:cake_wallet/view_model/dashboard/sign_view_model.dart';
import 'package:cake_wallet/view_model/payjoin_details_view_model.dart';
import 'package:cw_core/payjoin_session.dart';
import 'package:cake_wallet/view_model/restore/restore_wallet.dart';
import 'package:cake_wallet/view_model/send/fees_view_model.dart';
import 'package:cake_wallet/entities/preferences_key.dart';
@@ -286,6 +290,7 @@ late Box<ExchangeTemplate> _exchangeTemplates;
late Box<TransactionDescription> _transactionDescriptionBox;
late Box<Order> _ordersSource;
late Box<UnspentCoinsInfo> _unspentCoinsInfoSource;
late Box<PayjoinSession> _payjoinSessionSource;
late Box<AnonpayInvoiceInfo> _anonpayInvoiceInfoSource;
Future<void> setup({
@@ -299,6 +304,7 @@ Future<void> setup({
required Box<TransactionDescription> transactionDescriptionBox,
required Box<Order> ordersSource,
required Box<UnspentCoinsInfo> unspentCoinsInfoSource,
required Box<PayjoinSession> payjoinSessionSource,
required Box<AnonpayInvoiceInfo> anonpayInvoiceInfoSource,
required SecureStorage secureStorage,
required GlobalKey<NavigatorState> navigatorKey,
@@ -313,6 +319,7 @@ Future<void> setup({
_transactionDescriptionBox = transactionDescriptionBox;
_ordersSource = ordersSource;
_unspentCoinsInfoSource = unspentCoinsInfoSource;
_payjoinSessionSource = payjoinSessionSource;
_anonpayInvoiceInfoSource = anonpayInvoiceInfoSource;
if (!_isSetupFinished) {
@@ -354,6 +361,8 @@ Future<void> setup({
TradesStore(tradesSource: _tradesSource, settingsStore: getIt.get<SettingsStore>()));
getIt.registerSingleton<OrdersStore>(
OrdersStore(ordersSource: _ordersSource, settingsStore: getIt.get<SettingsStore>()));
getIt.registerFactory(() =>
PayjoinTransactionsStore(payjoinSessionSource: _payjoinSessionSource));
getIt.registerSingleton<TradeFilterStore>(TradeFilterStore());
getIt.registerSingleton<TransactionFilterStore>(TransactionFilterStore(getIt.get<AppStore>()));
getIt.registerSingleton<FiatConversionStore>(FiatConversionStore());
@@ -507,6 +516,7 @@ Future<void> setup({
yatStore: getIt.get<YatStore>(),
ordersStore: getIt.get<OrdersStore>(),
anonpayTransactionsStore: getIt.get<AnonpayTransactionsStore>(),
payjoinTransactionsStore: getIt.get<PayjoinTransactionsStore>(),
sharedPreferences: getIt.get<SharedPreferences>(),
keyService: getIt.get<KeyService>()));
@@ -1095,6 +1105,7 @@ Future<void> setup({
return bitcoin!.createBitcoinWalletService(
_walletInfoSource,
_unspentCoinsInfoSource,
_payjoinSessionSource,
getIt.get<SettingsStore>().silentPaymentsAlwaysScan,
SettingsStoreBase.walletPasswordDirectInput,
);
@@ -1423,6 +1434,15 @@ Future<void> setup({
settingsStore: getIt.get<SettingsStore>(),
));
getIt.registerFactoryParam<PayjoinDetailsViewModel, String, TransactionInfo?>(
(String sessionId, TransactionInfo? transactionInfo) =>
PayjoinDetailsViewModel(
sessionId,
transactionInfo,
payjoinSessionSource: _payjoinSessionSource,
settingsStore: getIt.get<SettingsStore>(),
));
getIt.registerFactoryParam<AnonPayReceivePage, AnonpayInfoBase, void>(
(AnonpayInfoBase anonpayInvoiceInfo, _) =>
AnonPayReceivePage(invoiceInfo: anonpayInvoiceInfo));
@@ -1431,6 +1451,11 @@ Future<void> setup({
(AnonpayInvoiceInfo anonpayInvoiceInfo, _) => AnonpayDetailsPage(
anonpayDetailsViewModel: getIt.get<AnonpayDetailsViewModel>(param1: anonpayInvoiceInfo)));
getIt.registerFactoryParam<PayjoinDetailsPage, String, TransactionInfo?>(
(String sessionId, TransactionInfo? transactionInfo) => PayjoinDetailsPage(
payjoinDetailsViewModel: getIt.get<PayjoinDetailsViewModel>(
param1: sessionId, param2: transactionInfo)));
getIt.registerFactoryParam<HomeSettingsPage, BalanceViewModel, void>((balanceViewModel, _) =>
HomeSettingsPage(getIt.get<HomeSettingsViewModel>(param1: balanceViewModel)));
+1
View File
@@ -82,6 +82,7 @@ class PreferencesKey {
static const lookupsOpenAlias = 'looks_up_open_alias';
static const lookupsENS = 'looks_up_ens';
static const lookupsWellKnown = 'looks_up_well_known';
static const usePayjoin = 'use_payjoin';
static const showCameraConsent = 'show_camera_consent';
static const showDecredInfoCard = 'show_decred_info_card';
+9
View File
@@ -35,6 +35,7 @@ import 'package:cw_core/cake_hive.dart';
import 'package:cw_core/hive_type_ids.dart';
import 'package:cw_core/mweb_utxo.dart';
import 'package:cw_core/node.dart';
import 'package:cw_core/payjoin_session.dart';
import 'package:cw_core/unspent_coins_info.dart';
import 'package:cw_core/utils/print_verbose.dart';
import 'package:cw_core/wallet_info.dart';
@@ -178,6 +179,10 @@ Future<void> initializeAppConfigs({bool loadWallet = true}) async {
CakeHive.registerAdapter(MwebUtxoAdapter());
}
if (!CakeHive.isAdapterRegistered(PayjoinSession.typeId)) {
CakeHive.registerAdapter(PayjoinSessionAdapter());
}
final secureStorage = secureStorageShared;
final transactionDescriptionsBoxKey =
await getEncryptionKey(secureStorage: secureStorage, forKey: TransactionDescription.boxKey);
@@ -197,6 +202,7 @@ Future<void> initializeAppConfigs({bool loadWallet = true}) async {
final exchangeTemplates = await CakeHive.openBox<ExchangeTemplate>(ExchangeTemplate.boxName);
final anonpayInvoiceInfo = await CakeHive.openBox<AnonpayInvoiceInfo>(AnonpayInvoiceInfo.boxName);
final unspentCoinsInfoSource = await CakeHive.openBox<UnspentCoinsInfo>(UnspentCoinsInfo.boxName);
final payjoinSessionSource = await CakeHive.openBox<PayjoinSession>(PayjoinSession.boxName);
final havenSeedStoreBoxKey =
await getEncryptionKey(secureStorage: secureStorage, forKey: HavenSeedStore.boxKey);
@@ -219,6 +225,7 @@ Future<void> initializeAppConfigs({bool loadWallet = true}) async {
exchangeTemplates: exchangeTemplates,
transactionDescriptions: transactionDescriptions,
secureStorage: secureStorage,
payjoinSessionSource: payjoinSessionSource,
anonpayInvoiceInfo: anonpayInvoiceInfo,
havenSeedStore: havenSeedStore,
initialMigrationVersion: 49,
@@ -241,6 +248,7 @@ Future<void> initialSetup(
required SecureStorage secureStorage,
required Box<AnonpayInvoiceInfo> anonpayInvoiceInfo,
required Box<UnspentCoinsInfo> unspentCoinsInfoSource,
required Box<PayjoinSession> payjoinSessionSource,
required Box<HavenSeedStore> havenSeedStore,
int initialMigrationVersion = 15, }) async {
LanguageService.loadLocaleList();
@@ -266,6 +274,7 @@ Future<void> initialSetup(
ordersSource: ordersSource,
anonpayInvoiceInfoSource: anonpayInvoiceInfo,
unspentCoinsInfoSource: unspentCoinsInfoSource,
payjoinSessionSource: payjoinSessionSource,
navigatorKey: navigatorKey,
secureStorage: secureStorage,
);
@@ -1,3 +1,4 @@
import 'package:cake_wallet/bitcoin/bitcoin.dart';
import 'package:cake_wallet/di.dart';
import 'package:cake_wallet/entities/auto_generate_subaddress_status.dart';
import 'package:cake_wallet/entities/fiat_api_mode.dart';
@@ -78,6 +79,10 @@ void startCurrentWalletChangeReaction(
_setAutoGenerateSubaddressStatus(wallet, settingsStore);
}
if (wallet.type == WalletType.bitcoin) {
bitcoin!.updatePayjoinState(wallet, settingsStore.usePayjoin);
}
await wallet.connectToNode(node: node);
if (wallet.type == WalletType.nano || wallet.type == WalletType.banano) {
final powNode = settingsStore.getCurrentPowNode(wallet.type);
+9
View File
@@ -58,6 +58,7 @@ import 'package:cake_wallet/src/screens/new_wallet/wallet_group_existing_seed_de
import 'package:cake_wallet/src/screens/nodes/node_create_or_edit_page.dart';
import 'package:cake_wallet/src/screens/nodes/pow_node_create_or_edit_page.dart';
import 'package:cake_wallet/src/screens/order_details/order_details_page.dart';
import 'package:cake_wallet/src/screens/payjoin_details/payjoin_details_page.dart';
import 'package:cake_wallet/src/screens/pin_code/pin_code_widget.dart';
import 'package:cake_wallet/src/screens/receive/address_list_page.dart';
import 'package:cake_wallet/src/screens/receive/anonpay_invoice_page.dart';
@@ -719,6 +720,14 @@ Route<dynamic> createRoute(RouteSettings settings) {
return CupertinoPageRoute<void>(
builder: (_) => getIt.get<AnonpayDetailsPage>(param1: anonInvoiceViewData));
case Routes.payjoinDetails:
final arguments = settings.arguments as List;
final sessionId = arguments.first as String;
final transactionInfo = arguments[1] as TransactionInfo?;
return CupertinoPageRoute<void>(
builder: (_) => getIt.get<PayjoinDetailsPage>(
param1: sessionId, param2: transactionInfo));
case Routes.desktop_actions:
return PageRouteBuilder(
opaque: false,
+1
View File
@@ -15,6 +15,7 @@ class Routes {
static const dashboard = '/dashboard';
static const send = '/send';
static const transactionDetails = '/transaction_info';
static const payjoinDetails = '/transaction_info/payjoin';
static const bumpFeePage = '/bump_fee_page';
static const receive = '/receive';
static const newSubaddress = '/new_subaddress';
@@ -1,12 +1,14 @@
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/src/screens/dashboard/widgets/payjoin_transaction_row.dart';
import 'package:cake_wallet/src/screens/dashboard/widgets/trade_row.dart';
import 'package:cake_wallet/themes/extensions/placeholder_theme.dart';
import 'package:cake_wallet/src/widgets/dashboard_card_widget.dart';
import 'package:cake_wallet/utils/responsive_layout_util.dart';
import 'package:cake_wallet/view_model/dashboard/anonpay_transaction_list_item.dart';
import 'package:cake_wallet/view_model/dashboard/order_list_item.dart';
import 'package:cake_wallet/view_model/dashboard/payjoin_transaction_list_item.dart';
import 'package:cake_wallet/view_model/dashboard/trade_list_item.dart';
import 'package:cw_core/crypto_currency.dart';
import 'package:cw_core/sync_status.dart';
@@ -143,6 +145,25 @@ class TransactionsPage extends StatelessWidget {
);
}
if (item is PayjoinTransactionListItem) {
final session = item.session;
return PayjoinTransactionRow(
key: item.key,
onTap: () => Navigator.of(context).pushNamed(
Routes.payjoinDetails,
arguments: [item.sessionId, item.transaction],
),
currency: "BTC",
state: item.status,
amount: bitcoin!.formatterBitcoinAmountToString(
amount: session.amount.toInt()),
createdAt: DateFormat('HH:mm')
.format(session.inProgressSince!),
isSending: session.isSenderSession,
);
}
if (item is TradeListItem) {
final trade = item.trade;
@@ -0,0 +1,99 @@
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/themes/extensions/cake_text_theme.dart';
import 'package:cake_wallet/themes/extensions/dashboard_page_theme.dart';
import 'package:flutter/material.dart';
class PayjoinTransactionRow extends StatelessWidget {
PayjoinTransactionRow({
required this.createdAt,
required this.currency,
required this.onTap,
required this.amount,
required this.state,
required this.isSending,
super.key,
});
final VoidCallback? onTap;
final String createdAt;
final String amount;
final String currency;
final String state;
final bool isSending;
@override
Widget build(BuildContext context) {
return InkWell(
onTap: onTap,
child: Container(
padding: EdgeInsets.fromLTRB(24, 8, 24, 8),
color: Colors.transparent,
child: Row(
mainAxisSize: MainAxisSize.max,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
_getImage(),
SizedBox(width: 12),
Expanded(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
Text(
"${isSending ? S.current.outgoing : S.current.incoming} Payjoin",
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
color: Theme.of(context)
.extension<DashboardPageTheme>()!
.textColor,
),
),
Text(
amount + ' ' + currency,
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
color: Theme.of(context)
.extension<DashboardPageTheme>()!
.textColor,
),
)
]),
SizedBox(height: 5),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
Text(
createdAt,
style: TextStyle(
fontSize: 14,
color: Theme.of(context)
.extension<CakeTextTheme>()!
.dateSectionRowColor,
),
),
Text(
state,
style: TextStyle(
fontSize: 14,
color: Theme.of(context)
.extension<CakeTextTheme>()!
.dateSectionRowColor,
),
),
])
],
))
],
),
));
}
Widget _getImage() => ClipRRect(
borderRadius: BorderRadius.circular(50),
child: Image.asset('assets/images/payjoin.png', width: 36, height: 36));
}
@@ -0,0 +1,77 @@
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/src/screens/base_page.dart';
import 'package:cake_wallet/src/screens/trade_details/trade_details_list_card.dart';
import 'package:cake_wallet/src/screens/trade_details/trade_details_status_item.dart';
import 'package:cake_wallet/src/widgets/list_row.dart';
import 'package:cake_wallet/src/widgets/standard_list.dart';
import 'package:cake_wallet/src/widgets/standard_list_card.dart';
import 'package:cake_wallet/src/widgets/standard_list_status_row.dart';
import 'package:cake_wallet/utils/show_bar.dart';
import 'package:cake_wallet/view_model/payjoin_details_view_model.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
class PayjoinDetailsPage extends BasePage {
PayjoinDetailsPage({required this.payjoinDetailsViewModel});
@override
String get title => S.current.payjoin_details;
final PayjoinDetailsViewModel payjoinDetailsViewModel;
@override
Widget body(BuildContext context) => PayjoinDetailsPageBody(payjoinDetailsViewModel);
}
class PayjoinDetailsPageBody extends StatefulWidget {
PayjoinDetailsPageBody(this.payjoinDetailsViewModel);
final PayjoinDetailsViewModel payjoinDetailsViewModel;
@override
State<PayjoinDetailsPageBody> createState() => _PayjoinDetailsPageBodyState();
}
class _PayjoinDetailsPageBodyState extends State<PayjoinDetailsPageBody> {
@override
void dispose() {
super.dispose();
widget.payjoinDetailsViewModel.listener.cancel();
}
@override
Widget build(BuildContext context) {
return SectionStandardList(
sectionCount: 1,
itemCounter: (int _) => widget.payjoinDetailsViewModel.items.length,
itemBuilder: (__, index) {
final item = widget.payjoinDetailsViewModel.items[index];
if (item is DetailsListStatusItem) {
return StandardListStatusRow(
title: item.title,
value: item.value,
status: item.status,
);
}
if (item is TradeDetailsListCardItem) {
return TradeDetailsStandardListCard(
id: item.id,
create: item.createdAt,
pair: item.pair,
currentTheme: widget.payjoinDetailsViewModel.settingsStore.currentTheme.type,
onTap: item.onTap,
);
}
return GestureDetector(
onTap: () {
Clipboard.setData(ClipboardData(text: item.value));
showBar<void>(context, S.of(context).transaction_details_copied(item.title));
},
child: ListRow(title: '${item.title}:', value: item.value),
);
});
}
}
+67 -19
View File
@@ -1,4 +1,6 @@
import 'package:cake_wallet/entities/qr_view_data.dart';
import 'package:cake_wallet/src/widgets/primary_button.dart';
import 'package:cake_wallet/themes/extensions/cake_text_theme.dart';
import 'package:cake_wallet/themes/extensions/picker_theme.dart';
import 'package:cake_wallet/themes/extensions/qr_code_theme.dart';
import 'package:cake_wallet/routes.dart';
@@ -91,26 +93,54 @@ class QRWidget extends StatelessWidget {
child: Hero(
tag: Key(heroTag ?? addressUri.toString()),
child: Center(
child: AspectRatio(
aspectRatio: 1.0,
child: Container(
padding: EdgeInsets.all(5),
decoration: BoxDecoration(
border: Border.all(
width: 3,
color: Theme.of(context)
.extension<DashboardPageTheme>()!
.textColor,
),
),
child: Container(
decoration: BoxDecoration(
border: Border.all(
width: 3,
color: Colors.white,
child: Container(
padding: EdgeInsets.zero,
decoration: BoxDecoration(
border: Border(top: BorderSide.none),
borderRadius:
BorderRadius.all(Radius.circular(5)),
color: Colors.white,
),
child: Column(
children: [
Container(
padding: EdgeInsets.all(3),
child: AspectRatio(
aspectRatio: 1.0,
child: QrImage(
data: addressUri.toString(),
),
),
child: QrImage(data: addressUri.toString())),
),
if (addressListViewModel
.payjoinEndpoint.isNotEmpty) ...[
Row(
mainAxisAlignment:
MainAxisAlignment.center,
children: [
Padding(
padding: EdgeInsets.only(
top: 4,
bottom: 4,
right: 4,
),
child: Image.asset(
'assets/images/payjoin.png',
width: 20,
),
),
Text(
S.of(context).payjoin_enabled,
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w600,
color: Colors.black,
),
),
],
),
]
],
),
),
),
@@ -179,7 +209,25 @@ class QRWidget extends StatelessWidget {
),
),
),
)
),
if (addressListViewModel.payjoinEndpoint.isNotEmpty) ...[
Padding(
padding: EdgeInsets.only(top: 12),
child: PrimaryImageButton(
onPressed: () {
Clipboard.setData(ClipboardData(
text: addressListViewModel.payjoinEndpoint));
showBar<void>(context, S.of(context).copied_to_clipboard);
},
image: Image.asset('assets/images/payjoin.png', width: 25,),
text: S.of(context).copy_payjoin_url,
color: Theme.of(context).cardColor,
textColor: Theme.of(context)
.extension<CakeTextTheme>()!
.buttonTextColor,
),
),
],
],
),
),
+9 -1
View File
@@ -1,5 +1,5 @@
import 'dart:async';
import 'dart:io';
import 'package:cake_wallet/bitcoin/bitcoin.dart';
import 'package:cake_wallet/core/auth_service.dart';
import 'package:cake_wallet/core/totp_request_details.dart';
import 'package:cake_wallet/utils/device_info.dart';
@@ -136,6 +136,10 @@ class RootState extends State<Root> with WidgetsBindingObserver {
setState(() => _setInactive(true));
}
if (widget.appStore.wallet?.type == WalletType.bitcoin) {
bitcoin!.stopPayjoinSessions(widget.appStore.wallet!);
}
break;
case AppLifecycleState.resumed:
widget.authService.requireAuth().then((value) {
@@ -145,6 +149,10 @@ class RootState extends State<Root> with WidgetsBindingObserver {
});
}
});
if (widget.appStore.wallet?.type == WalletType.bitcoin &&
widget.appStore.settingsStore.usePayjoin) {
bitcoin!.resumePayjoinSessions(widget.appStore.wallet!);
}
break;
default:
break;
+1 -1
View File
@@ -463,7 +463,7 @@ class SendPage extends BasePage {
},
);
},
text: S.of(context).send,
text: sendViewModel.payjoinUri != null ? S.of(context).send_payjoin : S.of(context).send,
color: Theme.of(context).primaryColor,
textColor: Colors.white,
isLoading: sendViewModel.state is IsExecutingState ||
+17 -13
View File
@@ -188,19 +188,23 @@ class SendCardState extends State<SendCard> with AutomaticKeepAliveClientMixin<S
sendViewModel.createOpenCryptoPayTransaction(uri.toString());
} else {
final paymentRequest = PaymentRequest.fromUri(uri);
addressController.text = paymentRequest.address;
cryptoAmountController.text = paymentRequest.amount;
noteController.text = paymentRequest.note;
}
},
options: [
AddressTextFieldOption.paste,
AddressTextFieldOption.qrCode,
AddressTextFieldOption.addressBook
],
buttonColor: Theme.of(context).extension<SendPageTheme>()!.textFieldButtonColor,
borderColor: Theme.of(context).extension<SendPageTheme>()!.textFieldBorderColor,
textStyle:
if (sendViewModel.usePayjoin) {
sendViewModel.payjoinUri = paymentRequest.pjUri;
}
addressController.text = paymentRequest.address;
cryptoAmountController.text = paymentRequest.amount;
noteController.text = paymentRequest.note;}
},
options: [
AddressTextFieldOption.paste,
AddressTextFieldOption.qrCode,
AddressTextFieldOption.addressBook
],
buttonColor:
Theme.of(context).extension<SendPageTheme>()!.textFieldButtonColor,
borderColor:
Theme.of(context).extension<SendPageTheme>()!.textFieldBorderColor,
textStyle:
TextStyle(fontSize: 14, fontWeight: FontWeight.w500, color: Colors.white),
hintStyle: TextStyle(
fontSize: 14,
@@ -49,6 +49,14 @@ class PrivacyPage extends BasePage {
_privacySettingsViewModel.setExchangeApiMode(mode),
),
),
if (_privacySettingsViewModel.canUsePayjoin)
SettingsSwitcherCell(
title: S.of(context).use_payjoin,
value: _privacySettingsViewModel.usePayjoin,
onValueChange: (BuildContext _, bool value) {
_privacySettingsViewModel.setUsePayjoin(value);
},
),
SettingsSwitcherCell(
title: S.current.settings_save_recipient_address,
value: _privacySettingsViewModel.shouldSaveRecipientAddress,
@@ -2,6 +2,8 @@ import 'package:cake_wallet/src/screens/transaction_details/standart_list_item.d
class DetailsListStatusItem extends StandartListItem {
DetailsListStatusItem(
{required String title, required String value})
{required String title, required String value, this.status})
: super(title: title, value: value);
final String? status; // waiting, action required, created, fetching, finished, success
}
+19 -15
View File
@@ -1,21 +1,20 @@
import 'package:cake_wallet/utils/device_info.dart';
import 'package:cake_wallet/entities/contact_base.dart';
import 'package:cake_wallet/entities/qr_scanner.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/routes.dart';
import 'package:cake_wallet/themes/extensions/cake_text_theme.dart';
import 'package:cake_wallet/themes/extensions/send_page_theme.dart';
import 'package:cake_wallet/utils/device_info.dart';
import 'package:cake_wallet/utils/permission_handler.dart';
import 'package:cake_wallet/utils/responsive_layout_util.dart';
import 'package:cw_core/currency.dart';
import 'package:flutter/services.dart';
import 'package:flutter/material.dart';
import 'package:cake_wallet/routes.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/entities/qr_scanner.dart';
import 'package:cake_wallet/entities/contact_base.dart';
import 'package:cake_wallet/themes/extensions/send_page_theme.dart';
import 'package:cake_wallet/utils/permission_handler.dart';
import 'package:flutter/services.dart';
import 'package:permission_handler/permission_handler.dart';
enum AddressTextFieldOption { paste, qrCode, addressBook, walletAddresses }
class AddressTextField<T extends Currency> extends StatelessWidget{
class AddressTextField<T extends Currency> extends StatelessWidget {
AddressTextField({
required this.controller,
this.isActive = true,
@@ -234,9 +233,7 @@ class AddressTextField<T extends Currency> extends StatelessWidget{
if (!isCameraPermissionGranted) return;
final code = await presentQRScanner(context);
if (code == null) return;
if (code.isEmpty) {
return;
}
if (code.isEmpty) return;
try {
final uri = Uri.parse(code);
@@ -259,7 +256,8 @@ class AddressTextField<T extends Currency> extends StatelessWidget{
}
Future<void> _presetWalletAddressPicker(BuildContext context) async {
final address = await Navigator.of(context).pushNamed(Routes.pickerWalletAddress);
final address =
await Navigator.of(context).pushNamed(Routes.pickerWalletAddress);
if (address is String) {
controller?.text = address;
@@ -272,7 +270,13 @@ class AddressTextField<T extends Currency> extends StatelessWidget{
final address = clipboard?.text ?? '';
if (address.isNotEmpty) {
controller?.text = address;
try {
final uri = Uri.parse(address);
controller?.text = uri.path;
onURIScanned?.call(uri);
} catch (_) {
controller?.text = address;
}
}
onPushPasteButton?.call(context);
@@ -1,5 +1,4 @@
import 'package:cake_wallet/themes/extensions/cake_text_theme.dart';
import 'package:cake_wallet/palette.dart';
import 'package:cake_wallet/src/screens/dashboard/widgets/sync_indicator_icon.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
@@ -7,10 +6,11 @@ import 'package:cake_wallet/themes/extensions/address_theme.dart';
import 'package:cake_wallet/themes/extensions/transaction_trade_theme.dart';
class StandardListStatusRow extends StatelessWidget {
StandardListStatusRow({required this.title, required this.value});
StandardListStatusRow({required this.title, required this.value, this.status});
final String title;
final String value;
final String? status; // waiting, action required, created, fetching, finished, success
@override
Widget build(BuildContext context) {
@@ -43,7 +43,7 @@ class StandardListStatusRow extends StatelessWidget {
children: <Widget>[
SyncIndicatorIcon(
boolMode: false,
value: value,
value: status ?? value,
size: 6,
),
SizedBox(
@@ -0,0 +1,47 @@
import 'dart:async';
import 'package:cake_wallet/view_model/dashboard/payjoin_transaction_list_item.dart';
import 'package:cw_core/payjoin_session.dart';
import 'package:flutter/foundation.dart';
import 'package:hive/hive.dart';
import 'package:mobx/mobx.dart';
part 'payjoin_transactions_store.g.dart';
class PayjoinTransactionsStore = PayjoinTransactionsStoreBase
with _$PayjoinTransactionsStore;
abstract class PayjoinTransactionsStoreBase with Store {
PayjoinTransactionsStoreBase({
required this.payjoinSessionSource,
}) : transactions = <PayjoinTransactionListItem>[] {
payjoinSessionSource.watch().listen((_) => updateTransactionList());
updateTransactionList();
}
Box<PayjoinSession> payjoinSessionSource;
@observable
List<PayjoinTransactionListItem> transactions;
@action
Future<void> updateTransactionList() async {
final updatedTransactions = <PayjoinTransactionListItem>[];
payjoinSessionSource.toMap().forEach((dynamic key, PayjoinSession session) {
if ([
PayjoinSessionStatus.inProgress.name,
PayjoinSessionStatus.success.name,
PayjoinSessionStatus.unrecoverable.name
].contains(session.status) &&
session.inProgressSince != null) {
updatedTransactions.add(PayjoinTransactionListItem(
sessionId: key as String,
session: session,
key: ValueKey('payjoin_transaction_list_item_${key}_key'),
));
}
});
transactions = updatedTransactions;
}
}
+11
View File
@@ -121,6 +121,7 @@ abstract class SettingsStoreBase with Store {
required this.lookupsOpenAlias,
required this.lookupsENS,
required this.lookupsWellKnown,
required this.usePayjoin,
required this.customBitcoinFeeRate,
required this.silentPaymentsCardDisplay,
required this.silentPaymentsAlwaysScan,
@@ -483,6 +484,11 @@ abstract class SettingsStoreBase with Store {
(bool looksUpWellKnown) =>
_sharedPreferences.setBool(PreferencesKey.lookupsWellKnown, looksUpWellKnown));
reaction(
(_) => usePayjoin,
(bool usePayjoin) =>
_sharedPreferences.setBool(PreferencesKey.usePayjoin, usePayjoin));
// secure storage keys:
reaction(
(_) => allowBiometricalAuthentication,
@@ -802,6 +808,9 @@ abstract class SettingsStoreBase with Store {
@observable
bool lookupsWellKnown;
@observable
bool usePayjoin;
@observable
SyncMode currentSyncMode;
@@ -1009,6 +1018,7 @@ abstract class SettingsStoreBase with Store {
final lookupsOpenAlias = sharedPreferences.getBool(PreferencesKey.lookupsOpenAlias) ?? true;
final lookupsENS = sharedPreferences.getBool(PreferencesKey.lookupsENS) ?? true;
final lookupsWellKnown = sharedPreferences.getBool(PreferencesKey.lookupsWellKnown) ?? true;
final usePayjoin = sharedPreferences.getBool(PreferencesKey.usePayjoin) ?? false;
final customBitcoinFeeRate = sharedPreferences.getInt(PreferencesKey.customBitcoinFeeRate) ?? 1;
final silentPaymentsCardDisplay =
sharedPreferences.getBool(PreferencesKey.silentPaymentsCardDisplay) ?? true;
@@ -1311,6 +1321,7 @@ abstract class SettingsStoreBase with Store {
lookupsOpenAlias: lookupsOpenAlias,
lookupsENS: lookupsENS,
lookupsWellKnown: lookupsWellKnown,
usePayjoin: usePayjoin,
customBitcoinFeeRate: customBitcoinFeeRate,
silentPaymentsCardDisplay: silentPaymentsCardDisplay,
silentPaymentsAlwaysScan: silentPaymentsAlwaysScan,
+9 -2
View File
@@ -1,8 +1,8 @@
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/nano/nano.dart';
class PaymentRequest {
PaymentRequest(this.address, this.amount, this.note, this.scheme, {this.callbackUrl, this.callbackMessage});
PaymentRequest(this.address, this.amount, this.note, this.scheme, this.pjUri,
{this.callbackUrl, this.callbackMessage});
factory PaymentRequest.fromUri(Uri? uri) {
var address = "";
@@ -12,8 +12,13 @@ class PaymentRequest {
String? walletType;
String? callbackUrl;
String? callbackMessage;
String? pjUri;
if (uri != null) {
if (uri.queryParameters['pj'] != null) {
pjUri = uri.toString();
}
address = uri.queryParameters['address'] ?? uri.path;
amount = uri.queryParameters['tx_amount'] ?? uri.queryParameters['amount'] ?? "";
note = uri.queryParameters['tx_description'] ?? uri.queryParameters['message'] ?? "";
@@ -42,6 +47,7 @@ class PaymentRequest {
amount,
note,
scheme,
pjUri,
callbackUrl: callbackUrl,
callbackMessage: callbackMessage,
);
@@ -51,6 +57,7 @@ class PaymentRequest {
final String amount;
final String note;
final String scheme;
final String? pjUri;
final String? callbackUrl;
final String? callbackMessage;
}
@@ -13,11 +13,11 @@ import 'package:cake_wallet/entities/service_status.dart';
import 'package:cake_wallet/exchange/exchange_provider_description.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/monero/monero.dart';
import 'package:cake_wallet/wownero/wownero.dart' as wow;
import 'package:cake_wallet/nano/nano.dart';
import 'package:cake_wallet/store/anonpay/anonpay_transactions_store.dart';
import 'package:cake_wallet/store/app_store.dart';
import 'package:cake_wallet/store/dashboard/orders_store.dart';
import 'package:cake_wallet/store/dashboard/payjoin_transactions_store.dart';
import 'package:cake_wallet/store/dashboard/trade_filter_store.dart';
import 'package:cake_wallet/store/dashboard/trades_store.dart';
import 'package:cake_wallet/store/dashboard/transaction_filter_store.dart';
@@ -29,9 +29,12 @@ import 'package:cake_wallet/view_model/dashboard/balance_view_model.dart';
import 'package:cake_wallet/view_model/dashboard/filter_item.dart';
import 'package:cake_wallet/view_model/dashboard/formatted_item_list.dart';
import 'package:cake_wallet/view_model/dashboard/order_list_item.dart';
import 'package:cake_wallet/view_model/dashboard/payjoin_transaction_list_item.dart';
import 'package:cake_wallet/view_model/dashboard/trade_list_item.dart';
import 'package:cake_wallet/view_model/dashboard/transaction_list_item.dart';
import 'package:cake_wallet/view_model/settings/sync_mode.dart';
import 'package:cake_wallet/wallet_type_utils.dart';
import 'package:cake_wallet/wownero/wownero.dart' as wow;
import 'package:cryptography/cryptography.dart';
import 'package:cw_core/balance.dart';
import 'package:cw_core/cake_hive.dart';
@@ -70,6 +73,7 @@ abstract class DashboardViewModelBase with Store {
required this.yatStore,
required this.ordersStore,
required this.anonpayTransactionsStore,
required this.payjoinTransactionsStore,
required this.sharedPreferences,
required this.keyService})
: hasTradeAction = true,
@@ -408,9 +412,16 @@ abstract class DashboardViewModelBase with Store {
ordersStore.orders.where((item) => item.order.walletId == wallet.id).toList();
@computed
List<AnonpayTransactionListItem> get anonpayTransactons => anonpayTransactionsStore.transactions
.where((item) => item.transaction.walletId == wallet.id)
.toList();
List<AnonpayTransactionListItem> get anonpayTransactions =>
anonpayTransactionsStore.transactions
.where((item) => item.transaction.walletId == wallet.id)
.toList();
@computed
List<PayjoinTransactionListItem> get payjoinTransactions =>
payjoinTransactionsStore.transactions
.where((item) => item.session.walletId == wallet.id)
.toList();
@computed
double get price => balanceViewModel.price;
@@ -423,11 +434,27 @@ abstract class DashboardViewModelBase with Store {
List<ActionListItem> get items {
final _items = <ActionListItem>[];
_items.addAll(
transactionFilterStore.filtered(transactions: [...transactions, ...anonpayTransactons]));
_items.addAll(transactionFilterStore
.filtered(transactions: [...transactions, ...anonpayTransactions]));
_items.addAll(tradeFilterStore.filtered(trades: trades, wallet: wallet));
_items.addAll(orders);
if (payjoinTransactions.isNotEmpty) {
final _payjoinTransactions = payjoinTransactions;
_items.forEach((e) {
if (e is TransactionListItem &&
_payjoinTransactions
.any((t) => t.session.txId == e.transaction.id)) {
_payjoinTransactions
.firstWhere((t) => t.session.txId == e.transaction.id)
.transaction = e.transaction;
}
});
_items.addAll(_payjoinTransactions);
_items.removeWhere((e) => (e is TransactionListItem &&
_payjoinTransactions.any((t) => t.session.txId == e.transaction.id)));
}
return formattedItemsList(_items);
}
@@ -755,6 +782,8 @@ abstract class DashboardViewModelBase with Store {
TransactionFilterStore transactionFilterStore;
PayjoinTransactionsStore payjoinTransactionsStore;
Map<String, List<FilterItem>> filterItems;
bool get isBuyEnabled => settingsStore.isBitcoinBuyEnabled;
@@ -0,0 +1,34 @@
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/view_model/dashboard/action_list_item.dart';
import 'package:cw_core/payjoin_session.dart';
import 'package:cw_core/transaction_info.dart';
class PayjoinTransactionListItem extends ActionListItem {
PayjoinTransactionListItem({
required this.sessionId,
required this.session,
required super.key,
});
final String sessionId;
final PayjoinSession session;
TransactionInfo? transaction;
@override
DateTime get date => session.inProgressSince!;
String get status {
switch (session.status) {
case 'success':
if (transaction?.isPending == true)
return S.current.payjoin_request_awaiting_tx;
return S.current.successful;
case 'inProgress':
return S.current.payjoin_request_in_progress;
case 'unrecoverable':
return S.current.error;
default:
return session.status;
}
}
}
@@ -0,0 +1,122 @@
import 'dart:async';
import 'package:cake_wallet/bitcoin/bitcoin.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/src/screens/trade_details/trade_details_list_card.dart';
import 'package:cake_wallet/src/screens/trade_details/trade_details_status_item.dart';
import 'package:cake_wallet/src/screens/transaction_details/standart_list_item.dart';
import 'package:cake_wallet/store/settings_store.dart';
import 'package:cake_wallet/utils/date_formatter.dart';
import 'package:cw_core/payjoin_session.dart';
import 'package:cw_core/transaction_info.dart';
import 'package:cw_core/utils/print_verbose.dart';
import 'package:flutter/widgets.dart';
import 'package:hive_flutter/hive_flutter.dart';
import 'package:mobx/mobx.dart';
part 'payjoin_details_view_model.g.dart';
class PayjoinDetailsViewModel = PayjoinDetailsViewModelBase
with _$PayjoinDetailsViewModel;
abstract class PayjoinDetailsViewModelBase with Store {
PayjoinDetailsViewModelBase(
this.payjoinSessionId,
this.transactionInfo, {
required this.payjoinSessionSource,
required this.settingsStore,
}) : items = ObservableList<StandartListItem>(),
payjoinSession = payjoinSessionSource.get(payjoinSessionId)! {
listener = payjoinSessionSource.watch().listen((e) {
if (e.key == payjoinSessionId) _updateItems();
});
_updateItems();
}
final Box<PayjoinSession> payjoinSessionSource;
final SettingsStore settingsStore;
final String payjoinSessionId;
final TransactionInfo? transactionInfo;
@observable
late PayjoinSession payjoinSession;
final ObservableList<StandartListItem> items;
late final StreamSubscription<BoxEvent> listener;
Timer? timer;
@action
void _updateItems() {
final dateFormat = DateFormatter.withCurrentLocal();
items.clear();
items.addAll([
DetailsListStatusItem(
title: S.current.status,
value: _getStatusString(),
status: payjoinSession.status,
),
TradeDetailsListCardItem(
id: "${payjoinSession.isSenderSession ? S.current.outgoing : S.current.incoming} Payjoin",
createdAt:
dateFormat.format(payjoinSession.inProgressSince!).toString(),
pair:
'${bitcoin!.formatterBitcoinAmountToString(amount: payjoinSession.amount.toInt())} BTC',
onTap: (_) {},
),
if (payjoinSession.error?.isNotEmpty == true)
StandartListItem(
title: S.current.error,
value: payjoinSession.error!,
),
if (payjoinSession.txId?.isNotEmpty == true)
StandartListItem(
title: S.current.transaction_details_transaction_id,
value: payjoinSession.txId!,
key: ValueKey('standard_list_item_transaction_details_id_key'),
)
]);
if (transactionInfo != null) {
items.addAll([
StandartListItem(
title: S.current.transaction_details_date,
value: dateFormat.format(transactionInfo!.date),
key: ValueKey('standard_list_item_transaction_details_date_key'),
),
StandartListItem(
title: S.current.confirmations,
value: transactionInfo!.confirmations.toString(),
key: ValueKey('standard_list_item_transaction_confirmations_key'),
),
StandartListItem(
title: S.current.transaction_details_height,
value: '${transactionInfo!.height}',
key: ValueKey('standard_list_item_transaction_details_height_key'),
),
if (transactionInfo!.feeFormatted()?.isNotEmpty ?? false)
StandartListItem(
title: S.current.transaction_details_fee,
value: transactionInfo!.feeFormatted()!,
key: ValueKey('standard_list_item_transaction_details_fee_key'),
),
]);
}
}
String _getStatusString() {
switch (payjoinSession.status) {
case 'success':
if (transactionInfo?.isPending == true)
return S.current.payjoin_request_awaiting_tx;
return S.current.successful;
case 'inProgress':
return S.current.payjoin_request_in_progress;
case 'unrecoverable':
return S.current.error;
default:
return payjoinSession.status;
}
}
}
+7
View File
@@ -619,6 +619,7 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
priority: priority!,
feeRate: feesViewModel.customBitcoinFeeRate,
coinTypeToSpendFrom: coinTypeToSpendFrom,
payjoinUri: _settingsStore.usePayjoin ? payjoinUri : null,
);
case WalletType.litecoin:
return bitcoin!.createBitcoinTransactionCredentials(
@@ -855,4 +856,10 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
return false;
}
@computed
bool get usePayjoin => _settingsStore.usePayjoin;
@observable
String? payjoinUri;
}
@@ -1,3 +1,4 @@
import 'package:cake_wallet/bitcoin/bitcoin.dart';
import 'package:cake_wallet/entities/auto_generate_subaddress_status.dart';
import 'package:cake_wallet/entities/exchange_api_mode.dart';
import 'package:cake_wallet/ethereum/ethereum.dart';
@@ -32,20 +33,19 @@ abstract class PrivacySettingsViewModelBase with Store {
@action
void setAutoGenerateSubaddresses(bool value) {
_wallet.isEnabledAutoGenerateSubaddress = value;
if (value) {
_settingsStore.autoGenerateSubaddressStatus = AutoGenerateSubaddressStatus.enabled;
} else {
_settingsStore.autoGenerateSubaddressStatus = AutoGenerateSubaddressStatus.disabled;
}
_settingsStore.autoGenerateSubaddressStatus = value
? AutoGenerateSubaddressStatus.enabled
: AutoGenerateSubaddressStatus.disabled;
}
bool get isAutoGenerateSubaddressesVisible =>
_wallet.type == WalletType.monero ||
_wallet.type == WalletType.wownero ||
_wallet.type == WalletType.bitcoin ||
_wallet.type == WalletType.litecoin ||
_wallet.type == WalletType.bitcoinCash ||
_wallet.type == WalletType.decred;
bool get isAutoGenerateSubaddressesVisible => [
WalletType.monero,
WalletType.wownero,
WalletType.bitcoin,
WalletType.litecoin,
WalletType.bitcoinCash,
WalletType.decred
].contains(_wallet.type);
bool get isMoneroWallet => _wallet.type == WalletType.monero;
@@ -100,6 +100,9 @@ abstract class PrivacySettingsViewModelBase with Store {
@computed
bool get looksUpWellKnown => _settingsStore.lookupsWellKnown;
@computed
bool get usePayjoin => _settingsStore.usePayjoin;
bool get canUseEtherscan => _wallet.type == WalletType.ethereum;
bool get canUsePolygonScan => _wallet.type == WalletType.polygon;
@@ -108,6 +111,8 @@ abstract class PrivacySettingsViewModelBase with Store {
bool get canUseMempoolFeeAPI => _wallet.type == WalletType.bitcoin;
bool get canUsePayjoin => _wallet.type == WalletType.bitcoin;
@action
void setShouldSaveRecipientAddress(bool value) =>
_settingsStore.shouldSaveRecipientAddress = value;
@@ -170,7 +175,12 @@ abstract class PrivacySettingsViewModelBase with Store {
}
@action
void setUseMempoolFeeAPI(bool value) {
_settingsStore.useMempoolFeeAPI = value;
void setUseMempoolFeeAPI(bool value) =>
_settingsStore.useMempoolFeeAPI = value;
@action
void setUsePayjoin(bool value) {
_settingsStore.usePayjoin = value;
bitcoin!.updatePayjoinState(_wallet, value);
}
}
@@ -1,4 +1,5 @@
import 'dart:developer' as dev;
import 'dart:core';
import 'package:cake_wallet/bitcoin/bitcoin.dart';
import 'package:cake_wallet/core/fiat_conversion_service.dart';
@@ -72,17 +73,21 @@ class HavenURI extends PaymentURI {
}
class BitcoinURI extends PaymentURI {
BitcoinURI({required super.amount, required super.address});
BitcoinURI({required super.amount, required super.address, this.pjUri = ''});
final String pjUri;
@override
String toString() {
var base = 'bitcoin:$address';
final qp = <String, String>{};
if (amount.isNotEmpty) {
base += '?amount=${amount.replaceAll(',', '.')}';
if (amount.isNotEmpty) qp['amount'] = amount.replaceAll(',', '.');
if (pjUri.isNotEmpty) {
qp['pjos'] = '0';
qp['pj'] = pjUri;
}
return base;
return Uri(scheme: 'bitcoin', path: address, queryParameters: qp).toString();
}
}
@@ -300,6 +305,11 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo
WalletAddressListItem get address =>
WalletAddressListItem(address: wallet.walletAddresses.address, isPrimary: false);
@computed
String get payjoinEndpoint => wallet.type == WalletType.bitcoin
? bitcoin!.getPayjoinEndpoint(wallet)
: "";
@computed
PaymentURI get uri {
switch (wallet.type) {
@@ -308,7 +318,10 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo
case WalletType.haven:
return HavenURI(amount: amount, address: address.address);
case WalletType.bitcoin:
return BitcoinURI(amount: amount, address: address.address);
return BitcoinURI(
amount: amount,
address: address.address,
pjUri: payjoinEndpoint);
case WalletType.litecoin:
return LitecoinURI(amount: amount, address: address.address);
case WalletType.ethereum: