mirror of
https://github.com/bisq-network/bisq.git
synced 2024-11-19 09:52:23 +01:00
Add mediation support (#3221)
* Refactoring: Move arbitration package inside dispute package * Use abstract base class DisputeResolver for arbitrator * Refactoring: Move mediator to mediator package. * Let Mediator inherit DisputeResolver. * Do not use protobuf inheritance - Do not use protobuf inheritance for Arbitrator and Mediator as it would break backward compatibility (and protobuf inheritance sucks anyway) * Refactoring: Move ArbitratorModule to parent package * Refactoring: Rename ArbitratorModule to DisputeModule * Add mediators to Filter * Add mediators to filter window * Use abstract DisputeResolverService as base class for ArbitratorService - Add common base class for ArbitratorService and MediatorService * Fix test * Use abstract DisputeResolverManager as base class for ArbitratorManager - Add common base class for ArbitratorManager and MediatorManager * Refactor: Move arbitratorregistration package inside register pkg * Refactor: Rename arbitratorregistration package to arbitrator * Add registration view for mediator - With cmd+D one can open the mediator regisration in account screen. For arbitrator its cmd+R * Separate pub key list for mediator (no new keys added yet) * Set new pubkeys for mediator registration - Before release set new keys from maintainer who manages keys * Set disputes @Nullable. Add null checks * Remove pre v0.9 handling for supported arbitrators from offer - We changed handling of arbitrator selection with v0.9 so the supported arbitrators in the offer is not used anymore. As we enforced v1.2 a while back for trading we can be sure no pre v0.9 clients are used anymore and we can remove the optional code part. * Remove supported arbitrators info in offer details window - As we do not use supported arbitraors in offer anymore since v0.9 we can remove that. * Remove check for matching arbitrator languages As we do not use the supported arbitratos from offer since v0.9 we can remove that check. * Remove not used classes * Remove checks for arbitrator and mediator in offer We do not use those fields anymore. We still need to keep the fields not nullable as old clients have the check still. * Add check if sig of proto object is not empty We got in dev testing sometimes an empty protobuf Alert. Might be caused from protobuf copatibility issues during development but not 100% clear. As it causes an exception and corrupted user db file we prefer to set it to null. * Remove TakerSelectMediator This is not used anymore. Currently we would get an exception in the trade but with follow up changes we will fix that... Mediator handling and selection will be done the same way like arbitrator. The current mediator handling was a relict from earlier partial support for mediators which never got completed. As still a null check is in place we need to ensure backward compatibility. * Set arbitratorNodeAddresses and mediatorNodeAddresses to deprecated We do not use arbitratorNodeAddresses and mediatorNodeAddresses anymore but as there is a null check we still need to keep the field ans set it to an empty arrayList. * Make ArbitratorSelection generic. Add MEDIATOR_ADDRESS We want to use the same selection algorithm for mediators as for arbitrators, so we make ArbitratorSelection generic. We add MEDIATOR_ADDRESS as extraMap entry to TradeStatistics2 to be able to track number of trades with specific mediators. ExtraMap is used to add new data to existing protobuf definitions which is supported also by not updated clients. Adding a new protobuf field would only be supported by new clients. As mediator support is a new feature we could add a new field but to keep it in the same style like arbitrator we prefer to use the map here as well. * Refactor: Rename ArbitratorSelection to DisputeResolverSelection * Add mediator to OfferAvailabilityResponse and mediatorNodeAddress to OpenOffer WIP for supporting mediator selection the same way like arbitrators. * Make arbitrator not nullable We can ensure that all users are post v0.9 so we can remove the nullable support. * Add selectedMediator to OfferAvailabilityModel Remove nullable support in ProcessOfferAvailabilityResponse as we can ensure all clients are post v0.9 * Refactor: Rename method * Add todo for using more generic keys for display strings * Refactor: Rename method * Fix wrong handling of registeredMediator Fix copy/paste error * Add mediatorNodeAddress to trade * Handle nullable mediator in ProcessOfferAvailabilityResponse We do not get the mediator set from old clients but we expect a not null value so we use the DisputeResolverSelection in case it is null. We need to pass mediatorManager and tradeStatisticsManager to the OfferAvailabilityModel. * Change log level, cleanup * Revert changes in OfferPayload due backward compatibility issues Because of backward compatibility issues we needed to revert the removal of arbitratorNodeAddresses and mediatorNodeAddresses. The signature check for the offer would fail as an old client would send a not-empty list but new clients would have had an empty list, so the hash would be different and the sig check fail and we would not accept that offer. That is the reason why we still need to support those data even it is not used anymore. This is one of the more tricky cases for backward compatibility issues. This version now is tested between new and old clients and trade and disputes work. * Add checks if any mediator is available * Cleanup classes * Fix test * Add mediator DisputeStates Add isMediationDispute to Dispute class. If a dispute opening gets requested we check if state is DisputeState.NO_DISPUTE and the open mediation. If state is DisputeState.MEDIATION_REQUESTED we open arbitration. * Cleanup; support isMediationDispute * Handle mediator data in Dispute domain - Add getConflictResolverNodeAddress method to Dispute to resolve arbitrator or mediator address based on isMediationDispute flag. - Rename arbitratorPubKeyRing to conflictResolverPubKeyRing in Dispute. We cannot rename arbitratorPubKeyRing in the protobuf definition as it would break backward compatibility. * Add support for mediation in dispute domain - Add isMediationDispute method to ChatSession - Add isMediationDispute method to DisputeCommunicationMessage - Add isMediationDispute to dispute id - Refactor findDispute method - Add null checks - Cleanups * Remove impossible case Reserved and locked funds are used for offers and trades only. * Fix typos * Handle mediator and arbitrator strings - Work in progress of adjusting correct terms. - Cleanups * Refactor: Rename arbitrator package to disputeresolvers * Refactor: Rename ArbitratorDisputeView classes to DisputeResolverView * Add support for close ticket from mediator (WIP) In mediator case we do not create any transaction but only send the dispute result which contains the mediators recommended payout distribution. At teh traders we set the disputeState in the trade to closed. This will be used in the next commits to update the trade so that the traders get displayed the recommended payout and get asked if they agree to that. * Refactoring: Rename class Rename MessageDeliveryFailedException to DisputeMessageDeliveryFailedException * Refactoring: Move dispute classes to dispute package * Refactoring: Move Attachment class to dispute package * Refactoring: Move package one level up Move bisq.core.dispute.arbitration.messages to bisq.core.dispute.messages * Add todo comment * Use ARBITRATION instead of DISPUTE * Make DisputeManager abstract base class for ArbitrationDisputeManager WIP for separating DisputeManager to ArbitrationDisputeManager and MediationDisputeManager * Add MediationDisputeManager * Add MediationDisputeManager and ArbitrationDisputeManager to test * Add mediationDisputeManager to relevant classes There are some cases where arbitrationDisputeManager only is used. Those are usually related to the payout tx. As mediators do not do a payout we don't need it there. * Add TradersArbitrationDisputeView and TradersMediationDisputeView WIP for separating TraderDisputeView * Refactor: Rename class * Refactor: Rename support.tab.support to support.tab.mediation.support I am aware that committing non default translation files is not recommended, but I think in that case it helps to avoid to show errors for developers who use non-english locale. The changes will be overwritten by transifex once it gets synced... * Add DisputeView as common base class Further refactor separation of diff. dispute views * Refactor: Rename package * Refactor: Rename DisputesView to SupportView * Refactor: Rename package * Add MediationDisputeManager to CorePersistedDataHost * Add MediationDisputeList as db file, refactor DisputeList WIP for making Dispute domain more generic. We want to separate arbitration and mediation clearly. * Further refactoring to split mediation and arbitration * Further refactoring to split mediation and arbitration Move methods used for arbitration only to ArbitrationDisputeManager * Refactor: Rename package Rename bisq.core.dispute to bisq.core.support No other changes in that commit. We want to improve the data structure with the trader chat. Support will be the top level. Then dispute containing arbitration and mediation. Next to dispute will be trader chat. bisq.core.support bisq.core.support.dispute.arbitration bisq.core.support.dispute.mediation bisq.core.support.traderchat (not happy with name for that yet) * Refactor: Move dispute domain classes into isq.core.support.dispute package * Refactor: Move classes Move bisq.core.chat.ChatSession to bisq.core.support.ChatSession Move bisq.core.chat.ChatManager to bisq.core.support.ChatManager Move bisq.core.trade.TradeChatSession to bisq.core.support.traderchat.TradeChatSession * Refactor: Move DisputeCommunicationMessage * Refactor: Rename DisputeCommunicationMessage to ChatMessage * Add comments * Refactor: Move class * Refactor: Rename class * Refactor: Rename addDisputeCommunicationMessage and strings and variables Rename disputeCommunicationMessage to chatMessage * Refactor: Rename method * Refactor: Rename methods and strings * Add ArbitrationChatMessage and DisputeChatMessage * Refactor: Rename class * Move ChatMessage.Type to SupportType Add to all supportMessages the SupportType so that we can filter in our chatSessions the messages we are interested in. * Refactor: Move classed to new package * Refactor: Rename package * Refactor: Move classed to new package * Refactor: Move classed to new package * Refactor: Rename classes * Refactor: Rename package * Refactor: Rename classes * Refactor: Rename classes * Remove empty DisputeModule * Refactor: Rename classes * Refactor SupportManager domain (WIP) * Refactor SupportSession domain (WIP) * Remove methods from SupportSession * Dont expose p2pService in SupportManager * Remove supportType in SupportSession * Remove supportSession from getPeerNodeAddress method * Remove isBuyer from supportSession * Move creation of ChatMessage to SupportManager * Remove isMediationDispute fielf in ChatMessage * Remove chatMessage.isMediationDispute() * Refactor: Rename trade.getCommunicationMessages() * Move creation of ChatMessage to Chat * Refactor: Rename class * Refactor: Move ChatView class * Refactor: Move PriceFeedComboBoxItem class to shared package * Refactor: Use 'public abstract' instead of 'abstract public' * Refactor: Use 'protected abstract' instead of 'abstract protected' * Add traderChatManager.onAllServicesInitialized() to BisqSetup * Remove unused param * Refactor: Rename addChatMessage to addAndPersistChatMessage * Fix missing check at ack msg handling Various WIP refactorings/improvements * Remove addAndPersistChatMessage from SupportSession * Remove disputeManager from DisputeSession * Fix missing getConcreteDisputeChatSession impl. * Refactor: Rename package * Refactor: Rename classes Avoid trader as it might confuse with trader chat. As for mediation/arbitration the agent (mediator/arbitrator) are acting a bit like a server we use the client terminology for the traders. * Refactor: Move classes to new package * Fix missing protobuf data - Add missing SupportType to protobuf - Remove is_mediation_dispute from Dispute protobuf definition - Add getAgentNodeAddress method - Var. other refactorings, cleanups * Clone list at persisting to avoid ConcurrentModificationException * Fix order of SupportType Old clients fall back to enum at slot 0. * Add getDisputeState_StartedByPeer template method * Add trade protocol tasks for mediation result tx signing and msg sending * Complete protocol tasks for mediation * Refactor: Remove unneeded SuppressWarnings type: "WeakerAccess" * Complete mediation result protocol Works now all but not much tested.... * Add activation date and capability We need to make sure that not updated users cannot cause problems once mediation is supported. We would get mixed cases where one has a mediation ticket and the not updated user an arbitration ticket. To avoid that we set an activation date with about 10 days from release. Until that date mediation is not supported. Additionally we use OfferRestrictions.REQUIRE_UPDATE_DATE for hiding offers from users how have not updated (we use the fact that mediator and arbitrator has been same in old version, in new version they are different). An old client cannot take an offer from a new maker as he does not has set the new MEDIATION capability. He will get an null value as AvailabilityResult as he has not the new entry MISSING_MANDATORY_CAPABILITY. We will also use the min version for trading in the filter, so that not updated users get a popup telling them to update and they see all offers deactivated. * Various fixes * Remove code part which does not make sense (anymore) Maybe in older versions there was use of openDisputes and closedDisputes but now it does not make sense anymore and arbitrator never gets 4 cases opened if offline. * Add check of balance is > 0 * Only close trade if payout tx is set * Add missing check if arbitrator and mediator are available * Fix wrong key * Improve handling of checks and popup display For create and take offer we check certain conditions and show a popup if not met. This commit moves that to GuiUtils. * Rename any occurrance of DisputeResolver to DisputeAgent * Fix handling of mediatorPubKeyRing * Remove disputeSummaryWindow.evidence fields * Add missing persistence for MediationResultState * Fix tests * Make text more compact to not exceed space * Refactor NotificationGroup * Improve text, add dev testing feature for popups * Improve text * Renamed a key and assigned a new text * Fix states * Do not set errorMessage Do not set errorMessage if both peers have opened a dispute and agent was not online * Remove logs used for dev testing * Fix getMedian method with empty list * Add new methods and tests Add fromCommaSeparatedOrdinals and toCommaSeparatedOrdinals to convert from string representations (used for handling backward compatibility with mediation release). Add check if int >= 0 to fromIntList * Move error log outside of delayed call * Add capabilities entry to extraDataMap in offer The previous implementation did not work for supporting updates and hiding offers from not updated clients. We use now the capabilities converted to a string list and put it into the extraDataMap. If a use with old persisted offers updates his offers gets converted to add the capabilities. Updated clients will ignore offers without the mediation capability set in the offer. * Rename non sync protobuf definitions As Christoph Sturm pointed out we can rename protobuf entries. Only index number must not be changed. * Fix UI state when arbitration has started Only set mediation state if we are not in arbitration state. * Remove restriction * Fix typo; remove errorMessage If both have opened a dispute and agent was not online we dont treat it as error. * Improve text * Store full address for localhost dev testing The arbitrator/mediator selection is based on statistics of usage of agents in past trades. We put the first 4 chars into the trade statistics, but for localhost that would be same vale for 2 diff nodes. * Remove errorMessage If both have opened a dispute and agent was not online we dont treat it as error. * Improve text * Keep accept or reject button enabled after accept - If peer never accepts the trader who has accepted first can change to reject to open a arbitration dispute. We could improve that by adding a new state to open arbitration directly and show a diff. button text and popup. But I think for now thats ok as well.... * Cleanups (no functional change) - remove unused params - remove not used code - reformat - clean up comments - fix log levels - remove redundant annotations * Update core/src/main/resources/i18n/displayStrings.properties Co-Authored-By: Steve Jain <mfiver@gmail.com> * Update core/src/main/resources/i18n/displayStrings.properties Co-Authored-By: Steve Jain <mfiver@gmail.com> * Update core/src/main/resources/i18n/displayStrings.properties Co-Authored-By: Steve Jain <mfiver@gmail.com> * Update core/src/main/resources/i18n/displayStrings.properties Co-Authored-By: Steve Jain <mfiver@gmail.com> * Update core/src/main/resources/i18n/displayStrings.properties Co-Authored-By: Steve Jain <mfiver@gmail.com> * Update core/src/main/resources/i18n/displayStrings.properties Co-Authored-By: Steve Jain <mfiver@gmail.com> * Update core/src/main/resources/i18n/displayStrings.properties Co-Authored-By: Steve Jain <mfiver@gmail.com> * Update core/src/main/resources/i18n/displayStrings.properties Co-Authored-By: Steve Jain <mfiver@gmail.com> * Update core/src/main/resources/i18n/displayStrings.properties Co-Authored-By: Steve Jain <mfiver@gmail.com> * Update core/src/main/resources/i18n/displayStrings.properties Co-Authored-By: Steve Jain <mfiver@gmail.com> * Improve text * Auto fill remaining amount in custom payout If mediator or arbitrator are doing a custom payout, we auto-fill counterpart field with remaining amount, so he does not need to calculate.
This commit is contained in:
parent
bb806cfbc3
commit
d55114e019
@ -1,5 +1,6 @@
|
||||
<component name="ProjectCodeStyleConfiguration">
|
||||
<state>
|
||||
<option name="USE_PER_PROJECT_SETTINGS" value="true" />
|
||||
<option name="PREFERRED_PROJECT_CODE_STYLE" value="Default" />
|
||||
</state>
|
||||
</component>
|
@ -17,10 +17,14 @@
|
||||
|
||||
package bisq.common.app;
|
||||
|
||||
import com.google.common.base.Joiner;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@ -43,7 +47,7 @@ public class Capabilities {
|
||||
|
||||
// Defines which most recent capability any node need to support.
|
||||
// This helps to clean network from very old inactive but still running nodes.
|
||||
private static final Capability mandatoryCapability = Capability.DAO_STATE;
|
||||
private static final Capability MANDATORY_CAPABILITY = Capability.DAO_STATE;
|
||||
|
||||
protected final Set<Capability> capabilities = new HashSet<>();
|
||||
|
||||
@ -101,7 +105,7 @@ public class Capabilities {
|
||||
* @return int list of Capability ordinals
|
||||
*/
|
||||
public static List<Integer> toIntList(Capabilities capabilities) {
|
||||
return capabilities.capabilities.stream().map(capability -> capability.ordinal()).sorted().collect(Collectors.toList());
|
||||
return capabilities.capabilities.stream().map(Enum::ordinal).sorted().collect(Collectors.toList());
|
||||
}
|
||||
|
||||
/**
|
||||
@ -113,11 +117,46 @@ public class Capabilities {
|
||||
public static Capabilities fromIntList(List<Integer> capabilities) {
|
||||
return new Capabilities(capabilities.stream()
|
||||
.filter(integer -> integer < Capability.values().length)
|
||||
.filter(integer -> integer >= 0)
|
||||
.map(integer -> Capability.values()[integer])
|
||||
.collect(Collectors.toSet()));
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param list Comma separated list of Capability ordinals.
|
||||
* @return Capabilities
|
||||
*/
|
||||
public static Capabilities fromStringList(String list) {
|
||||
if (list == null || list.isEmpty())
|
||||
return new Capabilities();
|
||||
|
||||
List<String> entries = List.of(list.replace(" ", "").split(","));
|
||||
List<Integer> capabilitiesList = entries.stream()
|
||||
.map(c -> {
|
||||
try {
|
||||
return Integer.parseInt(c);
|
||||
} catch (Throwable e) {
|
||||
return null;
|
||||
}
|
||||
})
|
||||
.filter(Objects::nonNull)
|
||||
.collect(Collectors.toList());
|
||||
return Capabilities.fromIntList(capabilitiesList);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Converts capabilities to list of ordinals as comma separated strings
|
||||
*/
|
||||
public String toStringList() {
|
||||
return Joiner.on(", ").join(Capabilities.toIntList(this));
|
||||
}
|
||||
|
||||
public static boolean hasMandatoryCapability(Capabilities capabilities) {
|
||||
return hasMandatoryCapability(capabilities, MANDATORY_CAPABILITY);
|
||||
}
|
||||
|
||||
public static boolean hasMandatoryCapability(Capabilities capabilities, Capability mandatoryCapability) {
|
||||
return capabilities.capabilities.stream().anyMatch(c -> c == mandatoryCapability);
|
||||
}
|
||||
|
||||
@ -126,6 +165,13 @@ public class Capabilities {
|
||||
return Arrays.toString(Capabilities.toIntList(this).toArray());
|
||||
}
|
||||
|
||||
public String prettyPrint() {
|
||||
return capabilities.stream()
|
||||
.sorted(Comparator.comparingInt(Enum::ordinal))
|
||||
.map(e -> e.name() + " [" + e.ordinal() + "]")
|
||||
.collect(Collectors.joining(", "));
|
||||
}
|
||||
|
||||
public int size() {
|
||||
return capabilities.size();
|
||||
}
|
||||
|
@ -20,7 +20,8 @@ package bisq.common.app;
|
||||
// We can define here special features the client is supporting.
|
||||
// Useful for updates to new versions where a new data type would break backwards compatibility or to
|
||||
// limit a node to certain behaviour and roles like the seed nodes.
|
||||
// We don't use the Enum in any serialized data, as changes in the enum would break backwards compatibility. We use the ordinal integer instead.
|
||||
// We don't use the Enum in any serialized data, as changes in the enum would break backwards compatibility.
|
||||
// We use the ordinal integer instead.
|
||||
// Sequence in the enum must not be changed (append only).
|
||||
public enum Capability {
|
||||
@Deprecated TRADE_STATISTICS, // Not required anymore as no old clients out there not having that support
|
||||
@ -37,5 +38,6 @@ public enum Capability {
|
||||
//TODO can be set deprecated after v1.1.6 as we enforce update there
|
||||
BUNDLE_OF_ENVELOPES, // Supports bundling of messages if many messages are sent in short interval
|
||||
|
||||
SIGNED_ACCOUNT_AGE_WITNESS // Supports the signed account age witness feature
|
||||
SIGNED_ACCOUNT_AGE_WITNESS, // Supports the signed account age witness feature
|
||||
MEDIATION // Supports mediation feature
|
||||
}
|
||||
|
@ -41,7 +41,6 @@ public class DevEnv {
|
||||
|
||||
// If set to true we ignore several UI behavior like confirmation popups as well dummy accounts are created and
|
||||
// offers are filled with default values. Intended to make dev testing faster.
|
||||
@SuppressWarnings("PointlessBooleanExpression")
|
||||
private static boolean devMode = false;
|
||||
|
||||
public static boolean isDevMode() {
|
||||
|
@ -35,7 +35,7 @@ public abstract class Task<T extends Model> {
|
||||
this.model = model;
|
||||
}
|
||||
|
||||
abstract protected void run();
|
||||
protected abstract void run();
|
||||
|
||||
protected void runInterceptHook() {
|
||||
if (getClass() == taskToIntercept)
|
||||
|
@ -93,7 +93,11 @@ public class MathUtils {
|
||||
return BigDecimal.valueOf(value1).multiply(BigDecimal.valueOf(value2)).doubleValue();
|
||||
}
|
||||
|
||||
public static Long getMedian(Long[] list) {
|
||||
public static long getMedian(Long[] list) {
|
||||
if (list.length == 0) {
|
||||
return 0L;
|
||||
}
|
||||
|
||||
int middle = list.length / 2;
|
||||
long median;
|
||||
if (list.length % 2 == 1) {
|
||||
|
@ -44,7 +44,7 @@ message NetworkEnvelope {
|
||||
|
||||
OpenNewDisputeMessage open_new_dispute_message = 22;
|
||||
PeerOpenedDisputeMessage peer_opened_dispute_message = 23;
|
||||
DisputeCommunicationMessage dispute_communication_message = 24;
|
||||
ChatMessage chat_message = 24;
|
||||
DisputeResultMessage dispute_result_message = 25;
|
||||
PeerPublishedDisputePayoutTxMessage peer_published_dispute_payout_tx_message = 26;
|
||||
|
||||
@ -68,6 +68,8 @@ message NetworkEnvelope {
|
||||
GetBlindVoteStateHashesResponse get_blind_vote_state_hashes_response = 42;
|
||||
|
||||
BundleOfEnvelopes bundle_of_envelopes = 43;
|
||||
MediatedPayoutTxSignatureMessage mediated_payout_tx_signature_message = 44;
|
||||
MediatedPayoutTxPublishedMessage mediated_payout_tx_published_message = 45;
|
||||
}
|
||||
}
|
||||
|
||||
@ -141,6 +143,7 @@ message OfferAvailabilityResponse {
|
||||
repeated int32 supported_capabilities = 3;
|
||||
string uid = 4;
|
||||
NodeAddress arbitrator = 5;
|
||||
NodeAddress mediator = 6;
|
||||
}
|
||||
|
||||
message RefreshOfferMessage {
|
||||
@ -267,26 +270,43 @@ message PayoutTxPublishedMessage {
|
||||
string uid = 4;
|
||||
}
|
||||
|
||||
message MediatedPayoutTxPublishedMessage {
|
||||
string trade_id = 1;
|
||||
bytes payout_tx = 2;
|
||||
NodeAddress sender_node_address = 3;
|
||||
string uid = 4;
|
||||
}
|
||||
|
||||
message MediatedPayoutTxSignatureMessage {
|
||||
string uid = 1;
|
||||
bytes tx_signature = 2;
|
||||
string trade_id = 3;
|
||||
NodeAddress sender_node_address = 4;
|
||||
}
|
||||
|
||||
// dispute
|
||||
|
||||
enum SupportType {
|
||||
ARBITRATION = 0;
|
||||
MEDIATION = 1;
|
||||
TRADE = 2;
|
||||
}
|
||||
|
||||
message OpenNewDisputeMessage {
|
||||
Dispute dispute = 1;
|
||||
NodeAddress sender_node_address = 2;
|
||||
string uid = 3;
|
||||
SupportType type = 4;
|
||||
}
|
||||
|
||||
message PeerOpenedDisputeMessage {
|
||||
Dispute dispute = 1;
|
||||
NodeAddress sender_node_address = 2;
|
||||
string uid = 3;
|
||||
SupportType type = 4;
|
||||
}
|
||||
|
||||
message DisputeCommunicationMessage {
|
||||
enum Type {
|
||||
MEDIATION = 0;
|
||||
ARBITRATION = 1;
|
||||
TRADE = 2;
|
||||
}
|
||||
message ChatMessage {
|
||||
int64 date = 1;
|
||||
string trade_id = 2;
|
||||
int32 trader_id = 3;
|
||||
@ -301,7 +321,7 @@ message DisputeCommunicationMessage {
|
||||
string send_message_error = 12;
|
||||
bool acknowledged = 13;
|
||||
string ack_error = 14;
|
||||
Type type = 15;
|
||||
SupportType type = 15;
|
||||
bool was_displayed = 16;
|
||||
}
|
||||
|
||||
@ -309,6 +329,7 @@ message DisputeResultMessage {
|
||||
string uid = 1;
|
||||
DisputeResult dispute_result = 2;
|
||||
NodeAddress sender_node_address = 3;
|
||||
SupportType type = 4;
|
||||
}
|
||||
|
||||
message PeerPublishedDisputePayoutTxMessage {
|
||||
@ -316,9 +337,9 @@ message PeerPublishedDisputePayoutTxMessage {
|
||||
bytes transaction = 2;
|
||||
string trade_id = 3;
|
||||
NodeAddress sender_node_address = 4;
|
||||
SupportType type = 5;
|
||||
}
|
||||
|
||||
|
||||
message PrivateNotificationMessage {
|
||||
string uid = 1;
|
||||
NodeAddress sender_node_address = 2;
|
||||
@ -546,6 +567,7 @@ message Filter {
|
||||
bool disable_dao = 14;
|
||||
string disable_dao_below_version = 15;
|
||||
string disable_trade_below_version = 16;
|
||||
repeated string mediators = 17;
|
||||
}
|
||||
|
||||
// not used anymore from v0.6 on. But leave it for receiving TradeStatistics objects from older
|
||||
@ -614,8 +636,8 @@ message OfferPayload {
|
||||
int64 min_amount = 10;
|
||||
string base_currency_code = 11;
|
||||
string counter_currency_code = 12;
|
||||
repeated NodeAddress arbitrator_node_addresses = 13;
|
||||
repeated NodeAddress mediator_node_addresses = 14;
|
||||
repeated NodeAddress arbitrator_node_addresses = 13 [deprecated = true]; // not used anymore but still required as old clients check for nonNull
|
||||
repeated NodeAddress mediator_node_addresses = 14 [deprecated = true]; // not used anymore but still required as old clients check for nonNull
|
||||
string payment_method_id = 15;
|
||||
string maker_payment_account_id = 16;
|
||||
string offer_fee_payment_tx_id = 17;
|
||||
@ -680,9 +702,9 @@ message Dispute {
|
||||
string contract_as_json = 15;
|
||||
string maker_contract_signature = 16;
|
||||
string taker_contract_signature = 17;
|
||||
PubKeyRing arbitrator_pub_key_ring = 18;
|
||||
PubKeyRing agent_pub_key_ring = 18;
|
||||
bool is_support_ticket = 19;
|
||||
repeated DisputeCommunicationMessage dispute_communication_messages = 20;
|
||||
repeated ChatMessage chat_message = 20;
|
||||
bool is_closed = 21;
|
||||
DisputeResult dispute_result = 22;
|
||||
string dispute_payout_tx_id = 23;
|
||||
@ -719,7 +741,7 @@ message DisputeResult {
|
||||
bool id_verification = 6;
|
||||
bool screen_cast = 7;
|
||||
string summary_notes = 8;
|
||||
DisputeCommunicationMessage dispute_communication_message = 9;
|
||||
ChatMessage chat_message = 9;
|
||||
bytes arbitrator_signature = 10;
|
||||
int64 buyer_payout_amount = 11;
|
||||
int64 seller_payout_amount = 12;
|
||||
@ -770,6 +792,7 @@ enum AvailabilityResult {
|
||||
NO_ARBITRATORS = 6;
|
||||
NO_MEDIATORS = 7;
|
||||
USER_IGNORED = 8;
|
||||
MISSING_MANDATORY_CAPABILITY = 9;
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
@ -1009,7 +1032,7 @@ message PersistableEnvelope {
|
||||
|
||||
TradableList tradable_list = 6;
|
||||
TradeStatisticsList trade_statistics_list = 7 [deprecated = true]; // Was used in pre v0.6.0 version. Not used anymore.
|
||||
DisputeList dispute_list = 8;
|
||||
ArbitrationDisputeList arbitration_dispute_list = 8;
|
||||
|
||||
PreferencesPayload preferences_payload = 9;
|
||||
UserPayload user_payload = 10;
|
||||
@ -1039,6 +1062,7 @@ message PersistableEnvelope {
|
||||
MyProofOfBurnList my_proof_of_burn_list = 26;
|
||||
UnconfirmedBsqChangeOutputList unconfirmed_bsq_change_output_list = 27;
|
||||
SignedWitnessStore signed_witness_store = 28;
|
||||
MediationDisputeList mediation_dispute_list = 29;
|
||||
}
|
||||
}
|
||||
|
||||
@ -1162,6 +1186,7 @@ message OpenOffer {
|
||||
Offer offer = 1;
|
||||
State state = 2;
|
||||
NodeAddress arbitrator_node_address = 3;
|
||||
NodeAddress mediator_node_address = 4;
|
||||
}
|
||||
|
||||
message Tradable {
|
||||
@ -1224,9 +1249,12 @@ message Trade {
|
||||
enum DisputeState {
|
||||
PB_ERROR_DISPUTE_STATE = 0;
|
||||
NO_DISPUTE = 1;
|
||||
DISPUTE_REQUESTED = 2;
|
||||
DISPUTE_STARTED_BY_PEER = 3;
|
||||
DISPUTE_CLOSED = 4;
|
||||
DISPUTE_REQUESTED = 2; // arbitration We use the enum name for resolving enums so it cannot be renamed
|
||||
DISPUTE_STARTED_BY_PEER = 3; // arbitration We use the enum name for resolving enums so it cannot be renamed
|
||||
DISPUTE_CLOSED = 4; // arbitration We use the enum name for resolving enums so it cannot be renamed
|
||||
MEDIATION_REQUESTED = 5;
|
||||
MEDIATION_STARTED_BY_PEER = 6;
|
||||
MEDIATION_CLOSED = 7;
|
||||
}
|
||||
|
||||
enum TradePeriodState {
|
||||
@ -1264,7 +1292,8 @@ message Trade {
|
||||
PubKeyRing arbitrator_pub_key_ring = 26;
|
||||
PubKeyRing mediator_pub_key_ring = 27;
|
||||
string counter_currency_tx_id = 28;
|
||||
repeated DisputeCommunicationMessage communication_messages = 29;
|
||||
repeated ChatMessage chat_message = 29;
|
||||
MediationResultState mediation_result_state = 30;
|
||||
}
|
||||
|
||||
message BuyerAsMakerTrade {
|
||||
@ -1301,6 +1330,9 @@ message ProcessModel {
|
||||
bytes my_multi_sig_pub_key = 15;
|
||||
NodeAddress temp_trading_peer_node_address = 16;
|
||||
string payment_started_message_state = 17;
|
||||
bytes mediated_payout_tx_signature = 18;
|
||||
int64 buyer_payout_amount_from_mediation = 19;
|
||||
int64 seller_payout_amount_from_mediation = 20;
|
||||
}
|
||||
|
||||
message TradingPeer {
|
||||
@ -1318,16 +1350,40 @@ message TradingPeer {
|
||||
bytes account_age_witness_nonce = 12;
|
||||
bytes account_age_witness_signature = 13;
|
||||
int64 current_date = 14;
|
||||
bytes mediated_payout_tx_signature = 15;
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
// Dispute
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
message DisputeList {
|
||||
message ArbitrationDisputeList {
|
||||
repeated Dispute dispute = 1;
|
||||
}
|
||||
|
||||
message MediationDisputeList {
|
||||
repeated Dispute dispute = 1;
|
||||
}
|
||||
|
||||
enum MediationResultState {
|
||||
PB_ERROR_MEDIATION_RESULT = 0;
|
||||
UNDEFINED_MEDIATION_RESULT = 1;
|
||||
MEDIATION_RESULT_ACCEPTED = 2;
|
||||
MEDIATION_RESULT_REJECTED = 3;
|
||||
SIG_MSG_SENT = 4;
|
||||
SIG_MSG_ARRIVED = 5;
|
||||
SIG_MSG_IN_MAILBOX = 6;
|
||||
SIG_MSG_SEND_FAILED = 7;
|
||||
RECEIVED_SIG_MSG = 8;
|
||||
PAYOUT_TX_PUBLISHED = 9;
|
||||
PAYOUT_TX_PUBLISHED_MSG_SENT = 10;
|
||||
PAYOUT_TX_PUBLISHED_MSG_ARRIVED = 11;
|
||||
PAYOUT_TX_PUBLISHED_MSG_IN_MAILBOX = 12;
|
||||
PAYOUT_TX_PUBLISHED_MSG_SEND_FAILED = 13;
|
||||
RECEIVED_PAYOUT_TX_PUBLISHED_MSG = 14;
|
||||
PAYOUT_TX_SEEN_IN_NETWORK = 15;
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
// Preferences
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
@ -17,11 +17,16 @@
|
||||
|
||||
package bisq.common.app;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import static bisq.common.app.Capability.*;
|
||||
import static bisq.common.app.Capability.SEED_NODE;
|
||||
import static bisq.common.app.Capability.TRADE_STATISTICS;
|
||||
import static bisq.common.app.Capability.TRADE_STATISTICS_2;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
@ -60,4 +65,47 @@ public class CapabilitiesTest {
|
||||
assertTrue(DUT.containsAll(new Capabilities(TRADE_STATISTICS, TRADE_STATISTICS_2)));
|
||||
assertFalse(DUT.containsAll(new Capabilities(SEED_NODE, TRADE_STATISTICS_2)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testToIntList() {
|
||||
assertEquals(Collections.emptyList(), Capabilities.toIntList(new Capabilities()));
|
||||
assertEquals(Collections.singletonList(12), Capabilities.toIntList(new Capabilities(Capability.MEDIATION)));
|
||||
assertEquals(Arrays.asList(6, 12), Capabilities.toIntList(new Capabilities(Capability.MEDIATION, Capability.BLIND_VOTE)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFromIntList() {
|
||||
assertEquals(new Capabilities(), Capabilities.fromIntList(Collections.emptyList()));
|
||||
assertEquals(new Capabilities(Capability.MEDIATION), Capabilities.fromIntList(Collections.singletonList(12)));
|
||||
assertEquals(new Capabilities(Capability.BLIND_VOTE, Capability.MEDIATION), Capabilities.fromIntList(Arrays.asList(6, 12)));
|
||||
|
||||
assertEquals(new Capabilities(), Capabilities.fromIntList(Collections.singletonList(-1)));
|
||||
assertEquals(new Capabilities(), Capabilities.fromIntList(Collections.singletonList(99)));
|
||||
assertEquals(new Capabilities(Capability.MEDIATION), Capabilities.fromIntList(Arrays.asList(-6, 12)));
|
||||
assertEquals(new Capabilities(Capability.MEDIATION), Capabilities.fromIntList(Arrays.asList(12, 99)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testToStringList() {
|
||||
assertEquals("", new Capabilities().toStringList());
|
||||
assertEquals("12", new Capabilities(Capability.MEDIATION).toStringList());
|
||||
assertEquals("6, 12", new Capabilities(Capability.BLIND_VOTE, Capability.MEDIATION).toStringList());
|
||||
// capabilities gets sorted, independent of our order
|
||||
assertEquals("6, 12", new Capabilities(Capability.MEDIATION, Capability.BLIND_VOTE).toStringList());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFromStringList() {
|
||||
assertEquals(new Capabilities(), Capabilities.fromStringList(null));
|
||||
assertEquals(new Capabilities(), Capabilities.fromStringList(""));
|
||||
assertEquals(new Capabilities(Capability.MEDIATION), Capabilities.fromStringList("12"));
|
||||
assertEquals(new Capabilities(Capability.BLIND_VOTE, Capability.MEDIATION), Capabilities.fromStringList("6,12"));
|
||||
assertEquals(new Capabilities(Capability.BLIND_VOTE, Capability.MEDIATION), Capabilities.fromStringList("12, 6"));
|
||||
assertEquals(new Capabilities(), Capabilities.fromStringList("a"));
|
||||
assertEquals(new Capabilities(), Capabilities.fromStringList("99"));
|
||||
assertEquals(new Capabilities(), Capabilities.fromStringList("-1"));
|
||||
assertEquals(new Capabilities(Capability.MEDIATION), Capabilities.fromStringList("12, a"));
|
||||
assertEquals(new Capabilities(Capability.MEDIATION), Capabilities.fromStringList("12, 99"));
|
||||
assertEquals(new Capabilities(Capability.MEDIATION), Capabilities.fromStringList("a,12, 99"));
|
||||
}
|
||||
}
|
||||
|
@ -20,7 +20,6 @@ package bisq.core;
|
||||
import bisq.core.alert.AlertModule;
|
||||
import bisq.core.app.AppOptionKeys;
|
||||
import bisq.core.app.BisqEnvironment;
|
||||
import bisq.core.arbitration.ArbitratorModule;
|
||||
import bisq.core.btc.BitcoinModule;
|
||||
import bisq.core.dao.DaoModule;
|
||||
import bisq.core.filter.FilterModule;
|
||||
@ -90,7 +89,6 @@ public class CoreModule extends AppModule {
|
||||
// ordering is used for shut down sequence
|
||||
install(tradeModule());
|
||||
install(encryptionServiceModule());
|
||||
install(arbitratorModule());
|
||||
install(offerModule());
|
||||
install(p2pModule());
|
||||
install(bitcoinModule());
|
||||
@ -109,10 +107,6 @@ public class CoreModule extends AppModule {
|
||||
return new EncryptionServiceModule(environment);
|
||||
}
|
||||
|
||||
private ArbitratorModule arbitratorModule() {
|
||||
return new ArbitratorModule(environment);
|
||||
}
|
||||
|
||||
private AlertModule alertModule() {
|
||||
return new AlertModule(environment);
|
||||
}
|
||||
|
@ -19,14 +19,14 @@ package bisq.core.account.sign;
|
||||
|
||||
import bisq.core.account.witness.AccountAgeWitness;
|
||||
import bisq.core.account.witness.AccountAgeWitnessService;
|
||||
import bisq.core.arbitration.ArbitratorManager;
|
||||
import bisq.core.arbitration.BuyerDataItem;
|
||||
import bisq.core.arbitration.Dispute;
|
||||
import bisq.core.arbitration.DisputeManager;
|
||||
import bisq.core.arbitration.DisputeResult;
|
||||
import bisq.core.payment.ChargeBackRisk;
|
||||
import bisq.core.payment.payload.PaymentAccountPayload;
|
||||
import bisq.core.payment.payload.PaymentMethod;
|
||||
import bisq.core.support.dispute.Dispute;
|
||||
import bisq.core.support.dispute.DisputeResult;
|
||||
import bisq.core.support.dispute.arbitration.ArbitrationManager;
|
||||
import bisq.core.support.dispute.arbitration.BuyerDataItem;
|
||||
import bisq.core.support.dispute.arbitration.arbitrator.ArbitratorManager;
|
||||
|
||||
import bisq.network.p2p.P2PService;
|
||||
import bisq.network.p2p.storage.P2PDataStorage;
|
||||
@ -75,7 +75,7 @@ public class SignedWitnessService {
|
||||
private final P2PService p2PService;
|
||||
private final AccountAgeWitnessService accountAgeWitnessService;
|
||||
private final ArbitratorManager arbitratorManager;
|
||||
private final DisputeManager disputeManager;
|
||||
private final ArbitrationManager arbitrationManager;
|
||||
private final ChargeBackRisk chargeBackRisk;
|
||||
|
||||
private final Map<P2PDataStorage.ByteArray, SignedWitness> signedWitnessMap = new HashMap<>();
|
||||
@ -92,13 +92,13 @@ public class SignedWitnessService {
|
||||
ArbitratorManager arbitratorManager,
|
||||
SignedWitnessStorageService signedWitnessStorageService,
|
||||
AppendOnlyDataStoreService appendOnlyDataStoreService,
|
||||
DisputeManager disputeManager,
|
||||
ArbitrationManager arbitrationManager,
|
||||
ChargeBackRisk chargeBackRisk) {
|
||||
this.keyRing = keyRing;
|
||||
this.p2PService = p2PService;
|
||||
this.accountAgeWitnessService = accountAgeWitnessService;
|
||||
this.arbitratorManager = arbitratorManager;
|
||||
this.disputeManager = disputeManager;
|
||||
this.arbitrationManager = arbitrationManager;
|
||||
this.chargeBackRisk = chargeBackRisk;
|
||||
|
||||
// We need to add that early (before onAllServicesInitialized) as it will be used at startup.
|
||||
@ -333,7 +333,7 @@ public class SignedWitnessService {
|
||||
|
||||
// Arbitrator signing
|
||||
public List<BuyerDataItem> getBuyerPaymentAccounts(long safeDate, PaymentMethod paymentMethod) {
|
||||
return disputeManager.getDisputesAsObservableList().stream()
|
||||
return arbitrationManager.getDisputesAsObservableList().stream()
|
||||
.filter(dispute -> dispute.getContract().getPaymentMethodId().equals(paymentMethod.getId()))
|
||||
.filter(this::hasChargebackRisk)
|
||||
.filter(this::isBuyerWinner)
|
||||
|
@ -99,7 +99,7 @@ public final class Alert implements ProtectedStoragePayload, ExpirablePayload {
|
||||
public protobuf.StoragePayload toProtoMessage() {
|
||||
checkNotNull(ownerPubKeyBytes, "storagePublicKeyBytes must not be null");
|
||||
checkNotNull(signatureAsBase64, "signatureAsBase64 must not be null");
|
||||
final protobuf.Alert.Builder builder = protobuf.Alert.newBuilder()
|
||||
protobuf.Alert.Builder builder = protobuf.Alert.newBuilder()
|
||||
.setMessage(message)
|
||||
.setIsUpdateInfo(isUpdateInfo)
|
||||
.setVersion(version)
|
||||
@ -109,7 +109,13 @@ public final class Alert implements ProtectedStoragePayload, ExpirablePayload {
|
||||
return protobuf.StoragePayload.newBuilder().setAlert(builder).build();
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public static Alert fromProto(protobuf.Alert proto) {
|
||||
// We got in dev testing sometimes an empty protobuf Alert. Not clear why that happened but as it causes an
|
||||
// exception and corrupted user db file we prefer to set it to null.
|
||||
if (proto.getSignatureAsBase64().isEmpty())
|
||||
return null;
|
||||
|
||||
return new Alert(proto.getMessage(),
|
||||
proto.getIsUpdateInfo(),
|
||||
proto.getVersion(),
|
||||
|
@ -17,7 +17,6 @@
|
||||
|
||||
package bisq.core.app;
|
||||
|
||||
import bisq.core.arbitration.ArbitratorManager;
|
||||
import bisq.core.btc.BtcOptionKeys;
|
||||
import bisq.core.btc.setup.RegTestHost;
|
||||
import bisq.core.btc.setup.WalletsSetup;
|
||||
@ -29,6 +28,7 @@ import bisq.core.exceptions.BisqException;
|
||||
import bisq.core.offer.OpenOfferManager;
|
||||
import bisq.core.setup.CorePersistedDataHost;
|
||||
import bisq.core.setup.CoreSetup;
|
||||
import bisq.core.support.dispute.arbitration.arbitrator.ArbitratorManager;
|
||||
import bisq.core.trade.TradeManager;
|
||||
|
||||
import bisq.network.NetworkOptionKeys;
|
||||
@ -242,11 +242,11 @@ public abstract class BisqExecutable implements GracefulShutDownHandler, BisqSet
|
||||
try {
|
||||
PersistedDataHost.apply(CorePersistedDataHost.getPersistedDataHosts(injector));
|
||||
} catch (Throwable t) {
|
||||
log.error("Error at PersistedDataHost.apply: {}", t.toString(), t);
|
||||
// If we are in dev mode we want to get the exception if some db files are corrupted
|
||||
// We need to delay it as the stage is not created yet and so popups would not be shown.
|
||||
if (DevEnv.isDevMode())
|
||||
UserThread.runAfter(() -> {
|
||||
log.error("Error at PersistedDataHost.apply: {}", t.toString());
|
||||
throw t;
|
||||
}, 2);
|
||||
}
|
||||
|
@ -23,8 +23,6 @@ import bisq.core.alert.Alert;
|
||||
import bisq.core.alert.AlertManager;
|
||||
import bisq.core.alert.PrivateNotificationManager;
|
||||
import bisq.core.alert.PrivateNotificationPayload;
|
||||
import bisq.core.arbitration.ArbitratorManager;
|
||||
import bisq.core.arbitration.DisputeManager;
|
||||
import bisq.core.btc.Balances;
|
||||
import bisq.core.btc.model.AddressEntry;
|
||||
import bisq.core.btc.setup.WalletsSetup;
|
||||
@ -47,6 +45,11 @@ import bisq.core.payment.PaymentAccount;
|
||||
import bisq.core.payment.TradeLimits;
|
||||
import bisq.core.provider.fee.FeeService;
|
||||
import bisq.core.provider.price.PriceFeedService;
|
||||
import bisq.core.support.dispute.arbitration.ArbitrationManager;
|
||||
import bisq.core.support.dispute.arbitration.arbitrator.ArbitratorManager;
|
||||
import bisq.core.support.dispute.mediation.MediationManager;
|
||||
import bisq.core.support.dispute.mediation.mediator.MediatorManager;
|
||||
import bisq.core.support.traderchat.TraderChatManager;
|
||||
import bisq.core.trade.TradeManager;
|
||||
import bisq.core.trade.statistics.AssetTradeActivityCheck;
|
||||
import bisq.core.trade.statistics.TradeStatisticsManager;
|
||||
@ -127,10 +130,13 @@ public class BisqSetup {
|
||||
private final Balances balances;
|
||||
private final PriceFeedService priceFeedService;
|
||||
private final ArbitratorManager arbitratorManager;
|
||||
private final MediatorManager mediatorManager;
|
||||
private final P2PService p2PService;
|
||||
private final TradeManager tradeManager;
|
||||
private final OpenOfferManager openOfferManager;
|
||||
private final DisputeManager disputeManager;
|
||||
private final ArbitrationManager arbitrationManager;
|
||||
private final MediationManager mediationManager;
|
||||
private final TraderChatManager traderChatManager;
|
||||
private final Preferences preferences;
|
||||
private final User user;
|
||||
private final AlertManager alertManager;
|
||||
@ -206,10 +212,13 @@ public class BisqSetup {
|
||||
Balances balances,
|
||||
PriceFeedService priceFeedService,
|
||||
ArbitratorManager arbitratorManager,
|
||||
MediatorManager mediatorManager,
|
||||
P2PService p2PService,
|
||||
TradeManager tradeManager,
|
||||
OpenOfferManager openOfferManager,
|
||||
DisputeManager disputeManager,
|
||||
ArbitrationManager arbitrationManager,
|
||||
MediationManager mediationManager,
|
||||
TraderChatManager traderChatManager,
|
||||
Preferences preferences,
|
||||
User user,
|
||||
AlertManager alertManager,
|
||||
@ -247,10 +256,13 @@ public class BisqSetup {
|
||||
this.balances = balances;
|
||||
this.priceFeedService = priceFeedService;
|
||||
this.arbitratorManager = arbitratorManager;
|
||||
this.mediatorManager = mediatorManager;
|
||||
this.p2PService = p2PService;
|
||||
this.tradeManager = tradeManager;
|
||||
this.openOfferManager = openOfferManager;
|
||||
this.disputeManager = disputeManager;
|
||||
this.arbitrationManager = arbitrationManager;
|
||||
this.mediationManager = mediationManager;
|
||||
this.traderChatManager = traderChatManager;
|
||||
this.preferences = preferences;
|
||||
this.user = user;
|
||||
this.alertManager = alertManager;
|
||||
@ -586,12 +598,15 @@ public class BisqSetup {
|
||||
.filter(e -> tradeManager.getSetOfAllTradeIds().contains(e.getOfferId()) &&
|
||||
e.getContext() == AddressEntry.Context.MULTI_SIG)
|
||||
.forEach(e -> {
|
||||
final Coin balance = e.getCoinLockedInMultiSig();
|
||||
final String message = Res.get("popup.warning.lockedUpFunds",
|
||||
formatter.formatCoinWithCode(balance), e.getAddressString(), e.getOfferId());
|
||||
log.warn(message);
|
||||
if (lockedUpFundsHandler != null)
|
||||
lockedUpFundsHandler.accept(message);
|
||||
Coin balance = e.getCoinLockedInMultiSig();
|
||||
if (balance.isPositive()) {
|
||||
String message = Res.get("popup.warning.lockedUpFunds",
|
||||
formatter.formatCoinWithCode(balance), e.getAddressString(), e.getOfferId());
|
||||
log.warn(message);
|
||||
if (lockedUpFundsHandler != null) {
|
||||
lockedUpFundsHandler.accept(message);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@ -614,7 +629,9 @@ public class BisqSetup {
|
||||
|
||||
tradeLimits.onAllServicesInitialized();
|
||||
|
||||
disputeManager.onAllServicesInitialized();
|
||||
arbitrationManager.onAllServicesInitialized();
|
||||
mediationManager.onAllServicesInitialized();
|
||||
traderChatManager.onAllServicesInitialized();
|
||||
|
||||
tradeManager.onAllServicesInitialized();
|
||||
|
||||
@ -626,6 +643,7 @@ public class BisqSetup {
|
||||
balances.onAllServicesInitialized();
|
||||
|
||||
arbitratorManager.onAllServicesInitialized();
|
||||
mediatorManager.onAllServicesInitialized();
|
||||
|
||||
alertManager.alertMessageProperty().addListener((observable, oldValue, newValue) ->
|
||||
displayAlertIfPresent(newValue, false));
|
||||
|
@ -20,11 +20,11 @@ package bisq.core.app.misc;
|
||||
import bisq.core.app.AppOptionKeys;
|
||||
import bisq.core.app.BisqEnvironment;
|
||||
import bisq.core.app.BisqExecutable;
|
||||
import bisq.core.arbitration.ArbitratorManager;
|
||||
import bisq.core.btc.setup.WalletsSetup;
|
||||
import bisq.core.btc.wallet.BsqWalletService;
|
||||
import bisq.core.btc.wallet.BtcWalletService;
|
||||
import bisq.core.offer.OpenOfferManager;
|
||||
import bisq.core.support.dispute.arbitration.arbitrator.ArbitratorManager;
|
||||
|
||||
import bisq.network.p2p.P2PService;
|
||||
|
||||
|
@ -21,7 +21,6 @@ import bisq.core.alert.AlertModule;
|
||||
import bisq.core.app.AppOptionKeys;
|
||||
import bisq.core.app.BisqEnvironment;
|
||||
import bisq.core.app.TorSetup;
|
||||
import bisq.core.arbitration.ArbitratorModule;
|
||||
import bisq.core.btc.BitcoinModule;
|
||||
import bisq.core.dao.DaoModule;
|
||||
import bisq.core.filter.FilterModule;
|
||||
@ -98,7 +97,6 @@ public class ModuleForAppWithP2p extends AppModule {
|
||||
// ordering is used for shut down sequence
|
||||
install(tradeModule());
|
||||
install(encryptionServiceModule());
|
||||
install(arbitratorModule());
|
||||
install(offerModule());
|
||||
install(p2pModule());
|
||||
install(bitcoinModule());
|
||||
@ -121,10 +119,6 @@ public class ModuleForAppWithP2p extends AppModule {
|
||||
return new EncryptionServiceModule(environment);
|
||||
}
|
||||
|
||||
protected ArbitratorModule arbitratorModule() {
|
||||
return new ArbitratorModule(environment);
|
||||
}
|
||||
|
||||
protected AlertModule alertModule() {
|
||||
return new AlertModule(environment);
|
||||
}
|
||||
|
@ -1,375 +0,0 @@
|
||||
/*
|
||||
* This file is part of Bisq.
|
||||
*
|
||||
* Bisq is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or (at
|
||||
* your option) any later version.
|
||||
*
|
||||
* Bisq is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
|
||||
* License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package bisq.core.arbitration;
|
||||
|
||||
import bisq.core.app.AppOptionKeys;
|
||||
import bisq.core.filter.FilterManager;
|
||||
import bisq.core.user.Preferences;
|
||||
import bisq.core.user.User;
|
||||
|
||||
import bisq.network.p2p.BootstrapListener;
|
||||
import bisq.network.p2p.NodeAddress;
|
||||
import bisq.network.p2p.P2PService;
|
||||
import bisq.network.p2p.storage.HashMapChangedListener;
|
||||
import bisq.network.p2p.storage.payload.ProtectedStorageEntry;
|
||||
|
||||
import bisq.common.Timer;
|
||||
import bisq.common.UserThread;
|
||||
import bisq.common.app.DevEnv;
|
||||
import bisq.common.crypto.KeyRing;
|
||||
import bisq.common.handlers.ErrorMessageHandler;
|
||||
import bisq.common.handlers.ResultHandler;
|
||||
import bisq.common.util.Utilities;
|
||||
|
||||
import org.bitcoinj.core.ECKey;
|
||||
import org.bitcoinj.core.Utils;
|
||||
|
||||
import com.google.inject.Inject;
|
||||
import com.google.inject.name.Named;
|
||||
|
||||
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.Arrays;
|
||||
import java.util.Collections;
|
||||
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 org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
import static org.bitcoinj.core.Utils.HEX;
|
||||
|
||||
public class ArbitratorManager {
|
||||
private static final Logger log = LoggerFactory.getLogger(ArbitratorManager.class);
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
// Static
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
private static final long REPUBLISH_MILLIS = Arbitrator.TTL / 2;
|
||||
private static final long RETRY_REPUBLISH_SEC = 5;
|
||||
private static final long REPEATED_REPUBLISH_AT_STARTUP_SEC = 60;
|
||||
|
||||
private final List<String> publicKeys;
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
// Instance fields
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
private final KeyRing keyRing;
|
||||
private final ArbitratorService arbitratorService;
|
||||
private final User user;
|
||||
private final Preferences preferences;
|
||||
private final FilterManager filterManager;
|
||||
private final ObservableMap<NodeAddress, Arbitrator> arbitratorsObservableMap = FXCollections.observableHashMap();
|
||||
private List<Arbitrator> persistedAcceptedArbitrators;
|
||||
private Timer republishArbitratorTimer, retryRepublishArbitratorTimer;
|
||||
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
// Constructor
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
@Inject
|
||||
public ArbitratorManager(KeyRing keyRing,
|
||||
ArbitratorService arbitratorService,
|
||||
User user,
|
||||
Preferences preferences,
|
||||
FilterManager filterManager,
|
||||
@Named(AppOptionKeys.USE_DEV_PRIVILEGE_KEYS) boolean useDevPrivilegeKeys) {
|
||||
this.keyRing = keyRing;
|
||||
this.arbitratorService = arbitratorService;
|
||||
this.user = user;
|
||||
this.preferences = preferences;
|
||||
this.filterManager = filterManager;
|
||||
publicKeys = useDevPrivilegeKeys ?
|
||||
Collections.unmodifiableList(Collections.singletonList(DevEnv.DEV_PRIVILEGE_PUB_KEY)) :
|
||||
Collections.unmodifiableList(Arrays.asList(
|
||||
"0365c6af94681dbee69de1851f98d4684063bf5c2d64b1c73ed5d90434f375a054",
|
||||
"031c502a60f9dbdb5ae5e438a79819e4e1f417211dd537ac12c9bc23246534c4bd",
|
||||
"02c1e5a242387b6d5319ce27246cea6edaaf51c3550591b528d2578a4753c56c2c",
|
||||
"025c319faf7067d9299590dd6c97fe7e56cd4dac61205ccee1cd1fc390142390a2",
|
||||
"038f6e24c2bfe5d51d0a290f20a9a657c270b94ef2b9c12cd15ca3725fa798fc55",
|
||||
"0255256ff7fb615278c4544a9bbd3f5298b903b8a011cd7889be19b6b1c45cbefe",
|
||||
"024a3a37289f08c910fbd925ebc72b946f33feaeff451a4738ee82037b4cda2e95",
|
||||
"02a88b75e9f0f8afba1467ab26799dcc38fd7a6468fb2795444b425eb43e2c10bd",
|
||||
"02349a51512c1c04c67118386f4d27d768c5195a83247c150a4b722d161722ba81",
|
||||
"03f718a2e0dc672c7cdec0113e72c3322efc70412bb95870750d25c32cd98de17d",
|
||||
"028ff47ee2c56e66313928975c58fa4f1b19a0f81f3a96c4e9c9c3c6768075509e",
|
||||
"02b517c0cbc3a49548f448ddf004ed695c5a1c52ec110be1bfd65fa0ca0761c94b",
|
||||
"03df837a3a0f3d858e82f3356b71d1285327f101f7c10b404abed2abc1c94e7169",
|
||||
"0203a90fb2ab698e524a5286f317a183a84327b8f8c3f7fa4a98fec9e1cefd6b72",
|
||||
"023c99cc073b851c892d8c43329ca3beb5d2213ee87111af49884e3ce66cbd5ba5"
|
||||
));
|
||||
}
|
||||
|
||||
public void shutDown() {
|
||||
stopRepublishArbitratorTimer();
|
||||
stopRetryRepublishArbitratorTimer();
|
||||
}
|
||||
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
// API
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
public void onAllServicesInitialized() {
|
||||
arbitratorService.addHashSetChangedListener(new HashMapChangedListener() {
|
||||
@Override
|
||||
public void onAdded(ProtectedStorageEntry data) {
|
||||
if (data.getProtectedStoragePayload() instanceof Arbitrator)
|
||||
updateArbitratorMap();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRemoved(ProtectedStorageEntry data) {
|
||||
if (data.getProtectedStoragePayload() instanceof Arbitrator) {
|
||||
updateArbitratorMap();
|
||||
final Arbitrator arbitrator = (Arbitrator) data.getProtectedStoragePayload();
|
||||
user.removeAcceptedArbitrator(arbitrator);
|
||||
user.removeAcceptedMediator(getMediator(arbitrator));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
persistedAcceptedArbitrators = new ArrayList<>(user.getAcceptedArbitrators());
|
||||
user.clearAcceptedArbitrators();
|
||||
|
||||
// TODO we mirror arbitrator data for mediator as long we have not impl. it in the UI
|
||||
user.clearAcceptedMediators();
|
||||
|
||||
if (user.getRegisteredArbitrator() != null) {
|
||||
P2PService p2PService = arbitratorService.getP2PService();
|
||||
if (p2PService.isBootstrapped())
|
||||
startRepublishArbitrator();
|
||||
else
|
||||
p2PService.addP2PServiceListener(new BootstrapListener() {
|
||||
@Override
|
||||
public void onUpdatedDataReceived() {
|
||||
startRepublishArbitrator();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
filterManager.filterProperty().addListener((observable, oldValue, newValue) -> updateArbitratorMap());
|
||||
|
||||
updateArbitratorMap();
|
||||
}
|
||||
|
||||
private void startRepublishArbitrator() {
|
||||
if (republishArbitratorTimer == null) {
|
||||
republishArbitratorTimer = UserThread.runPeriodically(this::republishArbitrator, REPUBLISH_MILLIS, TimeUnit.MILLISECONDS);
|
||||
UserThread.runAfter(this::republishArbitrator, REPEATED_REPUBLISH_AT_STARTUP_SEC);
|
||||
republishArbitrator();
|
||||
}
|
||||
}
|
||||
|
||||
public void updateArbitratorMap() {
|
||||
Map<NodeAddress, Arbitrator> map = arbitratorService.getArbitrators();
|
||||
arbitratorsObservableMap.clear();
|
||||
Map<NodeAddress, Arbitrator> filtered = map.values().stream()
|
||||
.filter(e -> {
|
||||
final String pubKeyAsHex = Utils.HEX.encode(e.getRegistrationPubKey());
|
||||
final 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 arbitrator 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 arbitrator failed. Arbitrator=", e.toString());
|
||||
|
||||
return isInPublicKeyInList && isSigValid;
|
||||
})
|
||||
.collect(Collectors.toMap(Arbitrator::getNodeAddress, Function.identity()));
|
||||
|
||||
arbitratorsObservableMap.putAll(filtered);
|
||||
arbitratorsObservableMap.values().stream()
|
||||
.filter(persistedAcceptedArbitrators::contains)
|
||||
.forEach(a -> {
|
||||
user.addAcceptedArbitrator(a);
|
||||
user.addAcceptedMediator(getMediator(a)
|
||||
);
|
||||
});
|
||||
|
||||
// We keep the domain with storing the arbitrators in user as it might be still useful for mediators
|
||||
arbitratorsObservableMap.values().forEach(a -> {
|
||||
user.addAcceptedArbitrator(a);
|
||||
user.addAcceptedMediator(getMediator(a)
|
||||
);
|
||||
});
|
||||
|
||||
log.info("Available arbitrators: {}", arbitratorsObservableMap.keySet());
|
||||
}
|
||||
|
||||
// TODO we mirror arbitrator data for mediator as long we have not impl. it in the UI
|
||||
@NotNull
|
||||
public static Mediator getMediator(Arbitrator arbitrator) {
|
||||
return new Mediator(arbitrator.getNodeAddress(),
|
||||
arbitrator.getPubKeyRing(),
|
||||
arbitrator.getLanguageCodes(),
|
||||
arbitrator.getRegistrationDate(),
|
||||
arbitrator.getRegistrationPubKey(),
|
||||
arbitrator.getRegistrationSignature(),
|
||||
arbitrator.getEmailAddress(),
|
||||
null,
|
||||
arbitrator.getExtraDataMap());
|
||||
}
|
||||
|
||||
public void addArbitrator(Arbitrator arbitrator, ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) {
|
||||
user.setRegisteredArbitrator(arbitrator);
|
||||
arbitratorsObservableMap.put(arbitrator.getNodeAddress(), arbitrator);
|
||||
arbitratorService.addArbitrator(arbitrator,
|
||||
() -> {
|
||||
log.debug("Arbitrator successfully saved in P2P network");
|
||||
resultHandler.handleResult();
|
||||
|
||||
if (arbitratorsObservableMap.size() > 0)
|
||||
UserThread.runAfter(this::updateArbitratorMap, 100, TimeUnit.MILLISECONDS);
|
||||
},
|
||||
errorMessageHandler::handleErrorMessage);
|
||||
}
|
||||
|
||||
public void removeArbitrator(ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) {
|
||||
Arbitrator registeredArbitrator = user.getRegisteredArbitrator();
|
||||
if (registeredArbitrator != null) {
|
||||
user.setRegisteredArbitrator(null);
|
||||
arbitratorsObservableMap.remove(registeredArbitrator.getNodeAddress());
|
||||
arbitratorService.removeArbitrator(registeredArbitrator,
|
||||
() -> {
|
||||
log.debug("Arbitrator successfully removed from P2P network");
|
||||
resultHandler.handleResult();
|
||||
},
|
||||
errorMessageHandler::handleErrorMessage);
|
||||
}
|
||||
}
|
||||
|
||||
public ObservableMap<NodeAddress, Arbitrator> getArbitratorsObservableMap() {
|
||||
return arbitratorsObservableMap;
|
||||
}
|
||||
|
||||
// A private key is handed over to selected arbitrators for registration.
|
||||
// An invited arbitrator will sign at registration his storageSignaturePubKey with that private 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 isArbitratorAvailableForLanguage(String languageCode) {
|
||||
return arbitratorsObservableMap.values().stream().anyMatch(arbitrator ->
|
||||
arbitrator.getLanguageCodes().stream().anyMatch(lc -> lc.equals(languageCode)));
|
||||
}
|
||||
|
||||
public List<String> getArbitratorLanguages(List<NodeAddress> nodeAddresses) {
|
||||
return arbitratorsObservableMap.values().stream()
|
||||
.filter(arbitrator -> nodeAddresses.stream().anyMatch(nodeAddress -> nodeAddress.equals(arbitrator.getNodeAddress())))
|
||||
.flatMap(arbitrator -> arbitrator.getLanguageCodes().stream())
|
||||
.distinct()
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
// Private
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
private void republishArbitrator() {
|
||||
Arbitrator registeredArbitrator = user.getRegisteredArbitrator();
|
||||
if (registeredArbitrator != null) {
|
||||
addArbitrator(registeredArbitrator,
|
||||
this::updateArbitratorMap,
|
||||
errorMessage -> {
|
||||
if (retryRepublishArbitratorTimer == null)
|
||||
retryRepublishArbitratorTimer = UserThread.runPeriodically(() -> {
|
||||
stopRetryRepublishArbitratorTimer();
|
||||
republishArbitrator();
|
||||
}, RETRY_REPUBLISH_SEC);
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private 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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private void stopRetryRepublishArbitratorTimer() {
|
||||
if (retryRepublishArbitratorTimer != null) {
|
||||
retryRepublishArbitratorTimer.stop();
|
||||
retryRepublishArbitratorTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
private void stopRepublishArbitratorTimer() {
|
||||
if (republishArbitratorTimer != null) {
|
||||
republishArbitratorTimer.stop();
|
||||
republishArbitratorTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
public Optional<Arbitrator> getArbitratorByNodeAddress(NodeAddress nodeAddress) {
|
||||
return arbitratorsObservableMap.containsKey(nodeAddress) ?
|
||||
Optional.of(arbitratorsObservableMap.get(nodeAddress)) :
|
||||
Optional.empty();
|
||||
}
|
||||
}
|
@ -1,123 +0,0 @@
|
||||
/*
|
||||
* This file is part of Bisq.
|
||||
*
|
||||
* Bisq is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or (at
|
||||
* your option) any later version.
|
||||
*
|
||||
* Bisq is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
|
||||
* License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package bisq.core.arbitration;
|
||||
|
||||
import bisq.core.app.BisqEnvironment;
|
||||
import bisq.core.filter.FilterManager;
|
||||
|
||||
import bisq.network.p2p.NodeAddress;
|
||||
import bisq.network.p2p.P2PService;
|
||||
import bisq.network.p2p.storage.HashMapChangedListener;
|
||||
|
||||
import bisq.common.app.DevEnv;
|
||||
import bisq.common.handlers.ErrorMessageHandler;
|
||||
import bisq.common.handlers.ResultHandler;
|
||||
import bisq.common.util.Utilities;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* Used to store arbitrators profile and load map of arbitrators
|
||||
*/
|
||||
public class ArbitratorService {
|
||||
private static final Logger log = LoggerFactory.getLogger(ArbitratorService.class);
|
||||
|
||||
private final P2PService p2PService;
|
||||
private final FilterManager filterManager;
|
||||
|
||||
interface ArbitratorMapResultHandler {
|
||||
void handleResult(Map<String, Arbitrator> arbitratorsMap);
|
||||
}
|
||||
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
// Constructor
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
@Inject
|
||||
public ArbitratorService(P2PService p2PService, FilterManager filterManager) {
|
||||
this.p2PService = p2PService;
|
||||
this.filterManager = filterManager;
|
||||
}
|
||||
|
||||
public void addHashSetChangedListener(HashMapChangedListener hashMapChangedListener) {
|
||||
p2PService.addHashSetChangedListener(hashMapChangedListener);
|
||||
}
|
||||
|
||||
public void addArbitrator(Arbitrator arbitrator, final ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) {
|
||||
log.debug("addArbitrator arbitrator.hashCode() " + arbitrator.hashCode());
|
||||
if (!BisqEnvironment.getBaseCurrencyNetwork().isMainnet() ||
|
||||
!Utilities.encodeToHex(arbitrator.getRegistrationPubKey()).equals(DevEnv.DEV_PRIVILEGE_PUB_KEY)) {
|
||||
boolean result = p2PService.addProtectedStorageEntry(arbitrator, true);
|
||||
if (result) {
|
||||
log.trace("Add arbitrator to network was successful. Arbitrator.hashCode() = " + arbitrator.hashCode());
|
||||
resultHandler.handleResult();
|
||||
} else {
|
||||
errorMessageHandler.handleErrorMessage("Add arbitrator failed");
|
||||
}
|
||||
} else {
|
||||
log.error("Attempt to publish dev arbitrator on mainnet.");
|
||||
errorMessageHandler.handleErrorMessage("Add arbitrator failed. Attempt to publish dev arbitrator on mainnet.");
|
||||
}
|
||||
}
|
||||
|
||||
public void removeArbitrator(Arbitrator arbitrator, ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) {
|
||||
log.debug("removeArbitrator arbitrator.hashCode() " + arbitrator.hashCode());
|
||||
if (p2PService.removeData(arbitrator, true)) {
|
||||
log.trace("Remove arbitrator from network was successful. Arbitrator.hashCode() = " + arbitrator.hashCode());
|
||||
resultHandler.handleResult();
|
||||
} else {
|
||||
errorMessageHandler.handleErrorMessage("Remove arbitrator failed");
|
||||
}
|
||||
}
|
||||
|
||||
P2PService getP2PService() {
|
||||
return p2PService;
|
||||
}
|
||||
|
||||
public Map<NodeAddress, Arbitrator> getArbitrators() {
|
||||
final List<String> bannedArbitrators = filterManager.getFilter() != null ? filterManager.getFilter().getArbitrators() : null;
|
||||
if (bannedArbitrators != null)
|
||||
log.warn("bannedArbitrators=" + bannedArbitrators);
|
||||
Set<Arbitrator> arbitratorSet = p2PService.getDataMap().values().stream()
|
||||
.filter(data -> data.getProtectedStoragePayload() instanceof Arbitrator)
|
||||
.map(data -> (Arbitrator) data.getProtectedStoragePayload())
|
||||
.filter(a -> bannedArbitrators == null ||
|
||||
!bannedArbitrators.contains(a.getNodeAddress().getFullAddress()))
|
||||
.collect(Collectors.toSet());
|
||||
|
||||
Map<NodeAddress, Arbitrator> map = new HashMap<>();
|
||||
for (Arbitrator arbitrator : arbitratorSet) {
|
||||
NodeAddress arbitratorNodeAddress = arbitrator.getNodeAddress();
|
||||
if (!map.containsKey(arbitratorNodeAddress))
|
||||
map.put(arbitratorNodeAddress, arbitrator);
|
||||
else
|
||||
log.warn("arbitratorAddress already exist in arbitrator map. Seems an arbitrator object is already registered with the same address.");
|
||||
}
|
||||
return map;
|
||||
}
|
||||
}
|
@ -1,177 +0,0 @@
|
||||
/*
|
||||
* This file is part of Bisq.
|
||||
*
|
||||
* Bisq is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or (at
|
||||
* your option) any later version.
|
||||
*
|
||||
* Bisq is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
|
||||
* License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package bisq.core.arbitration;
|
||||
|
||||
import bisq.core.arbitration.messages.DisputeCommunicationMessage;
|
||||
import bisq.core.arbitration.messages.DisputeMessage;
|
||||
import bisq.core.arbitration.messages.DisputeResultMessage;
|
||||
import bisq.core.arbitration.messages.OpenNewDisputeMessage;
|
||||
import bisq.core.arbitration.messages.PeerOpenedDisputeMessage;
|
||||
import bisq.core.arbitration.messages.PeerPublishedDisputePayoutTxMessage;
|
||||
import bisq.core.chat.ChatSession;
|
||||
|
||||
import bisq.network.p2p.NodeAddress;
|
||||
|
||||
import bisq.common.crypto.PubKeyRing;
|
||||
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.collections.ObservableList;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
public class DisputeChatSession extends ChatSession {
|
||||
private static final Logger log = LoggerFactory.getLogger(DisputeChatSession.class);
|
||||
|
||||
@Nullable
|
||||
private Dispute dispute;
|
||||
private DisputeManager disputeManager;
|
||||
|
||||
public DisputeChatSession(@Nullable Dispute dispute,
|
||||
DisputeManager disputeManager) {
|
||||
super(DisputeCommunicationMessage.Type.ARBITRATION);
|
||||
this.dispute = dispute;
|
||||
this.disputeManager = disputeManager;
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
// Dependent on selected dispute
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
@Override
|
||||
public boolean isClient() {
|
||||
return dispute != null && disputeManager.isTrader(dispute);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getTradeId() {
|
||||
return dispute != null ? dispute.getTradeId() : "";
|
||||
}
|
||||
|
||||
@Override
|
||||
public PubKeyRing getClientPubKeyRing() {
|
||||
// Get pubkeyring of trader. Arbitrator is considered server for the chat session
|
||||
return dispute != null ? dispute.getTraderPubKeyRing() : null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addDisputeCommunicationMessage(DisputeCommunicationMessage message) {
|
||||
if (dispute != null && (isClient() || (!isClient() && !message.isSystemMessage())))
|
||||
dispute.addDisputeCommunicationMessage(message);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void persist() {
|
||||
disputeManager.getDisputes().persist();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ObservableList<DisputeCommunicationMessage> getDisputeCommunicationMessages() {
|
||||
return dispute != null ? dispute.getDisputeCommunicationMessages() : FXCollections.observableArrayList();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean chatIsOpen() {
|
||||
return dispute != null && !dispute.isClosed();
|
||||
}
|
||||
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
// Not dependent on selected dispute
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public NodeAddress getPeerNodeAddress(DisputeCommunicationMessage message) {
|
||||
Optional<Dispute> disputeOptional = disputeManager.findDispute(message.getTradeId(), message.getTraderId());
|
||||
if (!disputeOptional.isPresent()) {
|
||||
log.warn("Could not find dispute for tradeId = {} traderId = {}",
|
||||
message.getTradeId(), message.getTraderId());
|
||||
return null;
|
||||
}
|
||||
return disputeManager.getNodeAddressPubKeyRingTuple(disputeOptional.get()).first;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public PubKeyRing getPeerPubKeyRing(DisputeCommunicationMessage message) {
|
||||
Optional<Dispute> disputeOptional = disputeManager.findDispute(message.getTradeId(), message.getTraderId());
|
||||
if (!disputeOptional.isPresent()) {
|
||||
log.warn("Could not find dispute for tradeId = {} traderId = {}",
|
||||
message.getTradeId(), message.getTraderId());
|
||||
return null;
|
||||
}
|
||||
|
||||
return disputeManager.getNodeAddressPubKeyRingTuple(disputeOptional.get()).second;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dispatchMessage(DisputeMessage message) {
|
||||
log.info("Received {} with tradeId {} and uid {}",
|
||||
message.getClass().getSimpleName(), message.getTradeId(), message.getUid());
|
||||
|
||||
if (message instanceof OpenNewDisputeMessage) {
|
||||
disputeManager.onOpenNewDisputeMessage((OpenNewDisputeMessage) message);
|
||||
} else if (message instanceof PeerOpenedDisputeMessage) {
|
||||
disputeManager.onPeerOpenedDisputeMessage((PeerOpenedDisputeMessage) message);
|
||||
} else if (message instanceof DisputeCommunicationMessage) {
|
||||
if (((DisputeCommunicationMessage) message).getType() != DisputeCommunicationMessage.Type.ARBITRATION) {
|
||||
log.debug("Ignore non dispute type communication message");
|
||||
return;
|
||||
}
|
||||
disputeManager.getChatManager().onDisputeDirectMessage((DisputeCommunicationMessage) message);
|
||||
} else if (message instanceof DisputeResultMessage) {
|
||||
disputeManager.onDisputeResultMessage((DisputeResultMessage) message);
|
||||
} else if (message instanceof PeerPublishedDisputePayoutTxMessage) {
|
||||
disputeManager.onDisputedPayoutTxMessage((PeerPublishedDisputePayoutTxMessage) message);
|
||||
} else {
|
||||
log.warn("Unsupported message at dispatchMessage.\nmessage=" + message);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<DisputeCommunicationMessage> getChatMessages() {
|
||||
return disputeManager.getDisputes().getList().stream()
|
||||
.flatMap(dispute -> dispute.getDisputeCommunicationMessages().stream())
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean channelOpen(DisputeCommunicationMessage message) {
|
||||
return disputeManager.findDispute(message.getTradeId(), message.getTraderId()).isPresent();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void storeDisputeCommunicationMessage(DisputeCommunicationMessage message) {
|
||||
Optional<Dispute> disputeOptional = disputeManager.findDispute(message.getTradeId(), message.getTraderId());
|
||||
if (disputeOptional.isPresent()) {
|
||||
if (disputeOptional.get().getDisputeCommunicationMessages().stream().noneMatch(m -> m.getUid().equals(message.getUid()))) {
|
||||
disputeOptional.get().addDisputeCommunicationMessage(message);
|
||||
} else {
|
||||
log.warn("We got a disputeCommunicationMessage what we have already stored. UId = {} TradeId = {}",
|
||||
message.getUid(), message.getTradeId());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,929 +0,0 @@
|
||||
/*
|
||||
* This file is part of Bisq.
|
||||
*
|
||||
* Bisq is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or (at
|
||||
* your option) any later version.
|
||||
*
|
||||
* Bisq is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
|
||||
* License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package bisq.core.arbitration;
|
||||
|
||||
import bisq.core.arbitration.messages.DisputeCommunicationMessage;
|
||||
import bisq.core.arbitration.messages.DisputeResultMessage;
|
||||
import bisq.core.arbitration.messages.OpenNewDisputeMessage;
|
||||
import bisq.core.arbitration.messages.PeerOpenedDisputeMessage;
|
||||
import bisq.core.arbitration.messages.PeerPublishedDisputePayoutTxMessage;
|
||||
import bisq.core.btc.exceptions.TransactionVerificationException;
|
||||
import bisq.core.btc.exceptions.TxBroadcastException;
|
||||
import bisq.core.btc.exceptions.WalletException;
|
||||
import bisq.core.btc.setup.WalletsSetup;
|
||||
import bisq.core.btc.wallet.BtcWalletService;
|
||||
import bisq.core.btc.wallet.TradeWalletService;
|
||||
import bisq.core.btc.wallet.TxBroadcaster;
|
||||
import bisq.core.chat.ChatManager;
|
||||
import bisq.core.locale.Res;
|
||||
import bisq.core.offer.OpenOffer;
|
||||
import bisq.core.offer.OpenOfferManager;
|
||||
import bisq.core.trade.Contract;
|
||||
import bisq.core.trade.Tradable;
|
||||
import bisq.core.trade.Trade;
|
||||
import bisq.core.trade.TradeManager;
|
||||
import bisq.core.trade.closed.ClosedTradableManager;
|
||||
|
||||
import bisq.network.p2p.BootstrapListener;
|
||||
import bisq.network.p2p.NodeAddress;
|
||||
import bisq.network.p2p.P2PService;
|
||||
import bisq.network.p2p.SendMailboxMessageListener;
|
||||
|
||||
import bisq.common.Timer;
|
||||
import bisq.common.UserThread;
|
||||
import bisq.common.app.Version;
|
||||
import bisq.common.crypto.KeyRing;
|
||||
import bisq.common.crypto.PubKeyRing;
|
||||
import bisq.common.handlers.FaultHandler;
|
||||
import bisq.common.handlers.ResultHandler;
|
||||
import bisq.common.proto.persistable.PersistedDataHost;
|
||||
import bisq.common.storage.Storage;
|
||||
import bisq.common.util.Tuple2;
|
||||
|
||||
import org.bitcoinj.core.AddressFormatException;
|
||||
import org.bitcoinj.core.Transaction;
|
||||
import org.bitcoinj.crypto.DeterministicKey;
|
||||
|
||||
import com.google.inject.Inject;
|
||||
|
||||
import org.fxmisc.easybind.EasyBind;
|
||||
import org.fxmisc.easybind.Subscription;
|
||||
|
||||
import javafx.beans.property.IntegerProperty;
|
||||
import javafx.beans.property.SimpleIntegerProperty;
|
||||
|
||||
import javafx.collections.ListChangeListener;
|
||||
import javafx.collections.ObservableList;
|
||||
|
||||
import java.util.Arrays;
|
||||
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 java.util.stream.Stream;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import lombok.Getter;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
public class DisputeManager implements PersistedDataHost {
|
||||
private static final Logger log = LoggerFactory.getLogger(DisputeManager.class);
|
||||
|
||||
private final TradeWalletService tradeWalletService;
|
||||
private final BtcWalletService walletService;
|
||||
private final WalletsSetup walletsSetup;
|
||||
private final TradeManager tradeManager;
|
||||
private final ClosedTradableManager closedTradableManager;
|
||||
private final OpenOfferManager openOfferManager;
|
||||
private final P2PService p2PService;
|
||||
private final KeyRing keyRing;
|
||||
private final Storage<DisputeList> disputeStorage;
|
||||
@Getter
|
||||
private DisputeList disputes;
|
||||
private final String disputeInfo;
|
||||
private final Map<String, Dispute> openDisputes;
|
||||
private final Map<String, Dispute> closedDisputes;
|
||||
private final Map<String, Timer> delayMsgMap = new HashMap<>();
|
||||
|
||||
private final Map<String, Subscription> disputeIsClosedSubscriptionsMap = new HashMap<>();
|
||||
@Getter
|
||||
private final IntegerProperty numOpenDisputes = new SimpleIntegerProperty();
|
||||
@Getter
|
||||
private final ChatManager chatManager;
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
// Constructor
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
@Inject
|
||||
public DisputeManager(P2PService p2PService,
|
||||
TradeWalletService tradeWalletService,
|
||||
BtcWalletService walletService,
|
||||
WalletsSetup walletsSetup,
|
||||
TradeManager tradeManager,
|
||||
ClosedTradableManager closedTradableManager,
|
||||
OpenOfferManager openOfferManager,
|
||||
KeyRing keyRing,
|
||||
Storage<DisputeList> storage) {
|
||||
this.p2PService = p2PService;
|
||||
this.tradeWalletService = tradeWalletService;
|
||||
this.walletService = walletService;
|
||||
this.walletsSetup = walletsSetup;
|
||||
this.tradeManager = tradeManager;
|
||||
this.closedTradableManager = closedTradableManager;
|
||||
this.openOfferManager = openOfferManager;
|
||||
this.keyRing = keyRing;
|
||||
|
||||
chatManager = new ChatManager(p2PService, walletsSetup);
|
||||
chatManager.setChatSession(new DisputeChatSession(null, this));
|
||||
|
||||
disputeStorage = storage;
|
||||
|
||||
openDisputes = new HashMap<>();
|
||||
closedDisputes = new HashMap<>();
|
||||
|
||||
disputeInfo = Res.get("support.initialInfo");
|
||||
}
|
||||
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
// API
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
@Override
|
||||
public void readPersisted() {
|
||||
disputes = new DisputeList(disputeStorage);
|
||||
disputes.readPersisted();
|
||||
disputes.stream().forEach(dispute -> dispute.setStorage(disputeStorage));
|
||||
}
|
||||
|
||||
public void onAllServicesInitialized() {
|
||||
chatManager.onAllServicesInitialized();
|
||||
p2PService.addP2PServiceListener(new BootstrapListener() {
|
||||
@Override
|
||||
public void onUpdatedDataReceived() {
|
||||
chatManager.tryApplyMessages();
|
||||
}
|
||||
});
|
||||
|
||||
walletsSetup.downloadPercentageProperty().addListener((observable, oldValue, newValue) -> {
|
||||
if (walletsSetup.isDownloadComplete())
|
||||
chatManager.tryApplyMessages();
|
||||
});
|
||||
|
||||
walletsSetup.numPeersProperty().addListener((observable, oldValue, newValue) -> {
|
||||
if (walletsSetup.hasSufficientPeersForBroadcast())
|
||||
chatManager.tryApplyMessages();
|
||||
});
|
||||
|
||||
chatManager.tryApplyMessages();
|
||||
|
||||
cleanupDisputes();
|
||||
|
||||
disputes.getList().addListener((ListChangeListener<Dispute>) change -> {
|
||||
change.next();
|
||||
onDisputesChangeListener(change.getAddedSubList(), change.getRemoved());
|
||||
});
|
||||
onDisputesChangeListener(disputes.getList(), null);
|
||||
}
|
||||
|
||||
private void onDisputesChangeListener(List<? extends Dispute> addedList,
|
||||
@Nullable List<? extends Dispute> removedList) {
|
||||
if (removedList != null) {
|
||||
removedList.forEach(dispute -> {
|
||||
String id = dispute.getId();
|
||||
if (disputeIsClosedSubscriptionsMap.containsKey(id)) {
|
||||
disputeIsClosedSubscriptionsMap.get(id).unsubscribe();
|
||||
disputeIsClosedSubscriptionsMap.remove(id);
|
||||
}
|
||||
});
|
||||
}
|
||||
addedList.forEach(dispute -> {
|
||||
String id = dispute.getId();
|
||||
Subscription disputeStateSubscription = EasyBind.subscribe(dispute.isClosedProperty(),
|
||||
isClosed -> {
|
||||
// We get the event before the list gets updated, so we execute on next frame
|
||||
UserThread.execute(() -> {
|
||||
int openDisputes = disputes.getList().stream()
|
||||
.filter(e -> !e.isClosed())
|
||||
.collect(Collectors.toList()).size();
|
||||
numOpenDisputes.set(openDisputes);
|
||||
});
|
||||
});
|
||||
disputeIsClosedSubscriptionsMap.put(id, disputeStateSubscription);
|
||||
});
|
||||
}
|
||||
|
||||
public void cleanupDisputes() {
|
||||
disputes.stream().forEach(dispute -> {
|
||||
dispute.setStorage(disputeStorage);
|
||||
if (dispute.isClosed())
|
||||
closedDisputes.put(dispute.getTradeId(), dispute);
|
||||
else
|
||||
openDisputes.put(dispute.getTradeId(), dispute);
|
||||
});
|
||||
|
||||
// If we have duplicate disputes we close the second one (might happen if both traders opened a dispute and arbitrator
|
||||
// was offline, so could not forward msg to other peer, then the arbitrator might have 4 disputes open for 1 trade)
|
||||
openDisputes.forEach((key, openDispute) -> {
|
||||
if (closedDisputes.containsKey(key)) {
|
||||
final Dispute closedDispute = closedDisputes.get(key);
|
||||
// We need to check if is from the same peer, we don't want to close the peers dispute
|
||||
if (closedDispute.getTraderId() == openDispute.getTraderId()) {
|
||||
openDispute.setIsClosed(true);
|
||||
tradeManager.closeDisputedTrade(openDispute.getTradeId());
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void sendOpenNewDisputeMessage(Dispute dispute, boolean reOpen, ResultHandler resultHandler, FaultHandler faultHandler) {
|
||||
if (!disputes.contains(dispute)) {
|
||||
final Optional<Dispute> storedDisputeOptional = findDispute(dispute.getTradeId(), dispute.getTraderId());
|
||||
if (!storedDisputeOptional.isPresent() || reOpen) {
|
||||
String sysMsg = dispute.isSupportTicket() ?
|
||||
Res.get("support.youOpenedTicket", disputeInfo, Version.VERSION)
|
||||
: Res.get("support.youOpenedDispute", disputeInfo, Version.VERSION);
|
||||
|
||||
DisputeCommunicationMessage disputeCommunicationMessage = new DisputeCommunicationMessage(
|
||||
chatManager.getChatSession().getType(),
|
||||
dispute.getTradeId(),
|
||||
keyRing.getPubKeyRing().hashCode(),
|
||||
false,
|
||||
Res.get("support.systemMsg", sysMsg),
|
||||
p2PService.getAddress()
|
||||
);
|
||||
disputeCommunicationMessage.setSystemMessage(true);
|
||||
dispute.addDisputeCommunicationMessage(disputeCommunicationMessage);
|
||||
if (!reOpen) {
|
||||
disputes.add(dispute);
|
||||
}
|
||||
|
||||
NodeAddress peersNodeAddress = dispute.getContract().getArbitratorNodeAddress();
|
||||
OpenNewDisputeMessage openNewDisputeMessage = new OpenNewDisputeMessage(dispute, p2PService.getAddress(),
|
||||
UUID.randomUUID().toString());
|
||||
log.info("Send {} to peer {}. tradeId={}, openNewDisputeMessage.uid={}, " +
|
||||
"disputeCommunicationMessage.uid={}",
|
||||
openNewDisputeMessage.getClass().getSimpleName(), peersNodeAddress,
|
||||
openNewDisputeMessage.getTradeId(), openNewDisputeMessage.getUid(),
|
||||
disputeCommunicationMessage.getUid());
|
||||
p2PService.sendEncryptedMailboxMessage(peersNodeAddress,
|
||||
dispute.getArbitratorPubKeyRing(),
|
||||
openNewDisputeMessage,
|
||||
new SendMailboxMessageListener() {
|
||||
@Override
|
||||
public void onArrived() {
|
||||
log.info("{} arrived at peer {}. tradeId={}, openNewDisputeMessage.uid={}, " +
|
||||
"disputeCommunicationMessage.uid={}",
|
||||
openNewDisputeMessage.getClass().getSimpleName(), peersNodeAddress,
|
||||
openNewDisputeMessage.getTradeId(), openNewDisputeMessage.getUid(),
|
||||
disputeCommunicationMessage.getUid());
|
||||
|
||||
// We use the disputeCommunicationMessage wrapped inside the openNewDisputeMessage for
|
||||
// the state, as that is displayed to the user and we only persist that msg
|
||||
disputeCommunicationMessage.setArrived(true);
|
||||
disputes.persist();
|
||||
resultHandler.handleResult();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStoredInMailbox() {
|
||||
log.info("{} stored in mailbox for peer {}. tradeId={}, openNewDisputeMessage.uid={}, " +
|
||||
"disputeCommunicationMessage.uid={}",
|
||||
openNewDisputeMessage.getClass().getSimpleName(), peersNodeAddress,
|
||||
openNewDisputeMessage.getTradeId(), openNewDisputeMessage.getUid(),
|
||||
disputeCommunicationMessage.getUid());
|
||||
|
||||
// We use the disputeCommunicationMessage wrapped inside the openNewDisputeMessage for
|
||||
// the state, as that is displayed to the user and we only persist that msg
|
||||
disputeCommunicationMessage.setStoredInMailbox(true);
|
||||
disputes.persist();
|
||||
resultHandler.handleResult();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFault(String errorMessage) {
|
||||
log.error("{} failed: Peer {}. tradeId={}, openNewDisputeMessage.uid={}, " +
|
||||
"disputeCommunicationMessage.uid={}, errorMessage={}",
|
||||
openNewDisputeMessage.getClass().getSimpleName(), peersNodeAddress,
|
||||
openNewDisputeMessage.getTradeId(), openNewDisputeMessage.getUid(),
|
||||
disputeCommunicationMessage.getUid(), errorMessage);
|
||||
|
||||
// We use the disputeCommunicationMessage wrapped inside the openNewDisputeMessage for
|
||||
// the state, as that is displayed to the user and we only persist that msg
|
||||
disputeCommunicationMessage.setSendMessageError(errorMessage);
|
||||
disputes.persist();
|
||||
faultHandler.handleFault("Sending dispute message failed: " +
|
||||
errorMessage, new MessageDeliveryFailedException());
|
||||
}
|
||||
}
|
||||
);
|
||||
} else {
|
||||
final String msg = "We got a dispute already open for that trade and trading peer.\n" +
|
||||
"TradeId = " + dispute.getTradeId();
|
||||
log.warn(msg);
|
||||
faultHandler.handleFault(msg, new DisputeAlreadyOpenException());
|
||||
}
|
||||
} else {
|
||||
final String msg = "We got a dispute msg what we have already stored. TradeId = " + dispute.getTradeId();
|
||||
log.warn(msg);
|
||||
faultHandler.handleFault(msg, new DisputeAlreadyOpenException());
|
||||
}
|
||||
}
|
||||
|
||||
// arbitrator sends that to trading peer when he received openDispute request
|
||||
private String sendPeerOpenedDisputeMessage(Dispute disputeFromOpener,
|
||||
Contract contractFromOpener,
|
||||
PubKeyRing pubKeyRing) {
|
||||
Dispute dispute = new Dispute(
|
||||
disputeStorage,
|
||||
disputeFromOpener.getTradeId(),
|
||||
pubKeyRing.hashCode(),
|
||||
!disputeFromOpener.isDisputeOpenerIsBuyer(),
|
||||
!disputeFromOpener.isDisputeOpenerIsMaker(),
|
||||
pubKeyRing,
|
||||
disputeFromOpener.getTradeDate().getTime(),
|
||||
contractFromOpener,
|
||||
disputeFromOpener.getContractHash(),
|
||||
disputeFromOpener.getDepositTxSerialized(),
|
||||
disputeFromOpener.getPayoutTxSerialized(),
|
||||
disputeFromOpener.getDepositTxId(),
|
||||
disputeFromOpener.getPayoutTxId(),
|
||||
disputeFromOpener.getContractAsJson(),
|
||||
disputeFromOpener.getMakerContractSignature(),
|
||||
disputeFromOpener.getTakerContractSignature(),
|
||||
disputeFromOpener.getArbitratorPubKeyRing(),
|
||||
disputeFromOpener.isSupportTicket()
|
||||
);
|
||||
final Optional<Dispute> storedDisputeOptional = findDispute(dispute.getTradeId(), dispute.getTraderId());
|
||||
if (!storedDisputeOptional.isPresent()) {
|
||||
String sysMsg = dispute.isSupportTicket() ?
|
||||
Res.get("support.peerOpenedTicket", disputeInfo)
|
||||
: Res.get("support.peerOpenedDispute", disputeInfo);
|
||||
DisputeCommunicationMessage disputeCommunicationMessage = new DisputeCommunicationMessage(
|
||||
chatManager.getChatSession().getType(),
|
||||
dispute.getTradeId(),
|
||||
keyRing.getPubKeyRing().hashCode(),
|
||||
false,
|
||||
Res.get("support.systemMsg", sysMsg),
|
||||
p2PService.getAddress()
|
||||
);
|
||||
disputeCommunicationMessage.setSystemMessage(true);
|
||||
dispute.addDisputeCommunicationMessage(disputeCommunicationMessage);
|
||||
disputes.add(dispute);
|
||||
|
||||
// we mirrored dispute already!
|
||||
Contract contract = dispute.getContract();
|
||||
PubKeyRing peersPubKeyRing = dispute.isDisputeOpenerIsBuyer() ? contract.getBuyerPubKeyRing() : contract.getSellerPubKeyRing();
|
||||
NodeAddress peersNodeAddress = dispute.isDisputeOpenerIsBuyer() ? contract.getBuyerNodeAddress() : contract.getSellerNodeAddress();
|
||||
PeerOpenedDisputeMessage peerOpenedDisputeMessage = new PeerOpenedDisputeMessage(dispute,
|
||||
p2PService.getAddress(),
|
||||
UUID.randomUUID().toString());
|
||||
log.info("Send {} to peer {}. tradeId={}, peerOpenedDisputeMessage.uid={}, " +
|
||||
"disputeCommunicationMessage.uid={}",
|
||||
peerOpenedDisputeMessage.getClass().getSimpleName(), peersNodeAddress,
|
||||
peerOpenedDisputeMessage.getTradeId(), peerOpenedDisputeMessage.getUid(),
|
||||
disputeCommunicationMessage.getUid());
|
||||
p2PService.sendEncryptedMailboxMessage(peersNodeAddress,
|
||||
peersPubKeyRing,
|
||||
peerOpenedDisputeMessage,
|
||||
new SendMailboxMessageListener() {
|
||||
@Override
|
||||
public void onArrived() {
|
||||
log.info("{} arrived at peer {}. tradeId={}, peerOpenedDisputeMessage.uid={}, " +
|
||||
"disputeCommunicationMessage.uid={}",
|
||||
peerOpenedDisputeMessage.getClass().getSimpleName(), peersNodeAddress,
|
||||
peerOpenedDisputeMessage.getTradeId(), peerOpenedDisputeMessage.getUid(),
|
||||
disputeCommunicationMessage.getUid());
|
||||
|
||||
// We use the disputeCommunicationMessage wrapped inside the peerOpenedDisputeMessage for
|
||||
// the state, as that is displayed to the user and we only persist that msg
|
||||
disputeCommunicationMessage.setArrived(true);
|
||||
disputes.persist();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStoredInMailbox() {
|
||||
log.info("{} stored in mailbox for peer {}. tradeId={}, peerOpenedDisputeMessage.uid={}, " +
|
||||
"disputeCommunicationMessage.uid={}",
|
||||
peerOpenedDisputeMessage.getClass().getSimpleName(), peersNodeAddress,
|
||||
peerOpenedDisputeMessage.getTradeId(), peerOpenedDisputeMessage.getUid(),
|
||||
disputeCommunicationMessage.getUid());
|
||||
|
||||
// We use the disputeCommunicationMessage wrapped inside the peerOpenedDisputeMessage for
|
||||
// the state, as that is displayed to the user and we only persist that msg
|
||||
disputeCommunicationMessage.setStoredInMailbox(true);
|
||||
disputes.persist();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFault(String errorMessage) {
|
||||
log.error("{} failed: Peer {}. tradeId={}, peerOpenedDisputeMessage.uid={}, " +
|
||||
"disputeCommunicationMessage.uid={}, errorMessage={}",
|
||||
peerOpenedDisputeMessage.getClass().getSimpleName(), peersNodeAddress,
|
||||
peerOpenedDisputeMessage.getTradeId(), peerOpenedDisputeMessage.getUid(),
|
||||
disputeCommunicationMessage.getUid(), errorMessage);
|
||||
|
||||
// We use the disputeCommunicationMessage wrapped inside the peerOpenedDisputeMessage for
|
||||
// the state, as that is displayed to the user and we only persist that msg
|
||||
disputeCommunicationMessage.setSendMessageError(errorMessage);
|
||||
disputes.persist();
|
||||
}
|
||||
}
|
||||
);
|
||||
return null;
|
||||
} else {
|
||||
String msg = "We got a dispute already open for that trade and trading peer.\n" +
|
||||
"TradeId = " + dispute.getTradeId();
|
||||
log.warn(msg);
|
||||
return msg;
|
||||
}
|
||||
}
|
||||
|
||||
// arbitrator send result to trader
|
||||
public void sendDisputeResultMessage(DisputeResult disputeResult, Dispute dispute, String text) {
|
||||
DisputeCommunicationMessage disputeCommunicationMessage = new DisputeCommunicationMessage(
|
||||
chatManager.getChatSession().getType(),
|
||||
dispute.getTradeId(),
|
||||
dispute.getTraderPubKeyRing().hashCode(),
|
||||
false,
|
||||
text,
|
||||
p2PService.getAddress()
|
||||
);
|
||||
|
||||
dispute.addDisputeCommunicationMessage(disputeCommunicationMessage);
|
||||
disputeResult.setDisputeCommunicationMessage(disputeCommunicationMessage);
|
||||
|
||||
NodeAddress peersNodeAddress;
|
||||
Contract contract = dispute.getContract();
|
||||
if (contract.getBuyerPubKeyRing().equals(dispute.getTraderPubKeyRing()))
|
||||
peersNodeAddress = contract.getBuyerNodeAddress();
|
||||
else
|
||||
peersNodeAddress = contract.getSellerNodeAddress();
|
||||
DisputeResultMessage disputeResultMessage = new DisputeResultMessage(disputeResult, p2PService.getAddress(),
|
||||
UUID.randomUUID().toString());
|
||||
log.info("Send {} to peer {}. tradeId={}, disputeResultMessage.uid={}, disputeCommunicationMessage.uid={}",
|
||||
disputeResultMessage.getClass().getSimpleName(), peersNodeAddress, disputeResultMessage.getTradeId(),
|
||||
disputeResultMessage.getUid(), disputeCommunicationMessage.getUid());
|
||||
p2PService.sendEncryptedMailboxMessage(peersNodeAddress,
|
||||
dispute.getTraderPubKeyRing(),
|
||||
disputeResultMessage,
|
||||
new SendMailboxMessageListener() {
|
||||
@Override
|
||||
public void onArrived() {
|
||||
log.info("{} arrived at peer {}. tradeId={}, disputeResultMessage.uid={}, " +
|
||||
"disputeCommunicationMessage.uid={}",
|
||||
disputeResultMessage.getClass().getSimpleName(), peersNodeAddress,
|
||||
disputeResultMessage.getTradeId(), disputeResultMessage.getUid(),
|
||||
disputeCommunicationMessage.getUid());
|
||||
|
||||
// We use the disputeCommunicationMessage wrapped inside the disputeResultMessage for
|
||||
// the state, as that is displayed to the user and we only persist that msg
|
||||
disputeCommunicationMessage.setArrived(true);
|
||||
disputes.persist();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStoredInMailbox() {
|
||||
log.info("{} stored in mailbox for peer {}. tradeId={}, disputeResultMessage.uid={}, " +
|
||||
"disputeCommunicationMessage.uid={}",
|
||||
disputeResultMessage.getClass().getSimpleName(), peersNodeAddress,
|
||||
disputeResultMessage.getTradeId(), disputeResultMessage.getUid(),
|
||||
disputeCommunicationMessage.getUid());
|
||||
|
||||
// We use the disputeCommunicationMessage wrapped inside the disputeResultMessage for
|
||||
// the state, as that is displayed to the user and we only persist that msg
|
||||
disputeCommunicationMessage.setStoredInMailbox(true);
|
||||
disputes.persist();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFault(String errorMessage) {
|
||||
log.error("{} failed: Peer {}. tradeId={}, disputeResultMessage.uid={}, " +
|
||||
"disputeCommunicationMessage.uid={}, errorMessage={}",
|
||||
disputeResultMessage.getClass().getSimpleName(), peersNodeAddress,
|
||||
disputeResultMessage.getTradeId(), disputeResultMessage.getUid(),
|
||||
disputeCommunicationMessage.getUid(), errorMessage);
|
||||
|
||||
// We use the disputeCommunicationMessage wrapped inside the disputeResultMessage for
|
||||
// the state, as that is displayed to the user and we only persist that msg
|
||||
disputeCommunicationMessage.setSendMessageError(errorMessage);
|
||||
disputes.persist();
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// winner (or buyer in case of 50/50) sends tx to other peer
|
||||
private void sendPeerPublishedPayoutTxMessage(Transaction transaction, Dispute dispute, Contract contract) {
|
||||
PubKeyRing peersPubKeyRing = dispute.isDisputeOpenerIsBuyer() ? contract.getSellerPubKeyRing() : contract.getBuyerPubKeyRing();
|
||||
NodeAddress peersNodeAddress = dispute.isDisputeOpenerIsBuyer() ? contract.getSellerNodeAddress() : contract.getBuyerNodeAddress();
|
||||
log.trace("sendPeerPublishedPayoutTxMessage to peerAddress " + peersNodeAddress);
|
||||
final PeerPublishedDisputePayoutTxMessage message = new PeerPublishedDisputePayoutTxMessage(transaction.bitcoinSerialize(),
|
||||
dispute.getTradeId(),
|
||||
p2PService.getAddress(),
|
||||
UUID.randomUUID().toString());
|
||||
log.info("Send {} to peer {}. tradeId={}, uid={}",
|
||||
message.getClass().getSimpleName(), peersNodeAddress, message.getTradeId(), message.getUid());
|
||||
p2PService.sendEncryptedMailboxMessage(peersNodeAddress,
|
||||
peersPubKeyRing,
|
||||
message,
|
||||
new SendMailboxMessageListener() {
|
||||
@Override
|
||||
public void onArrived() {
|
||||
log.info("{} arrived at peer {}. tradeId={}, uid={}",
|
||||
message.getClass().getSimpleName(), peersNodeAddress, message.getTradeId(), message.getUid());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStoredInMailbox() {
|
||||
log.info("{} stored in mailbox for peer {}. tradeId={}, uid={}",
|
||||
message.getClass().getSimpleName(), peersNodeAddress, message.getTradeId(), message.getUid());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFault(String errorMessage) {
|
||||
log.error("{} failed: Peer {}. tradeId={}, uid={}, errorMessage={}",
|
||||
message.getClass().getSimpleName(), peersNodeAddress, message.getTradeId(), message.getUid(), errorMessage);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
// Incoming message
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// arbitrator receives that from trader who opens dispute
|
||||
public void onOpenNewDisputeMessage(OpenNewDisputeMessage openNewDisputeMessage) {
|
||||
String errorMessage;
|
||||
Dispute dispute = openNewDisputeMessage.getDispute();
|
||||
Contract contractFromOpener = dispute.getContract();
|
||||
PubKeyRing peersPubKeyRing = dispute.isDisputeOpenerIsBuyer() ? contractFromOpener.getSellerPubKeyRing() : contractFromOpener.getBuyerPubKeyRing();
|
||||
if (isArbitrator(dispute)) {
|
||||
if (!disputes.contains(dispute)) {
|
||||
final Optional<Dispute> storedDisputeOptional = findDispute(dispute.getTradeId(), dispute.getTraderId());
|
||||
if (!storedDisputeOptional.isPresent()) {
|
||||
dispute.setStorage(disputeStorage);
|
||||
disputes.add(dispute);
|
||||
errorMessage = sendPeerOpenedDisputeMessage(dispute, contractFromOpener, peersPubKeyRing);
|
||||
} else {
|
||||
errorMessage = "We got a dispute already open for that trade and trading peer.\n" +
|
||||
"TradeId = " + dispute.getTradeId();
|
||||
log.warn(errorMessage);
|
||||
}
|
||||
} else {
|
||||
errorMessage = "We got a dispute msg what we have already stored. TradeId = " + dispute.getTradeId();
|
||||
log.warn(errorMessage);
|
||||
}
|
||||
} else {
|
||||
errorMessage = "Trader received openNewDisputeMessage. That must never happen.";
|
||||
log.error(errorMessage);
|
||||
}
|
||||
|
||||
// We use the DisputeCommunicationMessage not the openNewDisputeMessage for the ACK
|
||||
ObservableList<DisputeCommunicationMessage> messages = openNewDisputeMessage.getDispute().getDisputeCommunicationMessages();
|
||||
if (!messages.isEmpty()) {
|
||||
DisputeCommunicationMessage msg = messages.get(0);
|
||||
PubKeyRing sendersPubKeyRing = dispute.isDisputeOpenerIsBuyer() ? contractFromOpener.getBuyerPubKeyRing() : contractFromOpener.getSellerPubKeyRing();
|
||||
chatManager.sendAckMessage(msg, sendersPubKeyRing, errorMessage == null, errorMessage);
|
||||
}
|
||||
}
|
||||
|
||||
// not dispute requester receives that from arbitrator
|
||||
public void onPeerOpenedDisputeMessage(PeerOpenedDisputeMessage peerOpenedDisputeMessage) {
|
||||
String errorMessage;
|
||||
Dispute dispute = peerOpenedDisputeMessage.getDispute();
|
||||
if (!isArbitrator(dispute)) {
|
||||
if (!disputes.contains(dispute)) {
|
||||
final Optional<Dispute> storedDisputeOptional = findDispute(dispute.getTradeId(), dispute.getTraderId());
|
||||
if (!storedDisputeOptional.isPresent()) {
|
||||
dispute.setStorage(disputeStorage);
|
||||
disputes.add(dispute);
|
||||
Optional<Trade> tradeOptional = tradeManager.getTradeById(dispute.getTradeId());
|
||||
tradeOptional.ifPresent(trade -> trade.setDisputeState(Trade.DisputeState.DISPUTE_STARTED_BY_PEER));
|
||||
errorMessage = null;
|
||||
} else {
|
||||
errorMessage = "We got a dispute already open for that trade and trading peer.\n" +
|
||||
"TradeId = " + dispute.getTradeId();
|
||||
log.warn(errorMessage);
|
||||
}
|
||||
} else {
|
||||
errorMessage = "We got a dispute msg what we have already stored. TradeId = " + dispute.getTradeId();
|
||||
log.warn(errorMessage);
|
||||
}
|
||||
} else {
|
||||
errorMessage = "Arbitrator received peerOpenedDisputeMessage. That must never happen.";
|
||||
log.error(errorMessage);
|
||||
}
|
||||
|
||||
// We use the DisputeCommunicationMessage not the peerOpenedDisputeMessage for the ACK
|
||||
ObservableList<DisputeCommunicationMessage> messages = peerOpenedDisputeMessage.getDispute().getDisputeCommunicationMessages();
|
||||
if (!messages.isEmpty()) {
|
||||
DisputeCommunicationMessage msg = messages.get(0);
|
||||
chatManager.sendAckMessage(msg, dispute.getArbitratorPubKeyRing(), errorMessage == null, errorMessage);
|
||||
}
|
||||
|
||||
chatManager.sendAckMessage(peerOpenedDisputeMessage, dispute.getArbitratorPubKeyRing(), errorMessage == null, errorMessage);
|
||||
}
|
||||
|
||||
// We get that message at both peers. The dispute object is in context of the trader
|
||||
public void onDisputeResultMessage(DisputeResultMessage disputeResultMessage) {
|
||||
String errorMessage = null;
|
||||
boolean success = false;
|
||||
PubKeyRing arbitratorsPubKeyRing = null;
|
||||
DisputeResult disputeResult = disputeResultMessage.getDisputeResult();
|
||||
|
||||
if (isArbitrator(disputeResult)) {
|
||||
log.error("Arbitrator received disputeResultMessage. That must never happen.");
|
||||
return;
|
||||
}
|
||||
|
||||
final String tradeId = disputeResult.getTradeId();
|
||||
Optional<Dispute> disputeOptional = findDispute(tradeId, disputeResult.getTraderId());
|
||||
final String uid = disputeResultMessage.getUid();
|
||||
if (!disputeOptional.isPresent()) {
|
||||
log.debug("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 delay2 sec. to be sure the comm. msg gets added first
|
||||
Timer timer = UserThread.runAfter(() -> onDisputeResultMessage(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();
|
||||
try {
|
||||
cleanupRetryMap(uid);
|
||||
arbitratorsPubKeyRing = dispute.getArbitratorPubKeyRing();
|
||||
DisputeCommunicationMessage disputeCommunicationMessage = disputeResult.getDisputeCommunicationMessage();
|
||||
if (!dispute.getDisputeCommunicationMessages().contains(disputeCommunicationMessage))
|
||||
dispute.addDisputeCommunicationMessage(disputeCommunicationMessage);
|
||||
else if (disputeCommunicationMessage != null)
|
||||
log.warn("We got a dispute mail msg what we have already stored. TradeId = " + disputeCommunicationMessage.getTradeId());
|
||||
|
||||
dispute.setIsClosed(true);
|
||||
|
||||
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);
|
||||
|
||||
// We need to avoid publishing the tx from both traders as it would create problems with zero confirmation withdrawals
|
||||
// There would be different transactions if both sign and publish (signers: once buyer+arb, once seller+arb)
|
||||
// The tx publisher is the winner or in case both get 50% the buyer, as the buyer has more inventive to publish the tx as he receives
|
||||
// more BTC as he has deposited
|
||||
final Contract contract = dispute.getContract();
|
||||
|
||||
boolean isBuyer = keyRing.getPubKeyRing().equals(contract.getBuyerPubKeyRing());
|
||||
DisputeResult.Winner publisher = disputeResult.getWinner();
|
||||
|
||||
// Sometimes the user who receives the trade amount is never online, so we might want to
|
||||
// let the loser publish the tx. When the winner comes online he gets his funds as it was published by the other peer.
|
||||
// Default isLoserPublisher is set to false
|
||||
if (disputeResult.isLoserPublisher()) {
|
||||
// we invert the logic
|
||||
if (publisher == DisputeResult.Winner.BUYER)
|
||||
publisher = DisputeResult.Winner.SELLER;
|
||||
else if (publisher == DisputeResult.Winner.SELLER)
|
||||
publisher = DisputeResult.Winner.BUYER;
|
||||
}
|
||||
|
||||
if ((isBuyer && publisher == DisputeResult.Winner.BUYER)
|
||||
|| (!isBuyer && publisher == DisputeResult.Winner.SELLER)) {
|
||||
|
||||
final Optional<Trade> tradeOptional = tradeManager.getTradeById(tradeId);
|
||||
Transaction payoutTx = null;
|
||||
if (tradeOptional.isPresent()) {
|
||||
payoutTx = tradeOptional.get().getPayoutTx();
|
||||
} else {
|
||||
final Optional<Tradable> tradableOptional = closedTradableManager.getTradableById(tradeId);
|
||||
if (tradableOptional.isPresent() && tradableOptional.get() instanceof Trade) {
|
||||
payoutTx = ((Trade) tradableOptional.get()).getPayoutTx();
|
||||
}
|
||||
}
|
||||
|
||||
if (payoutTx == null) {
|
||||
if (dispute.getDepositTxSerialized() != null) {
|
||||
byte[] multiSigPubKey = isBuyer ? contract.getBuyerMultiSigPubKey() : contract.getSellerMultiSigPubKey();
|
||||
DeterministicKey multiSigKeyPair = walletService.getMultiSigKeyPair(dispute.getTradeId(), multiSigPubKey);
|
||||
Transaction signedDisputedPayoutTx = tradeWalletService.traderSignAndFinalizeDisputedPayoutTx(
|
||||
dispute.getDepositTxSerialized(),
|
||||
disputeResult.getArbitratorSignature(),
|
||||
disputeResult.getBuyerPayoutAmount(),
|
||||
disputeResult.getSellerPayoutAmount(),
|
||||
contract.getBuyerPayoutAddressString(),
|
||||
contract.getSellerPayoutAddressString(),
|
||||
multiSigKeyPair,
|
||||
contract.getBuyerMultiSigPubKey(),
|
||||
contract.getSellerMultiSigPubKey(),
|
||||
disputeResult.getArbitratorPubKey()
|
||||
);
|
||||
Transaction committedDisputedPayoutTx = tradeWalletService.addTxToWallet(signedDisputedPayoutTx);
|
||||
tradeWalletService.broadcastTx(committedDisputedPayoutTx, new TxBroadcaster.Callback() {
|
||||
@Override
|
||||
public void onSuccess(Transaction transaction) {
|
||||
// after successful publish we send peer the tx
|
||||
|
||||
dispute.setDisputePayoutTxId(transaction.getHashAsString());
|
||||
sendPeerPublishedPayoutTxMessage(transaction, dispute, contract);
|
||||
|
||||
// set state after payout as we call swapTradeEntryToAvailableEntry
|
||||
if (tradeManager.getTradeById(dispute.getTradeId()).isPresent())
|
||||
tradeManager.closeDisputedTrade(dispute.getTradeId());
|
||||
else {
|
||||
Optional<OpenOffer> openOfferOptional = openOfferManager.getOpenOfferById(dispute.getTradeId());
|
||||
openOfferOptional.ifPresent(openOffer -> openOfferManager.closeOpenOffer(openOffer.getOffer()));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(TxBroadcastException exception) {
|
||||
log.error(exception.getMessage());
|
||||
}
|
||||
}, 15);
|
||||
|
||||
success = true;
|
||||
} else {
|
||||
errorMessage = "DepositTx is null. TradeId = " + tradeId;
|
||||
log.warn(errorMessage);
|
||||
success = false;
|
||||
}
|
||||
} else {
|
||||
log.warn("We got already a payout tx. That might be the case if the other peer did not get the " +
|
||||
"payout tx and opened a dispute. TradeId = " + tradeId);
|
||||
dispute.setDisputePayoutTxId(payoutTx.getHashAsString());
|
||||
sendPeerPublishedPayoutTxMessage(payoutTx, dispute, contract);
|
||||
|
||||
success = true;
|
||||
}
|
||||
|
||||
} else {
|
||||
log.trace("We don't publish the tx as we are not the winning party.");
|
||||
// Clean up tangling trades
|
||||
if (dispute.disputeResultProperty().get() != null &&
|
||||
dispute.isClosed() &&
|
||||
tradeManager.getTradeById(dispute.getTradeId()).isPresent()) {
|
||||
tradeManager.closeDisputedTrade(dispute.getTradeId());
|
||||
}
|
||||
|
||||
success = true;
|
||||
}
|
||||
} catch (TransactionVerificationException e) {
|
||||
errorMessage = "Error at traderSignAndFinalizeDisputedPayoutTx " + e.toString();
|
||||
log.error(errorMessage, e);
|
||||
success = false;
|
||||
|
||||
// We prefer to close the dispute in that case. If there was no deposit tx and a random tx was used
|
||||
// we get a TransactionVerificationException. No reason to keep that dispute open...
|
||||
if (tradeManager.getTradeById(dispute.getTradeId()).isPresent())
|
||||
tradeManager.closeDisputedTrade(dispute.getTradeId());
|
||||
else {
|
||||
Optional<OpenOffer> openOfferOptional = openOfferManager.getOpenOfferById(dispute.getTradeId());
|
||||
openOfferOptional.ifPresent(openOffer -> openOfferManager.closeOpenOffer(openOffer.getOffer()));
|
||||
}
|
||||
dispute.setIsClosed(true);
|
||||
|
||||
throw new RuntimeException(errorMessage);
|
||||
} catch (AddressFormatException | WalletException e) {
|
||||
errorMessage = "Error at traderSignAndFinalizeDisputedPayoutTx " + e.toString();
|
||||
log.error(errorMessage, e);
|
||||
success = false;
|
||||
throw new RuntimeException(errorMessage);
|
||||
} finally {
|
||||
if (arbitratorsPubKeyRing != null) {
|
||||
// We use the disputeCommunicationMessage as we only persist those not the disputeResultMessage.
|
||||
// If we would use the disputeResultMessage we could not lookup for the msg when we receive the AckMessage.
|
||||
DisputeCommunicationMessage disputeCommunicationMessage = disputeResultMessage.getDisputeResult().getDisputeCommunicationMessage();
|
||||
chatManager.sendAckMessage(disputeCommunicationMessage, arbitratorsPubKeyRing, success, errorMessage);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Losing trader or in case of 50/50 the seller gets the tx sent from the winner or buyer
|
||||
public void onDisputedPayoutTxMessage(PeerPublishedDisputePayoutTxMessage peerPublishedDisputePayoutTxMessage) {
|
||||
final String uid = peerPublishedDisputePayoutTxMessage.getUid();
|
||||
final String tradeId = peerPublishedDisputePayoutTxMessage.getTradeId();
|
||||
Optional<Dispute> disputeOptional = findOwnDispute(tradeId);
|
||||
if (!disputeOptional.isPresent()) {
|
||||
log.debug("We got a peerPublishedPayoutTxMessage but we don't have a matching dispute. TradeId = " + tradeId);
|
||||
if (!delayMsgMap.containsKey(uid)) {
|
||||
// We delay 3 sec. to be sure the close msg gets added first
|
||||
Timer timer = UserThread.runAfter(() -> onDisputedPayoutTxMessage(peerPublishedDisputePayoutTxMessage), 3);
|
||||
delayMsgMap.put(uid, timer);
|
||||
} else {
|
||||
log.warn("We got a peerPublishedPayoutTxMessage after we already repeated to apply the message after a delay. " +
|
||||
"That should never happen. TradeId = " + tradeId);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
Dispute dispute = disputeOptional.get();
|
||||
final Contract contract = dispute.getContract();
|
||||
PubKeyRing ownPubKeyRing = keyRing.getPubKeyRing();
|
||||
boolean isBuyer = ownPubKeyRing.equals(contract.getBuyerPubKeyRing());
|
||||
PubKeyRing peersPubKeyRing = isBuyer ? contract.getSellerPubKeyRing() : contract.getBuyerPubKeyRing();
|
||||
|
||||
cleanupRetryMap(uid);
|
||||
Transaction walletTx = tradeWalletService.addTxToWallet(peerPublishedDisputePayoutTxMessage.getTransaction());
|
||||
dispute.setDisputePayoutTxId(walletTx.getHashAsString());
|
||||
BtcWalletService.printTx("Disputed payoutTx received from peer", walletTx);
|
||||
|
||||
// We can only send the ack msg if we have the peersPubKeyRing which requires the dispute
|
||||
chatManager.sendAckMessage(peerPublishedDisputePayoutTxMessage, peersPubKeyRing, true, null);
|
||||
}
|
||||
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
// Getters
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
public Storage<DisputeList> getDisputeStorage() {
|
||||
return disputeStorage;
|
||||
}
|
||||
|
||||
public ObservableList<Dispute> getDisputesAsObservableList() {
|
||||
return disputes.getList();
|
||||
}
|
||||
|
||||
public boolean isTrader(Dispute dispute) {
|
||||
return keyRing.getPubKeyRing().equals(dispute.getTraderPubKeyRing());
|
||||
}
|
||||
|
||||
private boolean isArbitrator(Dispute dispute) {
|
||||
return keyRing.getPubKeyRing().equals(dispute.getArbitratorPubKeyRing());
|
||||
}
|
||||
|
||||
private boolean isArbitrator(DisputeResult disputeResult) {
|
||||
return Arrays.equals(disputeResult.getArbitratorPubKey(),
|
||||
walletService.getArbitratorAddressEntry().getPubKey());
|
||||
}
|
||||
|
||||
public String getNrOfDisputes(boolean isBuyer, Contract contract) {
|
||||
return String.valueOf(getDisputesAsObservableList().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());
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
// Utils
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
|
||||
public Tuple2<NodeAddress, PubKeyRing> getNodeAddressPubKeyRingTuple(Dispute dispute) {
|
||||
PubKeyRing receiverPubKeyRing = null;
|
||||
NodeAddress peerNodeAddress = null;
|
||||
if (isTrader(dispute)) {
|
||||
receiverPubKeyRing = dispute.getArbitratorPubKeyRing();
|
||||
peerNodeAddress = dispute.getContract().getArbitratorNodeAddress();
|
||||
} else if (isArbitrator(dispute)) {
|
||||
receiverPubKeyRing = dispute.getTraderPubKeyRing();
|
||||
Contract contract = dispute.getContract();
|
||||
if (contract.getBuyerPubKeyRing().equals(receiverPubKeyRing))
|
||||
peerNodeAddress = contract.getBuyerNodeAddress();
|
||||
else
|
||||
peerNodeAddress = contract.getSellerNodeAddress();
|
||||
} else {
|
||||
log.error("That must not happen. Trader cannot communicate to other trader.");
|
||||
}
|
||||
return new Tuple2<>(peerNodeAddress, receiverPubKeyRing);
|
||||
}
|
||||
|
||||
public Optional<Dispute> findDispute(String tradeId, int traderId) {
|
||||
return disputes.stream().filter(e -> e.getTradeId().equals(tradeId) && e.getTraderId() == traderId).findAny();
|
||||
}
|
||||
|
||||
public Optional<Dispute> findOwnDispute(String tradeId) {
|
||||
return getDisputeStream(tradeId).findAny();
|
||||
}
|
||||
|
||||
private Stream<Dispute> getDisputeStream(String tradeId) {
|
||||
return disputes.stream().filter(e -> e.getTradeId().equals(tradeId));
|
||||
}
|
||||
|
||||
private void cleanupRetryMap(String uid) {
|
||||
if (delayMsgMap.containsKey(uid)) {
|
||||
Timer timer = delayMsgMap.remove(uid);
|
||||
if (timer != null)
|
||||
timer.stop();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -341,7 +341,10 @@ public class TradeWalletService {
|
||||
* @return A data container holding the inputs, the output value and address
|
||||
* @throws TransactionVerificationException
|
||||
*/
|
||||
public InputsAndChangeOutput takerCreatesDepositsTxInputs(Transaction takeOfferFeeTx, Coin inputAmount, Coin txFee, Address takersAddress) throws
|
||||
public InputsAndChangeOutput takerCreatesDepositsTxInputs(Transaction takeOfferFeeTx,
|
||||
Coin inputAmount,
|
||||
Coin txFee,
|
||||
Address takersAddress) throws
|
||||
TransactionVerificationException {
|
||||
log.debug("takerCreatesDepositsTxInputs called");
|
||||
log.debug("inputAmount {}", inputAmount.toFriendlyString());
|
||||
@ -793,7 +796,112 @@ public class TradeWalletService {
|
||||
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
// Dispute
|
||||
// Mediation
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
public byte[] signMediatedPayoutTx(Transaction depositTx,
|
||||
Coin buyerPayoutAmount,
|
||||
Coin sellerPayoutAmount,
|
||||
String buyerPayoutAddressString,
|
||||
String sellerPayoutAddressString,
|
||||
DeterministicKey myMultiSigKeyPair,
|
||||
byte[] buyerPubKey,
|
||||
byte[] sellerPubKey,
|
||||
byte[] arbitratorPubKey)
|
||||
throws AddressFormatException, TransactionVerificationException {
|
||||
log.trace("signMediatedPayoutTx called");
|
||||
log.trace("depositTx {}", depositTx.toString());
|
||||
log.trace("buyerPayoutAmount {}", buyerPayoutAmount.toFriendlyString());
|
||||
log.trace("sellerPayoutAmount {}", sellerPayoutAmount.toFriendlyString());
|
||||
log.trace("buyerPayoutAddressString {}", buyerPayoutAddressString);
|
||||
log.trace("sellerPayoutAddressString {}", sellerPayoutAddressString);
|
||||
log.trace("multiSigKeyPair (not displayed for security reasons)");
|
||||
log.trace("buyerPubKey HEX=" + ECKey.fromPublicOnly(buyerPubKey).getPublicKeyAsHex());
|
||||
log.trace("sellerPubKey HEX=" + ECKey.fromPublicOnly(sellerPubKey).getPublicKeyAsHex());
|
||||
log.trace("arbitratorPubKey HEX=" + ECKey.fromPublicOnly(arbitratorPubKey).getPublicKeyAsHex());
|
||||
Transaction preparedPayoutTx = createPayoutTx(depositTx,
|
||||
buyerPayoutAmount,
|
||||
sellerPayoutAmount,
|
||||
buyerPayoutAddressString,
|
||||
sellerPayoutAddressString);
|
||||
// MS redeemScript
|
||||
Script redeemScript = getMultiSigRedeemScript(buyerPubKey, sellerPubKey, arbitratorPubKey);
|
||||
// MS output from prev. tx is index 0
|
||||
Sha256Hash sigHash = preparedPayoutTx.hashForSignature(0, redeemScript, Transaction.SigHash.ALL, false);
|
||||
checkNotNull(myMultiSigKeyPair, "myMultiSigKeyPair must not be null");
|
||||
if (myMultiSigKeyPair.isEncrypted())
|
||||
checkNotNull(aesKey);
|
||||
|
||||
ECKey.ECDSASignature mySignature = myMultiSigKeyPair.sign(sigHash, aesKey).toCanonicalised();
|
||||
|
||||
WalletService.printTx("prepared mediated payoutTx for sig creation", preparedPayoutTx);
|
||||
|
||||
WalletService.verifyTransaction(preparedPayoutTx);
|
||||
|
||||
return mySignature.encodeToDER();
|
||||
}
|
||||
|
||||
public Transaction finalizeMediatedPayoutTx(Transaction depositTx,
|
||||
byte[] buyerSignature,
|
||||
byte[] sellerSignature,
|
||||
Coin buyerPayoutAmount,
|
||||
Coin sellerPayoutAmount,
|
||||
String buyerPayoutAddressString,
|
||||
String sellerPayoutAddressString,
|
||||
DeterministicKey multiSigKeyPair,
|
||||
byte[] buyerPubKey,
|
||||
byte[] sellerPubKey,
|
||||
byte[] arbitratorPubKey)
|
||||
throws AddressFormatException, TransactionVerificationException, WalletException {
|
||||
log.trace("finalizeMediatedPayoutTx called");
|
||||
log.trace("depositTx {}", depositTx.toString());
|
||||
log.trace("buyerSignature r {}", ECKey.ECDSASignature.decodeFromDER(buyerSignature).r.toString());
|
||||
log.trace("buyerSignature s {}", ECKey.ECDSASignature.decodeFromDER(buyerSignature).s.toString());
|
||||
log.trace("sellerSignature r {}", ECKey.ECDSASignature.decodeFromDER(sellerSignature).r.toString());
|
||||
log.trace("sellerSignature s {}", ECKey.ECDSASignature.decodeFromDER(sellerSignature).s.toString());
|
||||
log.trace("buyerPayoutAmount {}", buyerPayoutAmount.toFriendlyString());
|
||||
log.trace("sellerPayoutAmount {}", sellerPayoutAmount.toFriendlyString());
|
||||
log.trace("buyerPayoutAddressString {}", buyerPayoutAddressString);
|
||||
log.trace("sellerPayoutAddressString {}", sellerPayoutAddressString);
|
||||
log.trace("multiSigKeyPair (not displayed for security reasons)");
|
||||
log.trace("buyerPubKey {}", ECKey.fromPublicOnly(buyerPubKey).toString());
|
||||
log.trace("sellerPubKey {}", ECKey.fromPublicOnly(sellerPubKey).toString());
|
||||
log.trace("arbitratorPubKey {}", ECKey.fromPublicOnly(arbitratorPubKey).toString());
|
||||
|
||||
Transaction payoutTx = createPayoutTx(depositTx,
|
||||
buyerPayoutAmount,
|
||||
sellerPayoutAmount,
|
||||
buyerPayoutAddressString,
|
||||
sellerPayoutAddressString);
|
||||
// MS redeemScript
|
||||
Script redeemScript = getMultiSigRedeemScript(buyerPubKey, sellerPubKey, arbitratorPubKey);
|
||||
// MS output from prev. tx is index 0
|
||||
checkNotNull(multiSigKeyPair, "multiSigKeyPair must not be null");
|
||||
|
||||
TransactionSignature buyerTxSig = new TransactionSignature(ECKey.ECDSASignature.decodeFromDER(buyerSignature),
|
||||
Transaction.SigHash.ALL, false);
|
||||
TransactionSignature sellerTxSig = new TransactionSignature(ECKey.ECDSASignature.decodeFromDER(sellerSignature),
|
||||
Transaction.SigHash.ALL, false);
|
||||
|
||||
// Take care of order of signatures. Need to be reversed here. See comment below at getMultiSigRedeemScript (arbitrator, seller, buyer)
|
||||
Script inputScript = ScriptBuilder.createP2SHMultiSigInputScript(ImmutableList.of(sellerTxSig, buyerTxSig), redeemScript);
|
||||
|
||||
TransactionInput input = payoutTx.getInput(0);
|
||||
input.setScriptSig(inputScript);
|
||||
|
||||
WalletService.printTx("mediated payoutTx", payoutTx);
|
||||
|
||||
WalletService.verifyTransaction(payoutTx);
|
||||
WalletService.checkWalletConsistency(wallet);
|
||||
WalletService.checkScriptSig(payoutTx, input, 0);
|
||||
checkNotNull(input.getConnectedOutput(), "input.getConnectedOutput() must not be null");
|
||||
input.verify(input.getConnectedOutput());
|
||||
return payoutTx;
|
||||
}
|
||||
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
// Arbitration
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
/**
|
||||
@ -1127,7 +1235,9 @@ public class TradeWalletService {
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private TransactionInput getTransactionInput(Transaction depositTx, byte[] scriptProgram, RawTransactionInput rawTransactionInput) {
|
||||
private TransactionInput getTransactionInput(Transaction depositTx,
|
||||
byte[] scriptProgram,
|
||||
RawTransactionInput rawTransactionInput) {
|
||||
return new TransactionInput(params,
|
||||
depositTx,
|
||||
scriptProgram,
|
||||
@ -1191,7 +1301,10 @@ public class TradeWalletService {
|
||||
}
|
||||
}
|
||||
|
||||
private void addAvailableInputsAndChangeOutputs(Transaction transaction, Address address, Address changeAddress, Coin txFee) throws WalletException {
|
||||
private void addAvailableInputsAndChangeOutputs(Transaction transaction,
|
||||
Address address,
|
||||
Address changeAddress,
|
||||
Coin txFee) throws WalletException {
|
||||
SendRequest sendRequest = null;
|
||||
try {
|
||||
// Lets let the framework do the work to find the right inputs
|
||||
|
@ -1,219 +0,0 @@
|
||||
/*
|
||||
* This file is part of Bisq.
|
||||
*
|
||||
* Bisq is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or (at
|
||||
* your option) any later version.
|
||||
*
|
||||
* Bisq is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
|
||||
* License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package bisq.core.chat;
|
||||
|
||||
import bisq.core.arbitration.messages.DisputeCommunicationMessage;
|
||||
import bisq.core.arbitration.messages.DisputeMessage;
|
||||
import bisq.core.btc.setup.WalletsSetup;
|
||||
|
||||
import bisq.network.p2p.AckMessage;
|
||||
import bisq.network.p2p.AckMessageSourceType;
|
||||
import bisq.network.p2p.DecryptedMessageWithPubKey;
|
||||
import bisq.network.p2p.NodeAddress;
|
||||
import bisq.network.p2p.P2PService;
|
||||
import bisq.network.p2p.SendMailboxMessageListener;
|
||||
|
||||
import bisq.common.Timer;
|
||||
import bisq.common.UserThread;
|
||||
import bisq.common.crypto.PubKeyRing;
|
||||
import bisq.common.proto.network.NetworkEnvelope;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.CopyOnWriteArraySet;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
public class ChatManager {
|
||||
private static final Logger log = LoggerFactory.getLogger(ChatManager.class);
|
||||
|
||||
@Getter
|
||||
private final P2PService p2PService;
|
||||
private final WalletsSetup walletsSetup;
|
||||
@Setter
|
||||
@Getter
|
||||
private ChatSession chatSession;
|
||||
private final Map<String, Timer> delayMsgMap = new HashMap<>();
|
||||
|
||||
private final CopyOnWriteArraySet<DecryptedMessageWithPubKey> decryptedMailboxMessageWithPubKeys = new CopyOnWriteArraySet<>();
|
||||
private final CopyOnWriteArraySet<DecryptedMessageWithPubKey> decryptedDirectMessageWithPubKeys = new CopyOnWriteArraySet<>();
|
||||
private boolean allServicesInitialized;
|
||||
|
||||
public ChatManager(P2PService p2PService,
|
||||
WalletsSetup walletsSetup
|
||||
) {
|
||||
this.p2PService = p2PService;
|
||||
this.walletsSetup = walletsSetup;
|
||||
|
||||
// We get first the message handler called then the onBootstrapped
|
||||
p2PService.addDecryptedDirectMessageListener((decryptedMessageWithPubKey, senderAddress) -> {
|
||||
decryptedDirectMessageWithPubKeys.add(decryptedMessageWithPubKey);
|
||||
tryApplyMessages();
|
||||
});
|
||||
p2PService.addDecryptedMailboxListener((decryptedMessageWithPubKey, senderAddress) -> {
|
||||
decryptedMailboxMessageWithPubKeys.add(decryptedMessageWithPubKey);
|
||||
tryApplyMessages();
|
||||
});
|
||||
}
|
||||
|
||||
public void onAllServicesInitialized() {
|
||||
allServicesInitialized = true;
|
||||
}
|
||||
|
||||
public void tryApplyMessages() {
|
||||
if (isReady())
|
||||
applyMessages();
|
||||
}
|
||||
|
||||
private boolean isReady() {
|
||||
return allServicesInitialized &&
|
||||
p2PService.isBootstrapped() &&
|
||||
walletsSetup.isDownloadComplete() &&
|
||||
walletsSetup.hasSufficientPeersForBroadcast();
|
||||
}
|
||||
|
||||
private void applyMessages() {
|
||||
decryptedDirectMessageWithPubKeys.forEach(decryptedMessageWithPubKey -> {
|
||||
NetworkEnvelope networkEnvelope = decryptedMessageWithPubKey.getNetworkEnvelope();
|
||||
if (networkEnvelope instanceof DisputeMessage) {
|
||||
chatSession.dispatchMessage((DisputeMessage) networkEnvelope);
|
||||
} else if (networkEnvelope instanceof AckMessage) {
|
||||
processAckMessage((AckMessage) networkEnvelope, null);
|
||||
}
|
||||
});
|
||||
decryptedDirectMessageWithPubKeys.clear();
|
||||
|
||||
decryptedMailboxMessageWithPubKeys.forEach(decryptedMessageWithPubKey -> {
|
||||
NetworkEnvelope networkEnvelope = decryptedMessageWithPubKey.getNetworkEnvelope();
|
||||
log.debug("decryptedMessageWithPubKey.message " + networkEnvelope);
|
||||
if (networkEnvelope instanceof DisputeMessage) {
|
||||
chatSession.dispatchMessage((DisputeMessage) networkEnvelope);
|
||||
p2PService.removeEntryFromMailbox(decryptedMessageWithPubKey);
|
||||
} else if (networkEnvelope instanceof AckMessage) {
|
||||
processAckMessage((AckMessage) networkEnvelope, decryptedMessageWithPubKey);
|
||||
}
|
||||
});
|
||||
decryptedMailboxMessageWithPubKeys.clear();
|
||||
}
|
||||
|
||||
public void onDisputeDirectMessage(DisputeCommunicationMessage disputeCommunicationMessage) {
|
||||
final String tradeId = disputeCommunicationMessage.getTradeId();
|
||||
final String uid = disputeCommunicationMessage.getUid();
|
||||
boolean channelOpen = chatSession.channelOpen(disputeCommunicationMessage);
|
||||
if (!channelOpen) {
|
||||
log.debug("We got a disputeCommunicationMessage but we don't have a matching chat. TradeId = " + tradeId);
|
||||
if (!delayMsgMap.containsKey(uid)) {
|
||||
Timer timer = UserThread.runAfter(() -> onDisputeDirectMessage(disputeCommunicationMessage), 1);
|
||||
delayMsgMap.put(uid, timer);
|
||||
} else {
|
||||
String msg = "We got a disputeCommunicationMessage 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 = chatSession.getPeerPubKeyRing(disputeCommunicationMessage);
|
||||
|
||||
chatSession.storeDisputeCommunicationMessage(disputeCommunicationMessage);
|
||||
|
||||
// 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(disputeCommunicationMessage, receiverPubKeyRing, true, null);
|
||||
}
|
||||
|
||||
private void processAckMessage(AckMessage ackMessage,
|
||||
@Nullable DecryptedMessageWithPubKey decryptedMessageWithPubKey) {
|
||||
if (ackMessage.getSourceType() == AckMessageSourceType.DISPUTE_MESSAGE) {
|
||||
if (ackMessage.isSuccess()) {
|
||||
log.info("Received AckMessage for {} with tradeId {} and uid {}",
|
||||
ackMessage.getSourceMsgClassName(), ackMessage.getSourceId(), ackMessage.getSourceUid());
|
||||
} else {
|
||||
log.warn("Received AckMessage with error state for {} with tradeId {} and errorMessage={}",
|
||||
ackMessage.getSourceMsgClassName(), ackMessage.getSourceId(), ackMessage.getErrorMessage());
|
||||
}
|
||||
|
||||
chatSession.getChatMessages()
|
||||
.forEach(msg -> {
|
||||
if (ackMessage.isSuccess())
|
||||
msg.setAcknowledged(true);
|
||||
else
|
||||
msg.setAckError(ackMessage.getErrorMessage());
|
||||
});
|
||||
chatSession.persist();
|
||||
|
||||
if (decryptedMessageWithPubKey != null)
|
||||
p2PService.removeEntryFromMailbox(decryptedMessageWithPubKey);
|
||||
}
|
||||
}
|
||||
|
||||
public void sendAckMessage(DisputeMessage disputeMessage, PubKeyRing peersPubKeyRing,
|
||||
boolean result, @Nullable String errorMessage) {
|
||||
String tradeId = disputeMessage.getTradeId();
|
||||
String uid = disputeMessage.getUid();
|
||||
AckMessage ackMessage = new AckMessage(p2PService.getNetworkNode().getNodeAddress(),
|
||||
AckMessageSourceType.DISPUTE_MESSAGE,
|
||||
disputeMessage.getClass().getSimpleName(),
|
||||
uid,
|
||||
tradeId,
|
||||
result,
|
||||
errorMessage);
|
||||
final NodeAddress peersNodeAddress = disputeMessage.getSenderNodeAddress();
|
||||
log.info("Send AckMessage for {} to peer {}. tradeId={}, uid={}",
|
||||
ackMessage.getSourceMsgClassName(), peersNodeAddress, tradeId, uid);
|
||||
p2PService.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);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
private void cleanupRetryMap(String uid) {
|
||||
if (delayMsgMap.containsKey(uid)) {
|
||||
Timer timer = delayMsgMap.remove(uid);
|
||||
if (timer != null)
|
||||
timer.stop();
|
||||
}
|
||||
}
|
||||
}
|
@ -1,66 +0,0 @@
|
||||
/*
|
||||
* This file is part of Bisq.
|
||||
*
|
||||
* Bisq is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or (at
|
||||
* your option) any later version.
|
||||
*
|
||||
* Bisq is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
|
||||
* License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package bisq.core.chat;
|
||||
|
||||
import bisq.core.arbitration.messages.DisputeCommunicationMessage;
|
||||
import bisq.core.arbitration.messages.DisputeMessage;
|
||||
|
||||
import bisq.network.p2p.NodeAddress;
|
||||
|
||||
import bisq.common.crypto.PubKeyRing;
|
||||
|
||||
import javafx.collections.ObservableList;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import lombok.Getter;
|
||||
|
||||
public abstract class ChatSession {
|
||||
@Getter
|
||||
DisputeCommunicationMessage.Type type;
|
||||
|
||||
public ChatSession(DisputeCommunicationMessage.Type type) {
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
abstract public boolean isClient();
|
||||
|
||||
abstract public String getTradeId();
|
||||
|
||||
abstract public PubKeyRing getClientPubKeyRing();
|
||||
|
||||
abstract public void addDisputeCommunicationMessage(DisputeCommunicationMessage message);
|
||||
|
||||
abstract public void persist();
|
||||
|
||||
abstract public ObservableList<DisputeCommunicationMessage> getDisputeCommunicationMessages();
|
||||
|
||||
abstract public List<DisputeCommunicationMessage> getChatMessages();
|
||||
|
||||
abstract public boolean chatIsOpen();
|
||||
|
||||
abstract public NodeAddress getPeerNodeAddress(DisputeCommunicationMessage message);
|
||||
|
||||
abstract public PubKeyRing getPeerPubKeyRing(DisputeCommunicationMessage message);
|
||||
|
||||
abstract public void dispatchMessage(DisputeMessage message);
|
||||
|
||||
abstract public boolean channelOpen(DisputeCommunicationMessage message);
|
||||
|
||||
abstract public void storeDisputeCommunicationMessage(DisputeCommunicationMessage message);
|
||||
}
|
@ -198,11 +198,11 @@ public abstract class BondRepository<T extends Bond, R extends BondedAsset> impl
|
||||
// Protected
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
abstract protected T createBond(R bondedAsset);
|
||||
protected abstract T createBond(R bondedAsset);
|
||||
|
||||
abstract protected void updateBond(T bond, R bondedAsset, TxOutput lockupTxOutput);
|
||||
protected abstract void updateBond(T bond, R bondedAsset, TxOutput lockupTxOutput);
|
||||
|
||||
abstract protected Stream<R> getBondedAssetStream();
|
||||
protected abstract Stream<R> getBondedAssetStream();
|
||||
|
||||
protected void update() {
|
||||
log.debug("update");
|
||||
|
@ -202,7 +202,7 @@ public abstract class BsqNode implements DaoSetupService {
|
||||
return startBlockHeight;
|
||||
}
|
||||
|
||||
abstract protected void startParseBlocks();
|
||||
protected abstract void startParseBlocks();
|
||||
|
||||
protected void onParseBlockChainComplete() {
|
||||
log.info("onParseBlockChainComplete");
|
||||
|
@ -94,6 +94,10 @@ public final class Filter implements ProtectedStoragePayload, ExpirablePayload {
|
||||
@Nullable
|
||||
private final String disableTradeBelowVersion;
|
||||
|
||||
// added in v1.1.6
|
||||
@Nullable
|
||||
private final List<String> mediators;
|
||||
|
||||
public Filter(List<String> bannedOfferIds,
|
||||
List<String> bannedNodeAddress,
|
||||
List<PaymentAccountFilter> bannedPaymentAccounts,
|
||||
@ -106,7 +110,8 @@ public final class Filter implements ProtectedStoragePayload, ExpirablePayload {
|
||||
@Nullable List<String> btcNodes,
|
||||
boolean disableDao,
|
||||
@Nullable String disableDaoBelowVersion,
|
||||
@Nullable String disableTradeBelowVersion) {
|
||||
@Nullable String disableTradeBelowVersion,
|
||||
@Nullable List<String> mediators) {
|
||||
this.bannedOfferIds = bannedOfferIds;
|
||||
this.bannedNodeAddress = bannedNodeAddress;
|
||||
this.bannedPaymentAccounts = bannedPaymentAccounts;
|
||||
@ -120,6 +125,7 @@ public final class Filter implements ProtectedStoragePayload, ExpirablePayload {
|
||||
this.disableDao = disableDao;
|
||||
this.disableDaoBelowVersion = disableDaoBelowVersion;
|
||||
this.disableTradeBelowVersion = disableTradeBelowVersion;
|
||||
this.mediators = mediators;
|
||||
}
|
||||
|
||||
|
||||
@ -143,7 +149,8 @@ public final class Filter implements ProtectedStoragePayload, ExpirablePayload {
|
||||
@Nullable String disableTradeBelowVersion,
|
||||
String signatureAsBase64,
|
||||
byte[] ownerPubKeyBytes,
|
||||
@Nullable Map<String, String> extraDataMap) {
|
||||
@Nullable Map<String, String> extraDataMap,
|
||||
@Nullable List<String> mediators) {
|
||||
this(bannedOfferIds,
|
||||
bannedNodeAddress,
|
||||
bannedPaymentAccounts,
|
||||
@ -156,7 +163,8 @@ public final class Filter implements ProtectedStoragePayload, ExpirablePayload {
|
||||
btcNodes,
|
||||
disableDao,
|
||||
disableDaoBelowVersion,
|
||||
disableTradeBelowVersion);
|
||||
disableTradeBelowVersion,
|
||||
mediators);
|
||||
this.signatureAsBase64 = signatureAsBase64;
|
||||
this.ownerPubKeyBytes = ownerPubKeyBytes;
|
||||
this.extraDataMap = ExtraDataMapValidator.getValidatedExtraDataMap(extraDataMap);
|
||||
@ -189,6 +197,7 @@ public final class Filter implements ProtectedStoragePayload, ExpirablePayload {
|
||||
Optional.ofNullable(disableDaoBelowVersion).ifPresent(builder::setDisableDaoBelowVersion);
|
||||
Optional.ofNullable(disableTradeBelowVersion).ifPresent(builder::setDisableTradeBelowVersion);
|
||||
Optional.ofNullable(extraDataMap).ifPresent(builder::putAllExtraData);
|
||||
Optional.ofNullable(mediators).ifPresent(builder::addAllMediators);
|
||||
|
||||
return protobuf.StoragePayload.newBuilder().setFilter(builder).build();
|
||||
}
|
||||
@ -211,7 +220,8 @@ public final class Filter implements ProtectedStoragePayload, ExpirablePayload {
|
||||
proto.getDisableTradeBelowVersion().isEmpty() ? null : proto.getDisableTradeBelowVersion(),
|
||||
proto.getSignatureAsBase64(),
|
||||
proto.getOwnerPubKeyBytes().toByteArray(),
|
||||
CollectionUtils.isEmpty(proto.getExtraDataMap()) ? null : proto.getExtraDataMap());
|
||||
CollectionUtils.isEmpty(proto.getExtraDataMap()) ? null : proto.getExtraDataMap(),
|
||||
CollectionUtils.isEmpty(proto.getMediatorsList()) ? null : new ArrayList<>(proto.getMediatorsList()));
|
||||
}
|
||||
|
||||
|
||||
@ -224,7 +234,7 @@ public final class Filter implements ProtectedStoragePayload, ExpirablePayload {
|
||||
return TimeUnit.DAYS.toMillis(180);
|
||||
}
|
||||
|
||||
public void setSigAndPubKey(String signatureAsBase64, PublicKey ownerPubKey) {
|
||||
void setSigAndPubKey(String signatureAsBase64, PublicKey ownerPubKey) {
|
||||
this.signatureAsBase64 = signatureAsBase64;
|
||||
this.ownerPubKey = ownerPubKey;
|
||||
|
||||
|
@ -17,13 +17,13 @@
|
||||
|
||||
package bisq.core.notifications.alerts;
|
||||
|
||||
import bisq.core.arbitration.Dispute;
|
||||
import bisq.core.arbitration.DisputeManager;
|
||||
import bisq.core.arbitration.messages.DisputeCommunicationMessage;
|
||||
import bisq.core.locale.Res;
|
||||
import bisq.core.notifications.MobileMessage;
|
||||
import bisq.core.notifications.MobileMessageType;
|
||||
import bisq.core.notifications.MobileNotificationService;
|
||||
import bisq.core.support.dispute.Dispute;
|
||||
import bisq.core.support.dispute.arbitration.ArbitrationManager;
|
||||
import bisq.core.support.messages.ChatMessage;
|
||||
|
||||
import bisq.network.p2p.P2PService;
|
||||
|
||||
@ -43,18 +43,20 @@ public class DisputeMsgEvents {
|
||||
private final MobileNotificationService mobileNotificationService;
|
||||
|
||||
@Inject
|
||||
public DisputeMsgEvents(DisputeManager disputeManager, P2PService p2PService, MobileNotificationService mobileNotificationService) {
|
||||
public DisputeMsgEvents(ArbitrationManager arbitrationManager,
|
||||
P2PService p2PService,
|
||||
MobileNotificationService mobileNotificationService) {
|
||||
this.p2PService = p2PService;
|
||||
this.mobileNotificationService = mobileNotificationService;
|
||||
|
||||
// We need to handle it here in the constructor otherwise we get repeated the messages sent.
|
||||
disputeManager.getDisputesAsObservableList().addListener((ListChangeListener<Dispute>) c -> {
|
||||
arbitrationManager.getDisputesAsObservableList().addListener((ListChangeListener<Dispute>) c -> {
|
||||
c.next();
|
||||
if (c.wasAdded()) {
|
||||
c.getAddedSubList().forEach(this::setDisputeListener);
|
||||
}
|
||||
});
|
||||
disputeManager.getDisputesAsObservableList().forEach(this::setDisputeListener);
|
||||
arbitrationManager.getDisputesAsObservableList().forEach(this::setDisputeListener);
|
||||
}
|
||||
|
||||
// We ignore that onAllServicesInitialized here
|
||||
@ -64,22 +66,22 @@ public class DisputeMsgEvents {
|
||||
private void setDisputeListener(Dispute dispute) {
|
||||
//TODO use weak ref or remove listener
|
||||
log.debug("We got a dispute added. id={}, tradeId={}", dispute.getId(), dispute.getTradeId());
|
||||
dispute.getDisputeCommunicationMessages().addListener((ListChangeListener<DisputeCommunicationMessage>) c -> {
|
||||
log.debug("We got a DisputeCommunicationMessage added. id={}, tradeId={}", dispute.getId(), dispute.getTradeId());
|
||||
dispute.getChatMessages().addListener((ListChangeListener<ChatMessage>) c -> {
|
||||
log.debug("We got a ChatMessage added. id={}, tradeId={}", dispute.getId(), dispute.getTradeId());
|
||||
c.next();
|
||||
if (c.wasAdded()) {
|
||||
c.getAddedSubList().forEach(this::setDisputeCommunicationMessage);
|
||||
c.getAddedSubList().forEach(this::setChatMessage);
|
||||
}
|
||||
});
|
||||
|
||||
//TODO test
|
||||
if (!dispute.getDisputeCommunicationMessages().isEmpty())
|
||||
setDisputeCommunicationMessage(dispute.getDisputeCommunicationMessages().get(0));
|
||||
if (!dispute.getChatMessages().isEmpty())
|
||||
setChatMessage(dispute.getChatMessages().get(0));
|
||||
}
|
||||
|
||||
private void setDisputeCommunicationMessage(DisputeCommunicationMessage disputeMsg) {
|
||||
private void setChatMessage(ChatMessage disputeMsg) {
|
||||
// TODO we need to prevent to send msg for old dispute messages again at restart
|
||||
// Maybe we need a new property in DisputeCommunicationMessage
|
||||
// Maybe we need a new property in ChatMessage
|
||||
// As key is not set in initial iterations it seems we don't need an extra handling.
|
||||
// the mailbox msg is set a bit later so that triggers a notification, but not the old messages.
|
||||
|
||||
|
@ -25,5 +25,6 @@ public enum AvailabilityResult {
|
||||
MARKET_PRICE_NOT_AVAILABLE,
|
||||
NO_ARBITRATORS,
|
||||
NO_MEDIATORS,
|
||||
USER_IGNORED
|
||||
USER_IGNORED,
|
||||
MISSING_MANDATORY_CAPABILITY
|
||||
}
|
||||
|
@ -72,7 +72,8 @@ public class Offer implements NetworkPayload, PersistablePayload {
|
||||
// Market price might be different at maker's and takers side so we need a bit of tolerance.
|
||||
// The tolerance will get smaller once we have multiple price feeds avoiding fast price fluctuations
|
||||
// from one provider.
|
||||
final static double PRICE_TOLERANCE = 0.01;
|
||||
private final static double PRICE_TOLERANCE = 0.01;
|
||||
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
// Enums
|
||||
@ -390,14 +391,6 @@ public class Offer implements NetworkPayload, PersistablePayload {
|
||||
return offerPayload.getId();
|
||||
}
|
||||
|
||||
public List<NodeAddress> getArbitratorNodeAddresses() {
|
||||
return offerPayload.getArbitratorNodeAddresses();
|
||||
}
|
||||
|
||||
public List<NodeAddress> getMediatorNodeAddresses() {
|
||||
return offerPayload.getMediatorNodeAddresses();
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public List<String> getAcceptedBankIds() {
|
||||
return offerPayload.getAcceptedBankIds();
|
||||
|
@ -28,6 +28,7 @@ import bisq.network.p2p.storage.HashMapChangedListener;
|
||||
import bisq.network.p2p.storage.payload.ProtectedStorageEntry;
|
||||
|
||||
import bisq.common.UserThread;
|
||||
import bisq.common.app.Capability;
|
||||
import bisq.common.handlers.ErrorMessageHandler;
|
||||
import bisq.common.handlers.ResultHandler;
|
||||
import bisq.common.storage.JsonFileManager;
|
||||
@ -42,6 +43,7 @@ import java.io.File;
|
||||
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
@ -91,8 +93,10 @@ public class OfferBookService {
|
||||
if (data.getProtectedStoragePayload() instanceof OfferPayload) {
|
||||
OfferPayload offerPayload = (OfferPayload) data.getProtectedStoragePayload();
|
||||
Offer offer = new Offer(offerPayload);
|
||||
offer.setPriceFeedService(priceFeedService);
|
||||
listener.onAdded(offer);
|
||||
if (showOffer(offer)) {
|
||||
offer.setPriceFeedService(priceFeedService);
|
||||
listener.onAdded(offer);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
@ -131,6 +135,11 @@ public class OfferBookService {
|
||||
}
|
||||
}
|
||||
|
||||
private boolean showOffer(Offer offer) {
|
||||
return !OfferRestrictions.requiresUpdate() ||
|
||||
OfferRestrictions.hasOfferMandatoryCapability(offer, Capability.MEDIATION);
|
||||
}
|
||||
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
// API
|
||||
@ -150,7 +159,9 @@ public class OfferBookService {
|
||||
}
|
||||
}
|
||||
|
||||
public void refreshTTL(OfferPayload offerPayload, ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) {
|
||||
public void refreshTTL(OfferPayload offerPayload,
|
||||
ResultHandler resultHandler,
|
||||
ErrorMessageHandler errorMessageHandler) {
|
||||
if (filterManager.requireUpdateToNewVersionForTrading()) {
|
||||
errorMessageHandler.handleErrorMessage(Res.get("popup.warning.mandatoryUpdate.trading"));
|
||||
return;
|
||||
@ -164,15 +175,21 @@ public class OfferBookService {
|
||||
}
|
||||
}
|
||||
|
||||
public void activateOffer(Offer offer, @Nullable ResultHandler resultHandler, @Nullable ErrorMessageHandler errorMessageHandler) {
|
||||
public void activateOffer(Offer offer,
|
||||
@Nullable ResultHandler resultHandler,
|
||||
@Nullable ErrorMessageHandler errorMessageHandler) {
|
||||
addOffer(offer, resultHandler, errorMessageHandler);
|
||||
}
|
||||
|
||||
public void deactivateOffer(OfferPayload offerPayload, @Nullable ResultHandler resultHandler, @Nullable ErrorMessageHandler errorMessageHandler) {
|
||||
public void deactivateOffer(OfferPayload offerPayload,
|
||||
@Nullable ResultHandler resultHandler,
|
||||
@Nullable ErrorMessageHandler errorMessageHandler) {
|
||||
removeOffer(offerPayload, resultHandler, errorMessageHandler);
|
||||
}
|
||||
|
||||
public void removeOffer(OfferPayload offerPayload, @Nullable ResultHandler resultHandler, @Nullable ErrorMessageHandler errorMessageHandler) {
|
||||
public void removeOffer(OfferPayload offerPayload,
|
||||
@Nullable ResultHandler resultHandler,
|
||||
@Nullable ErrorMessageHandler errorMessageHandler) {
|
||||
if (p2PService.removeData(offerPayload, true)) {
|
||||
if (resultHandler != null)
|
||||
resultHandler.handleResult();
|
||||
@ -191,6 +208,7 @@ public class OfferBookService {
|
||||
offer.setPriceFeedService(priceFeedService);
|
||||
return offer;
|
||||
})
|
||||
.filter(this::showOffer)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@ -236,7 +254,7 @@ public class OfferBookService {
|
||||
return null;
|
||||
}
|
||||
})
|
||||
.filter(e -> e != null)
|
||||
.filter(Objects::nonNull)
|
||||
.collect(Collectors.toList());
|
||||
jsonFileManager.writeToDisc(Utilities.objectToJson(offerForJsonList), "offers_statistics");
|
||||
}
|
||||
|
@ -31,6 +31,7 @@ import org.springframework.util.CollectionUtils;
|
||||
|
||||
import java.security.PublicKey;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
@ -71,11 +72,18 @@ public final class OfferPayload implements ProtectedStoragePayload, ExpirablePay
|
||||
}
|
||||
|
||||
// Keys for extra map
|
||||
// Only set for fiat offers
|
||||
public static final String ACCOUNT_AGE_WITNESS_HASH = "accountAgeWitnessHash";
|
||||
public static final String REFERRAL_ID = "referralId";
|
||||
// Only used in payment method F2F
|
||||
public static final String F2F_CITY = "f2fCity";
|
||||
public static final String F2F_EXTRA_INFO = "f2fExtraInfo";
|
||||
|
||||
// Comma separated list of ordinal of a bisq.common.app.Capability. E.g. ordinal of
|
||||
// Capability.SIGNED_ACCOUNT_AGE_WITNESS is 11 and Capability.MEDIATION is 12 so if we want to signal that maker
|
||||
// of the offer supports both capabilities we add "11, 12" to capabilities.
|
||||
public static final String CAPABILITIES = "capabilities";
|
||||
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
// Instance fields
|
||||
@ -105,7 +113,11 @@ public final class OfferPayload implements ProtectedStoragePayload, ExpirablePay
|
||||
private final String baseCurrencyCode;
|
||||
private final String counterCurrencyCode;
|
||||
|
||||
@Deprecated
|
||||
// Not used anymore but we cannot set it Nullable or remove it to not break backward compatibility (diff. hash)
|
||||
private final List<NodeAddress> arbitratorNodeAddresses;
|
||||
@Deprecated
|
||||
// Not used anymore but we cannot set it Nullable or remove it to not break backward compatibility (diff. hash)
|
||||
private final List<NodeAddress> mediatorNodeAddresses;
|
||||
private final String paymentMethodId;
|
||||
private final String makerPaymentAccountId;
|
||||
@ -298,9 +310,9 @@ public final class OfferPayload implements ProtectedStoragePayload, ExpirablePay
|
||||
public static OfferPayload fromProto(protobuf.OfferPayload proto) {
|
||||
checkArgument(!proto.getOfferFeePaymentTxId().isEmpty(), "OfferFeePaymentTxId must be set in PB.OfferPayload");
|
||||
List<String> acceptedBankIds = proto.getAcceptedBankIdsList().isEmpty() ?
|
||||
null : proto.getAcceptedBankIdsList().stream().collect(Collectors.toList());
|
||||
null : new ArrayList<>(proto.getAcceptedBankIdsList());
|
||||
List<String> acceptedCountryCodes = proto.getAcceptedCountryCodesList().isEmpty() ?
|
||||
null : proto.getAcceptedCountryCodesList().stream().collect(Collectors.toList());
|
||||
null : new ArrayList<>(proto.getAcceptedCountryCodesList());
|
||||
String hashOfChallenge = ProtoUtil.stringOrNullFromProto(proto.getHashOfChallenge());
|
||||
Map<String, String> extraDataMapMap = CollectionUtils.isEmpty(proto.getExtraDataMap()) ?
|
||||
null : proto.getExtraDataMap();
|
||||
@ -388,8 +400,6 @@ public final class OfferPayload implements ProtectedStoragePayload, ExpirablePay
|
||||
",\n minAmount=" + minAmount +
|
||||
",\n baseCurrencyCode='" + baseCurrencyCode + '\'' +
|
||||
",\n counterCurrencyCode='" + counterCurrencyCode + '\'' +
|
||||
",\n arbitratorNodeAddresses=" + arbitratorNodeAddresses +
|
||||
",\n mediatorNodeAddresses=" + mediatorNodeAddresses +
|
||||
",\n paymentMethodId='" + paymentMethodId + '\'' +
|
||||
",\n makerPaymentAccountId='" + makerPaymentAccountId + '\'' +
|
||||
",\n offerFeePaymentTxId='" + offerFeePaymentTxId + '\'' +
|
||||
|
@ -20,9 +20,25 @@ package bisq.core.offer;
|
||||
import bisq.core.payment.payload.PaymentMethod;
|
||||
import bisq.core.trade.Trade;
|
||||
|
||||
import bisq.common.app.Capabilities;
|
||||
import bisq.common.app.Capability;
|
||||
import bisq.common.util.Utilities;
|
||||
|
||||
import org.bitcoinj.core.Coin;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.GregorianCalendar;
|
||||
import java.util.Map;
|
||||
|
||||
public class OfferRestrictions {
|
||||
// The date when traders who have not updated cannot take offers from updated clients and their offers become
|
||||
// invisible for updated clients.
|
||||
private static final Date REQUIRE_UPDATE_DATE = Utilities.getUTCDate(2019, GregorianCalendar.SEPTEMBER, 1);
|
||||
|
||||
static boolean requiresUpdate() {
|
||||
return new Date().after(REQUIRE_UPDATE_DATE);
|
||||
}
|
||||
|
||||
public static Coin TOLERATED_SMALL_TRADE_AMOUNT = Coin.parseCoin("0.01");
|
||||
|
||||
public static boolean isOfferRisky(Offer offer) {
|
||||
@ -53,7 +69,17 @@ public class OfferRestrictions {
|
||||
return isAmountRisky(offer.getMinAmount());
|
||||
}
|
||||
|
||||
public static boolean isAmountRisky(Coin amount) {
|
||||
private static boolean isAmountRisky(Coin amount) {
|
||||
return amount.isGreaterThan(TOLERATED_SMALL_TRADE_AMOUNT);
|
||||
}
|
||||
|
||||
static boolean hasOfferMandatoryCapability(Offer offer, Capability mandatoryCapability) {
|
||||
Map<String, String> extraDataMap = offer.getOfferPayload().getExtraDataMap();
|
||||
if (extraDataMap != null && extraDataMap.containsKey(OfferPayload.CAPABILITIES)) {
|
||||
String commaSeparatedOrdinals = extraDataMap.get(OfferPayload.CAPABILITIES);
|
||||
Capabilities capabilities = Capabilities.fromStringList(commaSeparatedOrdinals);
|
||||
return Capabilities.hasMandatoryCapability(capabilities, mandatoryCapability);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
@ -38,6 +38,7 @@ import bisq.core.util.CoinUtil;
|
||||
|
||||
import bisq.network.p2p.P2PService;
|
||||
|
||||
import bisq.common.app.Capabilities;
|
||||
import bisq.common.util.MathUtils;
|
||||
|
||||
import org.bitcoinj.core.Coin;
|
||||
@ -116,7 +117,9 @@ public class OfferUtil {
|
||||
* @param amount
|
||||
* @return
|
||||
*/
|
||||
public static boolean isCurrencyForMakerFeeBtc(Preferences preferences, BsqWalletService bsqWalletService, Coin amount) {
|
||||
public static boolean isCurrencyForMakerFeeBtc(Preferences preferences,
|
||||
BsqWalletService bsqWalletService,
|
||||
Coin amount) {
|
||||
boolean payFeeInBtc = preferences.getPayFeeInBtc();
|
||||
boolean bsqForFeeAvailable = isBsqForMakerFeeAvailable(bsqWalletService, amount);
|
||||
return payFeeInBtc || !bsqForFeeAvailable;
|
||||
@ -152,7 +155,9 @@ public class OfferUtil {
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean isCurrencyForTakerFeeBtc(Preferences preferences, BsqWalletService bsqWalletService, Coin amount) {
|
||||
public static boolean isCurrencyForTakerFeeBtc(Preferences preferences,
|
||||
BsqWalletService bsqWalletService,
|
||||
Coin amount) {
|
||||
boolean payFeeInBtc = preferences.getPayFeeInBtc();
|
||||
boolean bsqForFeeAvailable = isBsqForTakerFeeAvailable(bsqWalletService, amount);
|
||||
return payFeeInBtc || !bsqForFeeAvailable;
|
||||
@ -317,7 +322,9 @@ public class OfferUtil {
|
||||
}
|
||||
}
|
||||
|
||||
public static String getFeeWithFiatAmount(Coin makerFeeAsCoin, Optional<Volume> optionalFeeInFiat, BSFormatter formatter) {
|
||||
public static String getFeeWithFiatAmount(Coin makerFeeAsCoin,
|
||||
Optional<Volume> optionalFeeInFiat,
|
||||
BSFormatter formatter) {
|
||||
String fee = makerFeeAsCoin != null ? formatter.formatCoinWithCode(makerFeeAsCoin) : Res.get("shared.na");
|
||||
String feeInFiatAsString;
|
||||
if (optionalFeeInFiat != null && optionalFeeInFiat.isPresent()) {
|
||||
@ -333,27 +340,24 @@ public class OfferUtil {
|
||||
ReferralIdService referralIdService,
|
||||
PaymentAccount paymentAccount,
|
||||
String currencyCode) {
|
||||
Map<String, String> extraDataMap = null;
|
||||
Map<String, String> extraDataMap = new HashMap<>();
|
||||
if (CurrencyUtil.isFiatCurrency(currencyCode)) {
|
||||
extraDataMap = new HashMap<>();
|
||||
final String myWitnessHashAsHex = accountAgeWitnessService.getMyWitnessHashAsHex(paymentAccount.getPaymentAccountPayload());
|
||||
String myWitnessHashAsHex = accountAgeWitnessService.getMyWitnessHashAsHex(paymentAccount.getPaymentAccountPayload());
|
||||
extraDataMap.put(OfferPayload.ACCOUNT_AGE_WITNESS_HASH, myWitnessHashAsHex);
|
||||
}
|
||||
|
||||
if (referralIdService.getOptionalReferralId().isPresent()) {
|
||||
if (extraDataMap == null)
|
||||
extraDataMap = new HashMap<>();
|
||||
extraDataMap.put(OfferPayload.REFERRAL_ID, referralIdService.getOptionalReferralId().get());
|
||||
}
|
||||
|
||||
if (paymentAccount instanceof F2FAccount) {
|
||||
if (extraDataMap == null)
|
||||
extraDataMap = new HashMap<>();
|
||||
extraDataMap.put(OfferPayload.F2F_CITY, ((F2FAccount) paymentAccount).getCity());
|
||||
extraDataMap.put(OfferPayload.F2F_EXTRA_INFO, ((F2FAccount) paymentAccount).getExtraInfo());
|
||||
}
|
||||
|
||||
return extraDataMap;
|
||||
extraDataMap.put(OfferPayload.CAPABILITIES, Capabilities.app.toStringList());
|
||||
|
||||
return extraDataMap.isEmpty() ? null : extraDataMap;
|
||||
}
|
||||
|
||||
public static void validateOfferData(FilterManager filterManager,
|
||||
|
@ -60,6 +60,10 @@ public final class OpenOffer implements Tradable {
|
||||
@Setter
|
||||
@Nullable
|
||||
private NodeAddress arbitratorNodeAddress;
|
||||
@Getter
|
||||
@Setter
|
||||
@Nullable
|
||||
private NodeAddress mediatorNodeAddress;
|
||||
|
||||
transient private Storage<TradableList<OpenOffer>> storage;
|
||||
|
||||
@ -73,10 +77,14 @@ public final class OpenOffer implements Tradable {
|
||||
// PROTO BUFFER
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
private OpenOffer(Offer offer, State state, @Nullable NodeAddress arbitratorNodeAddress) {
|
||||
private OpenOffer(Offer offer,
|
||||
State state,
|
||||
@Nullable NodeAddress arbitratorNodeAddress,
|
||||
@Nullable NodeAddress mediatorNodeAddress) {
|
||||
this.offer = offer;
|
||||
this.state = state;
|
||||
this.arbitratorNodeAddress = arbitratorNodeAddress;
|
||||
this.mediatorNodeAddress = mediatorNodeAddress;
|
||||
|
||||
if (this.state == State.RESERVED)
|
||||
setState(State.AVAILABLE);
|
||||
@ -89,6 +97,7 @@ public final class OpenOffer implements Tradable {
|
||||
.setState(protobuf.OpenOffer.State.valueOf(state.name()));
|
||||
|
||||
Optional.ofNullable(arbitratorNodeAddress).ifPresent(nodeAddress -> builder.setArbitratorNodeAddress(nodeAddress.toProtoMessage()));
|
||||
Optional.ofNullable(mediatorNodeAddress).ifPresent(nodeAddress -> builder.setMediatorNodeAddress(nodeAddress.toProtoMessage()));
|
||||
|
||||
return protobuf.Tradable.newBuilder().setOpenOffer(builder).build();
|
||||
}
|
||||
@ -96,7 +105,8 @@ public final class OpenOffer implements Tradable {
|
||||
public static Tradable fromProto(protobuf.OpenOffer proto) {
|
||||
return new OpenOffer(Offer.fromProto(proto.getOffer()),
|
||||
ProtoUtil.enumFromProto(OpenOffer.State.class, proto.getState().name()),
|
||||
proto.hasArbitratorNodeAddress() ? NodeAddress.fromProto(proto.getArbitratorNodeAddress()) : null);
|
||||
proto.hasArbitratorNodeAddress() ? NodeAddress.fromProto(proto.getArbitratorNodeAddress()) : null,
|
||||
proto.hasMediatorNodeAddress() ? NodeAddress.fromProto(proto.getMediatorNodeAddress()) : null);
|
||||
}
|
||||
|
||||
|
||||
@ -157,12 +167,15 @@ public final class OpenOffer implements Tradable {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "OpenOffer{" +
|
||||
"\n\toffer=" + offer +
|
||||
"\n\tstate=" + state +
|
||||
'}';
|
||||
",\n offer=" + offer +
|
||||
",\n state=" + state +
|
||||
",\n arbitratorNodeAddress=" + arbitratorNodeAddress +
|
||||
",\n mediatorNodeAddress=" + mediatorNodeAddress +
|
||||
"\n}";
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -17,17 +17,18 @@
|
||||
|
||||
package bisq.core.offer;
|
||||
|
||||
import bisq.core.arbitration.ArbitratorManager;
|
||||
import bisq.core.btc.wallet.BsqWalletService;
|
||||
import bisq.core.btc.wallet.BtcWalletService;
|
||||
import bisq.core.btc.wallet.TradeWalletService;
|
||||
import bisq.core.exceptions.TradePriceOutOfToleranceException;
|
||||
import bisq.core.offer.availability.ArbitratorSelection;
|
||||
import bisq.core.offer.availability.DisputeAgentSelection;
|
||||
import bisq.core.offer.messages.OfferAvailabilityRequest;
|
||||
import bisq.core.offer.messages.OfferAvailabilityResponse;
|
||||
import bisq.core.offer.placeoffer.PlaceOfferModel;
|
||||
import bisq.core.offer.placeoffer.PlaceOfferProtocol;
|
||||
import bisq.core.provider.price.PriceFeedService;
|
||||
import bisq.core.support.dispute.arbitration.arbitrator.ArbitratorManager;
|
||||
import bisq.core.support.dispute.mediation.mediator.MediatorManager;
|
||||
import bisq.core.trade.TradableList;
|
||||
import bisq.core.trade.closed.ClosedTradableManager;
|
||||
import bisq.core.trade.handlers.TransactionResultHandler;
|
||||
@ -48,6 +49,8 @@ import bisq.network.p2p.peers.PeerManager;
|
||||
|
||||
import bisq.common.Timer;
|
||||
import bisq.common.UserThread;
|
||||
import bisq.common.app.Capabilities;
|
||||
import bisq.common.app.Capability;
|
||||
import bisq.common.crypto.KeyRing;
|
||||
import bisq.common.crypto.PubKeyRing;
|
||||
import bisq.common.handlers.ErrorMessageHandler;
|
||||
@ -100,6 +103,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
|
||||
private final Preferences preferences;
|
||||
private final TradeStatisticsManager tradeStatisticsManager;
|
||||
private final ArbitratorManager arbitratorManager;
|
||||
private final MediatorManager mediatorManager;
|
||||
private final Storage<TradableList<OpenOffer>> openOfferTradableListStorage;
|
||||
private final Map<String, OpenOffer> offersToBeEdited = new HashMap<>();
|
||||
private boolean stopped;
|
||||
@ -111,7 +115,6 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
|
||||
// Constructor, Initialization
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
@SuppressWarnings("WeakerAccess")
|
||||
@Inject
|
||||
public OpenOfferManager(KeyRing keyRing,
|
||||
User user,
|
||||
@ -125,6 +128,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
|
||||
Preferences preferences,
|
||||
TradeStatisticsManager tradeStatisticsManager,
|
||||
ArbitratorManager arbitratorManager,
|
||||
MediatorManager mediatorManager,
|
||||
Storage<TradableList<OpenOffer>> storage) {
|
||||
this.keyRing = keyRing;
|
||||
this.user = user;
|
||||
@ -138,6 +142,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
|
||||
this.preferences = preferences;
|
||||
this.tradeStatisticsManager = tradeStatisticsManager;
|
||||
this.arbitratorManager = arbitratorManager;
|
||||
this.mediatorManager = mediatorManager;
|
||||
|
||||
openOfferTradableListStorage = storage;
|
||||
|
||||
@ -150,7 +155,10 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
|
||||
@Override
|
||||
public void readPersisted() {
|
||||
openOffers = new TradableList<>(openOfferTradableListStorage, "OpenOffers");
|
||||
openOffers.forEach(e -> e.getOffer().setPriceFeedService(priceFeedService));
|
||||
openOffers.forEach(e -> {
|
||||
Offer offer = e.getOffer();
|
||||
offer.setPriceFeedService(priceFeedService);
|
||||
});
|
||||
}
|
||||
|
||||
public void onAllServicesInitialized() {
|
||||
@ -182,8 +190,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
|
||||
});
|
||||
}
|
||||
|
||||
@SuppressWarnings("WeakerAccess")
|
||||
public void shutDown() {
|
||||
private void shutDown() {
|
||||
shutDown(null);
|
||||
}
|
||||
|
||||
@ -214,7 +221,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
|
||||
removeOpenOffers(getObservableList(), completeHandler);
|
||||
}
|
||||
|
||||
public void removeOpenOffers(List<OpenOffer> openOffers, @Nullable Runnable completeHandler) {
|
||||
private void removeOpenOffers(List<OpenOffer> openOffers, @Nullable Runnable completeHandler) {
|
||||
final int size = openOffers.size();
|
||||
// Copy list as we remove in the loop
|
||||
List<OpenOffer> openOffersList = new ArrayList<>(openOffers);
|
||||
@ -260,6 +267,8 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
|
||||
private void onBootstrapComplete() {
|
||||
stopped = false;
|
||||
|
||||
maybeUpdatePersistedOffers();
|
||||
|
||||
// Republish means we send the complete offer object
|
||||
republishOffers();
|
||||
startPeriodicRepublishOffersTimer();
|
||||
@ -345,7 +354,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
|
||||
log.debug("We have stopped already. We ignore that placeOfferProtocol.placeOffer.onResult call.");
|
||||
}
|
||||
},
|
||||
errorMessageHandler::handleErrorMessage
|
||||
errorMessageHandler
|
||||
);
|
||||
placeOfferProtocol.placeOffer();
|
||||
}
|
||||
@ -365,7 +374,9 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
|
||||
}
|
||||
}
|
||||
|
||||
public void activateOpenOffer(OpenOffer openOffer, ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) {
|
||||
public void activateOpenOffer(OpenOffer openOffer,
|
||||
ResultHandler resultHandler,
|
||||
ErrorMessageHandler errorMessageHandler) {
|
||||
if (!offersToBeEdited.containsKey(openOffer.getId())) {
|
||||
Offer offer = openOffer.getOffer();
|
||||
openOffer.setStorage(openOfferTradableListStorage);
|
||||
@ -381,7 +392,9 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
|
||||
}
|
||||
}
|
||||
|
||||
public void deactivateOpenOffer(OpenOffer openOffer, ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) {
|
||||
public void deactivateOpenOffer(OpenOffer openOffer,
|
||||
ResultHandler resultHandler,
|
||||
ErrorMessageHandler errorMessageHandler) {
|
||||
Offer offer = openOffer.getOffer();
|
||||
openOffer.setStorage(openOfferTradableListStorage);
|
||||
offerBookService.deactivateOffer(offer.getOfferPayload(),
|
||||
@ -393,7 +406,9 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
|
||||
errorMessageHandler);
|
||||
}
|
||||
|
||||
public void removeOpenOffer(OpenOffer openOffer, ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) {
|
||||
public void removeOpenOffer(OpenOffer openOffer,
|
||||
ResultHandler resultHandler,
|
||||
ErrorMessageHandler errorMessageHandler) {
|
||||
if (!offersToBeEdited.containsKey(openOffer.getId())) {
|
||||
Offer offer = openOffer.getOffer();
|
||||
if (openOffer.isDeactivated()) {
|
||||
@ -409,7 +424,9 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
|
||||
}
|
||||
}
|
||||
|
||||
public void editOpenOfferStart(OpenOffer openOffer, ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) {
|
||||
public void editOpenOfferStart(OpenOffer openOffer,
|
||||
ResultHandler resultHandler,
|
||||
ErrorMessageHandler errorMessageHandler) {
|
||||
if (offersToBeEdited.containsKey(openOffer.getId())) {
|
||||
log.warn("editOpenOfferStart called for an offer which is already in edit mode.");
|
||||
resultHandler.handleResult();
|
||||
@ -422,7 +439,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
|
||||
resultHandler.handleResult();
|
||||
} else {
|
||||
deactivateOpenOffer(openOffer,
|
||||
() -> resultHandler.handleResult(),
|
||||
resultHandler,
|
||||
errorMessage -> {
|
||||
offersToBeEdited.remove(openOffer.getId());
|
||||
errorMessageHandler.handleErrorMessage(errorMessage);
|
||||
@ -430,7 +447,10 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
|
||||
}
|
||||
}
|
||||
|
||||
public void editOpenOfferPublish(Offer editedOffer, OpenOffer.State originalState, ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) {
|
||||
public void editOpenOfferPublish(Offer editedOffer,
|
||||
OpenOffer.State originalState,
|
||||
ResultHandler resultHandler,
|
||||
ErrorMessageHandler errorMessageHandler) {
|
||||
Optional<OpenOffer> openOfferOptional = getOpenOfferById(editedOffer.getId());
|
||||
|
||||
if (openOfferOptional.isPresent()) {
|
||||
@ -458,13 +478,14 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
|
||||
}
|
||||
}
|
||||
|
||||
public void editOpenOfferCancel(OpenOffer openOffer, OpenOffer.State originalState, ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) {
|
||||
public void editOpenOfferCancel(OpenOffer openOffer,
|
||||
OpenOffer.State originalState,
|
||||
ResultHandler resultHandler,
|
||||
ErrorMessageHandler errorMessageHandler) {
|
||||
if (offersToBeEdited.containsKey(openOffer.getId())) {
|
||||
offersToBeEdited.remove(openOffer.getId());
|
||||
if (originalState.equals(OpenOffer.State.AVAILABLE)) {
|
||||
activateOpenOffer(openOffer, () -> {
|
||||
resultHandler.handleResult();
|
||||
}, errorMessageHandler);
|
||||
activateOpenOffer(openOffer, resultHandler, errorMessageHandler);
|
||||
} else {
|
||||
resultHandler.handleResult();
|
||||
}
|
||||
@ -555,6 +576,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
|
||||
Optional<OpenOffer> openOfferOptional = getOpenOfferById(request.offerId);
|
||||
AvailabilityResult availabilityResult;
|
||||
NodeAddress arbitratorNodeAddress = null;
|
||||
NodeAddress mediatorNodeAddress = null;
|
||||
if (openOfferOptional.isPresent()) {
|
||||
OpenOffer openOffer = openOfferOptional.get();
|
||||
if (openOffer.getState() == OpenOffer.State.AVAILABLE) {
|
||||
@ -564,23 +586,35 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
|
||||
|
||||
List<NodeAddress> acceptedArbitrators = user.getAcceptedArbitratorAddresses();
|
||||
if (acceptedArbitrators != null && !acceptedArbitrators.isEmpty()) {
|
||||
arbitratorNodeAddress = ArbitratorSelection.getLeastUsedArbitrator(tradeStatisticsManager, arbitratorManager).getNodeAddress();
|
||||
arbitratorNodeAddress = DisputeAgentSelection.getLeastUsedArbitrator(tradeStatisticsManager, arbitratorManager).getNodeAddress();
|
||||
openOffer.setArbitratorNodeAddress(arbitratorNodeAddress);
|
||||
|
||||
// Check also tradePrice to avoid failures after taker fee is paid caused by a too big difference
|
||||
// in trade price between the peers. Also here poor connectivity might cause market price API connection
|
||||
// losses and therefore an outdated market price.
|
||||
try {
|
||||
offer.checkTradePriceTolerance(request.getTakersTradePrice());
|
||||
} catch (TradePriceOutOfToleranceException e) {
|
||||
log.warn("Trade price check failed because takers price is outside out tolerance.");
|
||||
availabilityResult = AvailabilityResult.PRICE_OUT_OF_TOLERANCE;
|
||||
} catch (MarketPriceNotAvailableException e) {
|
||||
log.warn(e.getMessage());
|
||||
availabilityResult = AvailabilityResult.MARKET_PRICE_NOT_AVAILABLE;
|
||||
} catch (Throwable e) {
|
||||
log.warn("Trade price check failed. " + e.getMessage());
|
||||
availabilityResult = AvailabilityResult.UNKNOWN_FAILURE;
|
||||
mediatorNodeAddress = DisputeAgentSelection.getLeastUsedMediator(tradeStatisticsManager, mediatorManager).getNodeAddress();
|
||||
openOffer.setMediatorNodeAddress(mediatorNodeAddress);
|
||||
Capabilities supportedCapabilities = request.getSupportedCapabilities();
|
||||
if (!OfferRestrictions.requiresUpdate() ||
|
||||
(supportedCapabilities != null &&
|
||||
Capabilities.hasMandatoryCapability(supportedCapabilities, Capability.MEDIATION))) {
|
||||
try {
|
||||
// Check also tradePrice to avoid failures after taker fee is paid caused by a too big difference
|
||||
// in trade price between the peers. Also here poor connectivity might cause market price API connection
|
||||
// losses and therefore an outdated market price.
|
||||
offer.checkTradePriceTolerance(request.getTakersTradePrice());
|
||||
} catch (TradePriceOutOfToleranceException e) {
|
||||
log.warn("Trade price check failed because takers price is outside out tolerance.");
|
||||
availabilityResult = AvailabilityResult.PRICE_OUT_OF_TOLERANCE;
|
||||
} catch (MarketPriceNotAvailableException e) {
|
||||
log.warn(e.getMessage());
|
||||
availabilityResult = AvailabilityResult.MARKET_PRICE_NOT_AVAILABLE;
|
||||
} catch (Throwable e) {
|
||||
log.warn("Trade price check failed. " + e.getMessage());
|
||||
availabilityResult = AvailabilityResult.UNKNOWN_FAILURE;
|
||||
}
|
||||
} else {
|
||||
log.warn("Taker has not mandatory capability MEDIATION");
|
||||
// Because an old peer has not AvailabilityResult.MISSING_MANDATORY_CAPABILITY and we
|
||||
// have not set the UNDEFINED fallback in AvailabilityResult the user will get a null value.
|
||||
availabilityResult = AvailabilityResult.MISSING_MANDATORY_CAPABILITY;
|
||||
}
|
||||
} else {
|
||||
log.warn("acceptedArbitrators is null or empty: acceptedArbitrators=" + acceptedArbitrators);
|
||||
@ -597,7 +631,10 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
|
||||
availabilityResult = AvailabilityResult.OFFER_TAKEN;
|
||||
}
|
||||
|
||||
OfferAvailabilityResponse offerAvailabilityResponse = new OfferAvailabilityResponse(request.offerId, availabilityResult, arbitratorNodeAddress);
|
||||
OfferAvailabilityResponse offerAvailabilityResponse = new OfferAvailabilityResponse(request.offerId,
|
||||
availabilityResult,
|
||||
arbitratorNodeAddress,
|
||||
mediatorNodeAddress);
|
||||
log.info("Send {} with offerId {} and uid {} to peer {}",
|
||||
offerAvailabilityResponse.getClass().getSimpleName(), offerAvailabilityResponse.getOfferId(),
|
||||
offerAvailabilityResponse.getUid(), peer);
|
||||
@ -608,14 +645,18 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
|
||||
@Override
|
||||
public void onArrived() {
|
||||
log.info("{} arrived at peer: offerId={}; uid={}",
|
||||
offerAvailabilityResponse.getClass().getSimpleName(), offerAvailabilityResponse.getOfferId(), offerAvailabilityResponse.getUid());
|
||||
offerAvailabilityResponse.getClass().getSimpleName(),
|
||||
offerAvailabilityResponse.getOfferId(),
|
||||
offerAvailabilityResponse.getUid());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFault(String errorMessage) {
|
||||
log.error("Sending {} failed: uid={}; peer={}; error={}",
|
||||
offerAvailabilityResponse.getClass().getSimpleName(), offerAvailabilityResponse.getUid(),
|
||||
peer, errorMessage);
|
||||
offerAvailabilityResponse.getClass().getSimpleName(),
|
||||
offerAvailabilityResponse.getUid(),
|
||||
peer,
|
||||
errorMessage);
|
||||
}
|
||||
});
|
||||
result = true;
|
||||
@ -628,7 +669,10 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
|
||||
}
|
||||
}
|
||||
|
||||
private void sendAckMessage(OfferAvailabilityRequest message, NodeAddress sender, boolean result, String errorMessage) {
|
||||
private void sendAckMessage(OfferAvailabilityRequest message,
|
||||
NodeAddress sender,
|
||||
boolean result,
|
||||
String errorMessage) {
|
||||
String offerId = message.getOfferId();
|
||||
String sourceUid = message.getUid();
|
||||
AckMessage ackMessage = new AckMessage(p2PService.getNetworkNode().getNodeAddress(),
|
||||
@ -664,6 +708,98 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
|
||||
}
|
||||
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
// Update persisted offer if a new capability is required after a software update
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
private void maybeUpdatePersistedOffers() {
|
||||
// We need to clone to avoid ConcurrentModificationException
|
||||
ArrayList<OpenOffer> openOffersClone = new ArrayList<>(openOffers.getList());
|
||||
openOffersClone.forEach(openOffer -> {
|
||||
Offer originalOffer = openOffer.getOffer();
|
||||
|
||||
OfferPayload originalOfferPayload = originalOffer.getOfferPayload();
|
||||
// We added CAPABILITIES with entry for Capability.MEDIATION in v1.1.6 and want to rewrite a
|
||||
// persisted offer after the user has updated to 1.1.6 so their offer will be accepted by the network.
|
||||
|
||||
if (!OfferRestrictions.hasOfferMandatoryCapability(originalOffer, Capability.MEDIATION)) {
|
||||
// We rewrite our offer with the additional capabilities entry
|
||||
|
||||
Map<String, String> originalExtraDataMap = originalOfferPayload.getExtraDataMap();
|
||||
Map<String, String> updatedExtraDataMap = new HashMap<>();
|
||||
|
||||
if (originalExtraDataMap != null) {
|
||||
updatedExtraDataMap.putAll(originalExtraDataMap);
|
||||
}
|
||||
|
||||
// We overwrite any entry with our current capabilities
|
||||
updatedExtraDataMap.put(OfferPayload.CAPABILITIES, Capabilities.app.toStringList());
|
||||
|
||||
OfferPayload updatedPayload = new OfferPayload(originalOfferPayload.getId(),
|
||||
originalOfferPayload.getDate(),
|
||||
originalOfferPayload.getOwnerNodeAddress(),
|
||||
originalOfferPayload.getPubKeyRing(),
|
||||
originalOfferPayload.getDirection(),
|
||||
originalOfferPayload.getPrice(),
|
||||
originalOfferPayload.getMarketPriceMargin(),
|
||||
originalOfferPayload.isUseMarketBasedPrice(),
|
||||
originalOfferPayload.getAmount(),
|
||||
originalOfferPayload.getMinAmount(),
|
||||
originalOfferPayload.getBaseCurrencyCode(),
|
||||
originalOfferPayload.getCounterCurrencyCode(),
|
||||
originalOfferPayload.getArbitratorNodeAddresses(),
|
||||
originalOfferPayload.getMediatorNodeAddresses(),
|
||||
originalOfferPayload.getPaymentMethodId(),
|
||||
originalOfferPayload.getMakerPaymentAccountId(),
|
||||
originalOfferPayload.getOfferFeePaymentTxId(),
|
||||
originalOfferPayload.getCountryCode(),
|
||||
originalOfferPayload.getAcceptedCountryCodes(),
|
||||
originalOfferPayload.getBankId(),
|
||||
originalOfferPayload.getAcceptedBankIds(),
|
||||
originalOfferPayload.getVersionNr(),
|
||||
originalOfferPayload.getBlockHeightAtOfferCreation(),
|
||||
originalOfferPayload.getTxFee(),
|
||||
originalOfferPayload.getMakerFee(),
|
||||
originalOfferPayload.isCurrencyForMakerFeeBtc(),
|
||||
originalOfferPayload.getBuyerSecurityDeposit(),
|
||||
originalOfferPayload.getSellerSecurityDeposit(),
|
||||
originalOfferPayload.getMaxTradeLimit(),
|
||||
originalOfferPayload.getMaxTradePeriod(),
|
||||
originalOfferPayload.isUseAutoClose(),
|
||||
originalOfferPayload.isUseReOpenAfterAutoClose(),
|
||||
originalOfferPayload.getLowerClosePrice(),
|
||||
originalOfferPayload.getUpperClosePrice(),
|
||||
originalOfferPayload.isPrivateOffer(),
|
||||
originalOfferPayload.getHashOfChallenge(),
|
||||
updatedExtraDataMap,
|
||||
originalOfferPayload.getProtocolVersion());
|
||||
|
||||
// Save states from original data to use the for updated
|
||||
Offer.State originalOfferState = originalOffer.getState();
|
||||
OpenOffer.State originalOpenOfferState = openOffer.getState();
|
||||
|
||||
// remove old offer
|
||||
originalOffer.setState(Offer.State.REMOVED);
|
||||
openOffer.setState(OpenOffer.State.CANCELED);
|
||||
openOffer.setStorage(openOfferTradableListStorage);
|
||||
openOffers.remove(openOffer);
|
||||
|
||||
// Create new Offer
|
||||
Offer updatedOffer = new Offer(updatedPayload);
|
||||
updatedOffer.setPriceFeedService(priceFeedService);
|
||||
updatedOffer.setState(originalOfferState);
|
||||
|
||||
OpenOffer updatedOpenOffer = new OpenOffer(updatedOffer, openOfferTradableListStorage);
|
||||
updatedOpenOffer.setState(originalOpenOfferState);
|
||||
updatedOpenOffer.setStorage(openOfferTradableListStorage);
|
||||
openOffers.add(updatedOpenOffer);
|
||||
|
||||
log.info("Converted offer to support new Capability.MEDIATION capability. id={}", originalOffer.getId());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
// RepublishOffers, refreshOffers
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
@ -17,13 +17,15 @@
|
||||
|
||||
package bisq.core.offer.availability;
|
||||
|
||||
import bisq.core.arbitration.Arbitrator;
|
||||
import bisq.core.arbitration.ArbitratorManager;
|
||||
import bisq.core.support.dispute.agent.DisputeAgent;
|
||||
import bisq.core.support.dispute.agent.DisputeAgentManager;
|
||||
import bisq.core.trade.statistics.TradeStatistics2;
|
||||
import bisq.core.trade.statistics.TradeStatisticsManager;
|
||||
|
||||
import bisq.common.util.Tuple2;
|
||||
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
@ -39,10 +41,25 @@ import lombok.extern.slf4j.Slf4j;
|
||||
import static com.google.common.base.Preconditions.checkArgument;
|
||||
|
||||
@Slf4j
|
||||
public class ArbitratorSelection {
|
||||
public class DisputeAgentSelection {
|
||||
|
||||
public static Arbitrator getLeastUsedArbitrator(TradeStatisticsManager tradeStatisticsManager,
|
||||
ArbitratorManager arbitratorManager) {
|
||||
public static <T extends DisputeAgent> T getLeastUsedArbitrator(TradeStatisticsManager tradeStatisticsManager,
|
||||
DisputeAgentManager<T> disputeAgentManager) {
|
||||
return getLeastUsedDisputeAgent(tradeStatisticsManager,
|
||||
disputeAgentManager,
|
||||
TradeStatistics2.ARBITRATOR_ADDRESS);
|
||||
}
|
||||
|
||||
public static <T extends DisputeAgent> T getLeastUsedMediator(TradeStatisticsManager tradeStatisticsManager,
|
||||
DisputeAgentManager<T> disputeAgentManager) {
|
||||
return getLeastUsedDisputeAgent(tradeStatisticsManager,
|
||||
disputeAgentManager,
|
||||
TradeStatistics2.MEDIATOR_ADDRESS);
|
||||
}
|
||||
|
||||
private static <T extends DisputeAgent> T getLeastUsedDisputeAgent(TradeStatisticsManager tradeStatisticsManager,
|
||||
DisputeAgentManager<T> disputeAgentManager,
|
||||
String extraMapKey) {
|
||||
// We take last 100 entries from trade statistics
|
||||
List<TradeStatistics2> list = new ArrayList<>(tradeStatisticsManager.getObservableTradeStatisticsSet());
|
||||
list.sort(Comparator.comparing(TradeStatistics2::getTradeDate));
|
||||
@ -52,32 +69,33 @@ public class ArbitratorSelection {
|
||||
list = list.subList(0, max);
|
||||
}
|
||||
|
||||
// We stored only first 4 chars of arbitrators onion address
|
||||
// We stored only first 4 chars of disputeAgents onion address
|
||||
List<String> lastAddressesUsedInTrades = list.stream()
|
||||
.filter(tradeStatistics2 -> tradeStatistics2.getExtraDataMap() != null)
|
||||
.map(tradeStatistics2 -> tradeStatistics2.getExtraDataMap().get(TradeStatistics2.ARBITRATOR_ADDRESS))
|
||||
.map(tradeStatistics2 -> tradeStatistics2.getExtraDataMap().get(extraMapKey))
|
||||
.filter(Objects::nonNull)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
Set<String> arbitrators = arbitratorManager.getArbitratorsObservableMap().values().stream()
|
||||
.map(arbitrator -> arbitrator.getNodeAddress().getFullAddress())
|
||||
Set<String> disputeAgents = disputeAgentManager.getObservableMap().values().stream()
|
||||
.map(disputeAgent -> disputeAgent.getNodeAddress().getFullAddress())
|
||||
.collect(Collectors.toSet());
|
||||
|
||||
String result = getLeastUsedArbitrator(lastAddressesUsedInTrades, arbitrators);
|
||||
String result = getLeastUsedDisputeAgent(lastAddressesUsedInTrades, disputeAgents);
|
||||
|
||||
Optional<Arbitrator> optionalArbitrator = arbitratorManager.getArbitratorsObservableMap().values().stream()
|
||||
Optional<T> optionalDisputeAgent = disputeAgentManager.getObservableMap().values().stream()
|
||||
.filter(e -> e.getNodeAddress().getFullAddress().equals(result))
|
||||
.findAny();
|
||||
checkArgument(optionalArbitrator.isPresent(), "optionalArbitrator has to be present");
|
||||
return optionalArbitrator.get();
|
||||
checkArgument(optionalDisputeAgent.isPresent(), "optionalDisputeAgent has to be present");
|
||||
return optionalDisputeAgent.get();
|
||||
}
|
||||
|
||||
static String getLeastUsedArbitrator(List<String> lastAddressesUsedInTrades, Set<String> arbitrators) {
|
||||
checkArgument(!arbitrators.isEmpty(), "arbitrators must not be empty");
|
||||
List<Tuple2<String, AtomicInteger>> arbitratorTuples = arbitrators.stream()
|
||||
@VisibleForTesting
|
||||
static String getLeastUsedDisputeAgent(List<String> lastAddressesUsedInTrades, Set<String> disputeAgents) {
|
||||
checkArgument(!disputeAgents.isEmpty(), "disputeAgents must not be empty");
|
||||
List<Tuple2<String, AtomicInteger>> disputeAgentTuples = disputeAgents.stream()
|
||||
.map(e -> new Tuple2<>(e, new AtomicInteger(0)))
|
||||
.collect(Collectors.toList());
|
||||
arbitratorTuples.forEach(tuple -> {
|
||||
disputeAgentTuples.forEach(tuple -> {
|
||||
int count = (int) lastAddressesUsedInTrades.stream()
|
||||
.filter(tuple.first::startsWith) // we use only first 4 chars for comparing
|
||||
.mapToInt(e -> 1)
|
||||
@ -85,8 +103,8 @@ public class ArbitratorSelection {
|
||||
tuple.second.set(count);
|
||||
});
|
||||
|
||||
arbitratorTuples.sort(Comparator.comparing(e -> e.first));
|
||||
arbitratorTuples.sort(Comparator.comparingInt(e -> e.second.get()));
|
||||
return arbitratorTuples.get(0).first;
|
||||
disputeAgentTuples.sort(Comparator.comparing(e -> e.first));
|
||||
disputeAgentTuples.sort(Comparator.comparingInt(e -> e.second.get()));
|
||||
return disputeAgentTuples.get(0).first;
|
||||
}
|
||||
}
|
@ -19,6 +19,8 @@ package bisq.core.offer.availability;
|
||||
|
||||
import bisq.core.offer.Offer;
|
||||
import bisq.core.offer.messages.OfferAvailabilityResponse;
|
||||
import bisq.core.support.dispute.mediation.mediator.MediatorManager;
|
||||
import bisq.core.trade.statistics.TradeStatisticsManager;
|
||||
import bisq.core.user.User;
|
||||
|
||||
import bisq.network.p2p.NodeAddress;
|
||||
@ -41,6 +43,10 @@ public class OfferAvailabilityModel implements Model {
|
||||
private final P2PService p2PService;
|
||||
@Getter
|
||||
final private User user;
|
||||
@Getter
|
||||
private final MediatorManager mediatorManager;
|
||||
@Getter
|
||||
private final TradeStatisticsManager tradeStatisticsManager;
|
||||
private NodeAddress peerNodeAddress; // maker
|
||||
private OfferAvailabilityResponse message;
|
||||
@Nullable
|
||||
@ -48,21 +54,32 @@ public class OfferAvailabilityModel implements Model {
|
||||
@Getter
|
||||
private NodeAddress selectedArbitrator;
|
||||
|
||||
// Added in v1.1.6
|
||||
@Nullable
|
||||
@Setter
|
||||
@Getter
|
||||
private NodeAddress selectedMediator;
|
||||
|
||||
|
||||
public OfferAvailabilityModel(Offer offer,
|
||||
PubKeyRing pubKeyRing,
|
||||
P2PService p2PService,
|
||||
User user) {
|
||||
User user,
|
||||
MediatorManager mediatorManager,
|
||||
TradeStatisticsManager tradeStatisticsManager) {
|
||||
this.offer = offer;
|
||||
this.pubKeyRing = pubKeyRing;
|
||||
this.p2PService = p2PService;
|
||||
this.user = user;
|
||||
this.mediatorManager = mediatorManager;
|
||||
this.tradeStatisticsManager = tradeStatisticsManager;
|
||||
}
|
||||
|
||||
public NodeAddress getPeerNodeAddress() {
|
||||
return peerNodeAddress;
|
||||
}
|
||||
|
||||
public void setPeerNodeAddress(NodeAddress peerNodeAddress) {
|
||||
void setPeerNodeAddress(NodeAddress peerNodeAddress) {
|
||||
this.peerNodeAddress = peerNodeAddress;
|
||||
}
|
||||
|
||||
|
@ -19,24 +19,18 @@ package bisq.core.offer.availability.tasks;
|
||||
|
||||
import bisq.core.offer.AvailabilityResult;
|
||||
import bisq.core.offer.Offer;
|
||||
import bisq.core.offer.availability.DisputeAgentSelection;
|
||||
import bisq.core.offer.availability.OfferAvailabilityModel;
|
||||
import bisq.core.offer.messages.OfferAvailabilityResponse;
|
||||
import bisq.core.trade.protocol.ArbitratorSelectionRule;
|
||||
|
||||
import bisq.network.p2p.NodeAddress;
|
||||
|
||||
import bisq.common.taskrunner.Task;
|
||||
import bisq.common.taskrunner.TaskRunner;
|
||||
|
||||
import com.google.common.collect.Lists;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import static com.google.common.base.Preconditions.checkArgument;
|
||||
import static com.google.common.base.Preconditions.checkNotNull;
|
||||
|
||||
@Slf4j
|
||||
public class ProcessOfferAvailabilityResponse extends Task<OfferAvailabilityModel> {
|
||||
@ -62,43 +56,14 @@ public class ProcessOfferAvailabilityResponse extends Task<OfferAvailabilityMode
|
||||
|
||||
offer.setState(Offer.State.AVAILABLE);
|
||||
|
||||
NodeAddress selectedArbitrator = offerAvailabilityResponse.getArbitrator();
|
||||
|
||||
if (selectedArbitrator != null) {
|
||||
model.setSelectedArbitrator(selectedArbitrator);
|
||||
complete();
|
||||
return;
|
||||
}
|
||||
|
||||
// We have an offer from a maker who runs a pre 0.9 version.
|
||||
log.info("Maker has on old version and does not send the selected arbitrator in the offerAvailabilityResponse. " +
|
||||
"We use the old selection model instead with the supported arbitrators of the offers");
|
||||
|
||||
List<NodeAddress> userArbitratorAddresses = model.getUser().getAcceptedArbitratorAddresses();
|
||||
checkNotNull(userArbitratorAddresses, "model.getUser().getAcceptedArbitratorAddresses() must not be null");
|
||||
|
||||
List<NodeAddress> offerArbitratorNodeAddresses = offer.getArbitratorNodeAddresses();
|
||||
|
||||
List<NodeAddress> matchingArbitrators = offerArbitratorNodeAddresses.stream()
|
||||
.filter(userArbitratorAddresses::contains)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
if (!matchingArbitrators.isEmpty()) {
|
||||
// We have at least one arbitrator which was used in the offer and is still available.
|
||||
try {
|
||||
model.setSelectedArbitrator(ArbitratorSelectionRule.select(Lists.newArrayList(matchingArbitrators), offer));
|
||||
complete();
|
||||
} catch (Throwable t) {
|
||||
failed("There is no arbitrator matching that offer. The maker has " +
|
||||
"not updated to the latest version and the arbitrators selected for that offer are not available anymore.");
|
||||
}
|
||||
} else {
|
||||
// If an arbitrator which was selected in the offer from an old version has revoked we would get a failed take-offer attempt
|
||||
// with lost trade fees. To avoid that we fail here after 1 week after the new rule is activated.
|
||||
// Because one arbitrator need to revoke his application and register new as he gets too many transactions already
|
||||
// we need to handle the planned revoke case.
|
||||
failed("You cannot take that offer because the maker has not updated to version 0.9.");
|
||||
model.setSelectedArbitrator(offerAvailabilityResponse.getArbitrator());
|
||||
NodeAddress mediator = offerAvailabilityResponse.getMediator();
|
||||
if (mediator == null) {
|
||||
// We do not get a mediator from old clients so we need to handle the null case.
|
||||
mediator = DisputeAgentSelection.getLeastUsedMediator(model.getTradeStatisticsManager(), model.getMediatorManager()).getNodeAddress();
|
||||
}
|
||||
model.setSelectedMediator(mediator);
|
||||
complete();
|
||||
} catch (Throwable t) {
|
||||
offer.setErrorMessage("An error occurred.\n" +
|
||||
"Error message:\n"
|
||||
|
@ -28,6 +28,7 @@ import java.util.UUID;
|
||||
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.Value;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
@ -35,6 +36,7 @@ import javax.annotation.Nullable;
|
||||
// to the trading peer
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@Value
|
||||
@Slf4j
|
||||
public final class OfferAvailabilityRequest extends OfferMessage implements SupportedCapabilitiesMessage {
|
||||
private final PubKeyRing pubKeyRing;
|
||||
private final long takersTradePrice;
|
||||
|
@ -44,17 +44,22 @@ public final class OfferAvailabilityResponse extends OfferMessage implements Sup
|
||||
@Nullable
|
||||
private final Capabilities supportedCapabilities;
|
||||
|
||||
// Was introduced in v 0.9.0. Might be null if msg received from node with old version
|
||||
@Nullable
|
||||
private final NodeAddress arbitrator;
|
||||
// Was introduced in v 1.1.6. Might be null if msg received from node with old version
|
||||
@Nullable
|
||||
private final NodeAddress mediator;
|
||||
|
||||
public OfferAvailabilityResponse(String offerId, AvailabilityResult availabilityResult, NodeAddress arbitrator) {
|
||||
public OfferAvailabilityResponse(String offerId,
|
||||
AvailabilityResult availabilityResult,
|
||||
NodeAddress arbitrator,
|
||||
NodeAddress mediator) {
|
||||
this(offerId,
|
||||
availabilityResult,
|
||||
Capabilities.app,
|
||||
Version.getP2PMessageVersion(),
|
||||
UUID.randomUUID().toString(),
|
||||
arbitrator);
|
||||
arbitrator,
|
||||
mediator);
|
||||
}
|
||||
|
||||
|
||||
@ -67,22 +72,25 @@ public final class OfferAvailabilityResponse extends OfferMessage implements Sup
|
||||
@Nullable Capabilities supportedCapabilities,
|
||||
int messageVersion,
|
||||
@Nullable String uid,
|
||||
@Nullable NodeAddress arbitrator) {
|
||||
NodeAddress arbitrator,
|
||||
@Nullable NodeAddress mediator) {
|
||||
super(messageVersion, offerId, uid);
|
||||
this.availabilityResult = availabilityResult;
|
||||
this.supportedCapabilities = supportedCapabilities;
|
||||
this.arbitrator = arbitrator;
|
||||
this.mediator = mediator;
|
||||
}
|
||||
|
||||
@Override
|
||||
public protobuf.NetworkEnvelope toProtoNetworkEnvelope() {
|
||||
final protobuf.OfferAvailabilityResponse.Builder builder = protobuf.OfferAvailabilityResponse.newBuilder()
|
||||
.setOfferId(offerId)
|
||||
.setAvailabilityResult(protobuf.AvailabilityResult.valueOf(availabilityResult.name()));
|
||||
.setAvailabilityResult(protobuf.AvailabilityResult.valueOf(availabilityResult.name()))
|
||||
.setArbitrator(arbitrator.toProtoMessage());
|
||||
|
||||
Optional.ofNullable(supportedCapabilities).ifPresent(e -> builder.addAllSupportedCapabilities(Capabilities.toIntList(supportedCapabilities)));
|
||||
Optional.ofNullable(uid).ifPresent(e -> builder.setUid(uid));
|
||||
Optional.ofNullable(arbitrator).ifPresent(e -> builder.setArbitrator(arbitrator.toProtoMessage()));
|
||||
Optional.ofNullable(mediator).ifPresent(e -> builder.setMediator(mediator.toProtoMessage()));
|
||||
|
||||
return getNetworkEnvelopeBuilder()
|
||||
.setOfferAvailabilityResponse(builder)
|
||||
@ -95,6 +103,7 @@ public final class OfferAvailabilityResponse extends OfferMessage implements Sup
|
||||
Capabilities.fromIntList(proto.getSupportedCapabilitiesList()),
|
||||
messageVersion,
|
||||
proto.getUid().isEmpty() ? null : proto.getUid(),
|
||||
proto.hasArbitrator() ? NodeAddress.fromProto(proto.getArbitrator()) : null);
|
||||
NodeAddress.fromProto(proto.getArbitrator()),
|
||||
proto.hasMediator() ? NodeAddress.fromProto(proto.getMediator()) : null);
|
||||
}
|
||||
}
|
||||
|
@ -17,12 +17,12 @@
|
||||
|
||||
package bisq.core.offer.placeoffer;
|
||||
|
||||
import bisq.core.arbitration.ArbitratorManager;
|
||||
import bisq.core.btc.wallet.BsqWalletService;
|
||||
import bisq.core.btc.wallet.BtcWalletService;
|
||||
import bisq.core.btc.wallet.TradeWalletService;
|
||||
import bisq.core.offer.Offer;
|
||||
import bisq.core.offer.OfferBookService;
|
||||
import bisq.core.support.dispute.arbitration.arbitrator.ArbitratorManager;
|
||||
import bisq.core.trade.statistics.TradeStatisticsManager;
|
||||
import bisq.core.user.User;
|
||||
|
||||
|
@ -17,7 +17,6 @@
|
||||
|
||||
package bisq.core.offer.placeoffer.tasks;
|
||||
|
||||
import bisq.core.arbitration.Arbitrator;
|
||||
import bisq.core.btc.exceptions.TxBroadcastException;
|
||||
import bisq.core.btc.model.AddressEntry;
|
||||
import bisq.core.btc.wallet.BsqWalletService;
|
||||
@ -28,8 +27,9 @@ import bisq.core.btc.wallet.WalletService;
|
||||
import bisq.core.dao.exceptions.DaoDisabledException;
|
||||
import bisq.core.dao.state.model.blockchain.TxType;
|
||||
import bisq.core.offer.Offer;
|
||||
import bisq.core.offer.availability.ArbitratorSelection;
|
||||
import bisq.core.offer.availability.DisputeAgentSelection;
|
||||
import bisq.core.offer.placeoffer.PlaceOfferModel;
|
||||
import bisq.core.support.dispute.arbitration.arbitrator.Arbitrator;
|
||||
|
||||
import bisq.common.UserThread;
|
||||
import bisq.common.taskrunner.Task;
|
||||
@ -47,7 +47,7 @@ public class CreateMakerFeeTx extends Task<PlaceOfferModel> {
|
||||
private static final Logger log = LoggerFactory.getLogger(CreateMakerFeeTx.class);
|
||||
private Transaction tradeFeeTx = null;
|
||||
|
||||
@SuppressWarnings({"WeakerAccess", "unused"})
|
||||
@SuppressWarnings({"unused"})
|
||||
public CreateMakerFeeTx(TaskRunner taskHandler, PlaceOfferModel model) {
|
||||
super(taskHandler, model);
|
||||
}
|
||||
@ -62,7 +62,7 @@ public class CreateMakerFeeTx extends Task<PlaceOfferModel> {
|
||||
String id = offer.getId();
|
||||
BtcWalletService walletService = model.getWalletService();
|
||||
|
||||
Arbitrator arbitrator = ArbitratorSelection.getLeastUsedArbitrator(model.getTradeStatisticsManager(),
|
||||
Arbitrator arbitrator = DisputeAgentSelection.getLeastUsedArbitrator(model.getTradeStatisticsManager(),
|
||||
model.getArbitratorManager());
|
||||
|
||||
Address fundingAddress = walletService.getOrCreateAddressEntry(id, AddressEntry.Context.OFFER_FUNDING).getAddress();
|
||||
|
@ -83,8 +83,6 @@ public class ValidateOffer extends Task<PlaceOfferModel> {
|
||||
checkArgument(offer.getDate().getTime() > 0,
|
||||
"Date must not be 0. date=" + offer.getDate().toString());
|
||||
|
||||
checkNotNull(offer.getArbitratorNodeAddresses(), "Arbitrator is null");
|
||||
checkNotNull(offer.getMediatorNodeAddresses(), "Mediator is null");
|
||||
checkNotNull(offer.getCurrencyCode(), "Currency is null");
|
||||
checkNotNull(offer.getDirection(), "Direction is null");
|
||||
checkNotNull(offer.getId(), "Id is null");
|
||||
|
@ -65,9 +65,9 @@ public abstract class CountryBasedPaymentAccountPayload extends PaymentAccountPa
|
||||
.setCountryBasedPaymentAccountPayload(builder);
|
||||
}
|
||||
|
||||
abstract public String getPaymentDetails();
|
||||
public abstract String getPaymentDetails();
|
||||
|
||||
abstract public String getPaymentDetailsForTradePopup();
|
||||
public abstract String getPaymentDetailsForTradePopup();
|
||||
|
||||
@Override
|
||||
protected byte[] getAgeWitnessInputData(byte[] data) {
|
||||
|
@ -118,9 +118,9 @@ public abstract class PaymentAccountPayload implements NetworkPayload, UsedForTr
|
||||
// API
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
abstract public String getPaymentDetails();
|
||||
public abstract String getPaymentDetails();
|
||||
|
||||
abstract public String getPaymentDetailsForTradePopup();
|
||||
public abstract String getPaymentDetailsForTradePopup();
|
||||
|
||||
public byte[] getSalt() {
|
||||
checkNotNull(excludeFromJsonDataMap, "excludeFromJsonDataMap must not be null");
|
||||
|
@ -32,7 +32,7 @@ public class CorePresentationModule extends AppModule {
|
||||
protected void configure() {
|
||||
bind(BalancePresentation.class).in(Singleton.class);
|
||||
bind(TradePresentation.class).in(Singleton.class);
|
||||
bind(DisputePresentation.class).in(Singleton.class);
|
||||
bind(SupportTicketsPresentation.class).in(Singleton.class);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,49 +0,0 @@
|
||||
/*
|
||||
* This file is part of Bisq.
|
||||
*
|
||||
* Bisq is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or (at
|
||||
* your option) any later version.
|
||||
*
|
||||
* Bisq is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
|
||||
* License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package bisq.core.presentation;
|
||||
|
||||
import bisq.core.arbitration.DisputeManager;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
import javafx.beans.property.SimpleBooleanProperty;
|
||||
import javafx.beans.property.SimpleStringProperty;
|
||||
import javafx.beans.property.StringProperty;
|
||||
|
||||
import lombok.Getter;
|
||||
|
||||
public class DisputePresentation {
|
||||
@Getter
|
||||
private final StringProperty numOpenDisputes = new SimpleStringProperty();
|
||||
@Getter
|
||||
private final BooleanProperty showOpenDisputesNotification = new SimpleBooleanProperty();
|
||||
|
||||
@Inject
|
||||
public DisputePresentation(DisputeManager disputeManager) {
|
||||
disputeManager.getNumOpenDisputes().addListener((observable, oldValue, newValue) -> {
|
||||
int openDisputes = (int) newValue;
|
||||
if (openDisputes > 0)
|
||||
numOpenDisputes.set(String.valueOf(openDisputes));
|
||||
if (openDisputes > 9)
|
||||
numOpenDisputes.set("★");
|
||||
|
||||
showOpenDisputesNotification.set(openDisputes > 0);
|
||||
});
|
||||
}
|
||||
}
|
@ -0,0 +1,78 @@
|
||||
/*
|
||||
* This file is part of Bisq.
|
||||
*
|
||||
* Bisq is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or (at
|
||||
* your option) any later version.
|
||||
*
|
||||
* Bisq is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
|
||||
* License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package bisq.core.presentation;
|
||||
|
||||
import bisq.core.support.dispute.arbitration.ArbitrationManager;
|
||||
import bisq.core.support.dispute.mediation.MediationManager;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
import javafx.beans.property.SimpleBooleanProperty;
|
||||
import javafx.beans.property.SimpleStringProperty;
|
||||
import javafx.beans.property.StringProperty;
|
||||
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
import lombok.Getter;
|
||||
|
||||
public class SupportTicketsPresentation {
|
||||
@Getter
|
||||
private final StringProperty numOpenArbitrationTickets = new SimpleStringProperty();
|
||||
@Getter
|
||||
private final BooleanProperty showOpenArbitrationTicketsNotification = new SimpleBooleanProperty();
|
||||
|
||||
@Getter
|
||||
private final StringProperty numOpenMediationTickets = new SimpleStringProperty();
|
||||
@Getter
|
||||
private final BooleanProperty showOpenMediationTicketsNotification = new SimpleBooleanProperty();
|
||||
|
||||
@Getter
|
||||
private final StringProperty numOpenSupportTickets = new SimpleStringProperty();
|
||||
@Getter
|
||||
private final BooleanProperty showOpenSupportTicketsNotification = new SimpleBooleanProperty();
|
||||
@org.jetbrains.annotations.NotNull
|
||||
private final ArbitrationManager arbitrationManager;
|
||||
@org.jetbrains.annotations.NotNull
|
||||
private final MediationManager mediationManager;
|
||||
|
||||
@Inject
|
||||
public SupportTicketsPresentation(ArbitrationManager arbitrationManager, MediationManager mediationManager) {
|
||||
this.arbitrationManager = arbitrationManager;
|
||||
this.mediationManager = mediationManager;
|
||||
|
||||
arbitrationManager.getNumOpenDisputes().addListener((observable, oldValue, newValue) -> onChange());
|
||||
mediationManager.getNumOpenDisputes().addListener((observable, oldValue, newValue) -> onChange());
|
||||
}
|
||||
|
||||
private void onChange() {
|
||||
AtomicInteger openArbitrationDisputes = new AtomicInteger(arbitrationManager.getNumOpenDisputes().get());
|
||||
int arbitrationTickets = openArbitrationDisputes.get();
|
||||
numOpenArbitrationTickets.set(String.valueOf(arbitrationTickets));
|
||||
showOpenArbitrationTicketsNotification.set(arbitrationTickets > 0);
|
||||
|
||||
AtomicInteger openMediationDisputes = new AtomicInteger(mediationManager.getNumOpenDisputes().get());
|
||||
int mediationTickets = openMediationDisputes.get();
|
||||
numOpenMediationTickets.set(String.valueOf(mediationTickets));
|
||||
showOpenMediationTicketsNotification.set(mediationTickets > 0);
|
||||
|
||||
int supportTickets = arbitrationTickets + mediationTickets;
|
||||
numOpenSupportTickets.set(String.valueOf(supportTickets));
|
||||
showOpenSupportTicketsNotification.set(supportTickets > 0);
|
||||
}
|
||||
}
|
@ -17,8 +17,8 @@
|
||||
|
||||
package bisq.core.proto;
|
||||
|
||||
import bisq.core.arbitration.DisputeResult;
|
||||
import bisq.core.btc.model.AddressEntry;
|
||||
import bisq.core.support.dispute.DisputeResult;
|
||||
import bisq.core.offer.AvailabilityResult;
|
||||
import bisq.core.offer.Offer;
|
||||
import bisq.core.offer.OfferPayload;
|
||||
|
@ -19,13 +19,6 @@ package bisq.core.proto.network;
|
||||
|
||||
import bisq.core.alert.Alert;
|
||||
import bisq.core.alert.PrivateNotificationMessage;
|
||||
import bisq.core.arbitration.Arbitrator;
|
||||
import bisq.core.arbitration.Mediator;
|
||||
import bisq.core.arbitration.messages.DisputeCommunicationMessage;
|
||||
import bisq.core.arbitration.messages.DisputeResultMessage;
|
||||
import bisq.core.arbitration.messages.OpenNewDisputeMessage;
|
||||
import bisq.core.arbitration.messages.PeerOpenedDisputeMessage;
|
||||
import bisq.core.arbitration.messages.PeerPublishedDisputePayoutTxMessage;
|
||||
import bisq.core.dao.governance.blindvote.network.messages.RepublishGovernanceDataRequest;
|
||||
import bisq.core.dao.governance.proposal.storage.temp.TempProposalPayload;
|
||||
import bisq.core.dao.monitoring.network.messages.GetBlindVoteStateHashesRequest;
|
||||
@ -45,8 +38,17 @@ import bisq.core.offer.OfferPayload;
|
||||
import bisq.core.offer.messages.OfferAvailabilityRequest;
|
||||
import bisq.core.offer.messages.OfferAvailabilityResponse;
|
||||
import bisq.core.proto.CoreProtoResolver;
|
||||
import bisq.core.support.dispute.arbitration.arbitrator.Arbitrator;
|
||||
import bisq.core.support.dispute.arbitration.messages.PeerPublishedDisputePayoutTxMessage;
|
||||
import bisq.core.support.dispute.mediation.mediator.Mediator;
|
||||
import bisq.core.support.dispute.messages.DisputeResultMessage;
|
||||
import bisq.core.support.dispute.messages.OpenNewDisputeMessage;
|
||||
import bisq.core.support.dispute.messages.PeerOpenedDisputeMessage;
|
||||
import bisq.core.support.messages.ChatMessage;
|
||||
import bisq.core.trade.messages.CounterCurrencyTransferStartedMessage;
|
||||
import bisq.core.trade.messages.DepositTxPublishedMessage;
|
||||
import bisq.core.trade.messages.MediatedPayoutTxPublishedMessage;
|
||||
import bisq.core.trade.messages.MediatedPayoutTxSignatureMessage;
|
||||
import bisq.core.trade.messages.PayDepositRequest;
|
||||
import bisq.core.trade.messages.PayoutTxPublishedMessage;
|
||||
import bisq.core.trade.messages.PublishDepositTxRequest;
|
||||
@ -142,13 +144,17 @@ public class CoreNetworkProtoResolver extends CoreProtoResolver implements Netwo
|
||||
return CounterCurrencyTransferStartedMessage.fromProto(proto.getCounterCurrencyTransferStartedMessage(), messageVersion);
|
||||
case PAYOUT_TX_PUBLISHED_MESSAGE:
|
||||
return PayoutTxPublishedMessage.fromProto(proto.getPayoutTxPublishedMessage(), messageVersion);
|
||||
case MEDIATED_PAYOUT_TX_SIGNATURE_MESSAGE:
|
||||
return MediatedPayoutTxSignatureMessage.fromProto(proto.getMediatedPayoutTxSignatureMessage(), messageVersion);
|
||||
case MEDIATED_PAYOUT_TX_PUBLISHED_MESSAGE:
|
||||
return MediatedPayoutTxPublishedMessage.fromProto(proto.getMediatedPayoutTxPublishedMessage(), messageVersion);
|
||||
|
||||
case OPEN_NEW_DISPUTE_MESSAGE:
|
||||
return OpenNewDisputeMessage.fromProto(proto.getOpenNewDisputeMessage(), this, messageVersion);
|
||||
case PEER_OPENED_DISPUTE_MESSAGE:
|
||||
return PeerOpenedDisputeMessage.fromProto(proto.getPeerOpenedDisputeMessage(), this, messageVersion);
|
||||
case DISPUTE_COMMUNICATION_MESSAGE:
|
||||
return DisputeCommunicationMessage.fromProto(proto.getDisputeCommunicationMessage(), messageVersion);
|
||||
case CHAT_MESSAGE:
|
||||
return ChatMessage.fromProto(proto.getChatMessage(), messageVersion);
|
||||
case DISPUTE_RESULT_MESSAGE:
|
||||
return DisputeResultMessage.fromProto(proto.getDisputeResultMessage(), messageVersion);
|
||||
case PEER_PUBLISHED_DISPUTE_PAYOUT_TX_MESSAGE:
|
||||
|
@ -19,7 +19,6 @@ package bisq.core.proto.persistable;
|
||||
|
||||
import bisq.core.account.sign.SignedWitnessStore;
|
||||
import bisq.core.account.witness.AccountAgeWitnessStore;
|
||||
import bisq.core.arbitration.DisputeList;
|
||||
import bisq.core.btc.model.AddressEntryList;
|
||||
import bisq.core.btc.wallet.BtcWalletService;
|
||||
import bisq.core.dao.governance.blindvote.MyBlindVoteList;
|
||||
@ -36,6 +35,8 @@ import bisq.core.dao.state.model.governance.MeritList;
|
||||
import bisq.core.dao.state.unconfirmed.UnconfirmedBsqChangeOutputList;
|
||||
import bisq.core.payment.PaymentAccountList;
|
||||
import bisq.core.proto.CoreProtoResolver;
|
||||
import bisq.core.support.dispute.arbitration.ArbitrationDisputeList;
|
||||
import bisq.core.support.dispute.mediation.MediationDisputeList;
|
||||
import bisq.core.trade.TradableList;
|
||||
import bisq.core.trade.statistics.TradeStatistics2Store;
|
||||
import bisq.core.user.PreferencesPayload;
|
||||
@ -101,8 +102,12 @@ public class CorePersistenceProtoResolver extends CoreProtoResolver implements P
|
||||
btcWalletService.get());
|
||||
case TRADE_STATISTICS_LIST:
|
||||
throw new ProtobufferRuntimeException("TRADE_STATISTICS_LIST is not used anymore");
|
||||
case DISPUTE_LIST:
|
||||
return DisputeList.fromProto(proto.getDisputeList(),
|
||||
case ARBITRATION_DISPUTE_LIST:
|
||||
return ArbitrationDisputeList.fromProto(proto.getArbitrationDisputeList(),
|
||||
this,
|
||||
new Storage<>(storageDir, this, corruptedDatabaseFilesHandler));
|
||||
case MEDIATION_DISPUTE_LIST:
|
||||
return MediationDisputeList.fromProto(proto.getMediationDisputeList(),
|
||||
this,
|
||||
new Storage<>(storageDir, this, corruptedDatabaseFilesHandler));
|
||||
case PREFERENCES_PAYLOAD:
|
||||
|
@ -27,7 +27,7 @@ import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
@Slf4j
|
||||
public class CoreNetworkCapabilities {
|
||||
public static void setSupportedCapabilities(BisqEnvironment bisqEnvironment) {
|
||||
static void setSupportedCapabilities(BisqEnvironment bisqEnvironment) {
|
||||
Capabilities.app.addAll(
|
||||
Capability.TRADE_STATISTICS,
|
||||
Capability.TRADE_STATISTICS_2,
|
||||
@ -36,7 +36,8 @@ public class CoreNetworkCapabilities {
|
||||
Capability.PROPOSAL,
|
||||
Capability.BLIND_VOTE,
|
||||
Capability.DAO_STATE,
|
||||
Capability.BUNDLE_OF_ENVELOPES
|
||||
Capability.BUNDLE_OF_ENVELOPES,
|
||||
Capability.MEDIATION
|
||||
);
|
||||
|
||||
if (BisqEnvironment.isDaoActivated(bisqEnvironment)) {
|
||||
|
@ -17,7 +17,6 @@
|
||||
|
||||
package bisq.core.setup;
|
||||
|
||||
import bisq.core.arbitration.DisputeManager;
|
||||
import bisq.core.btc.model.AddressEntryList;
|
||||
import bisq.core.dao.DaoOptionKeys;
|
||||
import bisq.core.dao.governance.ballot.BallotListService;
|
||||
@ -28,6 +27,8 @@ import bisq.core.dao.governance.proofofburn.MyProofOfBurnListService;
|
||||
import bisq.core.dao.governance.proposal.MyProposalListService;
|
||||
import bisq.core.dao.state.unconfirmed.UnconfirmedBsqChangeOutputListService;
|
||||
import bisq.core.offer.OpenOfferManager;
|
||||
import bisq.core.support.dispute.arbitration.ArbitrationDisputeListService;
|
||||
import bisq.core.support.dispute.mediation.MediationDisputeListService;
|
||||
import bisq.core.trade.TradeManager;
|
||||
import bisq.core.trade.closed.ClosedTradableManager;
|
||||
import bisq.core.trade.failed.FailedTradesManager;
|
||||
@ -60,7 +61,8 @@ public class CorePersistedDataHost {
|
||||
persistedDataHosts.add(injector.getInstance(TradeManager.class));
|
||||
persistedDataHosts.add(injector.getInstance(ClosedTradableManager.class));
|
||||
persistedDataHosts.add(injector.getInstance(FailedTradesManager.class));
|
||||
persistedDataHosts.add(injector.getInstance(DisputeManager.class));
|
||||
persistedDataHosts.add(injector.getInstance(ArbitrationDisputeListService.class));
|
||||
persistedDataHosts.add(injector.getInstance(MediationDisputeListService.class));
|
||||
persistedDataHosts.add(injector.getInstance(P2PService.class));
|
||||
|
||||
if (injector.getInstance(Key.get(Boolean.class, Names.named(DaoOptionKeys.DAO_ACTIVATED)))) {
|
||||
|
320
core/src/main/java/bisq/core/support/SupportManager.java
Normal file
320
core/src/main/java/bisq/core/support/SupportManager.java
Normal file
@ -0,0 +1,320 @@
|
||||
/*
|
||||
* This file is part of Bisq.
|
||||
*
|
||||
* Bisq is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or (at
|
||||
* your option) any later version.
|
||||
*
|
||||
* Bisq is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
|
||||
* License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package bisq.core.support;
|
||||
|
||||
import bisq.core.btc.setup.WalletsSetup;
|
||||
import bisq.core.support.messages.ChatMessage;
|
||||
import bisq.core.support.messages.SupportMessage;
|
||||
|
||||
import bisq.network.p2p.AckMessage;
|
||||
import bisq.network.p2p.AckMessageSourceType;
|
||||
import bisq.network.p2p.DecryptedMessageWithPubKey;
|
||||
import bisq.network.p2p.NodeAddress;
|
||||
import bisq.network.p2p.P2PService;
|
||||
import bisq.network.p2p.SendMailboxMessageListener;
|
||||
|
||||
import bisq.common.Timer;
|
||||
import bisq.common.UserThread;
|
||||
import bisq.common.crypto.PubKeyRing;
|
||||
import bisq.common.proto.network.NetworkEnvelope;
|
||||
|
||||
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 WalletsSetup walletsSetup;
|
||||
protected final Map<String, Timer> delayMsgMap = new HashMap<>();
|
||||
private final CopyOnWriteArraySet<DecryptedMessageWithPubKey> decryptedMailboxMessageWithPubKeys = new CopyOnWriteArraySet<>();
|
||||
private final CopyOnWriteArraySet<DecryptedMessageWithPubKey> decryptedDirectMessageWithPubKeys = new CopyOnWriteArraySet<>();
|
||||
private boolean allServicesInitialized;
|
||||
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
// Constructor
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
public SupportManager(P2PService p2PService, WalletsSetup walletsSetup) {
|
||||
this.p2PService = p2PService;
|
||||
this.walletsSetup = walletsSetup;
|
||||
|
||||
// We get first the message handler called then the onBootstrapped
|
||||
p2PService.addDecryptedDirectMessageListener((decryptedMessageWithPubKey, senderAddress) -> {
|
||||
decryptedDirectMessageWithPubKeys.add(decryptedMessageWithPubKey);
|
||||
tryApplyMessages();
|
||||
});
|
||||
p2PService.addDecryptedMailboxListener((decryptedMessageWithPubKey, senderAddress) -> {
|
||||
decryptedMailboxMessageWithPubKeys.add(decryptedMessageWithPubKey);
|
||||
tryApplyMessages();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
// Abstract methods
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
protected abstract void dispatchMessage(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 persist();
|
||||
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
// 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 onChatMessage(ChatMessage chatMessage) {
|
||||
final String tradeId = chatMessage.getTradeId();
|
||||
final String uid = chatMessage.getUid();
|
||||
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(() -> onChatMessage(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);
|
||||
|
||||
// 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,
|
||||
@Nullable DecryptedMessageWithPubKey decryptedMessageWithPubKey) {
|
||||
if (ackMessage.getSourceType() == getAckMessageSourceType()) {
|
||||
if (ackMessage.isSuccess()) {
|
||||
log.info("Received AckMessage for {} with tradeId {} and uid {}",
|
||||
ackMessage.getSourceMsgClassName(), ackMessage.getSourceId(), ackMessage.getSourceUid());
|
||||
} 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());
|
||||
});
|
||||
persist();
|
||||
|
||||
if (decryptedMessageWithPubKey != null)
|
||||
p2PService.removeEntryFromMailbox(decryptedMessageWithPubKey);
|
||||
}
|
||||
}
|
||||
|
||||
protected abstract AckMessageSourceType getAckMessageSourceType();
|
||||
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
// Send message
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
public ChatMessage sendChatMessage(ChatMessage message) {
|
||||
NodeAddress peersNodeAddress = getPeerNodeAddress(message);
|
||||
PubKeyRing receiverPubKeyRing = getPeerPubKeyRing(message);
|
||||
if (receiverPubKeyRing != null) {
|
||||
log.info("Send {} to peer {}. tradeId={}, uid={}",
|
||||
message.getClass().getSimpleName(), peersNodeAddress, message.getTradeId(), message.getUid());
|
||||
|
||||
p2PService.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);
|
||||
persist();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStoredInMailbox() {
|
||||
log.info("{} stored in mailbox for peer {}. tradeId={}, uid={}",
|
||||
message.getClass().getSimpleName(), peersNodeAddress, message.getTradeId(), message.getUid());
|
||||
message.setStoredInMailbox(true);
|
||||
persist();
|
||||
}
|
||||
|
||||
@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);
|
||||
persist();
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
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);
|
||||
p2PService.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 boolean isReady() {
|
||||
return allServicesInitialized &&
|
||||
p2PService.isBootstrapped() &&
|
||||
walletsSetup.isDownloadComplete() &&
|
||||
walletsSetup.hasSufficientPeersForBroadcast();
|
||||
}
|
||||
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
// Private
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
private void applyMessages() {
|
||||
decryptedDirectMessageWithPubKeys.forEach(decryptedMessageWithPubKey -> {
|
||||
NetworkEnvelope networkEnvelope = decryptedMessageWithPubKey.getNetworkEnvelope();
|
||||
if (networkEnvelope instanceof SupportMessage) {
|
||||
dispatchMessage((SupportMessage) networkEnvelope);
|
||||
} else if (networkEnvelope instanceof AckMessage) {
|
||||
onAckMessage((AckMessage) networkEnvelope, null);
|
||||
}
|
||||
});
|
||||
decryptedDirectMessageWithPubKeys.clear();
|
||||
|
||||
decryptedMailboxMessageWithPubKeys.forEach(decryptedMessageWithPubKey -> {
|
||||
NetworkEnvelope networkEnvelope = decryptedMessageWithPubKey.getNetworkEnvelope();
|
||||
log.debug("decryptedMessageWithPubKey.message " + networkEnvelope);
|
||||
if (networkEnvelope instanceof SupportMessage) {
|
||||
dispatchMessage((SupportMessage) networkEnvelope);
|
||||
p2PService.removeEntryFromMailbox(decryptedMessageWithPubKey);
|
||||
} else if (networkEnvelope instanceof AckMessage) {
|
||||
onAckMessage((AckMessage) networkEnvelope, decryptedMessageWithPubKey);
|
||||
}
|
||||
});
|
||||
decryptedMailboxMessageWithPubKeys.clear();
|
||||
}
|
||||
}
|
56
core/src/main/java/bisq/core/support/SupportSession.java
Normal file
56
core/src/main/java/bisq/core/support/SupportSession.java
Normal file
@ -0,0 +1,56 @@
|
||||
/*
|
||||
* This file is part of Bisq.
|
||||
*
|
||||
* Bisq is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or (at
|
||||
* your option) any later version.
|
||||
*
|
||||
* Bisq is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
|
||||
* License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package bisq.core.support;
|
||||
|
||||
import bisq.core.support.messages.ChatMessage;
|
||||
|
||||
import bisq.common.crypto.PubKeyRing;
|
||||
|
||||
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 PubKeyRing getClientPubKeyRing();
|
||||
|
||||
public abstract ObservableList<ChatMessage> getObservableChatMessageList();
|
||||
|
||||
public abstract boolean chatIsOpen();
|
||||
|
||||
public abstract boolean isDisputeAgent();
|
||||
}
|
@ -15,23 +15,21 @@
|
||||
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package bisq.core.arbitration;
|
||||
package bisq.core.support;
|
||||
|
||||
import bisq.common.app.AppModule;
|
||||
import bisq.common.proto.ProtoUtil;
|
||||
|
||||
import org.springframework.core.env.Environment;
|
||||
public enum SupportType {
|
||||
ARBITRATION, // Need to be at index 0 to be the fall back for old clients
|
||||
MEDIATION,
|
||||
TRADE;
|
||||
|
||||
import com.google.inject.Singleton;
|
||||
|
||||
public class ArbitratorModule extends AppModule {
|
||||
public ArbitratorModule(Environment environment) {
|
||||
super(environment);
|
||||
public static SupportType fromProto(
|
||||
protobuf.SupportType type) {
|
||||
return ProtoUtil.enumFromProto(SupportType.class, type.name());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected final void configure() {
|
||||
bind(ArbitratorManager.class).in(Singleton.class);
|
||||
bind(DisputeManager.class).in(Singleton.class);
|
||||
bind(ArbitratorService.class).in(Singleton.class);
|
||||
public static protobuf.SupportType toProtoMessage(SupportType supportType) {
|
||||
return protobuf.SupportType.valueOf(supportType.name());
|
||||
}
|
||||
}
|
@ -15,7 +15,7 @@
|
||||
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package bisq.core.arbitration;
|
||||
package bisq.core.support.dispute;
|
||||
|
||||
import bisq.common.proto.network.NetworkPayload;
|
||||
|
@ -15,10 +15,11 @@
|
||||
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package bisq.core.arbitration;
|
||||
package bisq.core.support.dispute;
|
||||
|
||||
import bisq.core.arbitration.messages.DisputeCommunicationMessage;
|
||||
import bisq.core.proto.CoreProtoResolver;
|
||||
import bisq.core.support.SupportType;
|
||||
import bisq.core.support.messages.ChatMessage;
|
||||
import bisq.core.trade.Contract;
|
||||
|
||||
import bisq.common.crypto.PubKeyRing;
|
||||
@ -39,7 +40,9 @@ import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.collections.ObservableList;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@ -77,25 +80,24 @@ public final class Dispute implements NetworkPayload {
|
||||
private final String makerContractSignature;
|
||||
@Nullable
|
||||
private final String takerContractSignature;
|
||||
private final PubKeyRing arbitratorPubKeyRing;
|
||||
private final PubKeyRing agentPubKeyRing; // arbitrator or mediator
|
||||
private final boolean isSupportTicket;
|
||||
private final ObservableList<DisputeCommunicationMessage> disputeCommunicationMessages = FXCollections.observableArrayList();
|
||||
private final ObservableList<ChatMessage> chatMessages = FXCollections.observableArrayList();
|
||||
private BooleanProperty isClosedProperty = new SimpleBooleanProperty();
|
||||
// disputeResultProperty.get is Nullable!
|
||||
private ObjectProperty<DisputeResult> disputeResultProperty = new SimpleObjectProperty<>();
|
||||
@Nullable
|
||||
private String disputePayoutTxId;
|
||||
|
||||
private long openingDate;
|
||||
|
||||
transient private Storage<DisputeList> storage;
|
||||
transient private Storage<? extends DisputeList> storage;
|
||||
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
// Constructor
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
public Dispute(Storage<DisputeList> storage,
|
||||
public Dispute(Storage<? extends DisputeList> storage,
|
||||
String tradeId,
|
||||
int traderId,
|
||||
boolean disputeOpenerIsBuyer,
|
||||
@ -111,7 +113,7 @@ public final class Dispute implements NetworkPayload {
|
||||
String contractAsJson,
|
||||
@Nullable String makerContractSignature,
|
||||
@Nullable String takerContractSignature,
|
||||
PubKeyRing arbitratorPubKeyRing,
|
||||
PubKeyRing agentPubKeyRing,
|
||||
boolean isSupportTicket) {
|
||||
this(tradeId,
|
||||
traderId,
|
||||
@ -128,7 +130,7 @@ public final class Dispute implements NetworkPayload {
|
||||
contractAsJson,
|
||||
makerContractSignature,
|
||||
takerContractSignature,
|
||||
arbitratorPubKeyRing,
|
||||
agentPubKeyRing,
|
||||
isSupportTicket);
|
||||
this.storage = storage;
|
||||
openingDate = new Date().getTime();
|
||||
@ -154,7 +156,7 @@ public final class Dispute implements NetworkPayload {
|
||||
String contractAsJson,
|
||||
@Nullable String makerContractSignature,
|
||||
@Nullable String takerContractSignature,
|
||||
PubKeyRing arbitratorPubKeyRing,
|
||||
PubKeyRing agentPubKeyRing,
|
||||
boolean isSupportTicket) {
|
||||
this.tradeId = tradeId;
|
||||
this.traderId = traderId;
|
||||
@ -171,7 +173,7 @@ public final class Dispute implements NetworkPayload {
|
||||
this.contractAsJson = contractAsJson;
|
||||
this.makerContractSignature = makerContractSignature;
|
||||
this.takerContractSignature = takerContractSignature;
|
||||
this.arbitratorPubKeyRing = arbitratorPubKeyRing;
|
||||
this.agentPubKeyRing = agentPubKeyRing;
|
||||
this.isSupportTicket = isSupportTicket;
|
||||
|
||||
id = tradeId + "_" + traderId;
|
||||
@ -179,6 +181,8 @@ public final class Dispute implements NetworkPayload {
|
||||
|
||||
@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)
|
||||
@ -188,10 +192,10 @@ public final class Dispute implements NetworkPayload {
|
||||
.setTradeDate(tradeDate)
|
||||
.setContract(contract.toProtoMessage())
|
||||
.setContractAsJson(contractAsJson)
|
||||
.setArbitratorPubKeyRing(arbitratorPubKeyRing.toProtoMessage())
|
||||
.setAgentPubKeyRing(agentPubKeyRing.toProtoMessage())
|
||||
.setIsSupportTicket(isSupportTicket)
|
||||
.addAllDisputeCommunicationMessages(disputeCommunicationMessages.stream()
|
||||
.map(msg -> msg.toProtoNetworkEnvelope().getDisputeCommunicationMessage())
|
||||
.addAllChatMessage(clonedChatMessages.stream()
|
||||
.map(msg -> msg.toProtoNetworkEnvelope().getChatMessage())
|
||||
.collect(Collectors.toList()))
|
||||
.setIsClosed(isClosedProperty.get())
|
||||
.setOpeningDate(openingDate)
|
||||
@ -225,11 +229,11 @@ public final class Dispute implements NetworkPayload {
|
||||
proto.getContractAsJson(),
|
||||
ProtoUtil.stringOrNullFromProto(proto.getMakerContractSignature()),
|
||||
ProtoUtil.stringOrNullFromProto(proto.getTakerContractSignature()),
|
||||
PubKeyRing.fromProto(proto.getArbitratorPubKeyRing()),
|
||||
PubKeyRing.fromProto(proto.getAgentPubKeyRing()),
|
||||
proto.getIsSupportTicket());
|
||||
|
||||
dispute.disputeCommunicationMessages.addAll(proto.getDisputeCommunicationMessagesList().stream()
|
||||
.map(DisputeCommunicationMessage::fromPayloadProto)
|
||||
dispute.chatMessages.addAll(proto.getChatMessageList().stream()
|
||||
.map(ChatMessage::fromPayloadProto)
|
||||
.collect(Collectors.toList()));
|
||||
|
||||
dispute.openingDate = proto.getOpeningDate();
|
||||
@ -245,22 +249,26 @@ public final class Dispute implements NetworkPayload {
|
||||
// API
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
public void addDisputeCommunicationMessage(DisputeCommunicationMessage disputeCommunicationMessage) {
|
||||
if (!disputeCommunicationMessages.contains(disputeCommunicationMessage)) {
|
||||
disputeCommunicationMessages.add(disputeCommunicationMessage);
|
||||
public void addAndPersistChatMessage(ChatMessage chatMessage) {
|
||||
if (!chatMessages.contains(chatMessage)) {
|
||||
chatMessages.add(chatMessage);
|
||||
storage.queueUpForSave();
|
||||
} else {
|
||||
log.error("disputeDirectMessage already exists");
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isMediationDispute() {
|
||||
return !chatMessages.isEmpty() && chatMessages.get(0).getSupportType() == SupportType.MEDIATION;
|
||||
}
|
||||
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
// Setters
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// In case we get the object via the network storage is not set as its transient, so we need to set it.
|
||||
public void setStorage(Storage<DisputeList> storage) {
|
||||
public void setStorage(Storage<? extends DisputeList> storage) {
|
||||
this.storage = storage;
|
||||
}
|
||||
|
||||
@ -278,7 +286,6 @@ public final class Dispute implements NetworkPayload {
|
||||
storage.queueUpForSave();
|
||||
}
|
||||
|
||||
@SuppressWarnings("NullableProblems")
|
||||
public void setDisputePayoutTxId(String disputePayoutTxId) {
|
||||
boolean changed = this.disputePayoutTxId == null || !this.disputePayoutTxId.equals(disputePayoutTxId);
|
||||
this.disputePayoutTxId = disputePayoutTxId;
|
||||
@ -335,9 +342,9 @@ public final class Dispute implements NetworkPayload {
|
||||
", contractAsJson='" + contractAsJson + '\'' +
|
||||
", makerContractSignature='" + makerContractSignature + '\'' +
|
||||
", takerContractSignature='" + takerContractSignature + '\'' +
|
||||
", arbitratorPubKeyRing=" + arbitratorPubKeyRing +
|
||||
", agentPubKeyRing=" + agentPubKeyRing +
|
||||
", isSupportTicket=" + isSupportTicket +
|
||||
", disputeCommunicationMessages=" + disputeCommunicationMessages +
|
||||
", chatMessages=" + chatMessages +
|
||||
", isClosed=" + isClosedProperty.get() +
|
||||
", disputeResult=" + disputeResultProperty.get() +
|
||||
", disputePayoutTxId='" + disputePayoutTxId + '\'' +
|
@ -15,7 +15,7 @@
|
||||
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package bisq.core.arbitration;
|
||||
package bisq.core.support.dispute;
|
||||
|
||||
public class DisputeAlreadyOpenException extends Exception {
|
||||
public DisputeAlreadyOpenException() {
|
@ -15,22 +15,16 @@
|
||||
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package bisq.core.arbitration;
|
||||
package bisq.core.support.dispute;
|
||||
|
||||
import bisq.core.proto.CoreProtoResolver;
|
||||
|
||||
import bisq.common.proto.ProtoUtil;
|
||||
import bisq.common.proto.persistable.PersistableEnvelope;
|
||||
import bisq.common.proto.persistable.PersistedDataHost;
|
||||
import bisq.common.storage.Storage;
|
||||
|
||||
import com.google.protobuf.Message;
|
||||
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.collections.ObservableList;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import lombok.Getter;
|
||||
@ -39,53 +33,32 @@ 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 final class DisputeList implements PersistableEnvelope, PersistedDataHost {
|
||||
transient private final Storage<DisputeList> storage;
|
||||
@Getter
|
||||
private final ObservableList<Dispute> list = FXCollections.observableArrayList();
|
||||
public abstract class DisputeList<T extends PersistableEnvelope> implements PersistableEnvelope, PersistedDataHost {
|
||||
transient protected final Storage<T> storage;
|
||||
|
||||
public DisputeList(Storage<DisputeList> storage) {
|
||||
@Getter
|
||||
protected final ObservableList<Dispute> list = FXCollections.observableArrayList();
|
||||
|
||||
public DisputeList(Storage<T> storage) {
|
||||
this.storage = storage;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void readPersisted() {
|
||||
DisputeList persisted = storage.initAndGetPersisted(this, 50);
|
||||
if (persisted != null)
|
||||
list.addAll(persisted.getList());
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
// PROTO BUFFER
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
private DisputeList(Storage<DisputeList> storage, List<Dispute> list) {
|
||||
protected DisputeList(Storage<T> storage, List<Dispute> list) {
|
||||
this.storage = storage;
|
||||
this.list.addAll(list);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Message toProtoMessage() {
|
||||
return protobuf.PersistableEnvelope.newBuilder().setDisputeList(protobuf.DisputeList.newBuilder()
|
||||
.addAllDispute(ProtoUtil.collectionToProto(list))).build();
|
||||
}
|
||||
|
||||
public static DisputeList fromProto(protobuf.DisputeList proto,
|
||||
CoreProtoResolver coreProtoResolver,
|
||||
Storage<DisputeList> storage) {
|
||||
List<Dispute> list = proto.getDisputeList().stream()
|
||||
.map(disputeProto -> Dispute.fromProto(disputeProto, coreProtoResolver))
|
||||
.collect(Collectors.toList());
|
||||
list.forEach(e -> e.setStorage(storage));
|
||||
return new DisputeList(storage, list);
|
||||
}
|
||||
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
// API
|
||||
@ -93,10 +66,9 @@ public final class DisputeList implements PersistableEnvelope, PersistedDataHost
|
||||
|
||||
public boolean add(Dispute dispute) {
|
||||
if (!list.contains(dispute)) {
|
||||
boolean changed = list.add(dispute);
|
||||
if (changed)
|
||||
persist();
|
||||
return changed;
|
||||
list.add(dispute);
|
||||
persist();
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
@ -122,7 +94,7 @@ public final class DisputeList implements PersistableEnvelope, PersistedDataHost
|
||||
return list.isEmpty();
|
||||
}
|
||||
|
||||
@SuppressWarnings({"BooleanMethodIsAlwaysInverted", "SuspiciousMethodCalls"})
|
||||
@SuppressWarnings({"SuspiciousMethodCalls"})
|
||||
public boolean contains(Object o) {
|
||||
return list.contains(o);
|
||||
}
|
@ -0,0 +1,190 @@
|
||||
/*
|
||||
* This file is part of Bisq.
|
||||
*
|
||||
* Bisq is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or (at
|
||||
* your option) any later version.
|
||||
*
|
||||
* Bisq is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
|
||||
* License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package bisq.core.support.dispute;
|
||||
|
||||
import bisq.core.trade.Contract;
|
||||
|
||||
import bisq.network.p2p.NodeAddress;
|
||||
|
||||
import bisq.common.UserThread;
|
||||
import bisq.common.proto.persistable.PersistedDataHost;
|
||||
import bisq.common.storage.Storage;
|
||||
|
||||
import org.fxmisc.easybind.EasyBind;
|
||||
import org.fxmisc.easybind.Subscription;
|
||||
|
||||
import javafx.beans.property.IntegerProperty;
|
||||
import javafx.beans.property.SimpleIntegerProperty;
|
||||
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.collections.ListChangeListener;
|
||||
import javafx.collections.ObservableList;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
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<? extends DisputeList>> implements PersistedDataHost {
|
||||
@Getter
|
||||
protected final Storage<T> storage;
|
||||
@Nullable
|
||||
@Getter
|
||||
private T disputeList;
|
||||
private final Map<String, Subscription> disputeIsClosedSubscriptionsMap = new HashMap<>();
|
||||
@Getter
|
||||
private final IntegerProperty numOpenDisputes = new SimpleIntegerProperty();
|
||||
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
// Constructor
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
public DisputeListService(Storage<T> storage) {
|
||||
this.storage = storage;
|
||||
}
|
||||
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
// Abstract methods
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
protected abstract T getConcreteDisputeList();
|
||||
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
// PersistedDataHost
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
@Override
|
||||
public void readPersisted() {
|
||||
disputeList = getConcreteDisputeList();
|
||||
disputeList.readPersisted();
|
||||
disputeList.stream().forEach(dispute -> dispute.setStorage(storage));
|
||||
}
|
||||
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
// Public
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
public void cleanupDisputes(@Nullable Consumer<String> closedDisputeHandler) {
|
||||
if (disputeList != null) {
|
||||
disputeList.stream().forEach(dispute -> {
|
||||
dispute.setStorage(storage);
|
||||
String tradeId = dispute.getTradeId();
|
||||
if (dispute.isClosed()) {
|
||||
if (closedDisputeHandler != null) {
|
||||
closedDisputeHandler.accept(tradeId);
|
||||
}
|
||||
}
|
||||
});
|
||||
} else {
|
||||
log.warn("disputes is null");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
// Package scope
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
void onAllServicesInitialized() {
|
||||
if (disputeList != null) {
|
||||
disputeList.getList().addListener((ListChangeListener<Dispute>) change -> {
|
||||
change.next();
|
||||
onDisputesChangeListener(change.getAddedSubList(), change.getRemoved());
|
||||
});
|
||||
onDisputesChangeListener(disputeList.getList(), null);
|
||||
} else {
|
||||
log.warn("disputes is null");
|
||||
}
|
||||
}
|
||||
|
||||
String getNrOfDisputes(boolean isBuyer, Contract contract) {
|
||||
return String.valueOf(getDisputesAsObservableList().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> getDisputesAsObservableList() {
|
||||
if (disputeList == null) {
|
||||
log.warn("disputes is null");
|
||||
return FXCollections.observableArrayList();
|
||||
}
|
||||
return disputeList.getList();
|
||||
}
|
||||
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
// Private
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
private void onDisputesChangeListener(List<? extends Dispute> addedList,
|
||||
@Nullable List<? extends Dispute> removedList) {
|
||||
if (removedList != null) {
|
||||
removedList.forEach(dispute -> {
|
||||
String id = dispute.getId();
|
||||
if (disputeIsClosedSubscriptionsMap.containsKey(id)) {
|
||||
disputeIsClosedSubscriptionsMap.get(id).unsubscribe();
|
||||
disputeIsClosedSubscriptionsMap.remove(id);
|
||||
}
|
||||
});
|
||||
}
|
||||
addedList.forEach(dispute -> {
|
||||
String id = dispute.getId();
|
||||
Subscription disputeStateSubscription = EasyBind.subscribe(dispute.isClosedProperty(),
|
||||
isClosed -> {
|
||||
if (disputeList != null) {
|
||||
// We get the event before the list gets updated, so we execute on next frame
|
||||
UserThread.execute(() -> {
|
||||
int openDisputes = (int) disputeList.getList().stream()
|
||||
.filter(e -> !e.isClosed()).count();
|
||||
numOpenDisputes.set(openDisputes);
|
||||
});
|
||||
}
|
||||
});
|
||||
disputeIsClosedSubscriptionsMap.put(id, disputeStateSubscription);
|
||||
});
|
||||
}
|
||||
|
||||
public void persist() {
|
||||
if (disputeList != null) {
|
||||
disputeList.persist();
|
||||
}
|
||||
}
|
||||
}
|
701
core/src/main/java/bisq/core/support/dispute/DisputeManager.java
Normal file
701
core/src/main/java/bisq/core/support/dispute/DisputeManager.java
Normal file
@ -0,0 +1,701 @@
|
||||
/*
|
||||
* This file is part of Bisq.
|
||||
*
|
||||
* Bisq is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or (at
|
||||
* your option) any later version.
|
||||
*
|
||||
* Bisq is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
|
||||
* License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package bisq.core.support.dispute;
|
||||
|
||||
import bisq.core.btc.setup.WalletsSetup;
|
||||
import bisq.core.btc.wallet.BtcWalletService;
|
||||
import bisq.core.btc.wallet.TradeWalletService;
|
||||
import bisq.core.locale.Res;
|
||||
import bisq.core.offer.OpenOfferManager;
|
||||
import bisq.core.support.SupportManager;
|
||||
import bisq.core.support.dispute.messages.DisputeResultMessage;
|
||||
import bisq.core.support.dispute.messages.OpenNewDisputeMessage;
|
||||
import bisq.core.support.dispute.messages.PeerOpenedDisputeMessage;
|
||||
import bisq.core.support.messages.ChatMessage;
|
||||
import bisq.core.trade.Contract;
|
||||
import bisq.core.trade.Trade;
|
||||
import bisq.core.trade.TradeManager;
|
||||
import bisq.core.trade.closed.ClosedTradableManager;
|
||||
|
||||
import bisq.network.p2p.BootstrapListener;
|
||||
import bisq.network.p2p.NodeAddress;
|
||||
import bisq.network.p2p.P2PService;
|
||||
import bisq.network.p2p.SendMailboxMessageListener;
|
||||
|
||||
import bisq.common.app.Version;
|
||||
import bisq.common.crypto.PubKeyRing;
|
||||
import bisq.common.handlers.FaultHandler;
|
||||
import bisq.common.handlers.ResultHandler;
|
||||
import bisq.common.storage.Storage;
|
||||
import bisq.common.util.Tuple2;
|
||||
|
||||
import javafx.beans.property.IntegerProperty;
|
||||
|
||||
import javafx.collections.ObservableList;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import static com.google.common.base.Preconditions.checkNotNull;
|
||||
|
||||
@Slf4j
|
||||
public abstract class DisputeManager<T extends DisputeList<? extends DisputeList>> extends SupportManager {
|
||||
protected final TradeWalletService tradeWalletService;
|
||||
protected final BtcWalletService walletService;
|
||||
protected final TradeManager tradeManager;
|
||||
protected final ClosedTradableManager closedTradableManager;
|
||||
protected final OpenOfferManager openOfferManager;
|
||||
protected final PubKeyRing pubKeyRing;
|
||||
protected final DisputeListService<T> disputeListService;
|
||||
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
// Constructor
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
public DisputeManager(P2PService p2PService,
|
||||
TradeWalletService tradeWalletService,
|
||||
BtcWalletService walletService,
|
||||
WalletsSetup walletsSetup,
|
||||
TradeManager tradeManager,
|
||||
ClosedTradableManager closedTradableManager,
|
||||
OpenOfferManager openOfferManager,
|
||||
PubKeyRing pubKeyRing,
|
||||
DisputeListService<T> disputeListService) {
|
||||
super(p2PService, walletsSetup);
|
||||
|
||||
this.tradeWalletService = tradeWalletService;
|
||||
this.walletService = walletService;
|
||||
this.tradeManager = tradeManager;
|
||||
this.closedTradableManager = closedTradableManager;
|
||||
this.openOfferManager = openOfferManager;
|
||||
this.pubKeyRing = pubKeyRing;
|
||||
this.disputeListService = disputeListService;
|
||||
}
|
||||
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
// Implement template methods
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
@Override
|
||||
public void persist() {
|
||||
disputeListService.persist();
|
||||
}
|
||||
|
||||
@Override
|
||||
public NodeAddress getPeerNodeAddress(ChatMessage message) {
|
||||
Optional<Dispute> disputeOptional = findDispute(message);
|
||||
if (!disputeOptional.isPresent()) {
|
||||
log.warn("Could not find dispute for tradeId = {} traderId = {}",
|
||||
message.getTradeId(), message.getTraderId());
|
||||
return null;
|
||||
}
|
||||
return getNodeAddressPubKeyRingTuple(disputeOptional.get()).first;
|
||||
}
|
||||
|
||||
@Override
|
||||
public PubKeyRing getPeerPubKeyRing(ChatMessage message) {
|
||||
Optional<Dispute> disputeOptional = findDispute(message);
|
||||
if (!disputeOptional.isPresent()) {
|
||||
log.warn("Could not find dispute for tradeId = {} traderId = {}",
|
||||
message.getTradeId(), message.getTraderId());
|
||||
return null;
|
||||
}
|
||||
|
||||
return getNodeAddressPubKeyRingTuple(disputeOptional.get()).second;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ChatMessage> getAllChatMessages() {
|
||||
return getDisputeList().stream()
|
||||
.flatMap(dispute -> dispute.getChatMessages().stream())
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean channelOpen(ChatMessage message) {
|
||||
return findDispute(message).isPresent();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addAndPersistChatMessage(ChatMessage message) {
|
||||
findDispute(message).ifPresent(dispute -> {
|
||||
if (dispute.getChatMessages().stream().noneMatch(m -> m.getUid().equals(message.getUid()))) {
|
||||
dispute.addAndPersistChatMessage(message);
|
||||
} else {
|
||||
log.warn("We got a chatMessage what we have already stored. UId = {} TradeId = {}",
|
||||
message.getUid(), message.getTradeId());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
// Abstract methods
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// We get that message at both peers. The dispute object is in context of the trader
|
||||
public abstract void onDisputeResultMessage(DisputeResultMessage disputeResultMessage);
|
||||
|
||||
public abstract NodeAddress getAgentNodeAddress(Dispute dispute);
|
||||
|
||||
protected abstract Trade.DisputeState getDisputeState_StartedByPeer();
|
||||
|
||||
public abstract void cleanupDisputes();
|
||||
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
// Delegates for disputeListService
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
public IntegerProperty getNumOpenDisputes() {
|
||||
return disputeListService.getNumOpenDisputes();
|
||||
}
|
||||
|
||||
public Storage<? extends DisputeList> getStorage() {
|
||||
return disputeListService.getStorage();
|
||||
}
|
||||
|
||||
public ObservableList<Dispute> getDisputesAsObservableList() {
|
||||
return disputeListService.getDisputesAsObservableList();
|
||||
}
|
||||
|
||||
public String getNrOfDisputes(boolean isBuyer, Contract contract) {
|
||||
return disputeListService.getNrOfDisputes(isBuyer, contract);
|
||||
}
|
||||
|
||||
private T getDisputeList() {
|
||||
return disputeListService.getDisputeList();
|
||||
}
|
||||
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
// API
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
public void onAllServicesInitialized() {
|
||||
super.onAllServicesInitialized();
|
||||
disputeListService.onAllServicesInitialized();
|
||||
|
||||
p2PService.addP2PServiceListener(new BootstrapListener() {
|
||||
@Override
|
||||
public void onUpdatedDataReceived() {
|
||||
tryApplyMessages();
|
||||
}
|
||||
});
|
||||
|
||||
walletsSetup.downloadPercentageProperty().addListener((observable, oldValue, newValue) -> {
|
||||
if (walletsSetup.isDownloadComplete())
|
||||
tryApplyMessages();
|
||||
});
|
||||
|
||||
walletsSetup.numPeersProperty().addListener((observable, oldValue, newValue) -> {
|
||||
if (walletsSetup.hasSufficientPeersForBroadcast())
|
||||
tryApplyMessages();
|
||||
});
|
||||
|
||||
tryApplyMessages();
|
||||
cleanupDisputes();
|
||||
}
|
||||
|
||||
public boolean isTrader(Dispute dispute) {
|
||||
return pubKeyRing.equals(dispute.getTraderPubKeyRing());
|
||||
}
|
||||
|
||||
|
||||
public Optional<Dispute> findOwnDispute(String tradeId) {
|
||||
T disputeList = getDisputeList();
|
||||
if (disputeList == null) {
|
||||
log.warn("disputes is null");
|
||||
return Optional.empty();
|
||||
}
|
||||
return disputeList.stream().filter(e -> e.getTradeId().equals(tradeId)).findAny();
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
// Message handler
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// arbitrator receives that from trader who opens dispute
|
||||
protected void onOpenNewDisputeMessage(OpenNewDisputeMessage openNewDisputeMessage) {
|
||||
T disputeList = getDisputeList();
|
||||
if (disputeList == null) {
|
||||
log.warn("disputes is null");
|
||||
return;
|
||||
}
|
||||
|
||||
String errorMessage = null;
|
||||
Dispute dispute = openNewDisputeMessage.getDispute();
|
||||
Contract contractFromOpener = dispute.getContract();
|
||||
PubKeyRing peersPubKeyRing = dispute.isDisputeOpenerIsBuyer() ? contractFromOpener.getSellerPubKeyRing() : contractFromOpener.getBuyerPubKeyRing();
|
||||
if (isAgent(dispute)) {
|
||||
if (!disputeList.contains(dispute)) {
|
||||
Optional<Dispute> storedDisputeOptional = findDispute(dispute);
|
||||
if (!storedDisputeOptional.isPresent()) {
|
||||
dispute.setStorage(disputeListService.getStorage());
|
||||
disputeList.add(dispute);
|
||||
errorMessage = sendPeerOpenedDisputeMessage(dispute, contractFromOpener, peersPubKeyRing);
|
||||
} else {
|
||||
// valid case if both have opened a dispute and agent was not online.
|
||||
log.debug("We got a dispute already open for that trade and trading peer. TradeId = {}",
|
||||
dispute.getTradeId());
|
||||
}
|
||||
} else {
|
||||
errorMessage = "We got a dispute msg what we have already stored. TradeId = " + dispute.getTradeId();
|
||||
log.warn(errorMessage);
|
||||
}
|
||||
} else {
|
||||
errorMessage = "Trader received openNewDisputeMessage. That must never happen.";
|
||||
log.error(errorMessage);
|
||||
}
|
||||
|
||||
// We use the ChatMessage not the openNewDisputeMessage for the ACK
|
||||
ObservableList<ChatMessage> messages = openNewDisputeMessage.getDispute().getChatMessages();
|
||||
if (!messages.isEmpty()) {
|
||||
ChatMessage msg = messages.get(0);
|
||||
PubKeyRing sendersPubKeyRing = dispute.isDisputeOpenerIsBuyer() ? contractFromOpener.getBuyerPubKeyRing() : contractFromOpener.getSellerPubKeyRing();
|
||||
sendAckMessage(msg, sendersPubKeyRing, errorMessage == null, errorMessage);
|
||||
}
|
||||
}
|
||||
|
||||
// not dispute requester receives that from arbitrator
|
||||
protected void onPeerOpenedDisputeMessage(PeerOpenedDisputeMessage peerOpenedDisputeMessage) {
|
||||
T disputeList = getDisputeList();
|
||||
if (disputeList == null) {
|
||||
log.warn("disputes is null");
|
||||
return;
|
||||
}
|
||||
|
||||
String errorMessage = null;
|
||||
Dispute dispute = peerOpenedDisputeMessage.getDispute();
|
||||
if (!isAgent(dispute)) {
|
||||
if (!disputeList.contains(dispute)) {
|
||||
Optional<Dispute> storedDisputeOptional = findDispute(dispute);
|
||||
if (!storedDisputeOptional.isPresent()) {
|
||||
dispute.setStorage(disputeListService.getStorage());
|
||||
disputeList.add(dispute);
|
||||
Optional<Trade> tradeOptional = tradeManager.getTradeById(dispute.getTradeId());
|
||||
tradeOptional.ifPresent(trade -> trade.setDisputeState(getDisputeState_StartedByPeer()));
|
||||
errorMessage = null;
|
||||
} else {
|
||||
// valid case if both have opened a dispute and agent was not online.
|
||||
log.debug("We got a dispute already open for that trade and trading peer. TradeId = {}",
|
||||
dispute.getTradeId());
|
||||
}
|
||||
} else {
|
||||
errorMessage = "We got a dispute msg what we have already stored. TradeId = " + dispute.getTradeId();
|
||||
log.warn(errorMessage);
|
||||
}
|
||||
} else {
|
||||
errorMessage = "Arbitrator received peerOpenedDisputeMessage. That must never happen.";
|
||||
log.error(errorMessage);
|
||||
}
|
||||
|
||||
// We use the ChatMessage not the peerOpenedDisputeMessage for the ACK
|
||||
ObservableList<ChatMessage> messages = peerOpenedDisputeMessage.getDispute().getChatMessages();
|
||||
if (!messages.isEmpty()) {
|
||||
ChatMessage msg = messages.get(0);
|
||||
sendAckMessage(msg, dispute.getAgentPubKeyRing(), errorMessage == null, errorMessage);
|
||||
}
|
||||
|
||||
sendAckMessage(peerOpenedDisputeMessage, dispute.getAgentPubKeyRing(), errorMessage == null, errorMessage);
|
||||
}
|
||||
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
// Send message
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
public void sendOpenNewDisputeMessage(Dispute dispute,
|
||||
boolean reOpen,
|
||||
ResultHandler resultHandler,
|
||||
FaultHandler faultHandler) {
|
||||
T disputeList = getDisputeList();
|
||||
if (disputeList == null) {
|
||||
log.warn("disputes is null");
|
||||
return;
|
||||
}
|
||||
|
||||
if (disputeList.contains(dispute)) {
|
||||
String msg = "We got a dispute msg what we have already stored. TradeId = " + dispute.getTradeId();
|
||||
log.warn(msg);
|
||||
faultHandler.handleFault(msg, new DisputeAlreadyOpenException());
|
||||
return;
|
||||
}
|
||||
|
||||
Optional<Dispute> storedDisputeOptional = findDispute(dispute);
|
||||
if (!storedDisputeOptional.isPresent() || reOpen) {
|
||||
String disputeInfo = getDisputeInfo(dispute.isMediationDispute());
|
||||
String sysMsg = dispute.isSupportTicket() ?
|
||||
Res.get("support.youOpenedTicket", disputeInfo, Version.VERSION)
|
||||
: Res.get("support.youOpenedDispute", disputeInfo, Version.VERSION);
|
||||
|
||||
ChatMessage chatMessage = new ChatMessage(
|
||||
getSupportType(),
|
||||
dispute.getTradeId(),
|
||||
pubKeyRing.hashCode(),
|
||||
false,
|
||||
Res.get("support.systemMsg", sysMsg),
|
||||
p2PService.getAddress());
|
||||
chatMessage.setSystemMessage(true);
|
||||
dispute.addAndPersistChatMessage(chatMessage);
|
||||
if (!reOpen) {
|
||||
disputeList.add(dispute);
|
||||
}
|
||||
|
||||
NodeAddress agentNodeAddress = getAgentNodeAddress(dispute);
|
||||
OpenNewDisputeMessage openNewDisputeMessage = new OpenNewDisputeMessage(dispute,
|
||||
p2PService.getAddress(),
|
||||
UUID.randomUUID().toString(),
|
||||
getSupportType());
|
||||
log.info("Send {} to peer {}. tradeId={}, openNewDisputeMessage.uid={}, " +
|
||||
"chatMessage.uid={}",
|
||||
openNewDisputeMessage.getClass().getSimpleName(), agentNodeAddress,
|
||||
openNewDisputeMessage.getTradeId(), openNewDisputeMessage.getUid(),
|
||||
chatMessage.getUid());
|
||||
p2PService.sendEncryptedMailboxMessage(agentNodeAddress,
|
||||
dispute.getAgentPubKeyRing(),
|
||||
openNewDisputeMessage,
|
||||
new SendMailboxMessageListener() {
|
||||
@Override
|
||||
public void onArrived() {
|
||||
log.info("{} arrived at peer {}. tradeId={}, openNewDisputeMessage.uid={}, " +
|
||||
"chatMessage.uid={}",
|
||||
openNewDisputeMessage.getClass().getSimpleName(), agentNodeAddress,
|
||||
openNewDisputeMessage.getTradeId(), openNewDisputeMessage.getUid(),
|
||||
chatMessage.getUid());
|
||||
|
||||
// We use the chatMessage wrapped inside the openNewDisputeMessage for
|
||||
// the state, as that is displayed to the user and we only persist that msg
|
||||
chatMessage.setArrived(true);
|
||||
disputeList.persist();
|
||||
resultHandler.handleResult();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStoredInMailbox() {
|
||||
log.info("{} stored in mailbox for peer {}. tradeId={}, openNewDisputeMessage.uid={}, " +
|
||||
"chatMessage.uid={}",
|
||||
openNewDisputeMessage.getClass().getSimpleName(), agentNodeAddress,
|
||||
openNewDisputeMessage.getTradeId(), openNewDisputeMessage.getUid(),
|
||||
chatMessage.getUid());
|
||||
|
||||
// We use the chatMessage wrapped inside the openNewDisputeMessage for
|
||||
// the state, as that is displayed to the user and we only persist that msg
|
||||
chatMessage.setStoredInMailbox(true);
|
||||
disputeList.persist();
|
||||
resultHandler.handleResult();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFault(String errorMessage) {
|
||||
log.error("{} failed: Peer {}. tradeId={}, openNewDisputeMessage.uid={}, " +
|
||||
"chatMessage.uid={}, errorMessage={}",
|
||||
openNewDisputeMessage.getClass().getSimpleName(), agentNodeAddress,
|
||||
openNewDisputeMessage.getTradeId(), openNewDisputeMessage.getUid(),
|
||||
chatMessage.getUid(), errorMessage);
|
||||
|
||||
// We use the chatMessage wrapped inside the openNewDisputeMessage for
|
||||
// the state, as that is displayed to the user and we only persist that msg
|
||||
chatMessage.setSendMessageError(errorMessage);
|
||||
disputeList.persist();
|
||||
faultHandler.handleFault("Sending dispute message failed: " +
|
||||
errorMessage, new DisputeMessageDeliveryFailedException());
|
||||
}
|
||||
}
|
||||
);
|
||||
} else {
|
||||
String msg = "We got a dispute already open for that trade and trading peer.\n" +
|
||||
"TradeId = " + dispute.getTradeId();
|
||||
log.warn(msg);
|
||||
faultHandler.handleFault(msg, new DisputeAlreadyOpenException());
|
||||
}
|
||||
}
|
||||
|
||||
// arbitrator sends that to trading peer when he received openDispute request
|
||||
private String sendPeerOpenedDisputeMessage(Dispute disputeFromOpener,
|
||||
Contract contractFromOpener,
|
||||
PubKeyRing pubKeyRing) {
|
||||
T disputeList = getDisputeList();
|
||||
if (disputeList == null) {
|
||||
log.warn("disputes is null");
|
||||
return null;
|
||||
}
|
||||
|
||||
Dispute dispute = new Dispute(disputeListService.getStorage(),
|
||||
disputeFromOpener.getTradeId(),
|
||||
pubKeyRing.hashCode(),
|
||||
!disputeFromOpener.isDisputeOpenerIsBuyer(),
|
||||
!disputeFromOpener.isDisputeOpenerIsMaker(),
|
||||
pubKeyRing,
|
||||
disputeFromOpener.getTradeDate().getTime(),
|
||||
contractFromOpener,
|
||||
disputeFromOpener.getContractHash(),
|
||||
disputeFromOpener.getDepositTxSerialized(),
|
||||
disputeFromOpener.getPayoutTxSerialized(),
|
||||
disputeFromOpener.getDepositTxId(),
|
||||
disputeFromOpener.getPayoutTxId(),
|
||||
disputeFromOpener.getContractAsJson(),
|
||||
disputeFromOpener.getMakerContractSignature(),
|
||||
disputeFromOpener.getTakerContractSignature(),
|
||||
disputeFromOpener.getAgentPubKeyRing(),
|
||||
disputeFromOpener.isSupportTicket());
|
||||
Optional<Dispute> storedDisputeOptional = findDispute(dispute);
|
||||
if (!storedDisputeOptional.isPresent()) {
|
||||
String disputeInfo = getDisputeInfo(dispute.isMediationDispute());
|
||||
String sysMsg = dispute.isSupportTicket() ?
|
||||
Res.get("support.peerOpenedTicket", disputeInfo)
|
||||
: Res.get("support.peerOpenedDispute", disputeInfo);
|
||||
ChatMessage chatMessage = new ChatMessage(
|
||||
getSupportType(),
|
||||
dispute.getTradeId(),
|
||||
pubKeyRing.hashCode(),
|
||||
false,
|
||||
Res.get("support.systemMsg", sysMsg),
|
||||
p2PService.getAddress());
|
||||
chatMessage.setSystemMessage(true);
|
||||
dispute.addAndPersistChatMessage(chatMessage);
|
||||
disputeList.add(dispute);
|
||||
|
||||
// we mirrored dispute already!
|
||||
Contract contract = dispute.getContract();
|
||||
PubKeyRing peersPubKeyRing = dispute.isDisputeOpenerIsBuyer() ? contract.getBuyerPubKeyRing() : contract.getSellerPubKeyRing();
|
||||
NodeAddress peersNodeAddress = dispute.isDisputeOpenerIsBuyer() ? contract.getBuyerNodeAddress() : contract.getSellerNodeAddress();
|
||||
PeerOpenedDisputeMessage peerOpenedDisputeMessage = new PeerOpenedDisputeMessage(dispute,
|
||||
p2PService.getAddress(),
|
||||
UUID.randomUUID().toString(),
|
||||
getSupportType());
|
||||
log.info("Send {} to peer {}. tradeId={}, peerOpenedDisputeMessage.uid={}, " +
|
||||
"chatMessage.uid={}",
|
||||
peerOpenedDisputeMessage.getClass().getSimpleName(), peersNodeAddress,
|
||||
peerOpenedDisputeMessage.getTradeId(), peerOpenedDisputeMessage.getUid(),
|
||||
chatMessage.getUid());
|
||||
p2PService.sendEncryptedMailboxMessage(peersNodeAddress,
|
||||
peersPubKeyRing,
|
||||
peerOpenedDisputeMessage,
|
||||
new SendMailboxMessageListener() {
|
||||
@Override
|
||||
public void onArrived() {
|
||||
log.info("{} arrived at peer {}. tradeId={}, peerOpenedDisputeMessage.uid={}, " +
|
||||
"chatMessage.uid={}",
|
||||
peerOpenedDisputeMessage.getClass().getSimpleName(), peersNodeAddress,
|
||||
peerOpenedDisputeMessage.getTradeId(), peerOpenedDisputeMessage.getUid(),
|
||||
chatMessage.getUid());
|
||||
|
||||
// We use the chatMessage wrapped inside the peerOpenedDisputeMessage for
|
||||
// the state, as that is displayed to the user and we only persist that msg
|
||||
chatMessage.setArrived(true);
|
||||
disputeList.persist();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStoredInMailbox() {
|
||||
log.info("{} stored in mailbox for peer {}. tradeId={}, peerOpenedDisputeMessage.uid={}, " +
|
||||
"chatMessage.uid={}",
|
||||
peerOpenedDisputeMessage.getClass().getSimpleName(), peersNodeAddress,
|
||||
peerOpenedDisputeMessage.getTradeId(), peerOpenedDisputeMessage.getUid(),
|
||||
chatMessage.getUid());
|
||||
|
||||
// We use the chatMessage wrapped inside the peerOpenedDisputeMessage for
|
||||
// the state, as that is displayed to the user and we only persist that msg
|
||||
chatMessage.setStoredInMailbox(true);
|
||||
disputeList.persist();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFault(String errorMessage) {
|
||||
log.error("{} failed: Peer {}. tradeId={}, peerOpenedDisputeMessage.uid={}, " +
|
||||
"chatMessage.uid={}, errorMessage={}",
|
||||
peerOpenedDisputeMessage.getClass().getSimpleName(), peersNodeAddress,
|
||||
peerOpenedDisputeMessage.getTradeId(), peerOpenedDisputeMessage.getUid(),
|
||||
chatMessage.getUid(), errorMessage);
|
||||
|
||||
// We use the chatMessage wrapped inside the peerOpenedDisputeMessage for
|
||||
// the state, as that is displayed to the user and we only persist that msg
|
||||
chatMessage.setSendMessageError(errorMessage);
|
||||
disputeList.persist();
|
||||
}
|
||||
}
|
||||
);
|
||||
return null;
|
||||
} else {
|
||||
// valid case if both have opened a dispute and agent was not online.
|
||||
log.debug("We got a dispute already open for that trade and trading peer. TradeId = {}",
|
||||
dispute.getTradeId());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// arbitrator send result to trader
|
||||
public void sendDisputeResultMessage(DisputeResult disputeResult, Dispute dispute, String text) {
|
||||
T disputeList = getDisputeList();
|
||||
if (disputeList == null) {
|
||||
log.warn("disputes is null");
|
||||
return;
|
||||
}
|
||||
|
||||
ChatMessage chatMessage = new ChatMessage(
|
||||
getSupportType(),
|
||||
dispute.getTradeId(),
|
||||
dispute.getTraderPubKeyRing().hashCode(),
|
||||
false,
|
||||
text,
|
||||
p2PService.getAddress());
|
||||
|
||||
dispute.addAndPersistChatMessage(chatMessage);
|
||||
disputeResult.setChatMessage(chatMessage);
|
||||
|
||||
NodeAddress peersNodeAddress;
|
||||
Contract contract = dispute.getContract();
|
||||
if (contract.getBuyerPubKeyRing().equals(dispute.getTraderPubKeyRing()))
|
||||
peersNodeAddress = contract.getBuyerNodeAddress();
|
||||
else
|
||||
peersNodeAddress = contract.getSellerNodeAddress();
|
||||
DisputeResultMessage disputeResultMessage = new DisputeResultMessage(disputeResult,
|
||||
p2PService.getAddress(),
|
||||
UUID.randomUUID().toString(),
|
||||
getSupportType());
|
||||
log.info("Send {} to peer {}. tradeId={}, disputeResultMessage.uid={}, chatMessage.uid={}",
|
||||
disputeResultMessage.getClass().getSimpleName(), peersNodeAddress, disputeResultMessage.getTradeId(),
|
||||
disputeResultMessage.getUid(), chatMessage.getUid());
|
||||
p2PService.sendEncryptedMailboxMessage(peersNodeAddress,
|
||||
dispute.getTraderPubKeyRing(),
|
||||
disputeResultMessage,
|
||||
new SendMailboxMessageListener() {
|
||||
@Override
|
||||
public void onArrived() {
|
||||
log.info("{} arrived at peer {}. tradeId={}, disputeResultMessage.uid={}, " +
|
||||
"chatMessage.uid={}",
|
||||
disputeResultMessage.getClass().getSimpleName(), peersNodeAddress,
|
||||
disputeResultMessage.getTradeId(), disputeResultMessage.getUid(),
|
||||
chatMessage.getUid());
|
||||
|
||||
// We use the chatMessage wrapped inside the disputeResultMessage for
|
||||
// the state, as that is displayed to the user and we only persist that msg
|
||||
chatMessage.setArrived(true);
|
||||
disputeList.persist();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStoredInMailbox() {
|
||||
log.info("{} stored in mailbox for peer {}. tradeId={}, disputeResultMessage.uid={}, " +
|
||||
"chatMessage.uid={}",
|
||||
disputeResultMessage.getClass().getSimpleName(), peersNodeAddress,
|
||||
disputeResultMessage.getTradeId(), disputeResultMessage.getUid(),
|
||||
chatMessage.getUid());
|
||||
|
||||
// We use the chatMessage wrapped inside the disputeResultMessage for
|
||||
// the state, as that is displayed to the user and we only persist that msg
|
||||
chatMessage.setStoredInMailbox(true);
|
||||
disputeList.persist();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFault(String errorMessage) {
|
||||
log.error("{} failed: Peer {}. tradeId={}, disputeResultMessage.uid={}, " +
|
||||
"chatMessage.uid={}, errorMessage={}",
|
||||
disputeResultMessage.getClass().getSimpleName(), peersNodeAddress,
|
||||
disputeResultMessage.getTradeId(), disputeResultMessage.getUid(),
|
||||
chatMessage.getUid(), errorMessage);
|
||||
|
||||
// We use the chatMessage wrapped inside the disputeResultMessage for
|
||||
// the state, as that is displayed to the user and we only persist that msg
|
||||
chatMessage.setSendMessageError(errorMessage);
|
||||
disputeList.persist();
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
// Utils
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
private Tuple2<NodeAddress, PubKeyRing> getNodeAddressPubKeyRingTuple(Dispute dispute) {
|
||||
PubKeyRing receiverPubKeyRing = null;
|
||||
NodeAddress peerNodeAddress = null;
|
||||
if (isTrader(dispute)) {
|
||||
receiverPubKeyRing = dispute.getAgentPubKeyRing();
|
||||
peerNodeAddress = getAgentNodeAddress(dispute);
|
||||
} else if (isAgent(dispute)) {
|
||||
receiverPubKeyRing = dispute.getTraderPubKeyRing();
|
||||
Contract contract = dispute.getContract();
|
||||
if (contract.getBuyerPubKeyRing().equals(receiverPubKeyRing))
|
||||
peerNodeAddress = contract.getBuyerNodeAddress();
|
||||
else
|
||||
peerNodeAddress = contract.getSellerNodeAddress();
|
||||
} else {
|
||||
log.error("That must not happen. Trader cannot communicate to other trader.");
|
||||
}
|
||||
return new Tuple2<>(peerNodeAddress, receiverPubKeyRing);
|
||||
}
|
||||
|
||||
private boolean isAgent(Dispute dispute) {
|
||||
return pubKeyRing.equals(dispute.getAgentPubKeyRing());
|
||||
}
|
||||
|
||||
private Optional<Dispute> findDispute(Dispute dispute) {
|
||||
return findDispute(dispute.getTradeId(), dispute.getTraderId());
|
||||
}
|
||||
|
||||
protected Optional<Dispute> findDispute(DisputeResult disputeResult) {
|
||||
ChatMessage chatMessage = disputeResult.getChatMessage();
|
||||
checkNotNull(chatMessage, "chatMessage must not be null");
|
||||
return findDispute(disputeResult.getTradeId(), disputeResult.getTraderId());
|
||||
}
|
||||
|
||||
private Optional<Dispute> findDispute(ChatMessage message) {
|
||||
return findDispute(message.getTradeId(), message.getTraderId());
|
||||
}
|
||||
|
||||
private Optional<Dispute> findDispute(String tradeId, int traderId) {
|
||||
T disputeList = getDisputeList();
|
||||
if (disputeList == null) {
|
||||
log.warn("disputes is null");
|
||||
return Optional.empty();
|
||||
}
|
||||
return disputeList.stream()
|
||||
.filter(e -> e.getTradeId().equals(tradeId) && e.getTraderId() == traderId)
|
||||
.findAny();
|
||||
}
|
||||
|
||||
public Optional<Dispute> findDispute(String tradeId) {
|
||||
T disputeList = getDisputeList();
|
||||
if (disputeList == null) {
|
||||
log.warn("disputes is null");
|
||||
return Optional.empty();
|
||||
}
|
||||
return disputeList.stream()
|
||||
.filter(e -> e.getTradeId().equals(tradeId))
|
||||
.findAny();
|
||||
}
|
||||
|
||||
private String getDisputeInfo(boolean isMediationDispute) {
|
||||
String role = isMediationDispute ? Res.get("shared.mediator").toLowerCase() :
|
||||
Res.get("shared.arbitrator2").toLowerCase();
|
||||
String link = isMediationDispute ? "https://docs.bisq.network/trading-rules.html#mediation" :
|
||||
"https://bisq.network/docs/exchange/arbitration-system";
|
||||
return Res.get("support.initialInfo", role, role, link);
|
||||
}
|
||||
}
|
@ -15,10 +15,10 @@
|
||||
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package bisq.core.arbitration;
|
||||
package bisq.core.support.dispute;
|
||||
|
||||
public class MessageDeliveryFailedException extends Exception {
|
||||
public MessageDeliveryFailedException() {
|
||||
public class DisputeMessageDeliveryFailedException extends Exception {
|
||||
public DisputeMessageDeliveryFailedException() {
|
||||
super();
|
||||
}
|
||||
}
|
@ -15,9 +15,9 @@
|
||||
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package bisq.core.arbitration;
|
||||
package bisq.core.support.dispute;
|
||||
|
||||
import bisq.core.arbitration.messages.DisputeCommunicationMessage;
|
||||
import bisq.core.support.messages.ChatMessage;
|
||||
|
||||
import bisq.common.proto.ProtoUtil;
|
||||
import bisq.common.proto.network.NetworkPayload;
|
||||
@ -74,7 +74,7 @@ public final class DisputeResult implements NetworkPayload {
|
||||
private final StringProperty summaryNotesProperty = new SimpleStringProperty("");
|
||||
@Setter
|
||||
@Nullable
|
||||
private DisputeCommunicationMessage disputeCommunicationMessage;
|
||||
private ChatMessage chatMessage;
|
||||
@Setter
|
||||
@Nullable
|
||||
private byte[] arbitratorSignature;
|
||||
@ -100,7 +100,7 @@ public final class DisputeResult implements NetworkPayload {
|
||||
boolean idVerification,
|
||||
boolean screenCast,
|
||||
String summaryNotes,
|
||||
@Nullable DisputeCommunicationMessage disputeCommunicationMessage,
|
||||
@Nullable ChatMessage chatMessage,
|
||||
@Nullable byte[] arbitratorSignature,
|
||||
long buyerPayoutAmount,
|
||||
long sellerPayoutAmount,
|
||||
@ -115,7 +115,7 @@ public final class DisputeResult implements NetworkPayload {
|
||||
this.idVerificationProperty.set(idVerification);
|
||||
this.screenCastProperty.set(screenCast);
|
||||
this.summaryNotesProperty.set(summaryNotes);
|
||||
this.disputeCommunicationMessage = disputeCommunicationMessage;
|
||||
this.chatMessage = chatMessage;
|
||||
this.arbitratorSignature = arbitratorSignature;
|
||||
this.buyerPayoutAmount = buyerPayoutAmount;
|
||||
this.sellerPayoutAmount = sellerPayoutAmount;
|
||||
@ -138,7 +138,7 @@ public final class DisputeResult implements NetworkPayload {
|
||||
proto.getIdVerification(),
|
||||
proto.getScreenCast(),
|
||||
proto.getSummaryNotes(),
|
||||
proto.getDisputeCommunicationMessage() == null ? null : DisputeCommunicationMessage.fromPayloadProto(proto.getDisputeCommunicationMessage()),
|
||||
proto.getChatMessage() == null ? null : ChatMessage.fromPayloadProto(proto.getChatMessage()),
|
||||
proto.getArbitratorSignature().toByteArray(),
|
||||
proto.getBuyerPayoutAmount(),
|
||||
proto.getSellerPayoutAmount(),
|
||||
@ -165,8 +165,8 @@ public final class DisputeResult implements NetworkPayload {
|
||||
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(disputeCommunicationMessage).ifPresent(disputeCommunicationMessage ->
|
||||
builder.setDisputeCommunicationMessage(disputeCommunicationMessage.toProtoNetworkEnvelope().getDisputeCommunicationMessage()));
|
||||
Optional.ofNullable(chatMessage).ifPresent(chatMessage ->
|
||||
builder.setChatMessage(chatMessage.toProtoNetworkEnvelope().getChatMessage()));
|
||||
|
||||
return builder.build();
|
||||
}
|
||||
@ -242,7 +242,7 @@ public final class DisputeResult implements NetworkPayload {
|
||||
",\n idVerificationProperty=" + idVerificationProperty +
|
||||
",\n screenCastProperty=" + screenCastProperty +
|
||||
",\n summaryNotesProperty=" + summaryNotesProperty +
|
||||
",\n disputeCommunicationMessage=" + disputeCommunicationMessage +
|
||||
",\n chatMessage=" + chatMessage +
|
||||
",\n arbitratorSignature=" + Utilities.bytesAsHexString(arbitratorSignature) +
|
||||
",\n buyerPayoutAmount=" + buyerPayoutAmount +
|
||||
",\n sellerPayoutAmount=" + sellerPayoutAmount +
|
@ -0,0 +1,79 @@
|
||||
/*
|
||||
* This file is part of Bisq.
|
||||
*
|
||||
* Bisq is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or (at
|
||||
* your option) any later version.
|
||||
*
|
||||
* Bisq is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
|
||||
* License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package bisq.core.support.dispute;
|
||||
|
||||
import bisq.core.support.SupportSession;
|
||||
import bisq.core.support.messages.ChatMessage;
|
||||
|
||||
import bisq.common.crypto.PubKeyRing;
|
||||
|
||||
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 PubKeyRing getClientPubKeyRing() {
|
||||
// Get pubKeyRing of trader. Arbitrator is considered server for the chat session
|
||||
return dispute != null ? dispute.getTraderPubKeyRing() : null;
|
||||
}
|
||||
|
||||
@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,113 @@
|
||||
/*
|
||||
* This file is part of Bisq.
|
||||
*
|
||||
* Bisq is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or (at
|
||||
* your option) any later version.
|
||||
*
|
||||
* Bisq is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
|
||||
* License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package bisq.core.support.dispute.agent;
|
||||
|
||||
import bisq.network.p2p.NodeAddress;
|
||||
import bisq.network.p2p.storage.payload.ExpirablePayload;
|
||||
import bisq.network.p2p.storage.payload.ProtectedStoragePayload;
|
||||
|
||||
import bisq.common.crypto.PubKeyRing;
|
||||
import bisq.common.util.ExtraDataMapValidator;
|
||||
import bisq.common.util.Utilities;
|
||||
|
||||
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,338 @@
|
||||
/*
|
||||
* This file is part of Bisq.
|
||||
*
|
||||
* Bisq is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or (at
|
||||
* your option) any later version.
|
||||
*
|
||||
* Bisq is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
|
||||
* License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package bisq.core.support.dispute.agent;
|
||||
|
||||
import bisq.core.filter.FilterManager;
|
||||
import bisq.core.user.User;
|
||||
|
||||
import bisq.network.p2p.BootstrapListener;
|
||||
import bisq.network.p2p.NodeAddress;
|
||||
import bisq.network.p2p.P2PService;
|
||||
import bisq.network.p2p.storage.HashMapChangedListener;
|
||||
import bisq.network.p2p.storage.payload.ProtectedStorageEntry;
|
||||
|
||||
import bisq.common.Timer;
|
||||
import bisq.common.UserThread;
|
||||
import bisq.common.app.DevEnv;
|
||||
import bisq.common.crypto.KeyRing;
|
||||
import bisq.common.handlers.ErrorMessageHandler;
|
||||
import bisq.common.handlers.ResultHandler;
|
||||
import bisq.common.util.Utilities;
|
||||
|
||||
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.Collections;
|
||||
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,
|
||||
boolean useDevPrivilegeKeys) {
|
||||
this.keyRing = keyRing;
|
||||
this.disputeAgentService = disputeAgentService;
|
||||
this.user = user;
|
||||
this.filterManager = filterManager;
|
||||
publicKeys = useDevPrivilegeKeys ? Collections.singletonList(DevEnv.DEV_PRIVILEGE_PUB_KEY) : 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(ProtectedStorageEntry data) {
|
||||
if (isExpectedInstance(data)) {
|
||||
updateMap();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRemoved(ProtectedStorageEntry data) {
|
||||
if (isExpectedInstance(data)) {
|
||||
updateMap();
|
||||
removeAcceptedDisputeAgentFromUser(data);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
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);
|
||||
|
||||
log.info("Available disputeAgents: {}", observableMap.keySet());
|
||||
}
|
||||
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
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,121 @@
|
||||
/*
|
||||
* This file is part of Bisq.
|
||||
*
|
||||
* Bisq is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or (at
|
||||
* your option) any later version.
|
||||
*
|
||||
* Bisq is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
|
||||
* License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package bisq.core.support.dispute.agent;
|
||||
|
||||
import bisq.core.app.BisqEnvironment;
|
||||
import bisq.core.filter.FilterManager;
|
||||
|
||||
import bisq.network.p2p.NodeAddress;
|
||||
import bisq.network.p2p.P2PService;
|
||||
import bisq.network.p2p.storage.HashMapChangedListener;
|
||||
|
||||
import bisq.common.app.DevEnv;
|
||||
import bisq.common.handlers.ErrorMessageHandler;
|
||||
import bisq.common.handlers.ResultHandler;
|
||||
import bisq.common.util.Utilities;
|
||||
|
||||
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 (!BisqEnvironment.getBaseCurrencyNetwork().isMainnet() ||
|
||||
!Utilities.encodeToHex(disputeAgent.getRegistrationPubKey()).equals(DevEnv.DEV_PRIVILEGE_PUB_KEY)) {
|
||||
boolean result = p2PService.addProtectedStorageEntry(disputeAgent, true);
|
||||
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, true)) {
|
||||
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)
|
||||
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))
|
||||
map.put(disputeAgentNodeAddress, disputeAgent);
|
||||
else
|
||||
log.warn("disputeAgentAddress already exist in disputeAgent map. Seems an disputeAgent object is already registered with the same address.");
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
protected abstract Set<T> getDisputeAgentSet(List<String> bannedDisputeAgents);
|
||||
|
||||
protected abstract List<String> getDisputeAgentsFromFilter();
|
||||
}
|
@ -0,0 +1,83 @@
|
||||
/*
|
||||
* This file is part of Bisq.
|
||||
*
|
||||
* Bisq is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or (at
|
||||
* your option) any later version.
|
||||
*
|
||||
* Bisq is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
|
||||
* License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package bisq.core.support.dispute.arbitration;
|
||||
|
||||
import bisq.core.proto.CoreProtoResolver;
|
||||
import bisq.core.support.dispute.Dispute;
|
||||
import bisq.core.support.dispute.DisputeList;
|
||||
|
||||
import bisq.common.proto.ProtoUtil;
|
||||
import bisq.common.storage.Storage;
|
||||
|
||||
import com.google.protobuf.Message;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import lombok.ToString;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
@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<ArbitrationDisputeList> {
|
||||
|
||||
ArbitrationDisputeList(Storage<ArbitrationDisputeList> storage) {
|
||||
super(storage);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void readPersisted() {
|
||||
// We need to use DisputeList as file name to not lose existing disputes which are stored in the DisputeList file
|
||||
ArbitrationDisputeList persisted = storage.initAndGetPersisted(this, "DisputeList", 50);
|
||||
if (persisted != null) {
|
||||
list.addAll(persisted.getList());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
// PROTO BUFFER
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
private ArbitrationDisputeList(Storage<ArbitrationDisputeList> storage, List<Dispute> list) {
|
||||
super(storage, list);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Message toProtoMessage() {
|
||||
return protobuf.PersistableEnvelope.newBuilder().setArbitrationDisputeList(protobuf.ArbitrationDisputeList.newBuilder()
|
||||
.addAllDispute(ProtoUtil.collectionToProto(new ArrayList<>(list)))).build();
|
||||
}
|
||||
|
||||
public static ArbitrationDisputeList fromProto(protobuf.ArbitrationDisputeList proto,
|
||||
CoreProtoResolver coreProtoResolver,
|
||||
Storage<ArbitrationDisputeList> storage) {
|
||||
List<Dispute> list = proto.getDisputeList().stream()
|
||||
.map(disputeProto -> Dispute.fromProto(disputeProto, coreProtoResolver))
|
||||
.collect(Collectors.toList());
|
||||
list.forEach(e -> e.setStorage(storage));
|
||||
return new ArbitrationDisputeList(storage, list);
|
||||
}
|
||||
}
|
@ -0,0 +1,48 @@
|
||||
/*
|
||||
* This file is part of Bisq.
|
||||
*
|
||||
* Bisq is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or (at
|
||||
* your option) any later version.
|
||||
*
|
||||
* Bisq is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
|
||||
* License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package bisq.core.support.dispute.arbitration;
|
||||
|
||||
import bisq.core.support.dispute.DisputeListService;
|
||||
|
||||
import bisq.common.storage.Storage;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Singleton;
|
||||
|
||||
@Singleton
|
||||
public final class ArbitrationDisputeListService extends DisputeListService<ArbitrationDisputeList> {
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
// Constructor
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
@Inject
|
||||
public ArbitrationDisputeListService(Storage<ArbitrationDisputeList> storage) {
|
||||
super(storage);
|
||||
}
|
||||
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
// Implement template methods
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
@Override
|
||||
protected ArbitrationDisputeList getConcreteDisputeList() {
|
||||
return new ArbitrationDisputeList(storage);
|
||||
}
|
||||
}
|
@ -0,0 +1,390 @@
|
||||
/*
|
||||
* This file is part of Bisq.
|
||||
*
|
||||
* Bisq is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or (at
|
||||
* your option) any later version.
|
||||
*
|
||||
* Bisq is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
|
||||
* License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package bisq.core.support.dispute.arbitration;
|
||||
|
||||
import bisq.core.btc.exceptions.TransactionVerificationException;
|
||||
import bisq.core.btc.exceptions.TxBroadcastException;
|
||||
import bisq.core.btc.exceptions.WalletException;
|
||||
import bisq.core.btc.setup.WalletsSetup;
|
||||
import bisq.core.btc.wallet.BtcWalletService;
|
||||
import bisq.core.btc.wallet.TradeWalletService;
|
||||
import bisq.core.btc.wallet.TxBroadcaster;
|
||||
import bisq.core.offer.OpenOffer;
|
||||
import bisq.core.offer.OpenOfferManager;
|
||||
import bisq.core.support.SupportType;
|
||||
import bisq.core.support.dispute.Dispute;
|
||||
import bisq.core.support.dispute.DisputeManager;
|
||||
import bisq.core.support.dispute.DisputeResult;
|
||||
import bisq.core.support.dispute.arbitration.messages.PeerPublishedDisputePayoutTxMessage;
|
||||
import bisq.core.support.dispute.messages.DisputeResultMessage;
|
||||
import bisq.core.support.dispute.messages.OpenNewDisputeMessage;
|
||||
import bisq.core.support.dispute.messages.PeerOpenedDisputeMessage;
|
||||
import bisq.core.support.messages.ChatMessage;
|
||||
import bisq.core.support.messages.SupportMessage;
|
||||
import bisq.core.trade.Contract;
|
||||
import bisq.core.trade.Tradable;
|
||||
import bisq.core.trade.Trade;
|
||||
import bisq.core.trade.TradeManager;
|
||||
import bisq.core.trade.closed.ClosedTradableManager;
|
||||
|
||||
import bisq.network.p2p.AckMessageSourceType;
|
||||
import bisq.network.p2p.NodeAddress;
|
||||
import bisq.network.p2p.P2PService;
|
||||
import bisq.network.p2p.SendMailboxMessageListener;
|
||||
|
||||
import bisq.common.Timer;
|
||||
import bisq.common.UserThread;
|
||||
import bisq.common.crypto.PubKeyRing;
|
||||
|
||||
import org.bitcoinj.core.AddressFormatException;
|
||||
import org.bitcoinj.core.Transaction;
|
||||
import org.bitcoinj.crypto.DeterministicKey;
|
||||
|
||||
import com.google.inject.Inject;
|
||||
import com.google.inject.Singleton;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import static com.google.common.base.Preconditions.checkNotNull;
|
||||
|
||||
@Slf4j
|
||||
@Singleton
|
||||
public final class ArbitrationManager extends DisputeManager<ArbitrationDisputeList> {
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
// Constructor
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
@Inject
|
||||
public ArbitrationManager(P2PService p2PService,
|
||||
TradeWalletService tradeWalletService,
|
||||
BtcWalletService walletService,
|
||||
WalletsSetup walletsSetup,
|
||||
TradeManager tradeManager,
|
||||
ClosedTradableManager closedTradableManager,
|
||||
OpenOfferManager openOfferManager,
|
||||
PubKeyRing pubKeyRing,
|
||||
ArbitrationDisputeListService arbitrationDisputeListService) {
|
||||
super(p2PService, tradeWalletService, walletService, walletsSetup, tradeManager, closedTradableManager,
|
||||
openOfferManager, pubKeyRing, arbitrationDisputeListService);
|
||||
}
|
||||
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
// Implement template methods
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
@Override
|
||||
public SupportType getSupportType() {
|
||||
return SupportType.ARBITRATION;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dispatchMessage(SupportMessage message) {
|
||||
if (canProcessMessage(message)) {
|
||||
log.info("Received {} with tradeId {} and uid {}",
|
||||
message.getClass().getSimpleName(), message.getTradeId(), message.getUid());
|
||||
|
||||
if (message instanceof OpenNewDisputeMessage) {
|
||||
onOpenNewDisputeMessage((OpenNewDisputeMessage) message);
|
||||
} else if (message instanceof PeerOpenedDisputeMessage) {
|
||||
onPeerOpenedDisputeMessage((PeerOpenedDisputeMessage) message);
|
||||
} else if (message instanceof ChatMessage) {
|
||||
onChatMessage((ChatMessage) message);
|
||||
} else if (message instanceof DisputeResultMessage) {
|
||||
onDisputeResultMessage((DisputeResultMessage) message);
|
||||
} else if (message instanceof PeerPublishedDisputePayoutTxMessage) {
|
||||
onDisputedPayoutTxMessage((PeerPublishedDisputePayoutTxMessage) message);
|
||||
} else {
|
||||
log.warn("Unsupported message at dispatchMessage. message={}", message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public NodeAddress getAgentNodeAddress(Dispute dispute) {
|
||||
return dispute.getContract().getArbitratorNodeAddress();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Trade.DisputeState getDisputeState_StartedByPeer() {
|
||||
return Trade.DisputeState.DISPUTE_STARTED_BY_PEER;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected AckMessageSourceType getAckMessageSourceType() {
|
||||
return AckMessageSourceType.ARBITRATION_MESSAGE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void cleanupDisputes() {
|
||||
disputeListService.cleanupDisputes(tradeId -> tradeManager.closeDisputedTrade(tradeId, Trade.DisputeState.DISPUTE_CLOSED));
|
||||
}
|
||||
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
// Message handler
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
@Override
|
||||
// We get that message at both peers. The dispute object is in context of the trader
|
||||
public void onDisputeResultMessage(DisputeResultMessage disputeResultMessage) {
|
||||
DisputeResult disputeResult = disputeResultMessage.getDisputeResult();
|
||||
ChatMessage chatMessage = disputeResult.getChatMessage();
|
||||
checkNotNull(chatMessage, "chatMessage must not be null");
|
||||
if (Arrays.equals(disputeResult.getArbitratorPubKey(),
|
||||
walletService.getArbitratorAddressEntry().getPubKey())) {
|
||||
log.error("Arbitrator received disputeResultMessage. That must never happen.");
|
||||
return;
|
||||
}
|
||||
|
||||
String tradeId = disputeResult.getTradeId();
|
||||
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(() -> onDisputeResultMessage(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(true);
|
||||
|
||||
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.getTradeById(tradeId);
|
||||
String errorMessage = null;
|
||||
boolean success = false;
|
||||
try {
|
||||
// We need to avoid publishing the tx from both traders as it would create problems with zero confirmation withdrawals
|
||||
// There would be different transactions if both sign and publish (signers: once buyer+arb, once seller+arb)
|
||||
// The tx publisher is the winner or in case both get 50% the buyer, as the buyer has more inventive to publish the tx as he receives
|
||||
// more BTC as he has deposited
|
||||
Contract contract = dispute.getContract();
|
||||
|
||||
boolean isBuyer = pubKeyRing.equals(contract.getBuyerPubKeyRing());
|
||||
DisputeResult.Winner publisher = disputeResult.getWinner();
|
||||
|
||||
// Sometimes the user who receives the trade amount is never online, so we might want to
|
||||
// let the loser publish the tx. When the winner comes online he gets his funds as it was published by the other peer.
|
||||
// Default isLoserPublisher is set to false
|
||||
if (disputeResult.isLoserPublisher()) {
|
||||
// we invert the logic
|
||||
if (publisher == DisputeResult.Winner.BUYER)
|
||||
publisher = DisputeResult.Winner.SELLER;
|
||||
else if (publisher == DisputeResult.Winner.SELLER)
|
||||
publisher = DisputeResult.Winner.BUYER;
|
||||
}
|
||||
|
||||
if ((isBuyer && publisher == DisputeResult.Winner.BUYER)
|
||||
|| (!isBuyer && publisher == DisputeResult.Winner.SELLER)) {
|
||||
|
||||
Transaction payoutTx = null;
|
||||
if (tradeOptional.isPresent()) {
|
||||
payoutTx = tradeOptional.get().getPayoutTx();
|
||||
} else {
|
||||
Optional<Tradable> tradableOptional = closedTradableManager.getTradableById(tradeId);
|
||||
if (tradableOptional.isPresent() && tradableOptional.get() instanceof Trade) {
|
||||
payoutTx = ((Trade) tradableOptional.get()).getPayoutTx();
|
||||
}
|
||||
}
|
||||
|
||||
if (payoutTx == null) {
|
||||
if (dispute.getDepositTxSerialized() != null) {
|
||||
byte[] multiSigPubKey = isBuyer ? contract.getBuyerMultiSigPubKey() : contract.getSellerMultiSigPubKey();
|
||||
DeterministicKey multiSigKeyPair = walletService.getMultiSigKeyPair(tradeId, multiSigPubKey);
|
||||
Transaction signedDisputedPayoutTx = tradeWalletService.traderSignAndFinalizeDisputedPayoutTx(
|
||||
dispute.getDepositTxSerialized(),
|
||||
disputeResult.getArbitratorSignature(),
|
||||
disputeResult.getBuyerPayoutAmount(),
|
||||
disputeResult.getSellerPayoutAmount(),
|
||||
contract.getBuyerPayoutAddressString(),
|
||||
contract.getSellerPayoutAddressString(),
|
||||
multiSigKeyPair,
|
||||
contract.getBuyerMultiSigPubKey(),
|
||||
contract.getSellerMultiSigPubKey(),
|
||||
disputeResult.getArbitratorPubKey()
|
||||
);
|
||||
Transaction committedDisputedPayoutTx = tradeWalletService.addTxToWallet(signedDisputedPayoutTx);
|
||||
tradeWalletService.broadcastTx(committedDisputedPayoutTx, new TxBroadcaster.Callback() {
|
||||
@Override
|
||||
public void onSuccess(Transaction transaction) {
|
||||
// after successful publish we send peer the tx
|
||||
dispute.setDisputePayoutTxId(transaction.getHashAsString());
|
||||
sendPeerPublishedPayoutTxMessage(transaction, dispute, contract);
|
||||
updateTradeOrOpenOfferManager(tradeId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(TxBroadcastException exception) {
|
||||
log.error(exception.getMessage());
|
||||
}
|
||||
}, 15);
|
||||
|
||||
success = true;
|
||||
} else {
|
||||
errorMessage = "DepositTx is null. TradeId = " + tradeId;
|
||||
log.warn(errorMessage);
|
||||
success = false;
|
||||
}
|
||||
} else {
|
||||
log.warn("We got already a payout tx. That might be the case if the other peer did not get the " +
|
||||
"payout tx and opened a dispute. TradeId = " + tradeId);
|
||||
dispute.setDisputePayoutTxId(payoutTx.getHashAsString());
|
||||
sendPeerPublishedPayoutTxMessage(payoutTx, dispute, contract);
|
||||
|
||||
success = true;
|
||||
}
|
||||
} else {
|
||||
log.trace("We don't publish the tx as we are not the winning party.");
|
||||
// Clean up tangling trades
|
||||
if (dispute.disputeResultProperty().get() != null && dispute.isClosed()) {
|
||||
updateTradeOrOpenOfferManager(tradeId);
|
||||
}
|
||||
|
||||
success = true;
|
||||
}
|
||||
} catch (TransactionVerificationException e) {
|
||||
errorMessage = "Error at traderSignAndFinalizeDisputedPayoutTx " + e.toString();
|
||||
log.error(errorMessage, e);
|
||||
success = false;
|
||||
|
||||
// We prefer to close the dispute in that case. If there was no deposit tx and a random tx was used
|
||||
// we get a TransactionVerificationException. No reason to keep that dispute open...
|
||||
updateTradeOrOpenOfferManager(tradeId);
|
||||
|
||||
throw new RuntimeException(errorMessage);
|
||||
} catch (AddressFormatException | WalletException e) {
|
||||
errorMessage = "Error at traderSignAndFinalizeDisputedPayoutTx " + e.toString();
|
||||
log.error(errorMessage, e);
|
||||
success = false;
|
||||
throw new RuntimeException(errorMessage);
|
||||
} finally {
|
||||
// We use the chatMessage as we only persist those not the disputeResultMessage.
|
||||
// If we would use the disputeResultMessage we could not lookup for the msg when we receive the AckMessage.
|
||||
sendAckMessage(chatMessage, dispute.getAgentPubKeyRing(), success, errorMessage);
|
||||
}
|
||||
}
|
||||
|
||||
// Losing trader or in case of 50/50 the seller gets the tx sent from the winner or buyer
|
||||
private void onDisputedPayoutTxMessage(PeerPublishedDisputePayoutTxMessage peerPublishedDisputePayoutTxMessage) {
|
||||
String uid = peerPublishedDisputePayoutTxMessage.getUid();
|
||||
String tradeId = peerPublishedDisputePayoutTxMessage.getTradeId();
|
||||
Optional<Dispute> disputeOptional = findOwnDispute(tradeId);
|
||||
if (!disputeOptional.isPresent()) {
|
||||
log.debug("We got a peerPublishedPayoutTxMessage but we don't have a matching dispute. TradeId = " + tradeId);
|
||||
if (!delayMsgMap.containsKey(uid)) {
|
||||
// We delay 3 sec. to be sure the close msg gets added first
|
||||
Timer timer = UserThread.runAfter(() -> onDisputedPayoutTxMessage(peerPublishedDisputePayoutTxMessage), 3);
|
||||
delayMsgMap.put(uid, timer);
|
||||
} else {
|
||||
log.warn("We got a peerPublishedPayoutTxMessage after we already repeated to apply the message after a delay. " +
|
||||
"That should never happen. TradeId = " + tradeId);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
Dispute dispute = disputeOptional.get();
|
||||
Contract contract = dispute.getContract();
|
||||
boolean isBuyer = pubKeyRing.equals(contract.getBuyerPubKeyRing());
|
||||
PubKeyRing peersPubKeyRing = isBuyer ? contract.getSellerPubKeyRing() : contract.getBuyerPubKeyRing();
|
||||
|
||||
cleanupRetryMap(uid);
|
||||
Transaction walletTx = tradeWalletService.addTxToWallet(peerPublishedDisputePayoutTxMessage.getTransaction());
|
||||
dispute.setDisputePayoutTxId(walletTx.getHashAsString());
|
||||
BtcWalletService.printTx("Disputed payoutTx received from peer", walletTx);
|
||||
|
||||
// We can only send the ack msg if we have the peersPubKeyRing which requires the dispute
|
||||
sendAckMessage(peerPublishedDisputePayoutTxMessage, peersPubKeyRing, true, null);
|
||||
}
|
||||
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
// Send messages
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// winner (or buyer in case of 50/50) sends tx to other peer
|
||||
private void sendPeerPublishedPayoutTxMessage(Transaction transaction, Dispute dispute, Contract contract) {
|
||||
PubKeyRing peersPubKeyRing = dispute.isDisputeOpenerIsBuyer() ? contract.getSellerPubKeyRing() : contract.getBuyerPubKeyRing();
|
||||
NodeAddress peersNodeAddress = dispute.isDisputeOpenerIsBuyer() ? contract.getSellerNodeAddress() : contract.getBuyerNodeAddress();
|
||||
log.trace("sendPeerPublishedPayoutTxMessage to peerAddress " + peersNodeAddress);
|
||||
PeerPublishedDisputePayoutTxMessage message = new PeerPublishedDisputePayoutTxMessage(transaction.bitcoinSerialize(),
|
||||
dispute.getTradeId(),
|
||||
p2PService.getAddress(),
|
||||
UUID.randomUUID().toString(),
|
||||
getSupportType());
|
||||
log.info("Send {} to peer {}. tradeId={}, uid={}",
|
||||
message.getClass().getSimpleName(), peersNodeAddress, message.getTradeId(), message.getUid());
|
||||
p2PService.sendEncryptedMailboxMessage(peersNodeAddress,
|
||||
peersPubKeyRing,
|
||||
message,
|
||||
new SendMailboxMessageListener() {
|
||||
@Override
|
||||
public void onArrived() {
|
||||
log.info("{} arrived at peer {}. tradeId={}, uid={}",
|
||||
message.getClass().getSimpleName(), peersNodeAddress, message.getTradeId(), message.getUid());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStoredInMailbox() {
|
||||
log.info("{} stored in mailbox for peer {}. tradeId={}, uid={}",
|
||||
message.getClass().getSimpleName(), peersNodeAddress, message.getTradeId(), message.getUid());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFault(String errorMessage) {
|
||||
log.error("{} failed: Peer {}. tradeId={}, uid={}, errorMessage={}",
|
||||
message.getClass().getSimpleName(), peersNodeAddress, message.getTradeId(), message.getUid(), errorMessage);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
private void updateTradeOrOpenOfferManager(String tradeId) {
|
||||
// set state after payout as we call swapTradeEntryToAvailableEntry
|
||||
if (tradeManager.getTradeById(tradeId).isPresent()) {
|
||||
tradeManager.closeDisputedTrade(tradeId, Trade.DisputeState.DISPUTE_CLOSED);
|
||||
} else {
|
||||
Optional<OpenOffer> openOfferOptional = openOfferManager.getOpenOfferById(tradeId);
|
||||
openOfferOptional.ifPresent(openOffer -> openOfferManager.closeOpenOffer(openOffer.getOffer()));
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,33 @@
|
||||
/*
|
||||
* This file is part of Bisq.
|
||||
*
|
||||
* Bisq is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or (at
|
||||
* your option) any later version.
|
||||
*
|
||||
* Bisq is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
|
||||
* License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package bisq.core.support.dispute.arbitration;
|
||||
|
||||
import bisq.core.support.dispute.Dispute;
|
||||
import bisq.core.support.dispute.DisputeSession;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
@Slf4j
|
||||
public class ArbitrationSession extends DisputeSession {
|
||||
|
||||
public ArbitrationSession(@Nullable Dispute dispute, boolean isTrader) {
|
||||
super(dispute, isTrader);
|
||||
}
|
||||
}
|
@ -15,7 +15,7 @@
|
||||
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package bisq.core.arbitration;
|
||||
package bisq.core.support.dispute.arbitration;
|
||||
|
||||
import bisq.core.account.witness.AccountAgeWitness;
|
||||
import bisq.core.payment.payload.PaymentAccountPayload;
|
||||
@ -27,6 +27,7 @@ import java.security.PublicKey;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.Getter;
|
||||
|
||||
// TODO consider to move to signed witness domain
|
||||
@Getter
|
||||
@EqualsAndHashCode(onlyExplicitlyIncluded = true)
|
||||
public class BuyerDataItem {
|
@ -15,28 +15,24 @@
|
||||
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package bisq.core.arbitration;
|
||||
package bisq.core.support.dispute.arbitration.arbitrator;
|
||||
|
||||
import bisq.core.support.dispute.agent.DisputeAgent;
|
||||
|
||||
import bisq.network.p2p.NodeAddress;
|
||||
import bisq.network.p2p.storage.payload.ExpirablePayload;
|
||||
import bisq.network.p2p.storage.payload.ProtectedStoragePayload;
|
||||
|
||||
import bisq.common.crypto.PubKeyRing;
|
||||
import bisq.common.proto.ProtoUtil;
|
||||
import bisq.common.util.ExtraDataMapValidator;
|
||||
import bisq.common.util.Utilities;
|
||||
|
||||
import com.google.protobuf.ByteString;
|
||||
|
||||
import org.springframework.util.CollectionUtils;
|
||||
|
||||
import java.security.PublicKey;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.Getter;
|
||||
@ -44,30 +40,12 @@ import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
@EqualsAndHashCode
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@Slf4j
|
||||
@Getter
|
||||
public final class Arbitrator implements ProtectedStoragePayload, ExpirablePayload {
|
||||
public static final long TTL = TimeUnit.DAYS.toMillis(10);
|
||||
|
||||
private final NodeAddress nodeAddress;
|
||||
public final class Arbitrator extends DisputeAgent {
|
||||
private final byte[] btcPubKey;
|
||||
private final String btcAddress;
|
||||
private final PubKeyRing pubKeyRing;
|
||||
private final List<String> languageCodes;
|
||||
private final long registrationDate;
|
||||
private final byte[] registrationPubKey;
|
||||
private final String registrationSignature;
|
||||
@Nullable
|
||||
private final String emailAddress;
|
||||
@Nullable
|
||||
private 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
|
||||
private Map<String, String> extraDataMap;
|
||||
|
||||
public Arbitrator(NodeAddress nodeAddress,
|
||||
byte[] btcPubKey,
|
||||
@ -80,17 +58,19 @@ public final class Arbitrator implements ProtectedStoragePayload, ExpirablePaylo
|
||||
@Nullable String emailAddress,
|
||||
@Nullable String info,
|
||||
@Nullable Map<String, String> extraDataMap) {
|
||||
this.nodeAddress = nodeAddress;
|
||||
|
||||
super(nodeAddress,
|
||||
pubKeyRing,
|
||||
languageCodes,
|
||||
registrationDate,
|
||||
registrationPubKey,
|
||||
registrationSignature,
|
||||
emailAddress,
|
||||
info,
|
||||
extraDataMap);
|
||||
|
||||
this.btcPubKey = btcPubKey;
|
||||
this.btcAddress = btcAddress;
|
||||
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);
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
@ -99,7 +79,7 @@ public final class Arbitrator implements ProtectedStoragePayload, ExpirablePaylo
|
||||
|
||||
@Override
|
||||
public protobuf.StoragePayload toProtoMessage() {
|
||||
final protobuf.Arbitrator.Builder builder = protobuf.Arbitrator.newBuilder()
|
||||
protobuf.Arbitrator.Builder builder = protobuf.Arbitrator.newBuilder()
|
||||
.setNodeAddress(nodeAddress.toProtoMessage())
|
||||
.setBtcPubKey(ByteString.copyFrom(btcPubKey))
|
||||
.setBtcAddress(btcAddress)
|
||||
@ -119,7 +99,7 @@ public final class Arbitrator implements ProtectedStoragePayload, ExpirablePaylo
|
||||
proto.getBtcPubKey().toByteArray(),
|
||||
proto.getBtcAddress(),
|
||||
PubKeyRing.fromProto(proto.getPubKeyRing()),
|
||||
proto.getLanguageCodesList().stream().collect(Collectors.toList()),
|
||||
new ArrayList<>(proto.getLanguageCodesList()),
|
||||
proto.getRegistrationDate(),
|
||||
proto.getRegistrationPubKey().toByteArray(),
|
||||
proto.getRegistrationSignature(),
|
||||
@ -133,31 +113,11 @@ public final class Arbitrator implements ProtectedStoragePayload, ExpirablePaylo
|
||||
// API
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
@Override
|
||||
public long getTTL() {
|
||||
return TTL;
|
||||
}
|
||||
|
||||
@Override
|
||||
public PublicKey getOwnerPubKey() {
|
||||
return pubKeyRing.getSignaturePubKey();
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "Arbitrator{" +
|
||||
"\n nodeAddress=" + nodeAddress +
|
||||
",\n btcPubKey=" + Utilities.bytesAsHexString(btcPubKey) +
|
||||
"\n btcPubKey=" + Utilities.bytesAsHexString(btcPubKey) +
|
||||
",\n btcAddress='" + btcAddress + '\'' +
|
||||
",\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}";
|
||||
"\n} " + super.toString();
|
||||
}
|
||||
}
|
@ -0,0 +1,103 @@
|
||||
/*
|
||||
* This file is part of Bisq.
|
||||
*
|
||||
* Bisq is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or (at
|
||||
* your option) any later version.
|
||||
*
|
||||
* Bisq is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
|
||||
* License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package bisq.core.support.dispute.arbitration.arbitrator;
|
||||
|
||||
import bisq.core.app.AppOptionKeys;
|
||||
import bisq.core.filter.FilterManager;
|
||||
import bisq.core.support.dispute.agent.DisputeAgentManager;
|
||||
import bisq.core.user.User;
|
||||
|
||||
import bisq.network.p2p.storage.payload.ProtectedStorageEntry;
|
||||
|
||||
import bisq.common.crypto.KeyRing;
|
||||
|
||||
import com.google.inject.Inject;
|
||||
import com.google.inject.Singleton;
|
||||
import com.google.inject.name.Named;
|
||||
|
||||
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,
|
||||
@Named(AppOptionKeys.USE_DEV_PRIVILEGE_KEYS) boolean useDevPrivilegeKeys) {
|
||||
super(keyRing, arbitratorService, user, filterManager, useDevPrivilegeKeys);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected List<String> getPubKeyList() {
|
||||
return List.of("0365c6af94681dbee69de1851f98d4684063bf5c2d64b1c73ed5d90434f375a054",
|
||||
"031c502a60f9dbdb5ae5e438a79819e4e1f417211dd537ac12c9bc23246534c4bd",
|
||||
"02c1e5a242387b6d5319ce27246cea6edaaf51c3550591b528d2578a4753c56c2c",
|
||||
"025c319faf7067d9299590dd6c97fe7e56cd4dac61205ccee1cd1fc390142390a2",
|
||||
"038f6e24c2bfe5d51d0a290f20a9a657c270b94ef2b9c12cd15ca3725fa798fc55",
|
||||
"0255256ff7fb615278c4544a9bbd3f5298b903b8a011cd7889be19b6b1c45cbefe",
|
||||
"024a3a37289f08c910fbd925ebc72b946f33feaeff451a4738ee82037b4cda2e95",
|
||||
"02a88b75e9f0f8afba1467ab26799dcc38fd7a6468fb2795444b425eb43e2c10bd",
|
||||
"02349a51512c1c04c67118386f4d27d768c5195a83247c150a4b722d161722ba81",
|
||||
"03f718a2e0dc672c7cdec0113e72c3322efc70412bb95870750d25c32cd98de17d",
|
||||
"028ff47ee2c56e66313928975c58fa4f1b19a0f81f3a96c4e9c9c3c6768075509e",
|
||||
"02b517c0cbc3a49548f448ddf004ed695c5a1c52ec110be1bfd65fa0ca0761c94b",
|
||||
"03df837a3a0f3d858e82f3356b71d1285327f101f7c10b404abed2abc1c94e7169",
|
||||
"0203a90fb2ab698e524a5286f317a183a84327b8f8c3f7fa4a98fec9e1cefd6b72",
|
||||
"023c99cc073b851c892d8c43329ca3beb5d2213ee87111af49884e3ce66cbd5ba5");
|
||||
}
|
||||
|
||||
@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,61 @@
|
||||
/*
|
||||
* This file is part of Bisq.
|
||||
*
|
||||
* Bisq is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or (at
|
||||
* your option) any later version.
|
||||
*
|
||||
* Bisq is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
|
||||
* License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package bisq.core.support.dispute.arbitration.arbitrator;
|
||||
|
||||
import bisq.core.filter.FilterManager;
|
||||
import bisq.core.support.dispute.agent.DisputeAgentService;
|
||||
|
||||
import bisq.network.p2p.NodeAddress;
|
||||
import bisq.network.p2p.P2PService;
|
||||
|
||||
import com.google.inject.Singleton;
|
||||
|
||||
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 Bisq.
|
||||
*
|
||||
* Bisq is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or (at
|
||||
* your option) any later version.
|
||||
*
|
||||
* Bisq is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
|
||||
* License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package bisq.core.support.dispute.arbitration.messages;
|
||||
|
||||
import bisq.core.support.SupportType;
|
||||
import bisq.core.support.dispute.messages.DisputeMessage;
|
||||
|
||||
abstract class ArbitrationMessage extends DisputeMessage {
|
||||
ArbitrationMessage(int messageVersion, String uid, SupportType supportType) {
|
||||
super(messageVersion, uid, supportType);
|
||||
}
|
||||
}
|
@ -15,7 +15,9 @@
|
||||
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package bisq.core.arbitration.messages;
|
||||
package bisq.core.support.dispute.arbitration.messages;
|
||||
|
||||
import bisq.core.support.SupportType;
|
||||
|
||||
import bisq.network.p2p.NodeAddress;
|
||||
|
||||
@ -29,7 +31,7 @@ import lombok.Value;
|
||||
|
||||
@Value
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
public final class PeerPublishedDisputePayoutTxMessage extends DisputeMessage {
|
||||
public final class PeerPublishedDisputePayoutTxMessage extends ArbitrationMessage {
|
||||
private final byte[] transaction;
|
||||
private final String tradeId;
|
||||
private final NodeAddress senderNodeAddress;
|
||||
@ -37,12 +39,14 @@ public final class PeerPublishedDisputePayoutTxMessage extends DisputeMessage {
|
||||
public PeerPublishedDisputePayoutTxMessage(byte[] transaction,
|
||||
String tradeId,
|
||||
NodeAddress senderNodeAddress,
|
||||
String uid) {
|
||||
String uid,
|
||||
SupportType supportType) {
|
||||
this(transaction,
|
||||
tradeId,
|
||||
senderNodeAddress,
|
||||
uid,
|
||||
Version.getP2PMessageVersion());
|
||||
Version.getP2PMessageVersion(),
|
||||
supportType);
|
||||
}
|
||||
|
||||
|
||||
@ -54,8 +58,9 @@ public final class PeerPublishedDisputePayoutTxMessage extends DisputeMessage {
|
||||
String tradeId,
|
||||
NodeAddress senderNodeAddress,
|
||||
String uid,
|
||||
int messageVersion) {
|
||||
super(messageVersion, uid);
|
||||
int messageVersion,
|
||||
SupportType supportType) {
|
||||
super(messageVersion, uid, supportType);
|
||||
this.transaction = transaction;
|
||||
this.tradeId = tradeId;
|
||||
this.senderNodeAddress = senderNodeAddress;
|
||||
@ -68,16 +73,19 @@ public final class PeerPublishedDisputePayoutTxMessage extends DisputeMessage {
|
||||
.setTransaction(ByteString.copyFrom(transaction))
|
||||
.setTradeId(tradeId)
|
||||
.setSenderNodeAddress(senderNodeAddress.toProtoMessage())
|
||||
.setUid(uid))
|
||||
.setUid(uid)
|
||||
.setType(SupportType.toProtoMessage(supportType)))
|
||||
.build();
|
||||
}
|
||||
|
||||
public static PeerPublishedDisputePayoutTxMessage fromProto(protobuf.PeerPublishedDisputePayoutTxMessage proto, int messageVersion) {
|
||||
public static PeerPublishedDisputePayoutTxMessage fromProto(protobuf.PeerPublishedDisputePayoutTxMessage proto,
|
||||
int messageVersion) {
|
||||
return new PeerPublishedDisputePayoutTxMessage(proto.getTransaction().toByteArray(),
|
||||
proto.getTradeId(),
|
||||
NodeAddress.fromProto(proto.getSenderNodeAddress()),
|
||||
proto.getUid(),
|
||||
messageVersion);
|
||||
messageVersion,
|
||||
SupportType.fromProto(proto.getType()));
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -93,6 +101,7 @@ public final class PeerPublishedDisputePayoutTxMessage extends DisputeMessage {
|
||||
",\n senderNodeAddress=" + senderNodeAddress +
|
||||
",\n PeerPublishedDisputePayoutTxMessage.uid='" + uid + '\'' +
|
||||
",\n messageVersion=" + messageVersion +
|
||||
",\n supportType=" + supportType +
|
||||
"\n} " + super.toString();
|
||||
}
|
||||
}
|
@ -0,0 +1,82 @@
|
||||
/*
|
||||
* This file is part of Bisq.
|
||||
*
|
||||
* Bisq is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or (at
|
||||
* your option) any later version.
|
||||
*
|
||||
* Bisq is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
|
||||
* License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package bisq.core.support.dispute.mediation;
|
||||
|
||||
import bisq.core.proto.CoreProtoResolver;
|
||||
import bisq.core.support.dispute.Dispute;
|
||||
import bisq.core.support.dispute.DisputeList;
|
||||
|
||||
import bisq.common.proto.ProtoUtil;
|
||||
import bisq.common.storage.Storage;
|
||||
|
||||
import com.google.protobuf.Message;
|
||||
|
||||
import java.util.ArrayList;
|
||||
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<MediationDisputeList> {
|
||||
|
||||
MediationDisputeList(Storage<MediationDisputeList> storage) {
|
||||
super(storage);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void readPersisted() {
|
||||
MediationDisputeList persisted = storage.initAndGetPersisted(this, "MediationDisputeList", 0);
|
||||
if (persisted != null) {
|
||||
list.addAll(persisted.getList());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
// PROTO BUFFER
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
private MediationDisputeList(Storage<MediationDisputeList> storage, List<Dispute> list) {
|
||||
super(storage, list);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Message toProtoMessage() {
|
||||
return protobuf.PersistableEnvelope.newBuilder().setMediationDisputeList(protobuf.MediationDisputeList.newBuilder()
|
||||
.addAllDispute(ProtoUtil.collectionToProto(new ArrayList<>(list)))).build();
|
||||
}
|
||||
|
||||
public static MediationDisputeList fromProto(protobuf.MediationDisputeList proto,
|
||||
CoreProtoResolver coreProtoResolver,
|
||||
Storage<MediationDisputeList> storage) {
|
||||
List<Dispute> list = proto.getDisputeList().stream()
|
||||
.map(disputeProto -> Dispute.fromProto(disputeProto, coreProtoResolver))
|
||||
.collect(Collectors.toList());
|
||||
list.forEach(e -> e.setStorage(storage));
|
||||
return new MediationDisputeList(storage, list);
|
||||
}
|
||||
}
|
@ -0,0 +1,48 @@
|
||||
/*
|
||||
* This file is part of Bisq.
|
||||
*
|
||||
* Bisq is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or (at
|
||||
* your option) any later version.
|
||||
*
|
||||
* Bisq is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
|
||||
* License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package bisq.core.support.dispute.mediation;
|
||||
|
||||
import bisq.core.support.dispute.DisputeListService;
|
||||
|
||||
import bisq.common.storage.Storage;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Singleton;
|
||||
|
||||
@Singleton
|
||||
public final class MediationDisputeListService extends DisputeListService<MediationDisputeList> {
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
// Constructor
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
@Inject
|
||||
public MediationDisputeListService(Storage<MediationDisputeList> storage) {
|
||||
super(storage);
|
||||
}
|
||||
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
// Implement template methods
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
@Override
|
||||
protected MediationDisputeList getConcreteDisputeList() {
|
||||
return new MediationDisputeList(storage);
|
||||
}
|
||||
}
|
@ -0,0 +1,251 @@
|
||||
/*
|
||||
* This file is part of Bisq.
|
||||
*
|
||||
* Bisq is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or (at
|
||||
* your option) any later version.
|
||||
*
|
||||
* Bisq is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
|
||||
* License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package bisq.core.support.dispute.mediation;
|
||||
|
||||
import bisq.core.btc.setup.WalletsSetup;
|
||||
import bisq.core.btc.wallet.BtcWalletService;
|
||||
import bisq.core.btc.wallet.TradeWalletService;
|
||||
import bisq.core.offer.OpenOffer;
|
||||
import bisq.core.offer.OpenOfferManager;
|
||||
import bisq.core.support.SupportType;
|
||||
import bisq.core.support.dispute.Dispute;
|
||||
import bisq.core.support.dispute.DisputeManager;
|
||||
import bisq.core.support.dispute.DisputeResult;
|
||||
import bisq.core.support.dispute.messages.DisputeResultMessage;
|
||||
import bisq.core.support.dispute.messages.OpenNewDisputeMessage;
|
||||
import bisq.core.support.dispute.messages.PeerOpenedDisputeMessage;
|
||||
import bisq.core.support.messages.ChatMessage;
|
||||
import bisq.core.support.messages.SupportMessage;
|
||||
import bisq.core.trade.Trade;
|
||||
import bisq.core.trade.TradeManager;
|
||||
import bisq.core.trade.closed.ClosedTradableManager;
|
||||
import bisq.core.trade.protocol.ProcessModel;
|
||||
import bisq.core.trade.protocol.TradeProtocol;
|
||||
|
||||
import bisq.network.p2p.AckMessageSourceType;
|
||||
import bisq.network.p2p.NodeAddress;
|
||||
import bisq.network.p2p.P2PService;
|
||||
|
||||
import bisq.common.Timer;
|
||||
import bisq.common.UserThread;
|
||||
import bisq.common.crypto.PubKeyRing;
|
||||
import bisq.common.handlers.ErrorMessageHandler;
|
||||
import bisq.common.handlers.ResultHandler;
|
||||
import bisq.common.util.Utilities;
|
||||
|
||||
import org.bitcoinj.core.Coin;
|
||||
|
||||
import com.google.inject.Inject;
|
||||
import com.google.inject.Singleton;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.GregorianCalendar;
|
||||
import java.util.Optional;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
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> {
|
||||
|
||||
// The date when mediation is activated
|
||||
private static final Date MEDIATION_ACTIVATED_DATE = Utilities.getUTCDate(2019, GregorianCalendar.SEPTEMBER, 1);
|
||||
|
||||
public static boolean isMediationActivated() {
|
||||
return new Date().after(MEDIATION_ACTIVATED_DATE);
|
||||
}
|
||||
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
// Constructor
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
@Inject
|
||||
public MediationManager(P2PService p2PService,
|
||||
TradeWalletService tradeWalletService,
|
||||
BtcWalletService walletService,
|
||||
WalletsSetup walletsSetup,
|
||||
TradeManager tradeManager,
|
||||
ClosedTradableManager closedTradableManager,
|
||||
OpenOfferManager openOfferManager,
|
||||
PubKeyRing pubKeyRing,
|
||||
MediationDisputeListService mediationDisputeListService) {
|
||||
super(p2PService, tradeWalletService, walletService, walletsSetup, tradeManager, closedTradableManager,
|
||||
openOfferManager, pubKeyRing, mediationDisputeListService);
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
// Implement template methods
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
@Override
|
||||
public SupportType getSupportType() {
|
||||
return SupportType.MEDIATION;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dispatchMessage(SupportMessage message) {
|
||||
if (canProcessMessage(message)) {
|
||||
log.info("Received {} with tradeId {} and uid {}",
|
||||
message.getClass().getSimpleName(), message.getTradeId(), message.getUid());
|
||||
|
||||
if (message instanceof OpenNewDisputeMessage) {
|
||||
onOpenNewDisputeMessage((OpenNewDisputeMessage) message);
|
||||
} else if (message instanceof PeerOpenedDisputeMessage) {
|
||||
onPeerOpenedDisputeMessage((PeerOpenedDisputeMessage) message);
|
||||
} else if (message instanceof ChatMessage) {
|
||||
onChatMessage((ChatMessage) message);
|
||||
} else if (message instanceof DisputeResultMessage) {
|
||||
onDisputeResultMessage((DisputeResultMessage) message);
|
||||
} else {
|
||||
log.warn("Unsupported message at dispatchMessage. message={}", message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Trade.DisputeState getDisputeState_StartedByPeer() {
|
||||
return Trade.DisputeState.MEDIATION_STARTED_BY_PEER;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected AckMessageSourceType getAckMessageSourceType() {
|
||||
return AckMessageSourceType.MEDIATION_MESSAGE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void cleanupDisputes() {
|
||||
disputeListService.cleanupDisputes(tradeId -> {
|
||||
tradeManager.getTradeById(tradeId).filter(trade -> trade.getPayoutTx() != null)
|
||||
.ifPresent(trade -> {
|
||||
tradeManager.closeDisputedTrade(tradeId, Trade.DisputeState.MEDIATION_CLOSED);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
// Message handler
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
@Override
|
||||
// We get that message at both peers. The dispute object is in context of the trader
|
||||
public void onDisputeResultMessage(DisputeResultMessage 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(() -> onDisputeResultMessage(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(true);
|
||||
|
||||
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.getTradeById(tradeId);
|
||||
if (tradeOptional.isPresent()) {
|
||||
Trade trade = tradeOptional.get();
|
||||
if (trade.getDisputeState() == Trade.DisputeState.MEDIATION_REQUESTED ||
|
||||
trade.getDisputeState() == Trade.DisputeState.MEDIATION_STARTED_BY_PEER) {
|
||||
trade.setDisputeState(Trade.DisputeState.MEDIATION_CLOSED);
|
||||
}
|
||||
} else {
|
||||
Optional<OpenOffer> openOfferOptional = openOfferManager.getOpenOfferById(tradeId);
|
||||
openOfferOptional.ifPresent(openOffer -> openOfferManager.closeOpenOffer(openOffer.getOffer()));
|
||||
}
|
||||
sendAckMessage(chatMessage, dispute.getAgentPubKeyRing(), true, null);
|
||||
}
|
||||
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
// API
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
@Override
|
||||
public NodeAddress getAgentNodeAddress(Dispute dispute) {
|
||||
return dispute.getContract().getMediatorNodeAddress();
|
||||
}
|
||||
|
||||
public void acceptMediationResult(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();
|
||||
Coin buyerPayoutAmount = disputeResult.getBuyerPayoutAmount();
|
||||
Coin sellerPayoutAmount = disputeResult.getSellerPayoutAmount();
|
||||
ProcessModel processModel = trade.getProcessModel();
|
||||
processModel.setBuyerPayoutAmountFromMediation(buyerPayoutAmount.value);
|
||||
processModel.setSellerPayoutAmountFromMediation(sellerPayoutAmount.value);
|
||||
TradeProtocol tradeProtocol = trade.getTradeProtocol();
|
||||
|
||||
trade.setMediationResultState(MediationResultState.MEDIATION_RESULT_ACCEPTED);
|
||||
|
||||
// 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 (processModel.getTradingPeer().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);
|
||||
}
|
||||
}
|
@ -0,0 +1,46 @@
|
||||
/*
|
||||
* This file is part of Bisq.
|
||||
*
|
||||
* Bisq is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or (at
|
||||
* your option) any later version.
|
||||
*
|
||||
* Bisq is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
|
||||
* License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package bisq.core.support.dispute.mediation;
|
||||
|
||||
import bisq.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,33 @@
|
||||
/*
|
||||
* This file is part of Bisq.
|
||||
*
|
||||
* Bisq is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or (at
|
||||
* your option) any later version.
|
||||
*
|
||||
* Bisq is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
|
||||
* License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package bisq.core.support.dispute.mediation;
|
||||
|
||||
import bisq.core.support.dispute.Dispute;
|
||||
import bisq.core.support.dispute.DisputeSession;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
@Slf4j
|
||||
public class MediationSession extends DisputeSession {
|
||||
|
||||
public MediationSession(@Nullable Dispute dispute, boolean isTrader) {
|
||||
super(dispute, isTrader);
|
||||
}
|
||||
}
|
@ -15,57 +15,32 @@
|
||||
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package bisq.core.arbitration;
|
||||
package bisq.core.support.dispute.mediation.mediator;
|
||||
|
||||
import bisq.core.support.dispute.agent.DisputeAgent;
|
||||
|
||||
import bisq.network.p2p.NodeAddress;
|
||||
import bisq.network.p2p.storage.payload.ExpirablePayload;
|
||||
import bisq.network.p2p.storage.payload.ProtectedStoragePayload;
|
||||
|
||||
import bisq.common.crypto.PubKeyRing;
|
||||
import bisq.common.proto.ProtoUtil;
|
||||
import bisq.common.util.ExtraDataMapValidator;
|
||||
|
||||
import com.google.protobuf.ByteString;
|
||||
|
||||
import org.springframework.util.CollectionUtils;
|
||||
|
||||
import java.security.PublicKey;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.Getter;
|
||||
import lombok.ToString;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
@EqualsAndHashCode
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@Slf4j
|
||||
@ToString
|
||||
@Getter
|
||||
public final class Mediator implements ProtectedStoragePayload, ExpirablePayload {
|
||||
private final PubKeyRing pubKeyRing;
|
||||
private final NodeAddress nodeAddress;
|
||||
private final List<String> languageCodes;
|
||||
private final long registrationDate;
|
||||
private final String registrationSignature;
|
||||
private final byte[] registrationPubKey;
|
||||
@Nullable
|
||||
private final String emailAddress;
|
||||
@Nullable
|
||||
private 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
|
||||
private Map<String, String> extraDataMap;
|
||||
|
||||
public final class Mediator extends DisputeAgent {
|
||||
public Mediator(NodeAddress nodeAddress,
|
||||
PubKeyRing pubKeyRing,
|
||||
List<String> languageCodes,
|
||||
@ -75,17 +50,17 @@ public final class Mediator implements ProtectedStoragePayload, ExpirablePayload
|
||||
@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);
|
||||
}
|
||||
|
||||
super(nodeAddress,
|
||||
pubKeyRing,
|
||||
languageCodes,
|
||||
registrationDate,
|
||||
registrationPubKey,
|
||||
registrationSignature,
|
||||
emailAddress,
|
||||
info,
|
||||
extraDataMap);
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
// PROTO BUFFER
|
||||
@ -109,7 +84,7 @@ public final class Mediator implements ProtectedStoragePayload, ExpirablePayload
|
||||
public static Mediator fromProto(protobuf.Mediator proto) {
|
||||
return new Mediator(NodeAddress.fromProto(proto.getNodeAddress()),
|
||||
PubKeyRing.fromProto(proto.getPubKeyRing()),
|
||||
proto.getLanguageCodesList().stream().collect(Collectors.toList()),
|
||||
new ArrayList<>(proto.getLanguageCodesList()),
|
||||
proto.getRegistrationDate(),
|
||||
proto.getRegistrationPubKey().toByteArray(),
|
||||
proto.getRegistrationSignature(),
|
||||
@ -118,17 +93,14 @@ public final class Mediator implements ProtectedStoragePayload, ExpirablePayload
|
||||
CollectionUtils.isEmpty(proto.getExtraDataMap()) ? null : proto.getExtraDataMap());
|
||||
}
|
||||
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
// API
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
@Override
|
||||
public long getTTL() {
|
||||
return TimeUnit.DAYS.toMillis(10);
|
||||
}
|
||||
|
||||
@Override
|
||||
public PublicKey getOwnerPubKey() {
|
||||
return pubKeyRing.getSignaturePubKey();
|
||||
public String toString() {
|
||||
return "Mediator{} " + super.toString();
|
||||
}
|
||||
}
|
@ -0,0 +1,101 @@
|
||||
/*
|
||||
* This file is part of Bisq.
|
||||
*
|
||||
* Bisq is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or (at
|
||||
* your option) any later version.
|
||||
*
|
||||
* Bisq is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
|
||||
* License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package bisq.core.support.dispute.mediation.mediator;
|
||||
|
||||
import bisq.core.app.AppOptionKeys;
|
||||
import bisq.core.filter.FilterManager;
|
||||
import bisq.core.support.dispute.agent.DisputeAgentManager;
|
||||
import bisq.core.user.User;
|
||||
|
||||
import bisq.network.p2p.storage.payload.ProtectedStorageEntry;
|
||||
|
||||
import bisq.common.crypto.KeyRing;
|
||||
|
||||
import com.google.inject.Singleton;
|
||||
import com.google.inject.name.Named;
|
||||
|
||||
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,
|
||||
@Named(AppOptionKeys.USE_DEV_PRIVILEGE_KEYS) boolean useDevPrivilegeKeys) {
|
||||
super(keyRing, mediatorService, user, filterManager, useDevPrivilegeKeys);
|
||||
}
|
||||
|
||||
@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,66 @@
|
||||
/*
|
||||
* This file is part of Bisq.
|
||||
*
|
||||
* Bisq is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or (at
|
||||
* your option) any later version.
|
||||
*
|
||||
* Bisq is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
|
||||
* License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package bisq.core.support.dispute.mediation.mediator;
|
||||
|
||||
import bisq.core.filter.FilterManager;
|
||||
import bisq.core.support.dispute.agent.DisputeAgentService;
|
||||
|
||||
import bisq.network.p2p.NodeAddress;
|
||||
import bisq.network.p2p.P2PService;
|
||||
|
||||
import com.google.inject.Singleton;
|
||||
|
||||
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,28 @@
|
||||
/*
|
||||
* This file is part of Bisq.
|
||||
*
|
||||
* Bisq is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or (at
|
||||
* your option) any later version.
|
||||
*
|
||||
* Bisq is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
|
||||
* License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package bisq.core.support.dispute.messages;
|
||||
|
||||
import bisq.core.support.SupportType;
|
||||
import bisq.core.support.messages.SupportMessage;
|
||||
|
||||
public abstract class DisputeMessage extends SupportMessage {
|
||||
|
||||
public DisputeMessage(int messageVersion, String uid, SupportType supportType) {
|
||||
super(messageVersion, uid, supportType);
|
||||
}
|
||||
}
|
@ -15,9 +15,10 @@
|
||||
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package bisq.core.arbitration.messages;
|
||||
package bisq.core.support.dispute.messages;
|
||||
|
||||
import bisq.core.arbitration.DisputeResult;
|
||||
import bisq.core.support.SupportType;
|
||||
import bisq.core.support.dispute.DisputeResult;
|
||||
|
||||
import bisq.network.p2p.NodeAddress;
|
||||
|
||||
@ -36,11 +37,13 @@ public final class DisputeResultMessage extends DisputeMessage {
|
||||
|
||||
public DisputeResultMessage(DisputeResult disputeResult,
|
||||
NodeAddress senderNodeAddress,
|
||||
String uid) {
|
||||
String uid,
|
||||
SupportType supportType) {
|
||||
this(disputeResult,
|
||||
senderNodeAddress,
|
||||
uid,
|
||||
Version.getP2PMessageVersion());
|
||||
Version.getP2PMessageVersion(),
|
||||
supportType);
|
||||
}
|
||||
|
||||
|
||||
@ -51,8 +54,9 @@ public final class DisputeResultMessage extends DisputeMessage {
|
||||
private DisputeResultMessage(DisputeResult disputeResult,
|
||||
NodeAddress senderNodeAddress,
|
||||
String uid,
|
||||
int messageVersion) {
|
||||
super(messageVersion, uid);
|
||||
int messageVersion,
|
||||
SupportType supportType) {
|
||||
super(messageVersion, uid, supportType);
|
||||
this.disputeResult = disputeResult;
|
||||
this.senderNodeAddress = senderNodeAddress;
|
||||
}
|
||||
@ -63,7 +67,8 @@ public final class DisputeResultMessage extends DisputeMessage {
|
||||
.setDisputeResultMessage(protobuf.DisputeResultMessage.newBuilder()
|
||||
.setDisputeResult(disputeResult.toProtoMessage())
|
||||
.setSenderNodeAddress(senderNodeAddress.toProtoMessage())
|
||||
.setUid(uid))
|
||||
.setUid(uid)
|
||||
.setType(SupportType.toProtoMessage(supportType)))
|
||||
.build();
|
||||
}
|
||||
|
||||
@ -72,7 +77,8 @@ public final class DisputeResultMessage extends DisputeMessage {
|
||||
return new DisputeResultMessage(DisputeResult.fromProto(proto.getDisputeResult()),
|
||||
NodeAddress.fromProto(proto.getSenderNodeAddress()),
|
||||
proto.getUid(),
|
||||
messageVersion);
|
||||
messageVersion,
|
||||
SupportType.fromProto(proto.getType()));
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -87,6 +93,7 @@ public final class DisputeResultMessage extends DisputeMessage {
|
||||
",\n senderNodeAddress=" + senderNodeAddress +
|
||||
",\n DisputeResultMessage.uid='" + uid + '\'' +
|
||||
",\n messageVersion=" + messageVersion +
|
||||
",\n supportType=" + supportType +
|
||||
"\n} " + super.toString();
|
||||
}
|
||||
}
|
@ -15,10 +15,11 @@
|
||||
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package bisq.core.arbitration.messages;
|
||||
package bisq.core.support.dispute.messages;
|
||||
|
||||
import bisq.core.arbitration.Dispute;
|
||||
import bisq.core.proto.CoreProtoResolver;
|
||||
import bisq.core.support.SupportType;
|
||||
import bisq.core.support.dispute.Dispute;
|
||||
|
||||
import bisq.network.p2p.NodeAddress;
|
||||
|
||||
@ -35,11 +36,13 @@ public final class OpenNewDisputeMessage extends DisputeMessage {
|
||||
|
||||
public OpenNewDisputeMessage(Dispute dispute,
|
||||
NodeAddress senderNodeAddress,
|
||||
String uid) {
|
||||
String uid,
|
||||
SupportType supportType) {
|
||||
this(dispute,
|
||||
senderNodeAddress,
|
||||
uid,
|
||||
Version.getP2PMessageVersion());
|
||||
Version.getP2PMessageVersion(),
|
||||
supportType);
|
||||
}
|
||||
|
||||
|
||||
@ -50,8 +53,9 @@ public final class OpenNewDisputeMessage extends DisputeMessage {
|
||||
private OpenNewDisputeMessage(Dispute dispute,
|
||||
NodeAddress senderNodeAddress,
|
||||
String uid,
|
||||
int messageVersion) {
|
||||
super(messageVersion, uid);
|
||||
int messageVersion,
|
||||
SupportType supportType) {
|
||||
super(messageVersion, uid, supportType);
|
||||
this.dispute = dispute;
|
||||
this.senderNodeAddress = senderNodeAddress;
|
||||
}
|
||||
@ -62,7 +66,8 @@ public final class OpenNewDisputeMessage extends DisputeMessage {
|
||||
.setOpenNewDisputeMessage(protobuf.OpenNewDisputeMessage.newBuilder()
|
||||
.setUid(uid)
|
||||
.setDispute(dispute.toProtoMessage())
|
||||
.setSenderNodeAddress(senderNodeAddress.toProtoMessage()))
|
||||
.setSenderNodeAddress(senderNodeAddress.toProtoMessage())
|
||||
.setType(SupportType.toProtoMessage(supportType)))
|
||||
.build();
|
||||
}
|
||||
|
||||
@ -72,7 +77,8 @@ public final class OpenNewDisputeMessage extends DisputeMessage {
|
||||
return new OpenNewDisputeMessage(Dispute.fromProto(proto.getDispute(), coreProtoResolver),
|
||||
NodeAddress.fromProto(proto.getSenderNodeAddress()),
|
||||
proto.getUid(),
|
||||
messageVersion);
|
||||
messageVersion,
|
||||
SupportType.fromProto(proto.getType()));
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -87,6 +93,7 @@ public final class OpenNewDisputeMessage extends DisputeMessage {
|
||||
",\n senderNodeAddress=" + senderNodeAddress +
|
||||
",\n OpenNewDisputeMessage.uid='" + uid + '\'' +
|
||||
",\n messageVersion=" + messageVersion +
|
||||
",\n supportType=" + supportType +
|
||||
"\n} " + super.toString();
|
||||
}
|
||||
}
|
@ -15,10 +15,11 @@
|
||||
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package bisq.core.arbitration.messages;
|
||||
package bisq.core.support.dispute.messages;
|
||||
|
||||
import bisq.core.arbitration.Dispute;
|
||||
import bisq.core.proto.CoreProtoResolver;
|
||||
import bisq.core.support.SupportType;
|
||||
import bisq.core.support.dispute.Dispute;
|
||||
|
||||
import bisq.network.p2p.NodeAddress;
|
||||
|
||||
@ -35,11 +36,13 @@ public final class PeerOpenedDisputeMessage extends DisputeMessage {
|
||||
|
||||
public PeerOpenedDisputeMessage(Dispute dispute,
|
||||
NodeAddress senderNodeAddress,
|
||||
String uid) {
|
||||
String uid,
|
||||
SupportType supportType) {
|
||||
this(dispute,
|
||||
senderNodeAddress,
|
||||
uid,
|
||||
Version.getP2PMessageVersion());
|
||||
Version.getP2PMessageVersion(),
|
||||
supportType);
|
||||
}
|
||||
|
||||
|
||||
@ -50,8 +53,9 @@ public final class PeerOpenedDisputeMessage extends DisputeMessage {
|
||||
private PeerOpenedDisputeMessage(Dispute dispute,
|
||||
NodeAddress senderNodeAddress,
|
||||
String uid,
|
||||
int messageVersion) {
|
||||
super(messageVersion, uid);
|
||||
int messageVersion,
|
||||
SupportType supportType) {
|
||||
super(messageVersion, uid, supportType);
|
||||
this.dispute = dispute;
|
||||
this.senderNodeAddress = senderNodeAddress;
|
||||
}
|
||||
@ -62,7 +66,8 @@ public final class PeerOpenedDisputeMessage extends DisputeMessage {
|
||||
.setPeerOpenedDisputeMessage(protobuf.PeerOpenedDisputeMessage.newBuilder()
|
||||
.setUid(uid)
|
||||
.setDispute(dispute.toProtoMessage())
|
||||
.setSenderNodeAddress(senderNodeAddress.toProtoMessage()))
|
||||
.setSenderNodeAddress(senderNodeAddress.toProtoMessage())
|
||||
.setType(SupportType.toProtoMessage(supportType)))
|
||||
.build();
|
||||
}
|
||||
|
||||
@ -70,7 +75,8 @@ public final class PeerOpenedDisputeMessage extends DisputeMessage {
|
||||
return new PeerOpenedDisputeMessage(Dispute.fromProto(proto.getDispute(), coreProtoResolver),
|
||||
NodeAddress.fromProto(proto.getSenderNodeAddress()),
|
||||
proto.getUid(),
|
||||
messageVersion);
|
||||
messageVersion,
|
||||
SupportType.fromProto(proto.getType()));
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -85,6 +91,7 @@ public final class PeerOpenedDisputeMessage extends DisputeMessage {
|
||||
",\n senderNodeAddress=" + senderNodeAddress +
|
||||
",\n PeerOpenedDisputeMessage.uid='" + uid + '\'' +
|
||||
",\n messageVersion=" + messageVersion +
|
||||
",\n supportType=" + supportType +
|
||||
"\n} " + super.toString();
|
||||
}
|
||||
}
|
@ -15,14 +15,14 @@
|
||||
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package bisq.core.arbitration.messages;
|
||||
package bisq.core.support.messages;
|
||||
|
||||
import bisq.core.arbitration.Attachment;
|
||||
import bisq.core.support.SupportType;
|
||||
import bisq.core.support.dispute.Attachment;
|
||||
|
||||
import bisq.network.p2p.NodeAddress;
|
||||
|
||||
import bisq.common.app.Version;
|
||||
import bisq.common.proto.ProtoUtil;
|
||||
import bisq.common.util.Utilities;
|
||||
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
@ -58,29 +58,12 @@ import javax.annotation.Nullable;
|
||||
@EqualsAndHashCode(callSuper = true) // listener is transient and therefore excluded anyway
|
||||
@Getter
|
||||
@Slf4j
|
||||
public final class DisputeCommunicationMessage extends DisputeMessage {
|
||||
|
||||
public enum Type {
|
||||
MEDIATION,
|
||||
ARBITRATION,
|
||||
TRADE;
|
||||
|
||||
public static DisputeCommunicationMessage.Type fromProto(
|
||||
protobuf.DisputeCommunicationMessage.Type type) {
|
||||
return ProtoUtil.enumFromProto(DisputeCommunicationMessage.Type.class, type.name());
|
||||
}
|
||||
|
||||
public static protobuf.DisputeCommunicationMessage.Type toProtoMessage(Type type) {
|
||||
return protobuf.DisputeCommunicationMessage.Type.valueOf(type.name());
|
||||
}
|
||||
}
|
||||
public final class ChatMessage extends SupportMessage {
|
||||
|
||||
public interface Listener {
|
||||
void onMessageStateChanged();
|
||||
}
|
||||
|
||||
// Added with v1.1.6. Old clients will not have set that field and we fall back to entry 0 which is DISPUTE.
|
||||
private final DisputeCommunicationMessage.Type type;
|
||||
private final String tradeId;
|
||||
private final int traderId;
|
||||
// This is only used for the server client relationship
|
||||
@ -93,10 +76,11 @@ public final class DisputeCommunicationMessage extends DisputeMessage {
|
||||
@Setter
|
||||
private boolean isSystemMessage;
|
||||
|
||||
// Added in v1.1.6.
|
||||
// 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;
|
||||
@ -105,13 +89,13 @@ public final class DisputeCommunicationMessage extends DisputeMessage {
|
||||
|
||||
transient private WeakReference<Listener> listener;
|
||||
|
||||
public DisputeCommunicationMessage(DisputeCommunicationMessage.Type type,
|
||||
String tradeId,
|
||||
int traderId,
|
||||
boolean senderIsTrader,
|
||||
String message,
|
||||
NodeAddress senderNodeAddress) {
|
||||
this(type,
|
||||
public ChatMessage(SupportType supportType,
|
||||
String tradeId,
|
||||
int traderId,
|
||||
boolean senderIsTrader,
|
||||
String message,
|
||||
NodeAddress senderNodeAddress) {
|
||||
this(supportType,
|
||||
tradeId,
|
||||
traderId,
|
||||
senderIsTrader,
|
||||
@ -129,14 +113,39 @@ public final class DisputeCommunicationMessage extends DisputeMessage {
|
||||
false);
|
||||
}
|
||||
|
||||
public DisputeCommunicationMessage(DisputeCommunicationMessage.Type type,
|
||||
String tradeId,
|
||||
int traderId,
|
||||
boolean senderIsTrader,
|
||||
String message,
|
||||
NodeAddress senderNodeAddress,
|
||||
long date) {
|
||||
this(type,
|
||||
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,
|
||||
@ -159,24 +168,23 @@ public final class DisputeCommunicationMessage extends DisputeMessage {
|
||||
// PROTO BUFFER
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
private DisputeCommunicationMessage(Type type,
|
||||
String tradeId,
|
||||
int traderId,
|
||||
boolean senderIsTrader,
|
||||
String message,
|
||||
@Nullable List<Attachment> attachments,
|
||||
NodeAddress senderNodeAddress,
|
||||
long date,
|
||||
boolean arrived,
|
||||
boolean storedInMailbox,
|
||||
String uid,
|
||||
int messageVersion,
|
||||
boolean acknowledged,
|
||||
@Nullable String sendMessageError,
|
||||
@Nullable String ackError,
|
||||
boolean wasDisplayed) {
|
||||
super(messageVersion, uid);
|
||||
this.type = type;
|
||||
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,
|
||||
int messageVersion,
|
||||
boolean acknowledged,
|
||||
@Nullable String sendMessageError,
|
||||
@Nullable String ackError,
|
||||
boolean wasDisplayed) {
|
||||
super(messageVersion, uid, supportType);
|
||||
this.tradeId = tradeId;
|
||||
this.traderId = traderId;
|
||||
this.senderIsTrader = senderIsTrader;
|
||||
@ -193,10 +201,11 @@ public final class DisputeCommunicationMessage extends DisputeMessage {
|
||||
notifyChangeListener();
|
||||
}
|
||||
|
||||
// We cannot rename protobuf definition because it would break backward compatibility
|
||||
@Override
|
||||
public protobuf.NetworkEnvelope toProtoNetworkEnvelope() {
|
||||
protobuf.DisputeCommunicationMessage.Builder builder = protobuf.DisputeCommunicationMessage.newBuilder()
|
||||
.setType(DisputeCommunicationMessage.Type.toProtoMessage(type))
|
||||
protobuf.ChatMessage.Builder builder = protobuf.ChatMessage.newBuilder()
|
||||
.setType(SupportType.toProtoMessage(supportType))
|
||||
.setTradeId(tradeId)
|
||||
.setTraderId(traderId)
|
||||
.setSenderIsTrader(senderIsTrader)
|
||||
@ -213,17 +222,17 @@ public final class DisputeCommunicationMessage extends DisputeMessage {
|
||||
Optional.ofNullable(sendMessageErrorProperty.get()).ifPresent(builder::setSendMessageError);
|
||||
Optional.ofNullable(ackErrorProperty.get()).ifPresent(builder::setAckError);
|
||||
return getNetworkEnvelopeBuilder()
|
||||
.setDisputeCommunicationMessage(builder)
|
||||
.setChatMessage(builder)
|
||||
.build();
|
||||
}
|
||||
|
||||
public static DisputeCommunicationMessage fromProto(protobuf.DisputeCommunicationMessage proto,
|
||||
int messageVersion) {
|
||||
// The protobuf definition ChatMessage cannot be changed as it would break backward compatibility.
|
||||
public static ChatMessage fromProto(protobuf.ChatMessage proto,
|
||||
int 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.
|
||||
DisputeCommunicationMessage.Type type = DisputeCommunicationMessage.Type.fromProto(proto.getType());
|
||||
final DisputeCommunicationMessage disputeCommunicationMessage = new DisputeCommunicationMessage(
|
||||
type,
|
||||
final ChatMessage chatMessage = new ChatMessage(
|
||||
SupportType.fromProto(proto.getType()),
|
||||
proto.getTradeId(),
|
||||
proto.getTraderId(),
|
||||
proto.getSenderIsTrader(),
|
||||
@ -239,11 +248,11 @@ public final class DisputeCommunicationMessage extends DisputeMessage {
|
||||
proto.getSendMessageError().isEmpty() ? null : proto.getSendMessageError(),
|
||||
proto.getAckError().isEmpty() ? null : proto.getAckError(),
|
||||
proto.getWasDisplayed());
|
||||
disputeCommunicationMessage.setSystemMessage(proto.getIsSystemMessage());
|
||||
return disputeCommunicationMessage;
|
||||
chatMessage.setSystemMessage(proto.getIsSystemMessage());
|
||||
return chatMessage;
|
||||
}
|
||||
|
||||
public static DisputeCommunicationMessage fromPayloadProto(protobuf.DisputeCommunicationMessage proto) {
|
||||
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
|
||||
@ -256,7 +265,7 @@ public final class DisputeCommunicationMessage extends DisputeMessage {
|
||||
// API
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
public void addAllAttachments(List<Attachment> attachments) {
|
||||
private void addAllAttachments(List<Attachment> attachments) {
|
||||
this.attachments.addAll(attachments);
|
||||
}
|
||||
|
||||
@ -330,8 +339,8 @@ public final class DisputeCommunicationMessage extends DisputeMessage {
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "DisputeCommunicationMessage{" +
|
||||
"\n type='" + type + '\'' +
|
||||
return "ChatMessage{" +
|
||||
"\n type='" + supportType + '\'' +
|
||||
",\n tradeId='" + tradeId + '\'' +
|
||||
",\n traderId=" + traderId +
|
||||
",\n senderIsTrader=" + senderIsTrader +
|
||||
@ -342,7 +351,7 @@ public final class DisputeCommunicationMessage extends DisputeMessage {
|
||||
",\n isSystemMessage=" + isSystemMessage +
|
||||
",\n arrivedProperty=" + arrivedProperty +
|
||||
",\n storedInMailboxProperty=" + storedInMailboxProperty +
|
||||
",\n DisputeCommunicationMessage.uid='" + uid + '\'' +
|
||||
",\n ChatMessage.uid='" + uid + '\'' +
|
||||
",\n messageVersion=" + messageVersion +
|
||||
",\n acknowledgedProperty=" + acknowledgedProperty +
|
||||
",\n sendMessageErrorProperty=" + sendMessageErrorProperty +
|
@ -15,7 +15,9 @@
|
||||
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package bisq.core.arbitration.messages;
|
||||
package bisq.core.support.messages;
|
||||
|
||||
import bisq.core.support.SupportType;
|
||||
|
||||
import bisq.network.p2p.MailboxMessage;
|
||||
import bisq.network.p2p.UidMessage;
|
||||
@ -27,12 +29,16 @@ import lombok.Getter;
|
||||
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@Getter
|
||||
public abstract class DisputeMessage extends NetworkEnvelope implements MailboxMessage, UidMessage {
|
||||
public abstract class SupportMessage extends NetworkEnvelope implements MailboxMessage, UidMessage {
|
||||
protected final String uid;
|
||||
|
||||
public DisputeMessage(int messageVersion, 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(int messageVersion, String uid, SupportType supportType) {
|
||||
super(messageVersion);
|
||||
this.uid = uid;
|
||||
this.supportType = supportType;
|
||||
}
|
||||
|
||||
public abstract String getTradeId();
|
||||
@ -42,6 +48,7 @@ public abstract class DisputeMessage extends NetworkEnvelope implements MailboxM
|
||||
return "DisputeMessage{" +
|
||||
"\n uid='" + uid + '\'' +
|
||||
",\n messageVersion=" + messageVersion +
|
||||
",\n supportType=" + supportType +
|
||||
"\n} " + super.toString();
|
||||
}
|
||||
}
|
@ -0,0 +1,73 @@
|
||||
/*
|
||||
* This file is part of Bisq.
|
||||
*
|
||||
* Bisq is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or (at
|
||||
* your option) any later version.
|
||||
*
|
||||
* Bisq is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
|
||||
* License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package bisq.core.support.traderchat;
|
||||
|
||||
import bisq.core.support.SupportSession;
|
||||
import bisq.core.support.messages.ChatMessage;
|
||||
import bisq.core.trade.Trade;
|
||||
|
||||
import bisq.common.crypto.PubKeyRing;
|
||||
|
||||
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 PubKeyRing getClientPubKeyRing() {
|
||||
// TODO remove that client-server concept for trade chat
|
||||
// Get pubKeyRing of taker. Maker is considered server for chat sessions
|
||||
if (trade != null && trade.getContract() != null)
|
||||
return trade.getContract().getTakerPubKeyRing();
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ObservableList<ChatMessage> getObservableChatMessageList() {
|
||||
return trade != null ? trade.getChatMessages() : FXCollections.observableArrayList();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean chatIsOpen() {
|
||||
return trade != null && trade.getState() != Trade.State.WITHDRAW_COMPLETED;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isDisputeAgent() {
|
||||
return false;
|
||||
}
|
||||
}
|
@ -0,0 +1,187 @@
|
||||
/*
|
||||
* This file is part of Bisq.
|
||||
*
|
||||
* Bisq is free software: you can redistribute it and/or modify it
|
||||
* under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or (at
|
||||
* your option) any later version.
|
||||
*
|
||||
* Bisq is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
|
||||
* License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package bisq.core.support.traderchat;
|
||||
|
||||
import bisq.core.btc.setup.WalletsSetup;
|
||||
import bisq.core.locale.Res;
|
||||
import bisq.core.support.SupportManager;
|
||||
import bisq.core.support.SupportType;
|
||||
import bisq.core.support.dispute.messages.DisputeResultMessage;
|
||||
import bisq.core.support.messages.ChatMessage;
|
||||
import bisq.core.support.messages.SupportMessage;
|
||||
import bisq.core.trade.Trade;
|
||||
import bisq.core.trade.TradeManager;
|
||||
|
||||
import bisq.network.p2p.AckMessageSourceType;
|
||||
import bisq.network.p2p.NodeAddress;
|
||||
import bisq.network.p2p.P2PService;
|
||||
|
||||
import bisq.common.crypto.PubKeyRing;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Singleton;
|
||||
|
||||
import javafx.collections.ObservableList;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CopyOnWriteArrayList;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
@Slf4j
|
||||
@Singleton
|
||||
public class TraderChatManager extends SupportManager {
|
||||
private final TradeManager tradeManager;
|
||||
private final PubKeyRing pubKeyRing;
|
||||
|
||||
public interface DisputeStateListener {
|
||||
void onDisputeClosed(String tradeId);
|
||||
}
|
||||
|
||||
// Needed to avoid ConcurrentModificationException as we remove a listener at the handler call
|
||||
private List<DisputeStateListener> disputeStateListeners = new CopyOnWriteArrayList<>();
|
||||
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
// Constructor
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
@Inject
|
||||
public TraderChatManager(P2PService p2PService,
|
||||
WalletsSetup walletsSetup,
|
||||
TradeManager tradeManager,
|
||||
PubKeyRing pubKeyRing) {
|
||||
super(p2PService, walletsSetup);
|
||||
this.tradeManager = tradeManager;
|
||||
this.pubKeyRing = pubKeyRing;
|
||||
}
|
||||
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
// Implement template methods
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
@Override
|
||||
public SupportType getSupportType() {
|
||||
return SupportType.TRADE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void persist() {
|
||||
tradeManager.persistTrades();
|
||||
}
|
||||
|
||||
@Override
|
||||
public NodeAddress getPeerNodeAddress(ChatMessage message) {
|
||||
return tradeManager.getTradeById(message.getTradeId()).map(trade -> {
|
||||
if (trade.getContract() != null) {
|
||||
return trade.getContract().getPeersNodeAddress(pubKeyRing);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}).orElse(null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public PubKeyRing getPeerPubKeyRing(ChatMessage message) {
|
||||
return tradeManager.getTradeById(message.getTradeId()).map(trade -> {
|
||||
if (trade.getContract() != null) {
|
||||
return trade.getContract().getPeersPubKeyRing(pubKeyRing);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}).orElse(null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ChatMessage> getAllChatMessages() {
|
||||
return tradeManager.getTradableList().stream()
|
||||
.flatMap(trade -> trade.getChatMessages().stream())
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean channelOpen(ChatMessage message) {
|
||||
return tradeManager.getTradeById(message.getTradeId()).isPresent();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addAndPersistChatMessage(ChatMessage message) {
|
||||
tradeManager.getTradeById(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);
|
||||
} else {
|
||||
log.warn("Trade got a chatMessage what we have already stored. UId = {} TradeId = {}",
|
||||
message.getUid(), message.getTradeId());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
protected AckMessageSourceType getAckMessageSourceType() {
|
||||
return AckMessageSourceType.TRADE_CHAT_MESSAGE;
|
||||
}
|
||||
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
// API
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
public void addDisputeStateListener(TraderChatManager.DisputeStateListener disputeStateListener) {
|
||||
disputeStateListeners.add(disputeStateListener);
|
||||
}
|
||||
|
||||
public void removeDisputeStateListener(TraderChatManager.DisputeStateListener disputeStateListener) {
|
||||
disputeStateListeners.remove(disputeStateListener);
|
||||
}
|
||||
|
||||
public void dispatchMessage(SupportMessage message) {
|
||||
if (canProcessMessage(message)) {
|
||||
log.info("Received {} with tradeId {} and uid {}",
|
||||
message.getClass().getSimpleName(), message.getTradeId(), message.getUid());
|
||||
if (message instanceof ChatMessage) {
|
||||
onChatMessage((ChatMessage) message);
|
||||
} else if (message instanceof DisputeResultMessage) {
|
||||
// We notify about dispute closed state
|
||||
disputeStateListeners.forEach(e -> e.onDisputeClosed(message.getTradeId()));
|
||||
} 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);
|
||||
}
|
||||
}
|
@ -47,9 +47,17 @@ public final class BuyerAsMakerTrade extends BuyerTrade implements MakerTrade {
|
||||
Coin takeOfferFee,
|
||||
boolean isCurrencyForTakerFeeBtc,
|
||||
@Nullable NodeAddress arbitratorNodeAddress,
|
||||
@Nullable NodeAddress mediatorNodeAddress,
|
||||
Storage<? extends TradableList> storage,
|
||||
BtcWalletService btcWalletService) {
|
||||
super(offer, txFee, takeOfferFee, isCurrencyForTakerFeeBtc, arbitratorNodeAddress, storage, btcWalletService);
|
||||
super(offer,
|
||||
txFee,
|
||||
takeOfferFee,
|
||||
isCurrencyForTakerFeeBtc,
|
||||
arbitratorNodeAddress,
|
||||
mediatorNodeAddress,
|
||||
storage,
|
||||
btcWalletService);
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
@ -75,6 +83,7 @@ public final class BuyerAsMakerTrade extends BuyerTrade implements MakerTrade {
|
||||
Coin.valueOf(proto.getTakerFeeAsLong()),
|
||||
proto.getIsCurrencyForTakerFeeBtc(),
|
||||
proto.hasArbitratorNodeAddress() ? NodeAddress.fromProto(proto.getArbitratorNodeAddress()) : null,
|
||||
proto.hasMediatorNodeAddress() ? NodeAddress.fromProto(proto.getMediatorNodeAddress()) : null,
|
||||
storage,
|
||||
btcWalletService);
|
||||
|
||||
@ -98,7 +107,9 @@ public final class BuyerAsMakerTrade extends BuyerTrade implements MakerTrade {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleTakeOfferRequest(TradeMessage message, NodeAddress taker, ErrorMessageHandler errorMessageHandler) {
|
||||
public void handleTakeOfferRequest(TradeMessage message,
|
||||
NodeAddress taker,
|
||||
ErrorMessageHandler errorMessageHandler) {
|
||||
((MakerProtocol) tradeProtocol).handleTakeOfferRequest(message, taker, errorMessageHandler);
|
||||
}
|
||||
}
|
||||
|
@ -50,10 +50,20 @@ public final class BuyerAsTakerTrade extends BuyerTrade implements TakerTrade {
|
||||
long tradePrice,
|
||||
NodeAddress tradingPeerNodeAddress,
|
||||
@Nullable NodeAddress arbitratorNodeAddress,
|
||||
@Nullable NodeAddress mediatorNodeAddress,
|
||||
Storage<? extends TradableList> storage,
|
||||
BtcWalletService btcWalletService) {
|
||||
super(offer, tradeAmount, txFee, takerFee, isCurrencyForTakerFeeBtc, tradePrice, tradingPeerNodeAddress,
|
||||
arbitratorNodeAddress, storage, btcWalletService);
|
||||
super(offer,
|
||||
tradeAmount,
|
||||
txFee,
|
||||
takerFee,
|
||||
isCurrencyForTakerFeeBtc,
|
||||
tradePrice,
|
||||
tradingPeerNodeAddress,
|
||||
arbitratorNodeAddress,
|
||||
mediatorNodeAddress,
|
||||
storage,
|
||||
btcWalletService);
|
||||
}
|
||||
|
||||
|
||||
@ -83,6 +93,7 @@ public final class BuyerAsTakerTrade extends BuyerTrade implements TakerTrade {
|
||||
proto.getTradePrice(),
|
||||
proto.hasTradingPeerNodeAddress() ? NodeAddress.fromProto(proto.getTradingPeerNodeAddress()) : null,
|
||||
proto.hasArbitratorNodeAddress() ? NodeAddress.fromProto(proto.getArbitratorNodeAddress()) : null,
|
||||
proto.hasMediatorNodeAddress() ? NodeAddress.fromProto(proto.getMediatorNodeAddress()) : null,
|
||||
storage,
|
||||
btcWalletService),
|
||||
proto,
|
||||
|
@ -46,10 +46,20 @@ public abstract class BuyerTrade extends Trade {
|
||||
long tradePrice,
|
||||
NodeAddress tradingPeerNodeAddress,
|
||||
@Nullable NodeAddress arbitratorNodeAddress,
|
||||
@Nullable NodeAddress mediatorNodeAddress,
|
||||
Storage<? extends TradableList> storage,
|
||||
BtcWalletService btcWalletService) {
|
||||
super(offer, tradeAmount, txFee, takerFee, isCurrencyForTakerFeeBtc, tradePrice,
|
||||
tradingPeerNodeAddress, arbitratorNodeAddress, storage, btcWalletService);
|
||||
super(offer,
|
||||
tradeAmount,
|
||||
txFee,
|
||||
takerFee,
|
||||
isCurrencyForTakerFeeBtc,
|
||||
tradePrice,
|
||||
tradingPeerNodeAddress,
|
||||
arbitratorNodeAddress,
|
||||
mediatorNodeAddress,
|
||||
storage,
|
||||
btcWalletService);
|
||||
}
|
||||
|
||||
BuyerTrade(Offer offer,
|
||||
@ -57,9 +67,17 @@ public abstract class BuyerTrade extends Trade {
|
||||
Coin takerFee,
|
||||
boolean isCurrencyForTakerFeeBtc,
|
||||
@Nullable NodeAddress arbitratorNodeAddress,
|
||||
@Nullable NodeAddress mediatorNodeAddress,
|
||||
Storage<? extends TradableList> storage,
|
||||
BtcWalletService btcWalletService) {
|
||||
super(offer, txFee, takerFee, isCurrencyForTakerFeeBtc, arbitratorNodeAddress, storage, btcWalletService);
|
||||
super(offer,
|
||||
txFee,
|
||||
takerFee,
|
||||
isCurrencyForTakerFeeBtc,
|
||||
arbitratorNodeAddress,
|
||||
mediatorNodeAddress,
|
||||
storage,
|
||||
btcWalletService);
|
||||
}
|
||||
|
||||
public void onFiatPaymentStarted(ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) {
|
||||
|
@ -236,6 +236,31 @@ public final class Contract implements NetworkPayload {
|
||||
return Price.valueOf(offerPayload.getCurrencyCode(), tradePrice);
|
||||
}
|
||||
|
||||
public NodeAddress getMyNodeAddress(PubKeyRing myPubKeyRing) {
|
||||
if (myPubKeyRing.equals(getBuyerPubKeyRing()))
|
||||
return buyerNodeAddress;
|
||||
else
|
||||
return sellerNodeAddress;
|
||||
}
|
||||
|
||||
public NodeAddress getPeersNodeAddress(PubKeyRing myPubKeyRing) {
|
||||
if (myPubKeyRing.equals(getSellerPubKeyRing()))
|
||||
return buyerNodeAddress;
|
||||
else
|
||||
return sellerNodeAddress;
|
||||
}
|
||||
|
||||
public PubKeyRing getPeersPubKeyRing(PubKeyRing myPubKeyRing) {
|
||||
if (myPubKeyRing.equals(getSellerPubKeyRing()))
|
||||
return getBuyerPubKeyRing();
|
||||
else
|
||||
return getSellerPubKeyRing();
|
||||
}
|
||||
|
||||
public boolean isMyRoleBuyer(PubKeyRing myPubKeyRing) {
|
||||
return getBuyerPubKeyRing().equals(myPubKeyRing);
|
||||
}
|
||||
|
||||
public void printDiff(@Nullable String peersContractAsJson) {
|
||||
final String json = Utilities.objectToJson(this);
|
||||
String diff = StringUtils.difference(json, peersContractAsJson);
|
||||
|
@ -47,9 +47,10 @@ public final class SellerAsMakerTrade extends SellerTrade implements MakerTrade
|
||||
Coin takerFee,
|
||||
boolean isCurrencyForTakerFeeBtc,
|
||||
@Nullable NodeAddress arbitratorNodeAddress,
|
||||
@Nullable NodeAddress mediatorNodeAddress,
|
||||
Storage<? extends TradableList> storage,
|
||||
BtcWalletService btcWalletService) {
|
||||
super(offer, txFee, takerFee, isCurrencyForTakerFeeBtc, arbitratorNodeAddress, storage, btcWalletService);
|
||||
super(offer, txFee, takerFee, isCurrencyForTakerFeeBtc, arbitratorNodeAddress, mediatorNodeAddress, storage, btcWalletService);
|
||||
}
|
||||
|
||||
|
||||
@ -76,6 +77,7 @@ public final class SellerAsMakerTrade extends SellerTrade implements MakerTrade
|
||||
Coin.valueOf(proto.getTakerFeeAsLong()),
|
||||
proto.getIsCurrencyForTakerFeeBtc(),
|
||||
proto.hasArbitratorNodeAddress() ? NodeAddress.fromProto(proto.getArbitratorNodeAddress()) : null,
|
||||
proto.hasMediatorNodeAddress() ? NodeAddress.fromProto(proto.getMediatorNodeAddress()) : null,
|
||||
storage,
|
||||
btcWalletService);
|
||||
|
||||
|
@ -50,10 +50,20 @@ public final class SellerAsTakerTrade extends SellerTrade implements TakerTrade
|
||||
long tradePrice,
|
||||
NodeAddress tradingPeerNodeAddress,
|
||||
@Nullable NodeAddress arbitratorNodeAddress,
|
||||
@Nullable NodeAddress mediatorNodeAddress,
|
||||
Storage<? extends TradableList> storage,
|
||||
BtcWalletService btcWalletService) {
|
||||
super(offer, tradeAmount, txFee, takerFee, isCurrencyForTakerFeeBtc, tradePrice,
|
||||
tradingPeerNodeAddress, arbitratorNodeAddress, storage, btcWalletService);
|
||||
super(offer,
|
||||
tradeAmount,
|
||||
txFee,
|
||||
takerFee,
|
||||
isCurrencyForTakerFeeBtc,
|
||||
tradePrice,
|
||||
tradingPeerNodeAddress,
|
||||
arbitratorNodeAddress,
|
||||
mediatorNodeAddress,
|
||||
storage,
|
||||
btcWalletService);
|
||||
}
|
||||
|
||||
|
||||
@ -83,6 +93,7 @@ public final class SellerAsTakerTrade extends SellerTrade implements TakerTrade
|
||||
proto.getTradePrice(),
|
||||
proto.hasTradingPeerNodeAddress() ? NodeAddress.fromProto(proto.getTradingPeerNodeAddress()) : null,
|
||||
proto.hasArbitratorNodeAddress() ? NodeAddress.fromProto(proto.getArbitratorNodeAddress()) : null,
|
||||
proto.hasMediatorNodeAddress() ? NodeAddress.fromProto(proto.getMediatorNodeAddress()) : null,
|
||||
storage,
|
||||
btcWalletService),
|
||||
proto,
|
||||
|
@ -45,10 +45,20 @@ public abstract class SellerTrade extends Trade {
|
||||
long tradePrice,
|
||||
NodeAddress tradingPeerNodeAddress,
|
||||
@Nullable NodeAddress arbitratorNodeAddress,
|
||||
@Nullable NodeAddress mediatorNodeAddress,
|
||||
Storage<? extends TradableList> storage,
|
||||
BtcWalletService btcWalletService) {
|
||||
super(offer, tradeAmount, txFee, takerFee, isCurrencyForTakerFeeBtc, tradePrice,
|
||||
tradingPeerNodeAddress, arbitratorNodeAddress, storage, btcWalletService);
|
||||
super(offer,
|
||||
tradeAmount,
|
||||
txFee,
|
||||
takerFee,
|
||||
isCurrencyForTakerFeeBtc,
|
||||
tradePrice,
|
||||
tradingPeerNodeAddress,
|
||||
arbitratorNodeAddress,
|
||||
mediatorNodeAddress,
|
||||
storage,
|
||||
btcWalletService);
|
||||
}
|
||||
|
||||
SellerTrade(Offer offer,
|
||||
@ -56,9 +66,17 @@ public abstract class SellerTrade extends Trade {
|
||||
Coin takeOfferFee,
|
||||
boolean isCurrencyForTakerFeeBtc,
|
||||
@Nullable NodeAddress arbitratorNodeAddress,
|
||||
@Nullable NodeAddress mediatorNodeAddress,
|
||||
Storage<? extends TradableList> storage,
|
||||
BtcWalletService btcWalletService) {
|
||||
super(offer, txFee, takeOfferFee, isCurrencyForTakerFeeBtc, arbitratorNodeAddress, storage, btcWalletService);
|
||||
super(offer,
|
||||
txFee,
|
||||
takeOfferFee,
|
||||
isCurrencyForTakerFeeBtc,
|
||||
arbitratorNodeAddress,
|
||||
mediatorNodeAddress,
|
||||
storage,
|
||||
btcWalletService);
|
||||
}
|
||||
|
||||
public void onFiatPaymentReceived(ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) {
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user