diff --git a/core/src/main/java/bisq/core/api/CoreApi.java b/core/src/main/java/bisq/core/api/CoreApi.java index 3e53abc2f5..ac4c8dcd00 100644 --- a/core/src/main/java/bisq/core/api/CoreApi.java +++ b/core/src/main/java/bisq/core/api/CoreApi.java @@ -29,6 +29,9 @@ import bisq.core.offer.OfferPayload; import bisq.core.offer.OpenOffer; import bisq.core.payment.PaymentAccount; import bisq.core.payment.payload.PaymentMethod; +import bisq.core.support.dispute.Attachment; +import bisq.core.support.dispute.Dispute; +import bisq.core.support.dispute.DisputeResult; import bisq.core.trade.Trade; import bisq.core.trade.statistics.TradeStatistics3; import bisq.core.trade.statistics.TradeStatisticsManager; @@ -36,6 +39,7 @@ import bisq.common.app.Version; import bisq.common.config.Config; import bisq.common.crypto.IncorrectPasswordException; import bisq.common.handlers.ErrorMessageHandler; +import bisq.common.handlers.FaultHandler; import bisq.common.handlers.ResultHandler; import bisq.proto.grpc.NotificationMessage; @@ -79,6 +83,7 @@ public class CoreApi { private final AppStartupState appStartupState; private final CoreAccountService coreAccountService; private final CoreDisputeAgentsService coreDisputeAgentsService; + private final CoreDisputesService coreDisputeService; private final CoreHelpService coreHelpService; private final CoreOffersService coreOffersService; private final CorePaymentAccountsService paymentAccountsService; @@ -94,6 +99,7 @@ public class CoreApi { AppStartupState appStartupState, CoreAccountService coreAccountService, CoreDisputeAgentsService coreDisputeAgentsService, + CoreDisputesService coreDisputeService, CoreHelpService coreHelpService, CoreOffersService coreOffersService, CorePaymentAccountsService paymentAccountsService, @@ -107,6 +113,7 @@ public class CoreApi { this.appStartupState = appStartupState; this.coreAccountService = coreAccountService; this.coreDisputeAgentsService = coreDisputeAgentsService; + this.coreDisputeService = coreDisputeService; this.coreHelpService = coreHelpService; this.coreOffersService = coreOffersService; this.paymentAccountsService = paymentAccountsService; @@ -134,7 +141,7 @@ public class CoreApi { /////////////////////////////////////////////////////////////////////////////////////////// // Account Service /////////////////////////////////////////////////////////////////////////////////////////// - + public boolean accountExists() { return coreAccountService.accountExists(); } @@ -174,7 +181,7 @@ public class CoreApi { public void restoreAccount(InputStream zipStream, int bufferSize, Runnable onShutdown) throws Exception { coreAccountService.restoreAccount(zipStream, bufferSize, onShutdown); } - + /////////////////////////////////////////////////////////////////////////////////////////// // Monero Connections /////////////////////////////////////////////////////////////////////////////////////////// @@ -333,6 +340,31 @@ public class CoreApi { notificationService.sendNotification(notification); } + /////////////////////////////////////////////////////////////////////////////////////////// + // Disputes + /////////////////////////////////////////////////////////////////////////////////////////// + + public List getDisputes() { + return coreDisputeService.getDisputes(); + } + + public Dispute getDispute(String tradeId) { + return coreDisputeService.getDispute(tradeId); + } + + public void openDispute(String tradeId, ResultHandler resultHandler, FaultHandler faultHandler) { + coreDisputeService.openDispute(tradeId, resultHandler, faultHandler); + } + + public void resolveDispute(String tradeId, DisputeResult.Winner winner, DisputeResult.Reason reason, + String summaryNotes, long customPayoutAmount) { + coreDisputeService.resolveDispute(tradeId, winner, reason, summaryNotes, customPayoutAmount); + } + + public void sendDisputeChatMessage(String disputeId, String message, ArrayList attachments) { + coreDisputeService.sendDisputeChatMessage(disputeId, message, attachments); + } + /////////////////////////////////////////////////////////////////////////////////////////// // Dispute Agents /////////////////////////////////////////////////////////////////////////////////////////// diff --git a/core/src/main/java/bisq/core/api/CoreDisputesService.java b/core/src/main/java/bisq/core/api/CoreDisputesService.java new file mode 100644 index 0000000000..1995085623 --- /dev/null +++ b/core/src/main/java/bisq/core/api/CoreDisputesService.java @@ -0,0 +1,335 @@ +package bisq.core.api; + +import bisq.core.btc.wallet.XmrWalletService; +import bisq.core.locale.Res; +import bisq.core.offer.Offer; +import bisq.core.offer.OfferPayload; +import bisq.core.support.SupportType; +import bisq.core.support.dispute.Attachment; +import bisq.core.support.dispute.Dispute; +import bisq.core.support.dispute.DisputeManager; +import bisq.core.support.dispute.DisputeResult; +import bisq.core.support.dispute.DisputeSummaryVerification; +import bisq.core.support.dispute.arbitration.ArbitrationManager; +import bisq.core.support.messages.ChatMessage; +import bisq.core.trade.Contract; +import bisq.core.trade.Trade; +import bisq.core.trade.TradeManager; +import bisq.core.util.FormattingUtils; +import bisq.core.util.coin.CoinFormatter; + +import bisq.common.crypto.KeyRing; +import bisq.common.crypto.PubKeyRing; +import bisq.common.handlers.FaultHandler; +import bisq.common.handlers.ResultHandler; + +import org.bitcoinj.core.AddressFormatException; +import org.bitcoinj.core.Coin; + +import com.google.inject.name.Named; +import javax.inject.Inject; +import javax.inject.Singleton; + +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.Optional; + +import lombok.extern.slf4j.Slf4j; + +import static com.google.common.base.Preconditions.checkNotNull; +import static java.lang.String.format; + +import monero.wallet.MoneroWallet; +import monero.wallet.model.MoneroTxWallet; + +@Singleton +@Slf4j +public class CoreDisputesService { + + public enum DisputePayout { + BUYER_GETS_TRADE_AMOUNT, + BUYER_GETS_ALL, // used in desktop + SELLER_GETS_TRADE_AMOUNT, + SELLER_GETS_ALL, // used in desktop + CUSTOM + } + + private final ArbitrationManager arbitrationManager; + private final CoinFormatter formatter; + private final KeyRing keyRing; + private final TradeManager tradeManager; + private final XmrWalletService xmrWalletService; + + @Inject + public CoreDisputesService(ArbitrationManager arbitrationManager, + @Named(FormattingUtils.BTC_FORMATTER_KEY) CoinFormatter formatter, // TODO: XMR? + KeyRing keyRing, + TradeManager tradeManager, + XmrWalletService xmrWalletService) { + this.arbitrationManager = arbitrationManager; + this.formatter = formatter; + this.keyRing = keyRing; + this.tradeManager = tradeManager; + this.xmrWalletService = xmrWalletService; + } + + public List getDisputes() { + return arbitrationManager.getDisputesAsObservableList(); + } + + public Dispute getDispute(String tradeId) { + Optional dispute = arbitrationManager.findDispute(tradeId); + if (dispute.isPresent()) return dispute.get(); + else throw new IllegalStateException(format("dispute for trade id '%s' not found", tradeId)); + } + + public void openDispute(String tradeId, ResultHandler resultHandler, FaultHandler faultHandler) { + Trade trade = tradeManager.getTradeById(tradeId).orElseThrow(() -> + new IllegalArgumentException(format("trade with id '%s' not found", tradeId))); + + Offer offer = trade.getOffer(); + if (offer == null) throw new IllegalStateException(format("offer with tradeId '%s' is null", tradeId)); + + // Dispute agents are registered as mediators and refund agents, but current UI appears to be hardcoded + // to reference the arbitrator. Reference code is in desktop PendingTradesDataModel.java and could be refactored. + var disputeManager = arbitrationManager; + var isSupportTicket = false; + var isMaker = tradeManager.isMyOffer(offer); + var dispute = createDisputeForTrade(trade, offer, keyRing.getPubKeyRing(), isMaker, isSupportTicket); + + // Sends the openNewDisputeMessage to arbitrator, who will then create 2 disputes + // one for the opener, the other for the peer, see sendPeerOpenedDisputeMessage. + MoneroWallet multisigWallet = xmrWalletService.getMultisigWallet(trade.getId()); + String updatedMultisigHex = multisigWallet.getMultisigHex(); + disputeManager.sendOpenNewDisputeMessage(dispute, false, updatedMultisigHex, resultHandler, faultHandler); + tradeManager.requestPersistence(); + } + + public Dispute createDisputeForTrade(Trade trade, Offer offer, PubKeyRing pubKey, boolean isMaker, boolean isSupportTicket) { + byte[] payoutTxSerialized = null; + String payoutTxHashAsString = null; + + PubKeyRing arbitratorPubKeyRing = trade.getArbitratorPubKeyRing(); + checkNotNull(arbitratorPubKeyRing, "arbitratorPubKeyRing must not be null"); + byte[] depositTxSerialized = null; // depositTx.bitcoinSerialize(); TODO (woodser) + String depositTxHashAsString = null; // depositTx.getHashAsString(); TODO (woodser) + Dispute dispute = new Dispute(new Date().getTime(), + trade.getId(), + pubKey.hashCode(), // trader id, + true, + (offer.getDirection() == OfferPayload.Direction.BUY) == isMaker, + isMaker, + pubKey, + trade.getDate().getTime(), + trade.getMaxTradePeriodDate().getTime(), + trade.getContract(), + trade.getContractHash(), + depositTxSerialized, + payoutTxSerialized, + depositTxHashAsString, + payoutTxHashAsString, + trade.getContractAsJson(), + trade.getMaker().getContractSignature(), + trade.getTaker().getContractSignature(), + trade.getMaker().getPaymentAccountPayload(), + trade.getTaker().getPaymentAccountPayload(), + arbitratorPubKeyRing, + isSupportTicket, + SupportType.ARBITRATION); + + trade.setDisputeState(Trade.DisputeState.DISPUTE_REQUESTED); + + return dispute; + } + + public void resolveDispute(String tradeId, DisputeResult.Winner winner, DisputeResult.Reason reason, String summaryNotes, long customWinnerAmount) { + try { + var disputeOptional = arbitrationManager.getDisputesAsObservableList().stream() + .filter(d -> tradeId.equals(d.getTradeId())) + .findFirst(); + Dispute dispute; + if (disputeOptional.isPresent()) dispute = disputeOptional.get(); + else throw new IllegalStateException(format("dispute for tradeId '%s' not found", tradeId)); + + var closeDate = new Date(); + var disputeResult = createDisputeResult(dispute, winner, reason, summaryNotes, closeDate); + var contract = dispute.getContract(); + + DisputePayout payout; + if (customWinnerAmount > 0) { + payout = DisputePayout.CUSTOM; + } else if (winner == DisputeResult.Winner.BUYER) { + payout = DisputePayout.BUYER_GETS_TRADE_AMOUNT; + } else if (winner == DisputeResult.Winner.SELLER) { + payout = DisputePayout.SELLER_GETS_TRADE_AMOUNT; + } else { + throw new IllegalStateException("Unexpected DisputeResult.Winner: " + winner); + } + applyPayoutAmountsToDisputeResult(payout, dispute, disputeResult, customWinnerAmount); + + // resolve the payout + resolveDisputePayout(dispute, disputeResult, contract); + + // close dispute ticket + closeDispute(arbitrationManager, dispute, disputeResult, false); + + // close dispute ticket for peer + var peersDisputeOptional = arbitrationManager.getDisputesAsObservableList().stream() + .filter(d -> tradeId.equals(d.getTradeId()) && dispute.getTraderId() != d.getTraderId()) + .findFirst(); + + if (peersDisputeOptional.isPresent()) { + var peerDispute = peersDisputeOptional.get(); + var peerDisputeResult = createDisputeResult(peerDispute, winner, reason, summaryNotes, closeDate); + peerDisputeResult.setBuyerPayoutAmount(disputeResult.getBuyerPayoutAmount()); + peerDisputeResult.setSellerPayoutAmount(disputeResult.getSellerPayoutAmount()); + peerDisputeResult.setLoserPublisher(disputeResult.isLoserPublisher()); + resolveDisputePayout(peerDispute, peerDisputeResult, peerDispute.getContract()); + closeDispute(arbitrationManager, peerDispute, peerDisputeResult, false); + } else { + throw new IllegalStateException("could not find peer dispute"); + } + + arbitrationManager.requestPersistence(); + } catch (Exception e) { + throw new IllegalStateException(e); + } + } + + private DisputeResult createDisputeResult(Dispute dispute, DisputeResult.Winner winner, DisputeResult.Reason reason, + String summaryNotes, Date closeDate) { + var disputeResult = new DisputeResult(dispute.getTradeId(), dispute.getTraderId()); + disputeResult.setWinner(winner); + disputeResult.setReason(reason); + disputeResult.setSummaryNotes(summaryNotes); + disputeResult.setCloseDate(closeDate); + return disputeResult; + } + + /** + * Sets payout amounts given a payout type. If custom is selected, the winner gets a custom amount, and the peer + * receives the remaining amount minus the mining fee. + */ + public void applyPayoutAmountsToDisputeResult(DisputePayout payout, Dispute dispute, DisputeResult disputeResult, long customWinnerAmount) { + Contract contract = dispute.getContract(); + Offer offer = new Offer(contract.getOfferPayload()); + Coin buyerSecurityDeposit = offer.getBuyerSecurityDeposit(); + Coin sellerSecurityDeposit = offer.getSellerSecurityDeposit(); + Coin tradeAmount = contract.getTradeAmount(); + if (payout == DisputePayout.BUYER_GETS_TRADE_AMOUNT) { + disputeResult.setBuyerPayoutAmount(tradeAmount.add(buyerSecurityDeposit)); + disputeResult.setSellerPayoutAmount(sellerSecurityDeposit); + } else if (payout == DisputePayout.BUYER_GETS_ALL) { + disputeResult.setBuyerPayoutAmount(tradeAmount + .add(buyerSecurityDeposit) + .add(sellerSecurityDeposit)); // TODO (woodser): apply min payout to incentivize loser (see post v1.1.7) + disputeResult.setSellerPayoutAmount(Coin.ZERO); + } else if (payout == DisputePayout.SELLER_GETS_TRADE_AMOUNT) { + disputeResult.setBuyerPayoutAmount(buyerSecurityDeposit); + disputeResult.setSellerPayoutAmount(tradeAmount.add(sellerSecurityDeposit)); + } else if (payout == DisputePayout.SELLER_GETS_ALL) { + disputeResult.setBuyerPayoutAmount(Coin.ZERO); + disputeResult.setSellerPayoutAmount(tradeAmount + .add(sellerSecurityDeposit) + .add(buyerSecurityDeposit)); + } else if (payout == DisputePayout.CUSTOM) { + Coin winnerAmount = Coin.valueOf(customWinnerAmount); + Coin loserAmount = tradeAmount.add(buyerSecurityDeposit).add(sellerSecurityDeposit).minus(winnerAmount); + disputeResult.setBuyerPayoutAmount(disputeResult.getWinner() == DisputeResult.Winner.BUYER ? winnerAmount : loserAmount); + disputeResult.setSellerPayoutAmount(disputeResult.getWinner() == DisputeResult.Winner.BUYER ? loserAmount : winnerAmount); + } + } + + public void resolveDisputePayout(Dispute dispute, DisputeResult disputeResult, Contract contract) { + // TODO (woodser): create disputed payout tx after showing payout tx confirmation, within doCloseIfValid() (see upstream/master) + if (!dispute.isMediationDispute()) { + try { + System.out.println(disputeResult); + MoneroWallet multisigWallet = xmrWalletService.getMultisigWallet(dispute.getTradeId()); + //dispute.getContract().getArbitratorPubKeyRing(); // TODO: support arbitrator pub key ring in contract? + //disputeResult.setArbitratorPubKey(arbitratorAddressEntry.getPubKey()); + + // TODO (woodser): don't send signed tx if opener is not co-signer? + // // determine if opener is co-signer + // boolean openerIsWinner = (contract.getBuyerPubKeyRing().equals(dispute.getTraderPubKeyRing()) && disputeResult.getWinner() == Winner.BUYER) || (contract.getSellerPubKeyRing().equals(dispute.getTraderPubKeyRing()) && disputeResult.getWinner() == Winner.SELLER); + // boolean openerIsCosigner = openerIsWinner || disputeResult.isLoserPublisher(); + // if (!openerIsCosigner) throw new RuntimeException("Need to query non-opener for updated multisig hex before creating tx"); + + // arbitrator creates and signs dispute payout tx if dispute is in context of opener, otherwise opener's peer must request payout tx by providing updated multisig hex + boolean isOpener = dispute.isOpener(); + System.out.println("Is dispute opener: " + isOpener); + if (isOpener) { + MoneroTxWallet arbitratorPayoutTx = ArbitrationManager.arbitratorCreatesDisputedPayoutTx(contract, dispute, disputeResult, multisigWallet); + System.out.println("Created arbitrator-signed payout tx: " + arbitratorPayoutTx); + if (arbitratorPayoutTx != null) + disputeResult.setArbitratorSignedPayoutTxHex(arbitratorPayoutTx.getTxSet().getMultisigTxHex()); + } + + // send arbitrator's updated multisig hex with dispute result + disputeResult.setArbitratorUpdatedMultisigHex(multisigWallet.getMultisigHex()); + } catch (AddressFormatException e2) { + log.error("Error at close dispute", e2); + return; + } + } + } + + // From DisputeSummaryWindow.java + public void closeDispute(DisputeManager disputeManager, Dispute dispute, DisputeResult disputeResult, boolean isRefundAgent) { + dispute.setDisputeResult(disputeResult); + dispute.setIsClosed(); + DisputeResult.Reason reason = disputeResult.getReason(); + + String role = isRefundAgent ? Res.get("shared.refundAgent") : Res.get("shared.mediator"); + String agentNodeAddress = checkNotNull(disputeManager.getAgentNodeAddress(dispute)).getFullAddress(); + Contract contract = dispute.getContract(); + String currencyCode = contract.getOfferPayload().getCurrencyCode(); + String amount = formatter.formatCoinWithCode(contract.getTradeAmount()); + + String textToSign = Res.get("disputeSummaryWindow.close.msg", + FormattingUtils.formatDateTime(disputeResult.getCloseDate(), true), + role, + agentNodeAddress, + dispute.getShortTradeId(), + currencyCode, + amount, + formatter.formatCoinWithCode(disputeResult.getBuyerPayoutAmount()), + formatter.formatCoinWithCode(disputeResult.getSellerPayoutAmount()), + Res.get("disputeSummaryWindow.reason." + reason.name()), + disputeResult.summaryNotesProperty().get() + ); + + if (reason == DisputeResult.Reason.OPTION_TRADE && + dispute.getChatMessages().size() > 1 && + dispute.getChatMessages().get(1).isSystemMessage()) { + textToSign += "\n" + dispute.getChatMessages().get(1).getMessage() + "\n"; + } + + String summaryText = DisputeSummaryVerification.signAndApply(disputeManager, disputeResult, textToSign); + + if (isRefundAgent) { + summaryText += Res.get("disputeSummaryWindow.close.nextStepsForRefundAgentArbitration"); + } else { + summaryText += Res.get("disputeSummaryWindow.close.nextStepsForMediation"); + } + disputeManager.sendDisputeResultMessage(disputeResult, dispute, summaryText); + } + + public void sendDisputeChatMessage(String disputeId, String message, ArrayList attachments) { + var disputeOptional = arbitrationManager.findDisputeById(disputeId); + Dispute dispute; + if (disputeOptional.isPresent()) dispute = disputeOptional.get(); + else throw new IllegalStateException(format("dispute with id '%s' not found", disputeId)); + ChatMessage chatMessage = new ChatMessage( + arbitrationManager.getSupportType(), + dispute.getTradeId(), + dispute.getTraderId(), + arbitrationManager.isTrader(dispute), + message, + arbitrationManager.getMyAddress(), + attachments); + dispute.addAndPersistChatMessage(chatMessage); + arbitrationManager.sendChatMessage(chatMessage); + } +} diff --git a/core/src/main/java/bisq/core/api/CoreNotificationService.java b/core/src/main/java/bisq/core/api/CoreNotificationService.java index 36f762321e..8825a86164 100644 --- a/core/src/main/java/bisq/core/api/CoreNotificationService.java +++ b/core/src/main/java/bisq/core/api/CoreNotificationService.java @@ -3,6 +3,7 @@ package bisq.core.api; import bisq.core.api.CoreApi.NotificationListener; import bisq.core.api.model.TradeInfo; import bisq.core.trade.Trade; +import bisq.core.support.messages.ChatMessage; import bisq.proto.grpc.NotificationMessage; import bisq.proto.grpc.NotificationMessage.NotificationType; import javax.inject.Singleton; @@ -56,4 +57,12 @@ public class CoreNotificationService { .setTitle(title) .setMessage(message).build()); } + + public void sendChatNotification(ChatMessage chatMessage) { + sendNotification(NotificationMessage.newBuilder() + .setType(NotificationType.CHAT_MESSAGE) + .setTimestamp(System.currentTimeMillis()) + .setChatMessage(chatMessage.toProtoChatMessageBuilder()) + .build()); + } } diff --git a/core/src/main/java/bisq/core/support/SupportManager.java b/core/src/main/java/bisq/core/support/SupportManager.java index a869279132..e233e5447f 100644 --- a/core/src/main/java/bisq/core/support/SupportManager.java +++ b/core/src/main/java/bisq/core/support/SupportManager.java @@ -18,6 +18,7 @@ package bisq.core.support; import bisq.core.api.CoreMoneroConnectionsService; +import bisq.core.api.CoreNotificationService; import bisq.core.locale.Res; import bisq.core.support.messages.ChatMessage; import bisq.core.support.messages.SupportMessage; @@ -48,6 +49,7 @@ import javax.annotation.Nullable; public abstract class SupportManager { protected final P2PService p2PService; protected final CoreMoneroConnectionsService connectionService; + protected final CoreNotificationService notificationService; protected final Map delayMsgMap = new HashMap<>(); private final CopyOnWriteArraySet decryptedMailboxMessageWithPubKeys = new CopyOnWriteArraySet<>(); private final CopyOnWriteArraySet decryptedDirectMessageWithPubKeys = new CopyOnWriteArraySet<>(); @@ -59,10 +61,11 @@ public abstract class SupportManager { // Constructor /////////////////////////////////////////////////////////////////////////////////////////// - public SupportManager(P2PService p2PService, CoreMoneroConnectionsService connectionService) { + public SupportManager(P2PService p2PService, CoreMoneroConnectionsService connectionService, CoreNotificationService notificationService) { this.p2PService = p2PService; this.connectionService = connectionService; - mailboxMessageService = p2PService.getMailboxMessageService(); + this.mailboxMessageService = p2PService.getMailboxMessageService(); + this.notificationService = notificationService; // We get first the message handler called then the onBootstrapped p2PService.addDecryptedDirectMessageListener((decryptedMessageWithPubKey, senderAddress) -> { @@ -152,6 +155,7 @@ public abstract class SupportManager { PubKeyRing receiverPubKeyRing = getPeerPubKeyRing(chatMessage); addAndPersistChatMessage(chatMessage); + notificationService.sendChatNotification(chatMessage); // We never get a errorMessage in that method (only if we cannot resolve the receiverPubKeyRing but then we // cannot send it anyway) diff --git a/core/src/main/java/bisq/core/support/dispute/DisputeManager.java b/core/src/main/java/bisq/core/support/dispute/DisputeManager.java index 1d3f55a1ce..0ef3068d3e 100644 --- a/core/src/main/java/bisq/core/support/dispute/DisputeManager.java +++ b/core/src/main/java/bisq/core/support/dispute/DisputeManager.java @@ -18,6 +18,7 @@ package bisq.core.support.dispute; import bisq.core.api.CoreMoneroConnectionsService; +import bisq.core.api.CoreNotificationService; import bisq.core.btc.wallet.Restrictions; import bisq.core.btc.wallet.TradeWalletService; import bisq.core.btc.wallet.XmrWalletService; @@ -111,6 +112,7 @@ public abstract class DisputeManager> extends Sup TradeWalletService tradeWalletService, XmrWalletService xmrWalletService, CoreMoneroConnectionsService connectionService, + CoreNotificationService notificationService, TradeManager tradeManager, ClosedTradableManager closedTradableManager, OpenOfferManager openOfferManager, @@ -118,7 +120,7 @@ public abstract class DisputeManager> extends Sup DisputeListService disputeListService, Config config, PriceFeedService priceFeedService) { - super(p2PService, connectionService); + super(p2PService, connectionService, notificationService); this.tradeWalletService = tradeWalletService; this.xmrWalletService = xmrWalletService; @@ -288,17 +290,6 @@ public abstract class DisputeManager> extends Sup return pubKeyRing.equals(dispute.getTraderPubKeyRing()); } - - public Optional 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 /////////////////////////////////////////////////////////////////////////////////////////// @@ -823,6 +814,17 @@ public abstract class DisputeManager> extends Sup .findAny(); } + public Optional findDisputeById(String disputeId) { + T disputeList = getDisputeList(); + if (disputeList == null) { + log.warn("disputes is null"); + return Optional.empty(); + } + return disputeList.stream() + .filter(e -> e.getId().equals(disputeId)) + .findAny(); + } + public Optional findTrade(Dispute dispute) { Optional retVal = tradeManager.getTradeById(dispute.getTradeId()); if (!retVal.isPresent()) { diff --git a/desktop/src/main/java/bisq/desktop/main/support/dispute/DisputeSummaryVerification.java b/core/src/main/java/bisq/core/support/dispute/DisputeSummaryVerification.java similarity index 94% rename from desktop/src/main/java/bisq/desktop/main/support/dispute/DisputeSummaryVerification.java rename to core/src/main/java/bisq/core/support/dispute/DisputeSummaryVerification.java index 184118d0be..1af3f5160b 100644 --- a/desktop/src/main/java/bisq/desktop/main/support/dispute/DisputeSummaryVerification.java +++ b/core/src/main/java/bisq/core/support/dispute/DisputeSummaryVerification.java @@ -15,13 +15,9 @@ * along with Haveno. If not, see . */ -package bisq.desktop.main.support.dispute; +package bisq.core.support.dispute; import bisq.core.locale.Res; -import bisq.core.support.dispute.Dispute; -import bisq.core.support.dispute.DisputeList; -import bisq.core.support.dispute.DisputeManager; -import bisq.core.support.dispute.DisputeResult; import bisq.core.support.dispute.agent.DisputeAgent; import bisq.core.support.dispute.mediation.mediator.MediatorManager; import bisq.core.support.dispute.refund.refundagent.RefundAgentManager; diff --git a/core/src/main/java/bisq/core/support/dispute/arbitration/ArbitrationManager.java b/core/src/main/java/bisq/core/support/dispute/arbitration/ArbitrationManager.java index 473f344462..0f02994469 100644 --- a/core/src/main/java/bisq/core/support/dispute/arbitration/ArbitrationManager.java +++ b/core/src/main/java/bisq/core/support/dispute/arbitration/ArbitrationManager.java @@ -18,6 +18,7 @@ package bisq.core.support.dispute.arbitration; import bisq.core.api.CoreMoneroConnectionsService; +import bisq.core.api.CoreNotificationService; import bisq.core.btc.wallet.TradeWalletService; import bisq.core.btc.wallet.XmrWalletService; import bisq.core.locale.Res; @@ -60,8 +61,6 @@ import bisq.common.crypto.PubKeyRing; import com.google.inject.Inject; import com.google.inject.Singleton; -import com.google.common.base.Preconditions; - import java.math.BigInteger; import java.util.Arrays; @@ -96,6 +95,7 @@ public final class ArbitrationManager extends DisputeManager disputeOptional = findOwnDispute(tradeId); + Optional disputeOptional = findDispute(tradeId); if (!disputeOptional.isPresent()) { log.debug("We got a peerPublishedPayoutTxMessage but we don't have a matching dispute. TradeId = " + tradeId); if (!delayMsgMap.containsKey(uid)) { @@ -473,7 +474,7 @@ public final class ArbitrationManager extends DisputeManager disputeOptional = findOwnDispute(tradeId); + Optional disputeOptional = findDispute(tradeId); if (!disputeOptional.isPresent()) { log.warn("Trader has no dispute when signing dispute payout tx. This should never happen. TradeId = " + tradeId); return; @@ -579,86 +580,54 @@ public final class ArbitrationManager extends DisputeManager 0) txConfig.addDestination(winnerPayoutAddress, winnerPayoutAmount.multiply(BigInteger.valueOf(9)).divide(BigInteger.valueOf(10))); // reduce payment amount to get fee of similar tx + if (loserPayoutAmount.compareTo(BigInteger.ZERO) > 0) txConfig.addDestination(loserPayoutAddress, loserPayoutAmount.multiply(BigInteger.valueOf(9)).divide(BigInteger.valueOf(10))); + MoneroTxWallet feeEstimateTx = multisigWallet.createTx(txConfig); -// Offer offer = new Offer(contract.getOfferPayload()); -// System.out.println("Buyer deposit tx fee: " + - - //System.out.println("sellerPayoutAddress: " + sellerPayoutAddress); - //System.out.println("sellerPayoutAmount: " + sellerPayoutAmount); - //System.out.println("Multisig balance: " + multisigWallet.getBalance()); - //System.out.println("Multisig unlocked balance: " + multisigWallet.getUnlockedBalance()); - //System.out.println("Multisig txs"); - //System.out.println(multisigWallet.getTxs(new MoneroTxQuery().setIncludeOutputs(true))); - - // create transaction to get fee estimate - if (multisigWallet.isMultisigImportNeeded()) { - log.info("Arbitrator's wallet needs updated multisig hex to create payout tx which means a trader must have already broadcast the payout tx"); - return null; - } - - // TODO (woodser): include arbitration fee - //System.out.println("Creating feeEstimateTx!"); - MoneroTxConfig txConfig = new MoneroTxConfig().setAccountIndex(0).setRelay(false); - if (buyerPayoutAmount.compareTo(BigInteger.ZERO) == 1) txConfig.addDestination(buyerPayoutAddress, buyerPayoutAmount.multiply(BigInteger.valueOf(4)).divide(BigInteger.valueOf(5))); // reduce payment amount to compute fee of similar tx - if (sellerPayoutAmount.compareTo(BigInteger.ZERO) == 1) txConfig.addDestination(sellerPayoutAddress, sellerPayoutAmount.multiply(BigInteger.valueOf(4)).divide(BigInteger.valueOf(5))); - MoneroTxWallet feeEstimateTx = multisigWallet.createTx(txConfig); - - System.out.println("Created fee estimate tx!"); - System.out.println(feeEstimateTx); - //BigInteger estimatedFee = feeEstimateTx.getFee(); - - // attempt to create payout tx by increasing estimated fee until successful - MoneroTxWallet payoutTx = null; - int numAttempts = 0; - int feeDivisor = 0; // adjust fee divisor based on number of payout destinations - if (buyerPayoutAmount.compareTo(BigInteger.ZERO) == 1) feeDivisor += 1; - if (sellerPayoutAmount.compareTo(BigInteger.ZERO) == 1) feeDivisor += 1; - - while (payoutTx == null && numAttempts < 50) { - BigInteger feeEstimate = feeEstimateTx.getFee().add(feeEstimateTx.getFee().multiply(BigInteger.valueOf(numAttempts)).divide(BigInteger.valueOf(10))); // add 1/10 of fee until tx is successful - txConfig = new MoneroTxConfig().setAccountIndex(0).setRelay(false); - if (buyerPayoutAmount.compareTo(BigInteger.ZERO) == 1) txConfig.addDestination(buyerPayoutAddress, buyerPayoutAmount.subtract(feeEstimate.divide(BigInteger.valueOf(feeDivisor)))); // split fee subtracted from each payout amount - if (sellerPayoutAmount.compareTo(BigInteger.ZERO) == 1) txConfig.addDestination(sellerPayoutAddress, sellerPayoutAmount.subtract(feeEstimate.divide(BigInteger.valueOf(feeDivisor)))); - try { + // create payout tx by increasing estimated fee until successful + MoneroTxWallet payoutTx = null; + int numAttempts = 0; + while (payoutTx == null && numAttempts < 50) { + BigInteger feeEstimate = feeEstimateTx.getFee().add(feeEstimateTx.getFee().multiply(BigInteger.valueOf(numAttempts)).divide(BigInteger.valueOf(10))); // add 1/10th of fee until tx is successful + txConfig = new MoneroTxConfig().setAccountIndex(0).setRelay(false); + if (winnerPayoutAmount.compareTo(BigInteger.ZERO) > 0) txConfig.addDestination(winnerPayoutAddress, winnerPayoutAmount.subtract(loserPayoutAmount.equals(BigInteger.ZERO) ? feeEstimate : BigInteger.ZERO)); // winner only pays fee if loser gets 0 + if (loserPayoutAmount.compareTo(BigInteger.ZERO) > 0) { + if (loserPayoutAmount.compareTo(feeEstimate) < 0) throw new RuntimeException("Loser payout is too small to cover the mining fee"); + if (loserPayoutAmount.compareTo(feeEstimate) > 0) txConfig.addDestination(loserPayoutAddress, loserPayoutAmount.subtract(feeEstimate)); // loser pays fee + } numAttempts++; - payoutTx = multisigWallet.createTx(txConfig); - } catch (MoneroError e) { - // exception expected // TODO: better way of estimating fee? + try { + payoutTx = multisigWallet.createTx(txConfig); + } catch (MoneroError e) { + // exception expected // TODO: better way of estimating fee? + } } - } - - if (payoutTx == null) throw new RuntimeException("Failed to generate dispute payout tx"); - System.out.println("DISPUTE PAYOUT TX GENERATED ON ATTEMPT " + numAttempts); - System.out.println(payoutTx); - return payoutTx; + if (payoutTx == null) throw new RuntimeException("Failed to generate dispute payout tx after " + numAttempts + " attempts"); + log.info("Dispute payout transaction generated on attempt {}: {}", numAttempts, payoutTx); + return payoutTx; } private MoneroTxSet traderSignsDisputePayoutTx(String tradeId, String payoutTxHex) { // gather trade info MoneroWallet multisigWallet = xmrWalletService.getMultisigWallet(tradeId); - Optional disputeOptional = findOwnDispute(tradeId); + Optional disputeOptional = findDispute(tradeId); if (!disputeOptional.isPresent()) throw new RuntimeException("Trader has no dispute when signing dispute payout tx. This should never happen. TradeId = " + tradeId); Dispute dispute = disputeOptional.get(); Contract contract = dispute.getContract(); @@ -668,10 +637,6 @@ public final class ArbitrationManager extends DisputeManager TradeWalletService tradeWalletService, XmrWalletService walletService, CoreMoneroConnectionsService connectionService, + CoreNotificationService notificationService, TradeManager tradeManager, ClosedTradableManager closedTradableManager, OpenOfferManager openOfferManager, @@ -85,7 +87,7 @@ public final class MediationManager extends DisputeManager MediationDisputeListService mediationDisputeListService, Config config, PriceFeedService priceFeedService) { - super(p2PService, tradeWalletService, walletService, connectionService, tradeManager, closedTradableManager, + super(p2PService, tradeWalletService, walletService, connectionService, notificationService, tradeManager, closedTradableManager, openOfferManager, keyRing, mediationDisputeListService, config, priceFeedService); } diff --git a/core/src/main/java/bisq/core/support/dispute/refund/RefundManager.java b/core/src/main/java/bisq/core/support/dispute/refund/RefundManager.java index 966640950a..064a9d195b 100644 --- a/core/src/main/java/bisq/core/support/dispute/refund/RefundManager.java +++ b/core/src/main/java/bisq/core/support/dispute/refund/RefundManager.java @@ -18,6 +18,7 @@ package bisq.core.support.dispute.refund; import bisq.core.api.CoreMoneroConnectionsService; +import bisq.core.api.CoreNotificationService; import bisq.core.btc.wallet.TradeWalletService; import bisq.core.btc.wallet.XmrWalletService; import bisq.core.locale.Res; @@ -72,6 +73,7 @@ public final class RefundManager extends DisputeManager { TradeWalletService tradeWalletService, XmrWalletService walletService, CoreMoneroConnectionsService connectionService, + CoreNotificationService notificationService, TradeManager tradeManager, ClosedTradableManager closedTradableManager, OpenOfferManager openOfferManager, @@ -80,7 +82,7 @@ public final class RefundManager extends DisputeManager { RefundDisputeListService refundDisputeListService, Config config, PriceFeedService priceFeedService) { - super(p2PService, tradeWalletService, walletService, connectionService, tradeManager, closedTradableManager, + super(p2PService, tradeWalletService, walletService, connectionService, notificationService, tradeManager, closedTradableManager, openOfferManager, keyRing, refundDisputeListService, config, priceFeedService); } diff --git a/core/src/main/java/bisq/core/support/messages/ChatMessage.java b/core/src/main/java/bisq/core/support/messages/ChatMessage.java index 489ce0aefd..f0ef20221c 100644 --- a/core/src/main/java/bisq/core/support/messages/ChatMessage.java +++ b/core/src/main/java/bisq/core/support/messages/ChatMessage.java @@ -205,9 +205,7 @@ public final class ChatMessage extends SupportMessage { notifyChangeListener(); } - // We cannot rename protobuf definition because it would break backward compatibility - @Override - public protobuf.NetworkEnvelope toProtoNetworkEnvelope() { + public protobuf.ChatMessage.Builder toProtoChatMessageBuilder() { protobuf.ChatMessage.Builder builder = protobuf.ChatMessage.newBuilder() .setType(SupportType.toProtoMessage(supportType)) .setTradeId(tradeId) @@ -225,6 +223,14 @@ public final class ChatMessage extends SupportMessage { .setWasDisplayed(wasDisplayed); Optional.ofNullable(sendMessageErrorProperty.get()).ifPresent(builder::setSendMessageError); Optional.ofNullable(ackErrorProperty.get()).ifPresent(builder::setAckError); + + return builder; + } + + // We cannot rename protobuf definition because it would break backward compatibility + @Override + public protobuf.NetworkEnvelope toProtoNetworkEnvelope() { + protobuf.ChatMessage.Builder builder = toProtoChatMessageBuilder(); return getNetworkEnvelopeBuilder() .setChatMessage(builder) .build(); diff --git a/core/src/main/java/bisq/core/support/traderchat/TradeChatSession.java b/core/src/main/java/bisq/core/support/traderchat/TradeChatSession.java index 1f4b3b2900..6fe34b8c66 100644 --- a/core/src/main/java/bisq/core/support/traderchat/TradeChatSession.java +++ b/core/src/main/java/bisq/core/support/traderchat/TradeChatSession.java @@ -21,8 +21,6 @@ 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; diff --git a/core/src/main/java/bisq/core/support/traderchat/TraderChatManager.java b/core/src/main/java/bisq/core/support/traderchat/TraderChatManager.java index 898c5ad997..a30448b228 100644 --- a/core/src/main/java/bisq/core/support/traderchat/TraderChatManager.java +++ b/core/src/main/java/bisq/core/support/traderchat/TraderChatManager.java @@ -18,6 +18,7 @@ package bisq.core.support.traderchat; import bisq.core.api.CoreMoneroConnectionsService; +import bisq.core.api.CoreNotificationService; import bisq.core.locale.Res; import bisq.core.support.SupportManager; import bisq.core.support.SupportType; @@ -57,9 +58,10 @@ public class TraderChatManager extends SupportManager { @Inject public TraderChatManager(P2PService p2PService, CoreMoneroConnectionsService connectionService, + CoreNotificationService notificationService, TradeManager tradeManager, PubKeyRingProvider pubKeyRingProvider) { - super(p2PService, connectionService); + super(p2PService, connectionService, notificationService); this.tradeManager = tradeManager; this.pubKeyRingProvider = pubKeyRingProvider; } diff --git a/core/src/main/java/bisq/core/util/ParsingUtils.java b/core/src/main/java/bisq/core/util/ParsingUtils.java index b248b48419..f88fc65a15 100644 --- a/core/src/main/java/bisq/core/util/ParsingUtils.java +++ b/core/src/main/java/bisq/core/util/ParsingUtils.java @@ -21,12 +21,13 @@ public class ParsingUtils { * Multiplier to convert centineros (the base XMR unit of Coin) to atomic units. * * TODO: change base unit to atomic units and long + * TODO: move these static utilities? */ private static BigInteger CENTINEROS_AU_MULTIPLIER = BigInteger.valueOf(10000); - + /** * Convert Coin (denominated in centineros) to atomic units. - * + * * @param coin has an amount denominated in centineros * @return BigInteger the coin amount denominated in atomic units */ @@ -43,7 +44,7 @@ public class ParsingUtils { public static BigInteger centinerosToAtomicUnits(long centineros) { return BigInteger.valueOf(centineros).multiply(ParsingUtils.CENTINEROS_AU_MULTIPLIER); } - + /** * Convert atomic units to centineros. * diff --git a/daemon/src/main/java/bisq/daemon/grpc/GrpcDisputesService.java b/daemon/src/main/java/bisq/daemon/grpc/GrpcDisputesService.java new file mode 100644 index 0000000000..7d28c26676 --- /dev/null +++ b/daemon/src/main/java/bisq/daemon/grpc/GrpcDisputesService.java @@ -0,0 +1,156 @@ +package bisq.daemon.grpc; + +import bisq.core.api.CoreApi; +import bisq.core.support.dispute.Attachment; +import bisq.core.support.dispute.DisputeResult; +import bisq.core.util.ParsingUtils; + +import bisq.common.proto.ProtoUtil; + +import bisq.proto.grpc.DisputesGrpc.DisputesImplBase; +import bisq.proto.grpc.GetDisputeReply; +import bisq.proto.grpc.GetDisputeRequest; +import bisq.proto.grpc.GetDisputesReply; +import bisq.proto.grpc.GetDisputesRequest; +import bisq.proto.grpc.OpenDisputeReply; +import bisq.proto.grpc.OpenDisputeRequest; +import bisq.proto.grpc.ResolveDisputeReply; +import bisq.proto.grpc.ResolveDisputeRequest; +import bisq.proto.grpc.SendDisputeChatMessageReply; +import bisq.proto.grpc.SendDisputeChatMessageRequest; + +import io.grpc.ServerInterceptor; +import io.grpc.stub.StreamObserver; + +import javax.inject.Inject; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Optional; +import java.util.stream.Collectors; + +import lombok.extern.slf4j.Slf4j; + +import static bisq.daemon.grpc.interceptor.GrpcServiceRateMeteringConfig.getCustomRateMeteringInterceptor; +import static bisq.proto.grpc.DisputesGrpc.getGetDisputeMethod; +import static bisq.proto.grpc.DisputesGrpc.getGetDisputesMethod; +import static bisq.proto.grpc.DisputesGrpc.getOpenDisputeMethod; +import static bisq.proto.grpc.DisputesGrpc.getResolveDisputeMethod; +import static bisq.proto.grpc.DisputesGrpc.getSendDisputeChatMessageMethod; +import static java.util.concurrent.TimeUnit.SECONDS; + +import bisq.daemon.grpc.interceptor.CallRateMeteringInterceptor; +import bisq.daemon.grpc.interceptor.GrpcCallRateMeter; + +@Slf4j +public class GrpcDisputesService extends DisputesImplBase { + + private final CoreApi coreApi; + private final GrpcExceptionHandler exceptionHandler; + + @Inject + public GrpcDisputesService(CoreApi coreApi, GrpcExceptionHandler exceptionHandler) { + this.coreApi = coreApi; + this.exceptionHandler = exceptionHandler; + } + + @Override + public void openDispute(OpenDisputeRequest req, StreamObserver responseObserver) { + try { + coreApi.openDispute(req.getTradeId(), + () -> { + var reply = OpenDisputeReply.newBuilder().build(); + responseObserver.onNext(reply); + responseObserver.onCompleted(); + }, + (errorMessage, throwable) -> { + log.info("Error in openDispute" + errorMessage); + exceptionHandler.handleException(log, throwable, responseObserver); + }); + } catch (Throwable cause) { + exceptionHandler.handleException(log, cause, responseObserver); + } + } + + @Override + public void getDispute(GetDisputeRequest req, StreamObserver responseObserver) { + try { + var dispute = coreApi.getDispute(req.getTradeId()); + var reply = GetDisputeReply.newBuilder() + .setDispute(dispute.toProtoMessage()) + .build(); + responseObserver.onNext(reply); + responseObserver.onCompleted(); + } catch (Throwable cause) { + exceptionHandler.handleException(log, cause, responseObserver); + } + } + + @Override + public void getDisputes(GetDisputesRequest req, StreamObserver responseObserver) { + try { + var disputes = coreApi.getDisputes(); + var disputesProtobuf = disputes.stream() + .map(d -> d.toProtoMessage()) + .collect(Collectors.toList()); + var reply = GetDisputesReply.newBuilder() + .addAllDisputes(disputesProtobuf) + .build(); + responseObserver.onNext(reply); + responseObserver.onCompleted(); + } catch (Throwable cause) { + exceptionHandler.handleException(log, cause, responseObserver); + } + } + + @Override + public void resolveDispute(ResolveDisputeRequest req, StreamObserver responseObserver) { + try { + var winner = ProtoUtil.enumFromProto(DisputeResult.Winner.class, req.getWinner().name()); + var reason = ProtoUtil.enumFromProto(DisputeResult.Reason.class, req.getReason().name()); + // scale atomic unit to centineros for consistency TODO switch base to atomic units? + var customPayoutAmount = ParsingUtils.atomicUnitsToCentineros(req.getCustomPayoutAmount()); + coreApi.resolveDispute(req.getTradeId(), winner, reason, req.getSummaryNotes(), customPayoutAmount); + var reply = ResolveDisputeReply.newBuilder().build(); + responseObserver.onNext(reply); + responseObserver.onCompleted(); + } catch (Throwable cause) { + exceptionHandler.handleException(log, cause, responseObserver); + } + } + + @Override + public void sendDisputeChatMessage(SendDisputeChatMessageRequest req, + StreamObserver responseObserver) { + try { + var attachmentsProto = req.getAttachmentsList(); + var attachments = attachmentsProto.stream().map(a -> Attachment.fromProto(a)) + .collect(Collectors.toList()); + coreApi.sendDisputeChatMessage(req.getDisputeId(), req.getMessage(), new ArrayList(attachments)); + var reply = SendDisputeChatMessageReply.newBuilder().build(); + responseObserver.onNext(reply); + responseObserver.onCompleted(); + } catch (Throwable cause) { + exceptionHandler.handleException(log, cause, responseObserver); + } + } + + final ServerInterceptor[] interceptors() { + Optional rateMeteringInterceptor = rateMeteringInterceptor(); + return rateMeteringInterceptor.map(serverInterceptor -> + new ServerInterceptor[]{serverInterceptor}).orElseGet(() -> new ServerInterceptor[0]); + } + + final Optional rateMeteringInterceptor() { + return getCustomRateMeteringInterceptor(coreApi.getConfig().appDataDir, this.getClass()) + .or(() -> Optional.of(CallRateMeteringInterceptor.valueOf( + new HashMap<>() {{ + put(getGetDisputeMethod().getFullMethodName(), new GrpcCallRateMeter(10, SECONDS)); + put(getGetDisputesMethod().getFullMethodName(), new GrpcCallRateMeter(10, SECONDS)); + put(getResolveDisputeMethod().getFullMethodName(), new GrpcCallRateMeter(10, SECONDS)); + put(getOpenDisputeMethod().getFullMethodName(), new GrpcCallRateMeter(10, SECONDS)); + put(getSendDisputeChatMessageMethod().getFullMethodName(), new GrpcCallRateMeter(10, SECONDS)); + }} + ))); + } +} diff --git a/daemon/src/main/java/bisq/daemon/grpc/GrpcServer.java b/daemon/src/main/java/bisq/daemon/grpc/GrpcServer.java index 3d91582398..549468bdff 100644 --- a/daemon/src/main/java/bisq/daemon/grpc/GrpcServer.java +++ b/daemon/src/main/java/bisq/daemon/grpc/GrpcServer.java @@ -51,6 +51,7 @@ public class GrpcServer { PasswordAuthInterceptor passwordAuthInterceptor, GrpcAccountService accountService, GrpcDisputeAgentsService disputeAgentsService, + GrpcDisputesService disputesService, GrpcHelpService helpService, GrpcOffersService offersService, GrpcPaymentAccountsService paymentAccountsService, @@ -66,6 +67,7 @@ public class GrpcServer { .executor(UserThread.getExecutor()) .addService(interceptForward(accountService, accountService.interceptors())) .addService(interceptForward(disputeAgentsService, disputeAgentsService.interceptors())) + .addService(interceptForward(disputesService, disputesService.interceptors())) .addService(interceptForward(helpService, helpService.interceptors())) .addService(interceptForward(offersService, offersService.interceptors())) .addService(interceptForward(paymentAccountsService, paymentAccountsService.interceptors())) diff --git a/desktop/src/main/java/bisq/desktop/main/overlays/notifications/NotificationCenter.java b/desktop/src/main/java/bisq/desktop/main/overlays/notifications/NotificationCenter.java index ee349f2853..0ac2698029 100644 --- a/desktop/src/main/java/bisq/desktop/main/overlays/notifications/NotificationCenter.java +++ b/desktop/src/main/java/bisq/desktop/main/overlays/notifications/NotificationCenter.java @@ -228,8 +228,8 @@ public class NotificationCenter { private void onDisputeStateChanged(Trade trade, Trade.DisputeState disputeState) { String message = null; - if (refundManager.findOwnDispute(trade.getId()).isPresent()) { - String disputeOrTicket = refundManager.findOwnDispute(trade.getId()).get().isSupportTicket() ? + if (refundManager.findDispute(trade.getId()).isPresent()) { + String disputeOrTicket = refundManager.findDispute(trade.getId()).get().isSupportTicket() ? Res.get("shared.supportTicket") : Res.get("shared.dispute"); switch (disputeState) { @@ -253,8 +253,8 @@ public class NotificationCenter { if (message != null) { goToSupport(trade, message, false); } - } else if (mediationManager.findOwnDispute(trade.getId()).isPresent()) { - String disputeOrTicket = mediationManager.findOwnDispute(trade.getId()).get().isSupportTicket() ? + } else if (mediationManager.findDispute(trade.getId()).isPresent()) { + String disputeOrTicket = mediationManager.findDispute(trade.getId()).get().isSupportTicket() ? Res.get("shared.supportTicket") : Res.get("shared.mediationCase"); switch (disputeState) { diff --git a/desktop/src/main/java/bisq/desktop/main/overlays/windows/DisputeSummaryWindow.java b/desktop/src/main/java/bisq/desktop/main/overlays/windows/DisputeSummaryWindow.java index 51fd5e749c..31f8428917 100644 --- a/desktop/src/main/java/bisq/desktop/main/overlays/windows/DisputeSummaryWindow.java +++ b/desktop/src/main/java/bisq/desktop/main/overlays/windows/DisputeSummaryWindow.java @@ -23,7 +23,8 @@ import bisq.desktop.components.HavenoTextArea; import bisq.desktop.components.InputTextField; import bisq.desktop.main.overlays.Overlay; import bisq.desktop.main.overlays.popups.Popup; -import bisq.desktop.main.support.dispute.DisputeSummaryVerification; + +import bisq.core.api.CoreDisputesService; import bisq.desktop.util.DisplayUtils; import bisq.desktop.util.Layout; @@ -49,7 +50,6 @@ import bisq.common.handlers.ResultHandler; import bisq.common.util.Tuple2; import bisq.common.util.Tuple3; -import org.bitcoinj.core.AddressFormatException; import org.bitcoinj.core.Coin; import com.google.inject.Inject; @@ -86,11 +86,6 @@ import static bisq.desktop.util.FormBuilder.addTitledGroupBg; import static bisq.desktop.util.FormBuilder.addTopLabelWithVBox; import static com.google.common.base.Preconditions.checkNotNull; - - -import monero.wallet.MoneroWallet; -import monero.wallet.model.MoneroTxWallet; - @Slf4j public class DisputeSummaryWindow extends Overlay { private final CoinFormatter formatter; @@ -98,8 +93,8 @@ public class DisputeSummaryWindow extends Overlay { private final MediationManager mediationManager; private final XmrWalletService walletService; private final TradeWalletService tradeWalletService; // TODO (woodser): remove for xmr or adapt to get/create multisig wallets for tx creation utils + private final CoreDisputesService disputesService; private Dispute dispute; - private Optional finalizeDisputeHandlerOptional = Optional.empty(); private ToggleGroup tradeAmountToggleGroup, reasonToggleGroup; private DisputeResult disputeResult; private RadioButton buyerGetsTradeAmountRadioButton, sellerGetsTradeAmountRadioButton, @@ -132,13 +127,15 @@ public class DisputeSummaryWindow extends Overlay { ArbitrationManager arbitrationManager, MediationManager mediationManager, XmrWalletService walletService, - TradeWalletService tradeWalletService) { + TradeWalletService tradeWalletService, + CoreDisputesService disputesService) { this.formatter = formatter; this.arbitrationManager = arbitrationManager; this.mediationManager = mediationManager; this.walletService = walletService; this.tradeWalletService = tradeWalletService; + this.disputesService = disputesService; type = Type.Confirmation; } @@ -159,12 +156,6 @@ public class DisputeSummaryWindow extends Overlay { } } - public DisputeSummaryWindow onFinalizeDispute(Runnable finalizeDisputeHandler) { - this.finalizeDisputeHandlerOptional = Optional.of(finalizeDisputeHandler); - return this; - } - - /////////////////////////////////////////////////////////////////////////////////////////// // Protected /////////////////////////////////////////////////////////////////////////////////////////// @@ -598,39 +589,7 @@ public class DisputeSummaryWindow extends Overlay { Button cancelButton = tuple.second; closeTicketButton.setOnAction(e -> { - - // TODO (woodser): create disputed payout tx after showing payout tx confirmation, within doCloseIfValid() (see upstream/master) - if (!dispute.isMediationDispute()) { - try { - System.out.println(disputeResult); - MoneroWallet multisigWallet = walletService.getMultisigWallet(dispute.getTradeId()); - //dispute.getContract().getArbitratorPubKeyRing(); // TODO: support arbitrator pub key ring in contract? - //disputeResult.setArbitratorPubKey(arbitratorAddressEntry.getPubKey()); - - // TODO (woodser): don't send signed tx if opener is not co-signer? - // // determine if opener is co-signer - // boolean openerIsWinner = (contract.getBuyerPubKeyRing().equals(dispute.getTraderPubKeyRing()) && disputeResult.getWinner() == Winner.BUYER) || (contract.getSellerPubKeyRing().equals(dispute.getTraderPubKeyRing()) && disputeResult.getWinner() == Winner.SELLER); - // boolean openerIsCosigner = openerIsWinner || disputeResult.isLoserPublisher(); - // if (!openerIsCosigner) throw new RuntimeException("Need to query non-opener for updated multisig hex before creating tx"); - - // arbitrator creates and signs dispute payout tx if dispute is in context of opener, otherwise opener's peer must request payout tx by providing updated multisig hex - boolean isOpener = dispute.isOpener(); - System.out.println("Is dispute opener: " + isOpener); - if (isOpener) { - MoneroTxWallet arbitratorPayoutTx = ArbitrationManager.arbitratorCreatesDisputedPayoutTx(contract, dispute, disputeResult, multisigWallet); - System.out.println("Created arbitrator-signed payout tx: " + arbitratorPayoutTx); - if (arbitratorPayoutTx != null) disputeResult.setArbitratorSignedPayoutTxHex(arbitratorPayoutTx.getTxSet().getMultisigTxHex()); - } - - // send arbitrator's updated multisig hex with dispute result - disputeResult.setArbitratorUpdatedMultisigHex(multisigWallet.getMultisigHex()); - } catch (AddressFormatException e2) { - log.error("Error at close dispute", e2); - return; - } - } - -// // TODO (woodser): handle with showPayoutTxConfirmation() / doCloseIfValid() in order to have confirmation window (see upstream/master) + disputesService.resolveDisputePayout(dispute, disputeResult, contract); doClose(closeTicketButton); // if (dispute.getDepositTxSerialized() == null) { @@ -801,49 +760,12 @@ public class DisputeSummaryWindow extends Overlay { return; } + summaryNotesTextArea.textProperty().unbindBidirectional(disputeResult.summaryNotesProperty()); + boolean isRefundAgent = disputeManager instanceof RefundManager; disputeResult.setLoserPublisher(isLoserPublisherCheckBox.isSelected()); disputeResult.setCloseDate(new Date()); - dispute.setDisputeResult(disputeResult); - dispute.setIsClosed(); - DisputeResult.Reason reason = disputeResult.getReason(); - - summaryNotesTextArea.textProperty().unbindBidirectional(disputeResult.summaryNotesProperty()); - - String role = isRefundAgent ? Res.get("shared.refundAgent") : Res.get("shared.mediator"); - String agentNodeAddress = checkNotNull(disputeManager.getAgentNodeAddress(dispute)).getFullAddress(); - Contract contract = dispute.getContract(); - String currencyCode = contract.getOfferPayload().getCurrencyCode(); - String amount = formatter.formatCoinWithCode(contract.getTradeAmount()); - - String textToSign = Res.get("disputeSummaryWindow.close.msg", - DisplayUtils.formatDateTime(disputeResult.getCloseDate()), - role, - agentNodeAddress, - dispute.getShortTradeId(), - currencyCode, - amount, - formatter.formatCoinWithCode(disputeResult.getBuyerPayoutAmount()), - formatter.formatCoinWithCode(disputeResult.getSellerPayoutAmount()), - Res.get("disputeSummaryWindow.reason." + reason.name()), - disputeResult.summaryNotesProperty().get() - ); - - if (reason == DisputeResult.Reason.OPTION_TRADE && - dispute.getChatMessages().size() > 1 && - dispute.getChatMessages().get(1).isSystemMessage()) { - textToSign += "\n" + dispute.getChatMessages().get(1).getMessage() + "\n"; - } - - String summaryText = DisputeSummaryVerification.signAndApply(disputeManager, disputeResult, textToSign); - - if (isRefundAgent) { - summaryText += Res.get("disputeSummaryWindow.close.nextStepsForRefundAgentArbitration"); - } else { - summaryText += Res.get("disputeSummaryWindow.close.nextStepsForMediation"); - } - - disputeManager.sendDisputeResultMessage(disputeResult, dispute, summaryText); + disputesService.closeDispute(disputeManager, dispute, disputeResult, isRefundAgent); if (peersDisputeOptional.isPresent() && !peersDisputeOptional.get().isClosed() && !DevEnv.isDevMode()) { UserThread.runAfter(() -> new Popup() @@ -852,12 +774,8 @@ public class DisputeSummaryWindow extends Overlay { 200, TimeUnit.MILLISECONDS); } - finalizeDisputeHandlerOptional.ifPresent(Runnable::run); - disputeManager.requestPersistence(); - closeTicketButton.disableProperty().unbind(); - hide(); } @@ -878,33 +796,24 @@ public class DisputeSummaryWindow extends Overlay { } private void applyPayoutAmountsToDisputeResult(Toggle selectedTradeAmountToggle) { - Contract contract = dispute.getContract(); - Offer offer = new Offer(contract.getOfferPayload()); - Coin buyerSecurityDeposit = offer.getBuyerSecurityDeposit(); - Coin sellerSecurityDeposit = offer.getSellerSecurityDeposit(); - Coin tradeAmount = contract.getTradeAmount(); + CoreDisputesService.DisputePayout payout; if (selectedTradeAmountToggle == buyerGetsTradeAmountRadioButton) { - disputeResult.setBuyerPayoutAmount(tradeAmount.add(buyerSecurityDeposit)); - disputeResult.setSellerPayoutAmount(sellerSecurityDeposit); + payout = CoreDisputesService.DisputePayout.BUYER_GETS_TRADE_AMOUNT; disputeResult.setWinner(DisputeResult.Winner.BUYER); } else if (selectedTradeAmountToggle == buyerGetsAllRadioButton) { - disputeResult.setBuyerPayoutAmount(tradeAmount - .add(buyerSecurityDeposit) - .add(sellerSecurityDeposit)); // TODO (woodser): apply min payout to incentivize loser (see post v1.1.7) - disputeResult.setSellerPayoutAmount(Coin.ZERO); + payout = CoreDisputesService.DisputePayout.BUYER_GETS_ALL; disputeResult.setWinner(DisputeResult.Winner.BUYER); } else if (selectedTradeAmountToggle == sellerGetsTradeAmountRadioButton) { - disputeResult.setBuyerPayoutAmount(buyerSecurityDeposit); - disputeResult.setSellerPayoutAmount(tradeAmount.add(sellerSecurityDeposit)); + payout = CoreDisputesService.DisputePayout.SELLER_GETS_TRADE_AMOUNT; disputeResult.setWinner(DisputeResult.Winner.SELLER); } else if (selectedTradeAmountToggle == sellerGetsAllRadioButton) { - disputeResult.setBuyerPayoutAmount(Coin.ZERO); - disputeResult.setSellerPayoutAmount(tradeAmount - .add(sellerSecurityDeposit) - .add(buyerSecurityDeposit)); + payout = CoreDisputesService.DisputePayout.SELLER_GETS_ALL; disputeResult.setWinner(DisputeResult.Winner.SELLER); + } else { + // should not happen + throw new IllegalStateException("Unknown radio button"); } - + disputesService.applyPayoutAmountsToDisputeResult(payout, dispute, disputeResult, -1); buyerPayoutAmountInputTextField.setText(formatter.formatCoin(disputeResult.getBuyerPayoutAmount())); sellerPayoutAmountInputTextField.setText(formatter.formatCoin(disputeResult.getSellerPayoutAmount())); } diff --git a/desktop/src/main/java/bisq/desktop/main/overlays/windows/TradeDetailsWindow.java b/desktop/src/main/java/bisq/desktop/main/overlays/windows/TradeDetailsWindow.java index 8090318703..5392a18802 100644 --- a/desktop/src/main/java/bisq/desktop/main/overlays/windows/TradeDetailsWindow.java +++ b/desktop/src/main/java/bisq/desktop/main/overlays/windows/TradeDetailsWindow.java @@ -204,8 +204,8 @@ public class TradeDetailsWindow extends Overlay { rows++; if (trade.getPayoutTx() != null) rows++; - boolean showDisputedTx = arbitrationManager.findOwnDispute(trade.getId()).isPresent() && - arbitrationManager.findOwnDispute(trade.getId()).get().getDisputePayoutTxId() != null; + boolean showDisputedTx = arbitrationManager.findDispute(trade.getId()).isPresent() && + arbitrationManager.findDispute(trade.getId()).get().getDisputePayoutTxId() != null; if (showDisputedTx) rows++; if (trade.hasFailed()) @@ -301,7 +301,7 @@ public class TradeDetailsWindow extends Overlay { trade.getPayoutTx().getHash()); if (showDisputedTx) addLabelTxIdTextField(gridPane, ++rowIndex, Res.get("tradeDetailsWindow.disputedPayoutTxId"), - arbitrationManager.findOwnDispute(trade.getId()).get().getDisputePayoutTxId()); + arbitrationManager.findDispute(trade.getId()).get().getDisputePayoutTxId()); if (trade.hasFailed()) { textArea = addConfirmationLabelTextArea(gridPane, ++rowIndex, Res.get("shared.errorMessage"), "", 0).second; diff --git a/desktop/src/main/java/bisq/desktop/main/overlays/windows/VerifyDisputeResultSignatureWindow.java b/desktop/src/main/java/bisq/desktop/main/overlays/windows/VerifyDisputeResultSignatureWindow.java index 46ede8240d..f6b6d08b73 100644 --- a/desktop/src/main/java/bisq/desktop/main/overlays/windows/VerifyDisputeResultSignatureWindow.java +++ b/desktop/src/main/java/bisq/desktop/main/overlays/windows/VerifyDisputeResultSignatureWindow.java @@ -18,7 +18,7 @@ package bisq.desktop.main.overlays.windows; import bisq.desktop.main.overlays.Overlay; -import bisq.desktop.main.support.dispute.DisputeSummaryVerification; +import bisq.core.support.dispute.DisputeSummaryVerification; import bisq.core.locale.Res; import bisq.core.support.dispute.mediation.mediator.MediatorManager; diff --git a/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/PendingTradesDataModel.java b/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/PendingTradesDataModel.java index c737c215a8..b3b3723046 100644 --- a/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/PendingTradesDataModel.java +++ b/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/PendingTradesDataModel.java @@ -29,6 +29,7 @@ import bisq.desktop.main.support.dispute.client.mediation.MediationClientView; import bisq.desktop.util.GUIUtil; import bisq.core.account.witness.AccountAgeWitnessService; +import bisq.core.api.CoreDisputesService; import bisq.core.api.CoreMoneroConnectionsService; import bisq.core.btc.wallet.XmrWalletService; import bisq.core.locale.Res; @@ -122,6 +123,7 @@ public class PendingTradesDataModel extends ActivatableDataModel { private Trade selectedTrade; @Getter private final PubKeyRingProvider pubKeyRingProvider; + private final CoreDisputesService disputesService; /////////////////////////////////////////////////////////////////////////////////////////// // Constructor, initialization @@ -141,7 +143,8 @@ public class PendingTradesDataModel extends ActivatableDataModel { Navigation navigation, WalletPasswordWindow walletPasswordWindow, NotificationCenter notificationCenter, - OfferUtil offerUtil) { + OfferUtil offerUtil, + CoreDisputesService disputesService) { this.tradeManager = tradeManager; this.xmrWalletService = xmrWalletService; this.pubKeyRingProvider = pubKeyRingProvider; @@ -156,6 +159,7 @@ public class PendingTradesDataModel extends ActivatableDataModel { this.walletPasswordWindow = walletPasswordWindow; this.notificationCenter = notificationCenter; this.offerUtil = offerUtil; + this.disputesService = disputesService; tradesListChangeListener = change -> onListChanged(); notificationCenter.setSelectItemByTradeIdConsumer(this::selectItemByTradeId); @@ -544,40 +548,12 @@ public class PendingTradesDataModel extends ActivatableDataModel { } else if (useArbitration) { // Only if we have completed mediation we allow arbitration disputeManager = arbitrationManager; - PubKeyRing arbitratorPubKeyRing = trade.getArbitratorPubKeyRing(); - checkNotNull(arbitratorPubKeyRing, "arbitratorPubKeyRing must not be null"); - byte[] depositTxSerialized = null; // depositTx.bitcoinSerialize(); TODO (woodser) - String depositTxHashAsString = null; // depositTx.getHashAsString(); TODO (woodser) - Dispute dispute = new Dispute(new Date().getTime(), - trade.getId(), - pubKeyRingProvider.get().hashCode(), // trader id, - true, - (offer.getDirection() == OfferPayload.Direction.BUY) == isMaker, - isMaker, - pubKeyRingProvider.get(), - trade.getDate().getTime(), - trade.getMaxTradePeriodDate().getTime(), - trade.getContract(), - trade.getContractHash(), - depositTxSerialized, - payoutTxSerialized, - depositTxHashAsString, - payoutTxHashAsString, - trade.getContractAsJson(), - trade.getMaker().getContractSignature(), - trade.getTaker().getContractSignature(), - trade.getMaker().getPaymentAccountPayload(), - trade.getTaker().getPaymentAccountPayload(), - arbitratorPubKeyRing, - isSupportTicket, - SupportType.ARBITRATION); - - trade.setDisputeState(Trade.DisputeState.DISPUTE_REQUESTED); + Dispute dispute = disputesService.createDisputeForTrade(trade, offer, pubKeyRingProvider.get(), isMaker, isSupportTicket); sendOpenNewDisputeMessage(dispute, false, disputeManager, updatedMultisigHex); + tradeManager.requestPersistence(); } else { log.warn("Invalid dispute state {}", disputeState.name()); } - tradeManager.requestPersistence(); } private void sendOpenNewDisputeMessage(Dispute dispute, boolean reOpen, DisputeManager> disputeManager, String senderMultisigHex) { diff --git a/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/steps/TradeStepView.java b/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/steps/TradeStepView.java index 7f0c38f7f6..8037250e95 100644 --- a/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/steps/TradeStepView.java +++ b/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/steps/TradeStepView.java @@ -486,7 +486,7 @@ public abstract class TradeStepView extends AnchorPane { } applyOnDisputeOpened(); - ownDispute = model.dataModel.arbitrationManager.findOwnDispute(trade.getId()); + ownDispute = model.dataModel.arbitrationManager.findDispute(trade.getId()); ownDispute.ifPresent(dispute -> { if (tradeStepInfo != null) tradeStepInfo.setState(TradeStepInfo.State.IN_ARBITRATION_SELF_REQUESTED); @@ -499,7 +499,7 @@ public abstract class TradeStepView extends AnchorPane { } applyOnDisputeOpened(); - ownDispute = model.dataModel.arbitrationManager.findOwnDispute(trade.getId()); + ownDispute = model.dataModel.arbitrationManager.findDispute(trade.getId()); ownDispute.ifPresent(dispute -> { if (tradeStepInfo != null) tradeStepInfo.setState(TradeStepInfo.State.IN_ARBITRATION_PEER_REQUESTED); @@ -513,7 +513,7 @@ public abstract class TradeStepView extends AnchorPane { } applyOnDisputeOpened(); - ownDispute = model.dataModel.mediationManager.findOwnDispute(trade.getId()); + ownDispute = model.dataModel.mediationManager.findDispute(trade.getId()); ownDispute.ifPresent(dispute -> { if (tradeStepInfo != null) tradeStepInfo.setState(TradeStepInfo.State.IN_MEDIATION_SELF_REQUESTED); @@ -525,7 +525,7 @@ public abstract class TradeStepView extends AnchorPane { } applyOnDisputeOpened(); - ownDispute = model.dataModel.mediationManager.findOwnDispute(trade.getId()); + ownDispute = model.dataModel.mediationManager.findDispute(trade.getId()); ownDispute.ifPresent(dispute -> { if (tradeStepInfo != null) { tradeStepInfo.setState(TradeStepInfo.State.IN_MEDIATION_PEER_REQUESTED); diff --git a/proto/src/main/proto/grpc.proto b/proto/src/main/proto/grpc.proto index 46904467b8..3a68ca9271 100644 --- a/proto/src/main/proto/grpc.proto +++ b/proto/src/main/proto/grpc.proto @@ -154,6 +154,82 @@ message RestoreAccountRequest { message RestoreAccountReply { } +/////////////////////////////////////////////////////////////////////////////////////////// +// Disputes +/////////////////////////////////////////////////////////////////////////////////////////// + +service Disputes { + rpc GetDispute (GetDisputeRequest) returns (GetDisputeReply) { + } + rpc GetDisputes (GetDisputesRequest) returns (GetDisputesReply) { + } + rpc OpenDispute (OpenDisputeRequest) returns (OpenDisputeReply) { + } + rpc ResolveDispute (ResolveDisputeRequest) returns (ResolveDisputeReply) { + } + rpc SendDisputeChatMessage (SendDisputeChatMessageRequest) returns (SendDisputeChatMessageReply) { + } +} + +message GetDisputesRequest { +} + +message GetDisputesReply { + repeated Dispute disputes = 1; // pb.proto +} + +message GetDisputeRequest { + string trade_id = 1; +} + +message GetDisputeReply { + Dispute dispute = 1; // pb.proto +} + +message OpenDisputeRequest { + string trade_id = 1; +} + +message OpenDisputeReply { +} + +message ResolveDisputeReply { +} + +message ResolveDisputeRequest { + string trade_id = 1; + DisputeResult.Winner winner = 2; + DisputeResult.Reason reason = 3; + string summary_notes = 4; + uint64 custom_payout_amount = 5 [jstype = JS_STRING]; +} + +message SendDisputeChatMessageRequest { + string dispute_id = 1; + string message = 2; + repeated Attachment attachments = 3; // pb.proto +} + +message SendDisputeChatMessageReply { +} + +/////////////////////////////////////////////////////////////////////////////////////////// +// DisputeAgents +/////////////////////////////////////////////////////////////////////////////////////////// + +service DisputeAgents { + rpc RegisterDisputeAgent (RegisterDisputeAgentRequest) returns (RegisterDisputeAgentReply) { + } +} + +message RegisterDisputeAgentRequest { + string dispute_agent_type = 1; + string registration_key = 2; +} + +message RegisterDisputeAgentReply { +} + /////////////////////////////////////////////////////////////////////////////////////////// // Notifications /////////////////////////////////////////////////////////////////////////////////////////// @@ -306,23 +382,6 @@ message SetAutoSwitchRequest { message SetAutoSwitchReply {} -/////////////////////////////////////////////////////////////////////////////////////////// -// DisputeAgents -/////////////////////////////////////////////////////////////////////////////////////////// - -service DisputeAgents { - rpc RegisterDisputeAgent (RegisterDisputeAgentRequest) returns (RegisterDisputeAgentReply) { - } -} - -message RegisterDisputeAgentRequest { - string dispute_agent_type = 1; - string registration_key = 2; -} - -message RegisterDisputeAgentReply { -} - /////////////////////////////////////////////////////////////////////////////////////////// // Offers /////////////////////////////////////////////////////////////////////////////////////////// @@ -543,7 +602,7 @@ message MarketDepthInfo { repeated double buy_prices = 2; repeated double buy_depth = 3; repeated double sell_prices = 4; - repeated double sell_depth = 5; + repeated double sell_depth = 5; } /////////////////////////////////////////////////////////////////////////////////////////// @@ -950,4 +1009,4 @@ message AddressBalanceInfo { int64 balance = 2; int64 num_confirmations = 3; bool is_address_unused = 4; -} \ No newline at end of file +}