diff --git a/core/src/main/java/bisq/core/btc/listeners/OutputSpendConfidenceListener.java b/core/src/main/java/bisq/core/btc/listeners/OutputSpendConfidenceListener.java
new file mode 100644
index 00000000000..08e73787358
--- /dev/null
+++ b/core/src/main/java/bisq/core/btc/listeners/OutputSpendConfidenceListener.java
@@ -0,0 +1,34 @@
+/*
+ * This file is part of Bisq.
+ *
+ * Bisq is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or (at
+ * your option) any later version.
+ *
+ * Bisq is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
+ * License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with Bisq. If not, see .
+ */
+
+package bisq.core.btc.listeners;
+
+import org.bitcoinj.core.TransactionConfidence;
+import org.bitcoinj.core.TransactionOutput;
+
+import lombok.Getter;
+
+public abstract class OutputSpendConfidenceListener {
+ @Getter
+ private final TransactionOutput output;
+
+ public OutputSpendConfidenceListener(TransactionOutput output) {
+ this.output = output;
+ }
+
+ public abstract void onOutputSpendConfidenceChanged(TransactionConfidence confidence);
+}
diff --git a/core/src/main/java/bisq/core/btc/wallet/WalletService.java b/core/src/main/java/bisq/core/btc/wallet/WalletService.java
index 12850070ee7..8b5ee65db3b 100644
--- a/core/src/main/java/bisq/core/btc/wallet/WalletService.java
+++ b/core/src/main/java/bisq/core/btc/wallet/WalletService.java
@@ -21,6 +21,7 @@
import bisq.core.btc.exceptions.WalletException;
import bisq.core.btc.listeners.AddressConfidenceListener;
import bisq.core.btc.listeners.BalanceListener;
+import bisq.core.btc.listeners.OutputSpendConfidenceListener;
import bisq.core.btc.listeners.TxConfidenceListener;
import bisq.core.btc.setup.WalletsSetup;
import bisq.core.btc.wallet.http.MemPoolSpaceTxBroadcaster;
@@ -125,6 +126,7 @@ public abstract class WalletService {
private final BisqWalletListener walletEventListener = new BisqWalletListener();
private final CopyOnWriteArraySet addressConfidenceListeners = new CopyOnWriteArraySet<>();
private final CopyOnWriteArraySet txConfidenceListeners = new CopyOnWriteArraySet<>();
+ private final CopyOnWriteArraySet spendConfidenceListeners = new CopyOnWriteArraySet<>();
private final CopyOnWriteArraySet balanceListeners = new CopyOnWriteArraySet<>();
private final WalletChangeEventListener cacheInvalidationListener;
private final AtomicReference> txOutputAddressCache = new AtomicReference<>();
@@ -228,6 +230,14 @@ public void removeTxConfidenceListener(TxConfidenceListener listener) {
txConfidenceListeners.remove(listener);
}
+ public void addSpendConfidenceListener(OutputSpendConfidenceListener listener) {
+ spendConfidenceListeners.add(listener);
+ }
+
+ public void removeSpendConfidenceListener(OutputSpendConfidenceListener listener) {
+ spendConfidenceListeners.remove(listener);
+ }
+
public void addBalanceListener(BalanceListener listener) {
balanceListeners.add(listener);
}
@@ -446,29 +456,28 @@ public void broadcastTx(Transaction tx, TxBroadcaster.Callback callback, int tim
@Nullable
public TransactionConfidence getConfidenceForAddress(Address address) {
- List transactionConfidenceList = new ArrayList<>();
if (wallet != null) {
Set transactions = getAddressToMatchingTxSetMultimap().get(address);
- transactionConfidenceList.addAll(transactions.stream().map(tx ->
- getTransactionConfidence(tx, address)).collect(Collectors.toList()));
+ return getMostRecentConfidence(transactions.stream()
+ .map(tx -> getTransactionConfidence(tx, address))
+ .collect(Collectors.toList()));
}
- return getMostRecentConfidence(transactionConfidenceList);
+ return null;
}
@Nullable
public TransactionConfidence getConfidenceForAddressFromBlockHeight(Address address, long targetHeight) {
- List transactionConfidenceList = new ArrayList<>();
if (wallet != null) {
Set transactions = getAddressToMatchingTxSetMultimap().get(address);
// "acceptable confidence" is either a new (pending) Tx, or a Tx confirmed after target block height
- transactionConfidenceList.addAll(transactions.stream()
+ return getMostRecentConfidence(transactions.stream()
.map(tx -> getTransactionConfidence(tx, address))
.filter(Objects::nonNull)
.filter(con -> con.getConfidenceType() == PENDING ||
(con.getConfidenceType() == BUILDING && con.getAppearedAtChainHeight() > targetHeight))
.collect(Collectors.toList()));
}
- return getMostRecentConfidence(transactionConfidenceList);
+ return null;
}
private SetMultimap getAddressToMatchingTxSetMultimap() {
@@ -500,7 +509,7 @@ public TransactionConfidence getConfidenceForTxId(@Nullable String txId) {
}
@Nullable
- private TransactionConfidence getTransactionConfidence(Transaction tx, Address address) {
+ private static TransactionConfidence getTransactionConfidence(Transaction tx, Address address) {
List transactionConfidenceList = getOutputsWithConnectedOutputs(tx).stream()
.filter(output -> address != null && address.equals(getAddressFromOutput(output)))
.flatMap(o -> Stream.ofNullable(o.getParentTransaction()))
@@ -510,7 +519,7 @@ private TransactionConfidence getTransactionConfidence(Transaction tx, Address a
}
- private List getOutputsWithConnectedOutputs(Transaction tx) {
+ private static List getOutputsWithConnectedOutputs(Transaction tx) {
List transactionOutputs = tx.getOutputs();
List connectedOutputs = new ArrayList<>();
@@ -530,7 +539,7 @@ private List getOutputsWithConnectedOutputs(Transaction tx) {
}
@Nullable
- private TransactionConfidence getMostRecentConfidence(List transactionConfidenceList) {
+ private static TransactionConfidence getMostRecentConfidence(List transactionConfidenceList) {
TransactionConfidence transactionConfidence = null;
for (TransactionConfidence confidence : transactionConfidenceList) {
if (confidence != null) {
@@ -932,7 +941,7 @@ public void onCoinsSent(Wallet wallet, Transaction tx, Coin prevBalance, Coin ne
@Override
public void onReorganize(Wallet wallet) {
- log.warn("onReorganize ");
+ log.warn("onReorganize");
}
@Override
@@ -941,13 +950,20 @@ public void onTransactionConfidenceChanged(Wallet wallet, Transaction tx) {
TransactionConfidence confidence = getTransactionConfidence(tx, addressConfidenceListener.getAddress());
addressConfidenceListener.onTransactionConfidenceChanged(confidence);
}
- txConfidenceListeners.stream()
- .filter(txConfidenceListener -> tx != null &&
- tx.getTxId().toString() != null &&
- txConfidenceListener != null &&
- tx.getTxId().toString().equals(txConfidenceListener.getTxId()))
- .forEach(txConfidenceListener ->
- txConfidenceListener.onTransactionConfidenceChanged(tx.getConfidence()));
+ for (OutputSpendConfidenceListener listener : spendConfidenceListeners) {
+ TransactionInput spentBy = listener.getOutput().getSpentBy();
+ if (spentBy != null && tx.equals(spentBy.getParentTransaction())) {
+ listener.onOutputSpendConfidenceChanged(tx.getConfidence());
+ }
+ }
+ if (!txConfidenceListeners.isEmpty()) {
+ String txId = tx.getTxId().toString();
+ for (TxConfidenceListener listener : txConfidenceListeners) {
+ if (txId.equals(listener.getTxId())) {
+ listener.onTransactionConfidenceChanged(tx.getConfidence());
+ }
+ }
+ }
}
void notifyBalanceListeners(Transaction tx) {
diff --git a/core/src/main/java/bisq/core/trade/protocol/bisq_v5/BaseBuyerProtocol_v5.java b/core/src/main/java/bisq/core/trade/protocol/bisq_v5/BaseBuyerProtocol_v5.java
index a89700b1cc2..0d7056f0256 100644
--- a/core/src/main/java/bisq/core/trade/protocol/bisq_v5/BaseBuyerProtocol_v5.java
+++ b/core/src/main/java/bisq/core/trade/protocol/bisq_v5/BaseBuyerProtocol_v5.java
@@ -19,6 +19,7 @@
import bisq.core.trade.model.bisq_v1.BuyerTrade;
import bisq.core.trade.model.bisq_v1.Trade;
+import bisq.core.trade.model.bisq_v1.Trade.Phase;
import bisq.core.trade.protocol.BuyerProtocol;
import bisq.core.trade.protocol.FluentProtocol;
import bisq.core.trade.protocol.TradeMessage;
@@ -35,6 +36,7 @@
import bisq.core.trade.protocol.bisq_v1.tasks.buyer.BuyerSetupPayoutTxListener;
import bisq.core.trade.protocol.bisq_v1.tasks.buyer.BuyerSignPayoutTx;
import bisq.core.trade.protocol.bisq_v5.messages.DepositTxAndSellerPaymentAccountMessage;
+import bisq.core.trade.protocol.bisq_v5.tasks.SetupWarningTxListener;
import bisq.core.trade.protocol.bisq_v5.tasks.buyer.BuyerProcessDepositTxAndSellerPaymentAccountMessage;
import bisq.network.p2p.NodeAddress;
@@ -64,17 +66,23 @@ protected void onInitialized() {
super.onInitialized();
// We get called the constructor with any possible state and phase. As we don't want to log an error for such
// cases we use the alternative 'given' method instead of 'expect'.
- given(phase(Trade.Phase.TAKER_FEE_PUBLISHED)
+ given(phase(Phase.TAKER_FEE_PUBLISHED)
.with(BuyerEvent.STARTUP))
.setup(tasks(BuyerSetupDepositTxListener.class))
.executeTasks();
- given(anyPhase(Trade.Phase.FIAT_SENT, Trade.Phase.FIAT_RECEIVED)
+ given(anyPhase(Phase.DEPOSIT_PUBLISHED, Phase.DEPOSIT_CONFIRMED, Phase.FIAT_SENT, Phase.FIAT_RECEIVED)
+ .preCondition(trade.hasV5Protocol()) // FIXME: If trade opened with v4 protocol, should use BaseBuyerProtocol_v4.
+ .with(BuyerEvent.STARTUP))
+ .setup(tasks(SetupWarningTxListener.class))
+ .executeTasks();
+
+ given(anyPhase(Phase.FIAT_SENT, Phase.FIAT_RECEIVED)
.with(BuyerEvent.STARTUP))
.setup(tasks(BuyerSetupPayoutTxListener.class))
.executeTasks();
- given(anyPhase(Trade.Phase.FIAT_SENT, Trade.Phase.FIAT_RECEIVED)
+ given(anyPhase(Phase.FIAT_SENT, Phase.FIAT_RECEIVED)
.anyState(Trade.State.BUYER_STORED_IN_MAILBOX_FIAT_PAYMENT_INITIATED_MSG,
Trade.State.BUYER_SEND_FAILED_FIAT_PAYMENT_INITIATED_MSG)
.with(BuyerEvent.STARTUP))
@@ -110,13 +118,13 @@ public void onMailboxMessage(TradeMessage message, NodeAddress peer) {
// mailbox message but the stored in mailbox case is not expected and the seller would try to send the message again
// in the hope to reach the buyer directly in case of network issues.
protected void handle(DepositTxAndSellerPaymentAccountMessage message, NodeAddress peer) {
- expect(anyPhase(Trade.Phase.TAKER_FEE_PUBLISHED, Trade.Phase.DEPOSIT_PUBLISHED)
+ expect(anyPhase(Phase.TAKER_FEE_PUBLISHED, Phase.DEPOSIT_PUBLISHED)
.with(message)
.from(peer)
.preCondition(trade.getDepositTx() == null || processModel.getTradePeer().getPaymentAccountPayload() == null,
() -> {
- log.warn("We received a DepositTxAndDelayedPayoutTxMessage but we have already processed the deposit and " +
- "delayed payout tx so we ignore the message. This can happen if the ACK message to the peer did not " +
+ log.warn("We received a DepositTxAndSellerPaymentAccountMessage but we have already processed the deposit tx and " +
+ "seller payment account so we ignore the message. This can happen if the ACK message to the peer did not " +
"arrive and the peer repeats sending us the message. We send another ACK msg.");
stopTimeout();
sendAckMessage(message, true, null);
@@ -124,6 +132,7 @@ protected void handle(DepositTxAndSellerPaymentAccountMessage message, NodeAddre
}))
.setup(tasks(BuyerProcessDepositTxAndSellerPaymentAccountMessage.class,
ApplyFilter.class,
+ SetupWarningTxListener.class,
VerifyPeersAccountAgeWitness.class,
BuyerSendsShareBuyerPaymentAccountMessage.class)
.using(new TradeTaskRunner(trade,
@@ -142,7 +151,7 @@ protected void handle(DepositTxAndSellerPaymentAccountMessage message, NodeAddre
@Override
public void onPaymentStarted(ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) {
BuyerEvent event = BuyerEvent.PAYMENT_SENT;
- expect(phase(Trade.Phase.DEPOSIT_CONFIRMED)
+ expect(phase(Phase.DEPOSIT_CONFIRMED)
.with(event)
.preCondition(trade.confirmPermitted()))
.setup(tasks(ApplyFilter.class,
@@ -171,7 +180,7 @@ public void onPaymentStarted(ResultHandler resultHandler, ErrorMessageHandler er
///////////////////////////////////////////////////////////////////////////////////////////
protected void handle(PayoutTxPublishedMessage message, NodeAddress peer) {
- expect(anyPhase(Trade.Phase.FIAT_SENT, Trade.Phase.PAYOUT_PUBLISHED)
+ expect(anyPhase(Phase.FIAT_SENT, Phase.PAYOUT_PUBLISHED)
.with(message)
.from(peer))
.setup(tasks(BuyerProcessPayoutTxPublishedMessage.class))
diff --git a/core/src/main/java/bisq/core/trade/protocol/bisq_v5/BaseSellerProtocol_v5.java b/core/src/main/java/bisq/core/trade/protocol/bisq_v5/BaseSellerProtocol_v5.java
index c55ec9cebf0..92f71fb4226 100644
--- a/core/src/main/java/bisq/core/trade/protocol/bisq_v5/BaseSellerProtocol_v5.java
+++ b/core/src/main/java/bisq/core/trade/protocol/bisq_v5/BaseSellerProtocol_v5.java
@@ -19,6 +19,7 @@
import bisq.core.trade.model.bisq_v1.SellerTrade;
import bisq.core.trade.model.bisq_v1.Trade;
+import bisq.core.trade.model.bisq_v1.Trade.Phase;
import bisq.core.trade.protocol.FluentProtocol;
import bisq.core.trade.protocol.SellerProtocol;
import bisq.core.trade.protocol.TradeMessage;
@@ -38,6 +39,7 @@
import bisq.core.trade.protocol.bisq_v1.tasks.seller.SellerSignAndFinalizePayoutTx;
import bisq.core.trade.protocol.bisq_v5.messages.PreparedTxBuyerSignaturesMessage;
import bisq.core.trade.protocol.bisq_v5.tasks.AddWatchedScriptsToWallet;
+import bisq.core.trade.protocol.bisq_v5.tasks.SetupWarningTxListener;
import bisq.core.trade.protocol.bisq_v5.tasks.seller.SellerProcessPreparedTxBuyerSignaturesMessage;
import bisq.core.trade.protocol.bisq_v5.tasks.seller.SellerSendsDepositTxAndSellerPaymentAccountMessage;
@@ -55,10 +57,26 @@ enum SellerEvent implements FluentProtocol.Event {
PAYMENT_RECEIVED
}
+ ///////////////////////////////////////////////////////////////////////////////////////////
+ // Constructor
+ ///////////////////////////////////////////////////////////////////////////////////////////
+
protected BaseSellerProtocol_v5(SellerTrade trade) {
super(trade);
}
+ @Override
+ protected void onInitialized() {
+ super.onInitialized();
+ // We get called the constructor with any possible state and phase. As we don't want to log an error for such
+ // cases we use the alternative 'given' method instead of 'expect'.
+ given(anyPhase(Phase.DEPOSIT_PUBLISHED, Phase.DEPOSIT_CONFIRMED, Phase.FIAT_SENT, Phase.FIAT_RECEIVED)
+ .preCondition(trade.hasV5Protocol()) // FIXME: If trade opened with v4 protocol, should use BaseSellerProtocol_v4.
+ .with(SellerEvent.STARTUP))
+ .setup(tasks(SetupWarningTxListener.class))
+ .executeTasks();
+ }
+
///////////////////////////////////////////////////////////////////////////////////////////
// Mailbox
@@ -79,19 +97,20 @@ public void onMailboxMessage(TradeMessage message, NodeAddress peerNodeAddress)
///////////////////////////////////////////////////////////////////////////////////////////
protected void handle(PreparedTxBuyerSignaturesMessage message, NodeAddress peer) {
- expect(phase(Trade.Phase.TAKER_FEE_PUBLISHED)
+ expect(phase(Phase.TAKER_FEE_PUBLISHED)
.with(message)
.from(peer))
.setup(tasks(SellerProcessPreparedTxBuyerSignaturesMessage.class,
AddWatchedScriptsToWallet.class,
SellerSendsDepositTxAndSellerPaymentAccountMessage.class,
SellerPublishesDepositTx.class,
+ SetupWarningTxListener.class,
SellerPublishesTradeStatistics.class))
.executeTasks();
}
protected void handle(ShareBuyerPaymentAccountMessage message, NodeAddress peer) {
- expect(anyPhase(Trade.Phase.TAKER_FEE_PUBLISHED, Trade.Phase.DEPOSIT_PUBLISHED, Trade.Phase.DEPOSIT_CONFIRMED)
+ expect(anyPhase(Phase.TAKER_FEE_PUBLISHED, Phase.DEPOSIT_PUBLISHED, Phase.DEPOSIT_CONFIRMED)
.with(message)
.from(peer))
.setup(tasks(SellerProcessShareBuyerPaymentAccountMessage.class,
@@ -117,7 +136,7 @@ protected void handle(CounterCurrencyTransferStartedMessage message, NodeAddress
// a mailbox message with CounterCurrencyTransferStartedMessage.
// TODO A better fix would be to add a listener for the wallet sync state and process
// the mailbox msg once wallet is ready and trade state set.
- expect(anyPhase(Trade.Phase.DEPOSIT_CONFIRMED, Trade.Phase.DEPOSIT_PUBLISHED)
+ expect(anyPhase(Phase.DEPOSIT_CONFIRMED, Phase.DEPOSIT_PUBLISHED)
.with(message)
.from(peer)
.preCondition(trade.getPayoutTx() == null,
@@ -143,7 +162,7 @@ protected void handle(CounterCurrencyTransferStartedMessage message, NodeAddress
@Override
public void onPaymentReceived(ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) {
SellerEvent event = SellerEvent.PAYMENT_RECEIVED;
- expect(anyPhase(Trade.Phase.FIAT_SENT, Trade.Phase.PAYOUT_PUBLISHED)
+ expect(anyPhase(Phase.FIAT_SENT, Phase.PAYOUT_PUBLISHED)
.with(event)
.preCondition(trade.confirmPermitted()))
.setup(tasks(
diff --git a/core/src/main/java/bisq/core/trade/protocol/bisq_v5/tasks/SetupWarningTxListener.java b/core/src/main/java/bisq/core/trade/protocol/bisq_v5/tasks/SetupWarningTxListener.java
new file mode 100644
index 00000000000..1740ed28588
--- /dev/null
+++ b/core/src/main/java/bisq/core/trade/protocol/bisq_v5/tasks/SetupWarningTxListener.java
@@ -0,0 +1,109 @@
+/*
+ * This file is part of Bisq.
+ *
+ * Bisq is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or (at
+ * your option) any later version.
+ *
+ * Bisq is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
+ * License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with Bisq. If not, see .
+ */
+
+package bisq.core.trade.protocol.bisq_v5.tasks;
+
+import bisq.core.btc.listeners.OutputSpendConfidenceListener;
+import bisq.core.btc.wallet.BtcWalletService;
+import bisq.core.trade.model.bisq_v1.Trade;
+import bisq.core.trade.protocol.bisq_v1.tasks.TradeTask;
+
+import bisq.common.taskrunner.TaskRunner;
+
+import org.bitcoinj.core.Transaction;
+import org.bitcoinj.core.TransactionConfidence;
+import org.bitcoinj.core.TransactionInput;
+import org.bitcoinj.core.TransactionOutput;
+
+import java.util.Objects;
+
+import lombok.extern.slf4j.Slf4j;
+
+@Slf4j
+public class SetupWarningTxListener extends TradeTask {
+ private OutputSpendConfidenceListener confidenceListener;
+
+ public SetupWarningTxListener(TaskRunner taskHandler, Trade trade) {
+ super(taskHandler, trade);
+ }
+
+ @Override
+ protected void run() {
+ try {
+ runInterceptHook();
+ Transaction depositTx = trade.getDepositTx();
+ if (depositTx != null && isFundsUnreleased()) {
+ BtcWalletService walletService = processModel.getBtcWalletService();
+ TransactionOutput depositTxOutput = depositTx.getOutput(0);
+
+ TransactionInput spentBy = depositTxOutput.getSpentBy();
+ TransactionConfidence confidence = spentBy != null ? spentBy.getParentTransaction().getConfidence() : null;
+ if (isMatchingTxInNetwork(confidence)) {
+ applyConfidence(confidence);
+ } else {
+ confidenceListener = new OutputSpendConfidenceListener(depositTxOutput) {
+ @Override
+ public void onOutputSpendConfidenceChanged(TransactionConfidence confidence) {
+ if (isMatchingTxInNetwork(confidence)) {
+ applyConfidence(confidence);
+ }
+ }
+ };
+ walletService.addSpendConfidenceListener(confidenceListener);
+ }
+ }
+ complete();
+ } catch (Throwable t) {
+ failed(t);
+ }
+ }
+
+ private void applyConfidence(TransactionConfidence confidence) {
+ BtcWalletService walletService = processModel.getBtcWalletService();
+ Transaction warningTx = Objects.requireNonNull(walletService.getTransaction(confidence.getTransactionHash()));
+ Transaction myWarningTx = walletService.getTxFromSerializedTx(processModel.getFinalizedWarningTx());
+ BtcWalletService.printTx("warningTx received from network", warningTx);
+ if (isFundsUnreleased()) {
+ Trade.DisputeState newDisputeState = warningTx.equals(myWarningTx) ?
+ Trade.DisputeState.WARNING_SENT : Trade.DisputeState.WARNING_SENT_BY_PEER;
+ log.info("Setting dispute state to {} for tradeId {}.", newDisputeState, processModel.getOfferId());
+ trade.setDisputeState(newDisputeState);
+ processModel.getTradeManager().requestPersistence();
+ } else {
+ log.warn("Received warning tx but trade funds have already been released.");
+ }
+ if (confidenceListener != null) {
+ walletService.removeSpendConfidenceListener(confidenceListener);
+ confidenceListener = null;
+ }
+ }
+
+ private boolean isFundsUnreleased() {
+ return trade.isFundsLockedIn() || !trade.isDepositConfirmed() && !trade.getDisputeState().isArbitrated();
+ }
+
+ private boolean isMatchingTxInNetwork(TransactionConfidence confidence) {
+ if (confidence == null ||
+ !confidence.getConfidenceType().equals(TransactionConfidence.ConfidenceType.BUILDING) &&
+ !confidence.getConfidenceType().equals(TransactionConfidence.ConfidenceType.PENDING)) {
+ return false;
+ }
+ BtcWalletService btcWalletService = processModel.getBtcWalletService();
+ Transaction tx = Objects.requireNonNull(btcWalletService.getTransaction(confidence.getTransactionHash()));
+ return tx.getLockTime() != 0;
+ }
+}
diff --git a/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/PendingTradesDataModel.java b/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/PendingTradesDataModel.java
index c02144865bd..20f8c3d2d58 100644
--- a/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/PendingTradesDataModel.java
+++ b/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/PendingTradesDataModel.java
@@ -259,8 +259,10 @@ public void onSendWarning() {
log.error("Trade is null");
return;
}
- // TODO: What if the peer has already broadcast his warning tx? We need to detect that.
- trade.setDisputeState(Trade.DisputeState.WARNING_SENT);
+ if (trade.getDisputeState().isEscalated()) {
+ log.error("A warning tx has already been seen for trade");
+ return;
+ }
((DisputeProtocol) tradeManager.getTradeProtocol(trade)).onPublishWarningTx(
() -> log.info("Warning tx published"),
errorMessage -> new Popup().error(errorMessage).show());