diff --git a/core/src/main/java/haveno/core/trade/protocol/BuyerProtocol.java b/core/src/main/java/haveno/core/trade/protocol/BuyerProtocol.java index 7299b08e76..6481212281 100644 --- a/core/src/main/java/haveno/core/trade/protocol/BuyerProtocol.java +++ b/core/src/main/java/haveno/core/trade/protocol/BuyerProtocol.java @@ -53,9 +53,9 @@ public class BuyerProtocol extends DisputeProtocol { protected void onInitialized() { super.onInitialized(); - // re-send payment sent message if not arrived + // re-send payment sent message if not acked synchronized (trade) { - if (trade.getState().ordinal() >= Trade.State.BUYER_SENT_PAYMENT_SENT_MSG.ordinal() && trade.getState().ordinal() <= Trade.State.BUYER_SEND_FAILED_PAYMENT_SENT_MSG.ordinal()) { + if (trade.getState().ordinal() >= Trade.State.BUYER_SENT_PAYMENT_SENT_MSG.ordinal() && trade.getState().ordinal() < Trade.State.SELLER_RECEIVED_PAYMENT_SENT_MSG.ordinal()) { latchTrade(); given(anyPhase(Trade.Phase.PAYMENT_SENT) .with(BuyerEvent.STARTUP)) diff --git a/core/src/main/java/haveno/core/trade/protocol/SellerProtocol.java b/core/src/main/java/haveno/core/trade/protocol/SellerProtocol.java index d45576fb2b..c6802aa3e6 100644 --- a/core/src/main/java/haveno/core/trade/protocol/SellerProtocol.java +++ b/core/src/main/java/haveno/core/trade/protocol/SellerProtocol.java @@ -49,9 +49,9 @@ public class SellerProtocol extends DisputeProtocol { protected void onInitialized() { super.onInitialized(); - // re-send payment received message if not arrived + // re-send payment received message if payout not published synchronized (trade) { - if (trade.getState().ordinal() >= Trade.State.SELLER_SENT_PAYMENT_RECEIVED_MSG.ordinal() && trade.getState().ordinal() <= Trade.State.SELLER_SEND_FAILED_PAYMENT_RECEIVED_MSG.ordinal()) { + if (trade.getState().ordinal() >= Trade.State.SELLER_SENT_PAYMENT_RECEIVED_MSG.ordinal() && !trade.isPayoutPublished()) { latchTrade(); given(anyPhase(Trade.Phase.PAYMENT_RECEIVED) .with(SellerEvent.STARTUP)) diff --git a/core/src/main/java/haveno/core/trade/protocol/tasks/BuyerSendPaymentSentMessage.java b/core/src/main/java/haveno/core/trade/protocol/tasks/BuyerSendPaymentSentMessage.java index 93ce52b486..5d52805a11 100644 --- a/core/src/main/java/haveno/core/trade/protocol/tasks/BuyerSendPaymentSentMessage.java +++ b/core/src/main/java/haveno/core/trade/protocol/tasks/BuyerSendPaymentSentMessage.java @@ -17,7 +17,10 @@ package haveno.core.trade.protocol.tasks; +import java.util.concurrent.TimeUnit; + import haveno.common.Timer; +import haveno.common.UserThread; import haveno.common.crypto.PubKeyRing; import haveno.common.taskrunner.TaskRunner; import haveno.core.network.MessageState; @@ -45,6 +48,9 @@ import lombok.extern.slf4j.Slf4j; public abstract class BuyerSendPaymentSentMessage extends SendMailboxMessageTask { private ChangeListener listener; private Timer timer; + private static final int MAX_RESEND_ATTEMPTS = 10; + private int delayInMin = 10; + private int resendCounter = 0; public BuyerSendPaymentSentMessage(TaskRunner taskHandler, Trade trade) { super(taskHandler, trade); @@ -61,8 +67,6 @@ public abstract class BuyerSendPaymentSentMessage extends SendMailboxMessageTask super.run(); } catch (Throwable t) { failed(t); - } finally { - cleanup(); } } @@ -105,22 +109,21 @@ public abstract class BuyerSendPaymentSentMessage extends SendMailboxMessageTask @Override protected void setStateSent() { - if (trade.getState().ordinal() < Trade.State.BUYER_SENT_PAYMENT_SENT_MSG.ordinal()) { - trade.setStateIfValidTransitionTo(Trade.State.BUYER_SENT_PAYMENT_SENT_MSG); - } + if (trade.getState().ordinal() < Trade.State.BUYER_SENT_PAYMENT_SENT_MSG.ordinal()) trade.setStateIfValidTransitionTo(Trade.State.BUYER_SENT_PAYMENT_SENT_MSG); + tryToSendAgainLater(); processModel.getTradeManager().requestPersistence(); } @Override protected void setStateArrived() { trade.setStateIfValidTransitionTo(Trade.State.BUYER_SAW_ARRIVED_PAYMENT_SENT_MSG); + processModel.getTradeManager().requestPersistence(); } @Override protected void setStateStoredInMailbox() { trade.setStateIfValidTransitionTo(Trade.State.BUYER_STORED_IN_MAILBOX_PAYMENT_SENT_MSG); processModel.getTradeManager().requestPersistence(); - // TODO: schedule repeat sending like haveno? } @Override @@ -137,4 +140,40 @@ public abstract class BuyerSendPaymentSentMessage extends SendMailboxMessageTask processModel.getPaymentSentMessageStateProperty().removeListener(listener); } } + + private void tryToSendAgainLater() { + + // skip if already acked + if (trade.getState().ordinal() >= Trade.State.SELLER_RECEIVED_PAYMENT_SENT_MSG.ordinal()) return; + + if (resendCounter >= MAX_RESEND_ATTEMPTS) { + cleanup(); + log.warn("We never received an ACK message when sending the PaymentSentMessage to the peer. We stop trying to send the message."); + return; + } + + log.info("We will send the message again to the peer after a delay of {} min.", delayInMin); + if (timer != null) { + timer.stop(); + } + + timer = UserThread.runAfter(this::run, delayInMin, TimeUnit.MINUTES); + + if (resendCounter == 0) { + listener = (observable, oldValue, newValue) -> onMessageStateChange(newValue); + processModel.getPaymentSentMessageStateProperty().addListener(listener); + onMessageStateChange(processModel.getPaymentSentMessageStateProperty().get()); + } + + delayInMin = delayInMin * 2; + resendCounter++; + } + + private void onMessageStateChange(MessageState newValue) { + if (newValue == MessageState.ACKNOWLEDGED) { + trade.setStateIfValidTransitionTo(Trade.State.SELLER_RECEIVED_PAYMENT_SENT_MSG); + processModel.getTradeManager().requestPersistence(); + cleanup(); + } + } } diff --git a/desktop/src/main/java/haveno/desktop/main/portfolio/pendingtrades/steps/buyer/BuyerStep2View.java b/desktop/src/main/java/haveno/desktop/main/portfolio/pendingtrades/steps/buyer/BuyerStep2View.java index e29ec8240e..1bdacc8078 100644 --- a/desktop/src/main/java/haveno/desktop/main/portfolio/pendingtrades/steps/buyer/BuyerStep2View.java +++ b/desktop/src/main/java/haveno/desktop/main/portfolio/pendingtrades/steps/buyer/BuyerStep2View.java @@ -149,43 +149,43 @@ public class BuyerStep2View extends TradeStepView { if (trade.isDepositsUnlocked() && !trade.isPaymentSent()) { showPopup(); - } else if (state.ordinal() <= Trade.State.BUYER_SEND_FAILED_PAYMENT_SENT_MSG.ordinal()) { + } else if (state.ordinal() <= Trade.State.SELLER_RECEIVED_PAYMENT_SENT_MSG.ordinal()) { switch (state) { - case BUYER_CONFIRMED_IN_UI_PAYMENT_SENT: - busyAnimation.play(); - statusLabel.setText(Res.get("shared.preparingConfirmation")); - break; - case BUYER_SENT_PAYMENT_SENT_MSG: - busyAnimation.play(); - statusLabel.setText(Res.get("shared.sendingConfirmation")); - model.setMessageStateProperty(MessageState.SENT); - timeoutTimer = UserThread.runAfter(() -> { + case BUYER_CONFIRMED_IN_UI_PAYMENT_SENT: + busyAnimation.play(); + statusLabel.setText(Res.get("shared.preparingConfirmation")); + break; + case BUYER_SENT_PAYMENT_SENT_MSG: + busyAnimation.play(); + statusLabel.setText(Res.get("shared.sendingConfirmation")); + model.setMessageStateProperty(MessageState.SENT); + timeoutTimer = UserThread.runAfter(() -> { + busyAnimation.stop(); + statusLabel.setText(Res.get("shared.sendingConfirmationAgain")); + }, 10); + break; + case BUYER_STORED_IN_MAILBOX_PAYMENT_SENT_MSG: + busyAnimation.stop(); + statusLabel.setText(Res.get("shared.messageStoredInMailbox")); + model.setMessageStateProperty(MessageState.STORED_IN_MAILBOX); + break; + case SELLER_RECEIVED_PAYMENT_SENT_MSG: + busyAnimation.stop(); + statusLabel.setText(Res.get("shared.messageArrived")); + model.setMessageStateProperty(MessageState.ARRIVED); + break; + case BUYER_SEND_FAILED_PAYMENT_SENT_MSG: + // We get a popup and the trade closed, so we dont need to show anything here + busyAnimation.stop(); + statusLabel.setText(""); + model.setMessageStateProperty(MessageState.FAILED); + break; + default: + log.warn("Unexpected case: State={}, tradeId={} ", state.name(), trade.getId()); busyAnimation.stop(); statusLabel.setText(Res.get("shared.sendingConfirmationAgain")); - }, 10); - break; - case BUYER_SAW_ARRIVED_PAYMENT_SENT_MSG: - busyAnimation.stop(); - statusLabel.setText(Res.get("shared.messageArrived")); - model.setMessageStateProperty(MessageState.ARRIVED); - break; - case BUYER_STORED_IN_MAILBOX_PAYMENT_SENT_MSG: - busyAnimation.stop(); - statusLabel.setText(Res.get("shared.messageStoredInMailbox")); - model.setMessageStateProperty(MessageState.STORED_IN_MAILBOX); - break; - case BUYER_SEND_FAILED_PAYMENT_SENT_MSG: - // We get a popup and the trade closed, so we dont need to show anything here - busyAnimation.stop(); - statusLabel.setText(""); - model.setMessageStateProperty(MessageState.FAILED); - break; - default: - log.warn("Unexpected case: State={}, tradeId={} ", state.name(), trade.getId()); - busyAnimation.stop(); - statusLabel.setText(Res.get("shared.sendingConfirmationAgain")); - break; - } + break; + } } }); }