mirror of
https://github.com/cake-tech/cake_wallet.git
synced 2025-06-28 20:39:51 +00:00
CW-1091-payjoin-error-handeling (#2317)
* feat: stop polling payjoin on switch wallet * refactor: improve Payjoin session handling and cleanup unused methods - Replaced `initReceiver` with `getUnusedReceiver` to reuse existing Payjoin sessions. - Streamlined session initialization by removing `spawnNewReceiver`. - Adjusted wallet sync reactions to resume Payjoin sessions when necessary. * fix: Receiver.fromJson correctly handle parameter format in Payjoin manager * fix: try reloading unspents if unspents are empty; No Unpsents available are now recoverable errors * fix: ensure transaction details display only if transactionInfo is available and adjust payjoin success status handling * fix: adjust payjoin success status handling for pending transactions * fix: add error handling for Payjoin initialization and receiver creation [skip-ci] * fix: add unrecoverable error handling for Payjoin sender sessions
This commit is contained in:
parent
21d5c51cc9
commit
4b137bc968
9 changed files with 82 additions and 53 deletions
|
@ -266,6 +266,12 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store {
|
||||||
derivationPath: walletInfo.derivationInfo!.derivationPath!);
|
derivationPath: walletInfo.derivationInfo!.derivationPath!);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> close({bool shouldCleanup = false}) async {
|
||||||
|
payjoinManager.cleanupSessions();
|
||||||
|
super.close(shouldCleanup: shouldCleanup);
|
||||||
|
}
|
||||||
|
|
||||||
late final PayjoinManager payjoinManager;
|
late final PayjoinManager payjoinManager;
|
||||||
|
|
||||||
bool get isPayjoinAvailable => unspentCoinsInfo.values
|
bool get isPayjoinAvailable => unspentCoinsInfo.values
|
||||||
|
|
|
@ -59,19 +59,26 @@ abstract class BitcoinWalletAddressesBase extends ElectrumWalletAddresses with S
|
||||||
|
|
||||||
@action
|
@action
|
||||||
Future<void> initPayjoin() async {
|
Future<void> initPayjoin() async {
|
||||||
await payjoinManager.initPayjoin();
|
try {
|
||||||
currentPayjoinReceiver = await payjoinManager.initReceiver(primaryAddress);
|
await payjoinManager.initPayjoin();
|
||||||
payjoinEndpoint = (await currentPayjoinReceiver?.pjUri())?.pjEndpoint();
|
currentPayjoinReceiver = await payjoinManager.getUnusedReceiver(primaryAddress);
|
||||||
|
payjoinEndpoint = (await currentPayjoinReceiver?.pjUri())?.pjEndpoint();
|
||||||
|
|
||||||
payjoinManager.resumeSessions();
|
payjoinManager.resumeSessions();
|
||||||
|
} catch (e) {
|
||||||
|
printV(e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@action
|
@action
|
||||||
Future<void> newPayjoinReceiver() async {
|
Future<void> newPayjoinReceiver() async {
|
||||||
currentPayjoinReceiver = await payjoinManager.initReceiver(primaryAddress);
|
try {
|
||||||
payjoinEndpoint = (await currentPayjoinReceiver?.pjUri())?.pjEndpoint();
|
currentPayjoinReceiver = await payjoinManager.getUnusedReceiver(primaryAddress);
|
||||||
|
payjoinEndpoint = (await currentPayjoinReceiver?.pjUri())?.pjEndpoint();
|
||||||
|
|
||||||
printV("Initializing new Payjoin Receiver");
|
payjoinManager.spawnReceiver(receiver: currentPayjoinReceiver!);
|
||||||
payjoinManager.spawnNewReceiver(receiver: currentPayjoinReceiver!);
|
} catch (e) {
|
||||||
|
printV(e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -53,7 +53,7 @@ class PayjoinManager {
|
||||||
}
|
}
|
||||||
final receiver = Receiver.fromJson(json: session.receiver!);
|
final receiver = Receiver.fromJson(json: session.receiver!);
|
||||||
printV("Resuming Payjoin Receiver Session ${receiver.id()}");
|
printV("Resuming Payjoin Receiver Session ${receiver.id()}");
|
||||||
return _spawnReceiver(receiver: receiver);
|
return spawnReceiver(receiver: receiver);
|
||||||
});
|
});
|
||||||
|
|
||||||
printV("Resumed ${spawnedSessions.length} Payjoin Sessions");
|
printV("Resumed ${spawnedSessions.length} Payjoin Sessions");
|
||||||
|
@ -121,15 +121,13 @@ class PayjoinManager {
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
_cleanupSession(pjUri);
|
_cleanupSession(pjUri);
|
||||||
printV(e);
|
await _payjoinStorage.markSenderSessionUnrecoverable(pjUri, e.toString());
|
||||||
await _payjoinStorage.markSenderSessionUnrecoverable(pjUri);
|
completer.complete();
|
||||||
completer.completeError(e);
|
|
||||||
}
|
}
|
||||||
} else if (message is PayjoinSessionError) {
|
} else if (message is PayjoinSessionError) {
|
||||||
_cleanupSession(pjUri);
|
_cleanupSession(pjUri);
|
||||||
if (message is UnrecoverableError) {
|
if (message is UnrecoverableError) {
|
||||||
printV(message.message);
|
await _payjoinStorage.markSenderSessionUnrecoverable(pjUri, message.message);
|
||||||
await _payjoinStorage.markSenderSessionUnrecoverable(pjUri);
|
|
||||||
completer.complete();
|
completer.complete();
|
||||||
} else if (message is RecoverableError) {
|
} else if (message is RecoverableError) {
|
||||||
completer.complete();
|
completer.complete();
|
||||||
|
@ -149,42 +147,41 @@ class PayjoinManager {
|
||||||
return completer.future;
|
return completer.future;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<Receiver> initReceiver(String address,
|
Future<Receiver> getUnusedReceiver(String address,
|
||||||
[bool isTestnet = false]) async {
|
[bool isTestnet = false]) async {
|
||||||
try {
|
final session = _payjoinStorage.getUnusedActiveReceiverSession(_wallet.id);
|
||||||
final ohttpKeys = await PayjoinUri.fetchOhttpKeys(
|
|
||||||
ohttpRelay: await randomOhttpRelayUrl(),
|
|
||||||
payjoinDirectory: payjoinDirectoryUrl,
|
|
||||||
);
|
|
||||||
|
|
||||||
final newReceiver = await NewReceiver.create(
|
if (session != null) {
|
||||||
address: address,
|
await PayjoinUri.Url.fromStr(payjoinDirectoryUrl);
|
||||||
network: isTestnet ? Network.testnet : Network.bitcoin,
|
|
||||||
directory: payjoinDirectoryUrl,
|
|
||||||
ohttpKeys: ohttpKeys,
|
|
||||||
);
|
|
||||||
final persister = PayjoinReceiverPersister.impl();
|
|
||||||
final receiverToken = await newReceiver.persist(persister: persister);
|
|
||||||
final receiver =
|
|
||||||
await Receiver.load(persister: persister, token: receiverToken);
|
|
||||||
|
|
||||||
await _payjoinStorage.insertReceiverSession(receiver, _wallet.id);
|
return Receiver.fromJson(json: session.receiver!);
|
||||||
|
|
||||||
return receiver;
|
|
||||||
} catch (e) {
|
|
||||||
throw Exception('Error initializing Payjoin Receiver: $e');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return initReceiver(address);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> spawnNewReceiver({
|
Future<Receiver> initReceiver(String address, [bool isTestnet = false]) async {
|
||||||
required Receiver receiver,
|
final ohttpKeys = await PayjoinUri.fetchOhttpKeys(
|
||||||
bool isTestnet = false,
|
ohttpRelay: await randomOhttpRelayUrl(),
|
||||||
}) async {
|
payjoinDirectory: payjoinDirectoryUrl,
|
||||||
|
);
|
||||||
|
|
||||||
|
final newReceiver = await NewReceiver.create(
|
||||||
|
address: address,
|
||||||
|
network: isTestnet ? Network.testnet : Network.bitcoin,
|
||||||
|
directory: payjoinDirectoryUrl,
|
||||||
|
ohttpKeys: ohttpKeys,
|
||||||
|
);
|
||||||
|
final persister = PayjoinReceiverPersister.impl();
|
||||||
|
final receiverToken = await newReceiver.persist(persister: persister);
|
||||||
|
final receiver = await Receiver.load(persister: persister, token: receiverToken);
|
||||||
|
|
||||||
await _payjoinStorage.insertReceiverSession(receiver, _wallet.id);
|
await _payjoinStorage.insertReceiverSession(receiver, _wallet.id);
|
||||||
return _spawnReceiver(isTestnet: isTestnet, receiver: receiver);
|
|
||||||
|
return receiver;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _spawnReceiver({
|
Future<void> spawnReceiver({
|
||||||
required Receiver receiver,
|
required Receiver receiver,
|
||||||
bool isTestnet = false,
|
bool isTestnet = false,
|
||||||
}) async {
|
}) async {
|
||||||
|
@ -229,6 +226,10 @@ class PayjoinManager {
|
||||||
|
|
||||||
case PayjoinReceiverRequestTypes.getCandidateInputs:
|
case PayjoinReceiverRequestTypes.getCandidateInputs:
|
||||||
utxos = _wallet.getUtxoWithPrivateKeys();
|
utxos = _wallet.getUtxoWithPrivateKeys();
|
||||||
|
if (utxos.isEmpty) {
|
||||||
|
await _wallet.updateAllUnspents();
|
||||||
|
utxos = _wallet.getUtxoWithPrivateKeys();
|
||||||
|
}
|
||||||
mainToIsolateSendPort?.send({
|
mainToIsolateSendPort?.send({
|
||||||
'requestId': message['requestId'],
|
'requestId': message['requestId'],
|
||||||
'result': utxos,
|
'result': utxos,
|
||||||
|
|
|
@ -174,7 +174,7 @@ class PayjoinReceiverWorker {
|
||||||
final listUnspent =
|
final listUnspent =
|
||||||
await _sendRequest(PayjoinReceiverRequestTypes.getCandidateInputs);
|
await _sendRequest(PayjoinReceiverRequestTypes.getCandidateInputs);
|
||||||
final unspent = listUnspent as List<UtxoWithPrivateKey>;
|
final unspent = listUnspent as List<UtxoWithPrivateKey>;
|
||||||
if (unspent.isEmpty) throw Exception('No unspent outputs available');
|
if (unspent.isEmpty) throw RecoverableError('No unspent outputs available');
|
||||||
|
|
||||||
final selectedUtxo = await _inputPairFromUtxo(unspent[0]);
|
final selectedUtxo = await _inputPairFromUtxo(unspent[0]);
|
||||||
final pj6 = await pj5.contributeInputs(replacementInputs: [selectedUtxo]);
|
final pj6 = await pj5.contributeInputs(replacementInputs: [selectedUtxo]);
|
||||||
|
|
|
@ -23,6 +23,14 @@ class PayjoinStorage {
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
PayjoinSession? getUnusedActiveReceiverSession(String walletId) =>
|
||||||
|
_payjoinSessionSources.values
|
||||||
|
.where((session) =>
|
||||||
|
session.walletId == walletId &&
|
||||||
|
session.status == PayjoinSessionStatus.created.name &&
|
||||||
|
!session.isSenderSession)
|
||||||
|
.firstOrNull;
|
||||||
|
|
||||||
Future<void> markReceiverSessionComplete(
|
Future<void> markReceiverSessionComplete(
|
||||||
String sessionId, String txId, String amount) async {
|
String sessionId, String txId, String amount) async {
|
||||||
final session = _payjoinSessionSources.get("$_receiverPrefix${sessionId}")!;
|
final session = _payjoinSessionSources.get("$_receiverPrefix${sessionId}")!;
|
||||||
|
@ -76,10 +84,11 @@ class PayjoinStorage {
|
||||||
await session.save();
|
await session.save();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> markSenderSessionUnrecoverable(String pjUrl) async {
|
Future<void> markSenderSessionUnrecoverable(String pjUrl, String reason) async {
|
||||||
final session = _payjoinSessionSources.get("$_senderPrefix$pjUrl")!;
|
final session = _payjoinSessionSources.get("$_senderPrefix$pjUrl")!;
|
||||||
|
|
||||||
session.status = PayjoinSessionStatus.unrecoverable.name;
|
session.status = PayjoinSessionStatus.unrecoverable.name;
|
||||||
|
session.error = reason;
|
||||||
await session.save();
|
await session.save();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -65,7 +65,7 @@ void startCurrentWalletChangeReaction(
|
||||||
|
|
||||||
final node = settingsStore.getCurrentNode(wallet.type);
|
final node = settingsStore.getCurrentNode(wallet.type);
|
||||||
|
|
||||||
startWalletSyncStatusChangeReaction(wallet, fiatConversionStore);
|
startWalletSyncStatusChangeReaction(wallet, settingsStore);
|
||||||
startCheckConnectionReaction(wallet, settingsStore);
|
startCheckConnectionReaction(wallet, settingsStore);
|
||||||
|
|
||||||
await Future.delayed(Duration.zero);
|
await Future.delayed(Duration.zero);
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
import 'package:cake_wallet/store/dashboard/fiat_conversion_store.dart';
|
import 'package:cake_wallet/bitcoin/bitcoin.dart';
|
||||||
|
import 'package:cake_wallet/store/settings_store.dart';
|
||||||
import 'package:cw_core/utils/print_verbose.dart';
|
import 'package:cw_core/utils/print_verbose.dart';
|
||||||
|
import 'package:cw_core/wallet_type.dart';
|
||||||
import 'package:mobx/mobx.dart';
|
import 'package:mobx/mobx.dart';
|
||||||
import 'package:cw_core/transaction_history.dart';
|
import 'package:cw_core/transaction_history.dart';
|
||||||
import 'package:cw_core/wallet_base.dart';
|
import 'package:cw_core/wallet_base.dart';
|
||||||
|
@ -12,7 +14,7 @@ ReactionDisposer? _onWalletSyncStatusChangeReaction;
|
||||||
|
|
||||||
void startWalletSyncStatusChangeReaction(
|
void startWalletSyncStatusChangeReaction(
|
||||||
WalletBase<Balance, TransactionHistoryBase<TransactionInfo>, TransactionInfo> wallet,
|
WalletBase<Balance, TransactionHistoryBase<TransactionInfo>, TransactionInfo> wallet,
|
||||||
FiatConversionStore fiatConversionStore) {
|
SettingsStore settingsStore) {
|
||||||
_onWalletSyncStatusChangeReaction?.reaction.dispose();
|
_onWalletSyncStatusChangeReaction?.reaction.dispose();
|
||||||
_onWalletSyncStatusChangeReaction = reaction((_) => wallet.syncStatus, (SyncStatus status) async {
|
_onWalletSyncStatusChangeReaction = reaction((_) => wallet.syncStatus, (SyncStatus status) async {
|
||||||
try {
|
try {
|
||||||
|
@ -25,6 +27,12 @@ void startWalletSyncStatusChangeReaction(
|
||||||
if (status is SyncedSyncStatus || status is FailedSyncStatus) {
|
if (status is SyncedSyncStatus || status is FailedSyncStatus) {
|
||||||
await WakelockPlus.disable();
|
await WakelockPlus.disable();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (status is SyncedSyncStatus &&
|
||||||
|
wallet.type == WalletType.bitcoin &&
|
||||||
|
settingsStore.usePayjoin) {
|
||||||
|
bitcoin!.resumePayjoinSessions(wallet);
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
printV(e.toString());
|
printV(e.toString());
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,9 +20,8 @@ class PayjoinTransactionListItem extends ActionListItem {
|
||||||
String get status {
|
String get status {
|
||||||
switch (session.status) {
|
switch (session.status) {
|
||||||
case 'success':
|
case 'success':
|
||||||
if (transaction?.isPending == true)
|
if (transaction?.isPending == false) return S.current.successful;
|
||||||
return S.current.payjoin_request_awaiting_tx;
|
return S.current.payjoin_request_awaiting_tx;
|
||||||
return S.current.successful;
|
|
||||||
case 'inProgress':
|
case 'inProgress':
|
||||||
return S.current.payjoin_request_in_progress;
|
return S.current.payjoin_request_in_progress;
|
||||||
case 'unrecoverable':
|
case 'unrecoverable':
|
||||||
|
|
|
@ -69,7 +69,7 @@ abstract class PayjoinDetailsViewModelBase with Store {
|
||||||
title: S.current.error,
|
title: S.current.error,
|
||||||
value: payjoinSession.error!,
|
value: payjoinSession.error!,
|
||||||
),
|
),
|
||||||
if (payjoinSession.txId?.isNotEmpty == true)
|
if (payjoinSession.txId?.isNotEmpty == true && transactionInfo != null)
|
||||||
StandartListItem(
|
StandartListItem(
|
||||||
title: S.current.transaction_details_transaction_id,
|
title: S.current.transaction_details_transaction_id,
|
||||||
value: payjoinSession.txId!,
|
value: payjoinSession.txId!,
|
||||||
|
@ -107,9 +107,8 @@ abstract class PayjoinDetailsViewModelBase with Store {
|
||||||
String _getStatusString() {
|
String _getStatusString() {
|
||||||
switch (payjoinSession.status) {
|
switch (payjoinSession.status) {
|
||||||
case 'success':
|
case 'success':
|
||||||
if (transactionInfo?.isPending == true)
|
if (transactionInfo?.isPending == false) return S.current.successful;
|
||||||
return S.current.payjoin_request_awaiting_tx;
|
return S.current.payjoin_request_awaiting_tx;
|
||||||
return S.current.successful;
|
|
||||||
case 'inProgress':
|
case 'inProgress':
|
||||||
return S.current.payjoin_request_in_progress;
|
return S.current.payjoin_request_in_progress;
|
||||||
case 'unrecoverable':
|
case 'unrecoverable':
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue