mirror of
https://github.com/haveno-dex/haveno.git
synced 2024-12-22 19:49:32 +00:00
migrate to DisputeValidation
Co-authored-by: HenrikJannsen <boilingfrog@gmx.com>
This commit is contained in:
parent
6f16a5ee92
commit
190003b5ba
6 changed files with 369 additions and 190 deletions
|
@ -104,7 +104,7 @@ public final class Dispute implements NetworkPayload, PersistablePayload {
|
|||
private final String depositTxId;
|
||||
@Nullable
|
||||
private final String payoutTxId;
|
||||
private final String contractAsJson;
|
||||
private String contractAsJson;
|
||||
@Nullable
|
||||
private final String makerContractSignature;
|
||||
@Nullable
|
||||
|
@ -351,6 +351,39 @@ public final class Dispute implements NetworkPayload, PersistablePayload {
|
|||
return !chatMessages.isEmpty() && chatMessages.get(0).getSupportType() == SupportType.MEDIATION;
|
||||
}
|
||||
|
||||
public boolean removeAllChatMessages() {
|
||||
if (chatMessages.size() > 1) {
|
||||
// removes all chat except the initial guidelines message.
|
||||
String firstMessageUid = chatMessages.get(0).getUid();
|
||||
chatMessages.removeIf((msg) -> !msg.getUid().equals(firstMessageUid));
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public void maybeClearSensitiveData() {
|
||||
String change = "";
|
||||
if (contract.maybeClearSensitiveData()) {
|
||||
change += "contract;";
|
||||
}
|
||||
String edited = Contract.sanitizeContractAsJson(contractAsJson);
|
||||
if (!edited.equals(contractAsJson)) {
|
||||
contractAsJson = edited;
|
||||
change += "contractAsJson;";
|
||||
}
|
||||
if (removeAllChatMessages()) {
|
||||
change += "chat messages;";
|
||||
}
|
||||
if (change.length() > 0) {
|
||||
log.info("cleared sensitive data from {} of dispute for trade {}", change, Utilities.getShortId(getTradeId()));
|
||||
}
|
||||
}
|
||||
|
||||
// sanitizes a contract json string
|
||||
public static String sanitizeContractAsJson(String contractAsJson) {
|
||||
return contractAsJson;
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
// Setters
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
|
|
@ -40,7 +40,6 @@ import bisq.core.trade.ClosedTradableManager;
|
|||
import bisq.core.trade.Contract;
|
||||
import bisq.core.trade.HavenoUtils;
|
||||
import bisq.core.trade.Trade;
|
||||
import bisq.core.trade.TradeDataValidation;
|
||||
import bisq.core.trade.TradeManager;
|
||||
import bisq.core.trade.protocol.TradePeer;
|
||||
import bisq.network.p2p.BootstrapListener;
|
||||
|
@ -67,6 +66,7 @@ import javafx.collections.ObservableList;
|
|||
|
||||
import java.math.BigInteger;
|
||||
import java.security.KeyPair;
|
||||
import java.time.Instant;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
@ -101,7 +101,7 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
|
|||
private final PriceFeedService priceFeedService;
|
||||
|
||||
@Getter
|
||||
protected final ObservableList<TradeDataValidation.ValidationException> validationExceptions =
|
||||
protected final ObservableList<DisputeValidation.ValidationException> validationExceptions =
|
||||
FXCollections.observableArrayList();
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
@ -272,21 +272,14 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
|
|||
List<Dispute> disputes = getDisputeList().getList();
|
||||
disputes.forEach(dispute -> {
|
||||
try {
|
||||
TradeDataValidation.validateDonationAddress(dispute, dispute.getDonationAddressOfDelayedPayoutTx());
|
||||
TradeDataValidation.validateNodeAddress(dispute, dispute.getContract().getBuyerNodeAddress(), config);
|
||||
TradeDataValidation.validateNodeAddress(dispute, dispute.getContract().getSellerNodeAddress(), config);
|
||||
} catch (TradeDataValidation.AddressException | TradeDataValidation.NodeAddressException e) {
|
||||
DisputeValidation.validateNodeAddresses(dispute, config);
|
||||
} catch (DisputeValidation.ValidationException e) {
|
||||
log.error(e.toString());
|
||||
validationExceptions.add(e);
|
||||
}
|
||||
});
|
||||
|
||||
// TODO (woodser): disabled for xmr, needed?
|
||||
// TradeDataValidation.testIfAnyDisputeTriedReplay(disputes,
|
||||
// disputeReplayException -> {
|
||||
// log.error(disputeReplayException.toString());
|
||||
// validationExceptions.add(disputeReplayException);
|
||||
// });
|
||||
maybeClearSensitiveData();
|
||||
}
|
||||
|
||||
public boolean isTrader(Dispute dispute) {
|
||||
|
@ -304,6 +297,16 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
|
|||
}
|
||||
}
|
||||
|
||||
public void maybeClearSensitiveData() {
|
||||
log.info("{} checking closed disputes eligibility for having sensitive data cleared", super.getClass().getSimpleName());
|
||||
Instant safeDate = closedTradableManager.getSafeDateForSensitiveDataClearing();
|
||||
getDisputeList().getList().stream()
|
||||
.filter(e -> e.isClosed())
|
||||
.filter(e -> e.getOpeningDate().toInstant().isBefore(safeDate))
|
||||
.forEach(Dispute::maybeClearSensitiveData);
|
||||
requestPersistence();
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
// Dispute handling
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
@ -457,14 +460,12 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
|
|||
|
||||
// validate dispute
|
||||
try {
|
||||
TradeDataValidation.validatePaymentAccountPayload(dispute);
|
||||
TradeDataValidation.validateDonationAddress(dispute.getDonationAddressOfDelayedPayoutTx());
|
||||
//TradeDataValidation.testIfDisputeTriesReplay(dispute, disputeList.getList()); // TODO (woodser): disabled for xmr, needed?
|
||||
TradeDataValidation.validateNodeAddress(dispute, contract.getBuyerNodeAddress(), config);
|
||||
TradeDataValidation.validateNodeAddress(dispute, contract.getSellerNodeAddress(), config);
|
||||
} catch (TradeDataValidation.AddressException |
|
||||
TradeDataValidation.NodeAddressException |
|
||||
TradeDataValidation.InvalidPaymentAccountPayloadException e) {
|
||||
DisputeValidation.validateDisputeData(dispute);
|
||||
DisputeValidation.validateNodeAddresses(dispute, config);
|
||||
DisputeValidation.validateSenderNodeAddress(dispute, message.getSenderNodeAddress());
|
||||
DisputeValidation.validatePaymentAccountPayload(dispute);
|
||||
//DisputeValidation.testIfDisputeTriesReplay(dispute, disputeList.getList());
|
||||
} catch (DisputeValidation.ValidationException e) {
|
||||
validationExceptions.add(e);
|
||||
throw e;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,299 @@
|
|||
/*
|
||||
* This file is part of Haveno.
|
||||
*
|
||||
* Haveno 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.
|
||||
*
|
||||
* Haveno 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 Haveno. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package bisq.core.support.dispute;
|
||||
|
||||
import bisq.core.support.SupportType;
|
||||
import bisq.core.trade.Contract;
|
||||
import bisq.core.trade.Trade;
|
||||
import bisq.core.util.JsonUtil;
|
||||
import bisq.core.util.validation.RegexValidatorFactory;
|
||||
|
||||
import bisq.network.p2p.NodeAddress;
|
||||
|
||||
import bisq.common.config.Config;
|
||||
import bisq.common.crypto.CryptoException;
|
||||
import bisq.common.crypto.Hash;
|
||||
import bisq.common.crypto.Sig;
|
||||
import bisq.common.util.Tuple3;
|
||||
|
||||
import org.bitcoinj.core.Address;
|
||||
import org.bitcoinj.core.NetworkParameters;
|
||||
import org.bitcoinj.core.Transaction;
|
||||
import org.bitcoinj.core.TransactionOutput;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import static com.google.common.base.Preconditions.checkArgument;
|
||||
import static com.google.common.base.Preconditions.checkNotNull;
|
||||
|
||||
@Slf4j
|
||||
public class DisputeValidation {
|
||||
|
||||
public static void validatePaymentAccountPayload(Dispute dispute) throws ValidationException {
|
||||
if (dispute.getSellerPaymentAccountPayload() == null) throw new ValidationException(dispute, "Seller's payment account payload is null in dispute opened for trade " + dispute.getTradeId());
|
||||
if (!Arrays.equals(dispute.getSellerPaymentAccountPayload().getHash(), dispute.getContract().getSellerPaymentAccountPayloadHash())) throw new ValidationException(dispute, "Hash of maker's payment account payload does not match contract");
|
||||
}
|
||||
|
||||
public static void validateDisputeData(Dispute dispute) throws ValidationException {
|
||||
try {
|
||||
Contract contract = dispute.getContract();
|
||||
checkArgument(contract.getOfferPayload().getId().equals(dispute.getTradeId()), "Invalid tradeId");
|
||||
checkArgument(dispute.getContractAsJson().equals(JsonUtil.objectToJson(contract)), "Invalid contractAsJson");
|
||||
checkArgument(Arrays.equals(Objects.requireNonNull(dispute.getContractHash()), Hash.getSha256Hash(checkNotNull(dispute.getContractAsJson()))),
|
||||
"Invalid contractHash");
|
||||
|
||||
try {
|
||||
// Only the dispute opener has set the signature
|
||||
String makerContractSignature = dispute.getMakerContractSignature();
|
||||
if (makerContractSignature != null) {
|
||||
Sig.verify(contract.getMakerPubKeyRing().getSignaturePubKey(),
|
||||
dispute.getContractAsJson(),
|
||||
makerContractSignature);
|
||||
}
|
||||
String takerContractSignature = dispute.getTakerContractSignature();
|
||||
if (takerContractSignature != null) {
|
||||
Sig.verify(contract.getTakerPubKeyRing().getSignaturePubKey(),
|
||||
dispute.getContractAsJson(),
|
||||
takerContractSignature);
|
||||
}
|
||||
} catch (CryptoException e) {
|
||||
throw new ValidationException(dispute, e.getMessage());
|
||||
}
|
||||
} catch (Throwable t) {
|
||||
throw new ValidationException(dispute, t.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public static void validateTradeAndDispute(Dispute dispute, Trade trade)
|
||||
throws ValidationException {
|
||||
try {
|
||||
checkArgument(dispute.getContract().equals(trade.getContract()),
|
||||
"contract must match contract from trade");
|
||||
|
||||
} catch (Throwable t) {
|
||||
throw new ValidationException(dispute, t.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public static void validateSenderNodeAddress(Dispute dispute,
|
||||
NodeAddress senderNodeAddress) throws NodeAddressException {
|
||||
if (!senderNodeAddress.equals(dispute.getContract().getBuyerNodeAddress())
|
||||
&& !senderNodeAddress.equals(dispute.getContract().getSellerNodeAddress())
|
||||
&& !senderNodeAddress.equals(dispute.getContract().getArbitratorNodeAddress())) {
|
||||
throw new NodeAddressException(dispute, "senderNodeAddress not matching any of the traders node addresses");
|
||||
}
|
||||
}
|
||||
|
||||
public static void validateNodeAddresses(Dispute dispute, Config config)
|
||||
throws NodeAddressException {
|
||||
if (!config.useLocalhostForP2P) {
|
||||
validateNodeAddress(dispute, dispute.getContract().getBuyerNodeAddress());
|
||||
validateNodeAddress(dispute, dispute.getContract().getSellerNodeAddress());
|
||||
}
|
||||
}
|
||||
|
||||
private static void validateNodeAddress(Dispute dispute, NodeAddress nodeAddress) throws NodeAddressException {
|
||||
if (!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(Dispute dispute,
|
||||
Transaction delayedPayoutTx,
|
||||
NetworkParameters params)
|
||||
throws AddressException {
|
||||
TransactionOutput output = delayedPayoutTx.getOutput(0);
|
||||
Address address = output.getScriptPubKey().getToAddress(params);
|
||||
if (address == null) {
|
||||
String errorMsg = "Donation address cannot be resolved (not of type P2PK nor P2SH nor P2WH). Output: " + output;
|
||||
log.error(errorMsg);
|
||||
log.error(delayedPayoutTx.toString());
|
||||
throw new DisputeValidation.AddressException(dispute, errorMsg);
|
||||
}
|
||||
|
||||
// Verify that address in the dispute matches the one in the trade.
|
||||
String delayedPayoutTxOutputAddress = address.toString();
|
||||
checkArgument(delayedPayoutTxOutputAddress.equals(dispute.getDonationAddressOfDelayedPayoutTx()),
|
||||
"donationAddressOfDelayedPayoutTx from dispute does not match address from delayed payout tx. " +
|
||||
"delayedPayoutTxOutputAddress=" + delayedPayoutTxOutputAddress +
|
||||
"; dispute.getDonationAddressOfDelayedPayoutTx()=" + dispute.getDonationAddressOfDelayedPayoutTx());
|
||||
}
|
||||
|
||||
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 = 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();
|
||||
if (delayedPayoutTxId != null) {
|
||||
disputesPerDelayedPayoutTxId.putIfAbsent(delayedPayoutTxId, new HashSet<>());
|
||||
set = disputesPerDelayedPayoutTxId.get(delayedPayoutTxId);
|
||||
set.add(uid);
|
||||
}
|
||||
|
||||
String depositTxId = dispute.getDepositTxId();
|
||||
if (depositTxId != null) {
|
||||
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();
|
||||
|
||||
// For pre v1.4.0 we do not get the delayed payout tx sent in mediation cases but in refund agent case we do.
|
||||
// So until all users have updated to 1.4.0 we only check in refund agent case. With 1.4.0 we send the
|
||||
// delayed payout tx also in mediation cases and that if check can be removed.
|
||||
if (disputeToTest.getSupportType() == SupportType.REFUND) {
|
||||
checkNotNull(disputeToTestDelayedPayoutTxId,
|
||||
"Delayed payout transaction ID is null. " +
|
||||
"Trade ID: " + disputeToTestTradeId);
|
||||
}
|
||||
checkNotNull(disputeToTestDepositTxId,
|
||||
"depositTxId must not be null. Trade ID: " + disputeToTestTradeId);
|
||||
checkNotNull(disputeToTestUid,
|
||||
"agentsUid must not be null. Trade ID: " + disputeToTestTradeId);
|
||||
|
||||
Set<String> disputesPerTradeIdItems = disputesPerTradeId.get(disputeToTestTradeId);
|
||||
checkArgument(disputesPerTradeIdItems != null && disputesPerTradeIdItems.size() <= 2,
|
||||
"We found more then 2 disputes with the same trade ID. " +
|
||||
"Trade ID: " + disputeToTestTradeId);
|
||||
if (!disputesPerDelayedPayoutTxId.isEmpty()) {
|
||||
Set<String> disputesPerDelayedPayoutTxIdItems = disputesPerDelayedPayoutTxId.get(disputeToTestDelayedPayoutTxId);
|
||||
checkArgument(disputesPerDelayedPayoutTxIdItems != null && disputesPerDelayedPayoutTxIdItems.size() <= 2,
|
||||
"We found more then 2 disputes with the same delayedPayoutTxId. " +
|
||||
"Trade ID: " + disputeToTestTradeId);
|
||||
}
|
||||
if (!disputesPerDepositTxId.isEmpty()) {
|
||||
Set<String> disputesPerDepositTxIdItems = disputesPerDepositTxId.get(disputeToTestDepositTxId);
|
||||
checkArgument(disputesPerDepositTxIdItems != null && disputesPerDepositTxIdItems.size() <= 2,
|
||||
"We found more then 2 disputes with the same depositTxId. " +
|
||||
"Trade ID: " + disputeToTestTradeId);
|
||||
}
|
||||
} catch (IllegalArgumentException e) {
|
||||
throw new DisputeReplayException(disputeToTest, e.getMessage());
|
||||
} catch (NullPointerException e) {
|
||||
log.error("NullPointerException at testIfDisputeTriesReplay: " +
|
||||
"disputeToTest={}, disputesPerTradeId={}, disputesPerDelayedPayoutTxId={}, " +
|
||||
"disputesPerDepositTxId={}",
|
||||
disputeToTest, disputesPerTradeId, disputesPerDelayedPayoutTxId, disputesPerDepositTxId);
|
||||
throw new DisputeReplayException(disputeToTest, e.toString() + " at dispute " + disputeToTest.toString());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
// Exceptions
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
public static class ValidationException extends Exception {
|
||||
@Getter
|
||||
private final Dispute dispute;
|
||||
|
||||
ValidationException(Dispute dispute, String msg) {
|
||||
super(msg);
|
||||
this.dispute = dispute;
|
||||
}
|
||||
}
|
||||
|
||||
public static class NodeAddressException extends ValidationException {
|
||||
NodeAddressException(Dispute dispute, String msg) {
|
||||
super(dispute, msg);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public static class AddressException extends ValidationException {
|
||||
AddressException(Dispute dispute, String msg) {
|
||||
super(dispute, msg);
|
||||
}
|
||||
}
|
||||
|
||||
public static class DisputeReplayException extends ValidationException {
|
||||
DisputeReplayException(Dispute dispute, String msg) {
|
||||
super(dispute, msg);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -257,6 +257,15 @@ public final class Contract implements NetworkPayload {
|
|||
return isBuyerMakerAndSellerTaker() == isMyRoleBuyer(myPubKeyRing);
|
||||
}
|
||||
|
||||
public boolean maybeClearSensitiveData() {
|
||||
return false; // TODO: anything to clear?
|
||||
}
|
||||
|
||||
// edits a contract json string
|
||||
public static String sanitizeContractAsJson(String contractAsJson) {
|
||||
return contractAsJson; // TODO: anything to sanitize?
|
||||
}
|
||||
|
||||
public void printDiff(@Nullable String peersContractAsJson) {
|
||||
String json = JsonUtil.objectToJson(this);
|
||||
String diff = StringUtils.difference(json, peersContractAsJson);
|
||||
|
|
|
@ -19,14 +19,7 @@ package bisq.core.trade;
|
|||
|
||||
import bisq.core.btc.wallet.BtcWalletService;
|
||||
import bisq.core.offer.Offer;
|
||||
import bisq.core.support.SupportType;
|
||||
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;
|
||||
|
@ -35,12 +28,6 @@ import org.bitcoinj.core.Transaction;
|
|||
import org.bitcoinj.core.TransactionInput;
|
||||
import org.bitcoinj.core.TransactionOutPoint;
|
||||
import org.bitcoinj.core.TransactionOutput;
|
||||
import java.util.Arrays;
|
||||
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;
|
||||
|
@ -54,153 +41,6 @@ import static com.google.common.base.Preconditions.checkNotNull;
|
|||
@Slf4j
|
||||
public class TradeDataValidation {
|
||||
|
||||
public static void validatePaymentAccountPayload(Dispute dispute) throws InvalidPaymentAccountPayloadException {
|
||||
if (dispute.getSellerPaymentAccountPayload() == null) throw new InvalidPaymentAccountPayloadException(dispute, "Seller's payment account payload is null in dispute opened for trade " + dispute.getTradeId());
|
||||
if (!Arrays.equals(dispute.getSellerPaymentAccountPayload().getHash(), dispute.getContract().getSellerPaymentAccountPayloadHash())) throw new InvalidPaymentAccountPayloadException(dispute, "Hash of maker's payment account payload does not match contract");
|
||||
}
|
||||
|
||||
public static void validateDonationAddress(String addressAsString)
|
||||
throws AddressException {
|
||||
validateDonationAddress(null, addressAsString);
|
||||
}
|
||||
|
||||
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)
|
||||
throws AddressException {
|
||||
|
||||
if (addressAsString == null) {
|
||||
log.debug("address is null at validateDonationAddress. This is expected in case of an not updated trader.");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
if (delayedPayoutTxId != null) {
|
||||
disputesPerDelayedPayoutTxId.putIfAbsent(delayedPayoutTxId, new HashSet<>());
|
||||
set = disputesPerDelayedPayoutTxId.get(delayedPayoutTxId);
|
||||
set.add(uid);
|
||||
}
|
||||
|
||||
String depositTxId = dispute.getDepositTxId();
|
||||
if (depositTxId != null) {
|
||||
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();
|
||||
|
||||
// For pre v1.4.0 we do not get the delayed payout tx sent in mediation cases but in refund agent case we do.
|
||||
// So until all users have updated to 1.4.0 we only check in refund agent case. With 1.4.0 we send the
|
||||
// delayed payout tx also in mediation cases and that if check can be removed.
|
||||
if (disputeToTest.getSupportType() == SupportType.REFUND) {
|
||||
checkNotNull(disputeToTestDelayedPayoutTxId,
|
||||
"Delayed payout transaction ID is null. " +
|
||||
"Trade ID: " + disputeToTestTradeId);
|
||||
}
|
||||
checkNotNull(disputeToTestDepositTxId,
|
||||
"depositTxId must not be null. Trade ID: " + disputeToTestTradeId);
|
||||
checkNotNull(disputeToTestUid,
|
||||
"agentsUid must not be null. Trade ID: " + disputeToTestTradeId);
|
||||
|
||||
Set<String> disputesPerTradeIdItems = disputesPerTradeId.get(disputeToTestTradeId);
|
||||
checkArgument(disputesPerTradeIdItems != null && disputesPerTradeIdItems.size() <= 2,
|
||||
"We found more then 2 disputes with the same trade ID. " +
|
||||
"Trade ID: " + disputeToTestTradeId);
|
||||
if (!disputesPerDelayedPayoutTxId.isEmpty()) {
|
||||
Set<String> disputesPerDelayedPayoutTxIdItems = disputesPerDelayedPayoutTxId.get(disputeToTestDelayedPayoutTxId);
|
||||
checkArgument(disputesPerDelayedPayoutTxIdItems != null && disputesPerDelayedPayoutTxIdItems.size() <= 2,
|
||||
"We found more then 2 disputes with the same delayedPayoutTxId. " +
|
||||
"Trade ID: " + disputeToTestTradeId);
|
||||
}
|
||||
if (!disputesPerDepositTxId.isEmpty()) {
|
||||
Set<String> disputesPerDepositTxIdItems = disputesPerDepositTxId.get(disputeToTestDepositTxId);
|
||||
checkArgument(disputesPerDepositTxIdItems != null && disputesPerDepositTxIdItems.size() <= 2,
|
||||
"We found more then 2 disputes with the same depositTxId. " +
|
||||
"Trade ID: " + disputeToTestTradeId);
|
||||
}
|
||||
} catch (IllegalArgumentException e) {
|
||||
throw new DisputeReplayException(disputeToTest, e.getMessage());
|
||||
} catch (NullPointerException e) {
|
||||
log.error("NullPointerException at testIfDisputeTriesReplay: " +
|
||||
"disputeToTest={}, disputesPerTradeId={}, disputesPerDelayedPayoutTxId={}, " +
|
||||
"disputesPerDepositTxId={}",
|
||||
disputeToTest, disputesPerTradeId, disputesPerDelayedPayoutTxId, disputesPerDepositTxId);
|
||||
throw new DisputeReplayException(disputeToTest, e.toString() + " at dispute " + disputeToTest.toString());
|
||||
}
|
||||
}
|
||||
|
||||
public static void validateDelayedPayoutTx(Trade trade,
|
||||
Transaction delayedPayoutTx,
|
||||
BtcWalletService btcWalletService)
|
||||
|
@ -315,8 +155,6 @@ public class TradeDataValidation {
|
|||
addressConsumer.accept(addressAsString);
|
||||
}
|
||||
|
||||
validateDonationAddress(addressAsString);
|
||||
|
||||
if (dispute != null) {
|
||||
// Verify that address in the dispute matches the one in the trade.
|
||||
String donationAddressOfDelayedPayoutTx = dispute.getDonationAddressOfDelayedPayoutTx();
|
||||
|
|
|
@ -30,9 +30,9 @@ 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.DisputeValidation;
|
||||
import bisq.core.support.dispute.agent.MultipleHolderNameDetection;
|
||||
import bisq.core.support.dispute.arbitration.arbitrator.ArbitratorManager;
|
||||
import bisq.core.trade.TradeDataValidation;
|
||||
import bisq.core.trade.TradeManager;
|
||||
import bisq.core.user.DontShowAgainLookup;
|
||||
import bisq.core.user.Preferences;
|
||||
|
@ -60,13 +60,12 @@ import java.util.List;
|
|||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
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;
|
||||
private ListChangeListener<DisputeValidation.ValidationException> validationExceptionListener;
|
||||
|
||||
public DisputeAgentView(DisputeManager<? extends DisputeList<Dispute>> disputeManager,
|
||||
KeyRing keyRing,
|
||||
|
@ -126,7 +125,7 @@ public abstract class DisputeAgentView extends DisputeView implements MultipleHo
|
|||
};
|
||||
}
|
||||
|
||||
protected void showWarningForValidationExceptions(List<? extends ValidationException> exceptions) {
|
||||
protected void showWarningForValidationExceptions(List<? extends DisputeValidation.ValidationException> exceptions) {
|
||||
exceptions.stream()
|
||||
.filter(ex -> ex.getDispute() != null)
|
||||
.filter(ex -> !ex.getDispute().isClosed()) // we show warnings only for open cases
|
||||
|
@ -134,7 +133,7 @@ public abstract class DisputeAgentView extends DisputeView implements MultipleHo
|
|||
.forEach(ex -> new Popup().width(900).warning(getValidationExceptionMessage(ex)).dontShowAgainId(getKey(ex)).show());
|
||||
}
|
||||
|
||||
private String getKey(ValidationException exception) {
|
||||
private String getKey(DisputeValidation.ValidationException exception) {
|
||||
Dispute dispute = exception.getDispute();
|
||||
if (dispute != null) {
|
||||
return "ValExcPopup-" + dispute.getTradeId() + "-" + dispute.getTraderId();
|
||||
|
@ -142,9 +141,9 @@ public abstract class DisputeAgentView extends DisputeView implements MultipleHo
|
|||
return "ValExcPopup-" + exception.toString();
|
||||
}
|
||||
|
||||
private String getValidationExceptionMessage(ValidationException exception) {
|
||||
private String getValidationExceptionMessage(DisputeValidation.ValidationException exception) {
|
||||
Dispute dispute = exception.getDispute();
|
||||
if (dispute != null && exception instanceof TradeDataValidation.AddressException) {
|
||||
if (dispute != null && exception instanceof DisputeValidation.AddressException) {
|
||||
return getAddressExceptionMessage(dispute);
|
||||
} else if (exception.getMessage() != null && !exception.getMessage().isEmpty()) {
|
||||
return exception.getMessage();
|
||||
|
|
Loading…
Reference in a new issue