diff --git a/core/src/main/java/bisq/core/api/CoreDisputesService.java b/core/src/main/java/bisq/core/api/CoreDisputesService.java index 4eb6081b4e..1b3ca1a8f6 100644 --- a/core/src/main/java/bisq/core/api/CoreDisputesService.java +++ b/core/src/main/java/bisq/core/api/CoreDisputesService.java @@ -35,6 +35,7 @@ import java.util.List; import java.util.Optional; import lombok.extern.slf4j.Slf4j; +import monero.wallet.model.MoneroTxWallet; import static com.google.common.base.Preconditions.checkNotNull; import static java.lang.String.format; @@ -170,7 +171,7 @@ public class CoreDisputesService { applyPayoutAmountsToDisputeResult(payout, winningDispute, disputeResult, customWinnerAmount); // close dispute ticket - closeDisputeTicket(arbitrationManager, winningDispute, disputeResult, () -> { + closeDisputeTicket(arbitrationManager, winningDispute, disputeResult, null, () -> { arbitrationManager.requestPersistence(); // close peer's dispute ticket @@ -182,12 +183,16 @@ public class CoreDisputesService { var peerDisputeResult = createDisputeResult(peerDispute, winner, reason, summaryNotes, closeDate); peerDisputeResult.setBuyerPayoutAmount(disputeResult.getBuyerPayoutAmount()); peerDisputeResult.setSellerPayoutAmount(disputeResult.getSellerPayoutAmount()); - closeDisputeTicket(arbitrationManager, peerDispute, peerDisputeResult, () -> { + closeDisputeTicket(arbitrationManager, peerDispute, peerDisputeResult, null, () -> { arbitrationManager.requestPersistence(); + }, (errMessage, err) -> { + throw new IllegalStateException(errMessage, err); }); } else { throw new IllegalStateException("could not find peer dispute"); } + }, (errMessage, err) -> { + throw new IllegalStateException(errMessage, err); }); } } catch (Exception e) { @@ -239,10 +244,7 @@ public class CoreDisputesService { } } - // From DisputeSummaryWindow.java - public void closeDisputeTicket(DisputeManager disputeManager, Dispute dispute, DisputeResult disputeResult, ResultHandler resultHandler) { - dispute.setDisputeResult(disputeResult); - dispute.setIsClosed(); + public void closeDisputeTicket(DisputeManager disputeManager, Dispute dispute, DisputeResult disputeResult, MoneroTxWallet payoutTx, ResultHandler resultHandler, FaultHandler faultHandler) { DisputeResult.Reason reason = disputeResult.getReason(); String role = Res.get("shared.arbitrator"); @@ -272,7 +274,12 @@ public class CoreDisputesService { String summaryText = DisputeSummaryVerification.signAndApply(disputeManager, disputeResult, textToSign); summaryText += Res.get("disputeSummaryWindow.close.nextStepsForRefundAgentArbitration"); - disputeManager.closeDisputeTicket(disputeResult, dispute, summaryText, resultHandler); + + disputeManager.closeDisputeTicket(disputeResult, dispute, summaryText, payoutTx, () -> { + dispute.setDisputeResult(disputeResult); + dispute.setIsClosed(); + resultHandler.handleResult(); + }, faultHandler); } public void sendDisputeChatMessage(String disputeId, String message, ArrayList attachments) { diff --git a/core/src/main/java/bisq/core/support/dispute/DisputeManager.java b/core/src/main/java/bisq/core/support/dispute/DisputeManager.java index 4ec3e8355c..2b9fd3e19c 100644 --- a/core/src/main/java/bisq/core/support/dispute/DisputeManager.java +++ b/core/src/main/java/bisq/core/support/dispute/DisputeManager.java @@ -689,30 +689,118 @@ public abstract class DisputeManager> extends Sup } // arbitrator sends result to trader when their dispute is closed - public void closeDisputeTicket(DisputeResult disputeResult, Dispute dispute, String summaryText, ResultHandler resultHandler) { - T disputeList = getDisputeList(); - if (disputeList == null) { - log.warn("disputes is null"); - return; - } + public void closeDisputeTicket(DisputeResult disputeResult, Dispute dispute, String summaryText, MoneroTxWallet payoutTx, ResultHandler resultHandler, FaultHandler faultHandler) { + try { - ChatMessage chatMessage = new ChatMessage( + // get trade + Trade trade = tradeManager.getTrade(dispute.getTradeId()); + if (trade == null) throw new RuntimeException("Dispute trade " + dispute.getTradeId() + " does not exist"); + + // create dispute payout tx if not given + if (payoutTx == null) payoutTx = createDisputePayoutTx(trade, dispute, disputeResult); // can be null if already published or we don't have receiver's multisig hex + + // persist result in dispute's chat message + ChatMessage chatMessage = new ChatMessage( getSupportType(), dispute.getTradeId(), dispute.getTraderPubKeyRing().hashCode(), false, summaryText, p2PService.getAddress()); + disputeResult.setChatMessage(chatMessage); + dispute.addAndPersistChatMessage(chatMessage); - disputeResult.setChatMessage(chatMessage); - dispute.addAndPersistChatMessage(chatMessage); + // create dispute closed message + TradingPeer receiver = trade.getTradingPeer(dispute.getTraderPubKeyRing()); + String unsignedPayoutTxHex = payoutTx == null ? null : payoutTx.getTxSet().getMultisigTxHex(); + TradingPeer receiverPeer = receiver == trade.getBuyer() ? trade.getSeller() : trade.getBuyer(); + boolean deferPublishPayout = unsignedPayoutTxHex != null && receiverPeer.getUpdatedMultisigHex() != null && trade.getDisputeState().ordinal() >= Trade.DisputeState.ARBITRATOR_SAW_ARRIVED_DISPUTE_CLOSED_MSG.ordinal() ; + DisputeClosedMessage disputeClosedMessage = new DisputeClosedMessage(disputeResult, + p2PService.getAddress(), + UUID.randomUUID().toString(), + getSupportType(), + trade.getSelf().getUpdatedMultisigHex(), + trade.isPayoutPublished() ? null : unsignedPayoutTxHex, // include dispute payout tx if unpublished and arbitrator has their updated multisig info + deferPublishPayout); // instruct trader to defer publishing payout tx because peer is expected to publish imminently - // get trade - Trade trade = tradeManager.getTrade(dispute.getTradeId()); - if (trade == null) { - log.warn("Dispute trade {} does not exist", dispute.getTradeId()); - return; + // send dispute closed message + log.info("Send {} to trader {}. tradeId={}, {}.uid={}, chatMessage.uid={}", + disputeClosedMessage.getClass().getSimpleName(), receiver.getNodeAddress(), + disputeClosedMessage.getClass().getSimpleName(), disputeClosedMessage.getTradeId(), + disputeClosedMessage.getUid(), chatMessage.getUid()); + mailboxMessageService.sendEncryptedMailboxMessage(receiver.getNodeAddress(), + dispute.getTraderPubKeyRing(), + disputeClosedMessage, + new SendMailboxMessageListener() { + @Override + public void onArrived() { + log.info("{} arrived at trader {}. tradeId={}, disputeClosedMessage.uid={}, " + + "chatMessage.uid={}", + disputeClosedMessage.getClass().getSimpleName(), receiver.getNodeAddress(), + disputeClosedMessage.getTradeId(), disputeClosedMessage.getUid(), + chatMessage.getUid()); + + // We use the chatMessage wrapped inside the DisputeClosedMessage for + // the state, as that is displayed to the user and we only persist that msg + chatMessage.setArrived(true); + trade.setDisputeStateIfProgress(Trade.DisputeState.ARBITRATOR_SAW_ARRIVED_DISPUTE_CLOSED_MSG); + trade.syncWalletNormallyForMs(30000); + requestPersistence(); + resultHandler.handleResult(); + } + + @Override + public void onStoredInMailbox() { + log.info("{} stored in mailbox for trader {}. tradeId={}, DisputeClosedMessage.uid={}, " + + "chatMessage.uid={}", + disputeClosedMessage.getClass().getSimpleName(), receiver.getNodeAddress(), + disputeClosedMessage.getTradeId(), disputeClosedMessage.getUid(), + chatMessage.getUid()); + + // We use the chatMessage wrapped inside the DisputeClosedMessage for + // the state, as that is displayed to the user and we only persist that msg + chatMessage.setStoredInMailbox(true); + Trade trade = tradeManager.getTrade(dispute.getTradeId()); + trade.setDisputeStateIfProgress(Trade.DisputeState.ARBITRATOR_STORED_IN_MAILBOX_DISPUTE_CLOSED_MSG); + requestPersistence(); + resultHandler.handleResult(); + } + + @Override + public void onFault(String errorMessage) { + log.error("{} failed: Trader {}. tradeId={}, DisputeClosedMessage.uid={}, " + + "chatMessage.uid={}, errorMessage={}", + disputeClosedMessage.getClass().getSimpleName(), receiver.getNodeAddress(), + disputeClosedMessage.getTradeId(), disputeClosedMessage.getUid(), + chatMessage.getUid(), errorMessage); + + // We use the chatMessage wrapped inside the DisputeClosedMessage for + // the state, as that is displayed to the user and we only persist that msg + chatMessage.setSendMessageError(errorMessage); + trade.setDisputeStateIfProgress(Trade.DisputeState.ARBITRATOR_SEND_FAILED_DISPUTE_CLOSED_MSG); + requestPersistence(); + faultHandler.handleFault(errorMessage, new RuntimeException(errorMessage)); + } + } + ); + + // save state + if (payoutTx != null) { + trade.setPayoutTx(payoutTx); + trade.setPayoutTxHex(payoutTx.getTxSet().getMultisigTxHex()); + } + trade.setDisputeStateIfProgress(Trade.DisputeState.ARBITRATOR_SENT_DISPUTE_CLOSED_MSG); + requestPersistence(); + } catch (Exception e) { + faultHandler.handleFault(e.getMessage(), e); } + } + + /////////////////////////////////////////////////////////////////////////////////////////// + // Utils + /////////////////////////////////////////////////////////////////////////////////////////// + + public MoneroTxWallet createDisputePayoutTx(Trade trade, Dispute dispute, DisputeResult disputeResult) { // sync and save wallet trade.syncWallet(); @@ -739,139 +827,55 @@ public abstract class DisputeManager> extends Sup if (!trade.isPayoutPublished()) { log.info("Arbitrator creating unsigned dispute payout tx for trade {}", trade.getId()); try { - MoneroTxWallet payoutTx = createDisputePayoutTx(trade, dispute, disputeResult); - trade.setPayoutTx(payoutTx); - trade.setPayoutTxHex(payoutTx.getTxSet().getMultisigTxHex()); + + // trade wallet must be synced + if (trade.getWallet().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 for trade " + dispute.getTradeId()); + + // collect winner and loser payout address and amounts + Contract contract = dispute.getContract(); + String winnerPayoutAddress = disputeResult.getWinner() == Winner.BUYER ? + (contract.isBuyerMakerAndSellerTaker() ? contract.getMakerPayoutAddressString() : contract.getTakerPayoutAddressString()) : + (contract.isBuyerMakerAndSellerTaker() ? contract.getTakerPayoutAddressString() : contract.getMakerPayoutAddressString()); + String loserPayoutAddress = winnerPayoutAddress.equals(contract.getMakerPayoutAddressString()) ? contract.getTakerPayoutAddressString() : contract.getMakerPayoutAddressString(); + BigInteger winnerPayoutAmount = HavenoUtils.coinToAtomicUnits(disputeResult.getWinner() == Winner.BUYER ? disputeResult.getBuyerPayoutAmount() : disputeResult.getSellerPayoutAmount()); + BigInteger loserPayoutAmount = HavenoUtils.coinToAtomicUnits(disputeResult.getWinner() == Winner.BUYER ? disputeResult.getSellerPayoutAmount() : disputeResult.getBuyerPayoutAmount()); + + // create transaction to get fee estimate + MoneroTxConfig txConfig = new MoneroTxConfig().setAccountIndex(0).setRelay(false); + 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 (loserPayoutAmount.compareTo(BigInteger.ZERO) > 0) txConfig.addDestination(loserPayoutAddress, loserPayoutAmount.multiply(BigInteger.valueOf(9)).divide(BigInteger.valueOf(10))); + MoneroTxWallet feeEstimateTx = trade.getWallet().createTx(txConfig); + + // create payout tx by increasing estimated fee until successful + MoneroTxWallet payoutTx = null; + int numAttempts = 0; + while (payoutTx == null && numAttempts < 50) { + BigInteger feeEstimate = feeEstimateTx.getFee().add(feeEstimateTx.getFee().multiply(BigInteger.valueOf(numAttempts)).divide(BigInteger.valueOf(10))); // add 1/10th of fee until tx is successful + txConfig = new MoneroTxConfig().setAccountIndex(0).setRelay(false); + if (winnerPayoutAmount.compareTo(BigInteger.ZERO) > 0) txConfig.addDestination(winnerPayoutAddress, winnerPayoutAmount.subtract(loserPayoutAmount.equals(BigInteger.ZERO) ? feeEstimate : BigInteger.ZERO)); // winner only pays fee if loser gets 0 + if (loserPayoutAmount.compareTo(BigInteger.ZERO) > 0) { + if (loserPayoutAmount.compareTo(feeEstimate) < 0) throw new RuntimeException("Loser payout is too small to cover the mining fee"); + if (loserPayoutAmount.compareTo(feeEstimate) > 0) txConfig.addDestination(loserPayoutAddress, loserPayoutAmount.subtract(feeEstimate)); // loser pays fee + } + numAttempts++; + try { + payoutTx = trade.getWallet().createTx(txConfig); + } catch (MoneroError e) { + // exception expected // TODO: better way of estimating fee? + } + } + if (payoutTx == null) throw new RuntimeException("Failed to generate dispute payout tx after " + numAttempts + " attempts"); + log.info("Dispute payout transaction generated on attempt {}", numAttempts); + + // save updated multisig hex + trade.getSelf().setUpdatedMultisigHex(trade.getWallet().exportMultisigHex()); + return payoutTx; } catch (Exception e) { if (!trade.isPayoutPublished()) throw e; } } } - - // create dispute closed message - String unsignedPayoutTxHex = receiver.getUpdatedMultisigHex() == null ? null : trade.getPayoutTxHex(); - TradingPeer receiverPeer = receiver == trade.getBuyer() ? trade.getSeller() : trade.getBuyer(); - boolean deferPublishPayout = unsignedPayoutTxHex != null && receiverPeer.getUpdatedMultisigHex() != null && trade.getDisputeState().ordinal() >= Trade.DisputeState.ARBITRATOR_SAW_ARRIVED_DISPUTE_CLOSED_MSG.ordinal() ; - DisputeClosedMessage disputeClosedMessage = new DisputeClosedMessage(disputeResult, - p2PService.getAddress(), - UUID.randomUUID().toString(), - getSupportType(), - trade.getSelf().getUpdatedMultisigHex(), - trade.isPayoutPublished() ? null : unsignedPayoutTxHex, // include dispute payout tx if unpublished and arbitrator has their updated multisig info - deferPublishPayout); // instruct trader to defer publishing payout tx because peer is expected to publish imminently - - // send dispute closed message - log.info("Send {} to trader {}. tradeId={}, {}.uid={}, chatMessage.uid={}", - disputeClosedMessage.getClass().getSimpleName(), receiver.getNodeAddress(), - disputeClosedMessage.getClass().getSimpleName(), disputeClosedMessage.getTradeId(), - disputeClosedMessage.getUid(), chatMessage.getUid()); - mailboxMessageService.sendEncryptedMailboxMessage(receiver.getNodeAddress(), - dispute.getTraderPubKeyRing(), - disputeClosedMessage, - new SendMailboxMessageListener() { - @Override - public void onArrived() { - log.info("{} arrived at trader {}. tradeId={}, disputeClosedMessage.uid={}, " + - "chatMessage.uid={}", - disputeClosedMessage.getClass().getSimpleName(), receiver.getNodeAddress(), - disputeClosedMessage.getTradeId(), disputeClosedMessage.getUid(), - chatMessage.getUid()); - - // We use the chatMessage wrapped inside the DisputeClosedMessage for - // the state, as that is displayed to the user and we only persist that msg - chatMessage.setArrived(true); - trade.setDisputeStateIfProgress(Trade.DisputeState.ARBITRATOR_SAW_ARRIVED_DISPUTE_CLOSED_MSG); - trade.syncWalletNormallyForMs(30000); - requestPersistence(); - resultHandler.handleResult(); - } - - @Override - public void onStoredInMailbox() { - log.info("{} stored in mailbox for trader {}. tradeId={}, DisputeClosedMessage.uid={}, " + - "chatMessage.uid={}", - disputeClosedMessage.getClass().getSimpleName(), receiver.getNodeAddress(), - disputeClosedMessage.getTradeId(), disputeClosedMessage.getUid(), - chatMessage.getUid()); - - // We use the chatMessage wrapped inside the DisputeClosedMessage for - // the state, as that is displayed to the user and we only persist that msg - chatMessage.setStoredInMailbox(true); - Trade trade = tradeManager.getTrade(dispute.getTradeId()); - trade.setDisputeStateIfProgress(Trade.DisputeState.ARBITRATOR_STORED_IN_MAILBOX_DISPUTE_CLOSED_MSG); - requestPersistence(); - resultHandler.handleResult(); - } - - @Override - public void onFault(String errorMessage) { - log.error("{} failed: Trader {}. tradeId={}, DisputeClosedMessage.uid={}, " + - "chatMessage.uid={}, errorMessage={}", - disputeClosedMessage.getClass().getSimpleName(), receiver.getNodeAddress(), - disputeClosedMessage.getTradeId(), disputeClosedMessage.getUid(), - chatMessage.getUid(), errorMessage); - - // We use the chatMessage wrapped inside the DisputeClosedMessage for - // the state, as that is displayed to the user and we only persist that msg - chatMessage.setSendMessageError(errorMessage); - trade.setDisputeStateIfProgress(Trade.DisputeState.ARBITRATOR_SEND_FAILED_DISPUTE_CLOSED_MSG); - requestPersistence(); - resultHandler.handleResult(); - } - } - ); - trade.setDisputeStateIfProgress(Trade.DisputeState.ARBITRATOR_SENT_DISPUTE_CLOSED_MSG); - requestPersistence(); - } - - /////////////////////////////////////////////////////////////////////////////////////////// - // Utils - /////////////////////////////////////////////////////////////////////////////////////////// - - private MoneroTxWallet createDisputePayoutTx(Trade trade, Dispute dispute, DisputeResult disputeResult) { - - // trade wallet must be synced - if (trade.getWallet().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 for trade " + dispute.getTradeId()); - - // collect winner and loser payout address and amounts - Contract contract = dispute.getContract(); - String winnerPayoutAddress = disputeResult.getWinner() == Winner.BUYER ? - (contract.isBuyerMakerAndSellerTaker() ? contract.getMakerPayoutAddressString() : contract.getTakerPayoutAddressString()) : - (contract.isBuyerMakerAndSellerTaker() ? contract.getTakerPayoutAddressString() : contract.getMakerPayoutAddressString()); - String loserPayoutAddress = winnerPayoutAddress.equals(contract.getMakerPayoutAddressString()) ? contract.getTakerPayoutAddressString() : contract.getMakerPayoutAddressString(); - BigInteger winnerPayoutAmount = HavenoUtils.coinToAtomicUnits(disputeResult.getWinner() == Winner.BUYER ? disputeResult.getBuyerPayoutAmount() : disputeResult.getSellerPayoutAmount()); - BigInteger loserPayoutAmount = HavenoUtils.coinToAtomicUnits(disputeResult.getWinner() == Winner.BUYER ? disputeResult.getSellerPayoutAmount() : disputeResult.getBuyerPayoutAmount()); - - // create transaction to get fee estimate - MoneroTxConfig txConfig = new MoneroTxConfig().setAccountIndex(0).setRelay(false); - 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 (loserPayoutAmount.compareTo(BigInteger.ZERO) > 0) txConfig.addDestination(loserPayoutAddress, loserPayoutAmount.multiply(BigInteger.valueOf(9)).divide(BigInteger.valueOf(10))); - MoneroTxWallet feeEstimateTx = trade.getWallet().createTx(txConfig); - - // create payout tx by increasing estimated fee until successful - MoneroTxWallet payoutTx = null; - int numAttempts = 0; - while (payoutTx == null && numAttempts < 50) { - BigInteger feeEstimate = feeEstimateTx.getFee().add(feeEstimateTx.getFee().multiply(BigInteger.valueOf(numAttempts)).divide(BigInteger.valueOf(10))); // add 1/10th of fee until tx is successful - txConfig = new MoneroTxConfig().setAccountIndex(0).setRelay(false); - if (winnerPayoutAmount.compareTo(BigInteger.ZERO) > 0) txConfig.addDestination(winnerPayoutAddress, winnerPayoutAmount.subtract(loserPayoutAmount.equals(BigInteger.ZERO) ? feeEstimate : BigInteger.ZERO)); // winner only pays fee if loser gets 0 - if (loserPayoutAmount.compareTo(BigInteger.ZERO) > 0) { - if (loserPayoutAmount.compareTo(feeEstimate) < 0) throw new RuntimeException("Loser payout is too small to cover the mining fee"); - if (loserPayoutAmount.compareTo(feeEstimate) > 0) txConfig.addDestination(loserPayoutAddress, loserPayoutAmount.subtract(feeEstimate)); // loser pays fee - } - numAttempts++; - try { - payoutTx = trade.getWallet().createTx(txConfig); - } catch (MoneroError e) { - // exception expected // TODO: better way of estimating fee? - } - } - if (payoutTx == null) throw new RuntimeException("Failed to generate dispute payout tx after " + numAttempts + " attempts"); - log.info("Dispute payout transaction generated on attempt {}", numAttempts); - - // save updated multisig hex - trade.getSelf().setUpdatedMultisigHex(trade.getWallet().exportMultisigHex()); - return payoutTx; + return null; // can be null if already published or we don't have receiver's multisig hex } private Tuple2 getNodeAddressPubKeyRingTuple(Dispute dispute) { diff --git a/core/src/main/resources/i18n/displayStrings.properties b/core/src/main/resources/i18n/displayStrings.properties index 52b270fc63..64b4f3d3e9 100644 --- a/core/src/main/resources/i18n/displayStrings.properties +++ b/core/src/main/resources/i18n/displayStrings.properties @@ -1849,8 +1849,7 @@ disputeSummaryWindow.close.txDetails.buyer=Buyer receives {0} on address: {1}\n disputeSummaryWindow.close.txDetails.seller=Seller receives {0} on address: {1}\n disputeSummaryWindow.close.txDetails=Spending: {0}\n\ {1}{2}\ - Transaction fee: {3} ({4} satoshis/vbyte)\n\ - Transaction vsize: {5} vKb\n\n\ + Transaction fee: {3}\n\n\ Are you sure you want to publish this transaction? disputeSummaryWindow.close.noPayout.headline=Close without any payout diff --git a/core/src/main/resources/i18n/displayStrings_cs.properties b/core/src/main/resources/i18n/displayStrings_cs.properties index 05c58ac86c..7a3ace61ce 100644 --- a/core/src/main/resources/i18n/displayStrings_cs.properties +++ b/core/src/main/resources/i18n/displayStrings_cs.properties @@ -1395,7 +1395,7 @@ disputeSummaryWindow.close.txDetails.headline=Zveřejněte transakci vrácení p disputeSummaryWindow.close.txDetails.buyer=Kupující obdrží {0} na adresu: {1}\n # suppress inspection "TrailingSpacesInProperty" disputeSummaryWindow.close.txDetails.seller=Prodejce obdrží {0} na adresu: {1}\n -disputeSummaryWindow.close.txDetails=Výdaje: {0}\n{1} {2} Transakční poplatek: {3} ({4} satoshi/vbyte)\nTransakční vsize: {5} vKb\n\nOpravdu chcete tuto transakci zveřejnit? +disputeSummaryWindow.close.txDetails=Výdaje: {0}\n{1} {2} Transakční poplatek: {3}\n\nOpravdu chcete tuto transakci zveřejnit? disputeSummaryWindow.close.noPayout.headline=Uzavřít bez jakékoli výplaty disputeSummaryWindow.close.noPayout.text=Chcete zavřít bez výplaty? diff --git a/core/src/main/resources/i18n/displayStrings_de.properties b/core/src/main/resources/i18n/displayStrings_de.properties index fe4741f8c7..f6857e5505 100644 --- a/core/src/main/resources/i18n/displayStrings_de.properties +++ b/core/src/main/resources/i18n/displayStrings_de.properties @@ -1395,7 +1395,7 @@ disputeSummaryWindow.close.txDetails.headline=Rückerstattungstransaktion veröf disputeSummaryWindow.close.txDetails.buyer=Käufer erhält {0} an Adresse: {1}\n # suppress inspection "TrailingSpacesInProperty" disputeSummaryWindow.close.txDetails.seller=Verkäufer erhält {0} an Adresse: {1}\n -disputeSummaryWindow.close.txDetails=Ausgaben: {0}\n{1}{2}Transaktionsgebühr: {3} ({4} Satoshis/Byte)\nTransaktionsgröße: {5} Kb\n\nSind Sie sicher, dass Sie diese Transaktion veröffentlichen möchten? +disputeSummaryWindow.close.txDetails=Ausgaben: {0}\n{1}{2}Transaktionsgebühr: {3}\n\nSind Sie sicher, dass Sie diese Transaktion veröffentlichen möchten? disputeSummaryWindow.close.noPayout.headline=Ohne Auszahlung schließen disputeSummaryWindow.close.noPayout.text=Wollen Sie schließen ohne eine Auszahlung zu tätigen? diff --git a/core/src/main/resources/i18n/displayStrings_es.properties b/core/src/main/resources/i18n/displayStrings_es.properties index 2af0060abe..125ad992b7 100644 --- a/core/src/main/resources/i18n/displayStrings_es.properties +++ b/core/src/main/resources/i18n/displayStrings_es.properties @@ -1395,7 +1395,7 @@ disputeSummaryWindow.close.txDetails.headline=Publicar transacción de devoluci disputeSummaryWindow.close.txDetails.buyer=El comprador recibe {0} en la dirección: {1}\n # suppress inspection "TrailingSpacesInProperty" disputeSummaryWindow.close.txDetails.seller=El vendedor recibe {0} en la dirección: {1}\n -disputeSummaryWindow.close.txDetails=Gastando: {0}\n{1}{2}Tasa de transacción: {3} ({4} satoshis/vbyte)\nTamaño virtual de transacción: {5} vKb\n\n¿Está seguro de que quiere publicar esta transacción?\n +disputeSummaryWindow.close.txDetails=Gastando: {0}\n{1}{2}Tasa de transacción: {3}\n\n¿Está seguro de que quiere publicar esta transacción?\n disputeSummaryWindow.close.noPayout.headline=Cerrar sin realizar algún pago disputeSummaryWindow.close.noPayout.text=¿Quiere cerrar sin realizar algún pago? diff --git a/core/src/main/resources/i18n/displayStrings_fa.properties b/core/src/main/resources/i18n/displayStrings_fa.properties index 8e70e9bd2a..0379d38bb8 100644 --- a/core/src/main/resources/i18n/displayStrings_fa.properties +++ b/core/src/main/resources/i18n/displayStrings_fa.properties @@ -1395,7 +1395,7 @@ disputeSummaryWindow.close.txDetails.headline=Publish refund transaction disputeSummaryWindow.close.txDetails.buyer=Buyer receives {0} on address: {1}\n # suppress inspection "TrailingSpacesInProperty" disputeSummaryWindow.close.txDetails.seller=Seller receives {0} on address: {1}\n -disputeSummaryWindow.close.txDetails=Spending: {0}\n{1}{2}Transaction fee: {3} ({4} satoshis/vbyte)\nTransaction vsize: {5} vKb\n\nAre you sure you want to publish this transaction? +disputeSummaryWindow.close.txDetails=Spending: {0}\n{1}{2}Transaction fee: {3}\n\nAre you sure you want to publish this transaction? disputeSummaryWindow.close.noPayout.headline=Close without any payout disputeSummaryWindow.close.noPayout.text=Do you want to close without doing any payout? diff --git a/core/src/main/resources/i18n/displayStrings_fr.properties b/core/src/main/resources/i18n/displayStrings_fr.properties index e3f9e9def8..a9f1918c7d 100644 --- a/core/src/main/resources/i18n/displayStrings_fr.properties +++ b/core/src/main/resources/i18n/displayStrings_fr.properties @@ -1396,7 +1396,7 @@ disputeSummaryWindow.close.txDetails.headline=Publier la transaction de rembours disputeSummaryWindow.close.txDetails.buyer=L''acheteur reçoit {0} à l''adresse: {1}\n # suppress inspection "TrailingSpacesInProperty" disputeSummaryWindow.close.txDetails.seller=Le vendeur reçoit {0} à l''adresse: {1}\n -disputeSummaryWindow.close.txDetails=Dépenser: {0}\n{1}{2}Frais de transaction: {3} ({4} satoshis/vbyte)\nTaille virtuelle de la transaction: {5} vKb\n\nÊtes-vous sûr de vouloir publier cette transaction ? +disputeSummaryWindow.close.txDetails=Dépenser: {0}\n{1}{2}Frais de transaction: {3}\n\nÊtes-vous sûr de vouloir publier cette transaction ? disputeSummaryWindow.close.noPayout.headline=Fermé sans paiement disputeSummaryWindow.close.noPayout.text=Voulez-vous fermer sans paiement ? diff --git a/core/src/main/resources/i18n/displayStrings_it.properties b/core/src/main/resources/i18n/displayStrings_it.properties index 4c7f48d841..b32fc32352 100644 --- a/core/src/main/resources/i18n/displayStrings_it.properties +++ b/core/src/main/resources/i18n/displayStrings_it.properties @@ -1395,7 +1395,7 @@ disputeSummaryWindow.close.txDetails.headline=Pubblica transazione di rimborso disputeSummaryWindow.close.txDetails.buyer=L'acquirente riceve {0} all'indirizzo: {1}\n # suppress inspection "TrailingSpacesInProperty" disputeSummaryWindow.close.txDetails.seller=Il venditore riceve {0} all'indirizzo: {1}\n -disputeSummaryWindow.close.txDetails=Spending: {0}\n{1}{2}Transaction fee: {3} ({4} satoshis/vbyte)\nTransaction vsize: {5} vKb\n\nAre you sure you want to publish this transaction? +disputeSummaryWindow.close.txDetails=Spending: {0}\n{1}{2}Transaction fee: {3}\n\nAre you sure you want to publish this transaction? disputeSummaryWindow.close.noPayout.headline=Close without any payout disputeSummaryWindow.close.noPayout.text=Do you want to close without doing any payout? diff --git a/core/src/main/resources/i18n/displayStrings_ja.properties b/core/src/main/resources/i18n/displayStrings_ja.properties index 2720f5e98d..da2705918e 100644 --- a/core/src/main/resources/i18n/displayStrings_ja.properties +++ b/core/src/main/resources/i18n/displayStrings_ja.properties @@ -1395,7 +1395,7 @@ disputeSummaryWindow.close.txDetails.headline=払い戻しトランザクショ disputeSummaryWindow.close.txDetails.buyer=買い手が {0} を受けます、入金先アドレス: {1}\n # suppress inspection "TrailingSpacesInProperty" disputeSummaryWindow.close.txDetails.seller=売り手が {0} を受けます、入金先アドレス: {1}\n -disputeSummaryWindow.close.txDetails=支払う金額: {0}\n{1}{2}トランザクション手数料: {3}({4}サトシ/vバイト)\nトランザクションvサイズ: {5} vKb\n\nこのトランザクションを発行してもよろしいですか? +disputeSummaryWindow.close.txDetails=支払う金額: {0}\n{1}{2}トランザクション手数料: {3}\n\nこのトランザクションを発行してもよろしいですか? disputeSummaryWindow.close.noPayout.headline=支払いなしで閉じる disputeSummaryWindow.close.noPayout.text=支払いなしで閉じてもよろしいですか? diff --git a/core/src/main/resources/i18n/displayStrings_pt-br.properties b/core/src/main/resources/i18n/displayStrings_pt-br.properties index fb0a03b405..0c6a070555 100644 --- a/core/src/main/resources/i18n/displayStrings_pt-br.properties +++ b/core/src/main/resources/i18n/displayStrings_pt-br.properties @@ -1399,7 +1399,7 @@ disputeSummaryWindow.close.txDetails.headline=Publicar transação de reembolso disputeSummaryWindow.close.txDetails.buyer=Comprador recebe {0} no endereço: {1}\n # suppress inspection "TrailingSpacesInProperty" disputeSummaryWindow.close.txDetails.seller=Vendedor recebe {0} no endereço: {1}\n -disputeSummaryWindow.close.txDetails=Spending: {0}\n{1}{2}Transaction fee: {3} ({4} satoshis/vbyte)\nTransaction vsize: {5} vKb\n\nAre you sure you want to publish this transaction? +disputeSummaryWindow.close.txDetails=Spending: {0}\n{1}{2}Transaction fee: {3}\n\nAre you sure you want to publish this transaction? disputeSummaryWindow.close.noPayout.headline=Close without any payout disputeSummaryWindow.close.noPayout.text=Do you want to close without doing any payout? diff --git a/core/src/main/resources/i18n/displayStrings_pt.properties b/core/src/main/resources/i18n/displayStrings_pt.properties index 0b55e62f79..1fb6e2bb55 100644 --- a/core/src/main/resources/i18n/displayStrings_pt.properties +++ b/core/src/main/resources/i18n/displayStrings_pt.properties @@ -1395,7 +1395,7 @@ disputeSummaryWindow.close.txDetails.headline=Publicar transação de reembolso disputeSummaryWindow.close.txDetails.buyer=O comprador recebe {0} no endereço: {1}\n # suppress inspection "TrailingSpacesInProperty" disputeSummaryWindow.close.txDetails.seller=O vendedor recebe {0} no endereço: {1}\n -disputeSummaryWindow.close.txDetails=Spending: {0}\n{1}{2}Transaction fee: {3} ({4} satoshis/vbyte)\nTransaction vsize: {5} vKb\n\nAre you sure you want to publish this transaction? +disputeSummaryWindow.close.txDetails=Spending: {0}\n{1}{2}Transaction fee: {3}\n\nAre you sure you want to publish this transaction? disputeSummaryWindow.close.noPayout.headline=Close without any payout disputeSummaryWindow.close.noPayout.text=Do you want to close without doing any payout? diff --git a/core/src/main/resources/i18n/displayStrings_ru.properties b/core/src/main/resources/i18n/displayStrings_ru.properties index 53cc9b0028..0b63e2452e 100644 --- a/core/src/main/resources/i18n/displayStrings_ru.properties +++ b/core/src/main/resources/i18n/displayStrings_ru.properties @@ -1395,7 +1395,7 @@ disputeSummaryWindow.close.txDetails.headline=Publish refund transaction disputeSummaryWindow.close.txDetails.buyer=Buyer receives {0} on address: {1}\n # suppress inspection "TrailingSpacesInProperty" disputeSummaryWindow.close.txDetails.seller=Seller receives {0} on address: {1}\n -disputeSummaryWindow.close.txDetails=Spending: {0}\n{1}{2}Transaction fee: {3} ({4} satoshis/vbyte)\nTransaction vsize: {5} vKb\n\nAre you sure you want to publish this transaction? +disputeSummaryWindow.close.txDetails=Spending: {0}\n{1}{2}Transaction fee: {3}\n\nAre you sure you want to publish this transaction? disputeSummaryWindow.close.noPayout.headline=Close without any payout disputeSummaryWindow.close.noPayout.text=Do you want to close without doing any payout? diff --git a/core/src/main/resources/i18n/displayStrings_th.properties b/core/src/main/resources/i18n/displayStrings_th.properties index c7eb251c2f..2e651b24b2 100644 --- a/core/src/main/resources/i18n/displayStrings_th.properties +++ b/core/src/main/resources/i18n/displayStrings_th.properties @@ -1395,7 +1395,7 @@ disputeSummaryWindow.close.txDetails.headline=Publish refund transaction disputeSummaryWindow.close.txDetails.buyer=Buyer receives {0} on address: {1}\n # suppress inspection "TrailingSpacesInProperty" disputeSummaryWindow.close.txDetails.seller=Seller receives {0} on address: {1}\n -disputeSummaryWindow.close.txDetails=Spending: {0}\n{1}{2}Transaction fee: {3} ({4} satoshis/vbyte)\nTransaction vsize: {5} vKb\n\nAre you sure you want to publish this transaction? +disputeSummaryWindow.close.txDetails=Spending: {0}\n{1}{2}Transaction fee: {3}\n\nAre you sure you want to publish this transaction? disputeSummaryWindow.close.noPayout.headline=Close without any payout disputeSummaryWindow.close.noPayout.text=Do you want to close without doing any payout? diff --git a/core/src/main/resources/i18n/displayStrings_vi.properties b/core/src/main/resources/i18n/displayStrings_vi.properties index c5e9e1466f..62d35d7007 100644 --- a/core/src/main/resources/i18n/displayStrings_vi.properties +++ b/core/src/main/resources/i18n/displayStrings_vi.properties @@ -1397,7 +1397,7 @@ disputeSummaryWindow.close.txDetails.headline=Publish refund transaction disputeSummaryWindow.close.txDetails.buyer=Buyer receives {0} on address: {1}\n # suppress inspection "TrailingSpacesInProperty" disputeSummaryWindow.close.txDetails.seller=Seller receives {0} on address: {1}\n -disputeSummaryWindow.close.txDetails=Spending: {0}\n{1}{2}Transaction fee: {3} ({4} satoshis/vbyte)\nTransaction vsize: {5} vKb\n\nAre you sure you want to publish this transaction? +disputeSummaryWindow.close.txDetails=Spending: {0}\n{1}{2}Transaction fee: {3}\n\nAre you sure you want to publish this transaction? disputeSummaryWindow.close.noPayout.headline=Close without any payout disputeSummaryWindow.close.noPayout.text=Do you want to close without doing any payout? diff --git a/core/src/main/resources/i18n/displayStrings_zh-hans.properties b/core/src/main/resources/i18n/displayStrings_zh-hans.properties index 481bf196af..3bb63dfa20 100644 --- a/core/src/main/resources/i18n/displayStrings_zh-hans.properties +++ b/core/src/main/resources/i18n/displayStrings_zh-hans.properties @@ -1395,7 +1395,7 @@ disputeSummaryWindow.close.txDetails.headline=发布交易退款 disputeSummaryWindow.close.txDetails.buyer=买方收到{0}在地址:{1} # suppress inspection "TrailingSpacesInProperty" disputeSummaryWindow.close.txDetails.seller=卖方收到{0}在地址:{1} -disputeSummaryWindow.close.txDetails=费用:{0}\n{1}{2}交易费:{3}({4}satoshis/byte)\n事务大小:{5} Kb\n\n您确定要发布此交易吗? +disputeSummaryWindow.close.txDetails=费用:{0}\n{1}{2}交易费:{3}\n\n您确定要发布此交易吗? disputeSummaryWindow.close.noPayout.headline=未支付关闭 disputeSummaryWindow.close.noPayout.text=你想要在未作支付的情况下关闭吗? diff --git a/core/src/main/resources/i18n/displayStrings_zh-hant.properties b/core/src/main/resources/i18n/displayStrings_zh-hant.properties index e723b14bd8..8bd600cc50 100644 --- a/core/src/main/resources/i18n/displayStrings_zh-hant.properties +++ b/core/src/main/resources/i18n/displayStrings_zh-hant.properties @@ -1395,7 +1395,7 @@ disputeSummaryWindow.close.txDetails.headline=發佈交易退款 disputeSummaryWindow.close.txDetails.buyer=買方收到{0}在地址:{1} # suppress inspection "TrailingSpacesInProperty" disputeSummaryWindow.close.txDetails.seller=賣方收到{0}在地址:{1} -disputeSummaryWindow.close.txDetails=費用:{0}\n{1}{2}交易費:{3}({4}satoshis/byte)\n事務大小:{5} Kb\n\n您確定要發佈此交易嗎? +disputeSummaryWindow.close.txDetails=費用:{0}\n{1}{2}交易費:{3}\n\n您確定要發佈此交易嗎? disputeSummaryWindow.close.noPayout.headline=未支付關閉 disputeSummaryWindow.close.noPayout.text=你想要在未作支付的情況下關閉嗎? diff --git a/desktop/src/main/java/bisq/desktop/main/overlays/windows/DisputeSummaryWindow.java b/desktop/src/main/java/bisq/desktop/main/overlays/windows/DisputeSummaryWindow.java index 663e81eba2..ef5ecdecae 100644 --- a/desktop/src/main/java/bisq/desktop/main/overlays/windows/DisputeSummaryWindow.java +++ b/desktop/src/main/java/bisq/desktop/main/overlays/windows/DisputeSummaryWindow.java @@ -30,6 +30,7 @@ import bisq.desktop.util.Layout; import bisq.core.btc.wallet.TradeWalletService; import bisq.core.btc.wallet.XmrWalletService; import bisq.core.locale.Res; +import bisq.core.support.SupportType; import bisq.core.support.dispute.Dispute; import bisq.core.support.dispute.DisputeList; import bisq.core.support.dispute.DisputeManager; @@ -38,6 +39,7 @@ import bisq.core.support.dispute.arbitration.ArbitrationManager; import bisq.core.support.dispute.mediation.MediationManager; import bisq.core.support.dispute.refund.RefundManager; import bisq.core.trade.Contract; +import bisq.core.trade.HavenoUtils; import bisq.core.trade.Trade; import bisq.core.trade.TradeManager; import bisq.core.util.FormattingUtils; @@ -78,6 +80,7 @@ import java.util.Date; import java.util.Optional; import lombok.extern.slf4j.Slf4j; +import monero.wallet.model.MoneroTxWallet; import static bisq.desktop.util.FormBuilder.add2ButtonsWithBox; import static bisq.desktop.util.FormBuilder.addConfirmationLabelLabel; @@ -113,7 +116,7 @@ public class DisputeSummaryWindow extends Overlay { private ChangeListener customRadioButtonSelectedListener; private ChangeListener reasonToggleSelectionListener; private InputTextField buyerPayoutAmountInputTextField, sellerPayoutAmountInputTextField; - private ChangeListener buyerPayoutAmountListener, sellerPayoutAmountListener; + private ChangeListener buyerPayoutAmountListener, sellerPayoutAmountListener; private ChangeListener tradeAmountToggleGroupListener; @@ -319,16 +322,16 @@ public class DisputeSummaryWindow extends Overlay { tradeAmountToggleGroupListener = (observable, oldValue, newValue) -> applyPayoutAmounts(newValue); tradeAmountToggleGroup.selectedToggleProperty().addListener(tradeAmountToggleGroupListener); - buyerPayoutAmountListener = (observable1, oldValue1, newValue1) -> applyCustomAmounts(buyerPayoutAmountInputTextField); - sellerPayoutAmountListener = (observable1, oldValue1, newValue1) -> applyCustomAmounts(sellerPayoutAmountInputTextField); + buyerPayoutAmountListener = (observable, oldValue, newValue) -> applyCustomAmounts(buyerPayoutAmountInputTextField, oldValue, newValue); + sellerPayoutAmountListener = (observable, oldValue, newValue) -> applyCustomAmounts(sellerPayoutAmountInputTextField, oldValue, newValue); customRadioButtonSelectedListener = (observable, oldValue, newValue) -> { buyerPayoutAmountInputTextField.setEditable(newValue); sellerPayoutAmountInputTextField.setEditable(newValue); if (newValue) { - buyerPayoutAmountInputTextField.textProperty().addListener(buyerPayoutAmountListener); - sellerPayoutAmountInputTextField.textProperty().addListener(sellerPayoutAmountListener); + buyerPayoutAmountInputTextField.focusedProperty().addListener(buyerPayoutAmountListener); + sellerPayoutAmountInputTextField.focusedProperty().addListener(sellerPayoutAmountListener); } else { removePayoutAmountListeners(); } @@ -338,11 +341,10 @@ public class DisputeSummaryWindow extends Overlay { private void removePayoutAmountListeners() { if (buyerPayoutAmountInputTextField != null && buyerPayoutAmountListener != null) - buyerPayoutAmountInputTextField.textProperty().removeListener(buyerPayoutAmountListener); + buyerPayoutAmountInputTextField.focusedProperty().removeListener(buyerPayoutAmountListener); if (sellerPayoutAmountInputTextField != null && sellerPayoutAmountListener != null) - sellerPayoutAmountInputTextField.textProperty().removeListener(sellerPayoutAmountListener); - + sellerPayoutAmountInputTextField.focusedProperty().removeListener(sellerPayoutAmountListener); } private boolean isPayoutAmountValid() { @@ -368,12 +370,12 @@ public class DisputeSummaryWindow extends Overlay { } } - private void applyCustomAmounts(InputTextField inputTextField) { -// // We only apply adjustments at focus out, otherwise we cannot enter certain values if we update at each -// // keystroke. -// if (!oldFocusValue || newFocusValue) { -// return; -// } + private void applyCustomAmounts(InputTextField inputTextField, boolean oldFocusValue, boolean newFocusValue) { + // We only apply adjustments at focus out, otherwise we cannot enter certain values if we update at each + // keystroke. + if (!oldFocusValue || newFocusValue) { + return; + } Contract contract = dispute.getContract(); Coin available = contract.getTradeAmount() @@ -562,188 +564,89 @@ public class DisputeSummaryWindow extends Overlay { } private void addButtons(Contract contract) { - Tuple3 tuple = add2ButtonsWithBox(gridPane, ++rowIndex, - Res.get("disputeSummaryWindow.close.button"), - Res.get("shared.cancel"), 15, true); - Button closeTicketButton = tuple.first; - closeTicketButton.disableProperty().bind(Bindings.createBooleanBinding( - () -> tradeAmountToggleGroup.getSelectedToggle() == null - || summaryNotesTextArea.getText() == null - || summaryNotesTextArea.getText().length() == 0 - || !isPayoutAmountValid(), - tradeAmountToggleGroup.selectedToggleProperty(), - summaryNotesTextArea.textProperty(), - buyerPayoutAmountInputTextField.textProperty(), - sellerPayoutAmountInputTextField.textProperty())); + Tuple3 tuple = add2ButtonsWithBox(gridPane, ++rowIndex, + Res.get("disputeSummaryWindow.close.button"), + Res.get("shared.cancel"), 15, true); + Button closeTicketButton = tuple.first; + closeTicketButton.disableProperty().bind(Bindings.createBooleanBinding( + () -> tradeAmountToggleGroup.getSelectedToggle() == null + || summaryNotesTextArea.getText() == null + || summaryNotesTextArea.getText().length() == 0 + || !isPayoutAmountValid(), + tradeAmountToggleGroup.selectedToggleProperty(), + summaryNotesTextArea.textProperty(), + buyerPayoutAmountInputTextField.textProperty(), + sellerPayoutAmountInputTextField.textProperty())); - Button cancelButton = tuple.second; + Button cancelButton = tuple.second; - closeTicketButton.setOnAction(e -> { - doClose(closeTicketButton); + closeTicketButton.setOnAction(e -> { -// if (dispute.getDepositTxSerialized() == null) { -// log.warn("dispute.getDepositTxSerialized is null"); -// return; -// } -// -// if (dispute.getSupportType() == SupportType.REFUND && -// peersDisputeOptional.isPresent() && -// !peersDisputeOptional.get().isClosed()) { -// showPayoutTxConfirmation(contract, -// disputeResult, -// () -> doCloseIfValid(closeTicketButton)); -// } else { -// doCloseIfValid(closeTicketButton); -// } - }); + // create payout tx + MoneroTxWallet payoutTx = arbitrationManager.createDisputePayoutTx(trade, dispute, disputeResult); - cancelButton.setOnAction(e -> { - dispute.setDisputeResult(disputeResult); - checkNotNull(getDisputeManager(dispute)).requestPersistence(); - hide(); - }); + // show confirmation + if (dispute.getSupportType() == SupportType.ARBITRATION && + peersDisputeOptional.isPresent() && + !peersDisputeOptional.get().isClosed()) { + showPayoutTxConfirmation(contract, + disputeResult, + payoutTx, + () -> doClose(closeTicketButton, payoutTx)); + } else { + doClose(closeTicketButton, payoutTx); + } + }); + + cancelButton.setOnAction(e -> { + dispute.setDisputeResult(disputeResult); + checkNotNull(getDisputeManager(dispute)).requestPersistence(); + hide(); + }); } - private void showPayoutTxConfirmation(Contract contract, DisputeResult disputeResult, ResultHandler resultHandler) { - throw new RuntimeException("DisputeSummaryWindow.showPayoutTxConfimration() needs updated for XMR"); -// Coin buyerPayoutAmount = disputeResult.getBuyerPayoutAmount(); -// String buyerPayoutAddressString = contract.getBuyerPayoutAddressString(); -// Coin sellerPayoutAmount = disputeResult.getSellerPayoutAmount(); -// String sellerPayoutAddressString = contract.getSellerPayoutAddressString(); -// Coin outputAmount = buyerPayoutAmount.add(sellerPayoutAmount); -// Tuple2 feeTuple = txFeeEstimationService.getEstimatedFeeAndTxSize(outputAmount, feeService, btcWalletService); -// Coin fee = feeTuple.first; -// Integer txSize = feeTuple.second; -// double feePerByte = CoinUtil.getFeePerByte(fee, txSize); -// double kb = txSize / 1000d; -// Coin inputAmount = outputAmount.add(fee); -// String buyerDetails = ""; -// if (buyerPayoutAmount.isPositive()) { -// buyerDetails = Res.get("disputeSummaryWindow.close.txDetails.buyer", -// formatter.formatCoinWithCode(buyerPayoutAmount), -// buyerPayoutAddressString); -// } -// String sellerDetails = ""; -// if (sellerPayoutAmount.isPositive()) { -// sellerDetails = Res.get("disputeSummaryWindow.close.txDetails.seller", -// formatter.formatCoinWithCode(sellerPayoutAmount), -// sellerPayoutAddressString); -// } -// if (outputAmount.isPositive()) { -// new Popup().width(900) -// .headLine(Res.get("disputeSummaryWindow.close.txDetails.headline")) -// .confirmation(Res.get("disputeSummaryWindow.close.txDetails", -// formatter.formatCoinWithCode(inputAmount), -// buyerDetails, -// sellerDetails, -// formatter.formatCoinWithCode(fee), -// feePerByte, -// kb)) -// .actionButtonText(Res.get("shared.yes")) -// .onAction(() -> { -// doPayout(buyerPayoutAmount, -// sellerPayoutAmount, -// fee, -// buyerPayoutAddressString, -// sellerPayoutAddressString, -// resultHandler); -// }) -// .closeButtonText(Res.get("shared.cancel")) -// .show(); -// } else { -// // No payout will be made -// new Popup().headLine(Res.get("disputeSummaryWindow.close.noPayout.headline")) -// .confirmation(Res.get("disputeSummaryWindow.close.noPayout.text")) -// .actionButtonText(Res.get("shared.yes")) -// .onAction(resultHandler::handleResult) -// .closeButtonText(Res.get("shared.cancel")) -// .show(); -// } + private void showPayoutTxConfirmation(Contract contract, DisputeResult disputeResult, MoneroTxWallet payoutTx, ResultHandler resultHandler) { + Coin buyerPayoutAmount = disputeResult.getBuyerPayoutAmount(); + String buyerPayoutAddressString = contract.getBuyerPayoutAddressString(); + Coin sellerPayoutAmount = disputeResult.getSellerPayoutAmount(); + String sellerPayoutAddressString = contract.getSellerPayoutAddressString(); + Coin outputAmount = buyerPayoutAmount.add(sellerPayoutAmount); + String buyerDetails = ""; + if (buyerPayoutAmount.isPositive()) { + buyerDetails = Res.get("disputeSummaryWindow.close.txDetails.buyer", + formatter.formatCoinWithCode(buyerPayoutAmount), + buyerPayoutAddressString); + } + String sellerDetails = ""; + if (sellerPayoutAmount.isPositive()) { + sellerDetails = Res.get("disputeSummaryWindow.close.txDetails.seller", + formatter.formatCoinWithCode(sellerPayoutAmount), + sellerPayoutAddressString); + } + if (outputAmount.isPositive()) { + new Popup().width(900) + .headLine(Res.get("disputeSummaryWindow.close.txDetails.headline")) + .confirmation(Res.get("disputeSummaryWindow.close.txDetails", + formatter.formatCoinWithCode(outputAmount), + buyerDetails, + sellerDetails, + formatter.formatCoinWithCode(HavenoUtils.atomicUnitsToCoin(payoutTx.getFee())))) + .actionButtonText(Res.get("shared.yes")) + .onAction(() -> resultHandler.handleResult()) + .closeButtonText(Res.get("shared.cancel")) + .show(); + } else { + // No payout will be made + new Popup().headLine(Res.get("disputeSummaryWindow.close.noPayout.headline")) + .confirmation(Res.get("disputeSummaryWindow.close.noPayout.text")) + .actionButtonText(Res.get("shared.yes")) + .onAction(resultHandler::handleResult) + .closeButtonText(Res.get("shared.cancel")) + .show(); + } } - private void doPayout(Coin buyerPayoutAmount, - Coin sellerPayoutAmount, - Coin fee, - String buyerPayoutAddressString, - String sellerPayoutAddressString, - ResultHandler resultHandler) { - throw new RuntimeException("DisputeSummaryWindow.doPayout() needs updated for XMR"); -// try { -// Transaction tx = btcWalletService.createRefundPayoutTx(buyerPayoutAmount, -// sellerPayoutAmount, -// fee, -// buyerPayoutAddressString, -// sellerPayoutAddressString); -// tradeWalletService.broadcastTx(tx, new TxBroadcaster.Callback() { -// @Override -// public void onSuccess(Transaction transaction) { -// resultHandler.handleResult(); -// } -// -// @Override -// public void onFailure(TxBroadcastException exception) { -// log.error("TxBroadcastException at doPayout", exception); -// new Popup().error(exception.toString()).show(); -// } -// }); -// } catch (InsufficientMoneyException | WalletException | TransactionVerificationException e) { -// log.error("Exception at doPayout", e); -// new Popup().error(e.toString()).show(); -// } - } - - private void doCloseIfValid(Button closeTicketButton) { - throw new RuntimeException("DisputeSummaryWindow.doCloseIfValid() needs updated for XMR"); -// var disputeManager = checkNotNull(getDisputeManager(dispute)); -// try { -// TradeDataValidation.validateDonationAddress(dispute.getDonationAddressOfDelayedPayoutTx()); -// TradeDataValidation.testIfDisputeTriesReplay(dispute, disputeManager.getDisputesAsObservableList()); -// doClose(closeTicketButton); -// } catch (TradeDataValidation.AddressException exception) { -// String addressAsString = dispute.getDonationAddressOfDelayedPayoutTx(); -// String tradeId = dispute.getTradeId(); -// -// // For mediators we do not enforce that the case cannot be closed to stay flexible, -// // but for refund agents we do. -// if (disputeManager instanceof MediationManager) { -// new Popup().width(900) -// .warning(Res.get("support.warning.disputesWithInvalidDonationAddress", -// addressAsString, -// tradeId, -// Res.get("support.warning.disputesWithInvalidDonationAddress.mediator"))) -// .onAction(() -> { -// doClose(closeTicketButton); -// }) -// .actionButtonText(Res.get("shared.yes")) -// .closeButtonText(Res.get("shared.no")) -// .show(); -// } else { -// new Popup().width(900) -// .warning(Res.get("support.warning.disputesWithInvalidDonationAddress", -// addressAsString, -// tradeId, -// Res.get("support.warning.disputesWithInvalidDonationAddress.refundAgent"))) -// .show(); -// } -// } catch (TradeDataValidation.DisputeReplayException exception) { -// if (disputeManager instanceof MediationManager) { -// new Popup().width(900) -// .warning(exception.getMessage()) -// .onAction(() -> { -// doClose(closeTicketButton); -// }) -// .actionButtonText(Res.get("shared.yes")) -// .closeButtonText(Res.get("shared.no")) -// .show(); -// } else { -// new Popup().width(900) -// .warning(exception.getMessage()) -// .show(); -// } -// } - } - - private void doClose(Button closeTicketButton) { + private void doClose(Button closeTicketButton, MoneroTxWallet payoutTx) { DisputeManager> disputeManager = getDisputeManager(dispute); if (disputeManager == null) { return; @@ -752,15 +655,17 @@ public class DisputeSummaryWindow extends Overlay { summaryNotesTextArea.textProperty().unbindBidirectional(disputeResult.summaryNotesProperty()); disputeResult.setCloseDate(new Date()); - disputesService.closeDisputeTicket(disputeManager, dispute, disputeResult, () -> { + disputesService.closeDisputeTicket(disputeManager, dispute, disputeResult, payoutTx, () -> { if (peersDisputeOptional.isPresent() && !peersDisputeOptional.get().isClosed() && !DevEnv.isDevMode()) { new Popup().attention(Res.get("disputeSummaryWindow.close.closePeer")).show(); } disputeManager.requestPersistence(); + closeTicketButton.disableProperty().unbind(); + hide(); + }, (errMessage, err) -> { + log.error(errMessage); + new Popup().error(err.toString()).show(); }); - - closeTicketButton.disableProperty().unbind(); - hide(); } private DisputeManager> getDisputeManager(Dispute dispute) { @@ -818,7 +723,7 @@ public class DisputeSummaryWindow extends Overlay { sellerPayoutAmount.equals(sellerSecurityDeposit)) { buyerGetsTradeAmountRadioButton.setSelected(true); } else if (buyerPayoutAmount.equals(tradeAmount.add(buyerSecurityDeposit).add(sellerSecurityDeposit)) && - sellerPayoutAmount.equals(Coin.ZERO)) { // TODO (woodser): apply min payout to incentivize loser (see post v1.1.7) + sellerPayoutAmount.equals(Coin.ZERO)) { buyerGetsAllRadioButton.setSelected(true); } else if (sellerPayoutAmount.equals(tradeAmount.add(sellerSecurityDeposit)) && buyerPayoutAmount.equals(buyerSecurityDeposit)) {