mirror of
https://github.com/haveno-dex/haveno.git
synced 2025-07-27 00:45:23 -04:00
rename all packages and other names from bisq to haveno
This commit is contained in:
parent
ab0b9e3b77
commit
1a1fb130c0
1775 changed files with 14575 additions and 16767 deletions
411
core/src/main/java/haveno/core/support/SupportManager.java
Normal file
411
core/src/main/java/haveno/core/support/SupportManager.java
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
54
core/src/main/java/haveno/core/support/SupportSession.java
Normal file
54
core/src/main/java/haveno/core/support/SupportSession.java
Normal 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();
|
||||
}
|
36
core/src/main/java/haveno/core/support/SupportType.java
Normal file
36
core/src/main/java/haveno/core/support/SupportType.java
Normal 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());
|
||||
}
|
||||
}
|
|
@ -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());
|
||||
}
|
||||
}
|
544
core/src/main/java/haveno/core/support/dispute/Dispute.java
Normal file
544
core/src/main/java/haveno/core/support/dispute/Dispute.java
Normal 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}";
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
1105
core/src/main/java/haveno/core/support/dispute/DisputeManager.java
Normal file
1105
core/src/main/java/haveno/core/support/dispute/DisputeManager.java
Normal file
File diff suppressed because it is too large
Load diff
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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}";
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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"));
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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}";
|
||||
}
|
||||
}
|
|
@ -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";
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
|
@ -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"));
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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";
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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());
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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());
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
381
core/src/main/java/haveno/core/support/messages/ChatMessage.java
Normal file
381
core/src/main/java/haveno/core/support/messages/ChatMessage.java
Normal 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();
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue