mirror of
https://github.com/bisq-network/bisq.git
synced 2025-03-03 10:46:54 +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.common.config.Config;
|
||||
import bisq.common.handlers.ErrorMessageHandler;
|
||||
import bisq.common.handlers.ExceptionHandler;
|
||||
import bisq.common.handlers.ResultHandler;
|
||||
|
@ -95,9 +96,14 @@ import java.io.File;
|
|||
import java.io.IOException;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
@ -423,10 +429,18 @@ public class DaoFacade implements DaoSetupService {
|
|||
case RESULT:
|
||||
break;
|
||||
}
|
||||
|
||||
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
|
||||
// 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) {
|
||||
|
@ -750,4 +764,32 @@ public class DaoFacade implements DaoSetupService {
|
|||
long baseFactor = daoStateService.getParamValueAsCoin(Param.BONDED_ROLE_FACTOR, height).value;
|
||||
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.List;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
@ -103,6 +104,15 @@ public final class Dispute implements NetworkPayload {
|
|||
@Nullable
|
||||
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
|
||||
|
@ -192,6 +202,7 @@ public final class Dispute implements NetworkPayload {
|
|||
this.supportType = supportType;
|
||||
|
||||
id = tradeId + "_" + traderId;
|
||||
uid = UUID.randomUUID().toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -228,6 +239,7 @@ public final class Dispute implements NetworkPayload {
|
|||
Optional.ofNullable(supportType).ifPresent(result -> builder.setSupportType(SupportType.toProtoMessage(supportType)));
|
||||
Optional.ofNullable(mediatorsDisputeResult).ifPresent(result -> builder.setMediatorsDisputeResult(mediatorsDisputeResult));
|
||||
Optional.ofNullable(delayedPayoutTxId).ifPresent(result -> builder.setDelayedPayoutTxId(delayedPayoutTxId));
|
||||
Optional.ofNullable(donationAddressOfDelayedPayoutTx).ifPresent(result -> builder.setDonationAddressOfDelayedPayoutTx(donationAddressOfDelayedPayoutTx));
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
|
@ -271,6 +283,11 @@ public final class Dispute implements NetworkPayload {
|
|||
dispute.setDelayedPayoutTxId(delayedPayoutTxId);
|
||||
}
|
||||
|
||||
String donationAddressOfDelayedPayoutTx = proto.getDonationAddressOfDelayedPayoutTx();
|
||||
if (!donationAddressOfDelayedPayoutTx.isEmpty()) {
|
||||
dispute.setDonationAddressOfDelayedPayoutTx(donationAddressOfDelayedPayoutTx);
|
||||
}
|
||||
|
||||
return dispute;
|
||||
}
|
||||
|
||||
|
@ -357,6 +374,7 @@ public final class Dispute implements NetworkPayload {
|
|||
return "Dispute{" +
|
||||
"\n tradeId='" + tradeId + '\'' +
|
||||
",\n id='" + id + '\'' +
|
||||
",\n uid='" + uid + '\'' +
|
||||
",\n traderId=" + traderId +
|
||||
",\n disputeOpenerIsBuyer=" + disputeOpenerIsBuyer +
|
||||
",\n disputeOpenerIsMaker=" + disputeOpenerIsMaker +
|
||||
|
@ -382,6 +400,7 @@ public final class Dispute implements NetworkPayload {
|
|||
",\n supportType=" + supportType +
|
||||
",\n mediatorsDisputeResult='" + mediatorsDisputeResult + '\'' +
|
||||
",\n delayedPayoutTxId='" + delayedPayoutTxId + '\'' +
|
||||
",\n donationAddressOfDelayedPayoutTx='" + donationAddressOfDelayedPayoutTx + '\'' +
|
||||
"\n}";
|
||||
}
|
||||
}
|
||||
|
|
|
@ -21,6 +21,7 @@ import bisq.core.btc.setup.WalletsSetup;
|
|||
import bisq.core.btc.wallet.BtcWalletService;
|
||||
import bisq.core.btc.wallet.Restrictions;
|
||||
import bisq.core.btc.wallet.TradeWalletService;
|
||||
import bisq.core.dao.DaoFacade;
|
||||
import bisq.core.locale.CurrencyUtil;
|
||||
import bisq.core.locale.Res;
|
||||
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.trade.Contract;
|
||||
import bisq.core.trade.Trade;
|
||||
import bisq.core.trade.TradeDataValidation;
|
||||
import bisq.core.trade.TradeManager;
|
||||
import bisq.core.trade.closed.ClosedTradableManager;
|
||||
|
||||
|
@ -46,6 +48,8 @@ import bisq.network.p2p.SendMailboxMessageListener;
|
|||
|
||||
import bisq.common.UserThread;
|
||||
import bisq.common.app.Version;
|
||||
import bisq.common.config.Config;
|
||||
import bisq.common.crypto.KeyRing;
|
||||
import bisq.common.crypto.PubKeyRing;
|
||||
import bisq.common.handlers.FaultHandler;
|
||||
import bisq.common.handlers.ResultHandler;
|
||||
|
@ -58,14 +62,18 @@ import org.bitcoinj.utils.Fiat;
|
|||
|
||||
import javafx.beans.property.IntegerProperty;
|
||||
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.collections.ObservableList;
|
||||
|
||||
import java.security.KeyPair;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
|
@ -81,7 +89,15 @@ public abstract class DisputeManager<T extends DisputeList<? extends DisputeList
|
|||
protected final OpenOfferManager openOfferManager;
|
||||
protected final PubKeyRing pubKeyRing;
|
||||
protected final DisputeListService<T> disputeListService;
|
||||
private final Config config;
|
||||
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,
|
||||
ClosedTradableManager closedTradableManager,
|
||||
OpenOfferManager openOfferManager,
|
||||
PubKeyRing pubKeyRing,
|
||||
DaoFacade daoFacade,
|
||||
KeyRing keyRing,
|
||||
DisputeListService<T> disputeListService,
|
||||
Config config,
|
||||
PriceFeedService priceFeedService) {
|
||||
super(p2PService, walletsSetup);
|
||||
|
||||
|
@ -105,8 +123,11 @@ public abstract class DisputeManager<T extends DisputeList<? extends DisputeList
|
|||
this.tradeManager = tradeManager;
|
||||
this.closedTradableManager = closedTradableManager;
|
||||
this.openOfferManager = openOfferManager;
|
||||
this.pubKeyRing = pubKeyRing;
|
||||
this.daoFacade = daoFacade;
|
||||
this.pubKeyRing = keyRing.getPubKeyRing();
|
||||
signatureKeyPair = keyRing.getSignatureKeyPair();
|
||||
this.disputeListService = disputeListService;
|
||||
this.config = config;
|
||||
this.priceFeedService = priceFeedService;
|
||||
}
|
||||
|
||||
|
@ -178,7 +199,7 @@ public abstract class DisputeManager<T extends DisputeList<? extends DisputeList
|
|||
@Nullable
|
||||
public abstract NodeAddress getAgentNodeAddress(Dispute dispute);
|
||||
|
||||
protected abstract Trade.DisputeState getDisputeState_StartedByPeer();
|
||||
protected abstract Trade.DisputeState getDisputeStateStartedByPeer();
|
||||
|
||||
public abstract void cleanupDisputes();
|
||||
|
||||
|
@ -209,7 +230,7 @@ public abstract class DisputeManager<T extends DisputeList<? extends DisputeList
|
|||
return disputeListService.getNrOfDisputes(isBuyer, contract);
|
||||
}
|
||||
|
||||
private T getDisputeList() {
|
||||
protected T getDisputeList() {
|
||||
return disputeListService.getDisputeList();
|
||||
}
|
||||
|
||||
|
@ -241,6 +262,24 @@ public abstract class DisputeManager<T extends DisputeList<? extends DisputeList
|
|||
|
||||
tryApplyMessages();
|
||||
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) {
|
||||
|
@ -308,9 +347,21 @@ public abstract class DisputeManager<T extends DisputeList<? extends DisputeList
|
|||
}
|
||||
|
||||
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) {
|
||||
T disputeList = getDisputeList();
|
||||
if (disputeList == null) {
|
||||
|
@ -320,14 +371,33 @@ public abstract class DisputeManager<T extends DisputeList<? extends DisputeList
|
|||
|
||||
String errorMessage = null;
|
||||
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 (!disputeList.contains(dispute)) {
|
||||
Optional<Dispute> storedDisputeOptional = findDispute(dispute);
|
||||
if (!storedDisputeOptional.isPresent()) {
|
||||
dispute.setStorage(disputeListService.getStorage());
|
||||
disputeList.add(dispute);
|
||||
Optional<Trade> tradeOptional = tradeManager.getTradeById(dispute.getTradeId());
|
||||
tradeOptional.ifPresent(trade -> trade.setDisputeState(getDisputeState_StartedByPeer()));
|
||||
trade.setDisputeState(getDisputeStateStartedByPeer());
|
||||
errorMessage = null;
|
||||
} else {
|
||||
// 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.getSupportType());
|
||||
dispute.setDelayedPayoutTxId(disputeFromOpener.getDelayedPayoutTxId());
|
||||
dispute.setDonationAddressOfDelayedPayoutTx(disputeFromOpener.getDonationAddressOfDelayedPayoutTx());
|
||||
|
||||
Optional<Dispute> storedDisputeOptional = findDispute(dispute);
|
||||
|
||||
|
@ -609,7 +680,7 @@ public abstract class DisputeManager<T extends DisputeList<? extends DisputeList
|
|||
}
|
||||
|
||||
// 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();
|
||||
if (disputeList == null) {
|
||||
log.warn("disputes is null");
|
||||
|
@ -621,7 +692,7 @@ public abstract class DisputeManager<T extends DisputeList<? extends DisputeList
|
|||
dispute.getTradeId(),
|
||||
dispute.getTraderPubKeyRing().hashCode(),
|
||||
false,
|
||||
text,
|
||||
summaryText,
|
||||
p2PService.getAddress());
|
||||
|
||||
disputeResult.setChatMessage(chatMessage);
|
||||
|
|
|
@ -25,6 +25,7 @@ import bisq.core.btc.wallet.BtcWalletService;
|
|||
import bisq.core.btc.wallet.TradeWalletService;
|
||||
import bisq.core.btc.wallet.TxBroadcaster;
|
||||
import bisq.core.btc.wallet.WalletService;
|
||||
import bisq.core.dao.DaoFacade;
|
||||
import bisq.core.locale.Res;
|
||||
import bisq.core.offer.OpenOffer;
|
||||
import bisq.core.offer.OpenOfferManager;
|
||||
|
@ -53,6 +54,8 @@ import bisq.network.p2p.SendMailboxMessageListener;
|
|||
import bisq.common.Timer;
|
||||
import bisq.common.UserThread;
|
||||
import bisq.common.app.Version;
|
||||
import bisq.common.config.Config;
|
||||
import bisq.common.crypto.KeyRing;
|
||||
import bisq.common.crypto.PubKeyRing;
|
||||
|
||||
import org.bitcoinj.core.AddressFormatException;
|
||||
|
@ -89,11 +92,13 @@ public final class ArbitrationManager extends DisputeManager<ArbitrationDisputeL
|
|||
TradeManager tradeManager,
|
||||
ClosedTradableManager closedTradableManager,
|
||||
OpenOfferManager openOfferManager,
|
||||
PubKeyRing pubKeyRing,
|
||||
DaoFacade daoFacade,
|
||||
KeyRing keyRing,
|
||||
ArbitrationDisputeListService arbitrationDisputeListService,
|
||||
Config config,
|
||||
PriceFeedService priceFeedService) {
|
||||
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
|
||||
protected Trade.DisputeState getDisputeState_StartedByPeer() {
|
||||
protected Trade.DisputeState getDisputeStateStartedByPeer() {
|
||||
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.wallet.BtcWalletService;
|
||||
import bisq.core.btc.wallet.TradeWalletService;
|
||||
import bisq.core.dao.DaoFacade;
|
||||
import bisq.core.locale.Res;
|
||||
import bisq.core.offer.OpenOffer;
|
||||
import bisq.core.offer.OpenOfferManager;
|
||||
|
@ -46,7 +47,8 @@ import bisq.network.p2p.P2PService;
|
|||
import bisq.common.Timer;
|
||||
import bisq.common.UserThread;
|
||||
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.ResultHandler;
|
||||
|
||||
|
@ -80,13 +82,16 @@ public final class MediationManager extends DisputeManager<MediationDisputeList>
|
|||
TradeManager tradeManager,
|
||||
ClosedTradableManager closedTradableManager,
|
||||
OpenOfferManager openOfferManager,
|
||||
PubKeyRing pubKeyRing,
|
||||
DaoFacade daoFacade,
|
||||
KeyRing keyRing,
|
||||
MediationDisputeListService mediationDisputeListService,
|
||||
Config config,
|
||||
PriceFeedService priceFeedService) {
|
||||
super(p2PService, tradeWalletService, walletService, walletsSetup, tradeManager, closedTradableManager,
|
||||
openOfferManager, pubKeyRing, mediationDisputeListService, priceFeedService);
|
||||
openOfferManager, daoFacade, keyRing, mediationDisputeListService, config, priceFeedService);
|
||||
}
|
||||
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
// Implement template methods
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
@ -117,7 +122,7 @@ public final class MediationManager extends DisputeManager<MediationDisputeList>
|
|||
}
|
||||
|
||||
@Override
|
||||
protected Trade.DisputeState getDisputeState_StartedByPeer() {
|
||||
protected Trade.DisputeState getDisputeStateStartedByPeer() {
|
||||
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.wallet.BtcWalletService;
|
||||
import bisq.core.btc.wallet.TradeWalletService;
|
||||
import bisq.core.dao.DaoFacade;
|
||||
import bisq.core.locale.Res;
|
||||
import bisq.core.offer.OpenOffer;
|
||||
import bisq.core.offer.OpenOfferManager;
|
||||
|
@ -44,7 +45,8 @@ import bisq.network.p2p.P2PService;
|
|||
import bisq.common.Timer;
|
||||
import bisq.common.UserThread;
|
||||
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.Singleton;
|
||||
|
@ -74,13 +76,16 @@ public final class RefundManager extends DisputeManager<RefundDisputeList> {
|
|||
TradeManager tradeManager,
|
||||
ClosedTradableManager closedTradableManager,
|
||||
OpenOfferManager openOfferManager,
|
||||
PubKeyRing pubKeyRing,
|
||||
DaoFacade daoFacade,
|
||||
KeyRing keyRing,
|
||||
RefundDisputeListService refundDisputeListService,
|
||||
Config config,
|
||||
PriceFeedService priceFeedService) {
|
||||
super(p2PService, tradeWalletService, walletService, walletsSetup, tradeManager, closedTradableManager,
|
||||
openOfferManager, pubKeyRing, refundDisputeListService, priceFeedService);
|
||||
openOfferManager, daoFacade, keyRing, refundDisputeListService, config, priceFeedService);
|
||||
}
|
||||
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
// Implement template methods
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
@ -111,7 +116,7 @@ public final class RefundManager extends DisputeManager<RefundDisputeList> {
|
|||
}
|
||||
|
||||
@Override
|
||||
protected Trade.DisputeState getDisputeState_StartedByPeer() {
|
||||
protected Trade.DisputeState getDisputeStateStartedByPeer() {
|
||||
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
|
||||
public Transaction getDelayedPayoutTx() {
|
||||
if (delayedPayoutTx == null) {
|
||||
delayedPayoutTx = delayedPayoutTxBytes != null && processModel.getBtcWalletService() != null ?
|
||||
processModel.getBtcWalletService().getTxFromSerializedTx(delayedPayoutTxBytes) :
|
||||
null;
|
||||
BtcWalletService btcWalletService = processModel.getBtcWalletService();
|
||||
if (btcWalletService == 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;
|
||||
}
|
||||
|
|
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 TradableList<Trade> tradableList;
|
||||
@Getter
|
||||
private final BooleanProperty pendingTradesInitialized = new SimpleBooleanProperty();
|
||||
private List<Trade> tradesForStatistics;
|
||||
@Setter
|
||||
|
@ -305,15 +306,11 @@ public class TradeManager implements PersistedDataHost {
|
|||
}
|
||||
|
||||
try {
|
||||
DelayedPayoutTxValidation.validatePayoutTx(trade,
|
||||
TradeDataValidation.validatePayoutTx(trade,
|
||||
trade.getDelayedPayoutTx(),
|
||||
daoFacade,
|
||||
btcWalletService);
|
||||
} catch (DelayedPayoutTxValidation.DonationAddressException |
|
||||
DelayedPayoutTxValidation.InvalidTxException |
|
||||
DelayedPayoutTxValidation.InvalidLockTimeException |
|
||||
DelayedPayoutTxValidation.MissingDelayedPayoutTxException |
|
||||
DelayedPayoutTxValidation.AmountMismatchException e) {
|
||||
} catch (TradeDataValidation.ValidationException e) {
|
||||
log.warn("Delayed payout tx exception, trade {}, exception {}", trade.getId(), e.getMessage());
|
||||
if (!allowFaultyDelayedTxs) {
|
||||
// We move it to failed trades so it cannot be continued.
|
||||
|
|
|
@ -17,8 +17,8 @@
|
|||
|
||||
package bisq.core.trade.protocol.tasks.buyer;
|
||||
|
||||
import bisq.core.trade.DelayedPayoutTxValidation;
|
||||
import bisq.core.trade.Trade;
|
||||
import bisq.core.trade.TradeDataValidation;
|
||||
import bisq.core.trade.protocol.tasks.TradeTask;
|
||||
|
||||
import bisq.common.taskrunner.TaskRunner;
|
||||
|
@ -40,23 +40,21 @@ public class BuyerVerifiesFinalDelayedPayoutTx extends TradeTask {
|
|||
try {
|
||||
runInterceptHook();
|
||||
|
||||
Transaction delayedPayoutTx = checkNotNull(trade.getDelayedPayoutTx());
|
||||
DelayedPayoutTxValidation.validatePayoutTx(trade,
|
||||
Transaction delayedPayoutTx = trade.getDelayedPayoutTx();
|
||||
checkNotNull(delayedPayoutTx, "trade.getDelayedPayoutTx() must not be null");
|
||||
// Check again tx
|
||||
TradeDataValidation.validatePayoutTx(trade,
|
||||
delayedPayoutTx,
|
||||
processModel.getDaoFacade(),
|
||||
processModel.getBtcWalletService());
|
||||
|
||||
// Now as we know the deposit tx we can also verify the input
|
||||
Transaction depositTx = checkNotNull(trade.getDepositTx());
|
||||
DelayedPayoutTxValidation.validatePayoutTxInput(depositTx, delayedPayoutTx);
|
||||
Transaction depositTx = trade.getDepositTx();
|
||||
checkNotNull(depositTx, "trade.getDepositTx() must not be null");
|
||||
TradeDataValidation.validatePayoutTxInput(depositTx, delayedPayoutTx);
|
||||
|
||||
complete();
|
||||
} catch (DelayedPayoutTxValidation.DonationAddressException |
|
||||
DelayedPayoutTxValidation.MissingDelayedPayoutTxException |
|
||||
DelayedPayoutTxValidation.InvalidTxException |
|
||||
DelayedPayoutTxValidation.InvalidLockTimeException |
|
||||
DelayedPayoutTxValidation.AmountMismatchException |
|
||||
DelayedPayoutTxValidation.InvalidInputException e) {
|
||||
} catch (TradeDataValidation.ValidationException e) {
|
||||
failed(e.getMessage());
|
||||
} catch (Throwable t) {
|
||||
failed(t);
|
||||
|
|
|
@ -17,8 +17,8 @@
|
|||
|
||||
package bisq.core.trade.protocol.tasks.buyer;
|
||||
|
||||
import bisq.core.trade.DelayedPayoutTxValidation;
|
||||
import bisq.core.trade.Trade;
|
||||
import bisq.core.trade.TradeDataValidation;
|
||||
import bisq.core.trade.protocol.tasks.TradeTask;
|
||||
|
||||
import bisq.common.taskrunner.TaskRunner;
|
||||
|
@ -36,17 +36,13 @@ public class BuyerVerifiesPreparedDelayedPayoutTx extends TradeTask {
|
|||
try {
|
||||
runInterceptHook();
|
||||
|
||||
DelayedPayoutTxValidation.validatePayoutTx(trade,
|
||||
TradeDataValidation.validatePayoutTx(trade,
|
||||
processModel.getPreparedDelayedPayoutTx(),
|
||||
processModel.getDaoFacade(),
|
||||
processModel.getBtcWalletService());
|
||||
|
||||
complete();
|
||||
} catch (DelayedPayoutTxValidation.DonationAddressException |
|
||||
DelayedPayoutTxValidation.MissingDelayedPayoutTxException |
|
||||
DelayedPayoutTxValidation.InvalidTxException |
|
||||
DelayedPayoutTxValidation.InvalidLockTimeException |
|
||||
DelayedPayoutTxValidation.AmountMismatchException e) {
|
||||
} catch (TradeDataValidation.ValidationException e) {
|
||||
failed(e.getMessage());
|
||||
} catch (Throwable t) {
|
||||
failed(t);
|
||||
|
|
|
@ -214,7 +214,8 @@ shared.mediator=Mediator
|
|||
shared.arbitrator=Arbitrator
|
||||
shared.refundAgent=Arbitrator
|
||||
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.
|
||||
|
||||
|
||||
|
@ -894,6 +895,12 @@ portfolio.pending.mediationResult.popup.info=The mediator has suggested the foll
|
|||
(or if the other peer is unresponsive).\n\n\
|
||||
More details about the new arbitration model:\n\
|
||||
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.alreadyAccepted=You've already accepted
|
||||
|
||||
|
@ -1007,11 +1014,24 @@ support.tab.legacyArbitration.support=Legacy Arbitration
|
|||
support.tab.ArbitratorsSupportTickets={0}'s tickets
|
||||
support.filter=Search disputes
|
||||
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.reOpenButton.label=Re-open dispute
|
||||
support.sendNotificationButton.label=Send private notification
|
||||
support.reportButton.label=Generate report
|
||||
support.fullReportButton.label=Get text dump of all disputes
|
||||
support.reOpenButton.label=Re-open
|
||||
support.sendNotificationButton.label=Private notification
|
||||
support.reportButton.label=Report
|
||||
support.fullReportButton.label=All disputes
|
||||
support.noTickets=There are no open tickets
|
||||
support.sendingMessage=Sending Message...
|
||||
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.mediatorsDisputeSummary=System message: Mediator''s dispute summary:\n{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.addSummaryNotes=Add summary notes
|
||||
disputeSummaryWindow.close.button=Close ticket
|
||||
disputeSummaryWindow.close.msg=Ticket closed on {0}\n\n\
|
||||
Summary:\n\
|
||||
Payout amount for BTC buyer: {1}\n\
|
||||
Payout amount for BTC seller: {2}\n\n\
|
||||
Reason for dispute: {3}\n\n\
|
||||
Summary notes:\n{4}
|
||||
disputeSummaryWindow.close.nextStepsForMediation=\n\nNext steps:\n\
|
||||
|
||||
# Do no change any line break or order of tokens as the structure is used for signature verification
|
||||
disputeSummaryWindow.close.msg=Ticket closed on {0}\n\
|
||||
{1} node address: {2}\n\n\
|
||||
Summary:\n\
|
||||
Trade ID: {3}\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
|
||||
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
|
||||
disputeSummaryWindow.close.closePeer=You need to close also the trading peers ticket!
|
||||
disputeSummaryWindow.close.txDetails.headline=Publish refund transaction
|
||||
|
@ -2457,6 +2496,9 @@ disputeSummaryWindow.close.txDetails=Spending: {0}\n\
|
|||
Transaction size: {5} Kb\n\n\
|
||||
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.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\
|
||||
|
|
|
@ -141,6 +141,8 @@ public class ContractWindow extends Overlay<ContractWindow> {
|
|||
rows++;
|
||||
if (dispute.getDelayedPayoutTxId() != null)
|
||||
rows++;
|
||||
if (dispute.getDonationAddressOfDelayedPayoutTx() != null)
|
||||
rows++;
|
||||
if (showAcceptedCountryCodes)
|
||||
rows++;
|
||||
if (showAcceptedBanks)
|
||||
|
@ -248,6 +250,11 @@ public class ContractWindow extends Overlay<ContractWindow> {
|
|||
if (dispute.getDelayedPayoutTxId() != null)
|
||||
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)
|
||||
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.main.overlays.Overlay;
|
||||
import bisq.desktop.main.overlays.popups.Popup;
|
||||
import bisq.desktop.main.support.dispute.DisputeSummaryVerification;
|
||||
import bisq.desktop.util.DisplayUtils;
|
||||
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.TradeWalletService;
|
||||
import bisq.core.btc.wallet.TxBroadcaster;
|
||||
import bisq.core.dao.DaoFacade;
|
||||
import bisq.core.locale.Res;
|
||||
import bisq.core.offer.Offer;
|
||||
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.refund.RefundManager;
|
||||
import bisq.core.trade.Contract;
|
||||
import bisq.core.trade.TradeDataValidation;
|
||||
import bisq.core.util.FormattingUtils;
|
||||
import bisq.core.util.ParsingUtils;
|
||||
import bisq.core.util.coin.CoinFormatter;
|
||||
|
@ -86,8 +89,7 @@ import java.util.Date;
|
|||
import java.util.Optional;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import static bisq.desktop.util.FormBuilder.add2ButtonsWithBox;
|
||||
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 com.google.common.base.Preconditions.checkNotNull;
|
||||
|
||||
@Slf4j
|
||||
public class DisputeSummaryWindow extends Overlay<DisputeSummaryWindow> {
|
||||
private static final Logger log = LoggerFactory.getLogger(DisputeSummaryWindow.class);
|
||||
|
||||
private final CoinFormatter formatter;
|
||||
private final MediationManager mediationManager;
|
||||
private final RefundManager refundManager;
|
||||
|
@ -105,8 +106,9 @@ public class DisputeSummaryWindow extends Overlay<DisputeSummaryWindow> {
|
|||
private final BtcWalletService btcWalletService;
|
||||
private final TxFeeEstimationService txFeeEstimationService;
|
||||
private final FeeService feeService;
|
||||
private final DaoFacade daoFacade;
|
||||
private Dispute dispute;
|
||||
private Optional<Runnable> finalizeDisputeHandlerOptional = Optional.<Runnable>empty();
|
||||
private Optional<Runnable> finalizeDisputeHandlerOptional = Optional.empty();
|
||||
private ToggleGroup tradeAmountToggleGroup, reasonToggleGroup;
|
||||
private DisputeResult disputeResult;
|
||||
private RadioButton buyerGetsTradeAmountRadioButton, sellerGetsTradeAmountRadioButton,
|
||||
|
@ -141,7 +143,8 @@ public class DisputeSummaryWindow extends Overlay<DisputeSummaryWindow> {
|
|||
TradeWalletService tradeWalletService,
|
||||
BtcWalletService btcWalletService,
|
||||
TxFeeEstimationService txFeeEstimationService,
|
||||
FeeService feeService) {
|
||||
FeeService feeService,
|
||||
DaoFacade daoFacade) {
|
||||
|
||||
this.formatter = formatter;
|
||||
this.mediationManager = mediationManager;
|
||||
|
@ -150,6 +153,7 @@ public class DisputeSummaryWindow extends Overlay<DisputeSummaryWindow> {
|
|||
this.btcWalletService = btcWalletService;
|
||||
this.txFeeEstimationService = txFeeEstimationService;
|
||||
this.feeService = feeService;
|
||||
this.daoFacade = daoFacade;
|
||||
|
||||
type = Type.Confirmation;
|
||||
}
|
||||
|
@ -222,7 +226,7 @@ public class DisputeSummaryWindow extends Overlay<DisputeSummaryWindow> {
|
|||
else
|
||||
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())
|
||||
.findFirst();
|
||||
|
||||
|
@ -382,14 +386,15 @@ public class DisputeSummaryWindow extends Overlay<DisputeSummaryWindow> {
|
|||
.add(offer.getSellerSecurityDeposit());
|
||||
Coin totalAmount = buyerAmount.add(sellerAmount);
|
||||
|
||||
if (!totalAmount.isPositive()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (getDisputeManager(dispute) instanceof RefundManager) {
|
||||
// We allow to spend less in case of RefundAgent
|
||||
boolean isRefundAgent = getDisputeManager(dispute) instanceof RefundManager;
|
||||
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
|
||||
return totalAmount.compareTo(available) <= 0;
|
||||
} else {
|
||||
if (!totalAmount.isPositive()) {
|
||||
return false;
|
||||
}
|
||||
return totalAmount.compareTo(available) == 0;
|
||||
}
|
||||
}
|
||||
|
@ -642,15 +647,15 @@ public class DisputeSummaryWindow extends Overlay<DisputeSummaryWindow> {
|
|||
log.warn("dispute.getDepositTxSerialized is null");
|
||||
return;
|
||||
}
|
||||
|
||||
if (dispute.getSupportType() == SupportType.REFUND &&
|
||||
peersDisputeOptional.isPresent() &&
|
||||
!peersDisputeOptional.get().isClosed()) {
|
||||
showPayoutTxConfirmation(contract, disputeResult,
|
||||
() -> {
|
||||
doClose(closeTicketButton);
|
||||
});
|
||||
showPayoutTxConfirmation(contract,
|
||||
disputeResult,
|
||||
() -> doCloseIfValid(closeTicketButton));
|
||||
} else {
|
||||
doClose(closeTicketButton);
|
||||
doCloseIfValid(closeTicketButton);
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -684,28 +689,36 @@ public class DisputeSummaryWindow extends Overlay<DisputeSummaryWindow> {
|
|||
formatter.formatCoinWithCode(sellerPayoutAmount),
|
||||
sellerPayoutAddressString);
|
||||
}
|
||||
new Popup().width(900)
|
||||
.headLine(Res.get("disputeSummaryWindow.close.txDetails.headline"))
|
||||
.confirmation(Res.get("disputeSummaryWindow.close.txDetails",
|
||||
formatter.formatCoinWithCode(inputAmount),
|
||||
buyerDetails,
|
||||
sellerDetails,
|
||||
formatter.formatCoinWithCode(fee),
|
||||
feePerByte,
|
||||
kb))
|
||||
.actionButtonText(Res.get("shared.yes"))
|
||||
.onAction(() -> {
|
||||
doPayout(buyerPayoutAmount,
|
||||
sellerPayoutAmount,
|
||||
fee,
|
||||
buyerPayoutAddressString,
|
||||
sellerPayoutAddressString,
|
||||
resultHandler);
|
||||
})
|
||||
.closeButtonText(Res.get("shared.cancel"))
|
||||
.onClose(() -> {
|
||||
})
|
||||
.show();
|
||||
if (outputAmount.isPositive()) {
|
||||
new Popup().width(900)
|
||||
.headLine(Res.get("disputeSummaryWindow.close.txDetails.headline"))
|
||||
.confirmation(Res.get("disputeSummaryWindow.close.txDetails",
|
||||
formatter.formatCoinWithCode(inputAmount),
|
||||
buyerDetails,
|
||||
sellerDetails,
|
||||
formatter.formatCoinWithCode(fee),
|
||||
feePerByte,
|
||||
kb))
|
||||
.actionButtonText(Res.get("shared.yes"))
|
||||
.onAction(() -> {
|
||||
doPayout(buyerPayoutAmount,
|
||||
sellerPayoutAmount,
|
||||
fee,
|
||||
buyerPayoutAddressString,
|
||||
sellerPayoutAddressString,
|
||||
resultHandler);
|
||||
})
|
||||
.closeButtonText(Res.get("shared.cancel"))
|
||||
.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,
|
||||
|
@ -720,7 +733,6 @@ public class DisputeSummaryWindow extends Overlay<DisputeSummaryWindow> {
|
|||
fee,
|
||||
buyerPayoutAddressString,
|
||||
sellerPayoutAddressString);
|
||||
log.error("transaction " + tx);
|
||||
tradeWalletService.broadcastTx(tx, new TxBroadcaster.Callback() {
|
||||
@Override
|
||||
public void onSuccess(Transaction transaction) {
|
||||
|
@ -731,7 +743,6 @@ public class DisputeSummaryWindow extends Overlay<DisputeSummaryWindow> {
|
|||
public void onFailure(TxBroadcastException exception) {
|
||||
log.error("TxBroadcastException at doPayout", exception);
|
||||
new Popup().error(exception.toString()).show();
|
||||
;
|
||||
}
|
||||
});
|
||||
} 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) {
|
||||
DisputeManager<? extends DisputeList<? extends DisputeList>> disputeManager = getDisputeManager(dispute);
|
||||
if (disputeManager == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
boolean isRefundAgent = disputeManager instanceof RefundManager;
|
||||
disputeResult.setLoserPublisher(isLoserPublisherCheckBox.isSelected());
|
||||
disputeResult.setCloseDate(new Date());
|
||||
dispute.setDisputeResult(disputeResult);
|
||||
dispute.setIsClosed(true);
|
||||
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()),
|
||||
role,
|
||||
agentNodeAddress,
|
||||
dispute.getShortTradeId(),
|
||||
currencyCode,
|
||||
amount,
|
||||
formatter.formatCoinWithCode(disputeResult.getBuyerPayoutAmount()),
|
||||
formatter.formatCoinWithCode(disputeResult.getSellerPayoutAmount()),
|
||||
Res.get("disputeSummaryWindow.reason." + reason.name()),
|
||||
disputeResult.summaryNotesProperty().get());
|
||||
disputeResult.summaryNotesProperty().get()
|
||||
);
|
||||
|
||||
if (reason == DisputeResult.Reason.OPTION_TRADE &&
|
||||
dispute.getChatMessages().size() > 1 &&
|
||||
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) {
|
||||
text += Res.get("disputeSummaryWindow.close.nextStepsForMediation");
|
||||
} else if (dispute.getSupportType() == SupportType.REFUND) {
|
||||
text += Res.get("disputeSummaryWindow.close.nextStepsForRefundAgentArbitration");
|
||||
String summaryText = DisputeSummaryVerification.signAndApply(disputeManager, disputeResult, textToSign);
|
||||
|
||||
if (isRefundAgent) {
|
||||
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()) {
|
||||
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.SellerTrade;
|
||||
import bisq.core.trade.Trade;
|
||||
import bisq.core.trade.TradeDataValidation;
|
||||
import bisq.core.trade.TradeManager;
|
||||
import bisq.core.user.Preferences;
|
||||
import bisq.core.util.FormattingUtils;
|
||||
|
@ -78,6 +79,8 @@ import javafx.collections.ObservableList;
|
|||
|
||||
import org.bouncycastle.crypto.params.KeyParameter;
|
||||
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
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
|
||||
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;
|
||||
if (useMediation) {
|
||||
// If no dispute state set we start with mediation
|
||||
|
@ -523,6 +548,11 @@ public class PendingTradesDataModel extends ActivatableDataModel {
|
|||
isSupportTicket,
|
||||
SupportType.MEDIATION);
|
||||
|
||||
dispute.setDonationAddressOfDelayedPayoutTx(donationAddressString.get());
|
||||
if (delayedPayoutTx != null) {
|
||||
dispute.setDelayedPayoutTxId(delayedPayoutTx.getTxId().toString());
|
||||
}
|
||||
|
||||
trade.setDisputeState(Trade.DisputeState.MEDIATION_REQUESTED);
|
||||
disputeManager.sendOpenNewDisputeMessage(dispute,
|
||||
false,
|
||||
|
@ -547,7 +577,7 @@ public class PendingTradesDataModel extends ActivatableDataModel {
|
|||
} else if (useRefundAgent) {
|
||||
resultHandler = () -> navigation.navigateTo(MainView.class, SupportView.class, RefundClientView.class);
|
||||
|
||||
if (trade.getDelayedPayoutTx() == null) {
|
||||
if (delayedPayoutTx == null) {
|
||||
log.error("Delayed payout tx is missing");
|
||||
return;
|
||||
}
|
||||
|
@ -562,13 +592,12 @@ public class PendingTradesDataModel extends ActivatableDataModel {
|
|||
return;
|
||||
}
|
||||
|
||||
long lockTime = trade.getDelayedPayoutTx().getLockTime();
|
||||
long lockTime = delayedPayoutTx.getLockTime();
|
||||
int bestChainHeight = btcWalletService.getBestChainHeight();
|
||||
long remaining = lockTime - bestChainHeight;
|
||||
if (remaining > 0) {
|
||||
new Popup()
|
||||
.instruction(Res.get("portfolio.pending.timeLockNotOver",
|
||||
FormattingUtils.getDateFromBlockHeight(remaining), remaining))
|
||||
new Popup().instruction(Res.get("portfolio.pending.timeLockNotOver",
|
||||
FormattingUtils.getDateFromBlockHeight(remaining), remaining))
|
||||
.show();
|
||||
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);
|
||||
|
||||
|
|
|
@ -30,6 +30,7 @@ import bisq.core.locale.CurrencyUtil;
|
|||
import bisq.core.locale.Res;
|
||||
import bisq.core.support.dispute.Dispute;
|
||||
import bisq.core.support.dispute.DisputeResult;
|
||||
import bisq.core.support.dispute.mediation.MediationResultState;
|
||||
import bisq.core.trade.Contract;
|
||||
import bisq.core.trade.Trade;
|
||||
import bisq.core.user.Preferences;
|
||||
|
@ -41,6 +42,9 @@ import bisq.common.ClockWatcher;
|
|||
import bisq.common.UserThread;
|
||||
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.AwesomeIcon;
|
||||
|
||||
|
@ -62,6 +66,7 @@ import javafx.geometry.Insets;
|
|||
import org.fxmisc.easybind.EasyBind;
|
||||
import org.fxmisc.easybind.Subscription;
|
||||
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
import javafx.beans.value.ChangeListener;
|
||||
|
||||
import java.util.Optional;
|
||||
|
@ -97,6 +102,8 @@ public abstract class TradeStepView extends AnchorPane {
|
|||
private Popup acceptMediationResultPopup;
|
||||
private BootstrapListener bootstrapListener;
|
||||
private TradeSubView.ChatCallback chatCallback;
|
||||
private final NewBestBlockListener newBestBlockListener;
|
||||
private ChangeListener<Boolean> pendingTradesInitializedListener;
|
||||
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
@ -158,6 +165,10 @@ public abstract class TradeStepView extends AnchorPane {
|
|||
updateTimeLeft();
|
||||
}
|
||||
};
|
||||
|
||||
newBestBlockListener = block -> {
|
||||
checkIfLockTimeIsOver();
|
||||
};
|
||||
}
|
||||
|
||||
public void activate() {
|
||||
|
@ -200,14 +211,34 @@ public abstract class TradeStepView extends AnchorPane {
|
|||
}
|
||||
|
||||
tradePeriodStateSubscription = EasyBind.subscribe(trade.tradePeriodStateProperty(), newValue -> {
|
||||
if (newValue != null)
|
||||
if (newValue != null) {
|
||||
updateTradePeriodState(newValue);
|
||||
}
|
||||
});
|
||||
|
||||
model.clockWatcher.addListener(clockListener);
|
||||
|
||||
if (infoLabel != null)
|
||||
if (infoLabel != null) {
|
||||
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() {
|
||||
|
@ -262,6 +293,15 @@ public abstract class TradeStepView extends AnchorPane {
|
|||
|
||||
if (tradeStepInfo != 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);
|
||||
});
|
||||
|
||||
if (acceptMediationResultPopup != null) {
|
||||
acceptMediationResultPopup.hide();
|
||||
acceptMediationResultPopup = null;
|
||||
}
|
||||
|
||||
break;
|
||||
case REFUND_REQUEST_STARTED_BY_PEER:
|
||||
if (tradeStepInfo != null) {
|
||||
|
@ -457,6 +502,11 @@ public abstract class TradeStepView extends AnchorPane {
|
|||
if (tradeStepInfo != null)
|
||||
tradeStepInfo.setState(TradeStepInfo.State.IN_REFUND_REQUEST_PEER_REQUESTED);
|
||||
});
|
||||
|
||||
if (acceptMediationResultPopup != null) {
|
||||
acceptMediationResultPopup.hide();
|
||||
acceptMediationResultPopup = null;
|
||||
}
|
||||
break;
|
||||
case REFUND_REQUEST_CLOSED:
|
||||
break;
|
||||
|
@ -563,13 +613,34 @@ public abstract class TradeStepView extends AnchorPane {
|
|||
String actionButtonText = hasSelfAccepted() ?
|
||||
Res.get("portfolio.pending.mediationResult.popup.alreadyAccepted") : Res.get("shared.accept");
|
||||
|
||||
acceptMediationResultPopup = new Popup().width(900)
|
||||
.headLine(headLine)
|
||||
.instruction(Res.get("portfolio.pending.mediationResult.popup.info",
|
||||
String message;
|
||||
MediationResultState mediationResultState = checkNotNull(trade).getMediationResultState();
|
||||
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,
|
||||
peersPayoutAmount,
|
||||
FormattingUtils.getDateFromBlockHeight(remaining),
|
||||
lockTime))
|
||||
lockTime);
|
||||
break;
|
||||
}
|
||||
|
||||
acceptMediationResultPopup = new Popup().width(900)
|
||||
.headLine(headLine)
|
||||
.instruction(message)
|
||||
.actionButtonText(actionButtonText)
|
||||
.onAction(() -> {
|
||||
model.dataModel.mediationManager.acceptMediationResult(trade,
|
||||
|
@ -656,6 +727,18 @@ public abstract class TradeStepView extends AnchorPane {
|
|||
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
|
||||
|
|
|
@ -22,7 +22,7 @@ import bisq.desktop.main.portfolio.pendingtrades.PendingTradesViewModel;
|
|||
import bisq.desktop.main.portfolio.pendingtrades.steps.TradeStepView;
|
||||
|
||||
import bisq.core.locale.Res;
|
||||
import bisq.core.trade.DelayedPayoutTxValidation;
|
||||
import bisq.core.trade.TradeDataValidation;
|
||||
|
||||
public class BuyerStep1View extends TradeStepView {
|
||||
|
||||
|
@ -35,23 +35,9 @@ public class BuyerStep1View extends TradeStepView {
|
|||
}
|
||||
|
||||
@Override
|
||||
public void 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();
|
||||
}
|
||||
}
|
||||
protected void onPendingTradesInitialized() {
|
||||
super.onPendingTradesInitialized();
|
||||
validatePayoutTx();
|
||||
}
|
||||
|
||||
|
||||
|
@ -86,6 +72,28 @@ public class BuyerStep1View extends TradeStepView {
|
|||
protected String getPeriodOverWarnText() {
|
||||
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.USPostalMoneyOrderAccountPayload;
|
||||
import bisq.core.payment.payload.WesternUnionAccountPayload;
|
||||
import bisq.core.trade.DelayedPayoutTxValidation;
|
||||
import bisq.core.trade.Trade;
|
||||
import bisq.core.trade.TradeDataValidation;
|
||||
import bisq.core.user.DontShowAgainLookup;
|
||||
|
||||
import bisq.common.Timer;
|
||||
|
@ -117,21 +117,6 @@ public class BuyerStep2View extends TradeStepView {
|
|||
public void 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)
|
||||
timeoutTimer.stop();
|
||||
|
||||
|
@ -209,6 +194,13 @@ public class BuyerStep2View extends TradeStepView {
|
|||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPendingTradesInitialized() {
|
||||
super.onPendingTradesInitialized();
|
||||
validatePayoutTx();
|
||||
}
|
||||
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
// 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
|
||||
protected void updateConfirmButtonDisableState(boolean 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.SendPrivateNotificationWindow;
|
||||
import bisq.desktop.main.overlays.windows.TradeDetailsWindow;
|
||||
import bisq.desktop.main.overlays.windows.VerifyDisputeResultSignatureWindow;
|
||||
import bisq.desktop.main.shared.ChatView;
|
||||
import bisq.desktop.util.DisplayUtils;
|
||||
import bisq.desktop.util.GUIUtil;
|
||||
|
||||
import bisq.core.account.witness.AccountAgeWitnessService;
|
||||
import bisq.core.alert.PrivateNotificationManager;
|
||||
import bisq.core.dao.DaoFacade;
|
||||
import bisq.core.locale.CurrencyUtil;
|
||||
import bisq.core.locale.Res;
|
||||
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.DisputeResult;
|
||||
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.trade.Contract;
|
||||
import bisq.core.trade.Trade;
|
||||
|
@ -58,8 +62,6 @@ import bisq.common.util.Utilities;
|
|||
|
||||
import org.bitcoinj.core.Coin;
|
||||
|
||||
import com.google.common.collect.Lists;
|
||||
|
||||
import de.jensd.fx.glyphs.materialdesignicons.MaterialDesignIcon;
|
||||
|
||||
import javafx.scene.control.Button;
|
||||
|
@ -89,9 +91,8 @@ import javafx.collections.transformation.FilteredList;
|
|||
import javafx.collections.transformation.SortedList;
|
||||
|
||||
import javafx.util.Callback;
|
||||
import javafx.util.Duration;
|
||||
|
||||
import java.text.DateFormat;
|
||||
import java.text.ParseException;
|
||||
import java.text.SimpleDateFormat;
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
@ -102,6 +103,7 @@ import java.util.List;
|
|||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
import lombok.Getter;
|
||||
|
||||
|
@ -110,6 +112,32 @@ import javax.annotation.Nullable;
|
|||
import static bisq.desktop.util.FormBuilder.getIconForLabel;
|
||||
|
||||
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 KeyRing keyRing;
|
||||
|
@ -121,6 +149,9 @@ public abstract class DisputeView extends ActivatableView<VBox, Void> {
|
|||
private final TradeDetailsWindow tradeDetailsWindow;
|
||||
|
||||
private final AccountAgeWitnessService accountAgeWitnessService;
|
||||
private final MediatorManager mediatorManager;
|
||||
private final RefundAgentManager refundAgentManager;
|
||||
protected final DaoFacade daoFacade;
|
||||
private final boolean useDevPrivilegeKeys;
|
||||
|
||||
protected TableView<Dispute> tableView;
|
||||
|
@ -136,7 +167,7 @@ public abstract class DisputeView extends ActivatableView<VBox, Void> {
|
|||
protected FilteredList<Dispute> filteredList;
|
||||
protected InputTextField filterTextField;
|
||||
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<>();
|
||||
@Nullable
|
||||
private ListChangeListener<Dispute> disputesListener; // Only set in mediation cases
|
||||
|
@ -157,6 +188,9 @@ public abstract class DisputeView extends ActivatableView<VBox, Void> {
|
|||
ContractWindow contractWindow,
|
||||
TradeDetailsWindow tradeDetailsWindow,
|
||||
AccountAgeWitnessService accountAgeWitnessService,
|
||||
MediatorManager mediatorManager,
|
||||
RefundAgentManager refundAgentManager,
|
||||
DaoFacade daoFacade,
|
||||
boolean useDevPrivilegeKeys) {
|
||||
this.disputeManager = disputeManager;
|
||||
this.keyRing = keyRing;
|
||||
|
@ -167,6 +201,9 @@ public abstract class DisputeView extends ActivatableView<VBox, Void> {
|
|||
this.contractWindow = contractWindow;
|
||||
this.tradeDetailsWindow = tradeDetailsWindow;
|
||||
this.accountAgeWitnessService = accountAgeWitnessService;
|
||||
this.mediatorManager = mediatorManager;
|
||||
this.refundAgentManager = refundAgentManager;
|
||||
this.daoFacade = daoFacade;
|
||||
this.useDevPrivilegeKeys = useDevPrivilegeKeys;
|
||||
}
|
||||
|
||||
|
@ -177,6 +214,10 @@ public abstract class DisputeView extends ActivatableView<VBox, Void> {
|
|||
HBox.setHgrow(label, Priority.NEVER);
|
||||
|
||||
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());
|
||||
HBox.setHgrow(filterTextField, Priority.NEVER);
|
||||
|
||||
|
@ -222,6 +263,12 @@ public abstract class DisputeView extends ActivatableView<VBox, Void> {
|
|||
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();
|
||||
HBox.setHgrow(spacer, Priority.ALWAYS);
|
||||
|
||||
|
@ -234,7 +281,8 @@ public abstract class DisputeView extends ActivatableView<VBox, Void> {
|
|||
reOpenButton,
|
||||
sendPrivateNotificationButton,
|
||||
reportButton,
|
||||
fullReportButton);
|
||||
fullReportButton,
|
||||
sigCheckButton);
|
||||
VBox.setVgrow(filterBox, Priority.NEVER);
|
||||
|
||||
tableView = new TableView<>();
|
||||
|
@ -255,7 +303,8 @@ public abstract class DisputeView extends ActivatableView<VBox, Void> {
|
|||
protected void activate() {
|
||||
filterTextField.textProperty().addListener(filterTextFieldListener);
|
||||
|
||||
filteredList = new FilteredList<>(disputeManager.getDisputesAsObservableList());
|
||||
ObservableList<Dispute> disputesAsObservableList = disputeManager.getDisputesAsObservableList();
|
||||
filteredList = new FilteredList<>(disputesAsObservableList);
|
||||
applyFilteredListPredicate(filterTextField.getText());
|
||||
|
||||
sortedList = new SortedList<>(filteredList);
|
||||
|
@ -276,54 +325,6 @@ public abstract class DisputeView extends ActivatableView<VBox, Void> {
|
|||
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);
|
||||
}
|
||||
|
||||
|
@ -385,10 +386,89 @@ public abstract class DisputeView extends ActivatableView<VBox, Void> {
|
|||
protected abstract DisputeSession getConcreteDisputeChatSession(Dispute dispute);
|
||||
|
||||
protected void applyFilteredListPredicate(String filterString) {
|
||||
// If in trader view we must not display arbitrators own disputes as trader (must not happen anyway)
|
||||
filteredList.setPredicate(dispute -> !dispute.getAgentPubKeyRing().equals(keyRing.getPubKeyRing()));
|
||||
AtomicReference<FilterResult> filterResult = new AtomicReference<>(FilterResult.NO_FILTER);
|
||||
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() {
|
||||
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
|
||||
|
@ -547,6 +617,36 @@ public abstract class DisputeView extends ActivatableView<VBox, Void> {
|
|||
map.forEach((key, value) -> allDisputes.add(value));
|
||||
allDisputes.sort(Comparator.comparing(o -> !o.isEmpty() ? o.get(0).getOpeningDate() : new Date(0)));
|
||||
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();
|
||||
allDisputes.forEach(disputesPerTrade -> {
|
||||
if (disputesPerTrade.size() > 0) {
|
||||
|
@ -561,38 +661,76 @@ public abstract class DisputeView extends ActivatableView<VBox, Void> {
|
|||
disputeResult.getWinner() == DisputeResult.Winner.BUYER ? "Buyer" : "Seller";
|
||||
String buyerPayoutAmount = disputeResult != null ? disputeResult.getBuyerPayoutAmount().toFriendlyString() : "";
|
||||
String sellerPayoutAmount = disputeResult != null ? disputeResult.getSellerPayoutAmount().toFriendlyString() : "";
|
||||
stringBuilder.append("\n")
|
||||
.append("Dispute nr. ")
|
||||
.append(disputeIndex.incrementAndGet())
|
||||
|
||||
int index = 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("Opening date: ")
|
||||
.append(DisplayUtils.formatDateTime(openingDate))
|
||||
.append("Opening date: ").append(openingDateString)
|
||||
.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) {
|
||||
Date closeDate = disputeResult.getCloseDate();
|
||||
long duration = closeDate.getTime() - openingDate.getTime();
|
||||
stringBuilder.append("Close date: ")
|
||||
.append(DisplayUtils.formatDateTime(closeDate))
|
||||
.append("\n")
|
||||
.append("Dispute duration: ")
|
||||
.append(FormattingUtils.formatDurationAsWords(duration))
|
||||
.append("\n");
|
||||
|
||||
String closeDateString = dateFormatter.format(closeDate);
|
||||
String durationAsWords = FormattingUtils.formatDurationAsWords(duration);
|
||||
stringBuilder.append("Close date: ").append(closeDateString).append("\n")
|
||||
.append("Dispute duration: ").append(durationAsWords).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: ")
|
||||
.append(Res.get(contract.getPaymentMethodId()))
|
||||
.append(paymentMethod)
|
||||
.append("\n")
|
||||
.append("Currency: ")
|
||||
.append(CurrencyUtil.getNameAndCode(contract.getOfferPayload().getCurrencyCode()))
|
||||
.append(currency)
|
||||
.append("\n")
|
||||
.append("Trade amount: ")
|
||||
.append(contract.getTradeAmount().toFriendlyString())
|
||||
.append(tradeAmount)
|
||||
.append("\n")
|
||||
.append("Buyer/seller security deposit: ")
|
||||
.append(Coin.valueOf(contract.getOfferPayload().getBuyerSecurityDeposit()).toFriendlyString())
|
||||
.append(buyerDeposit)
|
||||
.append("/")
|
||||
.append(Coin.valueOf(contract.getOfferPayload().getSellerSecurityDeposit()).toFriendlyString())
|
||||
.append(sellerDeposit)
|
||||
.append("\n")
|
||||
.append("Dispute opened by: ")
|
||||
.append(opener)
|
||||
|
@ -603,6 +741,28 @@ public abstract class DisputeView extends ActivatableView<VBox, Void> {
|
|||
.append(winner)
|
||||
.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) {
|
||||
DisputeResult.Reason reason = disputeResult.getReason();
|
||||
if (firstDispute.disputeResultProperty().get().getReason() != null) {
|
||||
|
@ -611,10 +771,18 @@ public abstract class DisputeView extends ActivatableView<VBox, Void> {
|
|||
stringBuilder.append("Reason: ")
|
||||
.append(reason.name())
|
||||
.append("\n");
|
||||
|
||||
csvStringBuilder.append(reason.name()).append(";");
|
||||
} else {
|
||||
csvStringBuilder.append(";");
|
||||
}
|
||||
|
||||
summaryNotes0 = disputeResult.getSummaryNotesProperty().get();
|
||||
stringBuilder.append("Summary notes: ").append(summaryNotes0).append("\n");
|
||||
summaryNotes = disputeResult.getSummaryNotesProperty().get();
|
||||
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
|
||||
|
@ -624,8 +792,12 @@ public abstract class DisputeView extends ActivatableView<VBox, Void> {
|
|||
DisputeResult disputeResult1 = dispute1.getDisputeResultProperty().get();
|
||||
if (disputeResult1 != null) {
|
||||
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");
|
||||
|
||||
csvStringBuilder.append(summaryNotes1).append(";");
|
||||
} else {
|
||||
csvStringBuilder.append(";");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -643,8 +815,9 @@ public abstract class DisputeView extends ActivatableView<VBox, Void> {
|
|||
.width(1200)
|
||||
.actionButtonText("Copy to clipboard")
|
||||
.onAction(() -> Utilities.copyToClipboard(message))
|
||||
.secondaryActionButtonText("Copy as csv data")
|
||||
.onSecondaryAction(() -> Utilities.copyToClipboard(csvStringBuilder.toString()))
|
||||
.show();
|
||||
|
||||
}
|
||||
|
||||
private void showFullReport() {
|
||||
|
|
|
@ -27,12 +27,16 @@ import bisq.desktop.main.support.dispute.DisputeView;
|
|||
|
||||
import bisq.core.account.witness.AccountAgeWitnessService;
|
||||
import bisq.core.alert.PrivateNotificationManager;
|
||||
import bisq.core.dao.DaoFacade;
|
||||
import bisq.core.locale.Res;
|
||||
import bisq.core.support.dispute.Dispute;
|
||||
import bisq.core.support.dispute.DisputeList;
|
||||
import bisq.core.support.dispute.DisputeManager;
|
||||
import bisq.core.support.dispute.DisputeSession;
|
||||
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.user.DontShowAgainLookup;
|
||||
import bisq.core.util.coin.CoinFormatter;
|
||||
|
@ -54,13 +58,17 @@ import javafx.geometry.Insets;
|
|||
|
||||
import javafx.beans.property.ReadOnlyObjectWrapper;
|
||||
|
||||
import javafx.collections.ListChangeListener;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static bisq.core.trade.TradeDataValidation.ValidationException;
|
||||
import static bisq.desktop.util.FormBuilder.getIconForLabel;
|
||||
|
||||
public abstract class DisputeAgentView extends DisputeView implements MultipleHolderNameDetection.Listener {
|
||||
|
||||
private final MultipleHolderNameDetection multipleHolderNameDetection;
|
||||
private ListChangeListener<ValidationException> validationExceptionListener;
|
||||
|
||||
public DisputeAgentView(DisputeManager<? extends DisputeList<? extends DisputeList>> disputeManager,
|
||||
KeyRing keyRing,
|
||||
|
@ -71,6 +79,9 @@ public abstract class DisputeAgentView extends DisputeView implements MultipleHo
|
|||
ContractWindow contractWindow,
|
||||
TradeDetailsWindow tradeDetailsWindow,
|
||||
AccountAgeWitnessService accountAgeWitnessService,
|
||||
DaoFacade daoFacade,
|
||||
MediatorManager mediatorManager,
|
||||
RefundAgentManager refundAgentManager,
|
||||
boolean useDevPrivilegeKeys) {
|
||||
super(disputeManager,
|
||||
keyRing,
|
||||
|
@ -81,6 +92,9 @@ public abstract class DisputeAgentView extends DisputeView implements MultipleHo
|
|||
contractWindow,
|
||||
tradeDetailsWindow,
|
||||
accountAgeWitnessService,
|
||||
mediatorManager,
|
||||
refundAgentManager,
|
||||
daoFacade,
|
||||
useDevPrivilegeKeys);
|
||||
|
||||
multipleHolderNameDetection = new MultipleHolderNameDetection(disputeManager);
|
||||
|
@ -107,6 +121,32 @@ public abstract class DisputeAgentView extends DisputeView implements MultipleHo
|
|||
fullReportButton.setManaged(true);
|
||||
|
||||
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
|
||||
|
@ -117,6 +157,9 @@ public abstract class DisputeAgentView extends DisputeView implements MultipleHo
|
|||
if (multipleHolderNameDetection.hasSuspiciousDisputesDetected()) {
|
||||
suspiciousDisputeDetected();
|
||||
}
|
||||
|
||||
disputeManager.getValidationExceptions().addListener(validationExceptionListener);
|
||||
showWarningForValidationExceptions(disputeManager.getValidationExceptions());
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -124,6 +167,8 @@ public abstract class DisputeAgentView extends DisputeView implements MultipleHo
|
|||
super.deactivate();
|
||||
|
||||
multipleHolderNameDetection.removeListener(this);
|
||||
|
||||
disputeManager.getValidationExceptions().removeListener(validationExceptionListener);
|
||||
}
|
||||
|
||||
|
||||
|
@ -142,17 +187,13 @@ public abstract class DisputeAgentView extends DisputeView implements MultipleHo
|
|||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
@Override
|
||||
protected void applyFilteredListPredicate(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 (!dispute.getAgentPubKeyRing().equals(keyRing.getPubKeyRing())) {
|
||||
return false;
|
||||
}
|
||||
boolean isOpen = !dispute.isClosed() && filterString.toLowerCase().equals("open");
|
||||
return filterString.isEmpty() ||
|
||||
isOpen ||
|
||||
anyMatchOfFilterString(dispute, filterString);
|
||||
});
|
||||
protected DisputeView.FilterResult getFilterResult(Dispute dispute, String filterString) {
|
||||
// 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())) {
|
||||
return FilterResult.NO_MATCH;
|
||||
}
|
||||
|
||||
return super.getFilterResult(dispute, filterString);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -25,11 +25,14 @@ import bisq.desktop.main.support.dispute.agent.DisputeAgentView;
|
|||
|
||||
import bisq.core.account.witness.AccountAgeWitnessService;
|
||||
import bisq.core.alert.PrivateNotificationManager;
|
||||
import bisq.core.dao.DaoFacade;
|
||||
import bisq.core.support.SupportType;
|
||||
import bisq.core.support.dispute.Dispute;
|
||||
import bisq.core.support.dispute.DisputeSession;
|
||||
import bisq.core.support.dispute.arbitration.ArbitrationManager;
|
||||
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.util.FormattingUtils;
|
||||
import bisq.core.util.coin.CoinFormatter;
|
||||
|
@ -53,6 +56,9 @@ public class ArbitratorView extends DisputeAgentView {
|
|||
ContractWindow contractWindow,
|
||||
TradeDetailsWindow tradeDetailsWindow,
|
||||
AccountAgeWitnessService accountAgeWitnessService,
|
||||
DaoFacade daoFacade,
|
||||
MediatorManager mediatorManager,
|
||||
RefundAgentManager refundAgentManager,
|
||||
@Named(Config.USE_DEV_PRIVILEGE_KEYS) boolean useDevPrivilegeKeys) {
|
||||
super(arbitrationManager,
|
||||
keyRing,
|
||||
|
@ -63,6 +69,9 @@ public class ArbitratorView extends DisputeAgentView {
|
|||
contractWindow,
|
||||
tradeDetailsWindow,
|
||||
accountAgeWitnessService,
|
||||
daoFacade,
|
||||
mediatorManager,
|
||||
refundAgentManager,
|
||||
useDevPrivilegeKeys);
|
||||
}
|
||||
|
||||
|
|
|
@ -25,11 +25,14 @@ import bisq.desktop.main.support.dispute.agent.DisputeAgentView;
|
|||
|
||||
import bisq.core.account.witness.AccountAgeWitnessService;
|
||||
import bisq.core.alert.PrivateNotificationManager;
|
||||
import bisq.core.dao.DaoFacade;
|
||||
import bisq.core.support.SupportType;
|
||||
import bisq.core.support.dispute.Dispute;
|
||||
import bisq.core.support.dispute.DisputeSession;
|
||||
import bisq.core.support.dispute.mediation.MediationManager;
|
||||
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.util.FormattingUtils;
|
||||
import bisq.core.util.coin.CoinFormatter;
|
||||
|
@ -53,6 +56,9 @@ public class MediatorView extends DisputeAgentView {
|
|||
ContractWindow contractWindow,
|
||||
TradeDetailsWindow tradeDetailsWindow,
|
||||
AccountAgeWitnessService accountAgeWitnessService,
|
||||
DaoFacade daoFacade,
|
||||
MediatorManager mediatorManager,
|
||||
RefundAgentManager refundAgentManager,
|
||||
@Named(Config.USE_DEV_PRIVILEGE_KEYS) boolean useDevPrivilegeKeys) {
|
||||
super(mediationManager,
|
||||
keyRing,
|
||||
|
@ -63,6 +69,9 @@ public class MediatorView extends DisputeAgentView {
|
|||
contractWindow,
|
||||
tradeDetailsWindow,
|
||||
accountAgeWitnessService,
|
||||
daoFacade,
|
||||
mediatorManager,
|
||||
refundAgentManager,
|
||||
useDevPrivilegeKeys);
|
||||
}
|
||||
|
||||
|
|
|
@ -25,11 +25,14 @@ import bisq.desktop.main.support.dispute.agent.DisputeAgentView;
|
|||
|
||||
import bisq.core.account.witness.AccountAgeWitnessService;
|
||||
import bisq.core.alert.PrivateNotificationManager;
|
||||
import bisq.core.dao.DaoFacade;
|
||||
import bisq.core.support.SupportType;
|
||||
import bisq.core.support.dispute.Dispute;
|
||||
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.RefundSession;
|
||||
import bisq.core.support.dispute.refund.refundagent.RefundAgentManager;
|
||||
import bisq.core.trade.TradeManager;
|
||||
import bisq.core.util.FormattingUtils;
|
||||
import bisq.core.util.coin.CoinFormatter;
|
||||
|
@ -37,9 +40,8 @@ import bisq.core.util.coin.CoinFormatter;
|
|||
import bisq.common.config.Config;
|
||||
import bisq.common.crypto.KeyRing;
|
||||
|
||||
import javax.inject.Named;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Named;
|
||||
|
||||
@FxmlView
|
||||
public class RefundAgentView extends DisputeAgentView {
|
||||
|
@ -54,6 +56,9 @@ public class RefundAgentView extends DisputeAgentView {
|
|||
ContractWindow contractWindow,
|
||||
TradeDetailsWindow tradeDetailsWindow,
|
||||
AccountAgeWitnessService accountAgeWitnessService,
|
||||
DaoFacade daoFacade,
|
||||
MediatorManager mediatorManager,
|
||||
RefundAgentManager refundAgentManager,
|
||||
@Named(Config.USE_DEV_PRIVILEGE_KEYS) boolean useDevPrivilegeKeys) {
|
||||
super(refundManager,
|
||||
keyRing,
|
||||
|
@ -64,6 +69,9 @@ public class RefundAgentView extends DisputeAgentView {
|
|||
contractWindow,
|
||||
tradeDetailsWindow,
|
||||
accountAgeWitnessService,
|
||||
daoFacade,
|
||||
mediatorManager,
|
||||
refundAgentManager,
|
||||
useDevPrivilegeKeys);
|
||||
}
|
||||
|
||||
|
|
|
@ -24,10 +24,13 @@ import bisq.desktop.main.support.dispute.DisputeView;
|
|||
|
||||
import bisq.core.account.witness.AccountAgeWitnessService;
|
||||
import bisq.core.alert.PrivateNotificationManager;
|
||||
import bisq.core.dao.DaoFacade;
|
||||
import bisq.core.support.dispute.Dispute;
|
||||
import bisq.core.support.dispute.DisputeList;
|
||||
import bisq.core.support.dispute.DisputeManager;
|
||||
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.util.coin.CoinFormatter;
|
||||
|
||||
|
@ -43,9 +46,13 @@ public abstract class DisputeClientView extends DisputeView {
|
|||
ContractWindow contractWindow,
|
||||
TradeDetailsWindow tradeDetailsWindow,
|
||||
AccountAgeWitnessService accountAgeWitnessService,
|
||||
MediatorManager mediatorManager,
|
||||
RefundAgentManager refundAgentManager,
|
||||
DaoFacade daoFacade,
|
||||
boolean useDevPrivilegeKeys) {
|
||||
super(DisputeManager, keyRing, tradeManager, formatter, disputeSummaryWindow, privateNotificationManager,
|
||||
contractWindow, tradeDetailsWindow, accountAgeWitnessService, useDevPrivilegeKeys);
|
||||
contractWindow, tradeDetailsWindow, accountAgeWitnessService,
|
||||
mediatorManager, refundAgentManager, daoFacade, useDevPrivilegeKeys);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -55,14 +62,12 @@ public abstract class DisputeClientView extends DisputeView {
|
|||
}
|
||||
|
||||
@Override
|
||||
protected void applyFilteredListPredicate(String filterString) {
|
||||
filteredList.setPredicate(dispute -> {
|
||||
// As we are in the client view we hide disputes where we are the agent
|
||||
if (dispute.getAgentPubKeyRing().equals(keyRing.getPubKeyRing())) {
|
||||
return false;
|
||||
}
|
||||
protected DisputeView.FilterResult getFilterResult(Dispute dispute, String filterString) {
|
||||
// As we are in the client view we hide disputes where we are the agent
|
||||
if (dispute.getAgentPubKeyRing().equals(keyRing.getPubKeyRing())) {
|
||||
return FilterResult.NO_MATCH;
|
||||
}
|
||||
|
||||
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.alert.PrivateNotificationManager;
|
||||
import bisq.core.dao.DaoFacade;
|
||||
import bisq.core.support.SupportType;
|
||||
import bisq.core.support.dispute.Dispute;
|
||||
import bisq.core.support.dispute.DisputeSession;
|
||||
import bisq.core.support.dispute.arbitration.ArbitrationManager;
|
||||
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.util.FormattingUtils;
|
||||
import bisq.core.util.coin.CoinFormatter;
|
||||
|
@ -37,9 +40,8 @@ import bisq.core.util.coin.CoinFormatter;
|
|||
import bisq.common.config.Config;
|
||||
import bisq.common.crypto.KeyRing;
|
||||
|
||||
import javax.inject.Named;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Named;
|
||||
|
||||
@FxmlView
|
||||
public class ArbitrationClientView extends DisputeClientView {
|
||||
|
@ -53,10 +55,13 @@ public class ArbitrationClientView extends DisputeClientView {
|
|||
ContractWindow contractWindow,
|
||||
TradeDetailsWindow tradeDetailsWindow,
|
||||
AccountAgeWitnessService accountAgeWitnessService,
|
||||
MediatorManager mediatorManager,
|
||||
RefundAgentManager refundAgentManager,
|
||||
DaoFacade daoFacade,
|
||||
@Named(Config.USE_DEV_PRIVILEGE_KEYS) boolean useDevPrivilegeKeys) {
|
||||
super(arbitrationManager, keyRing, tradeManager, formatter, disputeSummaryWindow,
|
||||
privateNotificationManager, contractWindow, tradeDetailsWindow, accountAgeWitnessService,
|
||||
useDevPrivilegeKeys);
|
||||
mediatorManager, refundAgentManager, daoFacade, useDevPrivilegeKeys);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -26,12 +26,15 @@ import bisq.desktop.main.support.dispute.client.DisputeClientView;
|
|||
|
||||
import bisq.core.account.witness.AccountAgeWitnessService;
|
||||
import bisq.core.alert.PrivateNotificationManager;
|
||||
import bisq.core.dao.DaoFacade;
|
||||
import bisq.core.locale.Res;
|
||||
import bisq.core.support.SupportType;
|
||||
import bisq.core.support.dispute.Dispute;
|
||||
import bisq.core.support.dispute.DisputeSession;
|
||||
import bisq.core.support.dispute.mediation.MediationManager;
|
||||
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.util.FormattingUtils;
|
||||
import bisq.core.util.coin.CoinFormatter;
|
||||
|
@ -54,10 +57,13 @@ public class MediationClientView extends DisputeClientView {
|
|||
ContractWindow contractWindow,
|
||||
TradeDetailsWindow tradeDetailsWindow,
|
||||
AccountAgeWitnessService accountAgeWitnessService,
|
||||
MediatorManager mediatorManager,
|
||||
RefundAgentManager refundAgentManager,
|
||||
DaoFacade daoFacade,
|
||||
@Named(Config.USE_DEV_PRIVILEGE_KEYS) boolean useDevPrivilegeKeys) {
|
||||
super(mediationManager, keyRing, tradeManager, formatter, disputeSummaryWindow,
|
||||
privateNotificationManager, contractWindow, tradeDetailsWindow, accountAgeWitnessService,
|
||||
useDevPrivilegeKeys);
|
||||
mediatorManager, refundAgentManager, daoFacade, useDevPrivilegeKeys);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -25,11 +25,14 @@ import bisq.desktop.main.support.dispute.client.DisputeClientView;
|
|||
|
||||
import bisq.core.account.witness.AccountAgeWitnessService;
|
||||
import bisq.core.alert.PrivateNotificationManager;
|
||||
import bisq.core.dao.DaoFacade;
|
||||
import bisq.core.support.SupportType;
|
||||
import bisq.core.support.dispute.Dispute;
|
||||
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.RefundSession;
|
||||
import bisq.core.support.dispute.refund.refundagent.RefundAgentManager;
|
||||
import bisq.core.trade.TradeManager;
|
||||
import bisq.core.util.FormattingUtils;
|
||||
import bisq.core.util.coin.CoinFormatter;
|
||||
|
@ -37,9 +40,8 @@ import bisq.core.util.coin.CoinFormatter;
|
|||
import bisq.common.config.Config;
|
||||
import bisq.common.crypto.KeyRing;
|
||||
|
||||
import javax.inject.Named;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Named;
|
||||
|
||||
@FxmlView
|
||||
public class RefundClientView extends DisputeClientView {
|
||||
|
@ -53,10 +55,13 @@ public class RefundClientView extends DisputeClientView {
|
|||
ContractWindow contractWindow,
|
||||
TradeDetailsWindow tradeDetailsWindow,
|
||||
AccountAgeWitnessService accountAgeWitnessService,
|
||||
MediatorManager mediatorManager,
|
||||
RefundAgentManager refundAgentManager,
|
||||
DaoFacade daoFacade,
|
||||
@Named(Config.USE_DEV_PRIVILEGE_KEYS) boolean useDevPrivilegeKeys) {
|
||||
super(refundManager, keyRing, tradeManager, formatter, disputeSummaryWindow,
|
||||
privateNotificationManager, contractWindow, tradeDetailsWindow, accountAgeWitnessService,
|
||||
useDevPrivilegeKeys);
|
||||
mediatorManager, refundAgentManager, daoFacade, useDevPrivilegeKeys);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -795,6 +795,7 @@ message Dispute {
|
|||
SupportType support_type = 24;
|
||||
string mediators_dispute_result = 25;
|
||||
string delayed_payout_tx_id = 26;
|
||||
string donation_address_of_delayed_payout_tx = 27;
|
||||
}
|
||||
|
||||
message Attachment {
|
||||
|
|
Loading…
Add table
Reference in a new issue