Skip to content

Commit 0ded109

Browse files
committed
improve handling payout failure
1 parent df4ae98 commit 0ded109

File tree

2 files changed

+79
-33
lines changed

2 files changed

+79
-33
lines changed

core/src/main/java/haveno/core/trade/Trade.java

Lines changed: 72 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
import com.google.protobuf.ByteString;
3939
import com.google.protobuf.Message;
4040
import haveno.common.ThreadUtils;
41+
import haveno.common.Timer;
4142
import haveno.common.UserThread;
4243
import haveno.common.crypto.Encryption;
4344
import haveno.common.crypto.PubKeyRing;
@@ -164,6 +165,12 @@ public abstract class Trade extends XmrWalletBase implements Tradable, Model {
164165
public static final String PROTOCOL_VERSION = "protocolVersion"; // key for extraDataMap in trade statistics
165166
public BooleanProperty wasWalletPolled = new SimpleBooleanProperty(false);
166167

168+
// failed payout tx handling
169+
private Object failedPayoutLock = new Object();
170+
private Timer failedPayoutTimer;
171+
private int failedPayoutTimerDelay = 5; // minimum delay before handling failed payout tx in minutes
172+
private boolean handleFailedPayoutTxOnNextPoll = false;
173+
167174
///////////////////////////////////////////////////////////////////////////////////////////
168175
// Enums
169176
///////////////////////////////////////////////////////////////////////////////////////////
@@ -2909,36 +2916,33 @@ private void doPollWallet() {
29092916
}
29102917
setDepositTxs(txs);
29112918

2912-
// check if any outputs spent (observed on payout published)
2913-
boolean hasSpentOutput = false;
2914-
boolean hasFailedTx = false;
2919+
// update payout state
2920+
boolean hasValidPayout = false;
2921+
boolean hasFailedPayout = false;
29152922
for (MoneroTxWallet tx : txs) {
2916-
if (tx.isFailed()) {
2917-
hasFailedTx = true;
2923+
boolean isOutgoing = !Boolean.TRUE.equals(tx.isIncoming()); // outgoing tx observed after wallet submits payout or on first confirmation
2924+
if (isOutgoing) {
2925+
if (tx.isFailed()) hasFailedPayout = true;
2926+
else {
2927+
hasValidPayout = true;
2928+
updatePayout(tx);
2929+
setPayoutStatePublished();
2930+
if (tx.isConfirmed()) setPayoutStateConfirmed();
2931+
if (!tx.isLocked()) setPayoutStateUnlocked();
2932+
if (tx.getNumConfirmations() != null && tx.getNumConfirmations() >= NUM_BLOCKS_PAYOUT_FINALIZED) setPayoutStateFinalized();
2933+
}
29182934
} else {
29192935
for (MoneroOutputWallet output : tx.getOutputsWallet()) {
2920-
if (Boolean.TRUE.equals(output.isSpent())) hasSpentOutput = true;
2936+
if (Boolean.TRUE.equals(output.isSpent())) hasValidPayout = true; // spent outputs observed on payout published (after rescanning)
29212937
}
29222938
}
29232939
}
2924-
if (hasSpentOutput) setPayoutStatePublished();
2925-
else if (hasFailedTx && isPayoutPublished()) {
2926-
log.warn("{} {} is in payout published state but has failed tx and no spent outputs, resetting payout state to unpublished", getClass().getSimpleName(), getShortId());
2927-
ThreadUtils.execute(() -> {
2928-
setPayoutState(PayoutState.PAYOUT_UNPUBLISHED);
2929-
onPayoutError(false, isSeller());
2930-
}, getId());
2931-
}
29322940

2933-
// check for outgoing txs (appears after wallet submits payout tx or on payout confirmed)
2934-
for (MoneroTxWallet tx : txs) {
2935-
if (tx.isOutgoing() && !tx.isFailed()) {
2936-
updatePayout(tx);
2937-
setPayoutStatePublished();
2938-
if (tx.isConfirmed()) setPayoutStateConfirmed();
2939-
if (!tx.isLocked()) setPayoutStateUnlocked();
2940-
if (tx.getNumConfirmations() != null && tx.getNumConfirmations() >= NUM_BLOCKS_PAYOUT_FINALIZED) setPayoutStateFinalized();
2941-
}
2941+
// handle payout validity
2942+
if (hasValidPayout) {
2943+
onValidPayoutTxPoll();
2944+
} else if (hasFailedPayout) {
2945+
onFailedPayoutTxPoll();
29422946
}
29432947
}
29442948
} catch (Exception e) {
@@ -2964,8 +2968,47 @@ else if (hasFailedTx && isPayoutPublished()) {
29642968
}
29652969
}
29662970

2967-
public boolean onPayoutError(boolean syncAndPoll) {
2968-
return onPayoutError(syncAndPoll, false);
2971+
private void onValidPayoutTxPoll() {
2972+
setPayoutStatePublished();
2973+
synchronized (failedPayoutLock) {
2974+
handleFailedPayoutTxOnNextPoll = false;
2975+
if (failedPayoutTimer == null) return;
2976+
log.warn("Received valid payout after previous payout failure for {} {}. Unscheduling failed payout tx handling", getClass().getSimpleName(), getId());
2977+
failedPayoutTimer.stop();
2978+
failedPayoutTimer = null;
2979+
}
2980+
}
2981+
2982+
private void onFailedPayoutTxPoll() {
2983+
if (!isPayoutPublished()) return; // ignore if payout unpublished
2984+
log.warn("The payout tx has failed for {} {} with payout state {}", getClass().getSimpleName(), getShortId(), getPayoutState());
2985+
synchronized (failedPayoutLock) {
2986+
2987+
// handle failed payout tx if previously set
2988+
if (handleFailedPayoutTxOnNextPoll) {
2989+
handleFailedPayoutTxOnNextPoll = false;
2990+
ThreadUtils.execute(() -> handleFailedPayoutTx(), getId());
2991+
} else {
2992+
if (failedPayoutTimer != null) return;
2993+
log.warn("Scheduling failed payout tx handling for {} {}", getClass().getSimpleName(), getShortId());
2994+
failedPayoutTimer = UserThread.runAfter(() -> {
2995+
ThreadUtils.execute(() -> {
2996+
synchronized (failedPayoutLock) {
2997+
if (failedPayoutTimer == null) return;
2998+
failedPayoutTimer.stop();
2999+
failedPayoutTimer = null;
3000+
handleFailedPayoutTxOnNextPoll = true; // handle failed payout tx on next poll in case we're outdated
3001+
}
3002+
}, getId());
3003+
}, failedPayoutTimerDelay, TimeUnit.MINUTES);
3004+
}
3005+
}
3006+
}
3007+
3008+
private void handleFailedPayoutTx() {
3009+
log.warn("Handling failed payout tx for {} {}", getClass().getSimpleName(), getId());
3010+
setPayoutState(PayoutState.PAYOUT_UNPUBLISHED);
3011+
onPayoutError(false, isSeller() && getProtocol().isAutoMarkPaymentReceived());
29693012
}
29703013

29713014
/**
@@ -2997,11 +3040,12 @@ public boolean onPayoutError(boolean syncAndPoll, boolean autoMarkPaymentReceive
29973040
// automatically mark payment received
29983041
if (autoMarkPaymentReceived) {
29993042
if (!isSeller()) throw new IllegalArgumentException("Must be the seller to auto mark payment received for " + getClass().getSimpleName() + " " + getId());
3000-
log.warn("Auto confirming payment received for {} {} after failure", getClass().getSimpleName(), getId());
3043+
log.warn("Auto confirming payment received for {} {} after payout error", getClass().getSimpleName(), getId());
3044+
getProtocol().setAutoMarkPaymentReceived(false); // only auto mark payment received once until restart
30013045
((SellerProtocol) getProtocol()).onPaymentReceived(() -> {
3002-
log.info("Finished auto marking payment received on NACK for {} {}", getClass().getSimpleName(), getId());
3046+
log.info("Finished auto marking payment received on payout error for {} {}", getClass().getSimpleName(), getId());
30033047
}, (errorMessage) -> {
3004-
log.warn("Error auto marking payment received on NACK for {} {}: {}", getClass().getSimpleName(), getId(), errorMessage);
3048+
log.warn("Error auto marking payment received on payout error for {} {}: {}", getClass().getSimpleName(), getId(), errorMessage);
30053049
});
30063050
return true;
30073051
}

core/src/main/java/haveno/core/trade/protocol/TradeProtocol.java

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,8 @@
8787
import haveno.network.p2p.mailbox.MailboxMessage;
8888
import haveno.network.p2p.mailbox.MailboxMessageService;
8989
import haveno.network.p2p.messaging.DecryptedMailboxListener;
90+
import lombok.Getter;
91+
import lombok.Setter;
9092
import lombok.extern.slf4j.Slf4j;
9193
import org.fxmisc.easybind.EasyBind;
9294

@@ -118,7 +120,9 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D
118120
private int reprocessPaymentSentMessageCount;
119121
private int reprocessPaymentReceivedMessageCount;
120122
private boolean makerInitTradeRequestHasBeenNacked = false;
121-
private boolean autoMarkPaymentReceivedOnNack = true;
123+
@Getter
124+
@Setter
125+
private boolean autoMarkPaymentReceived = true;
122126

123127

124128
///////////////////////////////////////////////////////////////////////////////////////////
@@ -870,8 +874,7 @@ private void onAckMessageAux(AckMessage ackMessage, NodeAddress sender) {
870874
if (ackMessage.getUpdatedMultisigHex() != null) {
871875
trade.getBuyer().setUpdatedMultisigHex(ackMessage.getUpdatedMultisigHex());
872876
processModel.getTradeManager().persistNow(null);
873-
boolean autoResent = trade.onPayoutError(true, autoMarkPaymentReceivedOnNack);
874-
autoMarkPaymentReceivedOnNack = false;
877+
boolean autoResent = trade.onPayoutError(true, autoMarkPaymentReceived);
875878
if (autoResent) return; // skip remaining processing if auto resent
876879
}
877880
}
@@ -890,8 +893,7 @@ else if (peer == trade.getArbitrator()) {
890893
if (ackMessage.getUpdatedMultisigHex() != null) {
891894
trade.getArbitrator().setUpdatedMultisigHex(ackMessage.getUpdatedMultisigHex());
892895
processModel.getTradeManager().persistNow(null);
893-
boolean autoResent = trade.onPayoutError(true, autoMarkPaymentReceivedOnNack);
894-
autoMarkPaymentReceivedOnNack = false;
896+
boolean autoResent = trade.onPayoutError(true, autoMarkPaymentReceived);
895897
if (autoResent) return; // skip remaining processing if auto resent
896898
}
897899
}

0 commit comments

Comments
 (0)