rename all packages and other names from bisq to haveno

This commit is contained in:
woodser 2023-03-06 19:14:00 -05:00
parent ab0b9e3b77
commit 1a1fb130c0
1775 changed files with 14575 additions and 16767 deletions

View file

@ -0,0 +1,411 @@
/*
* 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 haveno.core.support;
import haveno.common.Timer;
import haveno.common.UserThread;
import haveno.common.crypto.PubKeyRing;
import haveno.common.proto.network.NetworkEnvelope;
import haveno.core.api.CoreMoneroConnectionsService;
import haveno.core.api.CoreNotificationService;
import haveno.core.locale.Res;
import haveno.core.support.dispute.Dispute;
import haveno.core.support.messages.ChatMessage;
import haveno.core.support.messages.SupportMessage;
import haveno.core.trade.Trade;
import haveno.core.trade.TradeManager;
import haveno.core.trade.protocol.TradeProtocol;
import haveno.core.trade.protocol.TradeProtocol.MailboxMessageComparator;
import haveno.network.p2p.AckMessage;
import haveno.network.p2p.AckMessageSourceType;
import haveno.network.p2p.DecryptedMessageWithPubKey;
import haveno.network.p2p.NodeAddress;
import haveno.network.p2p.P2PService;
import haveno.network.p2p.SendMailboxMessageListener;
import haveno.network.p2p.mailbox.MailboxMessage;
import haveno.network.p2p.mailbox.MailboxMessageService;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CopyOnWriteArraySet;
import lombok.extern.slf4j.Slf4j;
import javax.annotation.Nullable;
@Slf4j
public abstract class SupportManager {
protected final P2PService p2PService;
protected final TradeManager tradeManager;
protected final CoreMoneroConnectionsService connectionService;
protected final CoreNotificationService notificationService;
protected final Map<String, Timer> delayMsgMap = new HashMap<>();
private final Object lock = new Object();
private final CopyOnWriteArraySet<DecryptedMessageWithPubKey> decryptedMailboxMessageWithPubKeys = new CopyOnWriteArraySet<>();
private final CopyOnWriteArraySet<DecryptedMessageWithPubKey> decryptedDirectMessageWithPubKeys = new CopyOnWriteArraySet<>();
protected final MailboxMessageService mailboxMessageService;
private boolean allServicesInitialized;
///////////////////////////////////////////////////////////////////////////////////////////
// Constructor
///////////////////////////////////////////////////////////////////////////////////////////
public SupportManager(P2PService p2PService,
CoreMoneroConnectionsService connectionService,
CoreNotificationService notificationService,
TradeManager tradeManager) {
this.p2PService = p2PService;
this.connectionService = connectionService;
this.mailboxMessageService = p2PService.getMailboxMessageService();
this.notificationService = notificationService;
this.tradeManager = tradeManager;
// We get first the message handler called then the onBootstrapped
p2PService.addDecryptedDirectMessageListener((decryptedMessageWithPubKey, senderAddress) -> {
if (isReady()) applyDirectMessage(decryptedMessageWithPubKey);
else {
synchronized (lock) {
// As decryptedDirectMessageWithPubKeys is a CopyOnWriteArraySet we do not need to check if it was already stored
decryptedDirectMessageWithPubKeys.add(decryptedMessageWithPubKey);
tryApplyMessages();
}
}
});
mailboxMessageService.addDecryptedMailboxListener((decryptedMessageWithPubKey, senderAddress) -> {
if (isReady()) applyMailboxMessage(decryptedMessageWithPubKey);
else {
synchronized (lock) {
// As decryptedMailboxMessageWithPubKeys is a CopyOnWriteArraySet we do not need to check if it was already stored
decryptedDirectMessageWithPubKeys.add(decryptedMessageWithPubKey);
tryApplyMessages();
}
}
});
}
///////////////////////////////////////////////////////////////////////////////////////////
// Abstract methods
///////////////////////////////////////////////////////////////////////////////////////////
protected abstract void onSupportMessage(SupportMessage networkEnvelope);
public abstract NodeAddress getPeerNodeAddress(ChatMessage message);
public abstract PubKeyRing getPeerPubKeyRing(ChatMessage message);
public abstract SupportType getSupportType();
public abstract boolean channelOpen(ChatMessage message);
public abstract List<ChatMessage> getAllChatMessages();
public abstract void addAndPersistChatMessage(ChatMessage message);
public abstract void requestPersistence();
///////////////////////////////////////////////////////////////////////////////////////////
// Delegates p2pService
///////////////////////////////////////////////////////////////////////////////////////////
public boolean isBootstrapped() {
return p2PService.isBootstrapped();
}
public NodeAddress getMyAddress() {
return p2PService.getAddress();
}
///////////////////////////////////////////////////////////////////////////////////////////
// API
///////////////////////////////////////////////////////////////////////////////////////////
public void onAllServicesInitialized() {
allServicesInitialized = true;
}
public void tryApplyMessages() {
if (isReady())
applyMessages();
}
///////////////////////////////////////////////////////////////////////////////////////////
// Message handler
///////////////////////////////////////////////////////////////////////////////////////////
protected void handleChatMessage(ChatMessage chatMessage) {
final String tradeId = chatMessage.getTradeId();
final String uid = chatMessage.getUid();
log.info("Received {} from peer {}. tradeId={}, uid={}", chatMessage.getClass().getSimpleName(), chatMessage.getSenderNodeAddress(), tradeId, uid);
boolean channelOpen = channelOpen(chatMessage);
if (!channelOpen) {
log.debug("We got a chatMessage but we don't have a matching chat. TradeId = " + tradeId);
if (!delayMsgMap.containsKey(uid)) {
Timer timer = UserThread.runAfter(() -> handleChatMessage(chatMessage), 1);
delayMsgMap.put(uid, timer);
} else {
String msg = "We got a chatMessage after we already repeated to apply the message after a delay. That should never happen. TradeId = " + tradeId;
log.warn(msg);
}
return;
}
cleanupRetryMap(uid);
PubKeyRing receiverPubKeyRing = getPeerPubKeyRing(chatMessage);
addAndPersistChatMessage(chatMessage);
notificationService.sendChatNotification(chatMessage);
// We never get a errorMessage in that method (only if we cannot resolve the receiverPubKeyRing but then we
// cannot send it anyway)
if (receiverPubKeyRing != null)
sendAckMessage(chatMessage, receiverPubKeyRing, true, null);
}
private void onAckMessage(AckMessage ackMessage) {
if (ackMessage.getSourceType() == getAckMessageSourceType()) {
if (ackMessage.isSuccess()) {
log.info("Received AckMessage for {} with tradeId {} and uid {}",
ackMessage.getSourceMsgClassName(), ackMessage.getSourceId(), ackMessage.getSourceUid());
// dispute is opened by ack on chat message
if (ackMessage.getSourceMsgClassName().equals(ChatMessage.class.getSimpleName())) {
Trade trade = tradeManager.getTrade(ackMessage.getSourceId());
for (Dispute dispute : trade.getDisputes()) {
for (ChatMessage chatMessage : dispute.getChatMessages()) {
if (chatMessage.getUid().equals(ackMessage.getSourceUid())) {
trade.advanceDisputeState(Trade.DisputeState.DISPUTE_OPENED);
}
}
}
}
} else {
log.warn("Received AckMessage with error state for {} with tradeId {} and errorMessage={}",
ackMessage.getSourceMsgClassName(), ackMessage.getSourceId(), ackMessage.getErrorMessage());
}
getAllChatMessages().stream()
.filter(msg -> msg.getUid().equals(ackMessage.getSourceUid()))
.forEach(msg -> {
if (ackMessage.isSuccess())
msg.setAcknowledged(true);
else
msg.setAckError(ackMessage.getErrorMessage());
});
requestPersistence();
}
}
protected abstract AckMessageSourceType getAckMessageSourceType();
///////////////////////////////////////////////////////////////////////////////////////////
// Send message
///////////////////////////////////////////////////////////////////////////////////////////
public ChatMessage sendChatMessage(ChatMessage message) {
NodeAddress peersNodeAddress = getPeerNodeAddress(message);
PubKeyRing receiverPubKeyRing = getPeerPubKeyRing(message);
if (peersNodeAddress == null || receiverPubKeyRing == null) {
UserThread.runAfter(() ->
message.setSendMessageError(Res.get("support.receiverNotKnown")), 1);
} else {
log.info("Send {} to peer {}. tradeId={}, uid={}",
message.getClass().getSimpleName(), peersNodeAddress, message.getTradeId(), message.getUid());
mailboxMessageService.sendEncryptedMailboxMessage(peersNodeAddress,
receiverPubKeyRing,
message,
new SendMailboxMessageListener() {
@Override
public void onArrived() {
log.info("{} arrived at peer {}. tradeId={}, uid={}",
message.getClass().getSimpleName(), peersNodeAddress, message.getTradeId(), message.getUid());
message.setArrived(true);
requestPersistence();
}
@Override
public void onStoredInMailbox() {
log.info("{} stored in mailbox for peer {}. tradeId={}, uid={}",
message.getClass().getSimpleName(), peersNodeAddress, message.getTradeId(), message.getUid());
message.setStoredInMailbox(true);
requestPersistence();
}
@Override
public void onFault(String errorMessage) {
log.error("{} failed: Peer {}. tradeId={}, uid={}, errorMessage={}",
message.getClass().getSimpleName(), peersNodeAddress, message.getTradeId(), message.getUid(), errorMessage);
message.setSendMessageError(errorMessage);
requestPersistence();
}
}
);
}
return message;
}
protected void sendAckMessage(SupportMessage supportMessage, PubKeyRing peersPubKeyRing,
boolean result, @Nullable String errorMessage) {
String tradeId = supportMessage.getTradeId();
String uid = supportMessage.getUid();
AckMessage ackMessage = new AckMessage(p2PService.getNetworkNode().getNodeAddress(),
getAckMessageSourceType(),
supportMessage.getClass().getSimpleName(),
uid,
tradeId,
result,
errorMessage);
final NodeAddress peersNodeAddress = supportMessage.getSenderNodeAddress();
log.info("Send AckMessage for {} to peer {}. tradeId={}, uid={}",
ackMessage.getSourceMsgClassName(), peersNodeAddress, tradeId, uid);
mailboxMessageService.sendEncryptedMailboxMessage(
peersNodeAddress,
peersPubKeyRing,
ackMessage,
new SendMailboxMessageListener() {
@Override
public void onArrived() {
log.info("AckMessage for {} arrived at peer {}. tradeId={}, uid={}",
ackMessage.getSourceMsgClassName(), peersNodeAddress, tradeId, uid);
}
@Override
public void onStoredInMailbox() {
log.info("AckMessage for {} stored in mailbox for peer {}. tradeId={}, uid={}",
ackMessage.getSourceMsgClassName(), peersNodeAddress, tradeId, uid);
}
@Override
public void onFault(String errorMessage) {
log.error("AckMessage for {} failed. Peer {}. tradeId={}, uid={}, errorMessage={}",
ackMessage.getSourceMsgClassName(), peersNodeAddress, tradeId, uid, errorMessage);
}
}
);
}
///////////////////////////////////////////////////////////////////////////////////////////
// Protected
///////////////////////////////////////////////////////////////////////////////////////////
protected boolean canProcessMessage(SupportMessage message) {
return message.getSupportType() == getSupportType();
}
protected void cleanupRetryMap(String uid) {
if (delayMsgMap.containsKey(uid)) {
Timer timer = delayMsgMap.remove(uid);
if (timer != null)
timer.stop();
}
}
///////////////////////////////////////////////////////////////////////////////////////////
// Private
///////////////////////////////////////////////////////////////////////////////////////////
private boolean isReady() {
return allServicesInitialized &&
p2PService.isBootstrapped() &&
connectionService.isDownloadComplete() &&
connectionService.hasSufficientPeersForBroadcast();
}
///////////////////////////////////////////////////////////////////////////////////////////
// Private
///////////////////////////////////////////////////////////////////////////////////////////
private void applyMessages() {
synchronized (lock) {
// apply non-mailbox messages
decryptedDirectMessageWithPubKeys.stream()
.filter(e -> !(e.getNetworkEnvelope() instanceof MailboxMessage))
.forEach(decryptedMessageWithPubKey -> applyDirectMessage(decryptedMessageWithPubKey));
decryptedMailboxMessageWithPubKeys.stream()
.filter(e -> !(e.getNetworkEnvelope() instanceof MailboxMessage))
.forEach(decryptedMessageWithPubKey -> applyMailboxMessage(decryptedMessageWithPubKey));
// apply mailbox messages in order
decryptedDirectMessageWithPubKeys.stream()
.filter(e -> (e.getNetworkEnvelope() instanceof MailboxMessage))
.sorted(new DecryptedMessageWithPubKeyComparator())
.forEach(decryptedMessageWithPubKey -> applyDirectMessage(decryptedMessageWithPubKey));
decryptedMailboxMessageWithPubKeys.stream()
.filter(e -> (e.getNetworkEnvelope() instanceof MailboxMessage))
.sorted(new DecryptedMessageWithPubKeyComparator())
.forEach(decryptedMessageWithPubKey -> applyMailboxMessage(decryptedMessageWithPubKey));
// clear messages
decryptedDirectMessageWithPubKeys.clear();
decryptedMailboxMessageWithPubKeys.clear();
}
}
private void applyDirectMessage(DecryptedMessageWithPubKey decryptedMessageWithPubKey) {
NetworkEnvelope networkEnvelope = decryptedMessageWithPubKey.getNetworkEnvelope();
if (networkEnvelope instanceof SupportMessage) {
onSupportMessage((SupportMessage) networkEnvelope);
} else if (networkEnvelope instanceof AckMessage) {
onAckMessage((AckMessage) networkEnvelope);
}
}
private void applyMailboxMessage(DecryptedMessageWithPubKey decryptedMessageWithPubKey) {
NetworkEnvelope networkEnvelope = decryptedMessageWithPubKey.getNetworkEnvelope();
log.trace("## decryptedMessageWithPubKey message={}", networkEnvelope.getClass().getSimpleName());
if (networkEnvelope instanceof SupportMessage) {
SupportMessage supportMessage = (SupportMessage) networkEnvelope;
onSupportMessage(supportMessage);
mailboxMessageService.removeMailboxMsg(supportMessage);
} else if (networkEnvelope instanceof AckMessage) {
AckMessage ackMessage = (AckMessage) networkEnvelope;
onAckMessage(ackMessage);
mailboxMessageService.removeMailboxMsg(ackMessage);
}
}
private static class DecryptedMessageWithPubKeyComparator implements Comparator<DecryptedMessageWithPubKey> {
MailboxMessageComparator mailboxMessageComparator;
public DecryptedMessageWithPubKeyComparator() {
mailboxMessageComparator = new TradeProtocol.MailboxMessageComparator();
}
@Override
public int compare(DecryptedMessageWithPubKey m1, DecryptedMessageWithPubKey m2) {
if (m1.getNetworkEnvelope() instanceof MailboxMessage) {
if (m2.getNetworkEnvelope() instanceof MailboxMessage) return mailboxMessageComparator.compare((MailboxMessage) m1.getNetworkEnvelope(), (MailboxMessage) m2.getNetworkEnvelope());
else return 1;
} else {
return m2.getNetworkEnvelope() instanceof MailboxMessage ? -1 : 0;
}
}
}
}

View file

@ -0,0 +1,54 @@
/*
* 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 haveno.core.support;
import haveno.common.crypto.PubKeyRing;
import haveno.core.support.messages.ChatMessage;
import javafx.collections.ObservableList;
/**
* A Support session is using a trade or a dispute to implement the methods.
* It keeps the ChatView transparent if used in dispute or trade chat context.
*/
public abstract class SupportSession {
// todo refactor ui so that can be converted to isTrader
private boolean isClient;
protected SupportSession(boolean isClient) {
this.isClient = isClient;
}
protected SupportSession() {
}
// todo refactor ui so that can be converted to isTrader
public boolean isClient() {
return isClient;
}
public abstract String getTradeId();
public abstract int getClientId();
public abstract ObservableList<ChatMessage> getObservableChatMessageList();
public abstract boolean chatIsOpen();
public abstract boolean isDisputeAgent();
}

View file

@ -0,0 +1,36 @@
/*
* 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 haveno.core.support;
import haveno.common.proto.ProtoUtil;
public enum SupportType {
ARBITRATION, // Need to be at index 0 to be the fallback for old clients
MEDIATION,
TRADE,
REFUND;
public static SupportType fromProto(
protobuf.SupportType type) {
return ProtoUtil.enumFromProto(SupportType.class, type.name());
}
public static protobuf.SupportType toProtoMessage(SupportType supportType) {
return protobuf.SupportType.valueOf(supportType.name());
}
}

View file

@ -0,0 +1,45 @@
/*
* 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 haveno.core.support.dispute;
import com.google.protobuf.ByteString;
import haveno.common.proto.network.NetworkPayload;
import lombok.Value;
@Value
public final class Attachment implements NetworkPayload {
private final String fileName;
private final byte[] bytes;
public Attachment(String fileName, byte[] bytes) {
this.fileName = fileName;
this.bytes = bytes;
}
@Override
public protobuf.Attachment toProtoMessage() {
return protobuf.Attachment.newBuilder()
.setFileName(fileName)
.setBytes(ByteString.copyFrom(bytes))
.build();
}
public static Attachment fromProto(protobuf.Attachment proto) {
return new Attachment(proto.getFileName(), proto.getBytes().toByteArray());
}
}

View file

@ -0,0 +1,544 @@
/*
* 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 haveno.core.support.dispute;
import com.google.protobuf.ByteString;
import haveno.common.UserThread;
import haveno.common.crypto.PubKeyRing;
import haveno.common.proto.ProtoUtil;
import haveno.common.proto.network.NetworkPayload;
import haveno.common.proto.persistable.PersistablePayload;
import haveno.common.util.CollectionUtils;
import haveno.common.util.ExtraDataMapValidator;
import haveno.common.util.Utilities;
import haveno.core.locale.Res;
import haveno.core.payment.payload.PaymentAccountPayload;
import haveno.core.proto.CoreProtoResolver;
import haveno.core.support.SupportType;
import haveno.core.support.messages.ChatMessage;
import haveno.core.trade.Contract;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.ReadOnlyBooleanProperty;
import javafx.beans.property.ReadOnlyIntegerProperty;
import javafx.beans.property.ReadOnlyObjectProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.UUID;
import java.util.stream.Collectors;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import javax.annotation.Nullable;
@Slf4j
@EqualsAndHashCode
@Getter
public final class Dispute implements NetworkPayload, PersistablePayload {
public enum State {
NEEDS_UPGRADE,
NEW,
OPEN,
REOPENED,
CLOSED;
public static Dispute.State fromProto(protobuf.Dispute.State state) {
return ProtoUtil.enumFromProto(Dispute.State.class, state.name());
}
public static protobuf.Dispute.State toProtoMessage(Dispute.State state) {
return protobuf.Dispute.State.valueOf(state.name());
}
}
private final String tradeId;
private final String id;
private final int traderId;
private final boolean disputeOpenerIsBuyer;
private final boolean disputeOpenerIsMaker;
// PubKeyRing of trader who opened the dispute
private final PubKeyRing traderPubKeyRing;
private final long tradeDate;
private final long tradePeriodEnd;
private final Contract contract;
@Nullable
private final byte[] contractHash;
@Nullable
private final byte[] depositTxSerialized;
@Nullable
private final byte[] payoutTxSerialized;
@Nullable
private final String depositTxId;
@Nullable
private final String payoutTxId;
private String contractAsJson;
@Nullable
private final String makerContractSignature;
@Nullable
private final String takerContractSignature;
private final PubKeyRing agentPubKeyRing; // dispute agent
private final boolean isSupportTicket;
private final ObservableList<ChatMessage> chatMessages = FXCollections.observableArrayList();
// disputeResultProperty.get is Nullable!
private final ObjectProperty<DisputeResult> disputeResultProperty = new SimpleObjectProperty<>();
private final long openingDate;
@Nullable
@Setter
private String disputePayoutTxId;
@Setter
// Added v1.2.0
private SupportType supportType;
// Only used at refundAgent so that he knows how the mediator resolved the case
@Setter
@Nullable
private String mediatorsDisputeResult;
@Setter
@Nullable
private String delayedPayoutTxId;
// Added at v1.4.0
@Setter
@Nullable
private String donationAddressOfDelayedPayoutTx;
// Added at v1.6.0
private Dispute.State disputeState = State.NEW;
// Should be only used in emergency case if we need to add data but do not want to break backward compatibility
// at the P2P network storage checks. The hash of the object will be used to verify if the data is valid. Any new
// field in a class would break that hash and therefore break the storage mechanism.
@Nullable
@Setter
private Map<String, String> extraDataMap;
// Added for XMR integration
private boolean isOpener;
@Nullable
private PaymentAccountPayload makerPaymentAccountPayload;
@Nullable
private PaymentAccountPayload takerPaymentAccountPayload;
// We do not persist uid, it is only used by dispute agents to guarantee an uid.
@Setter
@Nullable
private transient String uid;
@Setter
private transient long payoutTxConfirms = -1;
private transient final BooleanProperty isClosedProperty = new SimpleBooleanProperty();
private transient final IntegerProperty badgeCountProperty = new SimpleIntegerProperty();
///////////////////////////////////////////////////////////////////////////////////////////
// Constructor
///////////////////////////////////////////////////////////////////////////////////////////
public Dispute(long openingDate,
String tradeId,
int traderId,
boolean isOpener,
boolean disputeOpenerIsBuyer,
boolean disputeOpenerIsMaker,
PubKeyRing traderPubKeyRing,
long tradeDate,
long tradePeriodEnd,
Contract contract,
@Nullable byte[] contractHash,
@Nullable byte[] depositTxSerialized,
@Nullable byte[] payoutTxSerialized,
@Nullable String depositTxId,
@Nullable String payoutTxId,
String contractAsJson,
@Nullable String makerContractSignature,
@Nullable String takerContractSignature,
@Nullable PaymentAccountPayload makerPaymentAccountPayload,
@Nullable PaymentAccountPayload takerPaymentAccountPayload,
PubKeyRing agentPubKeyRing,
boolean isSupportTicket,
SupportType supportType) {
this.openingDate = openingDate;
this.tradeId = tradeId;
this.traderId = traderId;
this.isOpener = isOpener;
this.disputeOpenerIsBuyer = disputeOpenerIsBuyer;
this.disputeOpenerIsMaker = disputeOpenerIsMaker;
this.traderPubKeyRing = traderPubKeyRing;
this.tradeDate = tradeDate;
this.tradePeriodEnd = tradePeriodEnd;
this.contract = contract;
this.contractHash = contractHash;
this.depositTxSerialized = depositTxSerialized;
this.payoutTxSerialized = payoutTxSerialized;
this.depositTxId = depositTxId;
this.payoutTxId = payoutTxId;
this.contractAsJson = contractAsJson;
this.makerContractSignature = makerContractSignature;
this.takerContractSignature = takerContractSignature;
this.makerPaymentAccountPayload = makerPaymentAccountPayload;
this.takerPaymentAccountPayload = takerPaymentAccountPayload;
this.agentPubKeyRing = agentPubKeyRing;
this.isSupportTicket = isSupportTicket;
this.supportType = supportType;
id = tradeId + "_" + traderId;
uid = UUID.randomUUID().toString();
refreshAlertLevel(true);
}
///////////////////////////////////////////////////////////////////////////////////////////
// PROTO BUFFER
///////////////////////////////////////////////////////////////////////////////////////////
@Override
public protobuf.Dispute toProtoMessage() {
// Needed to avoid ConcurrentModificationException
List<ChatMessage> clonedChatMessages = new ArrayList<>(chatMessages);
protobuf.Dispute.Builder builder = protobuf.Dispute.newBuilder()
.setTradeId(tradeId)
.setTraderId(traderId)
.setIsOpener(isOpener)
.setDisputeOpenerIsBuyer(disputeOpenerIsBuyer)
.setDisputeOpenerIsMaker(disputeOpenerIsMaker)
.setTraderPubKeyRing(traderPubKeyRing.toProtoMessage())
.setTradeDate(tradeDate)
.setTradePeriodEnd(tradePeriodEnd)
.setContract(contract.toProtoMessage())
.setContractAsJson(contractAsJson)
.setAgentPubKeyRing(agentPubKeyRing.toProtoMessage())
.setIsSupportTicket(isSupportTicket)
.addAllChatMessage(clonedChatMessages.stream()
.map(msg -> msg.toProtoNetworkEnvelope().getChatMessage())
.collect(Collectors.toList()))
.setIsClosed(this.isClosed())
.setOpeningDate(openingDate)
.setState(Dispute.State.toProtoMessage(disputeState))
.setId(id);
Optional.ofNullable(contractHash).ifPresent(e -> builder.setContractHash(ByteString.copyFrom(e)));
Optional.ofNullable(depositTxSerialized).ifPresent(e -> builder.setDepositTxSerialized(ByteString.copyFrom(e)));
Optional.ofNullable(payoutTxSerialized).ifPresent(e -> builder.setPayoutTxSerialized(ByteString.copyFrom(e)));
Optional.ofNullable(depositTxId).ifPresent(builder::setDepositTxId);
Optional.ofNullable(payoutTxId).ifPresent(builder::setPayoutTxId);
Optional.ofNullable(disputePayoutTxId).ifPresent(builder::setDisputePayoutTxId);
Optional.ofNullable(makerContractSignature).ifPresent(builder::setMakerContractSignature);
Optional.ofNullable(takerContractSignature).ifPresent(builder::setTakerContractSignature);
Optional.ofNullable(makerPaymentAccountPayload).ifPresent(e -> builder.setMakerPaymentAccountPayload((protobuf.PaymentAccountPayload) makerPaymentAccountPayload.toProtoMessage()));
Optional.ofNullable(takerPaymentAccountPayload).ifPresent(e -> builder.setTakerPaymentAccountPayload((protobuf.PaymentAccountPayload) takerPaymentAccountPayload.toProtoMessage()));
Optional.ofNullable(disputeResultProperty.get()).ifPresent(result -> builder.setDisputeResult(disputeResultProperty.get().toProtoMessage()));
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));
Optional.ofNullable(getExtraDataMap()).ifPresent(builder::putAllExtraData);
return builder.build();
}
public static Dispute fromProto(protobuf.Dispute proto, CoreProtoResolver coreProtoResolver) {
Dispute dispute = new Dispute(proto.getOpeningDate(),
proto.getTradeId(),
proto.getTraderId(),
proto.getIsOpener(),
proto.getDisputeOpenerIsBuyer(),
proto.getDisputeOpenerIsMaker(),
PubKeyRing.fromProto(proto.getTraderPubKeyRing()),
proto.getTradeDate(),
proto.getTradePeriodEnd(),
Contract.fromProto(proto.getContract(), coreProtoResolver),
ProtoUtil.byteArrayOrNullFromProto(proto.getContractHash()),
ProtoUtil.byteArrayOrNullFromProto(proto.getDepositTxSerialized()),
ProtoUtil.byteArrayOrNullFromProto(proto.getPayoutTxSerialized()),
ProtoUtil.stringOrNullFromProto(proto.getDepositTxId()),
ProtoUtil.stringOrNullFromProto(proto.getPayoutTxId()),
proto.getContractAsJson(),
ProtoUtil.stringOrNullFromProto(proto.getMakerContractSignature()),
ProtoUtil.stringOrNullFromProto(proto.getTakerContractSignature()),
proto.hasMakerPaymentAccountPayload() ? coreProtoResolver.fromProto(proto.getMakerPaymentAccountPayload()) : null,
proto.hasTakerPaymentAccountPayload() ? coreProtoResolver.fromProto(proto.getTakerPaymentAccountPayload()) : null,
PubKeyRing.fromProto(proto.getAgentPubKeyRing()),
proto.getIsSupportTicket(),
SupportType.fromProto(proto.getSupportType()));
dispute.setExtraDataMap(CollectionUtils.isEmpty(proto.getExtraDataMap()) ?
null : ExtraDataMapValidator.getValidatedExtraDataMap(proto.getExtraDataMap()));
dispute.chatMessages.addAll(proto.getChatMessageList().stream()
.map(ChatMessage::fromPayloadProto)
.collect(Collectors.toList()));
if (proto.hasDisputeResult())
dispute.disputeResultProperty.set(DisputeResult.fromProto(proto.getDisputeResult()));
dispute.disputePayoutTxId = ProtoUtil.stringOrNullFromProto(proto.getDisputePayoutTxId());
String mediatorsDisputeResult = proto.getMediatorsDisputeResult();
if (!mediatorsDisputeResult.isEmpty()) {
dispute.setMediatorsDisputeResult(mediatorsDisputeResult);
}
String delayedPayoutTxId = proto.getDelayedPayoutTxId();
if (!delayedPayoutTxId.isEmpty()) {
dispute.setDelayedPayoutTxId(delayedPayoutTxId);
}
String donationAddressOfDelayedPayoutTx = proto.getDonationAddressOfDelayedPayoutTx();
if (!donationAddressOfDelayedPayoutTx.isEmpty()) {
dispute.setDonationAddressOfDelayedPayoutTx(donationAddressOfDelayedPayoutTx);
}
if (Dispute.State.fromProto(proto.getState()) == State.NEEDS_UPGRADE) {
// old disputes did not have a state field, so choose an appropriate state:
dispute.setState(proto.getIsClosed() ? State.CLOSED : State.OPEN);
if (dispute.getDisputeState() == State.CLOSED) {
// mark chat messages as read for pre-existing CLOSED disputes
// otherwise at upgrade, all old disputes would have 1 unread chat message
// because currently when a dispute is closed, the last chat message is not marked read
dispute.getChatMessages().forEach(m -> m.setWasDisplayed(true));
}
} else {
dispute.setState(Dispute.State.fromProto(proto.getState()));
}
dispute.refreshAlertLevel(true);
return dispute;
}
///////////////////////////////////////////////////////////////////////////////////////////
// API
///////////////////////////////////////////////////////////////////////////////////////////
public void addAndPersistChatMessage(ChatMessage chatMessage) {
if (!chatMessages.contains(chatMessage)) {
chatMessages.add(chatMessage);
} else {
log.error("disputeDirectMessage already exists");
}
}
public boolean isMediationDispute() {
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
///////////////////////////////////////////////////////////////////////////////////////////
public void setIsClosed() {
setState(State.CLOSED);
}
public void reOpen() {
setState(State.REOPENED);
}
public void setState(Dispute.State disputeState) {
this.disputeState = disputeState;
UserThread.execute(() -> this.isClosedProperty.set(disputeState == State.CLOSED));
}
public void setDisputeResult(DisputeResult disputeResult) {
disputeResultProperty.set(disputeResult);
}
public void setExtraData(String key, String value) {
if (key == null || value == null) {
return;
}
if (extraDataMap == null) {
extraDataMap = new HashMap<>();
}
extraDataMap.put(key, value);
}
///////////////////////////////////////////////////////////////////////////////////////////
// Getters
///////////////////////////////////////////////////////////////////////////////////////////
public String getShortTradeId() {
return Utilities.getShortId(tradeId);
}
public ReadOnlyBooleanProperty isClosedProperty() {
return isClosedProperty;
}
public ReadOnlyIntegerProperty getBadgeCountProperty() {
return badgeCountProperty;
}
public ReadOnlyObjectProperty<DisputeResult> disputeResultProperty() {
return disputeResultProperty;
}
public Date getTradeDate() {
return new Date(tradeDate);
}
public Date getTradePeriodEnd() {
return new Date(tradePeriodEnd);
}
public Date getOpeningDate() {
return new Date(openingDate);
}
public boolean isNew() {
return this.disputeState == State.NEW;
}
public boolean isClosed() {
return this.disputeState == State.CLOSED;
}
public void refreshAlertLevel(boolean senderFlag) {
// if the dispute is "new" that is 1 alert that has to be propagated upstream
// or if there are unread messages that is 1 alert that has to be propagated upstream
if (isNew() || unreadMessageCount(senderFlag) > 0) {
badgeCountProperty.setValue(1);
} else {
badgeCountProperty.setValue(0);
}
}
public long unreadMessageCount(boolean senderFlag) {
return chatMessages.stream()
.filter(m -> m.isSenderIsTrader() == senderFlag || m.isSystemMessage())
.filter(m -> !m.isWasDisplayed())
.count();
}
public void setDisputeSeen(boolean senderFlag) {
if (this.disputeState == State.NEW)
setState(State.OPEN);
refreshAlertLevel(senderFlag);
}
public void setChatMessagesSeen(boolean senderFlag) {
getChatMessages().forEach(m -> m.setWasDisplayed(true));
refreshAlertLevel(senderFlag);
}
public String getRoleString() {
if (disputeOpenerIsMaker) {
if (disputeOpenerIsBuyer)
return Res.get("support.buyerMaker");
else
return Res.get("support.sellerMaker");
} else {
if (disputeOpenerIsBuyer)
return Res.get("support.buyerTaker");
else
return Res.get("support.sellerTaker");
}
}
@Nullable
public PaymentAccountPayload getBuyerPaymentAccountPayload() {
return contract.isBuyerMakerAndSellerTaker() ? makerPaymentAccountPayload : takerPaymentAccountPayload;
}
@Nullable
public PaymentAccountPayload getSellerPaymentAccountPayload() {
return contract.isBuyerMakerAndSellerTaker() ? takerPaymentAccountPayload : makerPaymentAccountPayload;
}
@Override
public String toString() {
return "Dispute{" +
"\n tradeId='" + tradeId + '\'' +
",\n id='" + id + '\'' +
",\n uid='" + uid + '\'' +
",\n state=" + disputeState +
",\n traderId=" + traderId +
",\n isOpener=" + isOpener +
",\n disputeOpenerIsBuyer=" + disputeOpenerIsBuyer +
",\n disputeOpenerIsMaker=" + disputeOpenerIsMaker +
",\n traderPubKeyRing=" + traderPubKeyRing +
",\n tradeDate=" + tradeDate +
",\n tradePeriodEnd=" + tradePeriodEnd +
",\n contract=" + contract +
",\n contractHash=" + Utilities.bytesAsHexString(contractHash) +
",\n depositTxSerialized=" + Utilities.bytesAsHexString(depositTxSerialized) +
",\n payoutTxSerialized=" + Utilities.bytesAsHexString(payoutTxSerialized) +
",\n depositTxId='" + depositTxId + '\'' +
",\n payoutTxId='" + payoutTxId + '\'' +
",\n contractAsJson='" + contractAsJson + '\'' +
",\n makerContractSignature='" + makerContractSignature + '\'' +
",\n takerContractSignature='" + takerContractSignature + '\'' +
",\n agentPubKeyRing=" + agentPubKeyRing +
",\n isSupportTicket=" + isSupportTicket +
",\n chatMessages=" + chatMessages +
",\n isClosedProperty=" + isClosedProperty +
",\n disputeResultProperty=" + disputeResultProperty +
",\n disputePayoutTxId='" + disputePayoutTxId + '\'' +
",\n openingDate=" + openingDate +
",\n supportType=" + supportType +
",\n mediatorsDisputeResult='" + mediatorsDisputeResult + '\'' +
",\n delayedPayoutTxId='" + delayedPayoutTxId + '\'' +
",\n donationAddressOfDelayedPayoutTx='" + donationAddressOfDelayedPayoutTx + '\'' +
",\n makerPaymentAccountPayload='" + makerPaymentAccountPayload + '\'' +
",\n takerPaymentAccountPayload='" + takerPaymentAccountPayload + '\'' +
"\n}";
}
}

View file

@ -0,0 +1,24 @@
/*
* 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 haveno.core.support.dispute;
public class DisputeAlreadyOpenException extends Exception {
public DisputeAlreadyOpenException() {
super();
}
}

View file

@ -0,0 +1,43 @@
/*
* 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 haveno.core.support.dispute;
import haveno.common.proto.persistable.PersistableListAsObservable;
import haveno.common.proto.persistable.PersistablePayload;
import java.util.Collection;
import lombok.ToString;
import lombok.extern.slf4j.Slf4j;
@Slf4j
@ToString
/*
* Holds a List of Dispute objects.
*
* Calls to the List are delegated because this class intercepts the add/remove calls so changes
* can be saved to disc.
*/
public abstract class DisputeList<T extends PersistablePayload> extends PersistableListAsObservable<T> {
public DisputeList() {
}
protected DisputeList(Collection<T> collection) {
super(collection);
}
}

View file

@ -0,0 +1,171 @@
/*
* 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 haveno.core.support.dispute;
import haveno.common.UserThread;
import haveno.common.persistence.PersistenceManager;
import haveno.common.proto.persistable.PersistedDataHost;
import haveno.core.trade.Contract;
import haveno.network.p2p.NodeAddress;
import org.fxmisc.easybind.EasyBind;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.collections.ObservableList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.function.Consumer;
import java.util.stream.Collectors;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import javax.annotation.Nullable;
@Slf4j
public abstract class DisputeListService<T extends DisputeList<Dispute>> implements PersistedDataHost {
@Getter
protected final PersistenceManager<T> persistenceManager;
@Getter
private final T disputeList;
@Getter
private final IntegerProperty numOpenDisputes = new SimpleIntegerProperty();
@Getter
private final Set<String> disputedTradeIds = new HashSet<>();
///////////////////////////////////////////////////////////////////////////////////////////
// Constructor
///////////////////////////////////////////////////////////////////////////////////////////
public DisputeListService(PersistenceManager<T> persistenceManager) {
this.persistenceManager = persistenceManager;
disputeList = getConcreteDisputeList();
this.persistenceManager.initialize(disputeList, getFileName(), PersistenceManager.Source.PRIVATE);
}
///////////////////////////////////////////////////////////////////////////////////////////
// Abstract methods
///////////////////////////////////////////////////////////////////////////////////////////
protected abstract T getConcreteDisputeList();
///////////////////////////////////////////////////////////////////////////////////////////
// PersistedDataHost
///////////////////////////////////////////////////////////////////////////////////////////
@Override
public void readPersisted(Runnable completeHandler) {
persistenceManager.readPersisted(getFileName(), persisted -> {
disputeList.setAll(persisted.getList());
completeHandler.run();
},
completeHandler);
}
protected String getFileName() {
return disputeList.getDefaultStorageFileName();
}
///////////////////////////////////////////////////////////////////////////////////////////
// Public
///////////////////////////////////////////////////////////////////////////////////////////
public void cleanupDisputes(@Nullable Consumer<String> closedDisputeHandler) {
disputeList.stream().forEach(dispute -> {
String tradeId = dispute.getTradeId();
if (dispute.isClosed() && closedDisputeHandler != null) {
closedDisputeHandler.accept(tradeId);
}
});
}
///////////////////////////////////////////////////////////////////////////////////////////
// Package scope
///////////////////////////////////////////////////////////////////////////////////////////
void onAllServicesInitialized() {
disputeList.addListener(change -> {
change.next();
onDisputesChangeListener(change.getAddedSubList(), change.getRemoved());
});
onDisputesChangeListener(disputeList.getList(), null);
}
String getNrOfDisputes(boolean isBuyer, Contract contract) {
return String.valueOf(getObservableList().stream()
.filter(e -> {
Contract contract1 = e.getContract();
if (contract1 == null)
return false;
if (isBuyer) {
NodeAddress buyerNodeAddress = contract1.getBuyerNodeAddress();
return buyerNodeAddress != null && buyerNodeAddress.equals(contract.getBuyerNodeAddress());
} else {
NodeAddress sellerNodeAddress = contract1.getSellerNodeAddress();
return sellerNodeAddress != null && sellerNodeAddress.equals(contract.getSellerNodeAddress());
}
})
.collect(Collectors.toSet()).size());
}
ObservableList<Dispute> getObservableList() {
return disputeList.getObservableList();
}
///////////////////////////////////////////////////////////////////////////////////////////
// Private
///////////////////////////////////////////////////////////////////////////////////////////
private void onDisputesChangeListener(List<? extends Dispute> addedList,
@Nullable List<? extends Dispute> removedList) {
if (removedList != null) {
removedList.forEach(dispute -> {
disputedTradeIds.remove(dispute.getTradeId());
});
}
addedList.forEach(dispute -> {
// for each dispute added, keep track of its "BadgeCountProperty"
EasyBind.subscribe(dispute.getBadgeCountProperty(),
isAlerting -> {
// We get the event before the list gets updated, so we execute on next frame
UserThread.execute(() -> {
int numAlerts = (int) disputeList.getList().stream()
.mapToLong(x -> x.getBadgeCountProperty().getValue())
.sum();
numOpenDisputes.set(numAlerts);
});
});
disputedTradeIds.add(dispute.getTradeId());
});
}
public void requestPersistence() {
persistenceManager.requestPersistence();
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,24 @@
/*
* 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 haveno.core.support.dispute;
public class DisputeMessageDeliveryFailedException extends Exception {
public DisputeMessageDeliveryFailedException() {
super();
}
}

View file

@ -0,0 +1,248 @@
/*
* 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 haveno.core.support.dispute;
import com.google.protobuf.ByteString;
import haveno.common.proto.ProtoUtil;
import haveno.common.proto.network.NetworkPayload;
import haveno.common.util.Utilities;
import haveno.core.support.messages.ChatMessage;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import java.math.BigInteger;
import java.util.Date;
import java.util.Optional;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import javax.annotation.Nullable;
@EqualsAndHashCode
@Getter
@Slf4j
public final class DisputeResult implements NetworkPayload {
public enum Winner {
BUYER,
SELLER
}
public enum Reason {
OTHER,
BUG,
USABILITY,
SCAM, // Not used anymore
PROTOCOL_VIOLATION, // Not used anymore
NO_REPLY, // Not used anymore
BANK_PROBLEMS,
OPTION_TRADE,
SELLER_NOT_RESPONDING,
WRONG_SENDER_ACCOUNT,
TRADE_ALREADY_SETTLED,
PEER_WAS_LATE
}
private final String tradeId;
private final int traderId;
@Setter
@Nullable
private Winner winner;
private int reasonOrdinal = Reason.OTHER.ordinal();
private final BooleanProperty tamperProofEvidenceProperty = new SimpleBooleanProperty();
private final BooleanProperty idVerificationProperty = new SimpleBooleanProperty();
private final BooleanProperty screenCastProperty = new SimpleBooleanProperty();
private final StringProperty summaryNotesProperty = new SimpleStringProperty("");
@Setter
@Nullable
private ChatMessage chatMessage;
@Setter
@Nullable
private byte[] arbitratorSignature;
private long buyerPayoutAmount;
private long sellerPayoutAmount;
@Setter
@Nullable
private byte[] arbitratorPubKey;
private long closeDate;
public DisputeResult(String tradeId, int traderId) {
this.tradeId = tradeId;
this.traderId = traderId;
}
public DisputeResult(String tradeId,
int traderId,
@Nullable Winner winner,
int reasonOrdinal,
boolean tamperProofEvidence,
boolean idVerification,
boolean screenCast,
String summaryNotes,
@Nullable ChatMessage chatMessage,
@Nullable byte[] arbitratorSignature,
long buyerPayoutAmount,
long sellerPayoutAmount,
@Nullable byte[] arbitratorPubKey,
long closeDate) {
this.tradeId = tradeId;
this.traderId = traderId;
this.winner = winner;
this.reasonOrdinal = reasonOrdinal;
this.tamperProofEvidenceProperty.set(tamperProofEvidence);
this.idVerificationProperty.set(idVerification);
this.screenCastProperty.set(screenCast);
this.summaryNotesProperty.set(summaryNotes);
this.chatMessage = chatMessage;
this.arbitratorSignature = arbitratorSignature;
this.buyerPayoutAmount = buyerPayoutAmount;
this.sellerPayoutAmount = sellerPayoutAmount;
this.arbitratorPubKey = arbitratorPubKey;
this.closeDate = closeDate;
}
///////////////////////////////////////////////////////////////////////////////////////////
// PROTO BUFFER
///////////////////////////////////////////////////////////////////////////////////////////
public static DisputeResult fromProto(protobuf.DisputeResult proto) {
return new DisputeResult(proto.getTradeId(),
proto.getTraderId(),
ProtoUtil.enumFromProto(DisputeResult.Winner.class, proto.getWinner().name()),
proto.getReasonOrdinal(),
proto.getTamperProofEvidence(),
proto.getIdVerification(),
proto.getScreenCast(),
proto.getSummaryNotes(),
proto.getChatMessage() == null ? null : ChatMessage.fromPayloadProto(proto.getChatMessage()),
proto.getArbitratorSignature().toByteArray(),
proto.getBuyerPayoutAmount(),
proto.getSellerPayoutAmount(),
proto.getArbitratorPubKey().toByteArray(),
proto.getCloseDate());
}
@Override
public protobuf.DisputeResult toProtoMessage() {
final protobuf.DisputeResult.Builder builder = protobuf.DisputeResult.newBuilder()
.setTradeId(tradeId)
.setTraderId(traderId)
.setReasonOrdinal(reasonOrdinal)
.setTamperProofEvidence(tamperProofEvidenceProperty.get())
.setIdVerification(idVerificationProperty.get())
.setScreenCast(screenCastProperty.get())
.setSummaryNotes(summaryNotesProperty.get())
.setBuyerPayoutAmount(buyerPayoutAmount)
.setSellerPayoutAmount(sellerPayoutAmount)
.setCloseDate(closeDate);
Optional.ofNullable(arbitratorSignature).ifPresent(arbitratorSignature -> builder.setArbitratorSignature(ByteString.copyFrom(arbitratorSignature)));
Optional.ofNullable(arbitratorPubKey).ifPresent(arbitratorPubKey -> builder.setArbitratorPubKey(ByteString.copyFrom(arbitratorPubKey)));
Optional.ofNullable(winner).ifPresent(result -> builder.setWinner(protobuf.DisputeResult.Winner.valueOf(winner.name())));
Optional.ofNullable(chatMessage).ifPresent(chatMessage ->
builder.setChatMessage(chatMessage.toProtoNetworkEnvelope().getChatMessage()));
return builder.build();
}
///////////////////////////////////////////////////////////////////////////////////////////
// API
///////////////////////////////////////////////////////////////////////////////////////////
public BooleanProperty tamperProofEvidenceProperty() {
return tamperProofEvidenceProperty;
}
public BooleanProperty idVerificationProperty() {
return idVerificationProperty;
}
public BooleanProperty screenCastProperty() {
return screenCastProperty;
}
public void setReason(Reason reason) {
this.reasonOrdinal = reason.ordinal();
}
public Reason getReason() {
if (reasonOrdinal < Reason.values().length)
return Reason.values()[reasonOrdinal];
else
return Reason.OTHER;
}
public void setSummaryNotes(String summaryNotes) {
this.summaryNotesProperty.set(summaryNotes);
}
public StringProperty summaryNotesProperty() {
return summaryNotesProperty;
}
public void setBuyerPayoutAmount(BigInteger buyerPayoutAmount) {
this.buyerPayoutAmount = buyerPayoutAmount.longValueExact();
}
public BigInteger getBuyerPayoutAmount() {
return BigInteger.valueOf(buyerPayoutAmount);
}
public void setSellerPayoutAmount(BigInteger sellerPayoutAmount) {
this.sellerPayoutAmount = sellerPayoutAmount.longValueExact();
}
public BigInteger getSellerPayoutAmount() {
return BigInteger.valueOf(sellerPayoutAmount);
}
public void setCloseDate(Date closeDate) {
this.closeDate = closeDate.getTime();
}
public Date getCloseDate() {
return new Date(closeDate);
}
@Override
public String toString() {
return "DisputeResult{" +
"\n tradeId='" + tradeId + '\'' +
",\n traderId=" + traderId +
",\n winner=" + winner +
",\n reasonOrdinal=" + reasonOrdinal +
",\n tamperProofEvidenceProperty=" + tamperProofEvidenceProperty +
",\n idVerificationProperty=" + idVerificationProperty +
",\n screenCastProperty=" + screenCastProperty +
",\n summaryNotesProperty=" + summaryNotesProperty +
",\n chatMessage=" + chatMessage +
",\n arbitratorSignature=" + Utilities.bytesAsHexString(arbitratorSignature) +
",\n buyerPayoutAmount=" + buyerPayoutAmount +
",\n sellerPayoutAmount=" + sellerPayoutAmount +
",\n arbitratorPubKey=" + Utilities.bytesAsHexString(arbitratorPubKey) +
",\n closeDate=" + closeDate +
"\n}";
}
}

View file

@ -0,0 +1,82 @@
/*
* 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 haveno.core.support.dispute;
import haveno.common.crypto.PubKeyRing;
import haveno.core.support.SupportSession;
import haveno.core.support.messages.ChatMessage;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import lombok.extern.slf4j.Slf4j;
import javax.annotation.Nullable;
@Slf4j
public abstract class DisputeSession extends SupportSession {
@Nullable
private Dispute dispute;
private final boolean isTrader;
public DisputeSession(@Nullable Dispute dispute, boolean isTrader) {
super();
this.dispute = dispute;
this.isTrader = isTrader;
}
///////////////////////////////////////////////////////////////////////////////////////////
// Dependent on selected dispute
///////////////////////////////////////////////////////////////////////////////////////////
@Override
public boolean isClient() {
return isTrader;
}
@Override
public String getTradeId() {
return dispute != null ? dispute.getTradeId() : "";
}
@Override
public int getClientId() {
// Get pubKeyRing of trader. Arbitrator is considered server for the chat session
try {
return dispute.getTraderPubKeyRing().hashCode();
} catch (NullPointerException e) {
log.warn("Unable to get traderPubKeyRing from Dispute - {}", e.toString());
}
return 0;
}
@Override
public ObservableList<ChatMessage> getObservableChatMessageList() {
return dispute != null ? dispute.getChatMessages() : FXCollections.observableArrayList();
}
@Override
public boolean chatIsOpen() {
return dispute != null && !dispute.isClosed();
}
@Override
public boolean isDisputeAgent() {
return !isClient();
}
}

View file

@ -0,0 +1,92 @@
/*
* 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 haveno.core.support.dispute;
import haveno.common.crypto.CryptoException;
import haveno.common.crypto.Hash;
import haveno.common.crypto.Sig;
import haveno.common.util.Utilities;
import haveno.core.locale.Res;
import haveno.core.support.dispute.agent.DisputeAgent;
import haveno.core.support.dispute.arbitration.arbitrator.ArbitratorManager;
import haveno.network.p2p.NodeAddress;
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<Dispute>> 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 void verifySignature(String input,
ArbitratorManager arbitratorMediator) {
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 = arbitratorMediator.getDisputeAgentByNodeAddress(nodeAddress).orElse(null);
checkNotNull(disputeAgent, "Dispute agent is null");
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;
} else {
throw new IllegalArgumentException(Res.get("support.sigCheck.popup.failed"));
}
} catch (CryptoException e) {
throw new IllegalArgumentException(Res.get("support.sigCheck.popup.failed"));
}
} catch (Throwable e) {
e.printStackTrace();
throw new IllegalArgumentException(Res.get("support.sigCheck.popup.invalidFormat"));
}
}
}

View file

@ -0,0 +1,296 @@
/*
* 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 haveno.core.support.dispute;
import haveno.common.config.Config;
import haveno.common.crypto.CryptoException;
import haveno.common.crypto.Hash;
import haveno.common.crypto.Sig;
import haveno.common.util.Tuple3;
import haveno.core.support.SupportType;
import haveno.core.trade.Contract;
import haveno.core.trade.Trade;
import haveno.core.util.JsonUtil;
import haveno.core.util.validation.RegexValidatorFactory;
import haveno.network.p2p.NodeAddress;
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);
}
}
}

View file

@ -0,0 +1,111 @@
/*
* 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 haveno.core.support.dispute.agent;
import haveno.common.crypto.PubKeyRing;
import haveno.common.util.ExtraDataMapValidator;
import haveno.common.util.Utilities;
import haveno.network.p2p.NodeAddress;
import haveno.network.p2p.storage.payload.ExpirablePayload;
import haveno.network.p2p.storage.payload.ProtectedStoragePayload;
import java.security.PublicKey;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import javax.annotation.Nullable;
@EqualsAndHashCode
@Slf4j
@Getter
public abstract class DisputeAgent implements ProtectedStoragePayload, ExpirablePayload {
public static final long TTL = TimeUnit.DAYS.toMillis(10);
protected final NodeAddress nodeAddress;
protected final PubKeyRing pubKeyRing;
protected final List<String> languageCodes;
protected final long registrationDate;
protected final byte[] registrationPubKey;
protected final String registrationSignature;
@Nullable
protected final String emailAddress;
@Nullable
protected final String info;
// Should be only used in emergency case if we need to add data but do not want to break backward compatibility
// at the P2P network storage checks. The hash of the object will be used to verify if the data is valid. Any new
// field in a class would break that hash and therefore break the storage mechanism.
@Nullable
protected Map<String, String> extraDataMap;
public DisputeAgent(NodeAddress nodeAddress,
PubKeyRing pubKeyRing,
List<String> languageCodes,
long registrationDate,
byte[] registrationPubKey,
String registrationSignature,
@Nullable String emailAddress,
@Nullable String info,
@Nullable Map<String, String> extraDataMap) {
this.nodeAddress = nodeAddress;
this.pubKeyRing = pubKeyRing;
this.languageCodes = languageCodes;
this.registrationDate = registrationDate;
this.registrationPubKey = registrationPubKey;
this.registrationSignature = registrationSignature;
this.emailAddress = emailAddress;
this.info = info;
this.extraDataMap = ExtraDataMapValidator.getValidatedExtraDataMap(extraDataMap);
}
///////////////////////////////////////////////////////////////////////////////////////////
// API
///////////////////////////////////////////////////////////////////////////////////////////
@Override
public long getTTL() {
return TTL;
}
@Override
public PublicKey getOwnerPubKey() {
return pubKeyRing.getSignaturePubKey();
}
@Override
public String toString() {
return "DisputeAgent{" +
"\n nodeAddress=" + nodeAddress +
",\n pubKeyRing=" + pubKeyRing +
",\n languageCodes=" + languageCodes +
",\n registrationDate=" + registrationDate +
",\n registrationPubKey=" + Utilities.bytesAsHexString(registrationPubKey) +
",\n registrationSignature='" + registrationSignature + '\'' +
",\n emailAddress='" + emailAddress + '\'' +
",\n info='" + info + '\'' +
",\n extraDataMap=" + extraDataMap +
"\n}";
}
}

View file

@ -0,0 +1,59 @@
/*
* 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 haveno.core.support.dispute.agent;
import lombok.extern.slf4j.Slf4j;
import haveno.core.locale.Res;
import javax.annotation.Nullable;
@Slf4j
public class DisputeAgentLookupMap {
// See also: https://bisq.wiki/Finding_your_mediator
@Nullable
public static String getMatrixUserName(String fullAddress) {
if (fullAddress.matches("localhost(.*)")) {
return fullAddress; // on regtest, agent displays as localhost
}
switch (fullAddress) {
case "7hkpotiyaukuzcfy6faihjaols5r2mkysz7bm3wrhhbpbphzz3zbwyqd.onion:9999":
return "leo816";
case "wizhavenozd7ku25di7p2ztsajioabihlnyp5lq5av66tmu7do2dke2tid.onion:9999":
return "wiz";
case "apbp7ubuyezav4hy.onion:9999":
return "haveno_knight";
case "a56olqlmmpxrn5q34itq5g5tb5d3fg7vxekpbceq7xqvfl3cieocgsyd.onion:9999":
return "huey735";
case "3z5jnirlccgxzoxc6zwkcgwj66bugvqplzf6z2iyd5oxifiaorhnanqd.onion:9999":
return "refundagent2";
case "6c4cim7h7t3bm4bnchbf727qrhdfrfr6lhod25wjtizm2sifpkktvwad.onion:9999":
return "pazza83";
default:
log.warn("No user name for dispute agent with address {} found.", fullAddress);
return Res.get("shared.na");
}
}
public static String getMatrixLinkForAgent(String onion) {
// when a new mediator starts or an onion address changes, mediator name won't be known until
// the table above is updated in the software.
// as a stopgap measure, replace unknown ones with a link to the Haveno team
String agentName = getMatrixUserName(onion).replaceAll(Res.get("shared.na"), "haveno");
return "https://matrix.to/#/@" + agentName + ":matrix.org";
}
}

View file

@ -0,0 +1,338 @@
/*
* 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 haveno.core.support.dispute.agent;
import haveno.common.Timer;
import haveno.common.UserThread;
import haveno.common.app.DevEnv;
import haveno.common.crypto.KeyRing;
import haveno.common.handlers.ErrorMessageHandler;
import haveno.common.handlers.ResultHandler;
import haveno.common.util.Utilities;
import haveno.core.filter.FilterManager;
import haveno.core.user.User;
import haveno.network.p2p.BootstrapListener;
import haveno.network.p2p.NodeAddress;
import haveno.network.p2p.P2PService;
import haveno.network.p2p.storage.HashMapChangedListener;
import haveno.network.p2p.storage.payload.ProtectedStorageEntry;
import org.bitcoinj.core.ECKey;
import org.bitcoinj.core.Utils;
import javafx.collections.FXCollections;
import javafx.collections.ObservableMap;
import java.security.PublicKey;
import java.security.SignatureException;
import java.math.BigInteger;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;
import java.util.stream.Collectors;
import lombok.extern.slf4j.Slf4j;
import javax.annotation.Nullable;
import static org.bitcoinj.core.Utils.HEX;
@Slf4j
public abstract class DisputeAgentManager<T extends DisputeAgent> {
///////////////////////////////////////////////////////////////////////////////////////////
// Static
///////////////////////////////////////////////////////////////////////////////////////////
protected static final long REPUBLISH_MILLIS = DisputeAgent.TTL / 2;
protected static final long RETRY_REPUBLISH_SEC = 5;
protected static final long REPEATED_REPUBLISH_AT_STARTUP_SEC = 60;
protected final List<String> publicKeys;
///////////////////////////////////////////////////////////////////////////////////////////
// Instance fields
///////////////////////////////////////////////////////////////////////////////////////////
protected final KeyRing keyRing;
protected final DisputeAgentService<T> disputeAgentService;
protected final User user;
protected final FilterManager filterManager;
protected final ObservableMap<NodeAddress, T> observableMap = FXCollections.observableHashMap();
protected List<T> persistedAcceptedDisputeAgents;
protected Timer republishTimer, retryRepublishTimer;
///////////////////////////////////////////////////////////////////////////////////////////
// Constructor
///////////////////////////////////////////////////////////////////////////////////////////
public DisputeAgentManager(KeyRing keyRing,
DisputeAgentService<T> disputeAgentService,
User user,
FilterManager filterManager) {
this.keyRing = keyRing;
this.disputeAgentService = disputeAgentService;
this.user = user;
this.filterManager = filterManager;
publicKeys = getPubKeyList();
}
///////////////////////////////////////////////////////////////////////////////////////////
// Abstract methods
///////////////////////////////////////////////////////////////////////////////////////////
protected abstract List<String> getPubKeyList();
protected abstract boolean isExpectedInstance(ProtectedStorageEntry data);
protected abstract void addAcceptedDisputeAgentToUser(T disputeAgent);
protected abstract T getRegisteredDisputeAgentFromUser();
protected abstract void clearAcceptedDisputeAgentsAtUser();
protected abstract List<T> getAcceptedDisputeAgentsFromUser();
protected abstract void removeAcceptedDisputeAgentFromUser(ProtectedStorageEntry data);
protected abstract void setRegisteredDisputeAgentAtUser(T disputeAgent);
///////////////////////////////////////////////////////////////////////////////////////////
// API
///////////////////////////////////////////////////////////////////////////////////////////
public void onAllServicesInitialized() {
disputeAgentService.addHashSetChangedListener(new HashMapChangedListener() {
@Override
public void onAdded(Collection<ProtectedStorageEntry> protectedStorageEntries) {
protectedStorageEntries.forEach(protectedStorageEntry -> {
if (isExpectedInstance(protectedStorageEntry)) {
updateMap();
}
});
}
@Override
public void onRemoved(Collection<ProtectedStorageEntry> protectedStorageEntries) {
protectedStorageEntries.forEach(protectedStorageEntry -> {
if (isExpectedInstance(protectedStorageEntry)) {
updateMap();
removeAcceptedDisputeAgentFromUser(protectedStorageEntry);
}
});
}
});
persistedAcceptedDisputeAgents = new ArrayList<>(getAcceptedDisputeAgentsFromUser());
clearAcceptedDisputeAgentsAtUser();
if (getRegisteredDisputeAgentFromUser() != null) {
P2PService p2PService = disputeAgentService.getP2PService();
if (p2PService.isBootstrapped())
startRepublishDisputeAgent();
else
p2PService.addP2PServiceListener(new BootstrapListener() {
@Override
public void onUpdatedDataReceived() {
startRepublishDisputeAgent();
}
});
}
filterManager.filterProperty().addListener((observable, oldValue, newValue) -> updateMap());
updateMap();
}
public void shutDown() {
stopRepublishTimer();
stopRetryRepublishTimer();
}
protected void startRepublishDisputeAgent() {
if (republishTimer == null) {
republishTimer = UserThread.runPeriodically(this::republish, REPUBLISH_MILLIS, TimeUnit.MILLISECONDS);
UserThread.runAfter(this::republish, REPEATED_REPUBLISH_AT_STARTUP_SEC);
republish();
}
}
public void updateMap() {
Map<NodeAddress, T> map = disputeAgentService.getDisputeAgents();
observableMap.clear();
Map<NodeAddress, T> filtered = map.values().stream()
.filter(e -> {
String pubKeyAsHex = Utils.HEX.encode(e.getRegistrationPubKey());
boolean isInPublicKeyInList = isPublicKeyInList(pubKeyAsHex);
if (!isInPublicKeyInList) {
if (DevEnv.DEV_PRIVILEGE_PUB_KEY.equals(pubKeyAsHex))
log.info("We got the DEV_PRIVILEGE_PUB_KEY in our list of publicKeys. RegistrationPubKey={}, nodeAddress={}",
Utilities.bytesAsHexString(e.getRegistrationPubKey()),
e.getNodeAddress().getFullAddress());
else
log.warn("We got an disputeAgent which is not in our list of publicKeys. RegistrationPubKey={}, nodeAddress={}",
Utilities.bytesAsHexString(e.getRegistrationPubKey()),
e.getNodeAddress().getFullAddress());
}
final boolean isSigValid = verifySignature(e.getPubKeyRing().getSignaturePubKey(),
e.getRegistrationPubKey(),
e.getRegistrationSignature());
if (!isSigValid)
log.warn("Sig check for disputeAgent failed. DisputeAgent={}", e.toString());
return isInPublicKeyInList && isSigValid;
})
.collect(Collectors.toMap(DisputeAgent::getNodeAddress, Function.identity()));
observableMap.putAll(filtered);
observableMap.values().forEach(this::addAcceptedDisputeAgentToUser);
}
public void addDisputeAgent(T disputeAgent,
ResultHandler resultHandler,
ErrorMessageHandler errorMessageHandler) {
setRegisteredDisputeAgentAtUser(disputeAgent);
observableMap.put(disputeAgent.getNodeAddress(), disputeAgent);
disputeAgentService.addDisputeAgent(disputeAgent,
() -> {
log.info("DisputeAgent successfully saved in P2P network");
resultHandler.handleResult();
if (observableMap.size() > 0)
UserThread.runAfter(this::updateMap, 100, TimeUnit.MILLISECONDS);
},
errorMessageHandler);
}
public void removeDisputeAgent(ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) {
T registeredDisputeAgent = getRegisteredDisputeAgentFromUser();
if (registeredDisputeAgent != null) {
setRegisteredDisputeAgentAtUser(null);
observableMap.remove(registeredDisputeAgent.getNodeAddress());
disputeAgentService.removeDisputeAgent(registeredDisputeAgent,
() -> {
log.debug("DisputeAgent successfully removed from P2P network");
resultHandler.handleResult();
},
errorMessageHandler);
} else {
errorMessageHandler.handleErrorMessage("User is not registered dispute agent");
}
}
public ObservableMap<NodeAddress, T> getObservableMap() {
return observableMap;
}
// A protected key is handed over to selected disputeAgents for registration.
// An invited disputeAgent will sign at registration his storageSignaturePubKey with that protected key and attach the signature and pubKey to his data.
// Other users will check the signature with the list of public keys hardcoded in the app.
public String signStorageSignaturePubKey(ECKey key) {
String keyToSignAsHex = Utils.HEX.encode(keyRing.getPubKeyRing().getSignaturePubKey().getEncoded());
return key.signMessage(keyToSignAsHex);
}
@Nullable
public ECKey getRegistrationKey(String privKeyBigIntString) {
try {
return ECKey.fromPrivate(new BigInteger(1, HEX.decode(privKeyBigIntString)));
} catch (Throwable t) {
return null;
}
}
public boolean isPublicKeyInList(String pubKeyAsHex) {
return publicKeys.contains(pubKeyAsHex);
}
public boolean isAgentAvailableForLanguage(String languageCode) {
return observableMap.values().stream().anyMatch(agent ->
agent.getLanguageCodes().stream().anyMatch(lc -> lc.equals(languageCode)));
}
public List<String> getDisputeAgentLanguages(List<NodeAddress> nodeAddresses) {
return observableMap.values().stream()
.filter(disputeAgent -> nodeAddresses.stream().anyMatch(nodeAddress -> nodeAddress.equals(disputeAgent.getNodeAddress())))
.flatMap(disputeAgent -> disputeAgent.getLanguageCodes().stream())
.distinct()
.collect(Collectors.toList());
}
public Optional<T> getDisputeAgentByNodeAddress(NodeAddress nodeAddress) {
return observableMap.containsKey(nodeAddress) ?
Optional.of(observableMap.get(nodeAddress)) :
Optional.empty();
}
///////////////////////////////////////////////////////////////////////////////////////////
// protected
///////////////////////////////////////////////////////////////////////////////////////////
protected void republish() {
T registeredDisputeAgent = getRegisteredDisputeAgentFromUser();
if (registeredDisputeAgent != null) {
addDisputeAgent(registeredDisputeAgent,
this::updateMap,
errorMessage -> {
if (retryRepublishTimer == null)
retryRepublishTimer = UserThread.runPeriodically(() -> {
stopRetryRepublishTimer();
republish();
}, RETRY_REPUBLISH_SEC);
}
);
}
}
protected boolean verifySignature(PublicKey storageSignaturePubKey, byte[] registrationPubKey, String signature) {
String keyToSignAsHex = Utils.HEX.encode(storageSignaturePubKey.getEncoded());
try {
ECKey key = ECKey.fromPublicOnly(registrationPubKey);
key.verifyMessage(keyToSignAsHex, signature);
return true;
} catch (SignatureException e) {
log.warn("verifySignature failed");
return false;
}
}
protected void stopRetryRepublishTimer() {
if (retryRepublishTimer != null) {
retryRepublishTimer.stop();
retryRepublishTimer = null;
}
}
protected void stopRepublishTimer() {
if (republishTimer != null) {
republishTimer.stop();
republishTimer = null;
}
}
}

View file

@ -0,0 +1,119 @@
/*
* 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 haveno.core.support.dispute.agent;
import haveno.common.app.DevEnv;
import haveno.common.config.Config;
import haveno.common.handlers.ErrorMessageHandler;
import haveno.common.handlers.ResultHandler;
import haveno.common.util.Utilities;
import haveno.core.filter.FilterManager;
import haveno.network.p2p.NodeAddress;
import haveno.network.p2p.P2PService;
import haveno.network.p2p.storage.HashMapChangedListener;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import lombok.extern.slf4j.Slf4j;
/**
* Used to store disputeAgents profile and load map of disputeAgents
*/
@Slf4j
public abstract class DisputeAgentService<T extends DisputeAgent> {
protected final P2PService p2PService;
protected final FilterManager filterManager;
///////////////////////////////////////////////////////////////////////////////////////////
// Constructor
///////////////////////////////////////////////////////////////////////////////////////////
public DisputeAgentService(P2PService p2PService, FilterManager filterManager) {
this.p2PService = p2PService;
this.filterManager = filterManager;
}
public void addHashSetChangedListener(HashMapChangedListener hashMapChangedListener) {
p2PService.addHashSetChangedListener(hashMapChangedListener);
}
public void addDisputeAgent(T disputeAgent,
ResultHandler resultHandler,
ErrorMessageHandler errorMessageHandler) {
log.debug("addDisputeAgent disputeAgent.hashCode() " + disputeAgent.hashCode());
if (!Config.baseCurrencyNetwork().isMainnet() ||
!Utilities.encodeToHex(disputeAgent.getRegistrationPubKey()).equals(DevEnv.DEV_PRIVILEGE_PUB_KEY)) {
boolean result = p2PService.addProtectedStorageEntry(disputeAgent);
if (result) {
log.trace("Add disputeAgent to network was successful. DisputeAgent.hashCode() = {}", disputeAgent.hashCode());
resultHandler.handleResult();
} else {
errorMessageHandler.handleErrorMessage("Add disputeAgent failed");
}
} else {
log.error("Attempt to publish dev disputeAgent on mainnet.");
errorMessageHandler.handleErrorMessage("Add disputeAgent failed. Attempt to publish dev disputeAgent on mainnet.");
}
}
public void removeDisputeAgent(T disputeAgent,
ResultHandler resultHandler,
ErrorMessageHandler errorMessageHandler) {
log.debug("removeDisputeAgent disputeAgent.hashCode() " + disputeAgent.hashCode());
if (p2PService.removeData(disputeAgent)) {
log.trace("Remove disputeAgent from network was successful. DisputeAgent.hashCode() = {}", disputeAgent.hashCode());
resultHandler.handleResult();
} else {
errorMessageHandler.handleErrorMessage("Remove disputeAgent failed");
}
}
public P2PService getP2PService() {
return p2PService;
}
public Map<NodeAddress, T> getDisputeAgents() {
final List<String> bannedDisputeAgents;
if (filterManager.getFilter() != null) {
bannedDisputeAgents = getDisputeAgentsFromFilter();
} else {
bannedDisputeAgents = null;
}
if (bannedDisputeAgents != null && !bannedDisputeAgents.isEmpty()) {
log.warn("bannedDisputeAgents=" + bannedDisputeAgents);
}
Set<T> disputeAgentSet = getDisputeAgentSet(bannedDisputeAgents);
Map<NodeAddress, T> map = new HashMap<>();
for (T disputeAgent : disputeAgentSet) {
NodeAddress disputeAgentNodeAddress = disputeAgent.getNodeAddress();
if (map.containsKey(disputeAgentNodeAddress)) log.warn("disputeAgentAddress already exists in disputeAgent map. Seems a disputeAgent object is already registered with the same address.");
map.put(disputeAgentNodeAddress, disputeAgent);
}
return map;
}
protected abstract Set<T> getDisputeAgentSet(List<String> bannedDisputeAgents);
protected abstract List<String> getDisputeAgentsFromFilter();
}

View file

@ -0,0 +1,267 @@
/*
* 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 haveno.core.support.dispute.agent;
import haveno.common.crypto.Hash;
import haveno.common.crypto.PubKeyRing;
import haveno.common.util.Tuple2;
import haveno.common.util.Utilities;
import haveno.core.locale.Res;
import haveno.core.payment.payload.PayloadWithHolderName;
import haveno.core.payment.payload.PaymentAccountPayload;
import haveno.core.support.dispute.Dispute;
import haveno.core.support.dispute.DisputeList;
import haveno.core.support.dispute.DisputeManager;
import haveno.core.support.dispute.DisputeResult;
import haveno.core.user.DontShowAgainLookup;
import javafx.collections.ListChangeListener;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.stream.Collectors;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
/**
* Detects traders who had disputes where they used different account holder names. Only payment methods where a
* real name is required are used for the check.
* Strings are not translated here as it is only visible to dispute agents
*/
@Slf4j
public class MultipleHolderNameDetection {
///////////////////////////////////////////////////////////////////////////////////////////
// Listener
///////////////////////////////////////////////////////////////////////////////////////////
public interface Listener {
void onSuspiciousDisputeDetected();
}
///////////////////////////////////////////////////////////////////////////////////////////
// Static
///////////////////////////////////////////////////////////////////////////////////////////
private static final String ACK_KEY = "Ack-";
private static String getSigPuKeyHashAsHex(PubKeyRing pubKeyRing) {
return Utilities.encodeToHex(Hash.getRipemd160hash(pubKeyRing.getSignaturePubKeyBytes()));
}
private static String getSigPubKeyHashAsHex(Dispute dispute) {
return getSigPuKeyHashAsHex(dispute.getTraderPubKeyRing());
}
private static boolean isBuyer(Dispute dispute) {
String traderSigPubKeyHashAsHex = getSigPubKeyHashAsHex(dispute);
String buyerSigPubKeyHashAsHex = getSigPuKeyHashAsHex(dispute.getContract().getBuyerPubKeyRing());
return buyerSigPubKeyHashAsHex.equals(traderSigPubKeyHashAsHex);
}
private static PayloadWithHolderName getPayloadWithHolderName(Dispute dispute) {
return (PayloadWithHolderName) getPaymentAccountPayload(dispute);
}
public static PaymentAccountPayload getPaymentAccountPayload(Dispute dispute) {
return isBuyer(dispute) ?
dispute.getBuyerPaymentAccountPayload() :
dispute.getSellerPaymentAccountPayload();
}
public static String getAddress(Dispute dispute) {
return isBuyer(dispute) ?
dispute.getContract().getBuyerNodeAddress().getHostName() :
dispute.getContract().getSellerNodeAddress().getHostName();
}
public static String getAckKey(Dispute dispute) {
return ACK_KEY + getSigPubKeyHashAsHex(dispute).substring(0, 4) + "/" + dispute.getShortTradeId();
}
private static String getIsBuyerSubString(boolean isBuyer) {
return "'\n Role: " + (isBuyer ? "'Buyer'" : "'Seller'");
}
///////////////////////////////////////////////////////////////////////////////////////////
// Class fields
///////////////////////////////////////////////////////////////////////////////////////////
private final DisputeManager<? extends DisputeList<Dispute>> disputeManager;
// Key is hex of hash of sig pubKey which we consider a trader identity. We could use onion address as well but
// once we support multiple onion addresses that would not work anymore.
@Getter
private final Map<String, List<Dispute>> suspiciousDisputesByTraderMap = new HashMap<>();
private final List<Listener> listeners = new CopyOnWriteArrayList<>();
///////////////////////////////////////////////////////////////////////////////////////////
// Constructor
///////////////////////////////////////////////////////////////////////////////////////////
public MultipleHolderNameDetection(DisputeManager<? extends DisputeList<Dispute>> disputeManager) {
this.disputeManager = disputeManager;
disputeManager.getDisputesAsObservableList().addListener((ListChangeListener<Dispute>) c -> {
c.next();
if (c.wasAdded()) {
detectMultipleHolderNames();
}
});
}
///////////////////////////////////////////////////////////////////////////////////////////
// API
///////////////////////////////////////////////////////////////////////////////////////////
public void detectMultipleHolderNames() {
String previous = suspiciousDisputesByTraderMap.toString();
getAllDisputesByTraderMap().forEach((key, value) -> {
Set<String> userNames = value.stream()
.map(dispute -> getPayloadWithHolderName(dispute).getHolderName())
.collect(Collectors.toSet());
if (userNames.size() > 1) {
// As we compare previous results we need to make sorting deterministic
value.sort(Comparator.comparing(Dispute::getId));
suspiciousDisputesByTraderMap.put(key, value);
}
});
String updated = suspiciousDisputesByTraderMap.toString();
if (!previous.equals(updated)) {
listeners.forEach(Listener::onSuspiciousDisputeDetected);
}
}
public boolean hasSuspiciousDisputesDetected() {
return !suspiciousDisputesByTraderMap.isEmpty();
}
// Returns all disputes of a trader who used multiple names
public List<Dispute> getDisputesForTrader(Dispute dispute) {
String traderPubKeyHash = getSigPubKeyHashAsHex(dispute);
if (suspiciousDisputesByTraderMap.containsKey(traderPubKeyHash)) {
return suspiciousDisputesByTraderMap.get(traderPubKeyHash);
}
return new ArrayList<>();
}
// Get a report of traders who used multiple names with all their disputes listed
public String getReportForAllDisputes() {
return getReport(suspiciousDisputesByTraderMap.values());
}
// Get a report for a trader who used multiple names with all their disputes listed
public String getReportForDisputeOfTrader(List<Dispute> disputes) {
Collection<List<Dispute>> values = new ArrayList<>();
values.add(disputes);
return getReport(values);
}
public void addListener(Listener listener) {
listeners.add(listener);
}
public void removeListener(Listener listener) {
listeners.remove(listener);
}
///////////////////////////////////////////////////////////////////////////////////////////
// Private
///////////////////////////////////////////////////////////////////////////////////////////
private Map<String, List<Dispute>> getAllDisputesByTraderMap() {
Map<String, List<Dispute>> allDisputesByTraderMap = new HashMap<>();
disputeManager.getDisputesAsObservableList().stream()
.filter(dispute -> {
PaymentAccountPayload paymentAccountPayload = isBuyer(dispute) ?
dispute.getBuyerPaymentAccountPayload() :
dispute.getSellerPaymentAccountPayload();
return paymentAccountPayload instanceof PayloadWithHolderName;
})
.forEach(dispute -> {
String traderPubKeyHash = getSigPubKeyHashAsHex(dispute);
allDisputesByTraderMap.putIfAbsent(traderPubKeyHash, new ArrayList<>());
List<Dispute> disputes = allDisputesByTraderMap.get(traderPubKeyHash);
disputes.add(dispute);
});
return allDisputesByTraderMap;
}
// Get a text report for a trader who used multiple names and list all the his disputes
private String getReport(Collection<List<Dispute>> collectionOfDisputesOfTrader) {
return collectionOfDisputesOfTrader.stream()
.map(disputes -> {
Set<String> addresses = new HashSet<>();
Set<Boolean> isBuyerHashSet = new HashSet<>();
Set<String> names = new HashSet<>();
String disputesReport = disputes.stream()
.map(dispute -> {
addresses.add(getAddress(dispute));
String ackKey = getAckKey(dispute);
String ackSubString = " ";
if (!DontShowAgainLookup.showAgain(ackKey)) {
ackSubString = "[ACK] ";
}
String holderName = getPayloadWithHolderName(dispute).getHolderName();
names.add(holderName);
boolean isBuyer = isBuyer(dispute);
isBuyerHashSet.add(isBuyer);
String isBuyerSubString = getIsBuyerSubString(isBuyer);
DisputeResult disputeResult = dispute.disputeResultProperty().get();
String summaryNotes = disputeResult != null ? disputeResult.getSummaryNotesProperty().get().trim() : "Not closed yet";
return ackSubString +
"Trade ID: '" + dispute.getShortTradeId() +
"'\n Account holder name: '" + holderName +
"'\n Payment method: '" + Res.get(getPaymentAccountPayload(dispute).getPaymentMethodId()) +
isBuyerSubString +
"'\n Summary: '" + summaryNotes;
})
.collect(Collectors.joining("\n"));
String addressSubString = addresses.size() > 1 ?
"used multiple addresses " + addresses + " with" :
"with address " + new ArrayList<>(addresses).get(0) + " used";
String roleSubString = "Trader ";
if (isBuyerHashSet.size() == 1) {
boolean isBuyer = new ArrayList<>(isBuyerHashSet).get(0);
String isBuyerSubString = getIsBuyerSubString(isBuyer);
disputesReport = disputesReport.replace(isBuyerSubString, "");
roleSubString = isBuyer ? "Buyer " : "Seller ";
}
String traderReport = roleSubString + addressSubString + " multiple names: " + names.toString() + "\n" + disputesReport;
return new Tuple2<>(roleSubString, traderReport);
})
.sorted(Comparator.comparing(o -> o.first)) // Buyers first, then seller, then mixed (trader was in seller and buyer role)
.map(e -> e.second)
.collect(Collectors.joining("\n\n"));
}
}

View file

@ -0,0 +1,76 @@
/*
* 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 haveno.core.support.dispute.arbitration;
import com.google.protobuf.Message;
import haveno.common.proto.ProtoUtil;
import haveno.core.proto.CoreProtoResolver;
import haveno.core.support.SupportType;
import haveno.core.support.dispute.Dispute;
import haveno.core.support.dispute.DisputeList;
import java.util.Collection;
import java.util.List;
import java.util.stream.Collectors;
import lombok.ToString;
import lombok.extern.slf4j.Slf4j;
import static com.google.common.base.Preconditions.checkArgument;
@Slf4j
@ToString
/*
* Holds a List of arbitration dispute objects.
*
* Calls to the List are delegated because this class intercepts the add/remove calls so changes
* can be saved to disc.
*/
public final class ArbitrationDisputeList extends DisputeList<Dispute> {
ArbitrationDisputeList() {
super();
}
///////////////////////////////////////////////////////////////////////////////////////////
// PROTO BUFFER
///////////////////////////////////////////////////////////////////////////////////////////
protected ArbitrationDisputeList(Collection<Dispute> collection) {
super(collection);
}
@Override
public Message toProtoMessage() {
forEach(dispute -> checkArgument(dispute.getSupportType().equals(SupportType.ARBITRATION), "Support type has to be ARBITRATION"));
return protobuf.PersistableEnvelope.newBuilder().setArbitrationDisputeList(protobuf.ArbitrationDisputeList.newBuilder()
.addAllDispute(ProtoUtil.collectionToProto(getList(), protobuf.Dispute.class))).build();
}
public static ArbitrationDisputeList fromProto(protobuf.ArbitrationDisputeList proto,
CoreProtoResolver coreProtoResolver) {
List<Dispute> list = proto.getDisputeList().stream()
.map(disputeProto -> Dispute.fromProto(disputeProto, coreProtoResolver))
.filter(e -> e.getSupportType().equals(SupportType.ARBITRATION))
.collect(Collectors.toList());
return new ArbitrationDisputeList(list);
}
}

View file

@ -0,0 +1,51 @@
/*
* 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 haveno.core.support.dispute.arbitration;
import haveno.common.persistence.PersistenceManager;
import haveno.core.support.dispute.DisputeListService;
import javax.inject.Inject;
import javax.inject.Singleton;
@Singleton
public final class ArbitrationDisputeListService extends DisputeListService<ArbitrationDisputeList> {
///////////////////////////////////////////////////////////////////////////////////////////
// Constructor
///////////////////////////////////////////////////////////////////////////////////////////
@Inject
public ArbitrationDisputeListService(PersistenceManager<ArbitrationDisputeList> persistenceManager) {
super(persistenceManager);
}
///////////////////////////////////////////////////////////////////////////////////////////
// Implement template methods
///////////////////////////////////////////////////////////////////////////////////////////
@Override
protected ArbitrationDisputeList getConcreteDisputeList() {
return new ArbitrationDisputeList();
}
@Override
protected String getFileName() {
return "DisputeList";
}
}

View file

@ -0,0 +1,457 @@
/*
* 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 haveno.core.support.dispute.arbitration;
import common.utils.GenUtils;
import haveno.common.Timer;
import haveno.common.UserThread;
import haveno.common.app.Version;
import haveno.common.config.Config;
import haveno.common.crypto.KeyRing;
import haveno.core.api.CoreMoneroConnectionsService;
import haveno.core.api.CoreNotificationService;
import haveno.core.btc.wallet.TradeWalletService;
import haveno.core.btc.wallet.XmrWalletService;
import haveno.core.locale.Res;
import haveno.core.offer.OpenOfferManager;
import haveno.core.provider.price.PriceFeedService;
import haveno.core.support.SupportType;
import haveno.core.support.dispute.Dispute;
import haveno.core.support.dispute.DisputeManager;
import haveno.core.support.dispute.DisputeResult;
import haveno.core.support.dispute.DisputeSummaryVerification;
import haveno.core.support.dispute.DisputeResult.Winner;
import haveno.core.support.dispute.arbitration.arbitrator.ArbitratorManager;
import haveno.core.support.dispute.messages.DisputeClosedMessage;
import haveno.core.support.dispute.messages.DisputeOpenedMessage;
import haveno.core.support.messages.ChatMessage;
import haveno.core.support.messages.SupportMessage;
import haveno.core.trade.ClosedTradableManager;
import haveno.core.trade.Contract;
import haveno.core.trade.HavenoUtils;
import haveno.core.trade.Trade;
import haveno.core.trade.TradeManager;
import haveno.network.p2p.AckMessageSourceType;
import haveno.network.p2p.NodeAddress;
import haveno.network.p2p.P2PService;
import com.google.inject.Inject;
import com.google.inject.Singleton;
import java.math.BigInteger;
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 lombok.extern.slf4j.Slf4j;
import static com.google.common.base.Preconditions.checkNotNull;
import monero.wallet.MoneroWallet;
import monero.wallet.model.MoneroDestination;
import monero.wallet.model.MoneroMultisigSignResult;
import monero.wallet.model.MoneroTxSet;
import monero.wallet.model.MoneroTxWallet;
@Slf4j
@Singleton
public final class ArbitrationManager extends DisputeManager<ArbitrationDisputeList> {
private final ArbitratorManager arbitratorManager;
private Map<String, Integer> reprocessDisputeClosedMessageCounts = new HashMap<>();
///////////////////////////////////////////////////////////////////////////////////////////
// Constructor
///////////////////////////////////////////////////////////////////////////////////////////
@Inject
public ArbitrationManager(P2PService p2PService,
TradeWalletService tradeWalletService,
XmrWalletService walletService,
CoreMoneroConnectionsService connectionService,
CoreNotificationService notificationService,
ArbitratorManager arbitratorManager,
TradeManager tradeManager,
ClosedTradableManager closedTradableManager,
OpenOfferManager openOfferManager,
KeyRing keyRing,
ArbitrationDisputeListService arbitrationDisputeListService,
Config config,
PriceFeedService priceFeedService) {
super(p2PService, tradeWalletService, walletService, connectionService, notificationService, tradeManager, closedTradableManager,
openOfferManager, keyRing, arbitrationDisputeListService, config, priceFeedService);
this.arbitratorManager = arbitratorManager;
HavenoUtils.arbitrationManager = this; // TODO: storing static reference, better way?
}
///////////////////////////////////////////////////////////////////////////////////////////
// Implement template methods
///////////////////////////////////////////////////////////////////////////////////////////
@Override
public SupportType getSupportType() {
return SupportType.ARBITRATION;
}
@Override
public void onSupportMessage(SupportMessage message) {
if (canProcessMessage(message)) {
log.info("Received {} from {} with tradeId {} and uid {}",
message.getClass().getSimpleName(), message.getSenderNodeAddress(), message.getTradeId(), message.getUid());
new Thread(() -> {
if (message instanceof DisputeOpenedMessage) {
handleDisputeOpenedMessage((DisputeOpenedMessage) message);
} else if (message instanceof ChatMessage) {
handleChatMessage((ChatMessage) message);
} else if (message instanceof DisputeClosedMessage) {
handleDisputeClosedMessage((DisputeClosedMessage) message);
} else {
log.warn("Unsupported message at dispatchMessage. message={}", message);
}
}).start();
}
}
@Override
public NodeAddress getAgentNodeAddress(Dispute dispute) {
return dispute.getContract().getArbitratorNodeAddress();
}
@Override
protected AckMessageSourceType getAckMessageSourceType() {
return AckMessageSourceType.ARBITRATION_MESSAGE;
}
@Override
public void cleanupDisputes() {
// no action
}
@Override
protected String getDisputeInfo(Dispute dispute) {
String role = Res.get("shared.arbitrator").toLowerCase();
String link = "https://docs.bisq.network/trading-rules.html#legacy-arbitration";
return Res.get("support.initialInfo", role, role, link);
}
@Override
protected String getDisputeIntroForPeer(String disputeInfo) {
return Res.get("support.peerOpenedDispute", disputeInfo, Version.VERSION);
}
@Override
protected String getDisputeIntroForDisputeCreator(String disputeInfo) {
return Res.get("support.youOpenedDispute", disputeInfo, Version.VERSION);
}
@Override
protected void addPriceInfoMessage(Dispute dispute, int counter) {
// Arbitrator is not used anymore.
}
///////////////////////////////////////////////////////////////////////////////////////////
// Dispute handling
///////////////////////////////////////////////////////////////////////////////////////////
// received by both peers when arbitrator closes disputes
@Override
public void handleDisputeClosedMessage(DisputeClosedMessage disputeClosedMessage) {
handleDisputeClosedMessage(disputeClosedMessage, true);
}
private void handleDisputeClosedMessage(DisputeClosedMessage disputeClosedMessage, boolean reprocessOnError) {
// get dispute's trade
final Trade trade = tradeManager.getTrade(disputeClosedMessage.getTradeId());
if (trade == null) {
log.warn("Dispute trade {} does not exist", disputeClosedMessage.getTradeId());
return;
}
// try to process dispute closed message
ChatMessage chatMessage = null;
Dispute dispute = null;
synchronized (trade) {
try {
DisputeResult disputeResult = disputeClosedMessage.getDisputeResult();
chatMessage = disputeResult.getChatMessage();
checkNotNull(chatMessage, "chatMessage must not be null");
String tradeId = disputeResult.getTradeId();
log.info("Processing {} for {} {}", disputeClosedMessage.getClass().getSimpleName(), trade.getClass().getSimpleName(), disputeResult.getTradeId());
// verify arbitrator signature
String summaryText = chatMessage.getMessage();
DisputeSummaryVerification.verifySignature(summaryText, arbitratorManager);
// save dispute closed message for reprocessing
trade.getProcessModel().setDisputeClosedMessage(disputeClosedMessage);
requestPersistence();
// get dispute
Optional<Dispute> disputeOptional = findDispute(disputeResult);
String uid = disputeClosedMessage.getUid();
if (!disputeOptional.isPresent()) {
log.warn("We got a dispute closed msg but we don't have a matching dispute. " +
"That might happen when we get the DisputeClosedMessage before the dispute was created. " +
"We try again after 2 sec. to apply the DisputeClosedMessage. TradeId = " + tradeId);
if (!delayMsgMap.containsKey(uid)) {
// We delay 2 sec. to be sure the comm. msg gets added first
Timer timer = UserThread.runAfter(() -> handleDisputeClosedMessage(disputeClosedMessage), 2);
delayMsgMap.put(uid, timer);
} else {
log.warn("We got a dispute closed msg after we already repeated to apply the message after a delay. " +
"That should never happen. TradeId = " + tradeId);
}
return;
}
dispute = disputeOptional.get();
// verify that arbitrator does not get DisputeClosedMessage
if (keyRing.getPubKeyRing().equals(dispute.getAgentPubKeyRing())) {
log.error("Arbitrator received disputeResultMessage. That should never happen.");
trade.getProcessModel().setDisputeClosedMessage(null); // don't reprocess
return;
}
// set dispute state
cleanupRetryMap(uid);
if (!dispute.getChatMessages().contains(chatMessage)) {
dispute.addAndPersistChatMessage(chatMessage);
} else {
log.warn("We got a dispute mail msg what we have already stored. TradeId = " + chatMessage.getTradeId());
}
dispute.setIsClosed();
if (dispute.disputeResultProperty().get() != null) {
log.info("We already got a dispute result, indicating the message was resent after updating multisig info. TradeId = " + tradeId);
}
dispute.setDisputeResult(disputeResult);
// sync and save wallet
if (!trade.isPayoutPublished()) {
trade.syncWallet();
trade.saveWallet();
}
// import multisig hex
if (trade.walletExists()) {
if (disputeClosedMessage.getUpdatedMultisigHex() != null) trade.getArbitrator().setUpdatedMultisigHex(disputeClosedMessage.getUpdatedMultisigHex());
trade.importMultisigHex();
}
// attempt to sign and publish dispute payout tx if given and not already published
if (disputeClosedMessage.getUnsignedPayoutTxHex() != null && !trade.isPayoutPublished()) {
// wait to sign and publish payout tx if defer flag set
if (disputeClosedMessage.isDeferPublishPayout()) {
log.info("Deferring signing and publishing dispute payout tx for {} {}", trade.getClass().getSimpleName(), trade.getId());
GenUtils.waitFor(Trade.DEFER_PUBLISH_MS);
if (!trade.isPayoutUnlocked()) trade.syncWallet();
}
// sign and publish dispute payout tx if peer still has not published
if (!trade.isPayoutPublished()) {
try {
log.info("Signing and publishing dispute payout tx for {} {}", trade.getClass().getSimpleName(), trade.getId());
signAndPublishDisputePayoutTx(trade);
} catch (Exception e) {
// check if payout published again
trade.syncWallet();
if (trade.isPayoutPublished()) {
log.info("Dispute payout tx already published for {} {}", trade.getClass().getSimpleName(), trade.getId());
} else {
throw new RuntimeException("Failed to sign and publish dispute payout tx from arbitrator: " + e.getMessage() + ". TradeId = " + tradeId);
}
}
} else {
log.info("Dispute payout tx already published for {} {}", trade.getClass().getSimpleName(), trade.getId());
}
} else {
if (trade.isPayoutPublished()) log.info("Dispute payout tx already published for {} {}", trade.getClass().getSimpleName(), trade.getId());
else if (disputeClosedMessage.getUnsignedPayoutTxHex() == null) log.info("{} did not receive unsigned dispute payout tx for trade {} because the arbitrator did not have their updated multisig info (can happen if trader went offline after trade started)", trade.getClass().getSimpleName(), trade.getId());
}
// We use the chatMessage as we only persist those not the DisputeClosedMessage.
// If we would use the DisputeClosedMessage we could not lookup for the msg when we receive the AckMessage.
sendAckMessage(chatMessage, dispute.getAgentPubKeyRing(), true, null);
requestPersistence();
} catch (Exception e) {
log.warn("Error processing dispute closed message: " + e.getMessage());
e.printStackTrace();
requestPersistence();
// nack bad message and do not reprocess
if (e instanceof IllegalArgumentException) {
trade.getProcessModel().setPaymentReceivedMessage(null); // message is processed
sendAckMessage(chatMessage, dispute.getAgentPubKeyRing(), false, e.getMessage());
requestPersistence();
throw e;
}
// schedule to reprocess message unless deleted
if (trade.getProcessModel().getDisputeClosedMessage() != null) {
if (!reprocessDisputeClosedMessageCounts.containsKey(trade.getId())) reprocessDisputeClosedMessageCounts.put(trade.getId(), 0);
UserThread.runAfter(() -> {
reprocessDisputeClosedMessageCounts.put(trade.getId(), reprocessDisputeClosedMessageCounts.get(trade.getId()) + 1); // increment reprocess count
maybeReprocessDisputeClosedMessage(trade, reprocessOnError);
}, trade.getReprocessDelayInSeconds(reprocessDisputeClosedMessageCounts.get(trade.getId())));
}
}
}
}
public void maybeReprocessDisputeClosedMessage(Trade trade, boolean reprocessOnError) {
synchronized (trade) {
// skip if no need to reprocess
if (trade.isArbitrator() || trade.getProcessModel().getDisputeClosedMessage() == null || trade.getProcessModel().getDisputeClosedMessage().getUnsignedPayoutTxHex() == null || trade.getDisputeState().ordinal() >= Trade.DisputeState.DISPUTE_CLOSED.ordinal()) {
return;
}
log.warn("Reprocessing dispute closed message for {} {}", trade.getClass().getSimpleName(), trade.getId());
new Thread(() -> handleDisputeClosedMessage(trade.getProcessModel().getDisputeClosedMessage(), reprocessOnError)).start();
}
}
private MoneroTxSet signAndPublishDisputePayoutTx(Trade trade) {
// gather trade info
MoneroWallet multisigWallet = trade.getWallet();
Optional<Dispute> disputeOptional = findDispute(trade.getId());
if (!disputeOptional.isPresent()) throw new RuntimeException("Trader has no dispute when signing dispute payout tx. This should never happen. TradeId = " + trade.getId());
Dispute dispute = disputeOptional.get();
Contract contract = dispute.getContract();
DisputeResult disputeResult = dispute.getDisputeResultProperty().get();
String unsignedPayoutTxHex = trade.getProcessModel().getDisputeClosedMessage().getUnsignedPayoutTxHex();
// Offer offer = checkNotNull(trade.getOffer(), "offer must not be null");
// BigInteger sellerDepositAmount = multisigWallet.getTx(trade instanceof MakerTrade ? trade.getMaker().getDepositTxHash() : trade.getTaker().getDepositTxHash()).getIncomingAmount(); // TODO (woodser): use contract instead of trade to get deposit tx ids when contract has deposit tx ids
// BigInteger buyerDepositAmount = multisigWallet.getTx(trade instanceof MakerTrade ? trade.getTaker().getDepositTxHash() : trade.getMaker().getDepositTxHash()).getIncomingAmount();
// BigInteger tradeAmount = BigInteger.valueOf(contract.getTradeAmount().value).multiply(ParsingUtils.XMR_SATOSHI_MULTIPLIER);
// parse arbitrator-signed payout tx
MoneroTxSet disputeTxSet = multisigWallet.describeTxSet(new MoneroTxSet().setMultisigTxHex(unsignedPayoutTxHex));
if (disputeTxSet.getTxs() == null || disputeTxSet.getTxs().size() != 1) throw new RuntimeException("Bad arbitrator-signed payout tx"); // TODO (woodser): nack
MoneroTxWallet arbitratorSignedPayoutTx = disputeTxSet.getTxs().get(0);
// verify payout tx has 1 or 2 destinations
int numDestinations = arbitratorSignedPayoutTx.getOutgoingTransfer() == null || arbitratorSignedPayoutTx.getOutgoingTransfer().getDestinations() == null ? 0 : arbitratorSignedPayoutTx.getOutgoingTransfer().getDestinations().size();
if (numDestinations != 1 && numDestinations != 2) throw new RuntimeException("Buyer-signed payout tx does not have 1 or 2 destinations");
// get buyer and seller destinations (order not preserved)
List<MoneroDestination> destinations = arbitratorSignedPayoutTx.getOutgoingTransfer().getDestinations();
boolean buyerFirst = destinations.get(0).getAddress().equals(contract.getBuyerPayoutAddressString());
MoneroDestination buyerPayoutDestination = buyerFirst ? destinations.get(0) : numDestinations == 2 ? destinations.get(1) : null;
MoneroDestination sellerPayoutDestination = buyerFirst ? (numDestinations == 2 ? destinations.get(1) : null) : destinations.get(0);
// verify payout addresses
if (buyerPayoutDestination != null && !buyerPayoutDestination.getAddress().equals(contract.getBuyerPayoutAddressString())) throw new RuntimeException("Buyer payout address does not match contract");
if (sellerPayoutDestination != null && !sellerPayoutDestination.getAddress().equals(contract.getSellerPayoutAddressString())) throw new RuntimeException("Seller payout address does not match contract");
// verify change address is multisig's primary address
if (!arbitratorSignedPayoutTx.getChangeAmount().equals(BigInteger.ZERO) && !arbitratorSignedPayoutTx.getChangeAddress().equals(multisigWallet.getPrimaryAddress())) throw new RuntimeException("Change address is not multisig wallet's primary address");
// verify sum of outputs = destination amounts + change amount
BigInteger destinationSum = (buyerPayoutDestination == null ? BigInteger.ZERO : buyerPayoutDestination.getAmount()).add(sellerPayoutDestination == null ? BigInteger.ZERO : sellerPayoutDestination.getAmount());
if (!arbitratorSignedPayoutTx.getOutputSum().equals(destinationSum.add(arbitratorSignedPayoutTx.getChangeAmount()))) throw new RuntimeException("Sum of outputs != destination amounts + change amount");
// get actual payout amounts
BigInteger actualWinnerAmount = disputeResult.getWinner() == Winner.BUYER ? buyerPayoutDestination.getAmount() : sellerPayoutDestination.getAmount();
BigInteger actualLoserAmount = numDestinations == 1 ? BigInteger.ZERO : disputeResult.getWinner() == Winner.BUYER ? sellerPayoutDestination.getAmount() : buyerPayoutDestination.getAmount();
// verify payouts sum to unlocked balance within loss of precision due to conversion to centineros
BigInteger txCost = arbitratorSignedPayoutTx.getFee().add(arbitratorSignedPayoutTx.getChangeAmount()); // fee + lost dust change
if (trade.getWallet().getUnlockedBalance().subtract(actualWinnerAmount.add(actualLoserAmount).add(txCost)).compareTo(BigInteger.valueOf(0)) > 0) {
throw new RuntimeException("The dispute payout amounts do not sum to the wallet's unlocked balance while verifying the dispute payout tx, unlocked balance=" + trade.getWallet().getUnlockedBalance() + " vs sum payout amount=" + actualWinnerAmount.add(actualLoserAmount) + ", winner payout=" + actualWinnerAmount + ", loser payout=" + actualLoserAmount);
}
// get expected payout amounts
BigInteger expectedWinnerAmount = disputeResult.getWinner() == Winner.BUYER ? disputeResult.getBuyerPayoutAmount() : disputeResult.getSellerPayoutAmount();
BigInteger expectedLoserAmount = disputeResult.getWinner() == Winner.BUYER ? disputeResult.getSellerPayoutAmount() : disputeResult.getBuyerPayoutAmount();
// add any loss of precision to winner amount
expectedWinnerAmount = expectedWinnerAmount.add(trade.getWallet().getUnlockedBalance().subtract(expectedWinnerAmount.add(expectedLoserAmount)));
// winner pays cost if loser gets nothing, otherwise loser pays cost
if (expectedLoserAmount.equals(BigInteger.ZERO)) expectedWinnerAmount = expectedWinnerAmount.subtract(txCost);
else expectedLoserAmount = expectedLoserAmount.subtract(txCost);
// verify winner and loser payout amounts
if (!expectedWinnerAmount.equals(actualWinnerAmount)) throw new RuntimeException("Unexpected winner payout: " + expectedWinnerAmount + " vs " + actualWinnerAmount);
if (!expectedLoserAmount.equals(actualLoserAmount)) throw new RuntimeException("Unexpected loser payout: " + expectedLoserAmount + " vs " + actualLoserAmount);
// check wallet's daemon connection
trade.checkWalletConnection();
// determine if we already signed dispute payout tx
// TODO: better way, such as by saving signed dispute payout tx hex in designated field instead of shared payoutTxHex field?
Set<String> nonSignedDisputePayoutTxHexes = new HashSet<String>();
if (trade.getProcessModel().getPaymentSentMessage() != null) nonSignedDisputePayoutTxHexes.add(trade.getProcessModel().getPaymentSentMessage().getPayoutTxHex());
if (trade.getProcessModel().getPaymentReceivedMessage() != null) {
nonSignedDisputePayoutTxHexes.add(trade.getProcessModel().getPaymentReceivedMessage().getUnsignedPayoutTxHex());
nonSignedDisputePayoutTxHexes.add(trade.getProcessModel().getPaymentReceivedMessage().getSignedPayoutTxHex());
}
boolean signed = trade.getPayoutTxHex() != null && !nonSignedDisputePayoutTxHexes.contains(trade.getPayoutTxHex());
// sign arbitrator-signed payout tx
if (!signed) {
MoneroMultisigSignResult result = multisigWallet.signMultisigTxHex(unsignedPayoutTxHex);
if (result.getSignedMultisigTxHex() == null) throw new RuntimeException("Error signing arbitrator-signed payout tx");
String signedMultisigTxHex = result.getSignedMultisigTxHex();
disputeTxSet.setMultisigTxHex(signedMultisigTxHex);
trade.setPayoutTxHex(signedMultisigTxHex);
requestPersistence();
// verify mining fee is within tolerance by recreating payout tx
// TODO (monero-project): creating tx will require exchanging updated multisig hex if message needs reprocessed. provide weight with describe_transfer so fee can be estimated?
MoneroTxWallet feeEstimateTx = null;
try {
feeEstimateTx = createDisputePayoutTx(trade, dispute.getContract(), disputeResult, true);
} catch (Exception e) {
log.warn("Could not recreate dispute payout tx to verify fee: " + e.getMessage());
}
if (feeEstimateTx != null) {
BigInteger feeEstimate = feeEstimateTx.getFee();
double feeDiff = arbitratorSignedPayoutTx.getFee().subtract(feeEstimate).abs().doubleValue() / feeEstimate.doubleValue(); // TODO: use BigDecimal?
if (feeDiff > XmrWalletService.MINER_FEE_TOLERANCE) throw new IllegalArgumentException("Miner fee is not within " + (XmrWalletService.MINER_FEE_TOLERANCE * 100) + "% of estimated fee, expected " + feeEstimate + " but was " + arbitratorSignedPayoutTx.getFee());
log.info("Payout tx fee {} is within tolerance, diff %={}", arbitratorSignedPayoutTx.getFee(), feeDiff);
}
} else {
disputeTxSet.setMultisigTxHex(trade.getPayoutTxHex());
}
// submit fully signed payout tx to the network
List<String> txHashes = multisigWallet.submitMultisigTxHex(disputeTxSet.getMultisigTxHex());
disputeTxSet.getTxs().get(0).setHash(txHashes.get(0)); // manually update hash which is known after signed
// update state
trade.setPayoutTx(disputeTxSet.getTxs().get(0)); // TODO (woodser): is trade.payoutTx() mutually exclusive from dispute payout tx?
trade.setPayoutTxId(disputeTxSet.getTxs().get(0).getHash());
trade.setPayoutState(Trade.PayoutState.PAYOUT_PUBLISHED);
dispute.setDisputePayoutTxId(disputeTxSet.getTxs().get(0).getHash());
return disputeTxSet;
}
}

View file

@ -0,0 +1,31 @@
/*
* 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 haveno.core.support.dispute.arbitration;
import lombok.extern.slf4j.Slf4j;
import haveno.core.support.dispute.Dispute;
import haveno.core.support.dispute.DisputeSession;
import javax.annotation.Nullable;
@Slf4j
public class ArbitrationSession extends DisputeSession {
public ArbitrationSession(@Nullable Dispute dispute, boolean isTrader) {
super(dispute, isTrader);
}
}

View file

@ -0,0 +1,47 @@
/*
* 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 haveno.core.support.dispute.arbitration;
import haveno.core.account.witness.AccountAgeWitness;
import haveno.core.payment.payload.PaymentAccountPayload;
import java.math.BigInteger;
import java.security.PublicKey;
import lombok.EqualsAndHashCode;
import lombok.Getter;
// TODO consider to move to signed witness domain
@Getter
@EqualsAndHashCode(onlyExplicitlyIncluded = true)
public class TraderDataItem {
private final PaymentAccountPayload paymentAccountPayload;
@EqualsAndHashCode.Include
private final AccountAgeWitness accountAgeWitness;
private final BigInteger tradeAmount;
private final PublicKey peersPubKey;
public TraderDataItem(PaymentAccountPayload paymentAccountPayload,
AccountAgeWitness accountAgeWitness,
BigInteger tradeAmount,
PublicKey peersPubKey) {
this.paymentAccountPayload = paymentAccountPayload;
this.accountAgeWitness = accountAgeWitness;
this.tradeAmount = tradeAmount;
this.peersPubKey = peersPubKey;
}
}

View file

@ -0,0 +1,111 @@
/*
* 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 haveno.core.support.dispute.arbitration.arbitrator;
import com.google.protobuf.ByteString;
import haveno.common.crypto.PubKeyRing;
import haveno.common.proto.ProtoUtil;
import haveno.common.util.CollectionUtils;
import haveno.core.support.dispute.agent.DisputeAgent;
import haveno.network.p2p.NodeAddress;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import javax.annotation.Nullable;
@EqualsAndHashCode(callSuper = true)
@Slf4j
@Getter
public final class Arbitrator extends DisputeAgent {
private final String xmrAddress;
public Arbitrator(NodeAddress nodeAddress,
String xmrAddress,
PubKeyRing pubKeyRing,
List<String> languageCodes,
long registrationDate,
byte[] registrationPubKey,
String registrationSignature,
@Nullable String emailAddress,
@Nullable String info,
@Nullable Map<String, String> extraDataMap) {
super(nodeAddress,
pubKeyRing,
languageCodes,
registrationDate,
registrationPubKey,
registrationSignature,
emailAddress,
info,
extraDataMap);
this.xmrAddress = xmrAddress;
}
///////////////////////////////////////////////////////////////////////////////////////////
// PROTO BUFFER
///////////////////////////////////////////////////////////////////////////////////////////
@Override
public protobuf.StoragePayload toProtoMessage() {
protobuf.Arbitrator.Builder builder = protobuf.Arbitrator.newBuilder()
.setNodeAddress(nodeAddress.toProtoMessage())
.setXmrAddress(xmrAddress)
.setPubKeyRing(pubKeyRing.toProtoMessage())
.addAllLanguageCodes(languageCodes)
.setRegistrationDate(registrationDate)
.setRegistrationPubKey(ByteString.copyFrom(registrationPubKey))
.setRegistrationSignature(registrationSignature);
Optional.ofNullable(emailAddress).ifPresent(builder::setEmailAddress);
Optional.ofNullable(info).ifPresent(builder::setInfo);
Optional.ofNullable(extraDataMap).ifPresent(builder::putAllExtraData);
return protobuf.StoragePayload.newBuilder().setArbitrator(builder).build();
}
public static Arbitrator fromProto(protobuf.Arbitrator proto) {
return new Arbitrator(NodeAddress.fromProto(proto.getNodeAddress()),
proto.getXmrAddress(),
PubKeyRing.fromProto(proto.getPubKeyRing()),
new ArrayList<>(proto.getLanguageCodesList()),
proto.getRegistrationDate(),
proto.getRegistrationPubKey().toByteArray(),
proto.getRegistrationSignature(),
ProtoUtil.stringOrNullFromProto(proto.getEmailAddress()),
ProtoUtil.stringOrNullFromProto(proto.getInfo()),
CollectionUtils.isEmpty(proto.getExtraDataMap()) ? null : proto.getExtraDataMap());
}
///////////////////////////////////////////////////////////////////////////////////////////
// API
///////////////////////////////////////////////////////////////////////////////////////////
@Override
public String toString() {
return "Arbitrator{" +
",\n xmrAddress='" + xmrAddress + '\'' +
"\n} " + super.toString();
}
}

View file

@ -0,0 +1,106 @@
/*
* 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 haveno.core.support.dispute.arbitration.arbitrator;
import haveno.common.config.Config;
import haveno.common.crypto.KeyRing;
import haveno.core.filter.FilterManager;
import haveno.core.support.dispute.agent.DisputeAgentManager;
import haveno.core.user.User;
import haveno.network.p2p.storage.payload.ProtectedStorageEntry;
import javax.inject.Inject;
import javax.inject.Singleton;
import java.util.ArrayList;
import java.util.List;
import lombok.extern.slf4j.Slf4j;
@Slf4j
@Singleton
public class ArbitratorManager extends DisputeAgentManager<Arbitrator> {
@Inject
public ArbitratorManager(KeyRing keyRing,
ArbitratorService arbitratorService,
User user,
FilterManager filterManager) {
super(keyRing, arbitratorService, user, filterManager);
}
@Override
protected List<String> getPubKeyList() {
switch (Config.baseCurrencyNetwork()) {
case XMR_LOCAL:
return List.of(
"027a381b5333a56e1cc3d90d3a7d07f26509adf7029ed06fc997c656621f8da1ee",
"024baabdba90e7cc0dc4626ef73ea9d722ea7085d1104491da8c76f28187513492",
"026eeec3c119dd6d537249d74e5752a642dd2c3cc5b6a9b44588eb58344f29b519");
case XMR_STAGENET:
return List.of(
"03bb559ce207a4deb51d4c705076c95b85ad8581d35936b2a422dcb504eaf7cdb0",
"026c581ad773d987e6bd10785ac7f7e0e64864aedeb8bce5af37046de812a37854",
"025b058c9f2c60d839669dbfa5578cf5a8117d60e6b70e2f0946f8a691273c6a36",
"036c7d3f4bf05ef39b9d1b0a5d453a18210de36220c3d83cd16e59bd6132b037ad",
"030f7122a10ff73cd73808bddace95be77a94189c8a0eb24586265e125ce5ce6b9",
"03aa23e062afa0dda465f46986f8aa8d0374ad3e3f256141b05681dcb1e39c3859",
"02d3beb1293ca2ca14e6d42ca8bd18089a62aac62fd6bb23923ee6ead46ac60fba",
"03fa0f38f27bdd324db6f933f7e57851dadf3b911e4db6b19dd0950492c4525a31",
"02a1a458df5acf4ab08fdca748e28f33a955a30854c8c1a831ee733dca7f0d2fcd",
"0374dd70f3fa6e47ec5ab97932e1cec6233e98e6ae3129036b17118650c44fd3de");
case XMR_MAINNET:
return new ArrayList<String>();
default:
throw new RuntimeException("Unhandled base currency network: " + Config.baseCurrencyNetwork());
}
}
@Override
protected boolean isExpectedInstance(ProtectedStorageEntry data) {
return data.getProtectedStoragePayload() instanceof Arbitrator;
}
@Override
protected void addAcceptedDisputeAgentToUser(Arbitrator disputeAgent) {
user.addAcceptedArbitrator(disputeAgent);
}
@Override
protected void removeAcceptedDisputeAgentFromUser(ProtectedStorageEntry data) {
user.removeAcceptedArbitrator((Arbitrator) data.getProtectedStoragePayload());
}
@Override
protected List<Arbitrator> getAcceptedDisputeAgentsFromUser() {
return user.getAcceptedArbitrators();
}
@Override
protected void clearAcceptedDisputeAgentsAtUser() {
user.clearAcceptedArbitrators();
}
@Override
protected Arbitrator getRegisteredDisputeAgentFromUser() {
return user.getRegisteredArbitrator();
}
@Override
protected void setRegisteredDisputeAgentAtUser(Arbitrator disputeAgent) {
user.setRegisteredArbitrator(disputeAgent);
}
}

View file

@ -0,0 +1,58 @@
/*
* 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 haveno.core.support.dispute.arbitration.arbitrator;
import com.google.inject.Singleton;
import haveno.core.filter.FilterManager;
import haveno.core.support.dispute.agent.DisputeAgentService;
import haveno.network.p2p.NodeAddress;
import haveno.network.p2p.P2PService;
import javax.inject.Inject;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
@Singleton
public class ArbitratorService extends DisputeAgentService<Arbitrator> {
@Inject
public ArbitratorService(P2PService p2PService, FilterManager filterManager) {
super(p2PService, filterManager);
}
@Override
protected Set<Arbitrator> getDisputeAgentSet(List<String> bannedDisputeAgents) {
return p2PService.getDataMap().values().stream()
.filter(data -> data.getProtectedStoragePayload() instanceof Arbitrator)
.map(data -> (Arbitrator) data.getProtectedStoragePayload())
.filter(a -> bannedDisputeAgents == null ||
!bannedDisputeAgents.contains(a.getNodeAddress().getFullAddress()))
.collect(Collectors.toSet());
}
@Override
protected List<String> getDisputeAgentsFromFilter() {
return filterManager.getFilter() != null ? filterManager.getFilter().getArbitrators() : new ArrayList<>();
}
public Map<NodeAddress, Arbitrator> getArbitrators() {
return super.getDisputeAgents();
}
}

View file

@ -0,0 +1,27 @@
/*
* 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 haveno.core.support.dispute.arbitration.messages;
import haveno.core.support.SupportType;
import haveno.core.support.dispute.messages.DisputeMessage;
abstract class ArbitrationMessage extends DisputeMessage {
ArbitrationMessage(String messageVersion, String uid, SupportType supportType) {
super(messageVersion, uid, supportType);
}
}

View file

@ -0,0 +1,70 @@
/*
* 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 haveno.core.support.dispute.mediation;
import com.google.protobuf.Message;
import haveno.common.proto.ProtoUtil;
import haveno.core.proto.CoreProtoResolver;
import haveno.core.support.SupportType;
import haveno.core.support.dispute.Dispute;
import haveno.core.support.dispute.DisputeList;
import java.util.Collection;
import java.util.List;
import java.util.stream.Collectors;
import lombok.ToString;
import lombok.extern.slf4j.Slf4j;
@Slf4j
@ToString
/*
* Holds a List of mediation dispute objects.
*
* Calls to the List are delegated because this class intercepts the add/remove calls so changes
* can be saved to disc.
*/
public final class MediationDisputeList extends DisputeList<Dispute> {
MediationDisputeList() {
super();
}
///////////////////////////////////////////////////////////////////////////////////////////
// PROTO BUFFER
///////////////////////////////////////////////////////////////////////////////////////////
protected MediationDisputeList(Collection<Dispute> collection) {
super(collection);
}
@Override
public Message toProtoMessage() {
return protobuf.PersistableEnvelope.newBuilder().setMediationDisputeList(protobuf.MediationDisputeList.newBuilder()
.addAllDispute(ProtoUtil.collectionToProto(getList(), protobuf.Dispute.class))).build();
}
public static MediationDisputeList fromProto(protobuf.MediationDisputeList proto,
CoreProtoResolver coreProtoResolver) {
List<Dispute> list = proto.getDisputeList().stream()
.map(disputeProto -> Dispute.fromProto(disputeProto, coreProtoResolver))
.filter(e -> e.getSupportType().equals(SupportType.MEDIATION))
.collect(Collectors.toList());
return new MediationDisputeList(list);
}
}

View file

@ -0,0 +1,46 @@
/*
* 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 haveno.core.support.dispute.mediation;
import haveno.common.persistence.PersistenceManager;
import haveno.core.support.dispute.DisputeListService;
import javax.inject.Inject;
import javax.inject.Singleton;
@Singleton
public final class MediationDisputeListService extends DisputeListService<MediationDisputeList> {
///////////////////////////////////////////////////////////////////////////////////////////
// Constructor
///////////////////////////////////////////////////////////////////////////////////////////
@Inject
public MediationDisputeListService(PersistenceManager<MediationDisputeList> persistenceManager) {
super(persistenceManager);
}
///////////////////////////////////////////////////////////////////////////////////////////
// Implement template methods
///////////////////////////////////////////////////////////////////////////////////////////
@Override
protected MediationDisputeList getConcreteDisputeList() {
return new MediationDisputeList();
}
}

View file

@ -0,0 +1,259 @@
/*
* 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 haveno.core.support.dispute.mediation;
import com.google.inject.Inject;
import com.google.inject.Singleton;
import haveno.common.Timer;
import haveno.common.UserThread;
import haveno.common.app.Version;
import haveno.common.config.Config;
import haveno.common.crypto.KeyRing;
import haveno.common.handlers.ErrorMessageHandler;
import haveno.common.handlers.ResultHandler;
import haveno.core.api.CoreMoneroConnectionsService;
import haveno.core.api.CoreNotificationService;
import haveno.core.btc.wallet.TradeWalletService;
import haveno.core.btc.wallet.XmrWalletService;
import haveno.core.locale.Res;
import haveno.core.offer.OpenOffer;
import haveno.core.offer.OpenOfferManager;
import haveno.core.provider.price.PriceFeedService;
import haveno.core.support.SupportType;
import haveno.core.support.dispute.Dispute;
import haveno.core.support.dispute.DisputeManager;
import haveno.core.support.dispute.DisputeResult;
import haveno.core.support.dispute.messages.DisputeClosedMessage;
import haveno.core.support.dispute.messages.DisputeOpenedMessage;
import haveno.core.support.messages.ChatMessage;
import haveno.core.support.messages.SupportMessage;
import haveno.core.trade.ClosedTradableManager;
import haveno.core.trade.Trade;
import haveno.core.trade.TradeManager;
import haveno.core.trade.protocol.DisputeProtocol;
import haveno.core.trade.protocol.ProcessModel;
import haveno.network.p2p.AckMessageSourceType;
import haveno.network.p2p.NodeAddress;
import haveno.network.p2p.P2PService;
import java.math.BigInteger;
import java.util.Optional;
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
@Singleton
public final class MediationManager extends DisputeManager<MediationDisputeList> {
///////////////////////////////////////////////////////////////////////////////////////////
// Constructor
///////////////////////////////////////////////////////////////////////////////////////////
@Inject
public MediationManager(P2PService p2PService,
TradeWalletService tradeWalletService,
XmrWalletService walletService,
CoreMoneroConnectionsService connectionService,
CoreNotificationService notificationService,
TradeManager tradeManager,
ClosedTradableManager closedTradableManager,
OpenOfferManager openOfferManager,
KeyRing keyRing,
MediationDisputeListService mediationDisputeListService,
Config config,
PriceFeedService priceFeedService) {
super(p2PService, tradeWalletService, walletService, connectionService, notificationService, tradeManager, closedTradableManager,
openOfferManager, keyRing, mediationDisputeListService, config, priceFeedService);
}
///////////////////////////////////////////////////////////////////////////////////////////
// Implement template methods
///////////////////////////////////////////////////////////////////////////////////////////
@Override
public SupportType getSupportType() {
return SupportType.MEDIATION;
}
@Override
public void onSupportMessage(SupportMessage message) {
if (canProcessMessage(message)) {
log.info("Received {} with tradeId {} and uid {}",
message.getClass().getSimpleName(), message.getTradeId(), message.getUid());
if (message instanceof DisputeOpenedMessage) {
handleDisputeOpenedMessage((DisputeOpenedMessage) message);
} else if (message instanceof ChatMessage) {
handleChatMessage((ChatMessage) message);
} else if (message instanceof DisputeClosedMessage) {
handleDisputeClosedMessage((DisputeClosedMessage) message);
} else {
log.warn("Unsupported message at dispatchMessage. message={}", message);
}
}
}
@Override
protected AckMessageSourceType getAckMessageSourceType() {
return AckMessageSourceType.MEDIATION_MESSAGE;
}
@Override
public void cleanupDisputes() {
disputeListService.cleanupDisputes(tradeId -> {
tradeManager.getOpenTrade(tradeId).filter(trade -> trade.getPayoutTx() != null)
.ifPresent(trade -> {
tradeManager.closeDisputedTrade(tradeId, Trade.DisputeState.MEDIATION_CLOSED);
});
});
}
@Override
protected String getDisputeInfo(Dispute dispute) {
String role = Res.get("shared.mediator").toLowerCase();
String link = "https://docs.bisq.network/trading-rules.html#mediation";
return Res.get("support.initialInfo", role, role, link);
}
@Override
protected String getDisputeIntroForPeer(String disputeInfo) {
return Res.get("support.peerOpenedDisputeForMediation", disputeInfo, Version.VERSION);
}
@Override
protected String getDisputeIntroForDisputeCreator(String disputeInfo) {
return Res.get("support.youOpenedDisputeForMediation", disputeInfo, Version.VERSION);
}
///////////////////////////////////////////////////////////////////////////////////////////
// Message handler
///////////////////////////////////////////////////////////////////////////////////////////
@Override
// We get that message at both peers. The dispute object is in context of the trader
public void handleDisputeClosedMessage(DisputeClosedMessage disputeResultMessage) {
DisputeResult disputeResult = disputeResultMessage.getDisputeResult();
String tradeId = disputeResult.getTradeId();
ChatMessage chatMessage = disputeResult.getChatMessage();
checkNotNull(chatMessage, "chatMessage must not be null");
Optional<Dispute> disputeOptional = findDispute(disputeResult);
String uid = disputeResultMessage.getUid();
if (!disputeOptional.isPresent()) {
log.warn("We got a dispute result msg but we don't have a matching dispute. " +
"That might happen when we get the disputeResultMessage before the dispute was created. " +
"We try again after 2 sec. to apply the disputeResultMessage. TradeId = " + tradeId);
if (!delayMsgMap.containsKey(uid)) {
// We delay 2 sec. to be sure the comm. msg gets added first
Timer timer = UserThread.runAfter(() -> handleDisputeClosedMessage(disputeResultMessage), 2);
delayMsgMap.put(uid, timer);
} else {
log.warn("We got a dispute result msg after we already repeated to apply the message after a delay. " +
"That should never happen. TradeId = " + tradeId);
}
return;
}
Dispute dispute = disputeOptional.get();
cleanupRetryMap(uid);
if (!dispute.getChatMessages().contains(chatMessage)) {
dispute.addAndPersistChatMessage(chatMessage);
} else {
log.warn("We got a dispute mail msg what we have already stored. TradeId = " + chatMessage.getTradeId());
}
dispute.setIsClosed();
dispute.setDisputeResult(disputeResult);
Optional<Trade> tradeOptional = tradeManager.getOpenTrade(tradeId);
if (tradeOptional.isPresent()) {
Trade trade = tradeOptional.get();
if (trade.getDisputeState() == Trade.DisputeState.MEDIATION_REQUESTED ||
trade.getDisputeState() == Trade.DisputeState.MEDIATION_STARTED_BY_PEER) {
trade.getProcessModel().setBuyerPayoutAmountFromMediation(disputeResult.getBuyerPayoutAmount().longValueExact());
trade.getProcessModel().setSellerPayoutAmountFromMediation(disputeResult.getSellerPayoutAmount().longValueExact());
trade.setDisputeState(Trade.DisputeState.MEDIATION_CLOSED);
tradeManager.requestPersistence();
}
} else {
Optional<OpenOffer> openOfferOptional = openOfferManager.getOpenOfferById(tradeId);
openOfferOptional.ifPresent(openOffer -> openOfferManager.closeOpenOffer(openOffer.getOffer()));
}
sendAckMessage(chatMessage, dispute.getAgentPubKeyRing(), true, null);
requestPersistence();
}
///////////////////////////////////////////////////////////////////////////////////////////
// API
///////////////////////////////////////////////////////////////////////////////////////////
@Nullable
@Override
public NodeAddress getAgentNodeAddress(Dispute dispute) {
return dispute.getContract().getArbitratorNodeAddress(); // TODO (woodser): mediator becomes and replaces current arbitrator?
}
public void onAcceptMediationResult(Trade trade,
ResultHandler resultHandler,
ErrorMessageHandler errorMessageHandler) {
String tradeId = trade.getId();
Optional<Dispute> optionalDispute = findDispute(tradeId);
checkArgument(optionalDispute.isPresent(), "dispute must be present");
DisputeResult disputeResult = optionalDispute.get().getDisputeResultProperty().get();
BigInteger buyerPayoutAmount = disputeResult.getBuyerPayoutAmount();
BigInteger sellerPayoutAmount = disputeResult.getSellerPayoutAmount();
ProcessModel processModel = trade.getProcessModel();
processModel.setBuyerPayoutAmountFromMediation(buyerPayoutAmount.longValueExact());
processModel.setSellerPayoutAmountFromMediation(sellerPayoutAmount.longValueExact());
DisputeProtocol tradeProtocol = (DisputeProtocol) tradeManager.getTradeProtocol(trade);
trade.setMediationResultState(MediationResultState.MEDIATION_RESULT_ACCEPTED);
tradeManager.requestPersistence();
// If we have not got yet the peers signature we sign and send to the peer our signature.
// Otherwise we sign and complete with the peers signature the payout tx.
if (trade.getTradePeer().getMediatedPayoutTxSignature() == null) {
tradeProtocol.onAcceptMediationResult(() -> {
if (trade.getPayoutTx() != null) {
tradeManager.closeDisputedTrade(tradeId, Trade.DisputeState.MEDIATION_CLOSED);
}
resultHandler.handleResult();
}, errorMessageHandler);
} else {
tradeProtocol.onFinalizeMediationResultPayout(() -> {
if (trade.getPayoutTx() != null) {
tradeManager.closeDisputedTrade(tradeId, Trade.DisputeState.MEDIATION_CLOSED);
}
resultHandler.handleResult();
}, errorMessageHandler);
}
}
public void rejectMediationResult(Trade trade) {
trade.setMediationResultState(MediationResultState.MEDIATION_RESULT_REJECTED);
tradeManager.requestPersistence();
}
}

View file

@ -0,0 +1,46 @@
/*
* 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 haveno.core.support.dispute.mediation;
import haveno.common.proto.ProtoUtil;
public enum MediationResultState {
UNDEFINED_MEDIATION_RESULT,
MEDIATION_RESULT_ACCEPTED(),
MEDIATION_RESULT_REJECTED,
SIG_MSG_SENT,
SIG_MSG_ARRIVED,
SIG_MSG_IN_MAILBOX,
SIG_MSG_SEND_FAILED,
RECEIVED_SIG_MSG,
PAYOUT_TX_PUBLISHED,
PAYOUT_TX_PUBLISHED_MSG_SENT,
PAYOUT_TX_PUBLISHED_MSG_ARRIVED,
PAYOUT_TX_PUBLISHED_MSG_IN_MAILBOX,
PAYOUT_TX_PUBLISHED_MSG_SEND_FAILED,
RECEIVED_PAYOUT_TX_PUBLISHED_MSG,
PAYOUT_TX_SEEN_IN_NETWORK;
public static MediationResultState fromProto(protobuf.MediationResultState mediationResultState) {
return ProtoUtil.enumFromProto(MediationResultState.class, mediationResultState.name());
}
public static protobuf.MediationResultState toProtoMessage(MediationResultState mediationResultState) {
return protobuf.MediationResultState.valueOf(mediationResultState.name());
}
}

View file

@ -0,0 +1,31 @@
/*
* 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 haveno.core.support.dispute.mediation;
import lombok.extern.slf4j.Slf4j;
import haveno.core.support.dispute.Dispute;
import haveno.core.support.dispute.DisputeSession;
import javax.annotation.Nullable;
@Slf4j
public class MediationSession extends DisputeSession {
public MediationSession(@Nullable Dispute dispute, boolean isTrader) {
super(dispute, isTrader);
}
}

View file

@ -0,0 +1,101 @@
/*
* 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 haveno.core.support.dispute.mediation.mediator;
import com.google.protobuf.ByteString;
import haveno.common.crypto.PubKeyRing;
import haveno.common.proto.ProtoUtil;
import haveno.common.util.CollectionUtils;
import haveno.core.support.dispute.agent.DisputeAgent;
import haveno.network.p2p.NodeAddress;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import lombok.EqualsAndHashCode;
import lombok.extern.slf4j.Slf4j;
import javax.annotation.Nullable;
@EqualsAndHashCode(callSuper = true)
@Slf4j
public final class Mediator extends DisputeAgent {
public Mediator(NodeAddress nodeAddress,
PubKeyRing pubKeyRing,
List<String> languageCodes,
long registrationDate,
byte[] registrationPubKey,
String registrationSignature,
@Nullable String emailAddress,
@Nullable String info,
@Nullable Map<String, String> extraDataMap) {
super(nodeAddress,
pubKeyRing,
languageCodes,
registrationDate,
registrationPubKey,
registrationSignature,
emailAddress,
info,
extraDataMap);
}
///////////////////////////////////////////////////////////////////////////////////////////
// PROTO BUFFER
///////////////////////////////////////////////////////////////////////////////////////////
@Override
public protobuf.StoragePayload toProtoMessage() {
final protobuf.Mediator.Builder builder = protobuf.Mediator.newBuilder()
.setNodeAddress(nodeAddress.toProtoMessage())
.setPubKeyRing(pubKeyRing.toProtoMessage())
.addAllLanguageCodes(languageCodes)
.setRegistrationDate(registrationDate)
.setRegistrationPubKey(ByteString.copyFrom(registrationPubKey))
.setRegistrationSignature(registrationSignature);
Optional.ofNullable(emailAddress).ifPresent(builder::setEmailAddress);
Optional.ofNullable(info).ifPresent(builder::setInfo);
Optional.ofNullable(extraDataMap).ifPresent(builder::putAllExtraData);
return protobuf.StoragePayload.newBuilder().setMediator(builder).build();
}
public static Mediator fromProto(protobuf.Mediator proto) {
return new Mediator(NodeAddress.fromProto(proto.getNodeAddress()),
PubKeyRing.fromProto(proto.getPubKeyRing()),
new ArrayList<>(proto.getLanguageCodesList()),
proto.getRegistrationDate(),
proto.getRegistrationPubKey().toByteArray(),
proto.getRegistrationSignature(),
ProtoUtil.stringOrNullFromProto(proto.getEmailAddress()),
ProtoUtil.stringOrNullFromProto(proto.getInfo()),
CollectionUtils.isEmpty(proto.getExtraDataMap()) ? null : proto.getExtraDataMap());
}
///////////////////////////////////////////////////////////////////////////////////////////
// API
///////////////////////////////////////////////////////////////////////////////////////////
@Override
public String toString() {
return "Mediator{} " + super.toString();
}
}

View file

@ -0,0 +1,95 @@
/*
* 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 haveno.core.support.dispute.mediation.mediator;
import haveno.common.crypto.KeyRing;
import haveno.core.filter.FilterManager;
import haveno.core.support.dispute.agent.DisputeAgentManager;
import haveno.core.user.User;
import haveno.network.p2p.storage.payload.ProtectedStorageEntry;
import javax.inject.Singleton;
import javax.inject.Inject;
import java.util.List;
@Singleton
public class MediatorManager extends DisputeAgentManager<Mediator> {
@Inject
public MediatorManager(KeyRing keyRing,
MediatorService mediatorService,
User user,
FilterManager filterManager) {
super(keyRing, mediatorService, user, filterManager);
}
@Override
protected List<String> getPubKeyList() {
return List.of("03be5471ff9090d322110d87912eefe89871784b1754d0707fdb917be5d88d3809",
"023736953a5a6638db71d7f78edc38cea0e42143c3b184ee67f331dafdc2c59efa",
"03d82260038253f7367012a4fc0c52dac74cfc67ac9cfbc3c3ad8fca746d8e5fc6",
"02dac85f726121ef333d425bc8e13173b5b365a6444176306e6a0a9e76ae1073bd",
"0342a5b37c8f843c3302e930d0197cdd8948a6f76747c05e138a6671a6a4caf739",
"027afa67c920867a70dfad77db6c6f74051f5af8bf56a1ad479f0bc4005df92325",
"03505f44f1893b64a457f8883afdd60774d7f4def6f82bb6f60be83a4b5b85cf82",
"0277d2d505d28ad67a03b001ef66f0eaaf1184fa87ebeaa937703cec7073cb2e8f",
"027cb3e9a56a438714e2144e2f75db7293ad967f12d5c29b17623efbd35ddbceb0",
"03be5471ff9090d322110d87912eefe89871784b1754d0707fdb917be5d88d3809",
"03756937d33d028eea274a3154775b2bffd076ffcc4a23fe0f9080f8b7fa0dab5b",
"03d8359823a91736cb7aecfaf756872daf258084133c9dd25b96ab3643707c38ca",
"03589ed6ded1a1aa92d6ad38bead13e4ad8ba24c60ca6ed8a8efc6e154e3f60add",
"0356965753f77a9c0e33ca7cc47fd43ce7f99b60334308ad3c11eed3665de79a78",
"031112eb033ebacb635754a2b7163c68270c9171c40f271e70e37b22a2590d3c18");
}
@Override
protected boolean isExpectedInstance(ProtectedStorageEntry data) {
return data.getProtectedStoragePayload() instanceof Mediator;
}
@Override
protected void addAcceptedDisputeAgentToUser(Mediator disputeAgent) {
user.addAcceptedMediator(disputeAgent);
}
@Override
protected void removeAcceptedDisputeAgentFromUser(ProtectedStorageEntry data) {
user.removeAcceptedMediator((Mediator) data.getProtectedStoragePayload());
}
@Override
protected List<Mediator> getAcceptedDisputeAgentsFromUser() {
return user.getAcceptedMediators();
}
@Override
protected void clearAcceptedDisputeAgentsAtUser() {
user.clearAcceptedMediators();
}
@Override
protected Mediator getRegisteredDisputeAgentFromUser() {
return user.getRegisteredMediator();
}
@Override
protected void setRegisteredDisputeAgentAtUser(Mediator disputeAgent) {
user.setRegisteredMediator(disputeAgent);
}
}

View file

@ -0,0 +1,63 @@
/*
* 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 haveno.core.support.dispute.mediation.mediator;
import com.google.inject.Singleton;
import haveno.core.filter.FilterManager;
import haveno.core.support.dispute.agent.DisputeAgentService;
import haveno.network.p2p.NodeAddress;
import haveno.network.p2p.P2PService;
import javax.inject.Inject;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import lombok.extern.slf4j.Slf4j;
@Slf4j
@Singleton
public class MediatorService extends DisputeAgentService<Mediator> {
@Inject
public MediatorService(P2PService p2PService, FilterManager filterManager) {
super(p2PService, filterManager);
}
@Override
protected Set<Mediator> getDisputeAgentSet(List<String> bannedDisputeAgents) {
return p2PService.getDataMap().values().stream()
.filter(data -> data.getProtectedStoragePayload() instanceof Mediator)
.map(data -> (Mediator) data.getProtectedStoragePayload())
.filter(a -> bannedDisputeAgents == null ||
!bannedDisputeAgents.contains(a.getNodeAddress().getFullAddress()))
.collect(Collectors.toSet());
}
@Override
protected List<String> getDisputeAgentsFromFilter() {
return filterManager.getFilter() != null ? filterManager.getFilter().getMediators() : new ArrayList<>();
}
public Map<NodeAddress, Mediator> getMediators() {
return super.getDisputeAgents();
}
}

View file

@ -0,0 +1,123 @@
/*
* 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 haveno.core.support.dispute.messages;
import haveno.common.app.Version;
import haveno.common.proto.ProtoUtil;
import haveno.core.support.SupportType;
import haveno.core.support.dispute.DisputeResult;
import haveno.network.p2p.NodeAddress;
import lombok.EqualsAndHashCode;
import lombok.Value;
import static com.google.common.base.Preconditions.checkArgument;
import java.util.Optional;
import javax.annotation.Nullable;
@Value
@EqualsAndHashCode(callSuper = true)
public final class DisputeClosedMessage extends DisputeMessage {
private final DisputeResult disputeResult;
private final NodeAddress senderNodeAddress;
private final String updatedMultisigHex;
@Nullable
private final String unsignedPayoutTxHex;
private final boolean deferPublishPayout;
public DisputeClosedMessage(DisputeResult disputeResult,
NodeAddress senderNodeAddress,
String uid,
SupportType supportType,
String updatedMultisigHex,
@Nullable String unsignedPayoutTxHex,
boolean deferPublishPayout) {
this(disputeResult,
senderNodeAddress,
uid,
Version.getP2PMessageVersion(),
supportType,
updatedMultisigHex,
unsignedPayoutTxHex,
deferPublishPayout);
}
///////////////////////////////////////////////////////////////////////////////////////////
// PROTO BUFFER
///////////////////////////////////////////////////////////////////////////////////////////
private DisputeClosedMessage(DisputeResult disputeResult,
NodeAddress senderNodeAddress,
String uid,
String messageVersion,
SupportType supportType,
String updatedMultisigHex,
String unsignedPayoutTxHex,
boolean deferPublishPayout) {
super(messageVersion, uid, supportType);
this.disputeResult = disputeResult;
this.senderNodeAddress = senderNodeAddress;
this.updatedMultisigHex = updatedMultisigHex;
this.unsignedPayoutTxHex = unsignedPayoutTxHex;
this.deferPublishPayout = deferPublishPayout;
}
@Override
public protobuf.NetworkEnvelope toProtoNetworkEnvelope() {
protobuf.DisputeClosedMessage.Builder builder = protobuf.DisputeClosedMessage.newBuilder()
.setDisputeResult(disputeResult.toProtoMessage())
.setSenderNodeAddress(senderNodeAddress.toProtoMessage())
.setUid(uid)
.setType(SupportType.toProtoMessage(supportType))
.setUpdatedMultisigHex(updatedMultisigHex)
.setDeferPublishPayout(deferPublishPayout);
Optional.ofNullable(unsignedPayoutTxHex).ifPresent(e -> builder.setUnsignedPayoutTxHex(unsignedPayoutTxHex));
return getNetworkEnvelopeBuilder().setDisputeClosedMessage(builder).build();
}
public static DisputeClosedMessage fromProto(protobuf.DisputeClosedMessage proto, String messageVersion) {
checkArgument(proto.hasDisputeResult(), "DisputeResult must be set");
return new DisputeClosedMessage(DisputeResult.fromProto(proto.getDisputeResult()),
NodeAddress.fromProto(proto.getSenderNodeAddress()),
proto.getUid(),
messageVersion,
SupportType.fromProto(proto.getType()),
proto.getUpdatedMultisigHex(),
ProtoUtil.stringOrNullFromProto(proto.getUnsignedPayoutTxHex()),
proto.getDeferPublishPayout());
}
@Override
public String getTradeId() {
return disputeResult.getTradeId();
}
@Override
public String toString() {
return "DisputeClosedMessage{" +
"\n disputeResult=" + disputeResult +
",\n senderNodeAddress=" + senderNodeAddress +
",\n DisputeClosedMessage.uid='" + uid + '\'' +
",\n messageVersion=" + messageVersion +
",\n supportType=" + supportType +
",\n deferPublishPayout=" + deferPublishPayout +
"\n} " + super.toString();
}
}

View file

@ -0,0 +1,36 @@
/*
* 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 haveno.core.support.dispute.messages;
import haveno.core.support.SupportType;
import haveno.core.support.messages.SupportMessage;
import java.util.concurrent.TimeUnit;
public abstract class DisputeMessage extends SupportMessage {
public static final long TTL = TimeUnit.DAYS.toMillis(15);
public DisputeMessage(String messageVersion, String uid, SupportType supportType) {
super(messageVersion, uid, supportType);
}
@Override
public long getTTL() {
return TTL;
}
}

View file

@ -0,0 +1,113 @@
/*
* 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 haveno.core.support.dispute.messages;
import haveno.common.app.Version;
import haveno.core.proto.CoreProtoResolver;
import haveno.core.support.SupportType;
import haveno.core.support.dispute.Dispute;
import haveno.core.trade.messages.PaymentSentMessage;
import haveno.network.p2p.NodeAddress;
import java.util.Optional;
import lombok.EqualsAndHashCode;
import lombok.Value;
@EqualsAndHashCode(callSuper = true)
@Value
public final class DisputeOpenedMessage extends DisputeMessage {
private final Dispute dispute;
private final NodeAddress senderNodeAddress;
private final String updatedMultisigHex;
private final PaymentSentMessage paymentSentMessage;
public DisputeOpenedMessage(Dispute dispute,
NodeAddress senderNodeAddress,
String uid,
SupportType supportType,
String updatedMultisigHex,
PaymentSentMessage paymentSentMessage) {
this(dispute,
senderNodeAddress,
uid,
Version.getP2PMessageVersion(),
supportType,
updatedMultisigHex,
paymentSentMessage);
}
///////////////////////////////////////////////////////////////////////////////////////////
// PROTO BUFFER
///////////////////////////////////////////////////////////////////////////////////////////
private DisputeOpenedMessage(Dispute dispute,
NodeAddress senderNodeAddress,
String uid,
String messageVersion,
SupportType supportType,
String updatedMultisigHex,
PaymentSentMessage paymentSentMessage) {
super(messageVersion, uid, supportType);
this.dispute = dispute;
this.senderNodeAddress = senderNodeAddress;
this.updatedMultisigHex = updatedMultisigHex;
this.paymentSentMessage = paymentSentMessage;
}
@Override
public protobuf.NetworkEnvelope toProtoNetworkEnvelope() {
protobuf.DisputeOpenedMessage.Builder builder = protobuf.DisputeOpenedMessage.newBuilder()
.setUid(uid)
.setDispute(dispute.toProtoMessage())
.setSenderNodeAddress(senderNodeAddress.toProtoMessage())
.setType(SupportType.toProtoMessage(supportType))
.setUpdatedMultisigHex(updatedMultisigHex);
Optional.ofNullable(paymentSentMessage).ifPresent(e -> builder.setPaymentSentMessage(paymentSentMessage.toProtoNetworkEnvelope().getPaymentSentMessage()));
return getNetworkEnvelopeBuilder().setDisputeOpenedMessage(builder).build();
}
public static DisputeOpenedMessage fromProto(protobuf.DisputeOpenedMessage proto,
CoreProtoResolver coreProtoResolver,
String messageVersion) {
return new DisputeOpenedMessage(Dispute.fromProto(proto.getDispute(), coreProtoResolver),
NodeAddress.fromProto(proto.getSenderNodeAddress()),
proto.getUid(),
messageVersion,
SupportType.fromProto(proto.getType()),
proto.getUpdatedMultisigHex(),
proto.hasPaymentSentMessage() ? PaymentSentMessage.fromProto(proto.getPaymentSentMessage(), messageVersion) : null);
}
@Override
public String getTradeId() {
return dispute.getTradeId();
}
@Override
public String toString() {
return "DisputeOpenedMessage{" +
"\n dispute=" + dispute +
",\n senderNodeAddress=" + senderNodeAddress +
",\n DisputeOpenedMessage.uid='" + uid + '\'' +
",\n messageVersion=" + messageVersion +
",\n supportType=" + supportType +
",\n updatedMultisigHex=" + updatedMultisigHex +
",\n paymentSentMessage=" + paymentSentMessage +
"\n} " + super.toString();
}
}

View file

@ -0,0 +1,74 @@
/*
* 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 haveno.core.support.dispute.refund;
import com.google.protobuf.Message;
import haveno.common.proto.ProtoUtil;
import haveno.core.proto.CoreProtoResolver;
import haveno.core.support.SupportType;
import haveno.core.support.dispute.Dispute;
import haveno.core.support.dispute.DisputeList;
import java.util.Collection;
import java.util.List;
import java.util.stream.Collectors;
import lombok.ToString;
import lombok.extern.slf4j.Slf4j;
import static com.google.common.base.Preconditions.checkArgument;
@Slf4j
@ToString
/*
* Holds a List of refund dispute objects.
*
* Calls to the List are delegated because this class intercepts the add/remove calls so changes
* can be saved to disc.
*/
public final class RefundDisputeList extends DisputeList<Dispute> {
RefundDisputeList() {
super();
}
///////////////////////////////////////////////////////////////////////////////////////////
// PROTO BUFFER
///////////////////////////////////////////////////////////////////////////////////////////
protected RefundDisputeList(Collection<Dispute> collection) {
super(collection);
}
@Override
public Message toProtoMessage() {
forEach(dispute -> checkArgument(dispute.getSupportType().equals(SupportType.REFUND), "Support type has to be REFUND"));
return protobuf.PersistableEnvelope.newBuilder().setRefundDisputeList(protobuf.RefundDisputeList.newBuilder()
.addAllDispute(ProtoUtil.collectionToProto(getList(), protobuf.Dispute.class))).build();
}
public static RefundDisputeList fromProto(protobuf.RefundDisputeList proto,
CoreProtoResolver coreProtoResolver) {
List<Dispute> list = proto.getDisputeList().stream()
.map(disputeProto -> Dispute.fromProto(disputeProto, coreProtoResolver))
.filter(e -> e.getSupportType().equals(SupportType.REFUND))
.collect(Collectors.toList());
return new RefundDisputeList(list);
}
}

View file

@ -0,0 +1,46 @@
/*
* 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 haveno.core.support.dispute.refund;
import haveno.common.persistence.PersistenceManager;
import haveno.core.support.dispute.DisputeListService;
import javax.inject.Inject;
import javax.inject.Singleton;
@Singleton
public final class RefundDisputeListService extends DisputeListService<RefundDisputeList> {
///////////////////////////////////////////////////////////////////////////////////////////
// Constructor
///////////////////////////////////////////////////////////////////////////////////////////
@Inject
public RefundDisputeListService(PersistenceManager<RefundDisputeList> persistenceManager) {
super(persistenceManager);
}
///////////////////////////////////////////////////////////////////////////////////////////
// Implement template methods
///////////////////////////////////////////////////////////////////////////////////////////
@Override
protected RefundDisputeList getConcreteDisputeList() {
return new RefundDisputeList();
}
}

View file

@ -0,0 +1,227 @@
/*
* 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 haveno.core.support.dispute.refund;
import com.google.inject.Inject;
import com.google.inject.Singleton;
import haveno.common.Timer;
import haveno.common.UserThread;
import haveno.common.app.Version;
import haveno.common.config.Config;
import haveno.common.crypto.KeyRing;
import haveno.core.api.CoreMoneroConnectionsService;
import haveno.core.api.CoreNotificationService;
import haveno.core.btc.wallet.TradeWalletService;
import haveno.core.btc.wallet.XmrWalletService;
import haveno.core.locale.Res;
import haveno.core.offer.OpenOffer;
import haveno.core.offer.OpenOfferManager;
import haveno.core.provider.price.PriceFeedService;
import haveno.core.support.SupportType;
import haveno.core.support.dispute.Dispute;
import haveno.core.support.dispute.DisputeManager;
import haveno.core.support.dispute.DisputeResult;
import haveno.core.support.dispute.messages.DisputeClosedMessage;
import haveno.core.support.dispute.messages.DisputeOpenedMessage;
import haveno.core.support.messages.ChatMessage;
import haveno.core.support.messages.SupportMessage;
import haveno.core.trade.ClosedTradableManager;
import haveno.core.trade.Trade;
import haveno.core.trade.TradeManager;
import haveno.network.p2p.AckMessageSourceType;
import haveno.network.p2p.NodeAddress;
import haveno.network.p2p.P2PService;
import java.util.Optional;
import lombok.extern.slf4j.Slf4j;
import javax.annotation.Nullable;
import static com.google.common.base.Preconditions.checkNotNull;
@Slf4j
@Singleton
public final class RefundManager extends DisputeManager<RefundDisputeList> {
///////////////////////////////////////////////////////////////////////////////////////////
// Constructor
///////////////////////////////////////////////////////////////////////////////////////////
@Inject
public RefundManager(P2PService p2PService,
TradeWalletService tradeWalletService,
XmrWalletService walletService,
CoreMoneroConnectionsService connectionService,
CoreNotificationService notificationService,
TradeManager tradeManager,
ClosedTradableManager closedTradableManager,
OpenOfferManager openOfferManager,
// TODO (woodser): remove priceFeedService?
KeyRing keyRing,
RefundDisputeListService refundDisputeListService,
Config config,
PriceFeedService priceFeedService) {
super(p2PService, tradeWalletService, walletService, connectionService, notificationService, tradeManager, closedTradableManager,
openOfferManager, keyRing, refundDisputeListService, config, priceFeedService);
}
///////////////////////////////////////////////////////////////////////////////////////////
// Implement template methods
///////////////////////////////////////////////////////////////////////////////////////////
@Override
public SupportType getSupportType() {
return SupportType.REFUND;
}
@Override
public void onSupportMessage(SupportMessage message) {
if (canProcessMessage(message)) {
log.info("Received {} with tradeId {} and uid {}",
message.getClass().getSimpleName(), message.getTradeId(), message.getUid());
if (message instanceof DisputeOpenedMessage) {
handleDisputeOpenedMessage((DisputeOpenedMessage) message);
} else if (message instanceof ChatMessage) {
handleChatMessage((ChatMessage) message);
} else if (message instanceof DisputeClosedMessage) {
handleDisputeClosedMessage((DisputeClosedMessage) message);
} else {
log.warn("Unsupported message at dispatchMessage. message={}", message);
}
}
}
@Override
protected AckMessageSourceType getAckMessageSourceType() {
return AckMessageSourceType.REFUND_MESSAGE;
}
@Override
public void cleanupDisputes() {
disputeListService.cleanupDisputes(tradeId -> tradeManager.closeDisputedTrade(tradeId, Trade.DisputeState.REFUND_REQUEST_CLOSED));
}
@Override
protected String getDisputeInfo(Dispute dispute) {
String role = Res.get("shared.refundAgent").toLowerCase();
String link = "https://docs.bisq.network/trading-rules.html#arbitration";
return Res.get("support.initialInfo", role, role, link);
}
@Override
protected String getDisputeIntroForPeer(String disputeInfo) {
return Res.get("support.peerOpenedDispute", disputeInfo, Version.VERSION);
}
@Override
protected String getDisputeIntroForDisputeCreator(String disputeInfo) {
return Res.get("support.youOpenedDispute", disputeInfo, Version.VERSION);
}
@Override
protected void addPriceInfoMessage(Dispute dispute, int counter) {
// At refund agent we do not add the option trade price check as the time for dispute opening is not correct.
// In case of an option trade the mediator adds to the result summary message automatically the system message
// with the option trade detection info so the refund agent can see that as well.
}
///////////////////////////////////////////////////////////////////////////////////////////
// Message handler
///////////////////////////////////////////////////////////////////////////////////////////
@Override
// We get that message at both peers. The dispute object is in context of the trader
public void handleDisputeClosedMessage(DisputeClosedMessage disputeResultMessage) {
DisputeResult disputeResult = disputeResultMessage.getDisputeResult();
String tradeId = disputeResult.getTradeId();
ChatMessage chatMessage = disputeResult.getChatMessage();
checkNotNull(chatMessage, "chatMessage must not be null");
Optional<Dispute> disputeOptional = findDispute(disputeResult);
String uid = disputeResultMessage.getUid();
if (!disputeOptional.isPresent()) {
log.warn("We got a dispute result msg but we don't have a matching dispute. " +
"That might happen when we get the disputeResultMessage before the dispute was created. " +
"We try again after 2 sec. to apply the disputeResultMessage. TradeId = " + tradeId);
if (!delayMsgMap.containsKey(uid)) {
// We delay 2 sec. to be sure the comm. msg gets added first
Timer timer = UserThread.runAfter(() -> handleDisputeClosedMessage(disputeResultMessage), 2);
delayMsgMap.put(uid, timer);
} else {
log.warn("We got a dispute result msg after we already repeated to apply the message after a delay. " +
"That should never happen. TradeId = " + tradeId);
}
return;
}
Dispute dispute = disputeOptional.get();
cleanupRetryMap(uid);
if (!dispute.getChatMessages().contains(chatMessage)) {
dispute.addAndPersistChatMessage(chatMessage);
} else {
log.warn("We got a dispute mail msg what we have already stored. TradeId = " + chatMessage.getTradeId());
}
dispute.setIsClosed();
if (dispute.disputeResultProperty().get() != null) {
log.warn("We got already a dispute result. That should only happen if a dispute needs to be closed " +
"again because the first close did not succeed. TradeId = " + tradeId);
}
dispute.setDisputeResult(disputeResult);
Optional<Trade> tradeOptional = tradeManager.getOpenTrade(tradeId);
if (tradeOptional.isPresent()) {
Trade trade = tradeOptional.get();
if (trade.getDisputeState() == Trade.DisputeState.REFUND_REQUESTED ||
trade.getDisputeState() == Trade.DisputeState.REFUND_REQUEST_STARTED_BY_PEER) {
trade.setDisputeState(Trade.DisputeState.REFUND_REQUEST_CLOSED);
tradeManager.requestPersistence();
}
} else {
Optional<OpenOffer> openOfferOptional = openOfferManager.getOpenOfferById(tradeId);
openOfferOptional.ifPresent(openOffer -> openOfferManager.closeOpenOffer(openOffer.getOffer()));
}
sendAckMessage(chatMessage, dispute.getAgentPubKeyRing(), true, null);
// set state after payout as we call swapTradeEntryToAvailableEntry
if (tradeManager.getOpenTrade(tradeId).isPresent()) {
tradeManager.closeDisputedTrade(tradeId, Trade.DisputeState.REFUND_REQUEST_CLOSED);
} else {
Optional<OpenOffer> openOfferOptional = openOfferManager.getOpenOfferById(tradeId);
openOfferOptional.ifPresent(openOffer -> openOfferManager.closeOpenOffer(openOffer.getOffer()));
}
requestPersistence();
}
///////////////////////////////////////////////////////////////////////////////////////////
// API
///////////////////////////////////////////////////////////////////////////////////////////
@Nullable
@Override
public NodeAddress getAgentNodeAddress(Dispute dispute) {
throw new RuntimeException("Refund manager not used in XMR adapation");
//return dispute.getContract().getRefundAgentNodeAddress();
}
}

View file

@ -0,0 +1,33 @@
/*
* 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 haveno.core.support.dispute.refund;
import haveno.common.proto.ProtoUtil;
// todo
public enum RefundResultState {
UNDEFINED_REFUND_RESULT;
public static RefundResultState fromProto(protobuf.RefundResultState refundResultState) {
return ProtoUtil.enumFromProto(RefundResultState.class, refundResultState.name());
}
public static protobuf.RefundResultState toProtoMessage(RefundResultState refundResultState) {
return protobuf.RefundResultState.valueOf(refundResultState.name());
}
}

View file

@ -0,0 +1,31 @@
/*
* 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 haveno.core.support.dispute.refund;
import lombok.extern.slf4j.Slf4j;
import haveno.core.support.dispute.Dispute;
import haveno.core.support.dispute.DisputeSession;
import javax.annotation.Nullable;
@Slf4j
public class RefundSession extends DisputeSession {
public RefundSession(@Nullable Dispute dispute, boolean isTrader) {
super(dispute, isTrader);
}
}

View file

@ -0,0 +1,112 @@
/*
* 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 haveno.core.support.dispute.refund.refundagent;
import com.google.protobuf.ByteString;
import haveno.common.app.Capabilities;
import haveno.common.app.Capability;
import haveno.common.crypto.PubKeyRing;
import haveno.common.proto.ProtoUtil;
import haveno.common.util.CollectionUtils;
import haveno.core.support.dispute.agent.DisputeAgent;
import haveno.network.p2p.NodeAddress;
import haveno.network.p2p.storage.payload.CapabilityRequiringPayload;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import javax.annotation.Nullable;
@EqualsAndHashCode(callSuper = true)
@Slf4j
@Getter
public final class RefundAgent extends DisputeAgent implements CapabilityRequiringPayload {
public RefundAgent(NodeAddress nodeAddress,
PubKeyRing pubKeyRing,
List<String> languageCodes,
long registrationDate,
byte[] registrationPubKey,
String registrationSignature,
@Nullable String emailAddress,
@Nullable String info,
@Nullable Map<String, String> extraDataMap) {
super(nodeAddress,
pubKeyRing,
languageCodes,
registrationDate,
registrationPubKey,
registrationSignature,
emailAddress,
info,
extraDataMap);
}
///////////////////////////////////////////////////////////////////////////////////////////
// PROTO BUFFER
///////////////////////////////////////////////////////////////////////////////////////////
@Override
public protobuf.StoragePayload toProtoMessage() {
protobuf.RefundAgent.Builder builder = protobuf.RefundAgent.newBuilder()
.setNodeAddress(nodeAddress.toProtoMessage())
.setPubKeyRing(pubKeyRing.toProtoMessage())
.addAllLanguageCodes(languageCodes)
.setRegistrationDate(registrationDate)
.setRegistrationPubKey(ByteString.copyFrom(registrationPubKey))
.setRegistrationSignature(registrationSignature);
Optional.ofNullable(emailAddress).ifPresent(builder::setEmailAddress);
Optional.ofNullable(info).ifPresent(builder::setInfo);
Optional.ofNullable(extraDataMap).ifPresent(builder::putAllExtraData);
return protobuf.StoragePayload.newBuilder().setRefundAgent(builder).build();
}
public static RefundAgent fromProto(protobuf.RefundAgent proto) {
return new RefundAgent(NodeAddress.fromProto(proto.getNodeAddress()),
PubKeyRing.fromProto(proto.getPubKeyRing()),
new ArrayList<>(proto.getLanguageCodesList()),
proto.getRegistrationDate(),
proto.getRegistrationPubKey().toByteArray(),
proto.getRegistrationSignature(),
ProtoUtil.stringOrNullFromProto(proto.getEmailAddress()),
ProtoUtil.stringOrNullFromProto(proto.getInfo()),
CollectionUtils.isEmpty(proto.getExtraDataMap()) ? null : proto.getExtraDataMap());
}
///////////////////////////////////////////////////////////////////////////////////////////
// API
///////////////////////////////////////////////////////////////////////////////////////////
@Override
public String toString() {
return "RefundAgent{} " + super.toString();
}
@Override
public Capabilities getRequiredCapabilities() {
return new Capabilities(Capability.REFUND_AGENT);
}
}

View file

@ -0,0 +1,99 @@
/*
* 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 haveno.core.support.dispute.refund.refundagent;
import haveno.common.crypto.KeyRing;
import haveno.core.filter.FilterManager;
import haveno.core.support.dispute.agent.DisputeAgentManager;
import haveno.core.user.User;
import haveno.network.p2p.storage.payload.ProtectedStorageEntry;
import javax.inject.Inject;
import javax.inject.Singleton;
import java.util.List;
import lombok.extern.slf4j.Slf4j;
@Slf4j
@Singleton
public class RefundAgentManager extends DisputeAgentManager<RefundAgent> {
@Inject
public RefundAgentManager(KeyRing keyRing,
RefundAgentService refundAgentService,
User user,
FilterManager filterManager) {
super(keyRing, refundAgentService, user, filterManager);
}
@Override
protected List<String> getPubKeyList() {
return List.of("02a25798e256b800d7ea71c31098ac9a47cb20892176afdfeb051f5ded382d44af",
"0360455d3cffe00ef73cc1284c84eedacc8c5c3374c43f4aac8ffb95f5130b9ef5",
"03b0513afbb531bc4551b379eba027feddd33c92b5990fd477b0fa6eff90a5b7db",
"03533fd75fda29c351298e50b8ea696656dcb8ce4e263d10618c6901a50450bf0e",
"028124436482aa4c61a4bc4097d60c80b09f4285413be3b023a37a0164cbd5d818",
"0384fcf883116d8e9469720ed7808cc4141f6dc6a5ed23d76dd48f2f5f255590d7",
"029bd318ecee4e212ff06a4396770d600d72e9e0c6532142a428bdb401491e9721",
"02e375b4b24d0a858953f7f94666667554d41f78000b9c8a301294223688b29011",
"0232c088ae7c070de89d2b6c8d485b34bf0e3b2a964a2c6622f39ca501260c23f7",
"033e047f74f2aa1ce41e8c85731f97ab83d448d65dc8518ab3df4474a5d53a3d19",
"02f52a8cf373c8cbddb318e523b7f111168bf753fdfb6f8aa81f88c950ede3a5ce",
"039784029922c54bcd0f0e7f14530f586053a5f4e596e86b3474cd7404657088ae",
"037969f9d5ab2cc609104c6e61323df55428f8f108c11aab7c7b5f953081d39304",
"031bd37475b8c5615ac46d6816e791c59d806d72a0bc6739ae94e5fe4545c7f8a6",
"021bb92c636feacf5b082313eb071a63dfcd26501a48b3cd248e35438e5afb7daf");
}
@Override
protected boolean isExpectedInstance(ProtectedStorageEntry data) {
return data.getProtectedStoragePayload() instanceof RefundAgent;
}
@Override
protected void addAcceptedDisputeAgentToUser(RefundAgent disputeAgent) {
user.addAcceptedRefundAgent(disputeAgent);
}
@Override
protected void removeAcceptedDisputeAgentFromUser(ProtectedStorageEntry data) {
user.removeAcceptedRefundAgent((RefundAgent) data.getProtectedStoragePayload());
}
@Override
protected List<RefundAgent> getAcceptedDisputeAgentsFromUser() {
return user.getAcceptedRefundAgents();
}
@Override
protected void clearAcceptedDisputeAgentsAtUser() {
user.clearAcceptedRefundAgents();
}
@Override
protected RefundAgent getRegisteredDisputeAgentFromUser() {
return user.getRegisteredRefundAgent();
}
@Override
protected void setRegisteredDisputeAgentAtUser(RefundAgent disputeAgent) {
user.setRegisteredRefundAgent(disputeAgent);
}
}

View file

@ -0,0 +1,58 @@
/*
* 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 haveno.core.support.dispute.refund.refundagent;
import com.google.inject.Singleton;
import haveno.core.filter.FilterManager;
import haveno.core.support.dispute.agent.DisputeAgentService;
import haveno.network.p2p.NodeAddress;
import haveno.network.p2p.P2PService;
import javax.inject.Inject;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
@Singleton
public class RefundAgentService extends DisputeAgentService<RefundAgent> {
@Inject
public RefundAgentService(P2PService p2PService, FilterManager filterManager) {
super(p2PService, filterManager);
}
@Override
protected Set<RefundAgent> getDisputeAgentSet(List<String> bannedDisputeAgents) {
return p2PService.getDataMap().values().stream()
.filter(data -> data.getProtectedStoragePayload() instanceof RefundAgent)
.map(data -> (RefundAgent) data.getProtectedStoragePayload())
.filter(a -> bannedDisputeAgents == null ||
!bannedDisputeAgents.contains(a.getNodeAddress().getFullAddress()))
.collect(Collectors.toSet());
}
@Override
protected List<String> getDisputeAgentsFromFilter() {
return filterManager.getFilter() != null ? filterManager.getFilter().getRefundAgents() : new ArrayList<>();
}
public Map<NodeAddress, RefundAgent> getRefundAgents() {
return super.getDisputeAgents();
}
}

View file

@ -0,0 +1,381 @@
/*
* 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 haveno.core.support.messages;
import haveno.common.app.Version;
import haveno.common.util.Utilities;
import haveno.core.support.SupportType;
import haveno.core.support.dispute.Attachment;
import haveno.core.support.dispute.Dispute;
import haveno.core.support.dispute.DisputeResult;
import haveno.network.p2p.NodeAddress;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.ReadOnlyBooleanProperty;
import javafx.beans.property.ReadOnlyStringProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import java.lang.ref.WeakReference;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import javax.annotation.Nullable;
/* Message for direct communication between two nodes. Originally built for trader to
* arbitrator communication as no other direct communication was allowed. Arbitrator is
* considered as the server and trader as the client in arbitration chats
*
* For trader to trader communication the maker is considered to be the server
* and the taker is considered as the client.
* */
@EqualsAndHashCode(callSuper = true) // listener is transient and therefore excluded anyway
@Getter
@Slf4j
public final class ChatMessage extends SupportMessage {
public static final long TTL = TimeUnit.DAYS.toMillis(7);
public interface Listener {
void onMessageStateChanged();
}
private final String tradeId;
private final int traderId;
// This is only used for the server client relationship
// If senderIsTrader == true then the sender is the client
private final boolean senderIsTrader;
private final String message;
private final ArrayList<Attachment> attachments = new ArrayList<>();
private final NodeAddress senderNodeAddress;
private final long date;
@Setter
private boolean isSystemMessage;
// Added in v1.1.6. for trader chat to store if message was shown in popup
@Setter
private boolean wasDisplayed;
//todo move to base class
private final BooleanProperty arrivedProperty;
private final BooleanProperty storedInMailboxProperty;
private final BooleanProperty acknowledgedProperty;
private final StringProperty sendMessageErrorProperty;
private final StringProperty ackErrorProperty;
transient private WeakReference<Listener> listener;
public ChatMessage(SupportType supportType,
String tradeId,
int traderId,
boolean senderIsTrader,
String message,
NodeAddress senderNodeAddress) {
this(supportType,
tradeId,
traderId,
senderIsTrader,
message,
null,
senderNodeAddress,
new Date().getTime(),
false,
false,
UUID.randomUUID().toString(),
Version.getP2PMessageVersion(),
false,
null,
null,
false);
}
public ChatMessage(SupportType supportType,
String tradeId,
int traderId,
boolean senderIsTrader,
String message,
NodeAddress senderNodeAddress,
ArrayList<Attachment> attachments) {
this(supportType,
tradeId,
traderId,
senderIsTrader,
message,
attachments,
senderNodeAddress,
new Date().getTime(),
false,
false,
UUID.randomUUID().toString(),
Version.getP2PMessageVersion(),
false,
null,
null,
false);
}
public ChatMessage(SupportType supportType,
String tradeId,
int traderId,
boolean senderIsTrader,
String message,
NodeAddress senderNodeAddress,
long date) {
this(supportType,
tradeId,
traderId,
senderIsTrader,
message,
null,
senderNodeAddress,
date,
false,
false,
UUID.randomUUID().toString(),
Version.getP2PMessageVersion(),
false,
null,
null,
false);
}
///////////////////////////////////////////////////////////////////////////////////////////
// PROTO BUFFER
///////////////////////////////////////////////////////////////////////////////////////////
private ChatMessage(SupportType supportType,
String tradeId,
int traderId,
boolean senderIsTrader,
String message,
@Nullable List<Attachment> attachments,
NodeAddress senderNodeAddress,
long date,
boolean arrived,
boolean storedInMailbox,
String uid,
String messageVersion,
boolean acknowledged,
@Nullable String sendMessageError,
@Nullable String ackError,
boolean wasDisplayed) {
super(messageVersion, uid, supportType);
this.tradeId = tradeId;
this.traderId = traderId;
this.senderIsTrader = senderIsTrader;
this.message = message;
this.wasDisplayed = wasDisplayed;
Optional.ofNullable(attachments).ifPresent(e -> addAllAttachments(attachments));
this.senderNodeAddress = senderNodeAddress;
this.date = date;
arrivedProperty = new SimpleBooleanProperty(arrived);
storedInMailboxProperty = new SimpleBooleanProperty(storedInMailbox);
acknowledgedProperty = new SimpleBooleanProperty(acknowledged);
sendMessageErrorProperty = new SimpleStringProperty(sendMessageError);
ackErrorProperty = new SimpleStringProperty(ackError);
notifyChangeListener();
}
public protobuf.ChatMessage.Builder toProtoChatMessageBuilder() {
protobuf.ChatMessage.Builder builder = protobuf.ChatMessage.newBuilder()
.setType(SupportType.toProtoMessage(supportType))
.setTradeId(tradeId)
.setTraderId(traderId)
.setSenderIsTrader(senderIsTrader)
.setMessage(message)
.addAllAttachments(attachments.stream().map(Attachment::toProtoMessage).collect(Collectors.toList()))
.setSenderNodeAddress(senderNodeAddress.toProtoMessage())
.setDate(date)
.setArrived(arrivedProperty.get())
.setStoredInMailbox(storedInMailboxProperty.get())
.setIsSystemMessage(isSystemMessage)
.setUid(uid)
.setAcknowledged(acknowledgedProperty.get())
.setWasDisplayed(wasDisplayed);
Optional.ofNullable(sendMessageErrorProperty.get()).ifPresent(builder::setSendMessageError);
Optional.ofNullable(ackErrorProperty.get()).ifPresent(builder::setAckError);
return builder;
}
// We cannot rename protobuf definition because it would break backward compatibility
@Override
public protobuf.NetworkEnvelope toProtoNetworkEnvelope() {
protobuf.ChatMessage.Builder builder = toProtoChatMessageBuilder();
return getNetworkEnvelopeBuilder()
.setChatMessage(builder)
.build();
}
// The protobuf definition ChatMessage cannot be changed as it would break backward compatibility.
public static ChatMessage fromProto(protobuf.ChatMessage proto,
String messageVersion) {
// If we get a msg from an old client type will be ordinal 0 which is the dispute entry and as we only added
// the trade case it is the desired behaviour.
final ChatMessage chatMessage = new ChatMessage(
SupportType.fromProto(proto.getType()),
proto.getTradeId(),
proto.getTraderId(),
proto.getSenderIsTrader(),
proto.getMessage(),
new ArrayList<>(proto.getAttachmentsList().stream().map(Attachment::fromProto).collect(Collectors.toList())),
NodeAddress.fromProto(proto.getSenderNodeAddress()),
proto.getDate(),
proto.getArrived(),
proto.getStoredInMailbox(),
proto.getUid(),
messageVersion,
proto.getAcknowledged(),
proto.getSendMessageError().isEmpty() ? null : proto.getSendMessageError(),
proto.getAckError().isEmpty() ? null : proto.getAckError(),
proto.getWasDisplayed());
chatMessage.setSystemMessage(proto.getIsSystemMessage());
return chatMessage;
}
public static ChatMessage fromPayloadProto(protobuf.ChatMessage proto) {
// We have the case that an envelope got wrapped into a payload.
// We don't check the message version here as it was checked in the carrier envelope already (in connection class)
// Payloads don't have a message version and are also used for persistence
// We set the value to -1 to indicate it is set but irrelevant
return fromProto(proto, "-1");
}
///////////////////////////////////////////////////////////////////////////////////////////
// API
///////////////////////////////////////////////////////////////////////////////////////////
private void addAllAttachments(List<Attachment> attachments) {
this.attachments.addAll(attachments);
}
public void setArrived(@SuppressWarnings("SameParameterValue") boolean arrived) {
this.arrivedProperty.set(arrived);
notifyChangeListener();
}
public ReadOnlyBooleanProperty arrivedProperty() {
return arrivedProperty;
}
public void setStoredInMailbox(@SuppressWarnings("SameParameterValue") boolean storedInMailbox) {
this.storedInMailboxProperty.set(storedInMailbox);
notifyChangeListener();
}
public ReadOnlyBooleanProperty storedInMailboxProperty() {
return storedInMailboxProperty;
}
public void setAcknowledged(boolean acknowledged) {
this.acknowledgedProperty.set(acknowledged);
notifyChangeListener();
}
public ReadOnlyBooleanProperty acknowledgedProperty() {
return acknowledgedProperty;
}
public void setSendMessageError(String sendMessageError) {
this.sendMessageErrorProperty.set(sendMessageError);
notifyChangeListener();
}
public ReadOnlyStringProperty sendMessageErrorProperty() {
return sendMessageErrorProperty;
}
public void setAckError(String ackError) {
this.ackErrorProperty.set(ackError);
notifyChangeListener();
}
public ReadOnlyStringProperty ackErrorProperty() {
return ackErrorProperty;
}
@Override
public String getTradeId() {
return tradeId;
}
public String getShortId() {
return Utilities.getShortId(tradeId);
}
public void addWeakMessageStateListener(Listener listener) {
this.listener = new WeakReference<>(listener);
}
public boolean isResultMessage(Dispute dispute) {
DisputeResult disputeResult = dispute.getDisputeResultProperty().get();
if (disputeResult == null) {
return false;
}
ChatMessage resultChatMessage = disputeResult.getChatMessage();
return resultChatMessage != null && resultChatMessage.getUid().equals(uid);
}
@Override
public long getTTL() {
return TTL;
}
private void notifyChangeListener() {
if (listener != null) {
Listener listener = this.listener.get();
if (listener != null) {
listener.onMessageStateChanged();
}
}
}
@Override
public String toString() {
return "ChatMessage{" +
"\n tradeId='" + tradeId + '\'' +
",\n traderId=" + traderId +
",\n senderIsTrader=" + senderIsTrader +
",\n message='" + message + '\'' +
",\n attachments=" + attachments +
",\n senderNodeAddress=" + senderNodeAddress +
",\n date=" + date +
",\n isSystemMessage=" + isSystemMessage +
",\n wasDisplayed=" + wasDisplayed +
",\n arrivedProperty=" + arrivedProperty +
",\n storedInMailboxProperty=" + storedInMailboxProperty +
",\n acknowledgedProperty=" + acknowledgedProperty +
",\n sendMessageErrorProperty=" + sendMessageErrorProperty +
",\n ackErrorProperty=" + ackErrorProperty +
"\n} " + super.toString();
}
}

View file

@ -0,0 +1,51 @@
/*
* 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 haveno.core.support.messages;
import haveno.common.proto.network.NetworkEnvelope;
import haveno.core.support.SupportType;
import haveno.network.p2p.UidMessage;
import haveno.network.p2p.mailbox.MailboxMessage;
import lombok.EqualsAndHashCode;
import lombok.Getter;
@EqualsAndHashCode(callSuper = true)
@Getter
public abstract class SupportMessage extends NetworkEnvelope implements MailboxMessage, UidMessage {
protected final String uid;
// Added with v1.1.6. Old clients will not have set that field and we fall back to entry 0 which is ARBITRATION.
protected final SupportType supportType;
public SupportMessage(String messageVersion, String uid, SupportType supportType) {
super(messageVersion);
this.uid = uid;
this.supportType = supportType;
}
public abstract String getTradeId();
@Override
public String toString() {
return "DisputeMessage{" +
"\n uid='" + uid + '\'' +
",\n messageVersion=" + messageVersion +
",\n supportType=" + supportType +
"\n} " + super.toString();
}
}

View file

@ -0,0 +1,73 @@
/*
* 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 haveno.core.support.traderchat;
import haveno.core.support.SupportSession;
import haveno.core.support.messages.ChatMessage;
import haveno.core.trade.Trade;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import lombok.extern.slf4j.Slf4j;
import javax.annotation.Nullable;
@Slf4j
public class TradeChatSession extends SupportSession {
@Nullable
private Trade trade;
public TradeChatSession(@Nullable Trade trade,
boolean isClient) {
super(isClient);
this.trade = trade;
}
@Override
public String getTradeId() {
return trade != null ? trade.getId() : "";
}
@Override
public int getClientId() {
// TODO remove that client-server concept for trade chat
// Get pubKeyRing of taker. Maker is considered server for chat sessions
try {
return trade.getContract().getTakerPubKeyRing().hashCode();
} catch (NullPointerException e) {
log.warn("Unable to get takerPubKeyRing from Trade Contract - {}", e.toString());
}
return 0;
}
@Override
public ObservableList<ChatMessage> getObservableChatMessageList() {
return trade != null ? trade.getChatMessages() : FXCollections.observableArrayList();
}
@Override
public boolean chatIsOpen() {
return trade != null && trade.getState() != Trade.State.TRADE_COMPLETED;
}
@Override
public boolean isDisputeAgent() {
return false;
}
}

View file

@ -0,0 +1,175 @@
/*
* 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 haveno.core.support.traderchat;
import haveno.common.crypto.PubKeyRing;
import haveno.common.crypto.PubKeyRingProvider;
import haveno.core.api.CoreMoneroConnectionsService;
import haveno.core.api.CoreNotificationService;
import haveno.core.locale.Res;
import haveno.core.support.SupportManager;
import haveno.core.support.SupportType;
import haveno.core.support.messages.ChatMessage;
import haveno.core.support.messages.SupportMessage;
import haveno.core.trade.Trade;
import haveno.core.trade.TradeManager;
import haveno.network.p2p.AckMessageSourceType;
import haveno.network.p2p.NodeAddress;
import haveno.network.p2p.P2PService;
import javax.inject.Inject;
import javax.inject.Singleton;
import javafx.collections.ObservableList;
import java.util.List;
import java.util.stream.Collectors;
import lombok.extern.slf4j.Slf4j;
@Slf4j
@Singleton
public class TraderChatManager extends SupportManager {
private final PubKeyRingProvider pubKeyRingProvider;
///////////////////////////////////////////////////////////////////////////////////////////
// Constructor
///////////////////////////////////////////////////////////////////////////////////////////
@Inject
public TraderChatManager(P2PService p2PService,
CoreMoneroConnectionsService connectionService,
CoreNotificationService notificationService,
TradeManager tradeManager,
PubKeyRingProvider pubKeyRingProvider) {
super(p2PService, connectionService, notificationService, tradeManager);
this.pubKeyRingProvider = pubKeyRingProvider;
}
///////////////////////////////////////////////////////////////////////////////////////////
// Implement template methods
///////////////////////////////////////////////////////////////////////////////////////////
@Override
public SupportType getSupportType() {
return SupportType.TRADE;
}
@Override
public void requestPersistence() {
tradeManager.requestPersistence();
}
@Override
public NodeAddress getPeerNodeAddress(ChatMessage message) {
return tradeManager.getOpenTrade(message.getTradeId()).map(trade -> {
if (trade.getContract() != null) {
return trade.getContract().getPeersNodeAddress(pubKeyRingProvider.get());
} else {
return null;
}
}).orElse(null);
}
@Override
public PubKeyRing getPeerPubKeyRing(ChatMessage message) {
return tradeManager.getOpenTrade(message.getTradeId()).map(trade -> {
if (trade.getContract() != null) {
return trade.getContract().getPeersPubKeyRing(pubKeyRingProvider.get());
} else {
return null;
}
}).orElse(null);
}
@Override
public List<ChatMessage> getAllChatMessages() {
return tradeManager.getObservableList().stream()
.flatMap(trade -> trade.getChatMessages().stream())
.collect(Collectors.toList());
}
@Override
public boolean channelOpen(ChatMessage message) {
return tradeManager.getOpenTrade(message.getTradeId()).isPresent();
}
@Override
public void addAndPersistChatMessage(ChatMessage message) {
tradeManager.getOpenTrade(message.getTradeId()).ifPresent(trade -> {
ObservableList<ChatMessage> chatMessages = trade.getChatMessages();
if (chatMessages.stream().noneMatch(m -> m.getUid().equals(message.getUid()))) {
if (chatMessages.isEmpty()) {
addSystemMsg(trade);
}
trade.addAndPersistChatMessage(message);
tradeManager.requestPersistence();
} else {
log.warn("Trade got a chatMessage that we have already stored. UId = {} TradeId = {}",
message.getUid(), message.getTradeId());
}
});
}
@Override
protected AckMessageSourceType getAckMessageSourceType() {
return AckMessageSourceType.TRADE_CHAT_MESSAGE;
}
///////////////////////////////////////////////////////////////////////////////////////////
// API
///////////////////////////////////////////////////////////////////////////////////////////
@Override
public void onAllServicesInitialized() {
super.onAllServicesInitialized();
tryApplyMessages();
}
@Override
public void onSupportMessage(SupportMessage message) {
if (canProcessMessage(message)) {
log.info("Received {} with tradeId {} and uid {}",
message.getClass().getSimpleName(), message.getTradeId(), message.getUid());
if (message instanceof ChatMessage) {
handleChatMessage((ChatMessage) message);
} else {
log.warn("Unsupported message at dispatchMessage. message={}", message);
}
}
}
public void addSystemMsg(Trade trade) {
// We need to use the trade date as otherwise our system msg would not be displayed first as the list is sorted
// by date.
ChatMessage chatMessage = new ChatMessage(
getSupportType(),
trade.getId(),
0,
false,
Res.get("tradeChat.rules"),
new NodeAddress("null:0000"),
trade.getDate().getTime());
chatMessage.setSystemMessage(true);
trade.getChatMessages().add(chatMessage);
requestPersistence();
}
}