diff --git a/core/src/main/java/bisq/core/offer/OpenOfferManager.java b/core/src/main/java/bisq/core/offer/OpenOfferManager.java index 13ab8a9ae0..ec081c3006 100644 --- a/core/src/main/java/bisq/core/offer/OpenOfferManager.java +++ b/core/src/main/java/bisq/core/offer/OpenOfferManager.java @@ -232,7 +232,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe @Override public void readPersisted(Runnable completeHandler) { - + // read open offers persistenceManager.readPersisted(persisted -> { openOffers.setAll(persisted.getList()); @@ -496,12 +496,12 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe checkNotNull(offer.getMakerFee(), "makerFee must not be null"); boolean autoSplit = false; // TODO: support in api - + // TODO (woodser): validate offer - + // create open offer OpenOffer openOffer = new OpenOffer(offer, triggerPrice, autoSplit); - + // process open offer to schedule or post processUnpostedOffer(getOpenOffers(), openOffer, (transaction) -> { addOpenOffer(openOffer); @@ -702,6 +702,13 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe } } + + public ObservableList getObservableSignedOffersList() { + synchronized (signedOffers) { + return signedOffers.getObservableList(); + } + } + public ObservableList getObservableList() { return openOffers.getObservableList(); } @@ -711,7 +718,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe return openOffers.stream().filter(e -> e.getId().equals(offerId)).findFirst(); } } - + public Optional getSignedOfferById(String offerId) { synchronized (signedOffers) { return signedOffers.stream().filter(e -> e.getOfferId().equals(offerId)).findFirst(); @@ -939,11 +946,11 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe private void handleSignOfferRequest(SignOfferRequest request, NodeAddress peer) { log.info("Received SignOfferRequest from {} with offerId {} and uid {}", peer, request.getOfferId(), request.getUid()); - + boolean result = false; String errorMessage = null; try { - + // verify this node is an arbitrator Arbitrator thisArbitrator = user.getRegisteredArbitrator(); NodeAddress thisAddress = p2PService.getNetworkNode().getNodeAddress(); @@ -953,7 +960,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe sendAckMessage(request.getClass(), peer, request.getPubKeyRing(), request.getOfferId(), request.getUid(), false, errorMessage); return; } - + // verify arbitrator is signer of offer payload if (!thisAddress.equals(request.getOfferPayload().getArbitratorSigner())) { errorMessage = "Cannot sign offer because offer payload is for a different arbitrator"; @@ -961,7 +968,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe sendAckMessage(request.getClass(), peer, request.getPubKeyRing(), request.getOfferId(), request.getUid(), false, errorMessage); return; } - + // verify offer not seen before Optional openOfferOptional = getOpenOfferById(request.offerId); if (openOfferOptional.isPresent()) { @@ -980,7 +987,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe sendAckMessage(request.getClass(), peer, request.getPubKeyRing(), request.getOfferId(), request.getUid(), false, errorMessage); return; } - + // verify maker's reserve tx (double spend, trade fee, trade amount, mining fee) BigInteger sendAmount = HavenoUtils.coinToAtomicUnits(offer.getDirection() == OfferDirection.BUY ? Coin.ZERO : offer.getAmount()); BigInteger securityDeposit = HavenoUtils.coinToAtomicUnits(offer.getDirection() == OfferDirection.BUY ? offer.getBuyerSecurityDeposit() : offer.getSellerSecurityDeposit()); @@ -999,13 +1006,13 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe String signature = Sig.sign(keyRing.getSignatureKeyPair().getPrivate(), offerPayloadAsJson); OfferPayload signedOfferPayload = request.getOfferPayload(); signedOfferPayload.setArbitratorSignature(signature); - + // create record of signed offer SignedOffer signedOffer = new SignedOffer( System.currentTimeMillis(), signedOfferPayload.getId(), - offer.getAmount().longValue(), - HavenoUtils.getMakerFee(offer.getAmount()).longValue(), // TODO: these values are centineros, whereas reserve tx mining fee is BigInteger + HavenoUtils.coinToAtomicUnits(offer.getAmount()).longValueExact(), + HavenoUtils.coinToAtomicUnits(HavenoUtils.getMakerFee(offer.getAmount())).longValueExact(), request.getReserveTxHash(), request.getReserveTxHex(), request.getReserveTxKeyImages(), @@ -1049,7 +1056,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe sendAckMessage(request.getClass(), peer, request.getPubKeyRing(), request.getOfferId(), request.getUid(), result, errorMessage); } } - + private void handleSignOfferResponse(SignOfferResponse response, NodeAddress peer) { log.info("Received SignOfferResponse from {} with offerId {} and uid {}", peer, response.getOfferId(), response.getUid()); @@ -1122,7 +1129,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe if (openOffer.getState() == OpenOffer.State.AVAILABLE) { Offer offer = openOffer.getOffer(); if (preferences.getIgnoreTradersList().stream().noneMatch(fullAddress -> fullAddress.equals(peer.getFullAddress()))) { - + // maker signs taker's request String tradeRequestAsJson = JsonUtil.objectToJson(request.getTradeRequest()); makerSignature = Sig.sign(keyRing.getSignatureKeyPair().getPrivate(), tradeRequestAsJson); @@ -1204,7 +1211,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe private boolean apiUserDeniedByOffer(OfferAvailabilityRequest request) { return preferences.isDenyApiTaker() && request.isTakerApiUser(); } - + private boolean takerDeniedByMaker(OfferAvailabilityRequest request) { if (request.getTradeRequest() == null) return true; return false; // TODO (woodser): implement taker verification here, doing work of ApplyFilter and VerifyPeersAccountAgeWitness @@ -1251,7 +1258,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe /////////////////////////////////////////////////////////////////////////////////////////// // Update persisted offer if a new capability is required after a software update /////////////////////////////////////////////////////////////////////////////////////////// - + // TODO (woodser): arbitrator signature will be invalid if offer updated (exclude updateable fields from signature? re-sign?) private void maybeUpdatePersistedOffers() { diff --git a/core/src/main/java/bisq/core/trade/HavenoUtils.java b/core/src/main/java/bisq/core/trade/HavenoUtils.java index 3fcc997145..847fd40016 100644 --- a/core/src/main/java/bisq/core/trade/HavenoUtils.java +++ b/core/src/main/java/bisq/core/trade/HavenoUtils.java @@ -17,11 +17,6 @@ package bisq.core.trade; -import bisq.common.config.Config; -import bisq.common.crypto.Hash; -import bisq.common.crypto.PubKeyRing; -import bisq.common.crypto.Sig; -import bisq.common.util.Utilities; import bisq.core.offer.Offer; import bisq.core.offer.OfferPayload; import bisq.core.support.dispute.arbitration.ArbitrationManager; @@ -33,11 +28,27 @@ import bisq.core.util.JsonUtil; import bisq.core.util.ParsingUtils; import bisq.core.util.coin.CoinUtil; import bisq.network.p2p.NodeAddress; -import lombok.extern.slf4j.Slf4j; + +import bisq.common.config.Config; +import bisq.common.crypto.Hash; +import bisq.common.crypto.PubKeyRing; +import bisq.common.crypto.Sig; +import bisq.common.util.Utilities; + +import org.bitcoinj.core.Coin; +import org.bitcoinj.utils.MonetaryFormat; + +import com.google.common.base.CaseFormat; +import com.google.common.base.Charsets; + +import java.text.DecimalFormat; +import java.text.SimpleDateFormat; + +import java.net.URI; import java.math.BigDecimal; import java.math.BigInteger; -import java.net.URI; + import java.util.ArrayList; import java.util.Collection; import java.util.List; @@ -47,12 +58,9 @@ import java.util.concurrent.Executors; import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; -import javax.annotation.Nullable; +import lombok.extern.slf4j.Slf4j; -import org.bitcoinj.core.Coin; -import org.bitcoinj.utils.MonetaryFormat; -import com.google.common.base.CaseFormat; -import com.google.common.base.Charsets; +import javax.annotation.Nullable; /** * Collection of utilities. @@ -62,26 +70,19 @@ public class HavenoUtils { public static final String LOOPBACK_HOST = "127.0.0.1"; // local loopback address to host Monero node public static final String LOCALHOST = "localhost"; - - // multipliers to convert units - public static BigInteger CENTINEROS_AU_MULTIPLIER = new BigInteger("10000"); - private static BigInteger XMR_AU_MULTIPLIER = new BigInteger("1000000000000"); - - // global thread pool + public static final BigInteger CENTINEROS_AU_MULTIPLIER = new BigInteger("10000"); + private static final BigInteger XMR_AU_MULTIPLIER = new BigInteger("1000000000000"); + public static final DecimalFormat XMR_FORMATTER = new DecimalFormat("0.000000000000"); + public static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("dd-MM-yyyy HH:mm:ss"); private static final int POOL_SIZE = 10; private static final ExecutorService POOL = Executors.newFixedThreadPool(POOL_SIZE); - - // TODO: better way to share reference? - public static ArbitrationManager arbitrationManager; + + public static ArbitrationManager arbitrationManager; // TODO: better way to share reference? public static BigInteger coinToAtomicUnits(Coin coin) { return centinerosToAtomicUnits(coin.value); } - public static double coinToXmr(Coin coin) { - return atomicUnitsToXmr(coinToAtomicUnits(coin)); - } - public static BigInteger centinerosToAtomicUnits(long centineros) { return BigInteger.valueOf(centineros).multiply(CENTINEROS_AU_MULTIPLIER); } @@ -102,10 +103,18 @@ public class HavenoUtils { return atomicUnits.divide(CENTINEROS_AU_MULTIPLIER).longValueExact(); } - public static Coin atomicUnitsToCoin(BigInteger atomicUnits) { + public static Coin atomicUnitsToCoin(long atomicUnits) { return Coin.valueOf(atomicUnitsToCentineros(atomicUnits)); } + public static Coin atomicUnitsToCoin(BigInteger atomicUnits) { + return atomicUnitsToCoin(atomicUnits.longValueExact()); + } + + public static double atomicUnitsToXmr(long atomicUnits) { + return atomicUnitsToXmr(BigInteger.valueOf(atomicUnits)); + } + public static double atomicUnitsToXmr(BigInteger atomicUnits) { return new BigDecimal(atomicUnits).divide(new BigDecimal(XMR_AU_MULTIPLIER)).doubleValue(); } @@ -117,7 +126,27 @@ public class HavenoUtils { public static long xmrToCentineros(double xmr) { return atomicUnitsToCentineros(xmrToAtomicUnits(xmr)); } - + + public static double coinToXmr(Coin coin) { + return atomicUnitsToXmr(coinToAtomicUnits(coin)); + } + + public static String formatXmrWithCode(Coin coin) { + return formatXmrWithCode(coinToAtomicUnits(coin).longValueExact()); + } + + public static String formatXmrWithCode(long atomicUnits) { + String formatted = XMR_FORMATTER.format(atomicUnitsToXmr(atomicUnits)); + + // strip trailing 0s + if (formatted.contains(".")) { + while (formatted.length() > 3 && formatted.charAt(formatted.length() - 1) == '0') { + formatted = formatted.substring(0, formatted.length() - 1); + } + } + return formatted.concat(" ").concat("XMR"); + } + private static final MonetaryFormat xmrCoinFormat = Config.baseCurrencyNetworkParameters().getMonetaryFormat(); @Nullable @@ -166,7 +195,7 @@ public class HavenoUtils { /** * Get address to collect trade fees. - * + * * @return the address which collects trade fees */ public static String getTradeFeeAddress() { @@ -196,7 +225,7 @@ public class HavenoUtils { /** * Returns a unique deterministic id for sending a trade mailbox message. - * + * * @param trade the trade * @param tradeMessageClass the trade message class * @param receiver the receiver address @@ -209,23 +238,23 @@ public class HavenoUtils { /** * Check if the arbitrator signature is valid for an offer. - * + * * @param offer is a signed offer with payload * @param arbitrator is the original signing arbitrator * @return true if the arbitrator's signature is valid for the offer */ public static boolean isArbitratorSignatureValid(Offer offer, Arbitrator arbitrator) { - + // copy offer payload OfferPayload offerPayloadCopy = OfferPayload.fromProto(offer.toProtoMessage().getOfferPayload()); - + // remove arbitrator signature from signed payload String signature = offerPayloadCopy.getArbitratorSignature(); offerPayloadCopy.setArbitratorSignature(null); - + // get unsigned offer payload as json string String unsignedOfferAsJson = JsonUtil.objectToJson(offerPayloadCopy); - + // verify arbitrator signature try { return Sig.verify(arbitrator.getPubKeyRing().getSignaturePubKey(), unsignedOfferAsJson, signature); @@ -233,15 +262,15 @@ public class HavenoUtils { return false; } } - + /** * Check if the maker signature for a trade request is valid. - * + * * @param request is the trade request to check * @return true if the maker's signature is valid for the trade request */ public static boolean isMakerSignatureValid(InitTradeRequest request, String signature, PubKeyRing makerPubKeyRing) { - + // re-create trade request with signed fields InitTradeRequest signedRequest = new InitTradeRequest( request.getTradeId(), @@ -266,10 +295,10 @@ public class HavenoUtils { request.getPayoutAddress(), null ); - + // get trade request as string String tradeRequestAsJson = JsonUtil.objectToJson(signedRequest); - + // verify maker signature try { return Sig.verify(makerPubKeyRing.getSignaturePubKey(), @@ -282,7 +311,7 @@ public class HavenoUtils { /** * Verify the buyer signature for a PaymentSentMessage. - * + * * @param trade - the trade to verify * @param message - signed payment sent message to verify * @return true if the buyer's signature is valid for the message @@ -298,7 +327,7 @@ public class HavenoUtils { // replace signature message.setBuyerSignature(signature); - + // verify signature String errMessage = "The buyer signature is invalid for the " + message.getClass().getSimpleName() + " for " + trade.getClass().getSimpleName() + " " + trade.getId(); try { @@ -313,7 +342,7 @@ public class HavenoUtils { /** * Verify the seller signature for a PaymentReceivedMessage. - * + * * @param trade - the trade to verify * @param message - signed payment received message to verify * @return true if the seller's signature is valid for the message @@ -329,7 +358,7 @@ public class HavenoUtils { // replace signature message.setSellerSignature(signature); - + // verify signature String errMessage = "The seller signature is invalid for the " + message.getClass().getSimpleName() + " for " + trade.getClass().getSimpleName() + " " + trade.getId(); try { diff --git a/core/src/main/resources/i18n/displayStrings.properties b/core/src/main/resources/i18n/displayStrings.properties index f28545edc7..b4da468570 100644 --- a/core/src/main/resources/i18n/displayStrings.properties +++ b/core/src/main/resources/i18n/displayStrings.properties @@ -226,6 +226,8 @@ shared.numItemsLabel=Number of entries: {0} shared.filter=Filter shared.enabled=Enabled shared.me=Me +shared.maker=Maker +shared.taker=Taker #################################################################### @@ -990,7 +992,11 @@ portfolio.failed.cantUnfail=This trade cannot be moved back to open trades at th Try again after completion of trade(s) {0} portfolio.failed.depositTxNull=The trade cannot be reverted to a open trade. Deposit transaction is null. portfolio.failed.delayedPayoutTxNull=The trade cannot be reverted to a open trade. Delayed payout transaction is null. - +portfolio.failed.penalty.msg=This will charge the {0}/{1} the trade fee of {2} and return the remaining trade funds to their wallet. Are you sure you want to send?\n\n\ + Other Info:\n\ + Transaction Fee: {3}\n\ + Reserve Tx Hash: {4} +portfolio.failed.error.msg=Trade record does not exist. #################################################################### # Funds @@ -1089,6 +1095,18 @@ support.tab.legacyArbitration.support=Legacy Arbitration support.tab.ArbitratorsSupportTickets={0}'s tickets support.filter=Search disputes support.filter.prompt=Enter trade ID, date, onion address or account data +support.tab.SignedOffers=Signed Offers +support.prompt.signedOffer.penalty.msg=This will charge the maker the trade fee and return their remaining trade funds to their wallet. Are you sure you want to send?\n\n\ + Offer ID: {0}\n\ + Maker Trade Fee: {1}\n\ + Reserve Tx Miner Fee: {2}\n\ + Reserve Tx Hash: {3}\n\ + Reserve Tx Key Images: {4}\n\ + +support.contextmenu.penalize.msg=Penalize {0} by publishing reserve tx +support.prompt.signedOffer.error.msg=Signed Offer record does not exist; contact administrator. +support.info.submitTxHex=Reserve transaction has been published with the following result:\n +support.result.success=Transaction hex has been successfully submitted. support.sigCheck.button=Check signature support.sigCheck.popup.info=In case of a reimbursement request to the DAO you need to paste the summary message of the \ @@ -1148,6 +1166,12 @@ support.buyerMaker=XMR buyer/Maker support.sellerMaker=XMR seller/Maker support.buyerTaker=XMR buyer/Taker support.sellerTaker=XMR seller/Taker +support.txKeyImages=Key Images +support.txHash=Transaction Hash +support.txHex=Transaction Hex +support.signature=Signature +support.maker.trade.fee=Maker Trade Fee +support.tx.miner.fee=Miner Fee support.backgroundInfo=Haveno is not a company, so it handles disputes differently.\n\n\ Traders can communicate within the application via secure chat on the open trades screen to try solving disputes on their own. \ diff --git a/desktop/src/main/java/bisq/desktop/main/offer/OfferViewUtil.java b/desktop/src/main/java/bisq/desktop/main/offer/OfferViewUtil.java index 5f9c96551f..bb8a560d4e 100644 --- a/desktop/src/main/java/bisq/desktop/main/offer/OfferViewUtil.java +++ b/desktop/src/main/java/bisq/desktop/main/offer/OfferViewUtil.java @@ -21,7 +21,6 @@ import bisq.desktop.Navigation; import bisq.desktop.components.AutoTooltipButton; import bisq.desktop.components.AutoTooltipLabel; import bisq.desktop.components.HyperlinkWithIcon; -import bisq.desktop.main.MainView; import bisq.desktop.main.offer.offerbook.BtcOfferBookView; import bisq.desktop.main.offer.offerbook.OfferBookView; import bisq.desktop.main.offer.offerbook.OtherOfferBookView; @@ -29,6 +28,7 @@ import bisq.desktop.main.offer.offerbook.TopAltcoinOfferBookView; import bisq.desktop.main.overlays.popups.Popup; import bisq.desktop.util.GUIUtil; +import bisq.core.btc.wallet.XmrWalletService; import bisq.core.locale.CryptoCurrency; import bisq.core.locale.CurrencyUtil; import bisq.core.locale.Res; @@ -40,6 +40,7 @@ import bisq.common.UserThread; import bisq.common.util.Tuple2; import javafx.scene.control.Label; +import javafx.scene.control.TableView; import javafx.scene.control.TextField; import javafx.scene.layout.GridPane; import javafx.scene.layout.HBox; @@ -54,9 +55,16 @@ import java.util.Objects; import java.util.concurrent.TimeUnit; import java.util.stream.Stream; +import lombok.extern.slf4j.Slf4j; + import org.jetbrains.annotations.NotNull; + + +import monero.daemon.model.MoneroSubmitTxResult; + // Shared utils for Views +@Slf4j public class OfferViewUtil { public static Label createPopOverLabel(String text) { @@ -166,4 +174,18 @@ public class OfferViewUtil { return CurrencyUtil.getMainCryptoCurrencies().stream().filter(cryptoCurrency -> !Objects.equals(cryptoCurrency.getCode(), GUIUtil.TOP_ALTCOIN.getCode())); } + + public static void submitTransactionHex(XmrWalletService xmrWalletService, + TableView tableView, + String reserveTxHex) { + MoneroSubmitTxResult result = xmrWalletService.getDaemon().submitTxHex(reserveTxHex); + log.info("submitTransactionHex: reserveTxHex={} result={}", result); + tableView.refresh(); + + if(result.isGood()) { + new Popup().information(Res.get("support.result.success")).show(); + } else { + new Popup().attention(result.toString()).show(); + } + } } diff --git a/desktop/src/main/java/bisq/desktop/main/portfolio/failedtrades/FailedTradesView.java b/desktop/src/main/java/bisq/desktop/main/portfolio/failedtrades/FailedTradesView.java index 57583348c9..1f75005a1f 100644 --- a/desktop/src/main/java/bisq/desktop/main/portfolio/failedtrades/FailedTradesView.java +++ b/desktop/src/main/java/bisq/desktop/main/portfolio/failedtrades/FailedTradesView.java @@ -23,23 +23,26 @@ import bisq.desktop.components.AutoTooltipButton; import bisq.desktop.components.AutoTooltipLabel; import bisq.desktop.components.HyperlinkWithIcon; import bisq.desktop.components.InputTextField; +import bisq.desktop.main.offer.OfferViewUtil; import bisq.desktop.main.overlays.popups.Popup; import bisq.desktop.main.overlays.windows.TradeDetailsWindow; import bisq.desktop.util.FormBuilder; import bisq.desktop.util.GUIUtil; +import bisq.core.btc.wallet.XmrWalletService; import bisq.core.locale.Res; import bisq.core.offer.Offer; import bisq.core.trade.Contract; +import bisq.core.trade.HavenoUtils; import bisq.core.trade.Trade; -import bisq.common.config.Config; import bisq.common.util.Utilities; +import org.bitcoinj.core.Coin; + import com.googlecode.jcsv.writer.CSVEntryConverter; import javax.inject.Inject; -import javax.inject.Named; import de.jensd.fx.fontawesome.AwesomeIcon; @@ -50,9 +53,12 @@ import javafx.fxml.FXML; import javafx.stage.Stage; import javafx.scene.Scene; +import javafx.scene.control.ContextMenu; import javafx.scene.control.Label; +import javafx.scene.control.MenuItem; import javafx.scene.control.TableCell; import javafx.scene.control.TableColumn; +import javafx.scene.control.TableRow; import javafx.scene.control.TableView; import javafx.scene.control.Tooltip; import javafx.scene.input.KeyCode; @@ -107,12 +113,16 @@ public class FailedTradesView extends ActivatableViewAndModel keyEventEventHandler; private ChangeListener filterTextFieldListener; private Scene scene; + private XmrWalletService xmrWalletService; + private ContextMenu contextMenu; @Inject public FailedTradesView(FailedTradesViewModel model, - TradeDetailsWindow tradeDetailsWindow) { + TradeDetailsWindow tradeDetailsWindow, + XmrWalletService xmrWalletService) { super(model); this.tradeDetailsWindow = tradeDetailsWindow; + this.xmrWalletService = xmrWalletService; } @Override @@ -195,6 +205,39 @@ public class FailedTradesView extends ActivatableViewAndModel { + TableRow row = new TableRow<>(); + row.setOnContextMenuRequested(event -> { + contextMenu.show(row, event.getScreenX(), event.getScreenY()); + }); + return row; + }); + + item1.setOnAction(event -> { + Trade selectedFailedTrade = tableView.getSelectionModel().getSelectedItem().getTrade(); + handleContextMenu("portfolio.failed.penalty.msg", + Res.get(selectedFailedTrade.getMaker() == selectedFailedTrade.getBuyer() ? "shared.buyer" : "shared.seller"), + Res.get("shared.maker"), + selectedFailedTrade.getMakerFee(), + selectedFailedTrade.getMaker().getReserveTxHash(), + selectedFailedTrade.getMaker().getReserveTxHex()); + }); + + item2.setOnAction(event -> { + Trade selectedFailedTrade = tableView.getSelectionModel().getSelectedItem().getTrade(); + handleContextMenu("portfolio.failed.penalty.msg", + Res.get(selectedFailedTrade.getTaker() == selectedFailedTrade.getBuyer() ? "shared.buyer" : "shared.seller"), + Res.get("shared.taker"), + selectedFailedTrade.getTakerFee(), + selectedFailedTrade.getTaker().getReserveTxHash(), + selectedFailedTrade.getTaker().getReserveTxHex()); + }); + numItems.setText(Res.get("shared.numItemsLabel", sortedList.size())); exportButton.setOnAction(event -> { ObservableList> tableColumns = tableView.getColumns(); @@ -230,6 +273,23 @@ public class FailedTradesView extends ActivatableViewAndModel OfferViewUtil.submitTransactionHex(xmrWalletService, tableView, reserveTxHex)).show(); + } else { + new Popup().error(Res.get("portfolio.failed.error.msg")).show(); + } + } + @Override protected void deactivate() { if (scene != null) { diff --git a/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/PendingTradesView.java b/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/PendingTradesView.java index 3ccaa567a3..f1caf40a26 100644 --- a/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/PendingTradesView.java +++ b/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/PendingTradesView.java @@ -22,14 +22,11 @@ import bisq.desktop.common.view.ActivatableViewAndModel; import bisq.desktop.common.view.FxmlView; import bisq.desktop.components.AutoTooltipLabel; import bisq.desktop.components.HyperlinkWithIcon; -import bisq.desktop.components.PeerInfoIcon; import bisq.desktop.components.PeerInfoIconTrading; import bisq.desktop.components.list.FilterBox; import bisq.desktop.main.MainView; import bisq.desktop.main.overlays.popups.Popup; import bisq.desktop.main.overlays.windows.TradeDetailsWindow; -import bisq.desktop.main.portfolio.PortfolioView; -import bisq.desktop.main.portfolio.duplicateoffer.DuplicateOfferView; import bisq.desktop.main.portfolio.presentation.PortfolioUtil; import bisq.desktop.main.shared.ChatView; import bisq.desktop.util.CssTheme; @@ -55,7 +52,6 @@ import bisq.network.p2p.NodeAddress; import bisq.common.UserThread; import bisq.common.config.Config; import bisq.common.crypto.KeyRing; -import bisq.common.crypto.PubKeyRing; import bisq.common.util.Utilities; import javax.inject.Inject; diff --git a/desktop/src/main/java/bisq/desktop/main/support/SupportView.java b/desktop/src/main/java/bisq/desktop/main/support/SupportView.java index 99dd9f4050..6d11650179 100644 --- a/desktop/src/main/java/bisq/desktop/main/support/SupportView.java +++ b/desktop/src/main/java/bisq/desktop/main/support/SupportView.java @@ -25,6 +25,7 @@ import bisq.desktop.common.view.View; import bisq.desktop.common.view.ViewLoader; import bisq.desktop.main.MainView; import bisq.desktop.main.overlays.popups.Popup; +import bisq.desktop.main.support.dispute.agent.SignedOfferView; import bisq.desktop.main.support.dispute.agent.arbitration.ArbitratorView; import bisq.desktop.main.support.dispute.agent.mediation.MediatorView; import bisq.desktop.main.support.dispute.agent.refund.RefundAgentView; @@ -69,7 +70,8 @@ public class SupportView extends ActivatableView { private Tab mediatorTab, refundAgentTab; @Nullable private Tab arbitratorTab; - + @Nullable + private Tab signedOfferTab; private final Navigation navigation; private final ArbitratorManager arbitratorManager; private final MediatorManager mediatorManager; @@ -77,6 +79,7 @@ public class SupportView extends ActivatableView { private final ArbitrationManager arbitrationManager; private final MediationManager mediationManager; private final RefundManager refundManager; + private final KeyRing keyRing; private Navigation.Listener navigationListener; @@ -143,6 +146,8 @@ public class SupportView extends ActivatableView { navigation.navigateTo(MainView.class, SupportView.class, RefundClientView.class); else if (newValue == arbitratorTab) navigation.navigateTo(MainView.class, SupportView.class, ArbitratorView.class); + else if (newValue == signedOfferTab) + navigation.navigateTo(MainView.class, SupportView.class, SignedOfferView.class); else if (newValue == mediatorTab) navigation.navigateTo(MainView.class, SupportView.class, MediatorView.class); else if (newValue == refundAgentTab) @@ -161,15 +166,14 @@ public class SupportView extends ActivatableView { if (hasArbitrationCases) { boolean isActiveArbitrator = arbitratorManager.getObservableMap().values().stream() .anyMatch(e -> e.getPubKeyRing() != null && e.getPubKeyRing().equals(myPubKeyRing)); - if (arbitratorTab == null) { - // In case a arbitrator has become inactive he still might get disputes from pending trades - boolean hasDisputesAsArbitrator = arbitrationManager.getDisputesAsObservableList().stream() - .anyMatch(d -> d.getAgentPubKeyRing().equals(myPubKeyRing)); - if (isActiveArbitrator || hasDisputesAsArbitrator) { - arbitratorTab = new Tab(); - arbitratorTab.setClosable(false); - root.getTabs().add(arbitratorTab); - } + + // In case a arbitrator has become inactive he still might get disputes from pending trades + boolean hasDisputesAsArbitrator = arbitrationManager.getDisputesAsObservableList().stream() + .anyMatch(d -> d.getAgentPubKeyRing().equals(myPubKeyRing)); + if (arbitratorTab == null && (isActiveArbitrator || hasDisputesAsArbitrator)) { + arbitratorTab = new Tab(); + arbitratorTab.setClosable(false); + root.getTabs().add(arbitratorTab); } } @@ -186,6 +190,12 @@ public class SupportView extends ActivatableView { } } + if (signedOfferTab == null) { + signedOfferTab = new Tab(); + signedOfferTab.setClosable(false); + root.getTabs().add(signedOfferTab); + } + boolean isActiveRefundAgent = refundAgentManager.getObservableMap().values().stream() .anyMatch(e -> e.getPubKeyRing() != null && e.getPubKeyRing().equals(myPubKeyRing)); if (refundAgentTab == null) { @@ -203,6 +213,9 @@ public class SupportView extends ActivatableView { if (arbitratorTab != null) { arbitratorTab.setText(Res.get("support.tab.ArbitratorsSupportTickets", Res.get("shared.arbitrator")).toUpperCase()); } + if (signedOfferTab != null) { + signedOfferTab.setText(Res.get("support.tab.SignedOffers").toUpperCase()); + } if (mediatorTab != null) { mediatorTab.setText(Res.get("support.tab.ArbitratorsSupportTickets", Res.get("shared.mediator")).toUpperCase()); } @@ -235,6 +248,8 @@ public class SupportView extends ActivatableView { navigation.navigateTo(MainView.class, SupportView.class, RefundClientView.class); } else if (arbitratorTab != null) { navigation.navigateTo(MainView.class, SupportView.class, ArbitratorView.class); + } else if (signedOfferTab != null) { + navigation.navigateTo(MainView.class, SupportView.class, SignedOfferView.class); } else if (mediatorTab != null) { navigation.navigateTo(MainView.class, SupportView.class, MediatorView.class); } else if (refundAgentTab != null) { @@ -275,6 +290,8 @@ public class SupportView extends ActivatableView { currentTab = tradersRefundDisputesTab; } else if (view instanceof ArbitratorView) { currentTab = arbitratorTab; + } else if (view instanceof SignedOfferView) { + currentTab = signedOfferTab; } else if (view instanceof MediatorView) { currentTab = mediatorTab; } else if (view instanceof RefundAgentView) { diff --git a/desktop/src/main/java/bisq/desktop/main/support/dispute/agent/SignedOfferView.fxml b/desktop/src/main/java/bisq/desktop/main/support/dispute/agent/SignedOfferView.fxml new file mode 100644 index 0000000000..3801ba1390 --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/main/support/dispute/agent/SignedOfferView.fxml @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/desktop/src/main/java/bisq/desktop/main/support/dispute/agent/SignedOfferView.java b/desktop/src/main/java/bisq/desktop/main/support/dispute/agent/SignedOfferView.java new file mode 100644 index 0000000000..9ce14b9b22 --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/main/support/dispute/agent/SignedOfferView.java @@ -0,0 +1,444 @@ +/* + * This file is part of Haveno. + * + * Haveno is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Haveno is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Haveno. If not, see . + */ + +package bisq.desktop.main.support.dispute.agent; + +import bisq.desktop.common.view.ActivatableView; +import bisq.desktop.common.view.FxmlView; +import bisq.desktop.components.AutoTooltipLabel; +import bisq.desktop.components.AutoTooltipTableColumn; +import bisq.desktop.components.HyperlinkWithIcon; +import bisq.desktop.components.InputTextField; +import bisq.desktop.main.offer.OfferViewUtil; +import bisq.desktop.main.overlays.popups.Popup; +import bisq.desktop.util.DisplayUtils; +import bisq.desktop.util.GUIUtil; +import bisq.common.UserThread; +import bisq.core.btc.wallet.XmrWalletService; +import bisq.core.locale.Res; +import bisq.core.offer.OpenOfferManager; +import bisq.core.offer.SignedOffer; +import bisq.core.trade.HavenoUtils; + +import javax.inject.Inject; + +import javafx.fxml.FXML; + +import javafx.scene.control.ContextMenu; +import javafx.scene.control.Label; +import javafx.scene.control.MenuItem; +import javafx.scene.control.TableCell; +import javafx.scene.control.TableColumn; +import javafx.scene.control.TableRow; +import javafx.scene.control.TableView; +import javafx.scene.control.Tooltip; +import javafx.scene.layout.HBox; +import javafx.scene.layout.Priority; +import javafx.scene.layout.Region; +import javafx.scene.layout.VBox; + +import javafx.geometry.Insets; + +import javafx.beans.property.ReadOnlyObjectWrapper; + +import javafx.collections.ListChangeListener; +import javafx.collections.transformation.SortedList; + +import javafx.util.Callback; +import javafx.util.Duration; + +import java.util.Comparator; +import java.util.Date; + +@FxmlView +public class SignedOfferView extends ActivatableView { + + private final OpenOfferManager openOfferManager; + + @FXML + protected TableView tableView; + @FXML + TableColumn dateColumn; + @FXML + TableColumn offerIdColumn; + @FXML + TableColumn reserveTxHashColumn; + @FXML + TableColumn reserveTxHexColumn; + @FXML + TableColumn reserveTxKeyImages; + @FXML + TableColumn arbitratorSignatureColumn; + @FXML + TableColumn reserveTxMinerFeeColumn; + @FXML + TableColumn makerTradeFeeColumn; + @FXML + InputTextField filterTextField; + @FXML + Label numItems; + @FXML + Region footerSpacer; + + private SignedOffer selectedSignedOffer; + + private XmrWalletService xmrWalletService; + + private ContextMenu contextMenu; + + private final ListChangeListener signedOfferListChangeListener; + + @Inject + public SignedOfferView(OpenOfferManager openOfferManager, XmrWalletService xmrWalletService) { + this.openOfferManager = openOfferManager; + this.xmrWalletService = xmrWalletService; + + signedOfferListChangeListener = change -> applyList(); + } + + private void applyList() { + UserThread.execute(() -> { + SortedList sortedList = new SortedList<>(openOfferManager.getObservableSignedOffersList()); + sortedList.comparatorProperty().bind(tableView.comparatorProperty()); + tableView.setItems(sortedList); + numItems.setText(Res.get("shared.numItemsLabel", sortedList.size())); + }); + } + + /////////////////////////////////////////////////////////////////////////////////////////// + // Life cycle + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public void initialize() { + Label label = new AutoTooltipLabel(Res.get("support.filter")); + HBox.setMargin(label, new Insets(5, 0, 0, 0)); + HBox.setHgrow(label, Priority.NEVER); + + filterTextField = new InputTextField(); + Tooltip tooltip = new Tooltip(); + tooltip.setShowDelay(Duration.millis(100)); + tooltip.setShowDuration(Duration.seconds(10)); + filterTextField.setTooltip(tooltip); + HBox.setHgrow(filterTextField, Priority.NEVER); + + filterTextField.setText("open"); + + setupTable(); + } + @Override + protected void activate() { + super.activate(); + + applyList(); + openOfferManager.getObservableSignedOffersList().addListener(signedOfferListChangeListener); + contextMenu = new ContextMenu(); + MenuItem item1 = new MenuItem(Res.get("support.contextmenu.penalize.msg", + Res.get("shared.maker"))); + contextMenu.getItems().addAll(item1); + + tableView.setRowFactory(tv -> { + TableRow row = new TableRow<>(); + row.setOnContextMenuRequested(event -> { + contextMenu.show(row, event.getScreenX(), event.getScreenY()); + }); + return row; + }); + + item1.setOnAction(event -> { + selectedSignedOffer = tableView.getSelectionModel().getSelectedItem(); + if(selectedSignedOffer != null) { + new Popup().warning(Res.get("support.prompt.signedOffer.penalty.msg", + selectedSignedOffer.getOfferId(), + HavenoUtils.formatXmrWithCode(selectedSignedOffer.getMakerTradeFee()), + HavenoUtils.formatXmrWithCode(selectedSignedOffer.getReserveTxMinerFee()), + selectedSignedOffer.getReserveTxHash(), + selectedSignedOffer.getReserveTxKeyImages()) + ).onAction(() -> OfferViewUtil.submitTransactionHex(xmrWalletService, tableView, + selectedSignedOffer.getReserveTxHex())).show(); + } else { + new Popup().error(Res.get("support.prompt.signedOffer.error.msg")).show(); + } + }); + + GUIUtil.requestFocus(tableView); + } + + @Override + protected void deactivate() { + super.deactivate(); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // SignedOfferView + /////////////////////////////////////////////////////////////////////////////////////////// + + protected void setupTable() { + tableView.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY); + Label placeholder = new AutoTooltipLabel(Res.get("support.noTickets")); + placeholder.setWrapText(true); + tableView.setPlaceholder(placeholder); + tableView.getSelectionModel().clearSelection(); + + dateColumn = getDateColumn(); + tableView.getColumns().add(dateColumn); + + offerIdColumn = getOfferIdColumn(); + tableView.getColumns().add(offerIdColumn); + + reserveTxHashColumn = getReserveTxHashColumn(); + tableView.getColumns().add(reserveTxHashColumn); + + reserveTxHexColumn = getReserveTxHexColumn(); + tableView.getColumns().add(reserveTxHexColumn); + + reserveTxKeyImages = getReserveTxKeyImagesColumn(); + tableView.getColumns().add(reserveTxKeyImages); + + arbitratorSignatureColumn = getArbitratorSignatureColumn(); + tableView.getColumns().add(arbitratorSignatureColumn); + + makerTradeFeeColumn = getMakerTradeFeeColumn(); + tableView.getColumns().add(makerTradeFeeColumn); + + reserveTxMinerFeeColumn = getReserveTxMinerFeeColumn(); + tableView.getColumns().add(reserveTxMinerFeeColumn); + + offerIdColumn.setComparator(Comparator.comparing(SignedOffer::getOfferId)); + dateColumn.setComparator(Comparator.comparing(SignedOffer::getTimeStamp)); + + dateColumn.setSortType(TableColumn.SortType.DESCENDING); + tableView.getSortOrder().add(dateColumn); + } + + private TableColumn getDateColumn() { + TableColumn column = new AutoTooltipTableColumn<>(Res.get("shared.date")) { + { + setMinWidth(180); + } + }; + column.setCellValueFactory((signedOffer) -> new ReadOnlyObjectWrapper<>(signedOffer.getValue())); + column.setCellFactory( + new Callback<>() { + @Override + public TableCell call(TableColumn column) { + return new TableCell<>() { + @Override + public void updateItem(final SignedOffer item, boolean empty) { + super.updateItem(item, empty); + if (item != null && !empty) + setText(DisplayUtils.formatDateTime(new Date(item.getTimeStamp()))); + else + setText(""); + } + }; + } + }); + return column; + } + + private TableColumn getOfferIdColumn() { + TableColumn column = new AutoTooltipTableColumn<>(Res.get("shared.offerId")) { + { + setMinWidth(110); + } + }; + column.setCellValueFactory((signedOffer) -> new ReadOnlyObjectWrapper<>(signedOffer.getValue())); + column.setCellFactory( + new Callback<>() { + @Override + public TableCell call(TableColumn column) { + return new TableCell<>() { + private HyperlinkWithIcon field; + + @Override + public void updateItem(final SignedOffer item, boolean empty) { + super.updateItem(item, empty); + + if (item != null && !empty) { + setText(item.getOfferId()); + setGraphic(field); + } else { + setGraphic(null); + setText(""); + if (field != null) + field.setOnAction(null); + } + } + }; + } + }); + return column; + } + + private TableColumn getReserveTxHashColumn() { + TableColumn column = new AutoTooltipTableColumn<>(Res.get("support.txHash")) { + { + setMinWidth(160); + } + }; + column.setCellValueFactory((signedOffer) -> new ReadOnlyObjectWrapper<>(signedOffer.getValue())); + column.setCellFactory( + new Callback<>() { + @Override + public TableCell call(TableColumn column) { + return new TableCell<>() { + @Override + public void updateItem(final SignedOffer item, boolean empty) { + super.updateItem(item, empty); + if (item != null && !empty) + setText(item.getReserveTxHash()); + else + setText(""); + } + }; + } + }); + return column; + } + + private TableColumn getReserveTxHexColumn() { + TableColumn column = new AutoTooltipTableColumn<>(Res.get("support.txHex")) { + { + setMinWidth(160); + } + }; + column.setCellValueFactory((signedOffer) -> new ReadOnlyObjectWrapper<>(signedOffer.getValue())); + column.setCellFactory( + new Callback<>() { + @Override + public TableCell call(TableColumn column) { + return new TableCell<>() { + @Override + public void updateItem(final SignedOffer item, boolean empty) { + super.updateItem(item, empty); + if (item != null && !empty) + setText(item.getReserveTxHex()); + else + setText(""); + } + }; + } + }); + return column; + } + + private TableColumn getReserveTxKeyImagesColumn() { + TableColumn column = new AutoTooltipTableColumn<>(Res.get("support.txKeyImages")) { + { + setMinWidth(160); + } + }; + column.setCellValueFactory((signedOffer) -> new ReadOnlyObjectWrapper<>(signedOffer.getValue())); + column.setCellFactory( + new Callback<>() { + @Override + public TableCell call(TableColumn column) { + return new TableCell<>() { + @Override + public void updateItem(final SignedOffer item, boolean empty) { + super.updateItem(item, empty); + if (item != null && !empty) + setText(item.getReserveTxKeyImages().toString()); + else + setText(""); + } + }; + } + }); + return column; + } + + private TableColumn getArbitratorSignatureColumn() { + TableColumn column = new AutoTooltipTableColumn<>(Res.get("support.signature")) { + { + setMinWidth(160); + } + }; + column.setCellValueFactory((signedOffer) -> new ReadOnlyObjectWrapper<>(signedOffer.getValue())); + column.setCellFactory( + new Callback<>() { + @Override + public TableCell call(TableColumn column) { + return new TableCell<>() { + @Override + public void updateItem(final SignedOffer item, boolean empty) { + super.updateItem(item, empty); + if (item != null && !empty) + setText(item.getArbitratorSignature()); + else + setText(""); + } + }; + } + }); + return column; + } + + private TableColumn getMakerTradeFeeColumn() { + TableColumn column = new AutoTooltipTableColumn<>(Res.get("support.maker.trade.fee")) { + { + setMinWidth(160); + } + }; + column.setCellValueFactory((signedOffer) -> new ReadOnlyObjectWrapper<>(signedOffer.getValue())); + column.setCellFactory( + new Callback<>() { + @Override + public TableCell call(TableColumn column) { + return new TableCell<>() { + @Override + public void updateItem(final SignedOffer item, boolean empty) { + super.updateItem(item, empty); + if (item != null && !empty) + setText(HavenoUtils.formatXmrWithCode(item.getMakerTradeFee())); + else + setText(""); + } + }; + } + }); + return column; + } + + private TableColumn getReserveTxMinerFeeColumn() { + TableColumn column = new AutoTooltipTableColumn<>(Res.get("support.tx.miner.fee")) { + { + setMinWidth(160); + } + }; + column.setCellValueFactory((signedOffer) -> new ReadOnlyObjectWrapper<>(signedOffer.getValue())); + column.setCellFactory( + new Callback<>() { + @Override + public TableCell call(TableColumn column) { + return new TableCell<>() { + @Override + public void updateItem(final SignedOffer item, boolean empty) { + super.updateItem(item, empty); + if (item != null && !empty) + setText(HavenoUtils.formatXmrWithCode(item.getReserveTxMinerFee())); + else + setText(""); + } + }; + } + }); + return column; + } +}