Merge branch 'dispute-agent-branch' into wip-merge-tradeprot

# Conflicts:
#	core/src/main/java/bisq/core/trade/DelayedPayoutTxValidation.java
#	core/src/main/java/bisq/core/trade/protocol/tasks/buyer/BuyerVerifiesFinalDelayedPayoutTx.java
#	desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/PendingTradesDataModel.java
#	desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/steps/buyer/BuyerStep1View.java
#	desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/steps/buyer/BuyerStep2View.java
This commit is contained in:
chimp1984 2020-09-23 08:27:09 -05:00
commit 0fa45650b6
No known key found for this signature in database
GPG key ID: 9801B4EC591F90E3
31 changed files with 1550 additions and 488 deletions

View file

@ -73,6 +73,7 @@ import bisq.core.dao.state.model.governance.Vote;
import bisq.asset.Asset; import bisq.asset.Asset;
import bisq.common.config.Config;
import bisq.common.handlers.ErrorMessageHandler; import bisq.common.handlers.ErrorMessageHandler;
import bisq.common.handlers.ExceptionHandler; import bisq.common.handlers.ExceptionHandler;
import bisq.common.handlers.ResultHandler; import bisq.common.handlers.ResultHandler;
@ -95,9 +96,14 @@ import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.Optional; import java.util.Optional;
import java.util.Set; import java.util.Set;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Consumer; import java.util.function.Consumer;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
@ -423,10 +429,18 @@ public class DaoFacade implements DaoSetupService {
case RESULT: case RESULT:
break; break;
} }
return firstBlock; return firstBlock;
} }
public Map<Integer, Date> getBlockStartDateByCycleIndex() {
AtomicInteger index = new AtomicInteger();
Map<Integer, Date> map = new HashMap<>();
periodService.getCycles()
.forEach(cycle -> daoStateService.getBlockAtHeight(cycle.getHeightOfFirstBlock())
.ifPresent(block -> map.put(index.getAndIncrement(), new Date(block.getTime()))));
return map;
}
// Because last block in request and voting phases must not be used for making a tx as it will get confirmed in the // Because last block in request and voting phases must not be used for making a tx as it will get confirmed in the
// next block which would be already the next phase we hide that last block to the user and add it to the break. // next block which would be already the next phase we hide that last block to the user and add it to the break.
public int getLastBlockOfPhaseForDisplay(int height, DaoPhase.Phase phase) { public int getLastBlockOfPhaseForDisplay(int height, DaoPhase.Phase phase) {
@ -750,4 +764,32 @@ public class DaoFacade implements DaoSetupService {
long baseFactor = daoStateService.getParamValueAsCoin(Param.BONDED_ROLE_FACTOR, height).value; long baseFactor = daoStateService.getParamValueAsCoin(Param.BONDED_ROLE_FACTOR, height).value;
return requiredBondUnit * baseFactor; return requiredBondUnit * baseFactor;
} }
public Set<String> getAllPastParamValues(Param param) {
Set<String> set = new HashSet<>();
periodService.getCycles().forEach(cycle -> {
set.add(getParamValue(param, cycle.getHeightOfFirstBlock()));
});
return set;
}
public Set<String> getAllDonationAddresses() {
// We support any of the past addresses as well as in case the peer has not enabled the DAO or is out of sync we
// do not want to break validation.
Set<String> allPastParamValues = getAllPastParamValues(Param.RECIPIENT_BTC_ADDRESS);
// If Dao is deactivated we need to add the default address as getAllPastParamValues will not return us any.
if (allPastParamValues.isEmpty()) {
allPastParamValues.add(Param.RECIPIENT_BTC_ADDRESS.getDefaultValue());
}
if (Config.baseCurrencyNetwork().isMainnet()) {
// If Dao is deactivated we need to add the past addresses used as well.
// This list need to be updated once a new address gets defined.
allPastParamValues.add("3EtUWqsGThPtjwUczw27YCo6EWvQdaPUyp"); // burning man 2019
allPastParamValues.add("3A8Zc1XioE2HRzYfbb5P8iemCS72M6vRJV"); // burningman2
}
return allPastParamValues;
}
} }

View file

@ -44,6 +44,7 @@ import java.util.ArrayList;
import java.util.Date; import java.util.Date;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Optional;
import java.util.UUID;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import lombok.EqualsAndHashCode; import lombok.EqualsAndHashCode;
@ -103,6 +104,15 @@ public final class Dispute implements NetworkPayload {
@Nullable @Nullable
private String delayedPayoutTxId; private String delayedPayoutTxId;
// Added at v1.3.9
@Setter
@Nullable
private String donationAddressOfDelayedPayoutTx;
// We do not persist uid, it is only used by dispute agents to guarantee an uid.
@Setter
@Nullable
private transient String uid;
/////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////
// Constructor // Constructor
@ -192,6 +202,7 @@ public final class Dispute implements NetworkPayload {
this.supportType = supportType; this.supportType = supportType;
id = tradeId + "_" + traderId; id = tradeId + "_" + traderId;
uid = UUID.randomUUID().toString();
} }
@Override @Override
@ -228,6 +239,7 @@ public final class Dispute implements NetworkPayload {
Optional.ofNullable(supportType).ifPresent(result -> builder.setSupportType(SupportType.toProtoMessage(supportType))); Optional.ofNullable(supportType).ifPresent(result -> builder.setSupportType(SupportType.toProtoMessage(supportType)));
Optional.ofNullable(mediatorsDisputeResult).ifPresent(result -> builder.setMediatorsDisputeResult(mediatorsDisputeResult)); Optional.ofNullable(mediatorsDisputeResult).ifPresent(result -> builder.setMediatorsDisputeResult(mediatorsDisputeResult));
Optional.ofNullable(delayedPayoutTxId).ifPresent(result -> builder.setDelayedPayoutTxId(delayedPayoutTxId)); Optional.ofNullable(delayedPayoutTxId).ifPresent(result -> builder.setDelayedPayoutTxId(delayedPayoutTxId));
Optional.ofNullable(donationAddressOfDelayedPayoutTx).ifPresent(result -> builder.setDonationAddressOfDelayedPayoutTx(donationAddressOfDelayedPayoutTx));
return builder.build(); return builder.build();
} }
@ -271,6 +283,11 @@ public final class Dispute implements NetworkPayload {
dispute.setDelayedPayoutTxId(delayedPayoutTxId); dispute.setDelayedPayoutTxId(delayedPayoutTxId);
} }
String donationAddressOfDelayedPayoutTx = proto.getDonationAddressOfDelayedPayoutTx();
if (!donationAddressOfDelayedPayoutTx.isEmpty()) {
dispute.setDonationAddressOfDelayedPayoutTx(donationAddressOfDelayedPayoutTx);
}
return dispute; return dispute;
} }
@ -357,6 +374,7 @@ public final class Dispute implements NetworkPayload {
return "Dispute{" + return "Dispute{" +
"\n tradeId='" + tradeId + '\'' + "\n tradeId='" + tradeId + '\'' +
",\n id='" + id + '\'' + ",\n id='" + id + '\'' +
",\n uid='" + uid + '\'' +
",\n traderId=" + traderId + ",\n traderId=" + traderId +
",\n disputeOpenerIsBuyer=" + disputeOpenerIsBuyer + ",\n disputeOpenerIsBuyer=" + disputeOpenerIsBuyer +
",\n disputeOpenerIsMaker=" + disputeOpenerIsMaker + ",\n disputeOpenerIsMaker=" + disputeOpenerIsMaker +
@ -382,6 +400,7 @@ public final class Dispute implements NetworkPayload {
",\n supportType=" + supportType + ",\n supportType=" + supportType +
",\n mediatorsDisputeResult='" + mediatorsDisputeResult + '\'' + ",\n mediatorsDisputeResult='" + mediatorsDisputeResult + '\'' +
",\n delayedPayoutTxId='" + delayedPayoutTxId + '\'' + ",\n delayedPayoutTxId='" + delayedPayoutTxId + '\'' +
",\n donationAddressOfDelayedPayoutTx='" + donationAddressOfDelayedPayoutTx + '\'' +
"\n}"; "\n}";
} }
} }

View file

@ -21,6 +21,7 @@ import bisq.core.btc.setup.WalletsSetup;
import bisq.core.btc.wallet.BtcWalletService; import bisq.core.btc.wallet.BtcWalletService;
import bisq.core.btc.wallet.Restrictions; import bisq.core.btc.wallet.Restrictions;
import bisq.core.btc.wallet.TradeWalletService; import bisq.core.btc.wallet.TradeWalletService;
import bisq.core.dao.DaoFacade;
import bisq.core.locale.CurrencyUtil; import bisq.core.locale.CurrencyUtil;
import bisq.core.locale.Res; import bisq.core.locale.Res;
import bisq.core.monetary.Altcoin; import bisq.core.monetary.Altcoin;
@ -36,6 +37,7 @@ import bisq.core.support.dispute.messages.PeerOpenedDisputeMessage;
import bisq.core.support.messages.ChatMessage; import bisq.core.support.messages.ChatMessage;
import bisq.core.trade.Contract; import bisq.core.trade.Contract;
import bisq.core.trade.Trade; import bisq.core.trade.Trade;
import bisq.core.trade.TradeDataValidation;
import bisq.core.trade.TradeManager; import bisq.core.trade.TradeManager;
import bisq.core.trade.closed.ClosedTradableManager; import bisq.core.trade.closed.ClosedTradableManager;
@ -46,6 +48,8 @@ import bisq.network.p2p.SendMailboxMessageListener;
import bisq.common.UserThread; import bisq.common.UserThread;
import bisq.common.app.Version; import bisq.common.app.Version;
import bisq.common.config.Config;
import bisq.common.crypto.KeyRing;
import bisq.common.crypto.PubKeyRing; import bisq.common.crypto.PubKeyRing;
import bisq.common.handlers.FaultHandler; import bisq.common.handlers.FaultHandler;
import bisq.common.handlers.ResultHandler; import bisq.common.handlers.ResultHandler;
@ -58,14 +62,18 @@ import org.bitcoinj.utils.Fiat;
import javafx.beans.property.IntegerProperty; import javafx.beans.property.IntegerProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList; import javafx.collections.ObservableList;
import java.security.KeyPair;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Optional;
import java.util.UUID; import java.util.UUID;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import javax.annotation.Nullable; import javax.annotation.Nullable;
@ -81,7 +89,15 @@ public abstract class DisputeManager<T extends DisputeList<? extends DisputeList
protected final OpenOfferManager openOfferManager; protected final OpenOfferManager openOfferManager;
protected final PubKeyRing pubKeyRing; protected final PubKeyRing pubKeyRing;
protected final DisputeListService<T> disputeListService; protected final DisputeListService<T> disputeListService;
private final Config config;
private final PriceFeedService priceFeedService; private final PriceFeedService priceFeedService;
protected final DaoFacade daoFacade;
@Getter
protected final ObservableList<TradeDataValidation.ValidationException> validationExceptions =
FXCollections.observableArrayList();
@Getter
private final KeyPair signatureKeyPair;
/////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////
@ -95,8 +111,10 @@ public abstract class DisputeManager<T extends DisputeList<? extends DisputeList
TradeManager tradeManager, TradeManager tradeManager,
ClosedTradableManager closedTradableManager, ClosedTradableManager closedTradableManager,
OpenOfferManager openOfferManager, OpenOfferManager openOfferManager,
PubKeyRing pubKeyRing, DaoFacade daoFacade,
KeyRing keyRing,
DisputeListService<T> disputeListService, DisputeListService<T> disputeListService,
Config config,
PriceFeedService priceFeedService) { PriceFeedService priceFeedService) {
super(p2PService, walletsSetup); super(p2PService, walletsSetup);
@ -105,8 +123,11 @@ public abstract class DisputeManager<T extends DisputeList<? extends DisputeList
this.tradeManager = tradeManager; this.tradeManager = tradeManager;
this.closedTradableManager = closedTradableManager; this.closedTradableManager = closedTradableManager;
this.openOfferManager = openOfferManager; this.openOfferManager = openOfferManager;
this.pubKeyRing = pubKeyRing; this.daoFacade = daoFacade;
this.pubKeyRing = keyRing.getPubKeyRing();
signatureKeyPair = keyRing.getSignatureKeyPair();
this.disputeListService = disputeListService; this.disputeListService = disputeListService;
this.config = config;
this.priceFeedService = priceFeedService; this.priceFeedService = priceFeedService;
} }
@ -178,7 +199,7 @@ public abstract class DisputeManager<T extends DisputeList<? extends DisputeList
@Nullable @Nullable
public abstract NodeAddress getAgentNodeAddress(Dispute dispute); public abstract NodeAddress getAgentNodeAddress(Dispute dispute);
protected abstract Trade.DisputeState getDisputeState_StartedByPeer(); protected abstract Trade.DisputeState getDisputeStateStartedByPeer();
public abstract void cleanupDisputes(); public abstract void cleanupDisputes();
@ -209,7 +230,7 @@ public abstract class DisputeManager<T extends DisputeList<? extends DisputeList
return disputeListService.getNrOfDisputes(isBuyer, contract); return disputeListService.getNrOfDisputes(isBuyer, contract);
} }
private T getDisputeList() { protected T getDisputeList() {
return disputeListService.getDisputeList(); return disputeListService.getDisputeList();
} }
@ -241,6 +262,24 @@ public abstract class DisputeManager<T extends DisputeList<? extends DisputeList
tryApplyMessages(); tryApplyMessages();
cleanupDisputes(); cleanupDisputes();
ObservableList<Dispute> disputes = getDisputeList().getList();
disputes.forEach(dispute -> {
try {
TradeDataValidation.validateDonationAddress(dispute, dispute.getDonationAddressOfDelayedPayoutTx(), daoFacade);
TradeDataValidation.validateNodeAddress(dispute, dispute.getContract().getBuyerNodeAddress(), config);
TradeDataValidation.validateNodeAddress(dispute, dispute.getContract().getSellerNodeAddress(), config);
} catch (TradeDataValidation.AddressException | TradeDataValidation.NodeAddressException e) {
log.error(e.toString());
validationExceptions.add(e);
}
});
TradeDataValidation.testIfAnyDisputeTriedReplay(disputes,
disputeReplayException -> {
log.error(disputeReplayException.toString());
validationExceptions.add(disputeReplayException);
});
} }
public boolean isTrader(Dispute dispute) { public boolean isTrader(Dispute dispute) {
@ -308,9 +347,21 @@ public abstract class DisputeManager<T extends DisputeList<? extends DisputeList
} }
addMediationResultMessage(dispute); addMediationResultMessage(dispute);
try {
TradeDataValidation.validateDonationAddress(dispute.getDonationAddressOfDelayedPayoutTx(), daoFacade);
TradeDataValidation.testIfDisputeTriesReplay(dispute, disputeList.getList());
TradeDataValidation.validateNodeAddress(dispute, dispute.getContract().getBuyerNodeAddress(), config);
TradeDataValidation.validateNodeAddress(dispute, dispute.getContract().getSellerNodeAddress(), config);
} catch (TradeDataValidation.AddressException |
TradeDataValidation.DisputeReplayException |
TradeDataValidation.NodeAddressException e) {
log.error(e.toString());
validationExceptions.add(e);
}
} }
// not dispute requester receives that from dispute agent // Not-dispute-requester receives that msg from dispute agent
protected void onPeerOpenedDisputeMessage(PeerOpenedDisputeMessage peerOpenedDisputeMessage) { protected void onPeerOpenedDisputeMessage(PeerOpenedDisputeMessage peerOpenedDisputeMessage) {
T disputeList = getDisputeList(); T disputeList = getDisputeList();
if (disputeList == null) { if (disputeList == null) {
@ -320,14 +371,33 @@ public abstract class DisputeManager<T extends DisputeList<? extends DisputeList
String errorMessage = null; String errorMessage = null;
Dispute dispute = peerOpenedDisputeMessage.getDispute(); Dispute dispute = peerOpenedDisputeMessage.getDispute();
Optional<Trade> optionalTrade = tradeManager.getTradeById(dispute.getTradeId());
if (!optionalTrade.isPresent()) {
return;
}
Trade trade = optionalTrade.get();
try {
TradeDataValidation.validatePayoutTx(trade,
trade.getDelayedPayoutTx(),
dispute,
daoFacade,
btcWalletService);
} catch (TradeDataValidation.ValidationException e) {
// The peer sent us an invalid donation address. We do not return here as we don't want to break
// mediation/arbitration and log only the issue. The dispute agent will run validation as well and will get
// a popup displayed to react.
log.error("Donation address invalid. {}", e.toString());
}
if (!isAgent(dispute)) { if (!isAgent(dispute)) {
if (!disputeList.contains(dispute)) { if (!disputeList.contains(dispute)) {
Optional<Dispute> storedDisputeOptional = findDispute(dispute); Optional<Dispute> storedDisputeOptional = findDispute(dispute);
if (!storedDisputeOptional.isPresent()) { if (!storedDisputeOptional.isPresent()) {
dispute.setStorage(disputeListService.getStorage()); dispute.setStorage(disputeListService.getStorage());
disputeList.add(dispute); disputeList.add(dispute);
Optional<Trade> tradeOptional = tradeManager.getTradeById(dispute.getTradeId()); trade.setDisputeState(getDisputeStateStartedByPeer());
tradeOptional.ifPresent(trade -> trade.setDisputeState(getDisputeState_StartedByPeer()));
errorMessage = null; errorMessage = null;
} else { } else {
// valid case if both have opened a dispute and agent was not online. // valid case if both have opened a dispute and agent was not online.
@ -516,6 +586,7 @@ public abstract class DisputeManager<T extends DisputeList<? extends DisputeList
disputeFromOpener.isSupportTicket(), disputeFromOpener.isSupportTicket(),
disputeFromOpener.getSupportType()); disputeFromOpener.getSupportType());
dispute.setDelayedPayoutTxId(disputeFromOpener.getDelayedPayoutTxId()); dispute.setDelayedPayoutTxId(disputeFromOpener.getDelayedPayoutTxId());
dispute.setDonationAddressOfDelayedPayoutTx(disputeFromOpener.getDonationAddressOfDelayedPayoutTx());
Optional<Dispute> storedDisputeOptional = findDispute(dispute); Optional<Dispute> storedDisputeOptional = findDispute(dispute);
@ -609,7 +680,7 @@ public abstract class DisputeManager<T extends DisputeList<? extends DisputeList
} }
// dispute agent send result to trader // dispute agent send result to trader
public void sendDisputeResultMessage(DisputeResult disputeResult, Dispute dispute, String text) { public void sendDisputeResultMessage(DisputeResult disputeResult, Dispute dispute, String summaryText) {
T disputeList = getDisputeList(); T disputeList = getDisputeList();
if (disputeList == null) { if (disputeList == null) {
log.warn("disputes is null"); log.warn("disputes is null");
@ -621,7 +692,7 @@ public abstract class DisputeManager<T extends DisputeList<? extends DisputeList
dispute.getTradeId(), dispute.getTradeId(),
dispute.getTraderPubKeyRing().hashCode(), dispute.getTraderPubKeyRing().hashCode(),
false, false,
text, summaryText,
p2PService.getAddress()); p2PService.getAddress());
disputeResult.setChatMessage(chatMessage); disputeResult.setChatMessage(chatMessage);

View file

@ -25,6 +25,7 @@ import bisq.core.btc.wallet.BtcWalletService;
import bisq.core.btc.wallet.TradeWalletService; import bisq.core.btc.wallet.TradeWalletService;
import bisq.core.btc.wallet.TxBroadcaster; import bisq.core.btc.wallet.TxBroadcaster;
import bisq.core.btc.wallet.WalletService; import bisq.core.btc.wallet.WalletService;
import bisq.core.dao.DaoFacade;
import bisq.core.locale.Res; import bisq.core.locale.Res;
import bisq.core.offer.OpenOffer; import bisq.core.offer.OpenOffer;
import bisq.core.offer.OpenOfferManager; import bisq.core.offer.OpenOfferManager;
@ -53,6 +54,8 @@ import bisq.network.p2p.SendMailboxMessageListener;
import bisq.common.Timer; import bisq.common.Timer;
import bisq.common.UserThread; import bisq.common.UserThread;
import bisq.common.app.Version; import bisq.common.app.Version;
import bisq.common.config.Config;
import bisq.common.crypto.KeyRing;
import bisq.common.crypto.PubKeyRing; import bisq.common.crypto.PubKeyRing;
import org.bitcoinj.core.AddressFormatException; import org.bitcoinj.core.AddressFormatException;
@ -89,11 +92,13 @@ public final class ArbitrationManager extends DisputeManager<ArbitrationDisputeL
TradeManager tradeManager, TradeManager tradeManager,
ClosedTradableManager closedTradableManager, ClosedTradableManager closedTradableManager,
OpenOfferManager openOfferManager, OpenOfferManager openOfferManager,
PubKeyRing pubKeyRing, DaoFacade daoFacade,
KeyRing keyRing,
ArbitrationDisputeListService arbitrationDisputeListService, ArbitrationDisputeListService arbitrationDisputeListService,
Config config,
PriceFeedService priceFeedService) { PriceFeedService priceFeedService) {
super(p2PService, tradeWalletService, walletService, walletsSetup, tradeManager, closedTradableManager, super(p2PService, tradeWalletService, walletService, walletsSetup, tradeManager, closedTradableManager,
openOfferManager, pubKeyRing, arbitrationDisputeListService, priceFeedService); openOfferManager, daoFacade, keyRing, arbitrationDisputeListService, config, priceFeedService);
} }
@ -135,7 +140,7 @@ public final class ArbitrationManager extends DisputeManager<ArbitrationDisputeL
} }
@Override @Override
protected Trade.DisputeState getDisputeState_StartedByPeer() { protected Trade.DisputeState getDisputeStateStartedByPeer() {
return Trade.DisputeState.DISPUTE_STARTED_BY_PEER; return Trade.DisputeState.DISPUTE_STARTED_BY_PEER;
} }

View file

@ -20,6 +20,7 @@ package bisq.core.support.dispute.mediation;
import bisq.core.btc.setup.WalletsSetup; import bisq.core.btc.setup.WalletsSetup;
import bisq.core.btc.wallet.BtcWalletService; import bisq.core.btc.wallet.BtcWalletService;
import bisq.core.btc.wallet.TradeWalletService; import bisq.core.btc.wallet.TradeWalletService;
import bisq.core.dao.DaoFacade;
import bisq.core.locale.Res; import bisq.core.locale.Res;
import bisq.core.offer.OpenOffer; import bisq.core.offer.OpenOffer;
import bisq.core.offer.OpenOfferManager; import bisq.core.offer.OpenOfferManager;
@ -46,7 +47,8 @@ import bisq.network.p2p.P2PService;
import bisq.common.Timer; import bisq.common.Timer;
import bisq.common.UserThread; import bisq.common.UserThread;
import bisq.common.app.Version; import bisq.common.app.Version;
import bisq.common.crypto.PubKeyRing; import bisq.common.config.Config;
import bisq.common.crypto.KeyRing;
import bisq.common.handlers.ErrorMessageHandler; import bisq.common.handlers.ErrorMessageHandler;
import bisq.common.handlers.ResultHandler; import bisq.common.handlers.ResultHandler;
@ -80,13 +82,16 @@ public final class MediationManager extends DisputeManager<MediationDisputeList>
TradeManager tradeManager, TradeManager tradeManager,
ClosedTradableManager closedTradableManager, ClosedTradableManager closedTradableManager,
OpenOfferManager openOfferManager, OpenOfferManager openOfferManager,
PubKeyRing pubKeyRing, DaoFacade daoFacade,
KeyRing keyRing,
MediationDisputeListService mediationDisputeListService, MediationDisputeListService mediationDisputeListService,
Config config,
PriceFeedService priceFeedService) { PriceFeedService priceFeedService) {
super(p2PService, tradeWalletService, walletService, walletsSetup, tradeManager, closedTradableManager, super(p2PService, tradeWalletService, walletService, walletsSetup, tradeManager, closedTradableManager,
openOfferManager, pubKeyRing, mediationDisputeListService, priceFeedService); openOfferManager, daoFacade, keyRing, mediationDisputeListService, config, priceFeedService);
} }
/////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////
// Implement template methods // Implement template methods
/////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////
@ -117,7 +122,7 @@ public final class MediationManager extends DisputeManager<MediationDisputeList>
} }
@Override @Override
protected Trade.DisputeState getDisputeState_StartedByPeer() { protected Trade.DisputeState getDisputeStateStartedByPeer() {
return Trade.DisputeState.MEDIATION_STARTED_BY_PEER; return Trade.DisputeState.MEDIATION_STARTED_BY_PEER;
} }

View file

@ -20,6 +20,7 @@ package bisq.core.support.dispute.refund;
import bisq.core.btc.setup.WalletsSetup; import bisq.core.btc.setup.WalletsSetup;
import bisq.core.btc.wallet.BtcWalletService; import bisq.core.btc.wallet.BtcWalletService;
import bisq.core.btc.wallet.TradeWalletService; import bisq.core.btc.wallet.TradeWalletService;
import bisq.core.dao.DaoFacade;
import bisq.core.locale.Res; import bisq.core.locale.Res;
import bisq.core.offer.OpenOffer; import bisq.core.offer.OpenOffer;
import bisq.core.offer.OpenOfferManager; import bisq.core.offer.OpenOfferManager;
@ -44,7 +45,8 @@ import bisq.network.p2p.P2PService;
import bisq.common.Timer; import bisq.common.Timer;
import bisq.common.UserThread; import bisq.common.UserThread;
import bisq.common.app.Version; import bisq.common.app.Version;
import bisq.common.crypto.PubKeyRing; import bisq.common.config.Config;
import bisq.common.crypto.KeyRing;
import com.google.inject.Inject; import com.google.inject.Inject;
import com.google.inject.Singleton; import com.google.inject.Singleton;
@ -74,13 +76,16 @@ public final class RefundManager extends DisputeManager<RefundDisputeList> {
TradeManager tradeManager, TradeManager tradeManager,
ClosedTradableManager closedTradableManager, ClosedTradableManager closedTradableManager,
OpenOfferManager openOfferManager, OpenOfferManager openOfferManager,
PubKeyRing pubKeyRing, DaoFacade daoFacade,
KeyRing keyRing,
RefundDisputeListService refundDisputeListService, RefundDisputeListService refundDisputeListService,
Config config,
PriceFeedService priceFeedService) { PriceFeedService priceFeedService) {
super(p2PService, tradeWalletService, walletService, walletsSetup, tradeManager, closedTradableManager, super(p2PService, tradeWalletService, walletService, walletsSetup, tradeManager, closedTradableManager,
openOfferManager, pubKeyRing, refundDisputeListService, priceFeedService); openOfferManager, daoFacade, keyRing, refundDisputeListService, config, priceFeedService);
} }
/////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////
// Implement template methods // Implement template methods
/////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////
@ -111,7 +116,7 @@ public final class RefundManager extends DisputeManager<RefundDisputeList> {
} }
@Override @Override
protected Trade.DisputeState getDisputeState_StartedByPeer() { protected Trade.DisputeState getDisputeStateStartedByPeer() {
return Trade.DisputeState.REFUND_REQUEST_STARTED_BY_PEER; return Trade.DisputeState.REFUND_REQUEST_STARTED_BY_PEER;
} }

View file

@ -1,214 +0,0 @@
/*
* 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 <http://www.gnu.org/licenses/>.
*/
package bisq.core.trade;
import bisq.core.btc.wallet.BtcWalletService;
import bisq.core.dao.DaoFacade;
import bisq.core.dao.governance.param.Param;
import bisq.core.offer.Offer;
import bisq.common.config.Config;
import org.bitcoinj.core.Address;
import org.bitcoinj.core.Coin;
import org.bitcoinj.core.NetworkParameters;
import org.bitcoinj.core.Transaction;
import org.bitcoinj.core.TransactionInput;
import org.bitcoinj.core.TransactionOutPoint;
import org.bitcoinj.core.TransactionOutput;
import java.util.List;
import lombok.extern.slf4j.Slf4j;
import static com.google.common.base.Preconditions.checkNotNull;
@Slf4j
public class DelayedPayoutTxValidation {
public static class DonationAddressException extends Exception {
DonationAddressException(String msg) {
super(msg);
}
}
public static class MissingDelayedPayoutTxException extends Exception {
MissingDelayedPayoutTxException(String msg) {
super(msg);
}
}
public static class InvalidTxException extends Exception {
InvalidTxException(String msg) {
super(msg);
}
}
public static class AmountMismatchException extends Exception {
AmountMismatchException(String msg) {
super(msg);
}
}
public static class InvalidLockTimeException extends Exception {
InvalidLockTimeException(String msg) {
super(msg);
}
}
public static class InvalidInputException extends Exception {
InvalidInputException(String msg) {
super(msg);
}
}
public static void validatePayoutTx(Trade trade,
Transaction delayedPayoutTx,
DaoFacade daoFacade,
BtcWalletService btcWalletService)
throws DonationAddressException, MissingDelayedPayoutTxException,
InvalidTxException, InvalidLockTimeException, AmountMismatchException {
String errorMsg;
if (delayedPayoutTx == null) {
errorMsg = "DelayedPayoutTx must not be null. tradeId=" + trade.getShortId();
log.error(errorMsg);
throw new MissingDelayedPayoutTxException(errorMsg);
}
// Validate tx structure
if (delayedPayoutTx.getInputs().size() != 1) {
errorMsg = "Number of delayedPayoutTx inputs must be 1. tradeId=" + trade.getShortId();
log.error(errorMsg);
log.error(delayedPayoutTx.toString());
throw new InvalidTxException(errorMsg);
}
if (delayedPayoutTx.getOutputs().size() != 1) {
errorMsg = "Number of delayedPayoutTx outputs must be 1. tradeId=" + trade.getShortId();
log.error(errorMsg);
log.error(delayedPayoutTx.toString());
throw new InvalidTxException(errorMsg);
}
// connectedOutput is null and input.getValue() is null at that point as the tx is not committed to the wallet
// yet. So we cannot check that the input matches but we did the amount check earlier in the trade protocol.
// Validate lock time
if (delayedPayoutTx.getLockTime() != trade.getLockTime()) {
errorMsg = "delayedPayoutTx.getLockTime() must match trade.getLockTime(). tradeId=" + trade.getShortId();
log.error(errorMsg);
log.error(delayedPayoutTx.toString());
throw new InvalidLockTimeException(errorMsg);
}
// Validate seq num
if (delayedPayoutTx.getInput(0).getSequenceNumber() != TransactionInput.NO_SEQUENCE - 1) {
errorMsg = "Sequence number must be 0xFFFFFFFE. tradeId=" + trade.getShortId();
log.error(errorMsg);
log.error(delayedPayoutTx.toString());
throw new InvalidLockTimeException(errorMsg);
}
// Check amount
TransactionOutput output = delayedPayoutTx.getOutput(0);
Offer offer = checkNotNull(trade.getOffer());
Coin msOutputAmount = offer.getBuyerSecurityDeposit()
.add(offer.getSellerSecurityDeposit())
.add(checkNotNull(trade.getTradeAmount()));
if (!output.getValue().equals(msOutputAmount)) {
errorMsg = "Output value of deposit tx and delayed payout tx is not matching. Output: " + output +
" / msOutputAmount: " + msOutputAmount + ". tradeId=" + trade.getShortId();
log.error(errorMsg);
log.error(delayedPayoutTx.toString());
throw new AmountMismatchException(errorMsg);
}
// Validate donation address
// Get most recent donation address.
// We do not support past DAO param addresses to avoid that those receive funds (no bond set up anymore).
// Users who have not synced the DAO cannot trade.
NetworkParameters params = btcWalletService.getParams();
//TODO update to BitcoinJ API changes
Address address = output.getAddressFromP2PKHScript(params);
if (address == null) {
// The donation address can be as well be a multisig address.
//TODO update to BitcoinJ API changes
address = output.getAddressFromP2SH(params);
if (address == null) {
errorMsg = "Donation address cannot be resolved (not of type P2PKHScript or P2SH). Output: " + output;
log.error(errorMsg);
log.error(delayedPayoutTx.toString());
throw new DonationAddressException(errorMsg);
}
}
String addressAsString = address.toString();
// In case the seller has deactivated the DAO the default address will be used.
String defaultDonationAddressString = Param.RECIPIENT_BTC_ADDRESS.getDefaultValue();
boolean defaultNotMatching = !defaultDonationAddressString.equals(addressAsString);
String recentDonationAddressString = daoFacade.getParamValue(Param.RECIPIENT_BTC_ADDRESS);
boolean recentFromDaoNotMatching = !recentDonationAddressString.equals(addressAsString);
// If buyer has DAO deactivated or not synced he will not be able to see recent address used by the seller, so
// we add it hard coded here. We need to support also the default one as
// FIXME This is a quick fix and should be improved in future.
// We use the default addresses for non mainnet networks. For dev testing it need to be changed here.
// We use a list to gain more flexibility at updates of DAO param, but still might fail if buyer has not updated
// software. Needs a better solution....
List<String> hardCodedAddresses = Config.baseCurrencyNetwork().isMainnet() ?
List.of("3EtUWqsGThPtjwUczw27YCo6EWvQdaPUyp", "3A8Zc1XioE2HRzYfbb5P8iemCS72M6vRJV") : // mainnet
Config.baseCurrencyNetwork().isDaoBetaNet() ? List.of("1BVxNn3T12veSK6DgqwU4Hdn7QHcDDRag7") : // daoBetaNet
Config.baseCurrencyNetwork().isTestnet() ? List.of("2N4mVTpUZAnhm9phnxB7VrHB4aBhnWrcUrV") : // testnet
List.of("2MzBNTJDjjXgViKBGnatDU3yWkJ8pJkEg9w"); // regtest or DAO testnet (regtest)
boolean noneOfHardCodedMatching = hardCodedAddresses.stream().noneMatch(e -> e.equals(addressAsString));
// If seller has DAO deactivated as well we get default address
if (recentFromDaoNotMatching && defaultNotMatching && noneOfHardCodedMatching) {
errorMsg = "Donation address is invalid." +
"\nAddress used by BTC seller: " + addressAsString +
"\nRecent donation address:" + recentDonationAddressString +
"\nDefault donation address: " + defaultDonationAddressString;
log.error(errorMsg);
log.error(delayedPayoutTx.toString());
throw new DonationAddressException(errorMsg);
}
}
public static void validatePayoutTxInput(Transaction depositTx,
Transaction delayedPayoutTx)
throws InvalidInputException {
TransactionInput input = delayedPayoutTx.getInput(0);
checkNotNull(input, "delayedPayoutTx.getInput(0) must not be null");
// input.getConnectedOutput() is null as the tx is not committed at that point
TransactionOutPoint outpoint = input.getOutpoint();
if (!outpoint.getHash().toString().equals(depositTx.getTxId().toString()) || outpoint.getIndex() != 0) {
throw new InvalidInputException("Input of delayed payout transaction does not point to output of deposit tx.\n" +
"Delayed payout tx=" + delayedPayoutTx + "\n" +
"Deposit tx=" + depositTx);
}
}
}

View file

@ -745,9 +745,19 @@ public abstract class Trade implements Tradable, Model {
@Nullable @Nullable
public Transaction getDelayedPayoutTx() { public Transaction getDelayedPayoutTx() {
if (delayedPayoutTx == null) { if (delayedPayoutTx == null) {
delayedPayoutTx = delayedPayoutTxBytes != null && processModel.getBtcWalletService() != null ? BtcWalletService btcWalletService = processModel.getBtcWalletService();
processModel.getBtcWalletService().getTxFromSerializedTx(delayedPayoutTxBytes) : if (btcWalletService == null) {
null; log.warn("btcWalletService is null. You might call that method before the tradeManager has " +
"initialized all trades");
return null;
}
if (delayedPayoutTxBytes == null) {
log.warn("delayedPayoutTxBytes are null");
return null;
}
delayedPayoutTx = btcWalletService.getTxFromSerializedTx(delayedPayoutTxBytes);
} }
return delayedPayoutTx; return delayedPayoutTx;
} }

View file

@ -0,0 +1,406 @@
/*
* 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 <http://www.gnu.org/licenses/>.
*/
package bisq.core.trade;
import bisq.core.btc.wallet.BtcWalletService;
import bisq.core.dao.DaoFacade;
import bisq.core.offer.Offer;
import bisq.core.support.dispute.Dispute;
import bisq.core.util.validation.RegexValidatorFactory;
import bisq.network.p2p.NodeAddress;
import bisq.common.config.Config;
import bisq.common.util.Tuple3;
import org.bitcoinj.core.Address;
import org.bitcoinj.core.Coin;
import org.bitcoinj.core.NetworkParameters;
import org.bitcoinj.core.Transaction;
import org.bitcoinj.core.TransactionInput;
import org.bitcoinj.core.TransactionOutPoint;
import org.bitcoinj.core.TransactionOutput;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Consumer;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import javax.annotation.Nullable;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
@Slf4j
public class TradeDataValidation {
public static void validateDonationAddress(String addressAsString, DaoFacade daoFacade)
throws AddressException {
validateDonationAddress(null, addressAsString, daoFacade);
}
public static void validateNodeAddress(Dispute dispute, NodeAddress nodeAddress, Config config)
throws NodeAddressException {
if (!config.useLocalhostForP2P && !RegexValidatorFactory.onionAddressRegexValidator().validate(nodeAddress.getFullAddress()).isValid) {
String msg = "Node address " + nodeAddress.getFullAddress() + " at dispute with trade ID " +
dispute.getShortTradeId() + " is not a valid address";
log.error(msg);
throw new NodeAddressException(dispute, msg);
}
}
public static void validateDonationAddress(@Nullable Dispute dispute, String addressAsString, DaoFacade daoFacade)
throws AddressException {
if (addressAsString == null) {
log.debug("address is null at validateDonationAddress. This is expected in case of an not updated trader.");
return;
}
Set<String> allPastParamValues = daoFacade.getAllDonationAddresses();
if (!allPastParamValues.contains(addressAsString)) {
String errorMsg = "Donation address is not a valid DAO donation address." +
"\nAddress used in the dispute: " + addressAsString +
"\nAll DAO param donation addresses:" + allPastParamValues;
log.error(errorMsg);
throw new AddressException(dispute, errorMsg);
}
}
public static void testIfAnyDisputeTriedReplay(List<Dispute> disputeList,
Consumer<DisputeReplayException> exceptionHandler) {
var tuple = getTestReplayHashMaps(disputeList);
Map<String, Set<String>> disputesPerTradeId = tuple.first;
Map<String, Set<String>> disputesPerDelayedPayoutTxId = tuple.second;
Map<String, Set<String>> disputesPerDepositTxId = tuple.third;
disputeList.forEach(disputeToTest -> {
try {
testIfDisputeTriesReplay(disputeToTest,
disputesPerTradeId,
disputesPerDelayedPayoutTxId,
disputesPerDepositTxId);
} catch (DisputeReplayException e) {
exceptionHandler.accept(e);
}
});
}
public static void testIfDisputeTriesReplay(Dispute dispute,
List<Dispute> disputeList) throws DisputeReplayException {
var tuple = TradeDataValidation.getTestReplayHashMaps(disputeList);
Map<String, Set<String>> disputesPerTradeId = tuple.first;
Map<String, Set<String>> disputesPerDelayedPayoutTxId = tuple.second;
Map<String, Set<String>> disputesPerDepositTxId = tuple.third;
testIfDisputeTriesReplay(dispute,
disputesPerTradeId,
disputesPerDelayedPayoutTxId,
disputesPerDepositTxId);
}
private static Tuple3<Map<String, Set<String>>, Map<String, Set<String>>, Map<String, Set<String>>> getTestReplayHashMaps(
List<Dispute> disputeList) {
Map<String, Set<String>> disputesPerTradeId = new HashMap<>();
Map<String, Set<String>> disputesPerDelayedPayoutTxId = new HashMap<>();
Map<String, Set<String>> disputesPerDepositTxId = new HashMap<>();
disputeList.forEach(dispute -> {
String uid = dispute.getUid();
String tradeId = dispute.getTradeId();
disputesPerTradeId.putIfAbsent(tradeId, new HashSet<>());
Set<String> set = disputesPerTradeId.get(tradeId);
set.add(uid);
String delayedPayoutTxId = dispute.getDelayedPayoutTxId();
disputesPerDelayedPayoutTxId.putIfAbsent(delayedPayoutTxId, new HashSet<>());
set = disputesPerDelayedPayoutTxId.get(delayedPayoutTxId);
set.add(uid);
String depositTxId = dispute.getDepositTxId();
disputesPerDepositTxId.putIfAbsent(depositTxId, new HashSet<>());
set = disputesPerDepositTxId.get(depositTxId);
set.add(uid);
});
return new Tuple3<>(disputesPerTradeId, disputesPerDelayedPayoutTxId, disputesPerDepositTxId);
}
private static void testIfDisputeTriesReplay(Dispute disputeToTest,
Map<String, Set<String>> disputesPerTradeId,
Map<String, Set<String>> disputesPerDelayedPayoutTxId,
Map<String, Set<String>> disputesPerDepositTxId)
throws DisputeReplayException {
try {
String disputeToTestTradeId = disputeToTest.getTradeId();
String disputeToTestDelayedPayoutTxId = disputeToTest.getDelayedPayoutTxId();
String disputeToTestDepositTxId = disputeToTest.getDepositTxId();
String disputeToTestUid = disputeToTest.getUid();
checkNotNull(disputeToTestDelayedPayoutTxId,
"delayedPayoutTxId must not be null. Trade ID: " + disputeToTestTradeId);
checkNotNull(disputeToTestDepositTxId,
"depositTxId must not be null. Trade ID: " + disputeToTestTradeId);
checkNotNull(disputeToTestUid,
"agentsUid must not be null. Trade ID: " + disputeToTestTradeId);
checkArgument(disputesPerTradeId.get(disputeToTestTradeId).size() <= 2,
"We found more then 2 disputes with the same trade ID. " +
"Trade ID: " + disputeToTestTradeId);
checkArgument(disputesPerDelayedPayoutTxId.get(disputeToTestDelayedPayoutTxId).size() <= 2,
"We found more then 2 disputes with the same delayedPayoutTxId. " +
"Trade ID: " + disputeToTestTradeId);
checkArgument(disputesPerDepositTxId.get(disputeToTestDepositTxId).size() <= 2,
"We found more then 2 disputes with the same depositTxId. " +
"Trade ID: " + disputeToTestTradeId);
} catch (IllegalArgumentException | NullPointerException e) {
throw new DisputeReplayException(disputeToTest, e.getMessage());
}
}
public static void validatePayoutTx(Trade trade,
Transaction delayedPayoutTx,
DaoFacade daoFacade,
BtcWalletService btcWalletService)
throws AddressException, MissingTxException,
InvalidTxException, InvalidLockTimeException, InvalidAmountException {
validatePayoutTx(trade,
delayedPayoutTx,
null,
daoFacade,
btcWalletService,
null);
}
public static void validatePayoutTx(Trade trade,
Transaction delayedPayoutTx,
@Nullable Dispute dispute,
DaoFacade daoFacade,
BtcWalletService btcWalletService)
throws AddressException, MissingTxException,
InvalidTxException, InvalidLockTimeException, InvalidAmountException {
validatePayoutTx(trade,
delayedPayoutTx,
dispute,
daoFacade,
btcWalletService,
null);
}
public static void validatePayoutTx(Trade trade,
Transaction delayedPayoutTx,
DaoFacade daoFacade,
BtcWalletService btcWalletService,
@Nullable Consumer<String> addressConsumer)
throws AddressException, MissingTxException,
InvalidTxException, InvalidLockTimeException, InvalidAmountException {
validatePayoutTx(trade,
delayedPayoutTx,
null,
daoFacade,
btcWalletService,
addressConsumer);
}
public static void validatePayoutTx(Trade trade,
Transaction delayedPayoutTx,
@Nullable Dispute dispute,
DaoFacade daoFacade,
BtcWalletService btcWalletService,
@Nullable Consumer<String> addressConsumer)
throws AddressException, MissingTxException,
InvalidTxException, InvalidLockTimeException, InvalidAmountException {
String errorMsg;
if (delayedPayoutTx == null) {
errorMsg = "DelayedPayoutTx must not be null";
log.error(errorMsg);
throw new MissingTxException("DelayedPayoutTx must not be null");
}
// Validate tx structure
if (delayedPayoutTx.getInputs().size() != 1) {
errorMsg = "Number of delayedPayoutTx inputs must be 1";
log.error(errorMsg);
log.error(delayedPayoutTx.toString());
throw new InvalidTxException(errorMsg);
}
if (delayedPayoutTx.getOutputs().size() != 1) {
errorMsg = "Number of delayedPayoutTx outputs must be 1";
log.error(errorMsg);
log.error(delayedPayoutTx.toString());
throw new InvalidTxException(errorMsg);
}
// connectedOutput is null and input.getValue() is null at that point as the tx is not committed to the wallet
// yet. So we cannot check that the input matches but we did the amount check earlier in the trade protocol.
// Validate lock time
if (delayedPayoutTx.getLockTime() != trade.getLockTime()) {
errorMsg = "delayedPayoutTx.getLockTime() must match trade.getLockTime()";
log.error(errorMsg);
log.error(delayedPayoutTx.toString());
throw new InvalidLockTimeException(errorMsg);
}
// Validate seq num
if (delayedPayoutTx.getInput(0).getSequenceNumber() != TransactionInput.NO_SEQUENCE - 1) {
errorMsg = "Sequence number must be 0xFFFFFFFE";
log.error(errorMsg);
log.error(delayedPayoutTx.toString());
throw new InvalidLockTimeException(errorMsg);
}
// Check amount
TransactionOutput output = delayedPayoutTx.getOutput(0);
Offer offer = checkNotNull(trade.getOffer());
Coin msOutputAmount = offer.getBuyerSecurityDeposit()
.add(offer.getSellerSecurityDeposit())
.add(checkNotNull(trade.getTradeAmount()));
if (!output.getValue().equals(msOutputAmount)) {
errorMsg = "Output value of deposit tx and delayed payout tx is not matching. Output: " + output + " / msOutputAmount: " + msOutputAmount;
log.error(errorMsg);
log.error(delayedPayoutTx.toString());
throw new InvalidAmountException(errorMsg);
}
NetworkParameters params = btcWalletService.getParams();
Address address = output.getAddressFromP2PKHScript(params);
if (address == null) {
// The donation address can be a multisig address as well.
address = output.getAddressFromP2SH(params);
if (address == null) {
errorMsg = "Donation address cannot be resolved (not of type P2PKHScript or P2SH). Output: " + output;
log.error(errorMsg);
log.error(delayedPayoutTx.toString());
throw new AddressException(dispute, errorMsg);
}
}
String addressAsString = address.toString();
if (addressConsumer != null) {
addressConsumer.accept(addressAsString);
}
validateDonationAddress(addressAsString, daoFacade);
if (dispute != null) {
// Verify that address in the dispute matches the one in the trade.
String donationAddressOfDelayedPayoutTx = dispute.getDonationAddressOfDelayedPayoutTx();
// Old clients don't have it set yet. Can be removed after a forced update
if (donationAddressOfDelayedPayoutTx != null) {
checkArgument(addressAsString.equals(donationAddressOfDelayedPayoutTx),
"donationAddressOfDelayedPayoutTx from dispute does not match address from delayed payout tx");
}
}
}
public static void validatePayoutTxInput(Transaction depositTx,
Transaction delayedPayoutTx)
throws InvalidInputException {
TransactionInput input = delayedPayoutTx.getInput(0);
checkNotNull(input, "delayedPayoutTx.getInput(0) must not be null");
// input.getConnectedOutput() is null as the tx is not committed at that point
TransactionOutPoint outpoint = input.getOutpoint();
if (!outpoint.getHash().toString().equals(depositTx.getTxId().toString()) || outpoint.getIndex() != 0) {
throw new InvalidInputException("Input of delayed payout transaction does not point to output of deposit tx.\n" +
"Delayed payout tx=" + delayedPayoutTx + "\n" +
"Deposit tx=" + depositTx);
}
}
///////////////////////////////////////////////////////////////////////////////////////////
// Exceptions
///////////////////////////////////////////////////////////////////////////////////////////
public static class ValidationException extends Exception {
@Nullable
@Getter
private final Dispute dispute;
ValidationException(String msg) {
this(null, msg);
}
ValidationException(@Nullable Dispute dispute, String msg) {
super(msg);
this.dispute = dispute;
}
}
public static class AddressException extends ValidationException {
AddressException(@Nullable Dispute dispute, String msg) {
super(dispute, msg);
}
}
public static class MissingTxException extends ValidationException {
MissingTxException(String msg) {
super(msg);
}
}
public static class InvalidTxException extends ValidationException {
InvalidTxException(String msg) {
super(msg);
}
}
public static class InvalidAmountException extends ValidationException {
InvalidAmountException(String msg) {
super(msg);
}
}
public static class InvalidLockTimeException extends ValidationException {
InvalidLockTimeException(String msg) {
super(msg);
}
}
public static class InvalidInputException extends ValidationException {
InvalidInputException(String msg) {
super(msg);
}
}
public static class DisputeReplayException extends ValidationException {
DisputeReplayException(Dispute dispute, String msg) {
super(dispute, msg);
}
}
public static class NodeAddressException extends ValidationException {
NodeAddressException(Dispute dispute, String msg) {
super(dispute, msg);
}
}
}

View file

@ -135,6 +135,7 @@ public class TradeManager implements PersistedDataHost {
private final Storage<TradableList<Trade>> tradableListStorage; private final Storage<TradableList<Trade>> tradableListStorage;
private TradableList<Trade> tradableList; private TradableList<Trade> tradableList;
@Getter
private final BooleanProperty pendingTradesInitialized = new SimpleBooleanProperty(); private final BooleanProperty pendingTradesInitialized = new SimpleBooleanProperty();
private List<Trade> tradesForStatistics; private List<Trade> tradesForStatistics;
@Setter @Setter
@ -305,15 +306,11 @@ public class TradeManager implements PersistedDataHost {
} }
try { try {
DelayedPayoutTxValidation.validatePayoutTx(trade, TradeDataValidation.validatePayoutTx(trade,
trade.getDelayedPayoutTx(), trade.getDelayedPayoutTx(),
daoFacade, daoFacade,
btcWalletService); btcWalletService);
} catch (DelayedPayoutTxValidation.DonationAddressException | } catch (TradeDataValidation.ValidationException e) {
DelayedPayoutTxValidation.InvalidTxException |
DelayedPayoutTxValidation.InvalidLockTimeException |
DelayedPayoutTxValidation.MissingDelayedPayoutTxException |
DelayedPayoutTxValidation.AmountMismatchException e) {
log.warn("Delayed payout tx exception, trade {}, exception {}", trade.getId(), e.getMessage()); log.warn("Delayed payout tx exception, trade {}, exception {}", trade.getId(), e.getMessage());
if (!allowFaultyDelayedTxs) { if (!allowFaultyDelayedTxs) {
// We move it to failed trades so it cannot be continued. // We move it to failed trades so it cannot be continued.

View file

@ -17,8 +17,8 @@
package bisq.core.trade.protocol.tasks.buyer; package bisq.core.trade.protocol.tasks.buyer;
import bisq.core.trade.DelayedPayoutTxValidation;
import bisq.core.trade.Trade; import bisq.core.trade.Trade;
import bisq.core.trade.TradeDataValidation;
import bisq.core.trade.protocol.tasks.TradeTask; import bisq.core.trade.protocol.tasks.TradeTask;
import bisq.common.taskrunner.TaskRunner; import bisq.common.taskrunner.TaskRunner;
@ -40,23 +40,21 @@ public class BuyerVerifiesFinalDelayedPayoutTx extends TradeTask {
try { try {
runInterceptHook(); runInterceptHook();
Transaction delayedPayoutTx = checkNotNull(trade.getDelayedPayoutTx()); Transaction delayedPayoutTx = trade.getDelayedPayoutTx();
DelayedPayoutTxValidation.validatePayoutTx(trade, checkNotNull(delayedPayoutTx, "trade.getDelayedPayoutTx() must not be null");
// Check again tx
TradeDataValidation.validatePayoutTx(trade,
delayedPayoutTx, delayedPayoutTx,
processModel.getDaoFacade(), processModel.getDaoFacade(),
processModel.getBtcWalletService()); processModel.getBtcWalletService());
// Now as we know the deposit tx we can also verify the input // Now as we know the deposit tx we can also verify the input
Transaction depositTx = checkNotNull(trade.getDepositTx()); Transaction depositTx = trade.getDepositTx();
DelayedPayoutTxValidation.validatePayoutTxInput(depositTx, delayedPayoutTx); checkNotNull(depositTx, "trade.getDepositTx() must not be null");
TradeDataValidation.validatePayoutTxInput(depositTx, delayedPayoutTx);
complete(); complete();
} catch (DelayedPayoutTxValidation.DonationAddressException | } catch (TradeDataValidation.ValidationException e) {
DelayedPayoutTxValidation.MissingDelayedPayoutTxException |
DelayedPayoutTxValidation.InvalidTxException |
DelayedPayoutTxValidation.InvalidLockTimeException |
DelayedPayoutTxValidation.AmountMismatchException |
DelayedPayoutTxValidation.InvalidInputException e) {
failed(e.getMessage()); failed(e.getMessage());
} catch (Throwable t) { } catch (Throwable t) {
failed(t); failed(t);

View file

@ -17,8 +17,8 @@
package bisq.core.trade.protocol.tasks.buyer; package bisq.core.trade.protocol.tasks.buyer;
import bisq.core.trade.DelayedPayoutTxValidation;
import bisq.core.trade.Trade; import bisq.core.trade.Trade;
import bisq.core.trade.TradeDataValidation;
import bisq.core.trade.protocol.tasks.TradeTask; import bisq.core.trade.protocol.tasks.TradeTask;
import bisq.common.taskrunner.TaskRunner; import bisq.common.taskrunner.TaskRunner;
@ -36,17 +36,13 @@ public class BuyerVerifiesPreparedDelayedPayoutTx extends TradeTask {
try { try {
runInterceptHook(); runInterceptHook();
DelayedPayoutTxValidation.validatePayoutTx(trade, TradeDataValidation.validatePayoutTx(trade,
processModel.getPreparedDelayedPayoutTx(), processModel.getPreparedDelayedPayoutTx(),
processModel.getDaoFacade(), processModel.getDaoFacade(),
processModel.getBtcWalletService()); processModel.getBtcWalletService());
complete(); complete();
} catch (DelayedPayoutTxValidation.DonationAddressException | } catch (TradeDataValidation.ValidationException e) {
DelayedPayoutTxValidation.MissingDelayedPayoutTxException |
DelayedPayoutTxValidation.InvalidTxException |
DelayedPayoutTxValidation.InvalidLockTimeException |
DelayedPayoutTxValidation.AmountMismatchException e) {
failed(e.getMessage()); failed(e.getMessage());
} catch (Throwable t) { } catch (Throwable t) {
failed(t); failed(t);

View file

@ -214,7 +214,8 @@ shared.mediator=Mediator
shared.arbitrator=Arbitrator shared.arbitrator=Arbitrator
shared.refundAgent=Arbitrator shared.refundAgent=Arbitrator
shared.refundAgentForSupportStaff=Refund agent shared.refundAgentForSupportStaff=Refund agent
shared.delayedPayoutTxId=Refund collateral transaction ID shared.delayedPayoutTxId=Delayed payout transaction ID
shared.delayedPayoutTxReceiverAddress=Delayed payout transaction sent to
shared.unconfirmedTransactionsLimitReached=You have too many unconfirmed transactions at the moment. Please try again later. shared.unconfirmedTransactionsLimitReached=You have too many unconfirmed transactions at the moment. Please try again later.
@ -894,6 +895,12 @@ portfolio.pending.mediationResult.popup.info=The mediator has suggested the foll
(or if the other peer is unresponsive).\n\n\ (or if the other peer is unresponsive).\n\n\
More details about the new arbitration model:\n\ More details about the new arbitration model:\n\
https://docs.bisq.network/trading-rules.html#arbitration https://docs.bisq.network/trading-rules.html#arbitration
portfolio.pending.mediationResult.popup.selfAccepted.lockTimeOver=You have accepted the mediator''s suggested payout \
but it seems that your trading peer has not accepted it.\n\n\
The lock time is since {0} (block {1}) over and you can open a second-round dispute with an arbitrator who will \
investigate the case again and do a payout based on their findings.\n\n\
You can find more details about the arbitration model at:\n\
https://docs.bisq.network/trading-rules.html#arbitration
portfolio.pending.mediationResult.popup.openArbitration=Reject and request arbitration portfolio.pending.mediationResult.popup.openArbitration=Reject and request arbitration
portfolio.pending.mediationResult.popup.alreadyAccepted=You've already accepted portfolio.pending.mediationResult.popup.alreadyAccepted=You've already accepted
@ -1007,11 +1014,24 @@ support.tab.legacyArbitration.support=Legacy Arbitration
support.tab.ArbitratorsSupportTickets={0}'s tickets support.tab.ArbitratorsSupportTickets={0}'s tickets
support.filter=Search disputes support.filter=Search disputes
support.filter.prompt=Enter trade ID, date, onion address or account data support.filter.prompt=Enter trade ID, date, onion address or account data
support.sigCheck.button=Verify result
support.sigCheck.popup.info=In case of a reimbursement request to the DAO you need to paste the summary message of the \
mediation and arbitration process in your reimbursement request on Github. To make this statement verifiable any user can \
check with this tool if the signature of the mediator or arbitrator matches the summary message.
support.sigCheck.popup.header=Verify dispute result signature
support.sigCheck.popup.msg.label=Summary message
support.sigCheck.popup.msg.prompt=Copy & paste summary message from dispute
support.sigCheck.popup.result=Validation result
support.sigCheck.popup.success=Signature is valid
support.sigCheck.popup.failed=Signature verification failed
support.sigCheck.popup.invalidFormat=Message is not of expected format. Copy & paste summary message from dispute.
support.reOpenByTrader.prompt=Are you sure you want to re-open the dispute? support.reOpenByTrader.prompt=Are you sure you want to re-open the dispute?
support.reOpenButton.label=Re-open dispute support.reOpenButton.label=Re-open
support.sendNotificationButton.label=Send private notification support.sendNotificationButton.label=Private notification
support.reportButton.label=Generate report support.reportButton.label=Report
support.fullReportButton.label=Get text dump of all disputes support.fullReportButton.label=All disputes
support.noTickets=There are no open tickets support.noTickets=There are no open tickets
support.sendingMessage=Sending Message... support.sendingMessage=Sending Message...
support.receiverNotOnline=Receiver is not online. Message is saved to their mailbox. support.receiverNotOnline=Receiver is not online. Message is saved to their mailbox.
@ -1081,6 +1101,15 @@ support.peerOpenedDispute=Your trading peer has requested a dispute.\n\n{0}\n\nB
support.peerOpenedDisputeForMediation=Your trading peer has requested mediation.\n\n{0}\n\nBisq version: {1} support.peerOpenedDisputeForMediation=Your trading peer has requested mediation.\n\n{0}\n\nBisq version: {1}
support.mediatorsDisputeSummary=System message: Mediator''s dispute summary:\n{0} support.mediatorsDisputeSummary=System message: Mediator''s dispute summary:\n{0}
support.mediatorsAddress=Mediator''s node address: {0} support.mediatorsAddress=Mediator''s node address: {0}
support.warning.disputesWithInvalidDonationAddress=The delayed payout transaction has used an invalid receiver address. \
It does not match any of the DAO parameter values for the valid donation addresses.\n\nThis might be a scam attempt. \
Please inform the developers about that incident and do not close that case before the situation is resolved!\n\n\
Address used in the dispute: {0}\n\n\
All DAO param donation addresses: {1}\n\n\
Trade ID: {2}\
{3}
support.warning.disputesWithInvalidDonationAddress.mediator=\n\nDo you still want to close the dispute?
support.warning.disputesWithInvalidDonationAddress.refundAgent=\n\nYou must not do the payout.
#################################################################### ####################################################################
@ -2437,15 +2466,25 @@ disputeSummaryWindow.reason.TRADE_ALREADY_SETTLED=Trade already settled
disputeSummaryWindow.summaryNotes=Summary notes disputeSummaryWindow.summaryNotes=Summary notes
disputeSummaryWindow.addSummaryNotes=Add summary notes disputeSummaryWindow.addSummaryNotes=Add summary notes
disputeSummaryWindow.close.button=Close ticket disputeSummaryWindow.close.button=Close ticket
disputeSummaryWindow.close.msg=Ticket closed on {0}\n\n\
Summary:\n\ # Do no change any line break or order of tokens as the structure is used for signature verification
Payout amount for BTC buyer: {1}\n\ disputeSummaryWindow.close.msg=Ticket closed on {0}\n\
Payout amount for BTC seller: {2}\n\n\ {1} node address: {2}\n\n\
Reason for dispute: {3}\n\n\ Summary:\n\
Summary notes:\n{4} Trade ID: {3}\n\
disputeSummaryWindow.close.nextStepsForMediation=\n\nNext steps:\n\ Currency: {4}\n\
Trade amount: {5}\n\
Payout amount for BTC buyer: {6}\n\
Payout amount for BTC seller: {7}\n\n\
Reason for dispute: {8}\n\n\
Summary notes:\n{9}\n
# Do no change any line break or order of tokens as the structure is used for signature verification
disputeSummaryWindow.close.msgWithSig={0}{1}{2}{3}
disputeSummaryWindow.close.nextStepsForMediation=\nNext steps:\n\
Open trade and accept or reject suggestion from mediator Open trade and accept or reject suggestion from mediator
disputeSummaryWindow.close.nextStepsForRefundAgentArbitration=\n\nNext steps:\n\ disputeSummaryWindow.close.nextStepsForRefundAgentArbitration=\nNext steps:\n\
No further action is required from you. If the arbitrator decided in your favor, you'll see a "Refund from arbitration" transaction in Funds/Transactions No further action is required from you. If the arbitrator decided in your favor, you'll see a "Refund from arbitration" transaction in Funds/Transactions
disputeSummaryWindow.close.closePeer=You need to close also the trading peers ticket! disputeSummaryWindow.close.closePeer=You need to close also the trading peers ticket!
disputeSummaryWindow.close.txDetails.headline=Publish refund transaction disputeSummaryWindow.close.txDetails.headline=Publish refund transaction
@ -2457,6 +2496,9 @@ disputeSummaryWindow.close.txDetails=Spending: {0}\n\
Transaction size: {5} Kb\n\n\ Transaction size: {5} Kb\n\n\
Are you sure you want to publish this transaction? Are you sure you want to publish this transaction?
disputeSummaryWindow.close.noPayout.headline=Close without any payout
disputeSummaryWindow.close.noPayout.text=Do you want to close without doing any payout?
emptyWalletWindow.headline={0} emergency wallet tool emptyWalletWindow.headline={0} emergency wallet tool
emptyWalletWindow.info=Please use that only in emergency case if you cannot access your fund from the UI.\n\n\ emptyWalletWindow.info=Please use that only in emergency case if you cannot access your fund from the UI.\n\n\
Please note that all open offers will be closed automatically when using this tool.\n\n\ Please note that all open offers will be closed automatically when using this tool.\n\n\

View file

@ -141,6 +141,8 @@ public class ContractWindow extends Overlay<ContractWindow> {
rows++; rows++;
if (dispute.getDelayedPayoutTxId() != null) if (dispute.getDelayedPayoutTxId() != null)
rows++; rows++;
if (dispute.getDonationAddressOfDelayedPayoutTx() != null)
rows++;
if (showAcceptedCountryCodes) if (showAcceptedCountryCodes)
rows++; rows++;
if (showAcceptedBanks) if (showAcceptedBanks)
@ -248,6 +250,11 @@ public class ContractWindow extends Overlay<ContractWindow> {
if (dispute.getDelayedPayoutTxId() != null) if (dispute.getDelayedPayoutTxId() != null)
addLabelTxIdTextField(gridPane, ++rowIndex, Res.get("shared.delayedPayoutTxId"), dispute.getDelayedPayoutTxId()); addLabelTxIdTextField(gridPane, ++rowIndex, Res.get("shared.delayedPayoutTxId"), dispute.getDelayedPayoutTxId());
if (dispute.getDonationAddressOfDelayedPayoutTx() != null) {
addLabelTxIdTextField(gridPane, ++rowIndex, Res.get("shared.delayedPayoutTxReceiverAddress"),
dispute.getDonationAddressOfDelayedPayoutTx());
}
if (dispute.getPayoutTxSerialized() != null) if (dispute.getPayoutTxSerialized() != null)
addLabelTxIdTextField(gridPane, ++rowIndex, Res.get("shared.payoutTxId"), dispute.getPayoutTxId()); addLabelTxIdTextField(gridPane, ++rowIndex, Res.get("shared.payoutTxId"), dispute.getPayoutTxId());

View file

@ -23,6 +23,7 @@ import bisq.desktop.components.BisqTextArea;
import bisq.desktop.components.InputTextField; import bisq.desktop.components.InputTextField;
import bisq.desktop.main.overlays.Overlay; import bisq.desktop.main.overlays.Overlay;
import bisq.desktop.main.overlays.popups.Popup; import bisq.desktop.main.overlays.popups.Popup;
import bisq.desktop.main.support.dispute.DisputeSummaryVerification;
import bisq.desktop.util.DisplayUtils; import bisq.desktop.util.DisplayUtils;
import bisq.desktop.util.Layout; import bisq.desktop.util.Layout;
@ -34,6 +35,7 @@ import bisq.core.btc.wallet.BtcWalletService;
import bisq.core.btc.wallet.Restrictions; import bisq.core.btc.wallet.Restrictions;
import bisq.core.btc.wallet.TradeWalletService; import bisq.core.btc.wallet.TradeWalletService;
import bisq.core.btc.wallet.TxBroadcaster; import bisq.core.btc.wallet.TxBroadcaster;
import bisq.core.dao.DaoFacade;
import bisq.core.locale.Res; import bisq.core.locale.Res;
import bisq.core.offer.Offer; import bisq.core.offer.Offer;
import bisq.core.provider.fee.FeeService; import bisq.core.provider.fee.FeeService;
@ -45,6 +47,7 @@ import bisq.core.support.dispute.DisputeResult;
import bisq.core.support.dispute.mediation.MediationManager; import bisq.core.support.dispute.mediation.MediationManager;
import bisq.core.support.dispute.refund.RefundManager; import bisq.core.support.dispute.refund.RefundManager;
import bisq.core.trade.Contract; import bisq.core.trade.Contract;
import bisq.core.trade.TradeDataValidation;
import bisq.core.util.FormattingUtils; import bisq.core.util.FormattingUtils;
import bisq.core.util.ParsingUtils; import bisq.core.util.ParsingUtils;
import bisq.core.util.coin.CoinFormatter; import bisq.core.util.coin.CoinFormatter;
@ -86,8 +89,7 @@ import java.util.Date;
import java.util.Optional; import java.util.Optional;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import org.slf4j.Logger; import lombok.extern.slf4j.Slf4j;
import org.slf4j.LoggerFactory;
import static bisq.desktop.util.FormBuilder.add2ButtonsWithBox; import static bisq.desktop.util.FormBuilder.add2ButtonsWithBox;
import static bisq.desktop.util.FormBuilder.addConfirmationLabelLabel; import static bisq.desktop.util.FormBuilder.addConfirmationLabelLabel;
@ -95,9 +97,8 @@ import static bisq.desktop.util.FormBuilder.addTitledGroupBg;
import static bisq.desktop.util.FormBuilder.addTopLabelWithVBox; import static bisq.desktop.util.FormBuilder.addTopLabelWithVBox;
import static com.google.common.base.Preconditions.checkNotNull; import static com.google.common.base.Preconditions.checkNotNull;
@Slf4j
public class DisputeSummaryWindow extends Overlay<DisputeSummaryWindow> { public class DisputeSummaryWindow extends Overlay<DisputeSummaryWindow> {
private static final Logger log = LoggerFactory.getLogger(DisputeSummaryWindow.class);
private final CoinFormatter formatter; private final CoinFormatter formatter;
private final MediationManager mediationManager; private final MediationManager mediationManager;
private final RefundManager refundManager; private final RefundManager refundManager;
@ -105,8 +106,9 @@ public class DisputeSummaryWindow extends Overlay<DisputeSummaryWindow> {
private final BtcWalletService btcWalletService; private final BtcWalletService btcWalletService;
private final TxFeeEstimationService txFeeEstimationService; private final TxFeeEstimationService txFeeEstimationService;
private final FeeService feeService; private final FeeService feeService;
private final DaoFacade daoFacade;
private Dispute dispute; private Dispute dispute;
private Optional<Runnable> finalizeDisputeHandlerOptional = Optional.<Runnable>empty(); private Optional<Runnable> finalizeDisputeHandlerOptional = Optional.empty();
private ToggleGroup tradeAmountToggleGroup, reasonToggleGroup; private ToggleGroup tradeAmountToggleGroup, reasonToggleGroup;
private DisputeResult disputeResult; private DisputeResult disputeResult;
private RadioButton buyerGetsTradeAmountRadioButton, sellerGetsTradeAmountRadioButton, private RadioButton buyerGetsTradeAmountRadioButton, sellerGetsTradeAmountRadioButton,
@ -141,7 +143,8 @@ public class DisputeSummaryWindow extends Overlay<DisputeSummaryWindow> {
TradeWalletService tradeWalletService, TradeWalletService tradeWalletService,
BtcWalletService btcWalletService, BtcWalletService btcWalletService,
TxFeeEstimationService txFeeEstimationService, TxFeeEstimationService txFeeEstimationService,
FeeService feeService) { FeeService feeService,
DaoFacade daoFacade) {
this.formatter = formatter; this.formatter = formatter;
this.mediationManager = mediationManager; this.mediationManager = mediationManager;
@ -150,6 +153,7 @@ public class DisputeSummaryWindow extends Overlay<DisputeSummaryWindow> {
this.btcWalletService = btcWalletService; this.btcWalletService = btcWalletService;
this.txFeeEstimationService = txFeeEstimationService; this.txFeeEstimationService = txFeeEstimationService;
this.feeService = feeService; this.feeService = feeService;
this.daoFacade = daoFacade;
type = Type.Confirmation; type = Type.Confirmation;
} }
@ -222,7 +226,7 @@ public class DisputeSummaryWindow extends Overlay<DisputeSummaryWindow> {
else else
disputeResult = dispute.getDisputeResultProperty().get(); disputeResult = dispute.getDisputeResultProperty().get();
peersDisputeOptional = getDisputeManager(dispute).getDisputesAsObservableList().stream() peersDisputeOptional = checkNotNull(getDisputeManager(dispute)).getDisputesAsObservableList().stream()
.filter(d -> dispute.getTradeId().equals(d.getTradeId()) && dispute.getTraderId() != d.getTraderId()) .filter(d -> dispute.getTradeId().equals(d.getTradeId()) && dispute.getTraderId() != d.getTraderId())
.findFirst(); .findFirst();
@ -382,14 +386,15 @@ public class DisputeSummaryWindow extends Overlay<DisputeSummaryWindow> {
.add(offer.getSellerSecurityDeposit()); .add(offer.getSellerSecurityDeposit());
Coin totalAmount = buyerAmount.add(sellerAmount); Coin totalAmount = buyerAmount.add(sellerAmount);
if (!totalAmount.isPositive()) { boolean isRefundAgent = getDisputeManager(dispute) instanceof RefundManager;
return false; if (isRefundAgent) {
} // We allow to spend less in case of RefundAgent or even zero to both, so in that case no payout tx will
// be made
if (getDisputeManager(dispute) instanceof RefundManager) {
// We allow to spend less in case of RefundAgent
return totalAmount.compareTo(available) <= 0; return totalAmount.compareTo(available) <= 0;
} else { } else {
if (!totalAmount.isPositive()) {
return false;
}
return totalAmount.compareTo(available) == 0; return totalAmount.compareTo(available) == 0;
} }
} }
@ -642,15 +647,15 @@ public class DisputeSummaryWindow extends Overlay<DisputeSummaryWindow> {
log.warn("dispute.getDepositTxSerialized is null"); log.warn("dispute.getDepositTxSerialized is null");
return; return;
} }
if (dispute.getSupportType() == SupportType.REFUND && if (dispute.getSupportType() == SupportType.REFUND &&
peersDisputeOptional.isPresent() && peersDisputeOptional.isPresent() &&
!peersDisputeOptional.get().isClosed()) { !peersDisputeOptional.get().isClosed()) {
showPayoutTxConfirmation(contract, disputeResult, showPayoutTxConfirmation(contract,
() -> { disputeResult,
doClose(closeTicketButton); () -> doCloseIfValid(closeTicketButton));
});
} else { } else {
doClose(closeTicketButton); doCloseIfValid(closeTicketButton);
} }
}); });
@ -684,28 +689,36 @@ public class DisputeSummaryWindow extends Overlay<DisputeSummaryWindow> {
formatter.formatCoinWithCode(sellerPayoutAmount), formatter.formatCoinWithCode(sellerPayoutAmount),
sellerPayoutAddressString); sellerPayoutAddressString);
} }
new Popup().width(900) if (outputAmount.isPositive()) {
.headLine(Res.get("disputeSummaryWindow.close.txDetails.headline")) new Popup().width(900)
.confirmation(Res.get("disputeSummaryWindow.close.txDetails", .headLine(Res.get("disputeSummaryWindow.close.txDetails.headline"))
formatter.formatCoinWithCode(inputAmount), .confirmation(Res.get("disputeSummaryWindow.close.txDetails",
buyerDetails, formatter.formatCoinWithCode(inputAmount),
sellerDetails, buyerDetails,
formatter.formatCoinWithCode(fee), sellerDetails,
feePerByte, formatter.formatCoinWithCode(fee),
kb)) feePerByte,
.actionButtonText(Res.get("shared.yes")) kb))
.onAction(() -> { .actionButtonText(Res.get("shared.yes"))
doPayout(buyerPayoutAmount, .onAction(() -> {
sellerPayoutAmount, doPayout(buyerPayoutAmount,
fee, sellerPayoutAmount,
buyerPayoutAddressString, fee,
sellerPayoutAddressString, buyerPayoutAddressString,
resultHandler); sellerPayoutAddressString,
}) resultHandler);
.closeButtonText(Res.get("shared.cancel")) })
.onClose(() -> { .closeButtonText(Res.get("shared.cancel"))
}) .show();
.show(); } else {
// No payout will be made
new Popup().headLine(Res.get("disputeSummaryWindow.close.noPayout.headline"))
.confirmation(Res.get("disputeSummaryWindow.close.noPayout.text"))
.actionButtonText(Res.get("shared.yes"))
.onAction(resultHandler::handleResult)
.closeButtonText(Res.get("shared.cancel"))
.show();
}
} }
private void doPayout(Coin buyerPayoutAmount, private void doPayout(Coin buyerPayoutAmount,
@ -720,7 +733,6 @@ public class DisputeSummaryWindow extends Overlay<DisputeSummaryWindow> {
fee, fee,
buyerPayoutAddressString, buyerPayoutAddressString,
sellerPayoutAddressString); sellerPayoutAddressString);
log.error("transaction " + tx);
tradeWalletService.broadcastTx(tx, new TxBroadcaster.Callback() { tradeWalletService.broadcastTx(tx, new TxBroadcaster.Callback() {
@Override @Override
public void onSuccess(Transaction transaction) { public void onSuccess(Transaction transaction) {
@ -731,7 +743,6 @@ public class DisputeSummaryWindow extends Overlay<DisputeSummaryWindow> {
public void onFailure(TxBroadcastException exception) { public void onFailure(TxBroadcastException exception) {
log.error("TxBroadcastException at doPayout", exception); log.error("TxBroadcastException at doPayout", exception);
new Popup().error(exception.toString()).show(); new Popup().error(exception.toString()).show();
;
} }
}); });
} catch (InsufficientMoneyException | WalletException | TransactionVerificationException e) { } catch (InsufficientMoneyException | WalletException | TransactionVerificationException e) {
@ -740,32 +751,105 @@ public class DisputeSummaryWindow extends Overlay<DisputeSummaryWindow> {
} }
} }
private void doCloseIfValid(Button closeTicketButton) {
var disputeManager = checkNotNull(getDisputeManager(dispute));
try {
TradeDataValidation.validateDonationAddress(dispute.getDonationAddressOfDelayedPayoutTx(), daoFacade);
TradeDataValidation.testIfDisputeTriesReplay(dispute, disputeManager.getDisputesAsObservableList());
doClose(closeTicketButton);
} catch (TradeDataValidation.AddressException exception) {
String addressAsString = dispute.getDonationAddressOfDelayedPayoutTx();
String tradeId = dispute.getTradeId();
// For mediators we do not enforce that the case cannot be closed to stay flexible,
// but for refund agents we do.
if (disputeManager instanceof MediationManager) {
new Popup().width(900)
.warning(Res.get("support.warning.disputesWithInvalidDonationAddress",
addressAsString,
daoFacade.getAllDonationAddresses(),
tradeId,
Res.get("support.warning.disputesWithInvalidDonationAddress.mediator")))
.onAction(() -> {
doClose(closeTicketButton);
})
.actionButtonText(Res.get("shared.yes"))
.closeButtonText(Res.get("shared.no"))
.show();
} else {
new Popup().width(900)
.warning(Res.get("support.warning.disputesWithInvalidDonationAddress",
addressAsString,
daoFacade.getAllDonationAddresses(),
tradeId,
Res.get("support.warning.disputesWithInvalidDonationAddress.refundAgent")))
.show();
}
} catch (TradeDataValidation.DisputeReplayException exception) {
if (disputeManager instanceof MediationManager) {
new Popup().width(900)
.warning(exception.getMessage())
.onAction(() -> {
doClose(closeTicketButton);
})
.actionButtonText(Res.get("shared.yes"))
.closeButtonText(Res.get("shared.no"))
.show();
} else {
new Popup().width(900)
.warning(exception.getMessage())
.show();
}
}
}
private void doClose(Button closeTicketButton) { private void doClose(Button closeTicketButton) {
DisputeManager<? extends DisputeList<? extends DisputeList>> disputeManager = getDisputeManager(dispute);
if (disputeManager == null) {
return;
}
boolean isRefundAgent = disputeManager instanceof RefundManager;
disputeResult.setLoserPublisher(isLoserPublisherCheckBox.isSelected()); disputeResult.setLoserPublisher(isLoserPublisherCheckBox.isSelected());
disputeResult.setCloseDate(new Date()); disputeResult.setCloseDate(new Date());
dispute.setDisputeResult(disputeResult); dispute.setDisputeResult(disputeResult);
dispute.setIsClosed(true); dispute.setIsClosed(true);
DisputeResult.Reason reason = disputeResult.getReason(); DisputeResult.Reason reason = disputeResult.getReason();
String text = Res.get("disputeSummaryWindow.close.msg",
summaryNotesTextArea.textProperty().unbindBidirectional(disputeResult.summaryNotesProperty());
String role = isRefundAgent ? Res.get("shared.refundAgent") : Res.get("shared.mediator");
String agentNodeAddress = checkNotNull(disputeManager.getAgentNodeAddress(dispute)).getFullAddress();
Contract contract = dispute.getContract();
String currencyCode = contract.getOfferPayload().getCurrencyCode();
String amount = formatter.formatCoinWithCode(contract.getTradeAmount());
String textToSign = Res.get("disputeSummaryWindow.close.msg",
DisplayUtils.formatDateTime(disputeResult.getCloseDate()), DisplayUtils.formatDateTime(disputeResult.getCloseDate()),
role,
agentNodeAddress,
dispute.getShortTradeId(),
currencyCode,
amount,
formatter.formatCoinWithCode(disputeResult.getBuyerPayoutAmount()), formatter.formatCoinWithCode(disputeResult.getBuyerPayoutAmount()),
formatter.formatCoinWithCode(disputeResult.getSellerPayoutAmount()), formatter.formatCoinWithCode(disputeResult.getSellerPayoutAmount()),
Res.get("disputeSummaryWindow.reason." + reason.name()), Res.get("disputeSummaryWindow.reason." + reason.name()),
disputeResult.summaryNotesProperty().get()); disputeResult.summaryNotesProperty().get()
);
if (reason == DisputeResult.Reason.OPTION_TRADE && if (reason == DisputeResult.Reason.OPTION_TRADE &&
dispute.getChatMessages().size() > 1 && dispute.getChatMessages().size() > 1 &&
dispute.getChatMessages().get(1).isSystemMessage()) { dispute.getChatMessages().get(1).isSystemMessage()) {
text += "\n\n" + dispute.getChatMessages().get(1).getMessage(); textToSign += "\n" + dispute.getChatMessages().get(1).getMessage() + "\n";
} }
if (dispute.getSupportType() == SupportType.MEDIATION) { String summaryText = DisputeSummaryVerification.signAndApply(disputeManager, disputeResult, textToSign);
text += Res.get("disputeSummaryWindow.close.nextStepsForMediation");
} else if (dispute.getSupportType() == SupportType.REFUND) { if (isRefundAgent) {
text += Res.get("disputeSummaryWindow.close.nextStepsForRefundAgentArbitration"); summaryText += Res.get("disputeSummaryWindow.close.nextStepsForRefundAgentArbitration");
} else {
summaryText += Res.get("disputeSummaryWindow.close.nextStepsForMediation");
} }
checkNotNull(getDisputeManager(dispute)).sendDisputeResultMessage(disputeResult, dispute, text); disputeManager.sendDisputeResultMessage(disputeResult, dispute, summaryText);
if (peersDisputeOptional.isPresent() && !peersDisputeOptional.get().isClosed() && !DevEnv.isDevMode()) { if (peersDisputeOptional.isPresent() && !peersDisputeOptional.get().isClosed() && !DevEnv.isDevMode()) {
UserThread.runAfter(() -> new Popup() UserThread.runAfter(() -> new Popup()

View file

@ -0,0 +1,96 @@
/*
* 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 <http://www.gnu.org/licenses/>.
*/
package bisq.desktop.main.overlays.windows;
import bisq.desktop.main.overlays.Overlay;
import bisq.desktop.main.support.dispute.DisputeSummaryVerification;
import bisq.core.locale.Res;
import bisq.core.support.dispute.mediation.mediator.MediatorManager;
import bisq.core.support.dispute.refund.refundagent.RefundAgentManager;
import javafx.scene.control.TextArea;
import javafx.scene.control.TextField;
import javafx.scene.layout.ColumnConstraints;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.Priority;
import javafx.geometry.HPos;
import javafx.geometry.Insets;
import lombok.extern.slf4j.Slf4j;
import static bisq.desktop.util.FormBuilder.addMultilineLabel;
import static bisq.desktop.util.FormBuilder.addTopLabelTextArea;
import static bisq.desktop.util.FormBuilder.addTopLabelTextField;
@Slf4j
public class VerifyDisputeResultSignatureWindow extends Overlay<VerifyDisputeResultSignatureWindow> {
private TextArea textArea;
private TextField resultTextField;
private final MediatorManager mediatorManager;
private final RefundAgentManager refundAgentManager;
public VerifyDisputeResultSignatureWindow(MediatorManager mediatorManager, RefundAgentManager refundAgentManager) {
this.mediatorManager = mediatorManager;
this.refundAgentManager = refundAgentManager;
type = Type.Attention;
}
public void show() {
if (headLine == null)
headLine = Res.get("support.sigCheck.popup.header");
width = 1050;
createGridPane();
addHeadLine();
addContent();
addButtons();
applyStyles();
display();
textArea.textProperty().addListener((observable, oldValue, newValue) -> {
resultTextField.setText(DisputeSummaryVerification.verifySignature(newValue,
mediatorManager,
refundAgentManager));
});
}
@Override
protected void createGridPane() {
gridPane = new GridPane();
gridPane.setHgap(5);
gridPane.setVgap(5);
gridPane.setPadding(new Insets(64, 64, 64, 64));
gridPane.setPrefWidth(width);
ColumnConstraints columnConstraints1 = new ColumnConstraints();
columnConstraints1.setHalignment(HPos.RIGHT);
columnConstraints1.setHgrow(Priority.SOMETIMES);
gridPane.getColumnConstraints().addAll(columnConstraints1);
}
private void addContent() {
addMultilineLabel(gridPane, ++rowIndex, Res.get("support.sigCheck.popup.info"), 0, width);
textArea = addTopLabelTextArea(gridPane, ++rowIndex, Res.get("support.sigCheck.popup.msg.label"),
Res.get("support.sigCheck.popup.msg.prompt")).second;
resultTextField = addTopLabelTextField(gridPane, ++rowIndex, Res.get("support.sigCheck.popup.result")).second;
}
}

View file

@ -49,6 +49,7 @@ import bisq.core.support.traderchat.TraderChatManager;
import bisq.core.trade.BuyerTrade; import bisq.core.trade.BuyerTrade;
import bisq.core.trade.SellerTrade; import bisq.core.trade.SellerTrade;
import bisq.core.trade.Trade; import bisq.core.trade.Trade;
import bisq.core.trade.TradeDataValidation;
import bisq.core.trade.TradeManager; import bisq.core.trade.TradeManager;
import bisq.core.user.Preferences; import bisq.core.user.Preferences;
import bisq.core.util.FormattingUtils; import bisq.core.util.FormattingUtils;
@ -78,6 +79,8 @@ import javafx.collections.ObservableList;
import org.bouncycastle.crypto.params.KeyParameter; import org.bouncycastle.crypto.params.KeyParameter;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicReference;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import lombok.Getter; import lombok.Getter;
@ -495,6 +498,28 @@ public class PendingTradesDataModel extends ActivatableDataModel {
// In case we re-open a dispute we allow Trade.DisputeState.REFUND_REQUESTED // In case we re-open a dispute we allow Trade.DisputeState.REFUND_REQUESTED
useRefundAgent = disputeState == Trade.DisputeState.MEDIATION_CLOSED || disputeState == Trade.DisputeState.REFUND_REQUESTED; useRefundAgent = disputeState == Trade.DisputeState.MEDIATION_CLOSED || disputeState == Trade.DisputeState.REFUND_REQUESTED;
AtomicReference<String> donationAddressString = new AtomicReference<>("");
Transaction delayedPayoutTx = trade.getDelayedPayoutTx();
try {
TradeDataValidation.validatePayoutTx(trade,
delayedPayoutTx,
daoFacade,
btcWalletService,
donationAddressString::set);
} catch (TradeDataValidation.ValidationException e) {
// The peer sent us an invalid donation address. We do not return here as we don't want to break
// mediation/arbitration and log only the issue. The dispute agent will run validation as well and will get
// a popup displayed to react.
log.error("DelayedPayoutTxValidation failed. {}", e.toString());
if (useRefundAgent) {
// We don't allow to continue and publish payout tx and open refund agent case.
// In case it was caused by some bug we want to prevent a wrong payout. In case its a scam attempt we
// want to protect the refund agent.
return;
}
}
ResultHandler resultHandler; ResultHandler resultHandler;
if (useMediation) { if (useMediation) {
// If no dispute state set we start with mediation // If no dispute state set we start with mediation
@ -523,6 +548,11 @@ public class PendingTradesDataModel extends ActivatableDataModel {
isSupportTicket, isSupportTicket,
SupportType.MEDIATION); SupportType.MEDIATION);
dispute.setDonationAddressOfDelayedPayoutTx(donationAddressString.get());
if (delayedPayoutTx != null) {
dispute.setDelayedPayoutTxId(delayedPayoutTx.getTxId().toString());
}
trade.setDisputeState(Trade.DisputeState.MEDIATION_REQUESTED); trade.setDisputeState(Trade.DisputeState.MEDIATION_REQUESTED);
disputeManager.sendOpenNewDisputeMessage(dispute, disputeManager.sendOpenNewDisputeMessage(dispute,
false, false,
@ -547,7 +577,7 @@ public class PendingTradesDataModel extends ActivatableDataModel {
} else if (useRefundAgent) { } else if (useRefundAgent) {
resultHandler = () -> navigation.navigateTo(MainView.class, SupportView.class, RefundClientView.class); resultHandler = () -> navigation.navigateTo(MainView.class, SupportView.class, RefundClientView.class);
if (trade.getDelayedPayoutTx() == null) { if (delayedPayoutTx == null) {
log.error("Delayed payout tx is missing"); log.error("Delayed payout tx is missing");
return; return;
} }
@ -562,13 +592,12 @@ public class PendingTradesDataModel extends ActivatableDataModel {
return; return;
} }
long lockTime = trade.getDelayedPayoutTx().getLockTime(); long lockTime = delayedPayoutTx.getLockTime();
int bestChainHeight = btcWalletService.getBestChainHeight(); int bestChainHeight = btcWalletService.getBestChainHeight();
long remaining = lockTime - bestChainHeight; long remaining = lockTime - bestChainHeight;
if (remaining > 0) { if (remaining > 0) {
new Popup() new Popup().instruction(Res.get("portfolio.pending.timeLockNotOver",
.instruction(Res.get("portfolio.pending.timeLockNotOver", FormattingUtils.getDateFromBlockHeight(remaining), remaining))
FormattingUtils.getDateFromBlockHeight(remaining), remaining))
.show(); .show();
return; return;
} }
@ -611,7 +640,8 @@ public class PendingTradesDataModel extends ActivatableDataModel {
} }
}); });
dispute.setDelayedPayoutTxId(trade.getDelayedPayoutTx().getTxId().toString()); dispute.setDonationAddressOfDelayedPayoutTx(donationAddressString.get());
dispute.setDelayedPayoutTxId(delayedPayoutTx.getTxId().toString());
trade.setDisputeState(Trade.DisputeState.REFUND_REQUESTED); trade.setDisputeState(Trade.DisputeState.REFUND_REQUESTED);

View file

@ -30,6 +30,7 @@ import bisq.core.locale.CurrencyUtil;
import bisq.core.locale.Res; import bisq.core.locale.Res;
import bisq.core.support.dispute.Dispute; import bisq.core.support.dispute.Dispute;
import bisq.core.support.dispute.DisputeResult; import bisq.core.support.dispute.DisputeResult;
import bisq.core.support.dispute.mediation.MediationResultState;
import bisq.core.trade.Contract; import bisq.core.trade.Contract;
import bisq.core.trade.Trade; import bisq.core.trade.Trade;
import bisq.core.user.Preferences; import bisq.core.user.Preferences;
@ -41,6 +42,9 @@ import bisq.common.ClockWatcher;
import bisq.common.UserThread; import bisq.common.UserThread;
import bisq.common.util.Tuple3; import bisq.common.util.Tuple3;
import org.bitcoinj.core.Transaction;
import org.bitcoinj.core.listeners.NewBestBlockListener;
import de.jensd.fx.fontawesome.AwesomeDude; import de.jensd.fx.fontawesome.AwesomeDude;
import de.jensd.fx.fontawesome.AwesomeIcon; import de.jensd.fx.fontawesome.AwesomeIcon;
@ -62,6 +66,7 @@ import javafx.geometry.Insets;
import org.fxmisc.easybind.EasyBind; import org.fxmisc.easybind.EasyBind;
import org.fxmisc.easybind.Subscription; import org.fxmisc.easybind.Subscription;
import javafx.beans.property.BooleanProperty;
import javafx.beans.value.ChangeListener; import javafx.beans.value.ChangeListener;
import java.util.Optional; import java.util.Optional;
@ -97,6 +102,8 @@ public abstract class TradeStepView extends AnchorPane {
private Popup acceptMediationResultPopup; private Popup acceptMediationResultPopup;
private BootstrapListener bootstrapListener; private BootstrapListener bootstrapListener;
private TradeSubView.ChatCallback chatCallback; private TradeSubView.ChatCallback chatCallback;
private final NewBestBlockListener newBestBlockListener;
private ChangeListener<Boolean> pendingTradesInitializedListener;
/////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////
@ -158,6 +165,10 @@ public abstract class TradeStepView extends AnchorPane {
updateTimeLeft(); updateTimeLeft();
} }
}; };
newBestBlockListener = block -> {
checkIfLockTimeIsOver();
};
} }
public void activate() { public void activate() {
@ -200,14 +211,34 @@ public abstract class TradeStepView extends AnchorPane {
} }
tradePeriodStateSubscription = EasyBind.subscribe(trade.tradePeriodStateProperty(), newValue -> { tradePeriodStateSubscription = EasyBind.subscribe(trade.tradePeriodStateProperty(), newValue -> {
if (newValue != null) if (newValue != null) {
updateTradePeriodState(newValue); updateTradePeriodState(newValue);
}
}); });
model.clockWatcher.addListener(clockListener); model.clockWatcher.addListener(clockListener);
if (infoLabel != null) if (infoLabel != null) {
infoLabel.setText(getInfoText()); infoLabel.setText(getInfoText());
}
BooleanProperty pendingTradesInitialized = model.dataModel.tradeManager.getPendingTradesInitialized();
if (pendingTradesInitialized.get()) {
onPendingTradesInitialized();
} else {
pendingTradesInitializedListener = (observable, oldValue, newValue) -> {
if (newValue) {
onPendingTradesInitialized();
UserThread.execute(() -> pendingTradesInitialized.removeListener(pendingTradesInitializedListener));
}
};
pendingTradesInitialized.addListener(pendingTradesInitializedListener);
}
}
protected void onPendingTradesInitialized() {
model.dataModel.btcWalletService.addNewBestBlockListener(newBestBlockListener);
checkIfLockTimeIsOver();
} }
private void registerSubscriptions() { private void registerSubscriptions() {
@ -262,6 +293,15 @@ public abstract class TradeStepView extends AnchorPane {
if (tradeStepInfo != null) if (tradeStepInfo != null)
tradeStepInfo.setOnAction(null); tradeStepInfo.setOnAction(null);
if (newBestBlockListener != null) {
model.dataModel.btcWalletService.removeNewBestBlockListener(newBestBlockListener);
}
if (acceptMediationResultPopup != null) {
acceptMediationResultPopup.hide();
acceptMediationResultPopup = null;
}
} }
/////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////
@ -445,6 +485,11 @@ public abstract class TradeStepView extends AnchorPane {
tradeStepInfo.setState(TradeStepInfo.State.IN_REFUND_REQUEST_SELF_REQUESTED); tradeStepInfo.setState(TradeStepInfo.State.IN_REFUND_REQUEST_SELF_REQUESTED);
}); });
if (acceptMediationResultPopup != null) {
acceptMediationResultPopup.hide();
acceptMediationResultPopup = null;
}
break; break;
case REFUND_REQUEST_STARTED_BY_PEER: case REFUND_REQUEST_STARTED_BY_PEER:
if (tradeStepInfo != null) { if (tradeStepInfo != null) {
@ -457,6 +502,11 @@ public abstract class TradeStepView extends AnchorPane {
if (tradeStepInfo != null) if (tradeStepInfo != null)
tradeStepInfo.setState(TradeStepInfo.State.IN_REFUND_REQUEST_PEER_REQUESTED); tradeStepInfo.setState(TradeStepInfo.State.IN_REFUND_REQUEST_PEER_REQUESTED);
}); });
if (acceptMediationResultPopup != null) {
acceptMediationResultPopup.hide();
acceptMediationResultPopup = null;
}
break; break;
case REFUND_REQUEST_CLOSED: case REFUND_REQUEST_CLOSED:
break; break;
@ -563,13 +613,34 @@ public abstract class TradeStepView extends AnchorPane {
String actionButtonText = hasSelfAccepted() ? String actionButtonText = hasSelfAccepted() ?
Res.get("portfolio.pending.mediationResult.popup.alreadyAccepted") : Res.get("shared.accept"); Res.get("portfolio.pending.mediationResult.popup.alreadyAccepted") : Res.get("shared.accept");
acceptMediationResultPopup = new Popup().width(900) String message;
.headLine(headLine) MediationResultState mediationResultState = checkNotNull(trade).getMediationResultState();
.instruction(Res.get("portfolio.pending.mediationResult.popup.info", if (mediationResultState == null) {
return;
}
switch (mediationResultState) {
case MEDIATION_RESULT_ACCEPTED:
case SIG_MSG_SENT:
case SIG_MSG_ARRIVED:
case SIG_MSG_IN_MAILBOX:
case SIG_MSG_SEND_FAILED:
message = Res.get("portfolio.pending.mediationResult.popup.selfAccepted.lockTimeOver",
FormattingUtils.getDateFromBlockHeight(remaining),
lockTime);
break;
default:
message = Res.get("portfolio.pending.mediationResult.popup.info",
myPayoutAmount, myPayoutAmount,
peersPayoutAmount, peersPayoutAmount,
FormattingUtils.getDateFromBlockHeight(remaining), FormattingUtils.getDateFromBlockHeight(remaining),
lockTime)) lockTime);
break;
}
acceptMediationResultPopup = new Popup().width(900)
.headLine(headLine)
.instruction(message)
.actionButtonText(actionButtonText) .actionButtonText(actionButtonText)
.onAction(() -> { .onAction(() -> {
model.dataModel.mediationManager.acceptMediationResult(trade, model.dataModel.mediationManager.acceptMediationResult(trade,
@ -656,6 +727,18 @@ public abstract class TradeStepView extends AnchorPane {
return trade.getDisputeState() != Trade.DisputeState.NO_DISPUTE; return trade.getDisputeState() != Trade.DisputeState.NO_DISPUTE;
} }
private void checkIfLockTimeIsOver() {
Transaction delayedPayoutTx = trade.getDelayedPayoutTx();
if (delayedPayoutTx != null) {
long lockTime = delayedPayoutTx.getLockTime();
int bestChainHeight = model.dataModel.btcWalletService.getBestChainHeight();
long remaining = lockTime - bestChainHeight;
if (remaining <= 0) {
openMediationResultPopup(Res.get("portfolio.pending.mediationResult.popup.headline", trade.getShortId()));
}
}
}
/////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////
// TradeDurationLimitInfo // TradeDurationLimitInfo

View file

@ -22,7 +22,7 @@ import bisq.desktop.main.portfolio.pendingtrades.PendingTradesViewModel;
import bisq.desktop.main.portfolio.pendingtrades.steps.TradeStepView; import bisq.desktop.main.portfolio.pendingtrades.steps.TradeStepView;
import bisq.core.locale.Res; import bisq.core.locale.Res;
import bisq.core.trade.DelayedPayoutTxValidation; import bisq.core.trade.TradeDataValidation;
public class BuyerStep1View extends TradeStepView { public class BuyerStep1View extends TradeStepView {
@ -35,23 +35,9 @@ public class BuyerStep1View extends TradeStepView {
} }
@Override @Override
public void activate() { protected void onPendingTradesInitialized() {
super.activate(); super.onPendingTradesInitialized();
validatePayoutTx();
try {
DelayedPayoutTxValidation.validatePayoutTx(trade,
trade.getDelayedPayoutTx(),
model.dataModel.daoFacade,
model.dataModel.btcWalletService);
} catch (DelayedPayoutTxValidation.DonationAddressException |
DelayedPayoutTxValidation.InvalidTxException |
DelayedPayoutTxValidation.AmountMismatchException |
DelayedPayoutTxValidation.InvalidLockTimeException |
DelayedPayoutTxValidation.MissingDelayedPayoutTxException e) {
if (!model.dataModel.tradeManager.isAllowFaultyDelayedTxs()) {
new Popup().warning(Res.get("portfolio.pending.invalidDelayedPayoutTx", e.getMessage())).show();
}
}
} }
@ -86,6 +72,28 @@ public class BuyerStep1View extends TradeStepView {
protected String getPeriodOverWarnText() { protected String getPeriodOverWarnText() {
return Res.get("portfolio.pending.step1.openForDispute"); return Res.get("portfolio.pending.step1.openForDispute");
} }
///////////////////////////////////////////////////////////////////////////////////////////
// Private
///////////////////////////////////////////////////////////////////////////////////////////
private void validatePayoutTx() {
try {
TradeDataValidation.validatePayoutTx(trade,
trade.getDelayedPayoutTx(),
model.dataModel.daoFacade,
model.dataModel.btcWalletService);
} catch (TradeDataValidation.MissingTxException ignore) {
// We don't react on those errors as a failed trade might get listed initially but getting removed from the
// trade manager after initPendingTrades which happens after activate might be called.
} catch (TradeDataValidation.ValidationException e) {
if (!model.dataModel.tradeManager.isAllowFaultyDelayedTxs()) {
new Popup().warning(Res.get("portfolio.pending.invalidDelayedPayoutTx", e.getMessage())).show();
}
}
}
} }

View file

@ -70,8 +70,8 @@ import bisq.core.payment.payload.PaymentAccountPayload;
import bisq.core.payment.payload.PaymentMethod; import bisq.core.payment.payload.PaymentMethod;
import bisq.core.payment.payload.USPostalMoneyOrderAccountPayload; import bisq.core.payment.payload.USPostalMoneyOrderAccountPayload;
import bisq.core.payment.payload.WesternUnionAccountPayload; import bisq.core.payment.payload.WesternUnionAccountPayload;
import bisq.core.trade.DelayedPayoutTxValidation;
import bisq.core.trade.Trade; import bisq.core.trade.Trade;
import bisq.core.trade.TradeDataValidation;
import bisq.core.user.DontShowAgainLookup; import bisq.core.user.DontShowAgainLookup;
import bisq.common.Timer; import bisq.common.Timer;
@ -117,21 +117,6 @@ public class BuyerStep2View extends TradeStepView {
public void activate() { public void activate() {
super.activate(); super.activate();
try {
DelayedPayoutTxValidation.validatePayoutTx(trade,
trade.getDelayedPayoutTx(),
model.dataModel.daoFacade,
model.dataModel.btcWalletService);
} catch (DelayedPayoutTxValidation.DonationAddressException |
DelayedPayoutTxValidation.InvalidTxException |
DelayedPayoutTxValidation.AmountMismatchException |
DelayedPayoutTxValidation.InvalidLockTimeException |
DelayedPayoutTxValidation.MissingDelayedPayoutTxException e) {
if (!model.dataModel.tradeManager.isAllowFaultyDelayedTxs()) {
new Popup().warning(Res.get("portfolio.pending.invalidDelayedPayoutTx", e.getMessage())).show();
}
}
if (timeoutTimer != null) if (timeoutTimer != null)
timeoutTimer.stop(); timeoutTimer.stop();
@ -209,6 +194,13 @@ public class BuyerStep2View extends TradeStepView {
} }
} }
@Override
protected void onPendingTradesInitialized() {
super.onPendingTradesInitialized();
validatePayoutTx();
}
/////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////
// Content // Content
/////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////
@ -623,6 +615,22 @@ public class BuyerStep2View extends TradeStepView {
} }
} }
private void validatePayoutTx() {
try {
TradeDataValidation.validatePayoutTx(trade,
trade.getDelayedPayoutTx(),
model.dataModel.daoFacade,
model.dataModel.btcWalletService);
} catch (TradeDataValidation.MissingTxException ignore) {
// We don't react on those errors as a failed trade might get listed initially but getting removed from the
// trade manager after initPendingTrades which happens after activate might be called.
} catch (TradeDataValidation.ValidationException e) {
if (!model.dataModel.tradeManager.isAllowFaultyDelayedTxs()) {
new Popup().warning(Res.get("portfolio.pending.invalidDelayedPayoutTx", e.getMessage())).show();
}
}
}
@Override @Override
protected void updateConfirmButtonDisableState(boolean isDisabled) { protected void updateConfirmButtonDisableState(boolean isDisabled) {
confirmButton.setDisable(isDisabled); confirmButton.setDisable(isDisabled);

View file

@ -0,0 +1,102 @@
/*
* 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 <http://www.gnu.org/licenses/>.
*/
package bisq.desktop.main.support.dispute;
import bisq.core.locale.Res;
import bisq.core.support.dispute.DisputeList;
import bisq.core.support.dispute.DisputeManager;
import bisq.core.support.dispute.DisputeResult;
import bisq.core.support.dispute.agent.DisputeAgent;
import bisq.core.support.dispute.mediation.mediator.MediatorManager;
import bisq.core.support.dispute.refund.refundagent.RefundAgentManager;
import bisq.network.p2p.NodeAddress;
import bisq.common.crypto.CryptoException;
import bisq.common.crypto.Hash;
import bisq.common.crypto.Sig;
import bisq.common.util.Utilities;
import java.security.KeyPair;
import java.security.PublicKey;
import lombok.extern.slf4j.Slf4j;
import static com.google.common.base.Preconditions.checkNotNull;
@Slf4j
public class DisputeSummaryVerification {
// Must not change as it is used for splitting the text for verifying the signature of the summary message
private static final String SEPARATOR1 = "\n-----BEGIN SIGNATURE-----\n";
private static final String SEPARATOR2 = "\n-----END SIGNATURE-----\n";
public static String signAndApply(DisputeManager<? extends DisputeList<? extends DisputeList>> disputeManager,
DisputeResult disputeResult,
String textToSign) {
byte[] hash = Hash.getSha256Hash(textToSign);
KeyPair signatureKeyPair = disputeManager.getSignatureKeyPair();
String sigAsHex;
try {
byte[] signature = Sig.sign(signatureKeyPair.getPrivate(), hash);
sigAsHex = Utilities.encodeToHex(signature);
disputeResult.setArbitratorSignature(signature);
} catch (CryptoException e) {
sigAsHex = "Signing failed";
}
return Res.get("disputeSummaryWindow.close.msgWithSig",
textToSign,
SEPARATOR1,
sigAsHex,
SEPARATOR2);
}
public static String verifySignature(String input,
MediatorManager mediatorManager,
RefundAgentManager refundAgentManager) {
try {
String[] parts = input.split(SEPARATOR1);
String textToSign = parts[0];
String fullAddress = textToSign.split("\n")[1].split(": ")[1];
NodeAddress nodeAddress = new NodeAddress(fullAddress);
DisputeAgent disputeAgent = mediatorManager.getDisputeAgentByNodeAddress(nodeAddress).orElse(null);
if (disputeAgent == null) {
disputeAgent = refundAgentManager.getDisputeAgentByNodeAddress(nodeAddress).orElse(null);
}
checkNotNull(disputeAgent);
PublicKey pubKey = disputeAgent.getPubKeyRing().getSignaturePubKey();
String sigString = parts[1].split(SEPARATOR2)[0];
byte[] sig = Utilities.decodeFromHex(sigString);
byte[] hash = Hash.getSha256Hash(textToSign);
try {
boolean result = Sig.verify(pubKey, hash, sig);
if (result) {
return Res.get("support.sigCheck.popup.success");
} else {
return Res.get("support.sigCheck.popup.failed");
}
} catch (CryptoException e) {
return Res.get("support.sigCheck.popup.failed");
}
} catch (Throwable e) {
return Res.get("support.sigCheck.popup.invalidFormat");
}
}
}

View file

@ -28,12 +28,14 @@ import bisq.desktop.main.overlays.windows.ContractWindow;
import bisq.desktop.main.overlays.windows.DisputeSummaryWindow; import bisq.desktop.main.overlays.windows.DisputeSummaryWindow;
import bisq.desktop.main.overlays.windows.SendPrivateNotificationWindow; import bisq.desktop.main.overlays.windows.SendPrivateNotificationWindow;
import bisq.desktop.main.overlays.windows.TradeDetailsWindow; import bisq.desktop.main.overlays.windows.TradeDetailsWindow;
import bisq.desktop.main.overlays.windows.VerifyDisputeResultSignatureWindow;
import bisq.desktop.main.shared.ChatView; import bisq.desktop.main.shared.ChatView;
import bisq.desktop.util.DisplayUtils; import bisq.desktop.util.DisplayUtils;
import bisq.desktop.util.GUIUtil; import bisq.desktop.util.GUIUtil;
import bisq.core.account.witness.AccountAgeWitnessService; import bisq.core.account.witness.AccountAgeWitnessService;
import bisq.core.alert.PrivateNotificationManager; import bisq.core.alert.PrivateNotificationManager;
import bisq.core.dao.DaoFacade;
import bisq.core.locale.CurrencyUtil; import bisq.core.locale.CurrencyUtil;
import bisq.core.locale.Res; import bisq.core.locale.Res;
import bisq.core.support.SupportType; import bisq.core.support.SupportType;
@ -42,6 +44,8 @@ import bisq.core.support.dispute.DisputeList;
import bisq.core.support.dispute.DisputeManager; import bisq.core.support.dispute.DisputeManager;
import bisq.core.support.dispute.DisputeResult; import bisq.core.support.dispute.DisputeResult;
import bisq.core.support.dispute.DisputeSession; import bisq.core.support.dispute.DisputeSession;
import bisq.core.support.dispute.mediation.mediator.MediatorManager;
import bisq.core.support.dispute.refund.refundagent.RefundAgentManager;
import bisq.core.support.messages.ChatMessage; import bisq.core.support.messages.ChatMessage;
import bisq.core.trade.Contract; import bisq.core.trade.Contract;
import bisq.core.trade.Trade; import bisq.core.trade.Trade;
@ -58,8 +62,6 @@ import bisq.common.util.Utilities;
import org.bitcoinj.core.Coin; import org.bitcoinj.core.Coin;
import com.google.common.collect.Lists;
import de.jensd.fx.glyphs.materialdesignicons.MaterialDesignIcon; import de.jensd.fx.glyphs.materialdesignicons.MaterialDesignIcon;
import javafx.scene.control.Button; import javafx.scene.control.Button;
@ -89,9 +91,8 @@ import javafx.collections.transformation.FilteredList;
import javafx.collections.transformation.SortedList; import javafx.collections.transformation.SortedList;
import javafx.util.Callback; import javafx.util.Callback;
import javafx.util.Duration;
import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat; import java.text.SimpleDateFormat;
import java.util.ArrayList; import java.util.ArrayList;
@ -102,6 +103,7 @@ import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Optional; import java.util.Optional;
import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
import lombok.Getter; import lombok.Getter;
@ -110,6 +112,32 @@ import javax.annotation.Nullable;
import static bisq.desktop.util.FormBuilder.getIconForLabel; import static bisq.desktop.util.FormBuilder.getIconForLabel;
public abstract class DisputeView extends ActivatableView<VBox, Void> { public abstract class DisputeView extends ActivatableView<VBox, Void> {
public enum FilterResult {
NO_MATCH("No Match"),
NO_FILTER("No filter text"),
OPEN_DISPUTES("Open disputes"),
TRADE_ID("Trade ID"),
OPENING_DATE("Opening date"),
BUYER_NODE_ADDRESS("Buyer node address"),
SELLER_NODE_ADDRESS("Seller node address"),
BUYER_ACCOUNT_DETAILS("Buyer account details"),
SELLER_ACCOUNT_DETAILS("Seller account details"),
DEPOSIT_TX("Deposit tx ID"),
PAYOUT_TX("Payout tx ID"),
DEL_PAYOUT_TX("Delayed payout tx ID"),
RESULT_MESSAGE("Result message"),
REASON("Reason"),
JSON("Contract as json");
// Used in tooltip at search string to show where the match was found
@Getter
private final String displayString;
FilterResult(String displayString) {
this.displayString = displayString;
}
}
protected final DisputeManager<? extends DisputeList<? extends DisputeList>> disputeManager; protected final DisputeManager<? extends DisputeList<? extends DisputeList>> disputeManager;
protected final KeyRing keyRing; protected final KeyRing keyRing;
@ -121,6 +149,9 @@ public abstract class DisputeView extends ActivatableView<VBox, Void> {
private final TradeDetailsWindow tradeDetailsWindow; private final TradeDetailsWindow tradeDetailsWindow;
private final AccountAgeWitnessService accountAgeWitnessService; private final AccountAgeWitnessService accountAgeWitnessService;
private final MediatorManager mediatorManager;
private final RefundAgentManager refundAgentManager;
protected final DaoFacade daoFacade;
private final boolean useDevPrivilegeKeys; private final boolean useDevPrivilegeKeys;
protected TableView<Dispute> tableView; protected TableView<Dispute> tableView;
@ -136,7 +167,7 @@ public abstract class DisputeView extends ActivatableView<VBox, Void> {
protected FilteredList<Dispute> filteredList; protected FilteredList<Dispute> filteredList;
protected InputTextField filterTextField; protected InputTextField filterTextField;
private ChangeListener<String> filterTextFieldListener; private ChangeListener<String> filterTextFieldListener;
protected AutoTooltipButton reOpenButton, sendPrivateNotificationButton, reportButton, fullReportButton; protected AutoTooltipButton sigCheckButton, reOpenButton, sendPrivateNotificationButton, reportButton, fullReportButton;
private Map<String, ListChangeListener<ChatMessage>> disputeChatMessagesListeners = new HashMap<>(); private Map<String, ListChangeListener<ChatMessage>> disputeChatMessagesListeners = new HashMap<>();
@Nullable @Nullable
private ListChangeListener<Dispute> disputesListener; // Only set in mediation cases private ListChangeListener<Dispute> disputesListener; // Only set in mediation cases
@ -157,6 +188,9 @@ public abstract class DisputeView extends ActivatableView<VBox, Void> {
ContractWindow contractWindow, ContractWindow contractWindow,
TradeDetailsWindow tradeDetailsWindow, TradeDetailsWindow tradeDetailsWindow,
AccountAgeWitnessService accountAgeWitnessService, AccountAgeWitnessService accountAgeWitnessService,
MediatorManager mediatorManager,
RefundAgentManager refundAgentManager,
DaoFacade daoFacade,
boolean useDevPrivilegeKeys) { boolean useDevPrivilegeKeys) {
this.disputeManager = disputeManager; this.disputeManager = disputeManager;
this.keyRing = keyRing; this.keyRing = keyRing;
@ -167,6 +201,9 @@ public abstract class DisputeView extends ActivatableView<VBox, Void> {
this.contractWindow = contractWindow; this.contractWindow = contractWindow;
this.tradeDetailsWindow = tradeDetailsWindow; this.tradeDetailsWindow = tradeDetailsWindow;
this.accountAgeWitnessService = accountAgeWitnessService; this.accountAgeWitnessService = accountAgeWitnessService;
this.mediatorManager = mediatorManager;
this.refundAgentManager = refundAgentManager;
this.daoFacade = daoFacade;
this.useDevPrivilegeKeys = useDevPrivilegeKeys; this.useDevPrivilegeKeys = useDevPrivilegeKeys;
} }
@ -177,6 +214,10 @@ public abstract class DisputeView extends ActivatableView<VBox, Void> {
HBox.setHgrow(label, Priority.NEVER); HBox.setHgrow(label, Priority.NEVER);
filterTextField = new InputTextField(); filterTextField = new InputTextField();
Tooltip tooltip = new Tooltip();
tooltip.setShowDelay(Duration.millis(100));
tooltip.setShowDuration(Duration.seconds(10));
filterTextField.setTooltip(tooltip);
filterTextFieldListener = (observable, oldValue, newValue) -> applyFilteredListPredicate(filterTextField.getText()); filterTextFieldListener = (observable, oldValue, newValue) -> applyFilteredListPredicate(filterTextField.getText());
HBox.setHgrow(filterTextField, Priority.NEVER); HBox.setHgrow(filterTextField, Priority.NEVER);
@ -222,6 +263,12 @@ public abstract class DisputeView extends ActivatableView<VBox, Void> {
showFullReport(); showFullReport();
}); });
sigCheckButton = new AutoTooltipButton(Res.get("support.sigCheck.button"));
HBox.setHgrow(sigCheckButton, Priority.NEVER);
sigCheckButton.setOnAction(e -> {
new VerifyDisputeResultSignatureWindow(mediatorManager, refundAgentManager).show();
});
Pane spacer = new Pane(); Pane spacer = new Pane();
HBox.setHgrow(spacer, Priority.ALWAYS); HBox.setHgrow(spacer, Priority.ALWAYS);
@ -234,7 +281,8 @@ public abstract class DisputeView extends ActivatableView<VBox, Void> {
reOpenButton, reOpenButton,
sendPrivateNotificationButton, sendPrivateNotificationButton,
reportButton, reportButton,
fullReportButton); fullReportButton,
sigCheckButton);
VBox.setVgrow(filterBox, Priority.NEVER); VBox.setVgrow(filterBox, Priority.NEVER);
tableView = new TableView<>(); tableView = new TableView<>();
@ -255,7 +303,8 @@ public abstract class DisputeView extends ActivatableView<VBox, Void> {
protected void activate() { protected void activate() {
filterTextField.textProperty().addListener(filterTextFieldListener); filterTextField.textProperty().addListener(filterTextFieldListener);
filteredList = new FilteredList<>(disputeManager.getDisputesAsObservableList()); ObservableList<Dispute> disputesAsObservableList = disputeManager.getDisputesAsObservableList();
filteredList = new FilteredList<>(disputesAsObservableList);
applyFilteredListPredicate(filterTextField.getText()); applyFilteredListPredicate(filterTextField.getText());
sortedList = new SortedList<>(filteredList); sortedList = new SortedList<>(filteredList);
@ -276,54 +325,6 @@ public abstract class DisputeView extends ActivatableView<VBox, Void> {
chatView.scrollToBottom(); chatView.scrollToBottom();
} }
// If doPrint=true we print out a html page which opens tabs with all deposit txs
// (firefox needs about:config change to allow > 20 tabs)
// Useful to check if there any funds in not finished trades (no payout tx done).
// Last check 10.02.2017 found 8 trades and we contacted all traders as far as possible (email if available
// otherwise in-app private notification)
boolean doPrint = false;
//noinspection ConstantConditions
if (doPrint) {
try {
DateFormat formatter = new SimpleDateFormat("dd/MM/yy");
//noinspection UnusedAssignment
Date startDate = formatter.parse("10/02/17");
startDate = new Date(0); // print all from start
HashMap<String, Dispute> map = new HashMap<>();
disputeManager.getDisputesAsObservableList().forEach(dispute -> map.put(dispute.getDepositTxId(), dispute));
final Date finalStartDate = startDate;
List<Dispute> disputes = new ArrayList<>(map.values());
disputes.sort(Comparator.comparing(Dispute::getOpeningDate));
List<List<Dispute>> subLists = Lists.partition(disputes, 1000);
StringBuilder sb = new StringBuilder();
// We don't translate that as it is not intended for the public
subLists.forEach(list -> {
StringBuilder sb1 = new StringBuilder("\n<html><head><script type=\"text/javascript\">function load(){\n");
StringBuilder sb2 = new StringBuilder("\n}</script></head><body onload=\"load()\">\n");
list.forEach(dispute -> {
if (dispute.getOpeningDate().after(finalStartDate)) {
String txId = dispute.getDepositTxId();
sb1.append("window.open(\"https://blockchain.info/tx/").append(txId).append("\", '_blank');\n");
sb2.append("Dispute ID: ").append(dispute.getId()).
append(" Tx ID: ").
append("<a href=\"https://blockchain.info/tx/").append(txId).append("\">").
append(txId).append("</a> ").
append("Opening date: ").append(formatter.format(dispute.getOpeningDate())).append("<br/>\n");
}
});
sb2.append("</body></html>");
String res = sb1.toString() + sb2.toString();
sb.append(res).append("\n\n\n");
});
log.info(sb.toString());
} catch (ParseException ignore) {
}
}
GUIUtil.requestFocus(filterTextField); GUIUtil.requestFocus(filterTextField);
} }
@ -385,10 +386,89 @@ public abstract class DisputeView extends ActivatableView<VBox, Void> {
protected abstract DisputeSession getConcreteDisputeChatSession(Dispute dispute); protected abstract DisputeSession getConcreteDisputeChatSession(Dispute dispute);
protected void applyFilteredListPredicate(String filterString) { protected void applyFilteredListPredicate(String filterString) {
// If in trader view we must not display arbitrators own disputes as trader (must not happen anyway) AtomicReference<FilterResult> filterResult = new AtomicReference<>(FilterResult.NO_FILTER);
filteredList.setPredicate(dispute -> !dispute.getAgentPubKeyRing().equals(keyRing.getPubKeyRing())); filteredList.setPredicate(dispute -> {
filterResult.set(getFilterResult(dispute, filterString));
return filterResult.get() != FilterResult.NO_MATCH;
});
if (filterResult.get() == FilterResult.NO_MATCH) {
filterTextField.getTooltip().setText("No matches found");
} else if (filterResult.get() == FilterResult.NO_FILTER) {
filterTextField.getTooltip().setText("No filter applied");
} else if (filterResult.get() == FilterResult.OPEN_DISPUTES) {
filterTextField.getTooltip().setText("Show all open disputes");
} else {
filterTextField.getTooltip().setText("Data matching filter string: " + filterResult.get().getDisplayString());
}
} }
protected FilterResult getFilterResult(Dispute dispute, String filterTerm) {
String filter = filterTerm.toLowerCase();
if (filter.isEmpty()) {
return FilterResult.NO_FILTER;
}
// For open filter we do not want to continue further as json data would cause a match
if (filter.equalsIgnoreCase("open")) {
return !dispute.isClosed() ? FilterResult.OPEN_DISPUTES : FilterResult.NO_MATCH;
}
if (dispute.getTradeId().toLowerCase().contains(filter)) {
return FilterResult.TRADE_ID;
}
if (DisplayUtils.formatDate(dispute.getOpeningDate()).toLowerCase().contains(filter)) {
return FilterResult.OPENING_DATE;
}
if (dispute.getContract().getBuyerNodeAddress().getFullAddress().contains(filter)) {
return FilterResult.BUYER_NODE_ADDRESS;
}
if (dispute.getContract().getSellerNodeAddress().getFullAddress().contains(filter)) {
return FilterResult.SELLER_NODE_ADDRESS;
}
if (dispute.getContract().getBuyerPaymentAccountPayload().getPaymentDetails().toLowerCase().contains(filter)) {
return FilterResult.BUYER_ACCOUNT_DETAILS;
}
if (dispute.getContract().getSellerPaymentAccountPayload().getPaymentDetails().toLowerCase().contains(filter)) {
return FilterResult.SELLER_ACCOUNT_DETAILS;
}
if (dispute.getDepositTxId() != null && dispute.getDepositTxId().contains(filter)) {
return FilterResult.DEPOSIT_TX;
}
if (dispute.getPayoutTxId() != null && dispute.getPayoutTxId().contains(filter)) {
return FilterResult.PAYOUT_TX;
}
if (dispute.getDelayedPayoutTxId() != null && dispute.getDelayedPayoutTxId().contains(filter)) {
return FilterResult.DEL_PAYOUT_TX;
}
DisputeResult disputeResult = dispute.getDisputeResultProperty().get();
if (disputeResult != null) {
ChatMessage chatMessage = disputeResult.getChatMessage();
if (chatMessage != null && chatMessage.getMessage().toLowerCase().contains(filter)) {
return FilterResult.RESULT_MESSAGE;
}
if (disputeResult.getReason().name().toLowerCase().contains(filter)) {
return FilterResult.REASON;
}
}
if (dispute.getContractAsJson().toLowerCase().contains(filter)) {
return FilterResult.JSON;
}
return FilterResult.NO_MATCH;
}
protected void reOpenDisputeFromButton() { protected void reOpenDisputeFromButton() {
reOpenDispute(); reOpenDispute();
} }
@ -412,16 +492,6 @@ public abstract class DisputeView extends ActivatableView<VBox, Void> {
} }
} }
protected boolean anyMatchOfFilterString(Dispute dispute, String filterString) {
boolean matchesTradeId = dispute.getTradeId().contains(filterString);
boolean matchesDate = DisplayUtils.formatDate(dispute.getOpeningDate()).contains(filterString);
boolean isBuyerOnion = dispute.getContract().getBuyerNodeAddress().getFullAddress().contains(filterString);
boolean isSellerOnion = dispute.getContract().getSellerNodeAddress().getFullAddress().contains(filterString);
boolean matchesBuyersPaymentAccountData = dispute.getContract().getBuyerPaymentAccountPayload().getPaymentDetails().contains(filterString);
boolean matchesSellersPaymentAccountData = dispute.getContract().getSellerPaymentAccountPayload().getPaymentDetails().contains(filterString);
return matchesTradeId || matchesDate || isBuyerOnion || isSellerOnion ||
matchesBuyersPaymentAccountData || matchesSellersPaymentAccountData;
}
/////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////
// UI actions // UI actions
@ -547,6 +617,36 @@ public abstract class DisputeView extends ActivatableView<VBox, Void> {
map.forEach((key, value) -> allDisputes.add(value)); map.forEach((key, value) -> allDisputes.add(value));
allDisputes.sort(Comparator.comparing(o -> !o.isEmpty() ? o.get(0).getOpeningDate() : new Date(0))); allDisputes.sort(Comparator.comparing(o -> !o.isEmpty() ? o.get(0).getOpeningDate() : new Date(0)));
StringBuilder stringBuilder = new StringBuilder(); StringBuilder stringBuilder = new StringBuilder();
StringBuilder csvStringBuilder = new StringBuilder();
csvStringBuilder.append("Dispute nr").append(";")
.append("Closed during cycle").append(";")
.append("Status").append(";")
.append("Trade date").append(";")
.append("Trade ID").append(";")
.append("Offer version").append(";")
.append("Opening date").append(";")
.append("Close date").append(";")
.append("Duration").append(";")
.append("Currency").append(";")
.append("Trade amount").append(";")
.append("Payment method").append(";")
.append("Buyer account details").append(";")
.append("Seller account details").append(";")
.append("Buyer address").append(";")
.append("Seller address").append(";")
.append("Buyer security deposit").append(";")
.append("Seller security deposit").append(";")
.append("Dispute opened by").append(";")
.append("Payout to buyer").append(";")
.append("Payout to seller").append(";")
.append("Winner").append(";")
.append("Reason").append(";")
.append("Summary notes").append(";")
.append("Summary notes (other trader)");
Map<Integer, Date> blockStartDateByCycleIndex = daoFacade.getBlockStartDateByCycleIndex();
SimpleDateFormat dateFormatter = new SimpleDateFormat("yyyy MM dd HH:mm:ss");
AtomicInteger disputeIndex = new AtomicInteger(); AtomicInteger disputeIndex = new AtomicInteger();
allDisputes.forEach(disputesPerTrade -> { allDisputes.forEach(disputesPerTrade -> {
if (disputesPerTrade.size() > 0) { if (disputesPerTrade.size() > 0) {
@ -561,38 +661,76 @@ public abstract class DisputeView extends ActivatableView<VBox, Void> {
disputeResult.getWinner() == DisputeResult.Winner.BUYER ? "Buyer" : "Seller"; disputeResult.getWinner() == DisputeResult.Winner.BUYER ? "Buyer" : "Seller";
String buyerPayoutAmount = disputeResult != null ? disputeResult.getBuyerPayoutAmount().toFriendlyString() : ""; String buyerPayoutAmount = disputeResult != null ? disputeResult.getBuyerPayoutAmount().toFriendlyString() : "";
String sellerPayoutAmount = disputeResult != null ? disputeResult.getSellerPayoutAmount().toFriendlyString() : ""; String sellerPayoutAmount = disputeResult != null ? disputeResult.getSellerPayoutAmount().toFriendlyString() : "";
stringBuilder.append("\n")
.append("Dispute nr. ") int index = disputeIndex.incrementAndGet();
.append(disputeIndex.incrementAndGet()) String tradeDateString = dateFormatter.format(firstDispute.getTradeDate());
String openingDateString = dateFormatter.format(openingDate);
// Index we display starts with 1 not with 0
int cycleIndex = 0;
if (disputeResult != null) {
Date closeDate = disputeResult.getCloseDate();
cycleIndex = blockStartDateByCycleIndex.entrySet().stream()
.filter(e -> e.getValue().after(closeDate))
.findFirst()
.map(Map.Entry::getKey)
.orElse(0);
}
stringBuilder.append("\n").append("Dispute nr.: ").append(index).append("\n");
if (cycleIndex > 0) {
stringBuilder.append("Closed during cycle: ").append(cycleIndex).append("\n");
}
stringBuilder.append("Trade date: ").append(tradeDateString)
.append("\n") .append("\n")
.append("Opening date: ") .append("Opening date: ").append(openingDateString)
.append(DisplayUtils.formatDateTime(openingDate))
.append("\n"); .append("\n");
String summaryNotes0 = ""; String tradeId = firstDispute.getTradeId();
csvStringBuilder.append("\n").append(index).append(";");
if (cycleIndex > 0) {
csvStringBuilder.append(cycleIndex).append(";");
} else {
csvStringBuilder.append(";");
}
csvStringBuilder.append(firstDispute.isClosed() ? "Closed" : "Open").append(";")
.append(tradeDateString).append(";")
.append(firstDispute.getShortTradeId()).append(";")
.append(tradeId, tradeId.length() - 3, tradeId.length()).append(";")
.append(openingDateString).append(";");
String summaryNotes = "";
if (disputeResult != null) { if (disputeResult != null) {
Date closeDate = disputeResult.getCloseDate(); Date closeDate = disputeResult.getCloseDate();
long duration = closeDate.getTime() - openingDate.getTime(); long duration = closeDate.getTime() - openingDate.getTime();
stringBuilder.append("Close date: ")
.append(DisplayUtils.formatDateTime(closeDate)) String closeDateString = dateFormatter.format(closeDate);
.append("\n") String durationAsWords = FormattingUtils.formatDurationAsWords(duration);
.append("Dispute duration: ") stringBuilder.append("Close date: ").append(closeDateString).append("\n")
.append(FormattingUtils.formatDurationAsWords(duration)) .append("Dispute duration: ").append(durationAsWords).append("\n");
.append("\n"); csvStringBuilder.append(closeDateString).append(";")
.append(durationAsWords).append(";");
} else {
csvStringBuilder.append(";").append(";");
} }
String paymentMethod = Res.get(contract.getPaymentMethodId());
String currency = CurrencyUtil.getNameAndCode(contract.getOfferPayload().getCurrencyCode());
String tradeAmount = contract.getTradeAmount().toFriendlyString();
String buyerDeposit = Coin.valueOf(contract.getOfferPayload().getBuyerSecurityDeposit()).toFriendlyString();
String sellerDeposit = Coin.valueOf(contract.getOfferPayload().getSellerSecurityDeposit()).toFriendlyString();
stringBuilder.append("Payment method: ") stringBuilder.append("Payment method: ")
.append(Res.get(contract.getPaymentMethodId())) .append(paymentMethod)
.append("\n") .append("\n")
.append("Currency: ") .append("Currency: ")
.append(CurrencyUtil.getNameAndCode(contract.getOfferPayload().getCurrencyCode())) .append(currency)
.append("\n") .append("\n")
.append("Trade amount: ") .append("Trade amount: ")
.append(contract.getTradeAmount().toFriendlyString()) .append(tradeAmount)
.append("\n") .append("\n")
.append("Buyer/seller security deposit: ") .append("Buyer/seller security deposit: ")
.append(Coin.valueOf(contract.getOfferPayload().getBuyerSecurityDeposit()).toFriendlyString()) .append(buyerDeposit)
.append("/") .append("/")
.append(Coin.valueOf(contract.getOfferPayload().getSellerSecurityDeposit()).toFriendlyString()) .append(sellerDeposit)
.append("\n") .append("\n")
.append("Dispute opened by: ") .append("Dispute opened by: ")
.append(opener) .append(opener)
@ -603,6 +741,28 @@ public abstract class DisputeView extends ActivatableView<VBox, Void> {
.append(winner) .append(winner)
.append(")\n"); .append(")\n");
String buyerPaymentAccountPayload = Utilities.toTruncatedString(
contract.getBuyerPaymentAccountPayload().getPaymentDetails().
replace("\n", " ").replace(";", "."), 100);
String sellerPaymentAccountPayload = Utilities.toTruncatedString(
contract.getSellerPaymentAccountPayload().getPaymentDetails()
.replace("\n", " ").replace(";", "."), 100);
String buyerNodeAddress = contract.getBuyerNodeAddress().getFullAddress();
String sellerNodeAddress = contract.getSellerNodeAddress().getFullAddress();
csvStringBuilder.append(currency).append(";")
.append(tradeAmount.replace(" BTC", "")).append(";")
.append(paymentMethod).append(";")
.append(buyerPaymentAccountPayload).append(";")
.append(sellerPaymentAccountPayload).append(";")
.append(buyerNodeAddress.replace(".onion:9999", "")).append(";")
.append(sellerNodeAddress.replace(".onion:9999", "")).append(";")
.append(buyerDeposit.replace(" BTC", "")).append(";")
.append(sellerDeposit.replace(" BTC", "")).append(";")
.append(opener).append(";")
.append(buyerPayoutAmount.replace(" BTC", "")).append(";")
.append(sellerPayoutAmount.replace(" BTC", "")).append(";")
.append(winner).append(";");
if (disputeResult != null) { if (disputeResult != null) {
DisputeResult.Reason reason = disputeResult.getReason(); DisputeResult.Reason reason = disputeResult.getReason();
if (firstDispute.disputeResultProperty().get().getReason() != null) { if (firstDispute.disputeResultProperty().get().getReason() != null) {
@ -611,10 +771,18 @@ public abstract class DisputeView extends ActivatableView<VBox, Void> {
stringBuilder.append("Reason: ") stringBuilder.append("Reason: ")
.append(reason.name()) .append(reason.name())
.append("\n"); .append("\n");
csvStringBuilder.append(reason.name()).append(";");
} else {
csvStringBuilder.append(";");
} }
summaryNotes0 = disputeResult.getSummaryNotesProperty().get(); summaryNotes = disputeResult.getSummaryNotesProperty().get();
stringBuilder.append("Summary notes: ").append(summaryNotes0).append("\n"); stringBuilder.append("Summary notes: ").append(summaryNotes).append("\n");
csvStringBuilder.append(summaryNotes).append(";");
} else {
csvStringBuilder.append(";");
} }
// We might have a different summary notes at second trader. Only if it // We might have a different summary notes at second trader. Only if it
@ -624,8 +792,12 @@ public abstract class DisputeView extends ActivatableView<VBox, Void> {
DisputeResult disputeResult1 = dispute1.getDisputeResultProperty().get(); DisputeResult disputeResult1 = dispute1.getDisputeResultProperty().get();
if (disputeResult1 != null) { if (disputeResult1 != null) {
String summaryNotes1 = disputeResult1.getSummaryNotesProperty().get(); String summaryNotes1 = disputeResult1.getSummaryNotesProperty().get();
if (!summaryNotes1.equals(summaryNotes0)) { if (!summaryNotes1.equals(summaryNotes)) {
stringBuilder.append("Summary notes (different message to other trader was used): ").append(summaryNotes1).append("\n"); stringBuilder.append("Summary notes (different message to other trader was used): ").append(summaryNotes1).append("\n");
csvStringBuilder.append(summaryNotes1).append(";");
} else {
csvStringBuilder.append(";");
} }
} }
} }
@ -643,8 +815,9 @@ public abstract class DisputeView extends ActivatableView<VBox, Void> {
.width(1200) .width(1200)
.actionButtonText("Copy to clipboard") .actionButtonText("Copy to clipboard")
.onAction(() -> Utilities.copyToClipboard(message)) .onAction(() -> Utilities.copyToClipboard(message))
.secondaryActionButtonText("Copy as csv data")
.onSecondaryAction(() -> Utilities.copyToClipboard(csvStringBuilder.toString()))
.show(); .show();
} }
private void showFullReport() { private void showFullReport() {

View file

@ -27,12 +27,16 @@ import bisq.desktop.main.support.dispute.DisputeView;
import bisq.core.account.witness.AccountAgeWitnessService; import bisq.core.account.witness.AccountAgeWitnessService;
import bisq.core.alert.PrivateNotificationManager; import bisq.core.alert.PrivateNotificationManager;
import bisq.core.dao.DaoFacade;
import bisq.core.locale.Res; import bisq.core.locale.Res;
import bisq.core.support.dispute.Dispute; import bisq.core.support.dispute.Dispute;
import bisq.core.support.dispute.DisputeList; import bisq.core.support.dispute.DisputeList;
import bisq.core.support.dispute.DisputeManager; import bisq.core.support.dispute.DisputeManager;
import bisq.core.support.dispute.DisputeSession; import bisq.core.support.dispute.DisputeSession;
import bisq.core.support.dispute.agent.MultipleHolderNameDetection; import bisq.core.support.dispute.agent.MultipleHolderNameDetection;
import bisq.core.support.dispute.mediation.mediator.MediatorManager;
import bisq.core.support.dispute.refund.refundagent.RefundAgentManager;
import bisq.core.trade.TradeDataValidation;
import bisq.core.trade.TradeManager; import bisq.core.trade.TradeManager;
import bisq.core.user.DontShowAgainLookup; import bisq.core.user.DontShowAgainLookup;
import bisq.core.util.coin.CoinFormatter; import bisq.core.util.coin.CoinFormatter;
@ -54,13 +58,17 @@ import javafx.geometry.Insets;
import javafx.beans.property.ReadOnlyObjectWrapper; import javafx.beans.property.ReadOnlyObjectWrapper;
import javafx.collections.ListChangeListener;
import java.util.List; import java.util.List;
import static bisq.core.trade.TradeDataValidation.ValidationException;
import static bisq.desktop.util.FormBuilder.getIconForLabel; import static bisq.desktop.util.FormBuilder.getIconForLabel;
public abstract class DisputeAgentView extends DisputeView implements MultipleHolderNameDetection.Listener { public abstract class DisputeAgentView extends DisputeView implements MultipleHolderNameDetection.Listener {
private final MultipleHolderNameDetection multipleHolderNameDetection; private final MultipleHolderNameDetection multipleHolderNameDetection;
private ListChangeListener<ValidationException> validationExceptionListener;
public DisputeAgentView(DisputeManager<? extends DisputeList<? extends DisputeList>> disputeManager, public DisputeAgentView(DisputeManager<? extends DisputeList<? extends DisputeList>> disputeManager,
KeyRing keyRing, KeyRing keyRing,
@ -71,6 +79,9 @@ public abstract class DisputeAgentView extends DisputeView implements MultipleHo
ContractWindow contractWindow, ContractWindow contractWindow,
TradeDetailsWindow tradeDetailsWindow, TradeDetailsWindow tradeDetailsWindow,
AccountAgeWitnessService accountAgeWitnessService, AccountAgeWitnessService accountAgeWitnessService,
DaoFacade daoFacade,
MediatorManager mediatorManager,
RefundAgentManager refundAgentManager,
boolean useDevPrivilegeKeys) { boolean useDevPrivilegeKeys) {
super(disputeManager, super(disputeManager,
keyRing, keyRing,
@ -81,6 +92,9 @@ public abstract class DisputeAgentView extends DisputeView implements MultipleHo
contractWindow, contractWindow,
tradeDetailsWindow, tradeDetailsWindow,
accountAgeWitnessService, accountAgeWitnessService,
mediatorManager,
refundAgentManager,
daoFacade,
useDevPrivilegeKeys); useDevPrivilegeKeys);
multipleHolderNameDetection = new MultipleHolderNameDetection(disputeManager); multipleHolderNameDetection = new MultipleHolderNameDetection(disputeManager);
@ -107,6 +121,32 @@ public abstract class DisputeAgentView extends DisputeView implements MultipleHo
fullReportButton.setManaged(true); fullReportButton.setManaged(true);
multipleHolderNameDetection.detectMultipleHolderNames(); multipleHolderNameDetection.detectMultipleHolderNames();
validationExceptionListener = c -> {
c.next();
if (c.wasAdded()) {
showWarningForValidationExceptions(c.getAddedSubList());
}
};
}
protected void showWarningForValidationExceptions(List<? extends ValidationException> exceptions) {
exceptions.stream()
.filter(ex -> ex.getDispute() != null)
.filter(ex -> !ex.getDispute().isClosed()) // we show warnings only for open cases
.forEach(ex -> {
Dispute dispute = ex.getDispute();
if (ex instanceof TradeDataValidation.AddressException) {
new Popup().width(900).warning(Res.get("support.warning.disputesWithInvalidDonationAddress",
dispute.getDonationAddressOfDelayedPayoutTx(),
daoFacade.getAllDonationAddresses(),
dispute.getTradeId(),
""))
.show();
} else {
new Popup().width(900).warning(ex.getMessage()).show();
}
});
} }
@Override @Override
@ -117,6 +157,9 @@ public abstract class DisputeAgentView extends DisputeView implements MultipleHo
if (multipleHolderNameDetection.hasSuspiciousDisputesDetected()) { if (multipleHolderNameDetection.hasSuspiciousDisputesDetected()) {
suspiciousDisputeDetected(); suspiciousDisputeDetected();
} }
disputeManager.getValidationExceptions().addListener(validationExceptionListener);
showWarningForValidationExceptions(disputeManager.getValidationExceptions());
} }
@Override @Override
@ -124,6 +167,8 @@ public abstract class DisputeAgentView extends DisputeView implements MultipleHo
super.deactivate(); super.deactivate();
multipleHolderNameDetection.removeListener(this); multipleHolderNameDetection.removeListener(this);
disputeManager.getValidationExceptions().removeListener(validationExceptionListener);
} }
@ -142,17 +187,13 @@ public abstract class DisputeAgentView extends DisputeView implements MultipleHo
/////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////
@Override @Override
protected void applyFilteredListPredicate(String filterString) { protected DisputeView.FilterResult getFilterResult(Dispute dispute, String filterString) {
filteredList.setPredicate(dispute -> { // If in arbitrator view we must only display disputes where we are selected as arbitrator (must not receive others anyway)
// If in arbitrator view we must only display disputes where we are selected as arbitrator (must not receive others anyway) if (!dispute.getAgentPubKeyRing().equals(keyRing.getPubKeyRing())) {
if (!dispute.getAgentPubKeyRing().equals(keyRing.getPubKeyRing())) { return FilterResult.NO_MATCH;
return false; }
}
boolean isOpen = !dispute.isClosed() && filterString.toLowerCase().equals("open"); return super.getFilterResult(dispute, filterString);
return filterString.isEmpty() ||
isOpen ||
anyMatchOfFilterString(dispute, filterString);
});
} }
@Override @Override

View file

@ -25,11 +25,14 @@ import bisq.desktop.main.support.dispute.agent.DisputeAgentView;
import bisq.core.account.witness.AccountAgeWitnessService; import bisq.core.account.witness.AccountAgeWitnessService;
import bisq.core.alert.PrivateNotificationManager; import bisq.core.alert.PrivateNotificationManager;
import bisq.core.dao.DaoFacade;
import bisq.core.support.SupportType; import bisq.core.support.SupportType;
import bisq.core.support.dispute.Dispute; import bisq.core.support.dispute.Dispute;
import bisq.core.support.dispute.DisputeSession; import bisq.core.support.dispute.DisputeSession;
import bisq.core.support.dispute.arbitration.ArbitrationManager; import bisq.core.support.dispute.arbitration.ArbitrationManager;
import bisq.core.support.dispute.arbitration.ArbitrationSession; import bisq.core.support.dispute.arbitration.ArbitrationSession;
import bisq.core.support.dispute.mediation.mediator.MediatorManager;
import bisq.core.support.dispute.refund.refundagent.RefundAgentManager;
import bisq.core.trade.TradeManager; import bisq.core.trade.TradeManager;
import bisq.core.util.FormattingUtils; import bisq.core.util.FormattingUtils;
import bisq.core.util.coin.CoinFormatter; import bisq.core.util.coin.CoinFormatter;
@ -53,6 +56,9 @@ public class ArbitratorView extends DisputeAgentView {
ContractWindow contractWindow, ContractWindow contractWindow,
TradeDetailsWindow tradeDetailsWindow, TradeDetailsWindow tradeDetailsWindow,
AccountAgeWitnessService accountAgeWitnessService, AccountAgeWitnessService accountAgeWitnessService,
DaoFacade daoFacade,
MediatorManager mediatorManager,
RefundAgentManager refundAgentManager,
@Named(Config.USE_DEV_PRIVILEGE_KEYS) boolean useDevPrivilegeKeys) { @Named(Config.USE_DEV_PRIVILEGE_KEYS) boolean useDevPrivilegeKeys) {
super(arbitrationManager, super(arbitrationManager,
keyRing, keyRing,
@ -63,6 +69,9 @@ public class ArbitratorView extends DisputeAgentView {
contractWindow, contractWindow,
tradeDetailsWindow, tradeDetailsWindow,
accountAgeWitnessService, accountAgeWitnessService,
daoFacade,
mediatorManager,
refundAgentManager,
useDevPrivilegeKeys); useDevPrivilegeKeys);
} }

View file

@ -25,11 +25,14 @@ import bisq.desktop.main.support.dispute.agent.DisputeAgentView;
import bisq.core.account.witness.AccountAgeWitnessService; import bisq.core.account.witness.AccountAgeWitnessService;
import bisq.core.alert.PrivateNotificationManager; import bisq.core.alert.PrivateNotificationManager;
import bisq.core.dao.DaoFacade;
import bisq.core.support.SupportType; import bisq.core.support.SupportType;
import bisq.core.support.dispute.Dispute; import bisq.core.support.dispute.Dispute;
import bisq.core.support.dispute.DisputeSession; import bisq.core.support.dispute.DisputeSession;
import bisq.core.support.dispute.mediation.MediationManager; import bisq.core.support.dispute.mediation.MediationManager;
import bisq.core.support.dispute.mediation.MediationSession; import bisq.core.support.dispute.mediation.MediationSession;
import bisq.core.support.dispute.mediation.mediator.MediatorManager;
import bisq.core.support.dispute.refund.refundagent.RefundAgentManager;
import bisq.core.trade.TradeManager; import bisq.core.trade.TradeManager;
import bisq.core.util.FormattingUtils; import bisq.core.util.FormattingUtils;
import bisq.core.util.coin.CoinFormatter; import bisq.core.util.coin.CoinFormatter;
@ -53,6 +56,9 @@ public class MediatorView extends DisputeAgentView {
ContractWindow contractWindow, ContractWindow contractWindow,
TradeDetailsWindow tradeDetailsWindow, TradeDetailsWindow tradeDetailsWindow,
AccountAgeWitnessService accountAgeWitnessService, AccountAgeWitnessService accountAgeWitnessService,
DaoFacade daoFacade,
MediatorManager mediatorManager,
RefundAgentManager refundAgentManager,
@Named(Config.USE_DEV_PRIVILEGE_KEYS) boolean useDevPrivilegeKeys) { @Named(Config.USE_DEV_PRIVILEGE_KEYS) boolean useDevPrivilegeKeys) {
super(mediationManager, super(mediationManager,
keyRing, keyRing,
@ -63,6 +69,9 @@ public class MediatorView extends DisputeAgentView {
contractWindow, contractWindow,
tradeDetailsWindow, tradeDetailsWindow,
accountAgeWitnessService, accountAgeWitnessService,
daoFacade,
mediatorManager,
refundAgentManager,
useDevPrivilegeKeys); useDevPrivilegeKeys);
} }

View file

@ -25,11 +25,14 @@ import bisq.desktop.main.support.dispute.agent.DisputeAgentView;
import bisq.core.account.witness.AccountAgeWitnessService; import bisq.core.account.witness.AccountAgeWitnessService;
import bisq.core.alert.PrivateNotificationManager; import bisq.core.alert.PrivateNotificationManager;
import bisq.core.dao.DaoFacade;
import bisq.core.support.SupportType; import bisq.core.support.SupportType;
import bisq.core.support.dispute.Dispute; import bisq.core.support.dispute.Dispute;
import bisq.core.support.dispute.DisputeSession; import bisq.core.support.dispute.DisputeSession;
import bisq.core.support.dispute.mediation.mediator.MediatorManager;
import bisq.core.support.dispute.refund.RefundManager; import bisq.core.support.dispute.refund.RefundManager;
import bisq.core.support.dispute.refund.RefundSession; import bisq.core.support.dispute.refund.RefundSession;
import bisq.core.support.dispute.refund.refundagent.RefundAgentManager;
import bisq.core.trade.TradeManager; import bisq.core.trade.TradeManager;
import bisq.core.util.FormattingUtils; import bisq.core.util.FormattingUtils;
import bisq.core.util.coin.CoinFormatter; import bisq.core.util.coin.CoinFormatter;
@ -37,9 +40,8 @@ import bisq.core.util.coin.CoinFormatter;
import bisq.common.config.Config; import bisq.common.config.Config;
import bisq.common.crypto.KeyRing; import bisq.common.crypto.KeyRing;
import javax.inject.Named;
import javax.inject.Inject; import javax.inject.Inject;
import javax.inject.Named;
@FxmlView @FxmlView
public class RefundAgentView extends DisputeAgentView { public class RefundAgentView extends DisputeAgentView {
@ -54,6 +56,9 @@ public class RefundAgentView extends DisputeAgentView {
ContractWindow contractWindow, ContractWindow contractWindow,
TradeDetailsWindow tradeDetailsWindow, TradeDetailsWindow tradeDetailsWindow,
AccountAgeWitnessService accountAgeWitnessService, AccountAgeWitnessService accountAgeWitnessService,
DaoFacade daoFacade,
MediatorManager mediatorManager,
RefundAgentManager refundAgentManager,
@Named(Config.USE_DEV_PRIVILEGE_KEYS) boolean useDevPrivilegeKeys) { @Named(Config.USE_DEV_PRIVILEGE_KEYS) boolean useDevPrivilegeKeys) {
super(refundManager, super(refundManager,
keyRing, keyRing,
@ -64,6 +69,9 @@ public class RefundAgentView extends DisputeAgentView {
contractWindow, contractWindow,
tradeDetailsWindow, tradeDetailsWindow,
accountAgeWitnessService, accountAgeWitnessService,
daoFacade,
mediatorManager,
refundAgentManager,
useDevPrivilegeKeys); useDevPrivilegeKeys);
} }

View file

@ -24,10 +24,13 @@ import bisq.desktop.main.support.dispute.DisputeView;
import bisq.core.account.witness.AccountAgeWitnessService; import bisq.core.account.witness.AccountAgeWitnessService;
import bisq.core.alert.PrivateNotificationManager; import bisq.core.alert.PrivateNotificationManager;
import bisq.core.dao.DaoFacade;
import bisq.core.support.dispute.Dispute; import bisq.core.support.dispute.Dispute;
import bisq.core.support.dispute.DisputeList; import bisq.core.support.dispute.DisputeList;
import bisq.core.support.dispute.DisputeManager; import bisq.core.support.dispute.DisputeManager;
import bisq.core.support.dispute.DisputeSession; import bisq.core.support.dispute.DisputeSession;
import bisq.core.support.dispute.mediation.mediator.MediatorManager;
import bisq.core.support.dispute.refund.refundagent.RefundAgentManager;
import bisq.core.trade.TradeManager; import bisq.core.trade.TradeManager;
import bisq.core.util.coin.CoinFormatter; import bisq.core.util.coin.CoinFormatter;
@ -43,9 +46,13 @@ public abstract class DisputeClientView extends DisputeView {
ContractWindow contractWindow, ContractWindow contractWindow,
TradeDetailsWindow tradeDetailsWindow, TradeDetailsWindow tradeDetailsWindow,
AccountAgeWitnessService accountAgeWitnessService, AccountAgeWitnessService accountAgeWitnessService,
MediatorManager mediatorManager,
RefundAgentManager refundAgentManager,
DaoFacade daoFacade,
boolean useDevPrivilegeKeys) { boolean useDevPrivilegeKeys) {
super(DisputeManager, keyRing, tradeManager, formatter, disputeSummaryWindow, privateNotificationManager, super(DisputeManager, keyRing, tradeManager, formatter, disputeSummaryWindow, privateNotificationManager,
contractWindow, tradeDetailsWindow, accountAgeWitnessService, useDevPrivilegeKeys); contractWindow, tradeDetailsWindow, accountAgeWitnessService,
mediatorManager, refundAgentManager, daoFacade, useDevPrivilegeKeys);
} }
@Override @Override
@ -55,14 +62,12 @@ public abstract class DisputeClientView extends DisputeView {
} }
@Override @Override
protected void applyFilteredListPredicate(String filterString) { protected DisputeView.FilterResult getFilterResult(Dispute dispute, String filterString) {
filteredList.setPredicate(dispute -> { // As we are in the client view we hide disputes where we are the agent
// As we are in the client view we hide disputes where we are the agent if (dispute.getAgentPubKeyRing().equals(keyRing.getPubKeyRing())) {
if (dispute.getAgentPubKeyRing().equals(keyRing.getPubKeyRing())) { return FilterResult.NO_MATCH;
return false; }
}
return filterString.isEmpty() || anyMatchOfFilterString(dispute, filterString); return super.getFilterResult(dispute, filterString);
});
} }
} }

View file

@ -25,11 +25,14 @@ import bisq.desktop.main.support.dispute.client.DisputeClientView;
import bisq.core.account.witness.AccountAgeWitnessService; import bisq.core.account.witness.AccountAgeWitnessService;
import bisq.core.alert.PrivateNotificationManager; import bisq.core.alert.PrivateNotificationManager;
import bisq.core.dao.DaoFacade;
import bisq.core.support.SupportType; import bisq.core.support.SupportType;
import bisq.core.support.dispute.Dispute; import bisq.core.support.dispute.Dispute;
import bisq.core.support.dispute.DisputeSession; import bisq.core.support.dispute.DisputeSession;
import bisq.core.support.dispute.arbitration.ArbitrationManager; import bisq.core.support.dispute.arbitration.ArbitrationManager;
import bisq.core.support.dispute.arbitration.ArbitrationSession; import bisq.core.support.dispute.arbitration.ArbitrationSession;
import bisq.core.support.dispute.mediation.mediator.MediatorManager;
import bisq.core.support.dispute.refund.refundagent.RefundAgentManager;
import bisq.core.trade.TradeManager; import bisq.core.trade.TradeManager;
import bisq.core.util.FormattingUtils; import bisq.core.util.FormattingUtils;
import bisq.core.util.coin.CoinFormatter; import bisq.core.util.coin.CoinFormatter;
@ -37,9 +40,8 @@ import bisq.core.util.coin.CoinFormatter;
import bisq.common.config.Config; import bisq.common.config.Config;
import bisq.common.crypto.KeyRing; import bisq.common.crypto.KeyRing;
import javax.inject.Named;
import javax.inject.Inject; import javax.inject.Inject;
import javax.inject.Named;
@FxmlView @FxmlView
public class ArbitrationClientView extends DisputeClientView { public class ArbitrationClientView extends DisputeClientView {
@ -53,10 +55,13 @@ public class ArbitrationClientView extends DisputeClientView {
ContractWindow contractWindow, ContractWindow contractWindow,
TradeDetailsWindow tradeDetailsWindow, TradeDetailsWindow tradeDetailsWindow,
AccountAgeWitnessService accountAgeWitnessService, AccountAgeWitnessService accountAgeWitnessService,
MediatorManager mediatorManager,
RefundAgentManager refundAgentManager,
DaoFacade daoFacade,
@Named(Config.USE_DEV_PRIVILEGE_KEYS) boolean useDevPrivilegeKeys) { @Named(Config.USE_DEV_PRIVILEGE_KEYS) boolean useDevPrivilegeKeys) {
super(arbitrationManager, keyRing, tradeManager, formatter, disputeSummaryWindow, super(arbitrationManager, keyRing, tradeManager, formatter, disputeSummaryWindow,
privateNotificationManager, contractWindow, tradeDetailsWindow, accountAgeWitnessService, privateNotificationManager, contractWindow, tradeDetailsWindow, accountAgeWitnessService,
useDevPrivilegeKeys); mediatorManager, refundAgentManager, daoFacade, useDevPrivilegeKeys);
} }
@Override @Override

View file

@ -26,12 +26,15 @@ import bisq.desktop.main.support.dispute.client.DisputeClientView;
import bisq.core.account.witness.AccountAgeWitnessService; import bisq.core.account.witness.AccountAgeWitnessService;
import bisq.core.alert.PrivateNotificationManager; import bisq.core.alert.PrivateNotificationManager;
import bisq.core.dao.DaoFacade;
import bisq.core.locale.Res; import bisq.core.locale.Res;
import bisq.core.support.SupportType; import bisq.core.support.SupportType;
import bisq.core.support.dispute.Dispute; import bisq.core.support.dispute.Dispute;
import bisq.core.support.dispute.DisputeSession; import bisq.core.support.dispute.DisputeSession;
import bisq.core.support.dispute.mediation.MediationManager; import bisq.core.support.dispute.mediation.MediationManager;
import bisq.core.support.dispute.mediation.MediationSession; import bisq.core.support.dispute.mediation.MediationSession;
import bisq.core.support.dispute.mediation.mediator.MediatorManager;
import bisq.core.support.dispute.refund.refundagent.RefundAgentManager;
import bisq.core.trade.TradeManager; import bisq.core.trade.TradeManager;
import bisq.core.util.FormattingUtils; import bisq.core.util.FormattingUtils;
import bisq.core.util.coin.CoinFormatter; import bisq.core.util.coin.CoinFormatter;
@ -54,10 +57,13 @@ public class MediationClientView extends DisputeClientView {
ContractWindow contractWindow, ContractWindow contractWindow,
TradeDetailsWindow tradeDetailsWindow, TradeDetailsWindow tradeDetailsWindow,
AccountAgeWitnessService accountAgeWitnessService, AccountAgeWitnessService accountAgeWitnessService,
MediatorManager mediatorManager,
RefundAgentManager refundAgentManager,
DaoFacade daoFacade,
@Named(Config.USE_DEV_PRIVILEGE_KEYS) boolean useDevPrivilegeKeys) { @Named(Config.USE_DEV_PRIVILEGE_KEYS) boolean useDevPrivilegeKeys) {
super(mediationManager, keyRing, tradeManager, formatter, disputeSummaryWindow, super(mediationManager, keyRing, tradeManager, formatter, disputeSummaryWindow,
privateNotificationManager, contractWindow, tradeDetailsWindow, accountAgeWitnessService, privateNotificationManager, contractWindow, tradeDetailsWindow, accountAgeWitnessService,
useDevPrivilegeKeys); mediatorManager, refundAgentManager, daoFacade, useDevPrivilegeKeys);
} }
@Override @Override

View file

@ -25,11 +25,14 @@ import bisq.desktop.main.support.dispute.client.DisputeClientView;
import bisq.core.account.witness.AccountAgeWitnessService; import bisq.core.account.witness.AccountAgeWitnessService;
import bisq.core.alert.PrivateNotificationManager; import bisq.core.alert.PrivateNotificationManager;
import bisq.core.dao.DaoFacade;
import bisq.core.support.SupportType; import bisq.core.support.SupportType;
import bisq.core.support.dispute.Dispute; import bisq.core.support.dispute.Dispute;
import bisq.core.support.dispute.DisputeSession; import bisq.core.support.dispute.DisputeSession;
import bisq.core.support.dispute.mediation.mediator.MediatorManager;
import bisq.core.support.dispute.refund.RefundManager; import bisq.core.support.dispute.refund.RefundManager;
import bisq.core.support.dispute.refund.RefundSession; import bisq.core.support.dispute.refund.RefundSession;
import bisq.core.support.dispute.refund.refundagent.RefundAgentManager;
import bisq.core.trade.TradeManager; import bisq.core.trade.TradeManager;
import bisq.core.util.FormattingUtils; import bisq.core.util.FormattingUtils;
import bisq.core.util.coin.CoinFormatter; import bisq.core.util.coin.CoinFormatter;
@ -37,9 +40,8 @@ import bisq.core.util.coin.CoinFormatter;
import bisq.common.config.Config; import bisq.common.config.Config;
import bisq.common.crypto.KeyRing; import bisq.common.crypto.KeyRing;
import javax.inject.Named;
import javax.inject.Inject; import javax.inject.Inject;
import javax.inject.Named;
@FxmlView @FxmlView
public class RefundClientView extends DisputeClientView { public class RefundClientView extends DisputeClientView {
@ -53,10 +55,13 @@ public class RefundClientView extends DisputeClientView {
ContractWindow contractWindow, ContractWindow contractWindow,
TradeDetailsWindow tradeDetailsWindow, TradeDetailsWindow tradeDetailsWindow,
AccountAgeWitnessService accountAgeWitnessService, AccountAgeWitnessService accountAgeWitnessService,
MediatorManager mediatorManager,
RefundAgentManager refundAgentManager,
DaoFacade daoFacade,
@Named(Config.USE_DEV_PRIVILEGE_KEYS) boolean useDevPrivilegeKeys) { @Named(Config.USE_DEV_PRIVILEGE_KEYS) boolean useDevPrivilegeKeys) {
super(refundManager, keyRing, tradeManager, formatter, disputeSummaryWindow, super(refundManager, keyRing, tradeManager, formatter, disputeSummaryWindow,
privateNotificationManager, contractWindow, tradeDetailsWindow, accountAgeWitnessService, privateNotificationManager, contractWindow, tradeDetailsWindow, accountAgeWitnessService,
useDevPrivilegeKeys); mediatorManager, refundAgentManager, daoFacade, useDevPrivilegeKeys);
} }
@Override @Override

View file

@ -795,6 +795,7 @@ message Dispute {
SupportType support_type = 24; SupportType support_type = 24;
string mediators_dispute_result = 25; string mediators_dispute_result = 25;
string delayed_payout_tx_id = 26; string delayed_payout_tx_id = 26;
string donation_address_of_delayed_payout_tx = 27;
} }
message Attachment { message Attachment {