mirror of
https://github.com/haveno-dex/haveno.git
synced 2024-12-22 19:49:32 +00:00
Add API functions to open and resolve disputes (#244)
Co-authored-by: woodser <woodser@protonmail.com>
This commit is contained in:
parent
07c48a04f5
commit
e7b4627102
22 changed files with 752 additions and 306 deletions
|
@ -29,6 +29,9 @@ import bisq.core.offer.OfferPayload;
|
||||||
import bisq.core.offer.OpenOffer;
|
import bisq.core.offer.OpenOffer;
|
||||||
import bisq.core.payment.PaymentAccount;
|
import bisq.core.payment.PaymentAccount;
|
||||||
import bisq.core.payment.payload.PaymentMethod;
|
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.Trade;
|
||||||
import bisq.core.trade.statistics.TradeStatistics3;
|
import bisq.core.trade.statistics.TradeStatistics3;
|
||||||
import bisq.core.trade.statistics.TradeStatisticsManager;
|
import bisq.core.trade.statistics.TradeStatisticsManager;
|
||||||
|
@ -36,6 +39,7 @@ import bisq.common.app.Version;
|
||||||
import bisq.common.config.Config;
|
import bisq.common.config.Config;
|
||||||
import bisq.common.crypto.IncorrectPasswordException;
|
import bisq.common.crypto.IncorrectPasswordException;
|
||||||
import bisq.common.handlers.ErrorMessageHandler;
|
import bisq.common.handlers.ErrorMessageHandler;
|
||||||
|
import bisq.common.handlers.FaultHandler;
|
||||||
import bisq.common.handlers.ResultHandler;
|
import bisq.common.handlers.ResultHandler;
|
||||||
|
|
||||||
import bisq.proto.grpc.NotificationMessage;
|
import bisq.proto.grpc.NotificationMessage;
|
||||||
|
@ -79,6 +83,7 @@ public class CoreApi {
|
||||||
private final AppStartupState appStartupState;
|
private final AppStartupState appStartupState;
|
||||||
private final CoreAccountService coreAccountService;
|
private final CoreAccountService coreAccountService;
|
||||||
private final CoreDisputeAgentsService coreDisputeAgentsService;
|
private final CoreDisputeAgentsService coreDisputeAgentsService;
|
||||||
|
private final CoreDisputesService coreDisputeService;
|
||||||
private final CoreHelpService coreHelpService;
|
private final CoreHelpService coreHelpService;
|
||||||
private final CoreOffersService coreOffersService;
|
private final CoreOffersService coreOffersService;
|
||||||
private final CorePaymentAccountsService paymentAccountsService;
|
private final CorePaymentAccountsService paymentAccountsService;
|
||||||
|
@ -94,6 +99,7 @@ public class CoreApi {
|
||||||
AppStartupState appStartupState,
|
AppStartupState appStartupState,
|
||||||
CoreAccountService coreAccountService,
|
CoreAccountService coreAccountService,
|
||||||
CoreDisputeAgentsService coreDisputeAgentsService,
|
CoreDisputeAgentsService coreDisputeAgentsService,
|
||||||
|
CoreDisputesService coreDisputeService,
|
||||||
CoreHelpService coreHelpService,
|
CoreHelpService coreHelpService,
|
||||||
CoreOffersService coreOffersService,
|
CoreOffersService coreOffersService,
|
||||||
CorePaymentAccountsService paymentAccountsService,
|
CorePaymentAccountsService paymentAccountsService,
|
||||||
|
@ -107,6 +113,7 @@ public class CoreApi {
|
||||||
this.appStartupState = appStartupState;
|
this.appStartupState = appStartupState;
|
||||||
this.coreAccountService = coreAccountService;
|
this.coreAccountService = coreAccountService;
|
||||||
this.coreDisputeAgentsService = coreDisputeAgentsService;
|
this.coreDisputeAgentsService = coreDisputeAgentsService;
|
||||||
|
this.coreDisputeService = coreDisputeService;
|
||||||
this.coreHelpService = coreHelpService;
|
this.coreHelpService = coreHelpService;
|
||||||
this.coreOffersService = coreOffersService;
|
this.coreOffersService = coreOffersService;
|
||||||
this.paymentAccountsService = paymentAccountsService;
|
this.paymentAccountsService = paymentAccountsService;
|
||||||
|
@ -333,6 +340,31 @@ public class CoreApi {
|
||||||
notificationService.sendNotification(notification);
|
notificationService.sendNotification(notification);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// Disputes
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
public List<Dispute> 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<Attachment> attachments) {
|
||||||
|
coreDisputeService.sendDisputeChatMessage(disputeId, message, attachments);
|
||||||
|
}
|
||||||
|
|
||||||
///////////////////////////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////////////////////////
|
||||||
// Dispute Agents
|
// Dispute Agents
|
||||||
///////////////////////////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
335
core/src/main/java/bisq/core/api/CoreDisputesService.java
Normal file
335
core/src/main/java/bisq/core/api/CoreDisputesService.java
Normal file
|
@ -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<Dispute> getDisputes() {
|
||||||
|
return arbitrationManager.getDisputesAsObservableList();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Dispute getDispute(String tradeId) {
|
||||||
|
Optional<Dispute> 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<Attachment> 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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -3,6 +3,7 @@ package bisq.core.api;
|
||||||
import bisq.core.api.CoreApi.NotificationListener;
|
import bisq.core.api.CoreApi.NotificationListener;
|
||||||
import bisq.core.api.model.TradeInfo;
|
import bisq.core.api.model.TradeInfo;
|
||||||
import bisq.core.trade.Trade;
|
import bisq.core.trade.Trade;
|
||||||
|
import bisq.core.support.messages.ChatMessage;
|
||||||
import bisq.proto.grpc.NotificationMessage;
|
import bisq.proto.grpc.NotificationMessage;
|
||||||
import bisq.proto.grpc.NotificationMessage.NotificationType;
|
import bisq.proto.grpc.NotificationMessage.NotificationType;
|
||||||
import javax.inject.Singleton;
|
import javax.inject.Singleton;
|
||||||
|
@ -56,4 +57,12 @@ public class CoreNotificationService {
|
||||||
.setTitle(title)
|
.setTitle(title)
|
||||||
.setMessage(message).build());
|
.setMessage(message).build());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void sendChatNotification(ChatMessage chatMessage) {
|
||||||
|
sendNotification(NotificationMessage.newBuilder()
|
||||||
|
.setType(NotificationType.CHAT_MESSAGE)
|
||||||
|
.setTimestamp(System.currentTimeMillis())
|
||||||
|
.setChatMessage(chatMessage.toProtoChatMessageBuilder())
|
||||||
|
.build());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,6 +18,7 @@
|
||||||
package bisq.core.support;
|
package bisq.core.support;
|
||||||
|
|
||||||
import bisq.core.api.CoreMoneroConnectionsService;
|
import bisq.core.api.CoreMoneroConnectionsService;
|
||||||
|
import bisq.core.api.CoreNotificationService;
|
||||||
import bisq.core.locale.Res;
|
import bisq.core.locale.Res;
|
||||||
import bisq.core.support.messages.ChatMessage;
|
import bisq.core.support.messages.ChatMessage;
|
||||||
import bisq.core.support.messages.SupportMessage;
|
import bisq.core.support.messages.SupportMessage;
|
||||||
|
@ -48,6 +49,7 @@ import javax.annotation.Nullable;
|
||||||
public abstract class SupportManager {
|
public abstract class SupportManager {
|
||||||
protected final P2PService p2PService;
|
protected final P2PService p2PService;
|
||||||
protected final CoreMoneroConnectionsService connectionService;
|
protected final CoreMoneroConnectionsService connectionService;
|
||||||
|
protected final CoreNotificationService notificationService;
|
||||||
protected final Map<String, Timer> delayMsgMap = new HashMap<>();
|
protected final Map<String, Timer> delayMsgMap = new HashMap<>();
|
||||||
private final CopyOnWriteArraySet<DecryptedMessageWithPubKey> decryptedMailboxMessageWithPubKeys = new CopyOnWriteArraySet<>();
|
private final CopyOnWriteArraySet<DecryptedMessageWithPubKey> decryptedMailboxMessageWithPubKeys = new CopyOnWriteArraySet<>();
|
||||||
private final CopyOnWriteArraySet<DecryptedMessageWithPubKey> decryptedDirectMessageWithPubKeys = new CopyOnWriteArraySet<>();
|
private final CopyOnWriteArraySet<DecryptedMessageWithPubKey> decryptedDirectMessageWithPubKeys = new CopyOnWriteArraySet<>();
|
||||||
|
@ -59,10 +61,11 @@ public abstract class SupportManager {
|
||||||
// Constructor
|
// Constructor
|
||||||
///////////////////////////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
public SupportManager(P2PService p2PService, CoreMoneroConnectionsService connectionService) {
|
public SupportManager(P2PService p2PService, CoreMoneroConnectionsService connectionService, CoreNotificationService notificationService) {
|
||||||
this.p2PService = p2PService;
|
this.p2PService = p2PService;
|
||||||
this.connectionService = connectionService;
|
this.connectionService = connectionService;
|
||||||
mailboxMessageService = p2PService.getMailboxMessageService();
|
this.mailboxMessageService = p2PService.getMailboxMessageService();
|
||||||
|
this.notificationService = notificationService;
|
||||||
|
|
||||||
// We get first the message handler called then the onBootstrapped
|
// We get first the message handler called then the onBootstrapped
|
||||||
p2PService.addDecryptedDirectMessageListener((decryptedMessageWithPubKey, senderAddress) -> {
|
p2PService.addDecryptedDirectMessageListener((decryptedMessageWithPubKey, senderAddress) -> {
|
||||||
|
@ -152,6 +155,7 @@ public abstract class SupportManager {
|
||||||
PubKeyRing receiverPubKeyRing = getPeerPubKeyRing(chatMessage);
|
PubKeyRing receiverPubKeyRing = getPeerPubKeyRing(chatMessage);
|
||||||
|
|
||||||
addAndPersistChatMessage(chatMessage);
|
addAndPersistChatMessage(chatMessage);
|
||||||
|
notificationService.sendChatNotification(chatMessage);
|
||||||
|
|
||||||
// We never get a errorMessage in that method (only if we cannot resolve the receiverPubKeyRing but then we
|
// We never get a errorMessage in that method (only if we cannot resolve the receiverPubKeyRing but then we
|
||||||
// cannot send it anyway)
|
// cannot send it anyway)
|
||||||
|
|
|
@ -18,6 +18,7 @@
|
||||||
package bisq.core.support.dispute;
|
package bisq.core.support.dispute;
|
||||||
|
|
||||||
import bisq.core.api.CoreMoneroConnectionsService;
|
import bisq.core.api.CoreMoneroConnectionsService;
|
||||||
|
import bisq.core.api.CoreNotificationService;
|
||||||
import bisq.core.btc.wallet.Restrictions;
|
import bisq.core.btc.wallet.Restrictions;
|
||||||
import bisq.core.btc.wallet.TradeWalletService;
|
import bisq.core.btc.wallet.TradeWalletService;
|
||||||
import bisq.core.btc.wallet.XmrWalletService;
|
import bisq.core.btc.wallet.XmrWalletService;
|
||||||
|
@ -111,6 +112,7 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
|
||||||
TradeWalletService tradeWalletService,
|
TradeWalletService tradeWalletService,
|
||||||
XmrWalletService xmrWalletService,
|
XmrWalletService xmrWalletService,
|
||||||
CoreMoneroConnectionsService connectionService,
|
CoreMoneroConnectionsService connectionService,
|
||||||
|
CoreNotificationService notificationService,
|
||||||
TradeManager tradeManager,
|
TradeManager tradeManager,
|
||||||
ClosedTradableManager closedTradableManager,
|
ClosedTradableManager closedTradableManager,
|
||||||
OpenOfferManager openOfferManager,
|
OpenOfferManager openOfferManager,
|
||||||
|
@ -118,7 +120,7 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
|
||||||
DisputeListService<T> disputeListService,
|
DisputeListService<T> disputeListService,
|
||||||
Config config,
|
Config config,
|
||||||
PriceFeedService priceFeedService) {
|
PriceFeedService priceFeedService) {
|
||||||
super(p2PService, connectionService);
|
super(p2PService, connectionService, notificationService);
|
||||||
|
|
||||||
this.tradeWalletService = tradeWalletService;
|
this.tradeWalletService = tradeWalletService;
|
||||||
this.xmrWalletService = xmrWalletService;
|
this.xmrWalletService = xmrWalletService;
|
||||||
|
@ -288,17 +290,6 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
|
||||||
return pubKeyRing.equals(dispute.getTraderPubKeyRing());
|
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
|
// Message handler
|
||||||
///////////////////////////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
@ -823,6 +814,17 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
|
||||||
.findAny();
|
.findAny();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Optional<Dispute> 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<Trade> findTrade(Dispute dispute) {
|
public Optional<Trade> findTrade(Dispute dispute) {
|
||||||
Optional<Trade> retVal = tradeManager.getTradeById(dispute.getTradeId());
|
Optional<Trade> retVal = tradeManager.getTradeById(dispute.getTradeId());
|
||||||
if (!retVal.isPresent()) {
|
if (!retVal.isPresent()) {
|
||||||
|
|
|
@ -15,13 +15,9 @@
|
||||||
* along with Haveno. If not, see <http://www.gnu.org/licenses/>.
|
* along with Haveno. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package bisq.desktop.main.support.dispute;
|
package bisq.core.support.dispute;
|
||||||
|
|
||||||
import bisq.core.locale.Res;
|
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.agent.DisputeAgent;
|
||||||
import bisq.core.support.dispute.mediation.mediator.MediatorManager;
|
import bisq.core.support.dispute.mediation.mediator.MediatorManager;
|
||||||
import bisq.core.support.dispute.refund.refundagent.RefundAgentManager;
|
import bisq.core.support.dispute.refund.refundagent.RefundAgentManager;
|
|
@ -18,6 +18,7 @@
|
||||||
package bisq.core.support.dispute.arbitration;
|
package bisq.core.support.dispute.arbitration;
|
||||||
|
|
||||||
import bisq.core.api.CoreMoneroConnectionsService;
|
import bisq.core.api.CoreMoneroConnectionsService;
|
||||||
|
import bisq.core.api.CoreNotificationService;
|
||||||
import bisq.core.btc.wallet.TradeWalletService;
|
import bisq.core.btc.wallet.TradeWalletService;
|
||||||
import bisq.core.btc.wallet.XmrWalletService;
|
import bisq.core.btc.wallet.XmrWalletService;
|
||||||
import bisq.core.locale.Res;
|
import bisq.core.locale.Res;
|
||||||
|
@ -60,8 +61,6 @@ import bisq.common.crypto.PubKeyRing;
|
||||||
import com.google.inject.Inject;
|
import com.google.inject.Inject;
|
||||||
import com.google.inject.Singleton;
|
import com.google.inject.Singleton;
|
||||||
|
|
||||||
import com.google.common.base.Preconditions;
|
|
||||||
|
|
||||||
import java.math.BigInteger;
|
import java.math.BigInteger;
|
||||||
|
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
|
@ -96,6 +95,7 @@ public final class ArbitrationManager extends DisputeManager<ArbitrationDisputeL
|
||||||
TradeWalletService tradeWalletService,
|
TradeWalletService tradeWalletService,
|
||||||
XmrWalletService walletService,
|
XmrWalletService walletService,
|
||||||
CoreMoneroConnectionsService connectionService,
|
CoreMoneroConnectionsService connectionService,
|
||||||
|
CoreNotificationService notificationService,
|
||||||
TradeManager tradeManager,
|
TradeManager tradeManager,
|
||||||
ClosedTradableManager closedTradableManager,
|
ClosedTradableManager closedTradableManager,
|
||||||
OpenOfferManager openOfferManager,
|
OpenOfferManager openOfferManager,
|
||||||
|
@ -103,7 +103,7 @@ public final class ArbitrationManager extends DisputeManager<ArbitrationDisputeL
|
||||||
ArbitrationDisputeListService arbitrationDisputeListService,
|
ArbitrationDisputeListService arbitrationDisputeListService,
|
||||||
Config config,
|
Config config,
|
||||||
PriceFeedService priceFeedService) {
|
PriceFeedService priceFeedService) {
|
||||||
super(p2PService, tradeWalletService, walletService, connectionService, tradeManager, closedTradableManager,
|
super(p2PService, tradeWalletService, walletService, connectionService, notificationService, tradeManager, closedTradableManager,
|
||||||
openOfferManager, keyRing, arbitrationDisputeListService, config, priceFeedService);
|
openOfferManager, keyRing, arbitrationDisputeListService, config, priceFeedService);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -286,7 +286,8 @@ public final class ArbitrationManager extends DisputeManager<ArbitrationDisputeL
|
||||||
MoneroTxSet txSet = traderSignsDisputePayoutTx(tradeId, arbitratorSignedPayoutTxHex);
|
MoneroTxSet txSet = traderSignsDisputePayoutTx(tradeId, arbitratorSignedPayoutTxHex);
|
||||||
onTraderSignedDisputePayoutTx(tradeId, txSet);
|
onTraderSignedDisputePayoutTx(tradeId, txSet);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
errorMessage = "Failed to sign dispute payout tx from arbitrator: " + e.getMessage() + ". TradeId = " + tradeId;
|
e.printStackTrace();
|
||||||
|
errorMessage = "Failed to sign dispute payout tx from arbitrator: " + e.getMessage() + ". TradeId = " + tradeId + " SignedPayoutTx = " + arbitratorSignedPayoutTxHex;
|
||||||
log.warn(errorMessage);
|
log.warn(errorMessage);
|
||||||
success = false;
|
success = false;
|
||||||
}
|
}
|
||||||
|
@ -318,7 +319,7 @@ public final class ArbitrationManager extends DisputeManager<ArbitrationDisputeL
|
||||||
// }
|
// }
|
||||||
// catch (AddressFormatException | WalletException e) {
|
// catch (AddressFormatException | WalletException e) {
|
||||||
catch (Exception e) {
|
catch (Exception e) {
|
||||||
errorMessage = "Error at traderSignAndFinalizeDisputedPayoutTx " + e.toString();
|
errorMessage = "Error at traderSignAndFinalizeDisputedPayoutTx: " + e.toString();
|
||||||
log.error(errorMessage, e);
|
log.error(errorMessage, e);
|
||||||
success = false;
|
success = false;
|
||||||
|
|
||||||
|
@ -343,7 +344,7 @@ public final class ArbitrationManager extends DisputeManager<ArbitrationDisputeL
|
||||||
private void onDisputedPayoutTxMessage(PeerPublishedDisputePayoutTxMessage peerPublishedDisputePayoutTxMessage) {
|
private void onDisputedPayoutTxMessage(PeerPublishedDisputePayoutTxMessage peerPublishedDisputePayoutTxMessage) {
|
||||||
String uid = peerPublishedDisputePayoutTxMessage.getUid();
|
String uid = peerPublishedDisputePayoutTxMessage.getUid();
|
||||||
String tradeId = peerPublishedDisputePayoutTxMessage.getTradeId();
|
String tradeId = peerPublishedDisputePayoutTxMessage.getTradeId();
|
||||||
Optional<Dispute> disputeOptional = findOwnDispute(tradeId);
|
Optional<Dispute> disputeOptional = findDispute(tradeId);
|
||||||
if (!disputeOptional.isPresent()) {
|
if (!disputeOptional.isPresent()) {
|
||||||
log.debug("We got a peerPublishedPayoutTxMessage but we don't have a matching dispute. TradeId = " + tradeId);
|
log.debug("We got a peerPublishedPayoutTxMessage but we don't have a matching dispute. TradeId = " + tradeId);
|
||||||
if (!delayMsgMap.containsKey(uid)) {
|
if (!delayMsgMap.containsKey(uid)) {
|
||||||
|
@ -473,7 +474,7 @@ public final class ArbitrationManager extends DisputeManager<ArbitrationDisputeL
|
||||||
|
|
||||||
// gather trade info
|
// gather trade info
|
||||||
MoneroWallet multisigWallet = xmrWalletService.getMultisigWallet(tradeId);
|
MoneroWallet multisigWallet = xmrWalletService.getMultisigWallet(tradeId);
|
||||||
Optional<Dispute> disputeOptional = findOwnDispute(tradeId);
|
Optional<Dispute> disputeOptional = findDispute(tradeId);
|
||||||
if (!disputeOptional.isPresent()) {
|
if (!disputeOptional.isPresent()) {
|
||||||
log.warn("Trader has no dispute when signing dispute payout tx. This should never happen. TradeId = " + tradeId);
|
log.warn("Trader has no dispute when signing dispute payout tx. This should never happen. TradeId = " + tradeId);
|
||||||
return;
|
return;
|
||||||
|
@ -579,78 +580,46 @@ public final class ArbitrationManager extends DisputeManager<ArbitrationDisputeL
|
||||||
// Disputed payout tx signing
|
// Disputed payout tx signing
|
||||||
///////////////////////////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
// TODO (woodser): where to move this common logic?
|
|
||||||
public static MoneroTxWallet arbitratorCreatesDisputedPayoutTx(Contract contract, Dispute dispute, DisputeResult disputeResult, MoneroWallet multisigWallet) {
|
public static MoneroTxWallet arbitratorCreatesDisputedPayoutTx(Contract contract, Dispute dispute, DisputeResult disputeResult, MoneroWallet multisigWallet) {
|
||||||
|
|
||||||
//System.out.println("DisputeSummaryWindow.arbitratorSignsDisputedPayoutTx()");
|
// multisig wallet must be synced
|
||||||
//System.out.println("=== DISPUTE ===");
|
if (multisigWallet.isMultisigImportNeeded()) throw new RuntimeException("Arbitrator's wallet needs updated multisig hex to create payout tx which means a trader must have already broadcast the payout tx");
|
||||||
//System.out.println(dispute);
|
|
||||||
//System.out.println("=== CONTRACT ===");
|
|
||||||
//System.out.println(contract); // TODO (woodser): contract should include deposit tx hashes (pre-created then hash shared then contract signed)
|
|
||||||
//System.out.println("=== DISPUTE RESULT ===");
|
|
||||||
//System.out.println(disputeResult);
|
|
||||||
|
|
||||||
// gather relevant trade info
|
// collect winner and loser payout address and amounts
|
||||||
String buyerPayoutAddress = contract.isBuyerMakerAndSellerTaker() ? contract.getMakerPayoutAddressString() : contract.getTakerPayoutAddressString();
|
String winnerPayoutAddress = disputeResult.getWinner() == Winner.BUYER ?
|
||||||
String sellerPayoutAddress = contract.isBuyerMakerAndSellerTaker() ? contract.getTakerPayoutAddressString() : contract.getMakerPayoutAddressString();
|
(contract.isBuyerMakerAndSellerTaker() ? contract.getMakerPayoutAddressString() : contract.getTakerPayoutAddressString()) :
|
||||||
Preconditions.checkNotNull(buyerPayoutAddress, "buyerPayoutAddress must not be null");
|
(contract.isBuyerMakerAndSellerTaker() ? contract.getTakerPayoutAddressString() : contract.getMakerPayoutAddressString());
|
||||||
Preconditions.checkNotNull(sellerPayoutAddress, "sellerPayoutAddress must not be null");
|
String loserPayoutAddress = winnerPayoutAddress.equals(contract.getMakerPayoutAddressString()) ? contract.getTakerPayoutAddressString() : contract.getMakerPayoutAddressString();
|
||||||
BigInteger buyerPayoutAmount = ParsingUtils.coinToAtomicUnits(disputeResult.getBuyerPayoutAmount());
|
BigInteger winnerPayoutAmount = ParsingUtils.coinToAtomicUnits(disputeResult.getWinner() == Winner.BUYER ? disputeResult.getBuyerPayoutAmount() : disputeResult.getSellerPayoutAmount());
|
||||||
BigInteger sellerPayoutAmount = ParsingUtils.coinToAtomicUnits(disputeResult.getSellerPayoutAmount());
|
BigInteger loserPayoutAmount = ParsingUtils.coinToAtomicUnits(disputeResult.getWinner() == Winner.BUYER ? disputeResult.getSellerPayoutAmount() : disputeResult.getBuyerPayoutAmount());
|
||||||
|
|
||||||
//System.out.println("buyerPayoutAddress: " + buyerPayoutAddress);
|
|
||||||
//System.out.println("buyerPayoutAmount: " + buyerPayoutAmount);
|
|
||||||
|
|
||||||
// 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
|
// 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
|
// TODO (woodser): include arbitration fee
|
||||||
//System.out.println("Creating feeEstimateTx!");
|
|
||||||
MoneroTxConfig txConfig = new MoneroTxConfig().setAccountIndex(0).setRelay(false);
|
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 (winnerPayoutAmount.compareTo(BigInteger.ZERO) > 0) txConfig.addDestination(winnerPayoutAddress, winnerPayoutAmount.multiply(BigInteger.valueOf(9)).divide(BigInteger.valueOf(10))); // reduce payment amount to get fee of similar tx
|
||||||
if (sellerPayoutAmount.compareTo(BigInteger.ZERO) == 1) txConfig.addDestination(sellerPayoutAddress, sellerPayoutAmount.multiply(BigInteger.valueOf(4)).divide(BigInteger.valueOf(5)));
|
if (loserPayoutAmount.compareTo(BigInteger.ZERO) > 0) txConfig.addDestination(loserPayoutAddress, loserPayoutAmount.multiply(BigInteger.valueOf(9)).divide(BigInteger.valueOf(10)));
|
||||||
MoneroTxWallet feeEstimateTx = multisigWallet.createTx(txConfig);
|
MoneroTxWallet feeEstimateTx = multisigWallet.createTx(txConfig);
|
||||||
|
|
||||||
System.out.println("Created fee estimate tx!");
|
// create payout tx by increasing estimated fee until successful
|
||||||
System.out.println(feeEstimateTx);
|
|
||||||
//BigInteger estimatedFee = feeEstimateTx.getFee();
|
|
||||||
|
|
||||||
// attempt to create payout tx by increasing estimated fee until successful
|
|
||||||
MoneroTxWallet payoutTx = null;
|
MoneroTxWallet payoutTx = null;
|
||||||
int numAttempts = 0;
|
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) {
|
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
|
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);
|
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 (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 (sellerPayoutAmount.compareTo(BigInteger.ZERO) == 1) txConfig.addDestination(sellerPayoutAddress, sellerPayoutAmount.subtract(feeEstimate.divide(BigInteger.valueOf(feeDivisor))));
|
if (loserPayoutAmount.compareTo(BigInteger.ZERO) > 0) {
|
||||||
try {
|
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++;
|
numAttempts++;
|
||||||
|
try {
|
||||||
payoutTx = multisigWallet.createTx(txConfig);
|
payoutTx = multisigWallet.createTx(txConfig);
|
||||||
} catch (MoneroError e) {
|
} catch (MoneroError e) {
|
||||||
// exception expected // TODO: better way of estimating fee?
|
// exception expected // TODO: better way of estimating fee?
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (payoutTx == null) throw new RuntimeException("Failed to generate dispute payout tx after " + numAttempts + " attempts");
|
||||||
if (payoutTx == null) throw new RuntimeException("Failed to generate dispute payout tx");
|
log.info("Dispute payout transaction generated on attempt {}: {}", numAttempts, payoutTx);
|
||||||
System.out.println("DISPUTE PAYOUT TX GENERATED ON ATTEMPT " + numAttempts);
|
|
||||||
System.out.println(payoutTx);
|
|
||||||
return payoutTx;
|
return payoutTx;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -658,7 +627,7 @@ public final class ArbitrationManager extends DisputeManager<ArbitrationDisputeL
|
||||||
|
|
||||||
// gather trade info
|
// gather trade info
|
||||||
MoneroWallet multisigWallet = xmrWalletService.getMultisigWallet(tradeId);
|
MoneroWallet multisigWallet = xmrWalletService.getMultisigWallet(tradeId);
|
||||||
Optional<Dispute> disputeOptional = findOwnDispute(tradeId);
|
Optional<Dispute> disputeOptional = findDispute(tradeId);
|
||||||
if (!disputeOptional.isPresent()) throw new RuntimeException("Trader has no dispute when signing dispute payout tx. This should never happen. TradeId = " + 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();
|
Dispute dispute = disputeOptional.get();
|
||||||
Contract contract = dispute.getContract();
|
Contract contract = dispute.getContract();
|
||||||
|
@ -668,10 +637,6 @@ public final class ArbitrationManager extends DisputeManager<ArbitrationDisputeL
|
||||||
// BigInteger sellerDepositAmount = multisigWallet.getTx(trade instanceof MakerTrade ? trade.getMaker().getDepositTxHash() : trade.getTaker().getDepositTxHash()).getIncomingAmount(); // TODO (woodser): use contract instead of trade to get deposit tx ids when contract has deposit tx ids
|
// BigInteger sellerDepositAmount = multisigWallet.getTx(trade instanceof MakerTrade ? trade.getMaker().getDepositTxHash() : trade.getTaker().getDepositTxHash()).getIncomingAmount(); // TODO (woodser): use contract instead of trade to get deposit tx ids when contract has deposit tx ids
|
||||||
// BigInteger buyerDepositAmount = multisigWallet.getTx(trade instanceof MakerTrade ? trade.getTaker().getDepositTxHash() : trade.getMaker().getDepositTxHash()).getIncomingAmount();
|
// BigInteger buyerDepositAmount = multisigWallet.getTx(trade instanceof MakerTrade ? trade.getTaker().getDepositTxHash() : trade.getMaker().getDepositTxHash()).getIncomingAmount();
|
||||||
// BigInteger tradeAmount = BigInteger.valueOf(contract.getTradeAmount().value).multiply(ParsingUtils.XMR_SATOSHI_MULTIPLIER);
|
// BigInteger tradeAmount = BigInteger.valueOf(contract.getTradeAmount().value).multiply(ParsingUtils.XMR_SATOSHI_MULTIPLIER);
|
||||||
BigInteger buyerPayoutAmount = ParsingUtils.coinToAtomicUnits(disputeResult.getBuyerPayoutAmount());
|
|
||||||
BigInteger sellerPayoutAmount = ParsingUtils.coinToAtomicUnits(disputeResult.getSellerPayoutAmount());
|
|
||||||
System.out.println("Buyer payout amount (with multiplier): " + buyerPayoutAmount);
|
|
||||||
System.out.println("Seller payout amount (with multiplier): " + sellerPayoutAmount);
|
|
||||||
|
|
||||||
// parse arbitrator-signed payout tx
|
// parse arbitrator-signed payout tx
|
||||||
MoneroTxSet parsedTxSet = multisigWallet.describeTxSet(new MoneroTxSet().setMultisigTxHex(payoutTxHex));
|
MoneroTxSet parsedTxSet = multisigWallet.describeTxSet(new MoneroTxSet().setMultisigTxHex(payoutTxHex));
|
||||||
|
@ -694,41 +659,31 @@ public final class ArbitrationManager extends DisputeManager<ArbitrationDisputeL
|
||||||
if (sellerPayoutDestination != null && !sellerPayoutDestination.getAddress().equals(contract.getSellerPayoutAddressString())) throw new RuntimeException("Seller payout address does not match contract");
|
if (sellerPayoutDestination != null && !sellerPayoutDestination.getAddress().equals(contract.getSellerPayoutAddressString())) throw new RuntimeException("Seller payout address does not match contract");
|
||||||
|
|
||||||
// verify change address is multisig's primary address
|
// verify change address is multisig's primary address
|
||||||
if (!arbitratorSignedPayoutTx.getChangeAddress().equals(multisigWallet.getPrimaryAddress())) throw new RuntimeException("Change address is not multisig wallet's primary address");
|
if (arbitratorSignedPayoutTx.getChangeAddress() != null && !arbitratorSignedPayoutTx.getChangeAddress().equals(multisigWallet.getPrimaryAddress())) throw new RuntimeException("Change address is not multisig wallet's primary address");
|
||||||
|
|
||||||
// verify sum of outputs = destination amounts + change amount
|
// verify sum of outputs = destination amounts + change amount
|
||||||
BigInteger destinationSum = (buyerPayoutDestination == null ? BigInteger.ZERO : buyerPayoutDestination.getAmount()).add(sellerPayoutDestination == null ? BigInteger.ZERO : sellerPayoutDestination.getAmount());
|
BigInteger destinationSum = (buyerPayoutDestination == null ? BigInteger.ZERO : buyerPayoutDestination.getAmount()).add(sellerPayoutDestination == null ? BigInteger.ZERO : sellerPayoutDestination.getAmount());
|
||||||
if (!arbitratorSignedPayoutTx.getOutputSum().equals(destinationSum.add(arbitratorSignedPayoutTx.getChangeAmount()))) throw new RuntimeException("Sum of outputs != destination amounts + change amount");
|
if (!arbitratorSignedPayoutTx.getOutputSum().equals(destinationSum.add(arbitratorSignedPayoutTx.getChangeAmount()))) throw new RuntimeException("Sum of outputs != destination amounts + change amount");
|
||||||
|
|
||||||
// verify buyer destination amount is payout amount - 1/2 tx costs
|
|
||||||
if (buyerPayoutDestination != null) {
|
|
||||||
BigInteger txCost = arbitratorSignedPayoutTx.getFee().add(arbitratorSignedPayoutTx.getChangeAmount());
|
|
||||||
BigInteger expectedBuyerPayout = buyerPayoutAmount.subtract(txCost.divide(BigInteger.valueOf(2)));
|
|
||||||
|
|
||||||
System.out.println("Dispute buyer payout amount: " + buyerPayoutAmount);
|
|
||||||
System.out.println("Tx cost: " + txCost);
|
|
||||||
System.out.println("Buyer destination payout amount: " + buyerPayoutDestination.getAmount());
|
|
||||||
}
|
|
||||||
|
|
||||||
// payout amount is dispute payout amount - 1/2 tx cost - deposit tx fee
|
|
||||||
|
|
||||||
// TODO (woodser): VERIFY PAYOUT TX AMOUNTS WHICH CONSIDERS FEE IF LONG TRADE, EXACT AMOUNT IF SHORT TRADE
|
|
||||||
|
|
||||||
// if (!buyerPayoutDestination.getAmount().equals(expectedBuyerPayout)) throw new RuntimeException("Buyer destination amount is not payout amount - 1/2 tx costs, " + buyerPayoutDestination.getAmount() + " vs " + expectedBuyerPayout);
|
|
||||||
|
|
||||||
// verify seller destination amount is payout amount - 1/2 tx costs
|
|
||||||
// BigInteger expectedSellerPayout = sellerPayoutAmount.subtract(txCost.divide(BigInteger.valueOf(2)));
|
|
||||||
// if (!sellerPayoutDestination.getAmount().equals(expectedSellerPayout)) throw new RuntimeException("Seller destination amount is not payout amount - 1/2 tx costs, " + sellerPayoutDestination.getAmount() + " vs " + expectedSellerPayout);
|
|
||||||
|
|
||||||
// TODO (woodser): verify fee is reasonable (e.g. within 2x of fee estimate tx)
|
// TODO (woodser): verify fee is reasonable (e.g. within 2x of fee estimate tx)
|
||||||
|
|
||||||
|
// verify winner and loser payout amounts
|
||||||
|
BigInteger txCost = arbitratorSignedPayoutTx.getFee().add(arbitratorSignedPayoutTx.getChangeAmount()); // fee + lost dust change
|
||||||
|
BigInteger expectedWinnerAmount = ParsingUtils.coinToAtomicUnits(disputeResult.getWinner() == Winner.BUYER ? disputeResult.getBuyerPayoutAmount() : disputeResult.getSellerPayoutAmount());
|
||||||
|
BigInteger expectedLoserAmount = ParsingUtils.coinToAtomicUnits(disputeResult.getWinner() == Winner.BUYER ? disputeResult.getSellerPayoutAmount() : disputeResult.getBuyerPayoutAmount());
|
||||||
|
if (expectedLoserAmount.equals(BigInteger.ZERO)) expectedWinnerAmount = expectedWinnerAmount.subtract(txCost); // winner only pays tx cost if loser gets 0
|
||||||
|
else expectedLoserAmount = expectedLoserAmount.subtract(txCost); // loser pays tx cost
|
||||||
|
BigInteger actualWinnerAmount = disputeResult.getWinner() == Winner.BUYER ? buyerPayoutDestination.getAmount() : sellerPayoutDestination.getAmount();
|
||||||
|
BigInteger actualLoserAmount = numDestinations == 1 ? BigInteger.ZERO : disputeResult.getWinner() == Winner.BUYER ? sellerPayoutDestination.getAmount() : buyerPayoutDestination.getAmount();
|
||||||
|
if (!expectedWinnerAmount.equals(actualWinnerAmount)) throw new RuntimeException("Unexpected winner payout: " + expectedWinnerAmount + " vs " + actualWinnerAmount);
|
||||||
|
if (!expectedLoserAmount.equals(actualLoserAmount)) throw new RuntimeException("Unexpected loser payout: " + expectedLoserAmount + " vs " + actualLoserAmount);
|
||||||
|
|
||||||
// update multisig wallet from arbitrator
|
// update multisig wallet from arbitrator
|
||||||
System.out.println("Updating multisig hex from arbitrator: " + disputeResult.getArbitratorUpdatedMultisigHex());
|
System.out.println("Updating multisig hex from arbitrator");
|
||||||
multisigWallet.importMultisigHex(Arrays.asList(disputeResult.getArbitratorUpdatedMultisigHex()));
|
multisigWallet.importMultisigHex(Arrays.asList(disputeResult.getArbitratorUpdatedMultisigHex()));
|
||||||
|
|
||||||
// sign arbitrator-signed payout tx
|
// sign arbitrator-signed payout tx
|
||||||
MoneroMultisigSignResult result = multisigWallet.signMultisigTxHex(payoutTxHex);
|
MoneroMultisigSignResult result = multisigWallet.signMultisigTxHex(payoutTxHex);
|
||||||
|
|
||||||
if (result.getSignedMultisigTxHex() == null) throw new RuntimeException("Error signing arbitrator-signed payout tx");
|
if (result.getSignedMultisigTxHex() == null) throw new RuntimeException("Error signing arbitrator-signed payout tx");
|
||||||
String signedMultisigTxHex = result.getSignedMultisigTxHex();
|
String signedMultisigTxHex = result.getSignedMultisigTxHex();
|
||||||
parsedTxSet.setMultisigTxHex(signedMultisigTxHex);
|
parsedTxSet.setMultisigTxHex(signedMultisigTxHex);
|
||||||
|
|
|
@ -18,6 +18,7 @@
|
||||||
package bisq.core.support.dispute.mediation;
|
package bisq.core.support.dispute.mediation;
|
||||||
|
|
||||||
import bisq.core.api.CoreMoneroConnectionsService;
|
import bisq.core.api.CoreMoneroConnectionsService;
|
||||||
|
import bisq.core.api.CoreNotificationService;
|
||||||
import bisq.core.btc.wallet.TradeWalletService;
|
import bisq.core.btc.wallet.TradeWalletService;
|
||||||
import bisq.core.btc.wallet.XmrWalletService;
|
import bisq.core.btc.wallet.XmrWalletService;
|
||||||
import bisq.core.locale.Res;
|
import bisq.core.locale.Res;
|
||||||
|
@ -78,6 +79,7 @@ public final class MediationManager extends DisputeManager<MediationDisputeList>
|
||||||
TradeWalletService tradeWalletService,
|
TradeWalletService tradeWalletService,
|
||||||
XmrWalletService walletService,
|
XmrWalletService walletService,
|
||||||
CoreMoneroConnectionsService connectionService,
|
CoreMoneroConnectionsService connectionService,
|
||||||
|
CoreNotificationService notificationService,
|
||||||
TradeManager tradeManager,
|
TradeManager tradeManager,
|
||||||
ClosedTradableManager closedTradableManager,
|
ClosedTradableManager closedTradableManager,
|
||||||
OpenOfferManager openOfferManager,
|
OpenOfferManager openOfferManager,
|
||||||
|
@ -85,7 +87,7 @@ public final class MediationManager extends DisputeManager<MediationDisputeList>
|
||||||
MediationDisputeListService mediationDisputeListService,
|
MediationDisputeListService mediationDisputeListService,
|
||||||
Config config,
|
Config config,
|
||||||
PriceFeedService priceFeedService) {
|
PriceFeedService priceFeedService) {
|
||||||
super(p2PService, tradeWalletService, walletService, connectionService, tradeManager, closedTradableManager,
|
super(p2PService, tradeWalletService, walletService, connectionService, notificationService, tradeManager, closedTradableManager,
|
||||||
openOfferManager, keyRing, mediationDisputeListService, config, priceFeedService);
|
openOfferManager, keyRing, mediationDisputeListService, config, priceFeedService);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -18,6 +18,7 @@
|
||||||
package bisq.core.support.dispute.refund;
|
package bisq.core.support.dispute.refund;
|
||||||
|
|
||||||
import bisq.core.api.CoreMoneroConnectionsService;
|
import bisq.core.api.CoreMoneroConnectionsService;
|
||||||
|
import bisq.core.api.CoreNotificationService;
|
||||||
import bisq.core.btc.wallet.TradeWalletService;
|
import bisq.core.btc.wallet.TradeWalletService;
|
||||||
import bisq.core.btc.wallet.XmrWalletService;
|
import bisq.core.btc.wallet.XmrWalletService;
|
||||||
import bisq.core.locale.Res;
|
import bisq.core.locale.Res;
|
||||||
|
@ -72,6 +73,7 @@ public final class RefundManager extends DisputeManager<RefundDisputeList> {
|
||||||
TradeWalletService tradeWalletService,
|
TradeWalletService tradeWalletService,
|
||||||
XmrWalletService walletService,
|
XmrWalletService walletService,
|
||||||
CoreMoneroConnectionsService connectionService,
|
CoreMoneroConnectionsService connectionService,
|
||||||
|
CoreNotificationService notificationService,
|
||||||
TradeManager tradeManager,
|
TradeManager tradeManager,
|
||||||
ClosedTradableManager closedTradableManager,
|
ClosedTradableManager closedTradableManager,
|
||||||
OpenOfferManager openOfferManager,
|
OpenOfferManager openOfferManager,
|
||||||
|
@ -80,7 +82,7 @@ public final class RefundManager extends DisputeManager<RefundDisputeList> {
|
||||||
RefundDisputeListService refundDisputeListService,
|
RefundDisputeListService refundDisputeListService,
|
||||||
Config config,
|
Config config,
|
||||||
PriceFeedService priceFeedService) {
|
PriceFeedService priceFeedService) {
|
||||||
super(p2PService, tradeWalletService, walletService, connectionService, tradeManager, closedTradableManager,
|
super(p2PService, tradeWalletService, walletService, connectionService, notificationService, tradeManager, closedTradableManager,
|
||||||
openOfferManager, keyRing, refundDisputeListService, config, priceFeedService);
|
openOfferManager, keyRing, refundDisputeListService, config, priceFeedService);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -205,9 +205,7 @@ public final class ChatMessage extends SupportMessage {
|
||||||
notifyChangeListener();
|
notifyChangeListener();
|
||||||
}
|
}
|
||||||
|
|
||||||
// We cannot rename protobuf definition because it would break backward compatibility
|
public protobuf.ChatMessage.Builder toProtoChatMessageBuilder() {
|
||||||
@Override
|
|
||||||
public protobuf.NetworkEnvelope toProtoNetworkEnvelope() {
|
|
||||||
protobuf.ChatMessage.Builder builder = protobuf.ChatMessage.newBuilder()
|
protobuf.ChatMessage.Builder builder = protobuf.ChatMessage.newBuilder()
|
||||||
.setType(SupportType.toProtoMessage(supportType))
|
.setType(SupportType.toProtoMessage(supportType))
|
||||||
.setTradeId(tradeId)
|
.setTradeId(tradeId)
|
||||||
|
@ -225,6 +223,14 @@ public final class ChatMessage extends SupportMessage {
|
||||||
.setWasDisplayed(wasDisplayed);
|
.setWasDisplayed(wasDisplayed);
|
||||||
Optional.ofNullable(sendMessageErrorProperty.get()).ifPresent(builder::setSendMessageError);
|
Optional.ofNullable(sendMessageErrorProperty.get()).ifPresent(builder::setSendMessageError);
|
||||||
Optional.ofNullable(ackErrorProperty.get()).ifPresent(builder::setAckError);
|
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()
|
return getNetworkEnvelopeBuilder()
|
||||||
.setChatMessage(builder)
|
.setChatMessage(builder)
|
||||||
.build();
|
.build();
|
||||||
|
|
|
@ -21,8 +21,6 @@ import bisq.core.support.SupportSession;
|
||||||
import bisq.core.support.messages.ChatMessage;
|
import bisq.core.support.messages.ChatMessage;
|
||||||
import bisq.core.trade.Trade;
|
import bisq.core.trade.Trade;
|
||||||
|
|
||||||
import bisq.common.crypto.PubKeyRing;
|
|
||||||
|
|
||||||
import javafx.collections.FXCollections;
|
import javafx.collections.FXCollections;
|
||||||
import javafx.collections.ObservableList;
|
import javafx.collections.ObservableList;
|
||||||
|
|
||||||
|
|
|
@ -18,6 +18,7 @@
|
||||||
package bisq.core.support.traderchat;
|
package bisq.core.support.traderchat;
|
||||||
|
|
||||||
import bisq.core.api.CoreMoneroConnectionsService;
|
import bisq.core.api.CoreMoneroConnectionsService;
|
||||||
|
import bisq.core.api.CoreNotificationService;
|
||||||
import bisq.core.locale.Res;
|
import bisq.core.locale.Res;
|
||||||
import bisq.core.support.SupportManager;
|
import bisq.core.support.SupportManager;
|
||||||
import bisq.core.support.SupportType;
|
import bisq.core.support.SupportType;
|
||||||
|
@ -57,9 +58,10 @@ public class TraderChatManager extends SupportManager {
|
||||||
@Inject
|
@Inject
|
||||||
public TraderChatManager(P2PService p2PService,
|
public TraderChatManager(P2PService p2PService,
|
||||||
CoreMoneroConnectionsService connectionService,
|
CoreMoneroConnectionsService connectionService,
|
||||||
|
CoreNotificationService notificationService,
|
||||||
TradeManager tradeManager,
|
TradeManager tradeManager,
|
||||||
PubKeyRingProvider pubKeyRingProvider) {
|
PubKeyRingProvider pubKeyRingProvider) {
|
||||||
super(p2PService, connectionService);
|
super(p2PService, connectionService, notificationService);
|
||||||
this.tradeManager = tradeManager;
|
this.tradeManager = tradeManager;
|
||||||
this.pubKeyRingProvider = pubKeyRingProvider;
|
this.pubKeyRingProvider = pubKeyRingProvider;
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,6 +21,7 @@ public class ParsingUtils {
|
||||||
* Multiplier to convert centineros (the base XMR unit of Coin) to atomic units.
|
* Multiplier to convert centineros (the base XMR unit of Coin) to atomic units.
|
||||||
*
|
*
|
||||||
* TODO: change base unit to atomic units and long
|
* TODO: change base unit to atomic units and long
|
||||||
|
* TODO: move these static utilities?
|
||||||
*/
|
*/
|
||||||
private static BigInteger CENTINEROS_AU_MULTIPLIER = BigInteger.valueOf(10000);
|
private static BigInteger CENTINEROS_AU_MULTIPLIER = BigInteger.valueOf(10000);
|
||||||
|
|
||||||
|
|
156
daemon/src/main/java/bisq/daemon/grpc/GrpcDisputesService.java
Normal file
156
daemon/src/main/java/bisq/daemon/grpc/GrpcDisputesService.java
Normal file
|
@ -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<OpenDisputeReply> 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<GetDisputeReply> 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<GetDisputesReply> 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<ResolveDisputeReply> 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<SendDisputeChatMessageReply> 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<ServerInterceptor> rateMeteringInterceptor = rateMeteringInterceptor();
|
||||||
|
return rateMeteringInterceptor.map(serverInterceptor ->
|
||||||
|
new ServerInterceptor[]{serverInterceptor}).orElseGet(() -> new ServerInterceptor[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
final Optional<ServerInterceptor> 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));
|
||||||
|
}}
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
}
|
|
@ -51,6 +51,7 @@ public class GrpcServer {
|
||||||
PasswordAuthInterceptor passwordAuthInterceptor,
|
PasswordAuthInterceptor passwordAuthInterceptor,
|
||||||
GrpcAccountService accountService,
|
GrpcAccountService accountService,
|
||||||
GrpcDisputeAgentsService disputeAgentsService,
|
GrpcDisputeAgentsService disputeAgentsService,
|
||||||
|
GrpcDisputesService disputesService,
|
||||||
GrpcHelpService helpService,
|
GrpcHelpService helpService,
|
||||||
GrpcOffersService offersService,
|
GrpcOffersService offersService,
|
||||||
GrpcPaymentAccountsService paymentAccountsService,
|
GrpcPaymentAccountsService paymentAccountsService,
|
||||||
|
@ -66,6 +67,7 @@ public class GrpcServer {
|
||||||
.executor(UserThread.getExecutor())
|
.executor(UserThread.getExecutor())
|
||||||
.addService(interceptForward(accountService, accountService.interceptors()))
|
.addService(interceptForward(accountService, accountService.interceptors()))
|
||||||
.addService(interceptForward(disputeAgentsService, disputeAgentsService.interceptors()))
|
.addService(interceptForward(disputeAgentsService, disputeAgentsService.interceptors()))
|
||||||
|
.addService(interceptForward(disputesService, disputesService.interceptors()))
|
||||||
.addService(interceptForward(helpService, helpService.interceptors()))
|
.addService(interceptForward(helpService, helpService.interceptors()))
|
||||||
.addService(interceptForward(offersService, offersService.interceptors()))
|
.addService(interceptForward(offersService, offersService.interceptors()))
|
||||||
.addService(interceptForward(paymentAccountsService, paymentAccountsService.interceptors()))
|
.addService(interceptForward(paymentAccountsService, paymentAccountsService.interceptors()))
|
||||||
|
|
|
@ -228,8 +228,8 @@ public class NotificationCenter {
|
||||||
|
|
||||||
private void onDisputeStateChanged(Trade trade, Trade.DisputeState disputeState) {
|
private void onDisputeStateChanged(Trade trade, Trade.DisputeState disputeState) {
|
||||||
String message = null;
|
String message = null;
|
||||||
if (refundManager.findOwnDispute(trade.getId()).isPresent()) {
|
if (refundManager.findDispute(trade.getId()).isPresent()) {
|
||||||
String disputeOrTicket = refundManager.findOwnDispute(trade.getId()).get().isSupportTicket() ?
|
String disputeOrTicket = refundManager.findDispute(trade.getId()).get().isSupportTicket() ?
|
||||||
Res.get("shared.supportTicket") :
|
Res.get("shared.supportTicket") :
|
||||||
Res.get("shared.dispute");
|
Res.get("shared.dispute");
|
||||||
switch (disputeState) {
|
switch (disputeState) {
|
||||||
|
@ -253,8 +253,8 @@ public class NotificationCenter {
|
||||||
if (message != null) {
|
if (message != null) {
|
||||||
goToSupport(trade, message, false);
|
goToSupport(trade, message, false);
|
||||||
}
|
}
|
||||||
} else if (mediationManager.findOwnDispute(trade.getId()).isPresent()) {
|
} else if (mediationManager.findDispute(trade.getId()).isPresent()) {
|
||||||
String disputeOrTicket = mediationManager.findOwnDispute(trade.getId()).get().isSupportTicket() ?
|
String disputeOrTicket = mediationManager.findDispute(trade.getId()).get().isSupportTicket() ?
|
||||||
Res.get("shared.supportTicket") :
|
Res.get("shared.supportTicket") :
|
||||||
Res.get("shared.mediationCase");
|
Res.get("shared.mediationCase");
|
||||||
switch (disputeState) {
|
switch (disputeState) {
|
||||||
|
|
|
@ -23,7 +23,8 @@ import bisq.desktop.components.HavenoTextArea;
|
||||||
import bisq.desktop.components.InputTextField;
|
import bisq.desktop.components.InputTextField;
|
||||||
import bisq.desktop.main.overlays.Overlay;
|
import bisq.desktop.main.overlays.Overlay;
|
||||||
import bisq.desktop.main.overlays.popups.Popup;
|
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.DisplayUtils;
|
||||||
import bisq.desktop.util.Layout;
|
import bisq.desktop.util.Layout;
|
||||||
|
|
||||||
|
@ -49,7 +50,6 @@ import bisq.common.handlers.ResultHandler;
|
||||||
import bisq.common.util.Tuple2;
|
import bisq.common.util.Tuple2;
|
||||||
import bisq.common.util.Tuple3;
|
import bisq.common.util.Tuple3;
|
||||||
|
|
||||||
import org.bitcoinj.core.AddressFormatException;
|
|
||||||
import org.bitcoinj.core.Coin;
|
import org.bitcoinj.core.Coin;
|
||||||
|
|
||||||
import com.google.inject.Inject;
|
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 bisq.desktop.util.FormBuilder.addTopLabelWithVBox;
|
||||||
import static com.google.common.base.Preconditions.checkNotNull;
|
import static com.google.common.base.Preconditions.checkNotNull;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
import monero.wallet.MoneroWallet;
|
|
||||||
import monero.wallet.model.MoneroTxWallet;
|
|
||||||
|
|
||||||
@Slf4j
|
@Slf4j
|
||||||
public class DisputeSummaryWindow extends Overlay<DisputeSummaryWindow> {
|
public class DisputeSummaryWindow extends Overlay<DisputeSummaryWindow> {
|
||||||
private final CoinFormatter formatter;
|
private final CoinFormatter formatter;
|
||||||
|
@ -98,8 +93,8 @@ public class DisputeSummaryWindow extends Overlay<DisputeSummaryWindow> {
|
||||||
private final MediationManager mediationManager;
|
private final MediationManager mediationManager;
|
||||||
private final XmrWalletService walletService;
|
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 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 Dispute dispute;
|
||||||
private Optional<Runnable> finalizeDisputeHandlerOptional = Optional.empty();
|
|
||||||
private ToggleGroup tradeAmountToggleGroup, reasonToggleGroup;
|
private ToggleGroup tradeAmountToggleGroup, reasonToggleGroup;
|
||||||
private DisputeResult disputeResult;
|
private DisputeResult disputeResult;
|
||||||
private RadioButton buyerGetsTradeAmountRadioButton, sellerGetsTradeAmountRadioButton,
|
private RadioButton buyerGetsTradeAmountRadioButton, sellerGetsTradeAmountRadioButton,
|
||||||
|
@ -132,13 +127,15 @@ public class DisputeSummaryWindow extends Overlay<DisputeSummaryWindow> {
|
||||||
ArbitrationManager arbitrationManager,
|
ArbitrationManager arbitrationManager,
|
||||||
MediationManager mediationManager,
|
MediationManager mediationManager,
|
||||||
XmrWalletService walletService,
|
XmrWalletService walletService,
|
||||||
TradeWalletService tradeWalletService) {
|
TradeWalletService tradeWalletService,
|
||||||
|
CoreDisputesService disputesService) {
|
||||||
|
|
||||||
this.formatter = formatter;
|
this.formatter = formatter;
|
||||||
this.arbitrationManager = arbitrationManager;
|
this.arbitrationManager = arbitrationManager;
|
||||||
this.mediationManager = mediationManager;
|
this.mediationManager = mediationManager;
|
||||||
this.walletService = walletService;
|
this.walletService = walletService;
|
||||||
this.tradeWalletService = tradeWalletService;
|
this.tradeWalletService = tradeWalletService;
|
||||||
|
this.disputesService = disputesService;
|
||||||
|
|
||||||
type = Type.Confirmation;
|
type = Type.Confirmation;
|
||||||
}
|
}
|
||||||
|
@ -159,12 +156,6 @@ public class DisputeSummaryWindow extends Overlay<DisputeSummaryWindow> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public DisputeSummaryWindow onFinalizeDispute(Runnable finalizeDisputeHandler) {
|
|
||||||
this.finalizeDisputeHandlerOptional = Optional.of(finalizeDisputeHandler);
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
///////////////////////////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////////////////////////
|
||||||
// Protected
|
// Protected
|
||||||
///////////////////////////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
@ -598,39 +589,7 @@ public class DisputeSummaryWindow extends Overlay<DisputeSummaryWindow> {
|
||||||
Button cancelButton = tuple.second;
|
Button cancelButton = tuple.second;
|
||||||
|
|
||||||
closeTicketButton.setOnAction(e -> {
|
closeTicketButton.setOnAction(e -> {
|
||||||
|
disputesService.resolveDisputePayout(dispute, disputeResult, 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 = 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)
|
|
||||||
doClose(closeTicketButton);
|
doClose(closeTicketButton);
|
||||||
|
|
||||||
// if (dispute.getDepositTxSerialized() == null) {
|
// if (dispute.getDepositTxSerialized() == null) {
|
||||||
|
@ -801,49 +760,12 @@ public class DisputeSummaryWindow extends Overlay<DisputeSummaryWindow> {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
summaryNotesTextArea.textProperty().unbindBidirectional(disputeResult.summaryNotesProperty());
|
||||||
|
|
||||||
boolean isRefundAgent = disputeManager instanceof RefundManager;
|
boolean isRefundAgent = disputeManager instanceof RefundManager;
|
||||||
disputeResult.setLoserPublisher(isLoserPublisherCheckBox.isSelected());
|
disputeResult.setLoserPublisher(isLoserPublisherCheckBox.isSelected());
|
||||||
disputeResult.setCloseDate(new Date());
|
disputeResult.setCloseDate(new Date());
|
||||||
dispute.setDisputeResult(disputeResult);
|
disputesService.closeDispute(disputeManager, dispute, disputeResult, isRefundAgent);
|
||||||
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);
|
|
||||||
|
|
||||||
if (peersDisputeOptional.isPresent() && !peersDisputeOptional.get().isClosed() && !DevEnv.isDevMode()) {
|
if (peersDisputeOptional.isPresent() && !peersDisputeOptional.get().isClosed() && !DevEnv.isDevMode()) {
|
||||||
UserThread.runAfter(() -> new Popup()
|
UserThread.runAfter(() -> new Popup()
|
||||||
|
@ -852,12 +774,8 @@ public class DisputeSummaryWindow extends Overlay<DisputeSummaryWindow> {
|
||||||
200, TimeUnit.MILLISECONDS);
|
200, TimeUnit.MILLISECONDS);
|
||||||
}
|
}
|
||||||
|
|
||||||
finalizeDisputeHandlerOptional.ifPresent(Runnable::run);
|
|
||||||
|
|
||||||
disputeManager.requestPersistence();
|
disputeManager.requestPersistence();
|
||||||
|
|
||||||
closeTicketButton.disableProperty().unbind();
|
closeTicketButton.disableProperty().unbind();
|
||||||
|
|
||||||
hide();
|
hide();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -878,33 +796,24 @@ public class DisputeSummaryWindow extends Overlay<DisputeSummaryWindow> {
|
||||||
}
|
}
|
||||||
|
|
||||||
private void applyPayoutAmountsToDisputeResult(Toggle selectedTradeAmountToggle) {
|
private void applyPayoutAmountsToDisputeResult(Toggle selectedTradeAmountToggle) {
|
||||||
Contract contract = dispute.getContract();
|
CoreDisputesService.DisputePayout payout;
|
||||||
Offer offer = new Offer(contract.getOfferPayload());
|
|
||||||
Coin buyerSecurityDeposit = offer.getBuyerSecurityDeposit();
|
|
||||||
Coin sellerSecurityDeposit = offer.getSellerSecurityDeposit();
|
|
||||||
Coin tradeAmount = contract.getTradeAmount();
|
|
||||||
if (selectedTradeAmountToggle == buyerGetsTradeAmountRadioButton) {
|
if (selectedTradeAmountToggle == buyerGetsTradeAmountRadioButton) {
|
||||||
disputeResult.setBuyerPayoutAmount(tradeAmount.add(buyerSecurityDeposit));
|
payout = CoreDisputesService.DisputePayout.BUYER_GETS_TRADE_AMOUNT;
|
||||||
disputeResult.setSellerPayoutAmount(sellerSecurityDeposit);
|
|
||||||
disputeResult.setWinner(DisputeResult.Winner.BUYER);
|
disputeResult.setWinner(DisputeResult.Winner.BUYER);
|
||||||
} else if (selectedTradeAmountToggle == buyerGetsAllRadioButton) {
|
} else if (selectedTradeAmountToggle == buyerGetsAllRadioButton) {
|
||||||
disputeResult.setBuyerPayoutAmount(tradeAmount
|
payout = CoreDisputesService.DisputePayout.BUYER_GETS_ALL;
|
||||||
.add(buyerSecurityDeposit)
|
|
||||||
.add(sellerSecurityDeposit)); // TODO (woodser): apply min payout to incentivize loser (see post v1.1.7)
|
|
||||||
disputeResult.setSellerPayoutAmount(Coin.ZERO);
|
|
||||||
disputeResult.setWinner(DisputeResult.Winner.BUYER);
|
disputeResult.setWinner(DisputeResult.Winner.BUYER);
|
||||||
} else if (selectedTradeAmountToggle == sellerGetsTradeAmountRadioButton) {
|
} else if (selectedTradeAmountToggle == sellerGetsTradeAmountRadioButton) {
|
||||||
disputeResult.setBuyerPayoutAmount(buyerSecurityDeposit);
|
payout = CoreDisputesService.DisputePayout.SELLER_GETS_TRADE_AMOUNT;
|
||||||
disputeResult.setSellerPayoutAmount(tradeAmount.add(sellerSecurityDeposit));
|
|
||||||
disputeResult.setWinner(DisputeResult.Winner.SELLER);
|
disputeResult.setWinner(DisputeResult.Winner.SELLER);
|
||||||
} else if (selectedTradeAmountToggle == sellerGetsAllRadioButton) {
|
} else if (selectedTradeAmountToggle == sellerGetsAllRadioButton) {
|
||||||
disputeResult.setBuyerPayoutAmount(Coin.ZERO);
|
payout = CoreDisputesService.DisputePayout.SELLER_GETS_ALL;
|
||||||
disputeResult.setSellerPayoutAmount(tradeAmount
|
|
||||||
.add(sellerSecurityDeposit)
|
|
||||||
.add(buyerSecurityDeposit));
|
|
||||||
disputeResult.setWinner(DisputeResult.Winner.SELLER);
|
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()));
|
buyerPayoutAmountInputTextField.setText(formatter.formatCoin(disputeResult.getBuyerPayoutAmount()));
|
||||||
sellerPayoutAmountInputTextField.setText(formatter.formatCoin(disputeResult.getSellerPayoutAmount()));
|
sellerPayoutAmountInputTextField.setText(formatter.formatCoin(disputeResult.getSellerPayoutAmount()));
|
||||||
}
|
}
|
||||||
|
|
|
@ -204,8 +204,8 @@ public class TradeDetailsWindow extends Overlay<TradeDetailsWindow> {
|
||||||
rows++;
|
rows++;
|
||||||
if (trade.getPayoutTx() != null)
|
if (trade.getPayoutTx() != null)
|
||||||
rows++;
|
rows++;
|
||||||
boolean showDisputedTx = arbitrationManager.findOwnDispute(trade.getId()).isPresent() &&
|
boolean showDisputedTx = arbitrationManager.findDispute(trade.getId()).isPresent() &&
|
||||||
arbitrationManager.findOwnDispute(trade.getId()).get().getDisputePayoutTxId() != null;
|
arbitrationManager.findDispute(trade.getId()).get().getDisputePayoutTxId() != null;
|
||||||
if (showDisputedTx)
|
if (showDisputedTx)
|
||||||
rows++;
|
rows++;
|
||||||
if (trade.hasFailed())
|
if (trade.hasFailed())
|
||||||
|
@ -301,7 +301,7 @@ public class TradeDetailsWindow extends Overlay<TradeDetailsWindow> {
|
||||||
trade.getPayoutTx().getHash());
|
trade.getPayoutTx().getHash());
|
||||||
if (showDisputedTx)
|
if (showDisputedTx)
|
||||||
addLabelTxIdTextField(gridPane, ++rowIndex, Res.get("tradeDetailsWindow.disputedPayoutTxId"),
|
addLabelTxIdTextField(gridPane, ++rowIndex, Res.get("tradeDetailsWindow.disputedPayoutTxId"),
|
||||||
arbitrationManager.findOwnDispute(trade.getId()).get().getDisputePayoutTxId());
|
arbitrationManager.findDispute(trade.getId()).get().getDisputePayoutTxId());
|
||||||
|
|
||||||
if (trade.hasFailed()) {
|
if (trade.hasFailed()) {
|
||||||
textArea = addConfirmationLabelTextArea(gridPane, ++rowIndex, Res.get("shared.errorMessage"), "", 0).second;
|
textArea = addConfirmationLabelTextArea(gridPane, ++rowIndex, Res.get("shared.errorMessage"), "", 0).second;
|
||||||
|
|
|
@ -18,7 +18,7 @@
|
||||||
package bisq.desktop.main.overlays.windows;
|
package bisq.desktop.main.overlays.windows;
|
||||||
|
|
||||||
import bisq.desktop.main.overlays.Overlay;
|
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.locale.Res;
|
||||||
import bisq.core.support.dispute.mediation.mediator.MediatorManager;
|
import bisq.core.support.dispute.mediation.mediator.MediatorManager;
|
||||||
|
|
|
@ -29,6 +29,7 @@ import bisq.desktop.main.support.dispute.client.mediation.MediationClientView;
|
||||||
import bisq.desktop.util.GUIUtil;
|
import bisq.desktop.util.GUIUtil;
|
||||||
|
|
||||||
import bisq.core.account.witness.AccountAgeWitnessService;
|
import bisq.core.account.witness.AccountAgeWitnessService;
|
||||||
|
import bisq.core.api.CoreDisputesService;
|
||||||
import bisq.core.api.CoreMoneroConnectionsService;
|
import bisq.core.api.CoreMoneroConnectionsService;
|
||||||
import bisq.core.btc.wallet.XmrWalletService;
|
import bisq.core.btc.wallet.XmrWalletService;
|
||||||
import bisq.core.locale.Res;
|
import bisq.core.locale.Res;
|
||||||
|
@ -122,6 +123,7 @@ public class PendingTradesDataModel extends ActivatableDataModel {
|
||||||
private Trade selectedTrade;
|
private Trade selectedTrade;
|
||||||
@Getter
|
@Getter
|
||||||
private final PubKeyRingProvider pubKeyRingProvider;
|
private final PubKeyRingProvider pubKeyRingProvider;
|
||||||
|
private final CoreDisputesService disputesService;
|
||||||
|
|
||||||
///////////////////////////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////////////////////////
|
||||||
// Constructor, initialization
|
// Constructor, initialization
|
||||||
|
@ -141,7 +143,8 @@ public class PendingTradesDataModel extends ActivatableDataModel {
|
||||||
Navigation navigation,
|
Navigation navigation,
|
||||||
WalletPasswordWindow walletPasswordWindow,
|
WalletPasswordWindow walletPasswordWindow,
|
||||||
NotificationCenter notificationCenter,
|
NotificationCenter notificationCenter,
|
||||||
OfferUtil offerUtil) {
|
OfferUtil offerUtil,
|
||||||
|
CoreDisputesService disputesService) {
|
||||||
this.tradeManager = tradeManager;
|
this.tradeManager = tradeManager;
|
||||||
this.xmrWalletService = xmrWalletService;
|
this.xmrWalletService = xmrWalletService;
|
||||||
this.pubKeyRingProvider = pubKeyRingProvider;
|
this.pubKeyRingProvider = pubKeyRingProvider;
|
||||||
|
@ -156,6 +159,7 @@ public class PendingTradesDataModel extends ActivatableDataModel {
|
||||||
this.walletPasswordWindow = walletPasswordWindow;
|
this.walletPasswordWindow = walletPasswordWindow;
|
||||||
this.notificationCenter = notificationCenter;
|
this.notificationCenter = notificationCenter;
|
||||||
this.offerUtil = offerUtil;
|
this.offerUtil = offerUtil;
|
||||||
|
this.disputesService = disputesService;
|
||||||
|
|
||||||
tradesListChangeListener = change -> onListChanged();
|
tradesListChangeListener = change -> onListChanged();
|
||||||
notificationCenter.setSelectItemByTradeIdConsumer(this::selectItemByTradeId);
|
notificationCenter.setSelectItemByTradeIdConsumer(this::selectItemByTradeId);
|
||||||
|
@ -544,40 +548,12 @@ public class PendingTradesDataModel extends ActivatableDataModel {
|
||||||
} else if (useArbitration) {
|
} else if (useArbitration) {
|
||||||
// Only if we have completed mediation we allow arbitration
|
// Only if we have completed mediation we allow arbitration
|
||||||
disputeManager = arbitrationManager;
|
disputeManager = arbitrationManager;
|
||||||
PubKeyRing arbitratorPubKeyRing = trade.getArbitratorPubKeyRing();
|
Dispute dispute = disputesService.createDisputeForTrade(trade, offer, pubKeyRingProvider.get(), isMaker, isSupportTicket);
|
||||||
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);
|
|
||||||
sendOpenNewDisputeMessage(dispute, false, disputeManager, updatedMultisigHex);
|
sendOpenNewDisputeMessage(dispute, false, disputeManager, updatedMultisigHex);
|
||||||
|
tradeManager.requestPersistence();
|
||||||
} else {
|
} else {
|
||||||
log.warn("Invalid dispute state {}", disputeState.name());
|
log.warn("Invalid dispute state {}", disputeState.name());
|
||||||
}
|
}
|
||||||
tradeManager.requestPersistence();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void sendOpenNewDisputeMessage(Dispute dispute, boolean reOpen, DisputeManager<? extends DisputeList<Dispute>> disputeManager, String senderMultisigHex) {
|
private void sendOpenNewDisputeMessage(Dispute dispute, boolean reOpen, DisputeManager<? extends DisputeList<Dispute>> disputeManager, String senderMultisigHex) {
|
||||||
|
|
|
@ -486,7 +486,7 @@ public abstract class TradeStepView extends AnchorPane {
|
||||||
}
|
}
|
||||||
applyOnDisputeOpened();
|
applyOnDisputeOpened();
|
||||||
|
|
||||||
ownDispute = model.dataModel.arbitrationManager.findOwnDispute(trade.getId());
|
ownDispute = model.dataModel.arbitrationManager.findDispute(trade.getId());
|
||||||
ownDispute.ifPresent(dispute -> {
|
ownDispute.ifPresent(dispute -> {
|
||||||
if (tradeStepInfo != null)
|
if (tradeStepInfo != null)
|
||||||
tradeStepInfo.setState(TradeStepInfo.State.IN_ARBITRATION_SELF_REQUESTED);
|
tradeStepInfo.setState(TradeStepInfo.State.IN_ARBITRATION_SELF_REQUESTED);
|
||||||
|
@ -499,7 +499,7 @@ public abstract class TradeStepView extends AnchorPane {
|
||||||
}
|
}
|
||||||
applyOnDisputeOpened();
|
applyOnDisputeOpened();
|
||||||
|
|
||||||
ownDispute = model.dataModel.arbitrationManager.findOwnDispute(trade.getId());
|
ownDispute = model.dataModel.arbitrationManager.findDispute(trade.getId());
|
||||||
ownDispute.ifPresent(dispute -> {
|
ownDispute.ifPresent(dispute -> {
|
||||||
if (tradeStepInfo != null)
|
if (tradeStepInfo != null)
|
||||||
tradeStepInfo.setState(TradeStepInfo.State.IN_ARBITRATION_PEER_REQUESTED);
|
tradeStepInfo.setState(TradeStepInfo.State.IN_ARBITRATION_PEER_REQUESTED);
|
||||||
|
@ -513,7 +513,7 @@ public abstract class TradeStepView extends AnchorPane {
|
||||||
}
|
}
|
||||||
applyOnDisputeOpened();
|
applyOnDisputeOpened();
|
||||||
|
|
||||||
ownDispute = model.dataModel.mediationManager.findOwnDispute(trade.getId());
|
ownDispute = model.dataModel.mediationManager.findDispute(trade.getId());
|
||||||
ownDispute.ifPresent(dispute -> {
|
ownDispute.ifPresent(dispute -> {
|
||||||
if (tradeStepInfo != null)
|
if (tradeStepInfo != null)
|
||||||
tradeStepInfo.setState(TradeStepInfo.State.IN_MEDIATION_SELF_REQUESTED);
|
tradeStepInfo.setState(TradeStepInfo.State.IN_MEDIATION_SELF_REQUESTED);
|
||||||
|
@ -525,7 +525,7 @@ public abstract class TradeStepView extends AnchorPane {
|
||||||
}
|
}
|
||||||
applyOnDisputeOpened();
|
applyOnDisputeOpened();
|
||||||
|
|
||||||
ownDispute = model.dataModel.mediationManager.findOwnDispute(trade.getId());
|
ownDispute = model.dataModel.mediationManager.findDispute(trade.getId());
|
||||||
ownDispute.ifPresent(dispute -> {
|
ownDispute.ifPresent(dispute -> {
|
||||||
if (tradeStepInfo != null) {
|
if (tradeStepInfo != null) {
|
||||||
tradeStepInfo.setState(TradeStepInfo.State.IN_MEDIATION_PEER_REQUESTED);
|
tradeStepInfo.setState(TradeStepInfo.State.IN_MEDIATION_PEER_REQUESTED);
|
||||||
|
|
|
@ -154,6 +154,82 @@ message RestoreAccountRequest {
|
||||||
message RestoreAccountReply {
|
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
|
// Notifications
|
||||||
///////////////////////////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
@ -306,23 +382,6 @@ message SetAutoSwitchRequest {
|
||||||
|
|
||||||
message SetAutoSwitchReply {}
|
message SetAutoSwitchReply {}
|
||||||
|
|
||||||
///////////////////////////////////////////////////////////////////////////////////////////
|
|
||||||
// DisputeAgents
|
|
||||||
///////////////////////////////////////////////////////////////////////////////////////////
|
|
||||||
|
|
||||||
service DisputeAgents {
|
|
||||||
rpc RegisterDisputeAgent (RegisterDisputeAgentRequest) returns (RegisterDisputeAgentReply) {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
message RegisterDisputeAgentRequest {
|
|
||||||
string dispute_agent_type = 1;
|
|
||||||
string registration_key = 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
message RegisterDisputeAgentReply {
|
|
||||||
}
|
|
||||||
|
|
||||||
///////////////////////////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////////////////////////
|
||||||
// Offers
|
// Offers
|
||||||
///////////////////////////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
Loading…
Reference in a new issue