mirror of
https://github.com/Such-Software/hash-wallet.git
synced 2026-10-20 06:08:34 +02:00
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:
co-authored by
cyan
Omar Hatem
parent
4a08e18f00
commit
82e3ebf4fa
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user