mirror of
https://github.com/bisq-network/bisq.git
synced 2025-03-03 18:56:59 +01:00
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:
commit
0fa45650b6
31 changed files with 1550 additions and 488 deletions
|
@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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}";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
406
core/src/main/java/bisq/core/trade/TradeDataValidation.java
Normal file
406
core/src/main/java/bisq/core/trade/TradeDataValidation.java
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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.
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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\
|
||||||
|
|
|
@ -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());
|
||||||
|
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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() {
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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);
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
Loading…
Add table
Reference in a new issue