From b421605345c034f808a13a223d49a5ab87ac1573 Mon Sep 17 00:00:00 2001 From: chimp1984 Date: Mon, 15 Mar 2021 11:05:15 -0500 Subject: [PATCH 1/5] Add formatCoin method with appendCode param Remove @NotNull annotation (unnecessary verbose, default is non null, if null then we annotate with @Nullable) --- .../main/java/bisq/core/util/coin/BsqFormatter.java | 13 ++++++++----- .../java/bisq/core/util/coin/CoinFormatter.java | 5 ++--- .../bisq/core/util/coin/ImmutableCoinFormatter.java | 8 +++++--- .../main/java/bisq/desktop/util/DisplayUtils.java | 6 +++++- 4 files changed, 20 insertions(+), 12 deletions(-) diff --git a/core/src/main/java/bisq/core/util/coin/BsqFormatter.java b/core/src/main/java/bisq/core/util/coin/BsqFormatter.java index 548305f8008..ab476097bed 100644 --- a/core/src/main/java/bisq/core/util/coin/BsqFormatter.java +++ b/core/src/main/java/bisq/core/util/coin/BsqFormatter.java @@ -47,8 +47,6 @@ import lombok.Getter; import lombok.extern.slf4j.Slf4j; -import org.jetbrains.annotations.NotNull; - @Slf4j @Singleton public class BsqFormatter implements CoinFormatter { @@ -224,10 +222,15 @@ public String parseParamValueToString(Param param, String inputValue) throws Pro } public String formatCoin(Coin coin) { - return immutableCoinFormatter.formatCoin(coin); + return formatCoin(coin, false); + } + + public String formatCoin(Coin coin, boolean appendCode) { + return appendCode ? + immutableCoinFormatter.formatCoinWithCode(coin) : + immutableCoinFormatter.formatCoin(coin); } - @NotNull public String formatCoin(Coin coin, int decimalPlaces) { return immutableCoinFormatter.formatCoin(coin, decimalPlaces); } @@ -240,7 +243,7 @@ public String formatCoin(Coin coin, } public String formatCoinWithCode(Coin coin) { - return immutableCoinFormatter.formatCoinWithCode(coin); + return formatCoin(coin, true); } public String formatCoinWithCode(long value) { diff --git a/core/src/main/java/bisq/core/util/coin/CoinFormatter.java b/core/src/main/java/bisq/core/util/coin/CoinFormatter.java index 1959e64b7ff..2b65460e6bc 100644 --- a/core/src/main/java/bisq/core/util/coin/CoinFormatter.java +++ b/core/src/main/java/bisq/core/util/coin/CoinFormatter.java @@ -2,12 +2,11 @@ import org.bitcoinj.core.Coin; -import org.jetbrains.annotations.NotNull; - public interface CoinFormatter { String formatCoin(Coin coin); - @NotNull + String formatCoin(Coin coin, boolean appendCode); + String formatCoin(Coin coin, int decimalPlaces); String formatCoin(Coin coin, int decimalPlaces, boolean decimalAligned, int maxNumberOfDigits); diff --git a/core/src/main/java/bisq/core/util/coin/ImmutableCoinFormatter.java b/core/src/main/java/bisq/core/util/coin/ImmutableCoinFormatter.java index 41395c303fe..80a9d7ef866 100644 --- a/core/src/main/java/bisq/core/util/coin/ImmutableCoinFormatter.java +++ b/core/src/main/java/bisq/core/util/coin/ImmutableCoinFormatter.java @@ -27,8 +27,6 @@ import lombok.Getter; import lombok.extern.slf4j.Slf4j; -import org.jetbrains.annotations.NotNull; - @Slf4j public class ImmutableCoinFormatter implements CoinFormatter { @@ -56,7 +54,11 @@ public String formatCoin(Coin coin) { } @Override - @NotNull + public String formatCoin(Coin coin, boolean appendCode) { + return appendCode ? formatCoinWithCode(coin) : formatCoin(coin); + } + + @Override public String formatCoin(Coin coin, int decimalPlaces) { return formatCoin(coin, decimalPlaces, false, 0); } diff --git a/desktop/src/main/java/bisq/desktop/util/DisplayUtils.java b/desktop/src/main/java/bisq/desktop/util/DisplayUtils.java index 21ff4f1f8ba..92ef911b918 100644 --- a/desktop/src/main/java/bisq/desktop/util/DisplayUtils.java +++ b/desktop/src/main/java/bisq/desktop/util/DisplayUtils.java @@ -144,7 +144,11 @@ private static String formatVolume(Volume volume, MonetaryFormat fiatVolumeForma } public static String formatVolumeWithCode(Volume volume) { - return formatVolume(volume, FIAT_VOLUME_FORMAT, true); + return formatVolume(volume, true); + } + + public static String formatVolume(Volume volume, boolean appendCode) { + return formatVolume(volume, FIAT_VOLUME_FORMAT, appendCode); } public static String formatAverageVolumeWithCode(Volume volume) { From 53276578060a25b2c8bcca45f0383af0f4a79e58 Mon Sep 17 00:00:00 2001 From: chimp1984 Date: Mon, 15 Mar 2021 11:12:57 -0500 Subject: [PATCH 2/5] Various improvements to make make work with csv data easier Separate volume with currency (add new column for currency) Separate trade fee in btc and bsq, remove currency from value Replace "-" with empty string Improve detection of bsq fee tx. Previous version was more complicate and would not have covered case where BSQ fee input matches exactly required fee so not BSQ output would have been created. --- .../closedtrades/ClosedTradesView.java | 29 ++++--- .../closedtrades/ClosedTradesViewModel.java | 87 ++++++++++++------- 2 files changed, 73 insertions(+), 43 deletions(-) diff --git a/desktop/src/main/java/bisq/desktop/main/portfolio/closedtrades/ClosedTradesView.java b/desktop/src/main/java/bisq/desktop/main/portfolio/closedtrades/ClosedTradesView.java index cc44cba579f..2ec1fc97a80 100644 --- a/desktop/src/main/java/bisq/desktop/main/portfolio/closedtrades/ClosedTradesView.java +++ b/desktop/src/main/java/bisq/desktop/main/portfolio/closedtrades/ClosedTradesView.java @@ -68,7 +68,6 @@ import javafx.beans.property.ReadOnlyObjectWrapper; import javafx.beans.value.ChangeListener; -import javafx.collections.ObservableList; import javafx.collections.transformation.FilteredList; import javafx.collections.transformation.SortedList; @@ -89,8 +88,10 @@ private enum ColumnNames { DEVIATION(Res.get("shared.deviation")), AMOUNT(Res.get("shared.amountWithCur", Res.getBaseCurrencyCode())), VOLUME(Res.get("shared.amount")), + VOLUME_CURRENCY(Res.get("shared.currency")), TX_FEE(Res.get("shared.txFee")), - TRADE_FEE(Res.get("shared.tradeFee")), + TRADE_FEE_BTC(Res.get("shared.tradeFee") + " BTC"), + TRADE_FEE_BSQ(Res.get("shared.tradeFee") + " BSQ"), BUYER_SEC(Res.get("shared.buyerSecurityDeposit")), SELLER_SEC(Res.get("shared.sellerSecurityDeposit")), OFFER_TYPE(Res.get("shared.offerType")), @@ -157,7 +158,7 @@ public ClosedTradesView(ClosedTradesViewModel model, public void initialize() { widthListener = (observable, oldValue, newValue) -> onWidthChange((double) newValue); txFeeColumn.setGraphic(new AutoTooltipLabel(ColumnNames.TX_FEE.toString())); - tradeFeeColumn.setGraphic(new AutoTooltipLabel(ColumnNames.TRADE_FEE.toString())); + tradeFeeColumn.setGraphic(new AutoTooltipLabel(ColumnNames.TRADE_FEE_BTC.toString().replace(" BTC", ""))); buyerSecurityDepositColumn.setGraphic(new AutoTooltipLabel(ColumnNames.BUYER_SEC.toString())); sellerSecurityDepositColumn.setGraphic(new AutoTooltipLabel(ColumnNames.SELLER_SEC.toString())); priceColumn.setGraphic(new AutoTooltipLabel(ColumnNames.PRICE.toString())); @@ -211,7 +212,7 @@ public void initialize() { // tradeFeeColumn.setComparator(Comparator.comparing(item -> { - String tradeFee = model.getTradeFee(item); + String tradeFee = model.getTradeFee(item, true); // We want to separate BSQ and BTC fees so we use a prefix if (item.getTradable().getOffer().isCurrencyForMakerFeeBtc()) { return "BTC" + tradeFee; @@ -254,7 +255,6 @@ protected void activate() { numItems.setText(Res.get("shared.numItemsLabel", sortedList.size())); exportButton.setOnAction(event -> { - final ObservableList> tableColumns = tableView.getColumns(); CSVEntryConverter headerConverter = item -> { String[] columns = new String[ColumnNames.values().length]; for (ColumnNames m : ColumnNames.values()) { @@ -270,9 +270,16 @@ protected void activate() { columns[ColumnNames.PRICE.ordinal()] = model.getPrice(item); columns[ColumnNames.DEVIATION.ordinal()] = model.getPriceDeviation(item); columns[ColumnNames.AMOUNT.ordinal()] = model.getAmount(item); - columns[ColumnNames.VOLUME.ordinal()] = model.getVolume(item); + columns[ColumnNames.VOLUME.ordinal()] = model.getVolume(item, false); + columns[ColumnNames.VOLUME_CURRENCY.ordinal()] = model.getVolumeCurrency(item); columns[ColumnNames.TX_FEE.ordinal()] = model.getTxFee(item); - columns[ColumnNames.TRADE_FEE.ordinal()] = model.getTradeFee(item); + if (model.isCurrencyForTradeFeeBtc(item)) { + columns[ColumnNames.TRADE_FEE_BTC.ordinal()] = model.getTradeFee(item, false); + columns[ColumnNames.TRADE_FEE_BSQ.ordinal()] = ""; + } else { + columns[ColumnNames.TRADE_FEE_BTC.ordinal()] = ""; + columns[ColumnNames.TRADE_FEE_BSQ.ordinal()] = model.getTradeFee(item, false); + } columns[ColumnNames.BUYER_SEC.ordinal()] = model.getBuyerSecurityDeposit(item); columns[ColumnNames.SELLER_SEC.ordinal()] = model.getSellerSecurityDeposit(item); columns[ColumnNames.OFFER_TYPE.ordinal()] = model.getDirectionLabel(item); @@ -343,13 +350,13 @@ private void applyFilteredListPredicate(String filterString) { return true; } - if (model.getVolume(item).contains(filterString)) { + if (model.getVolume(item, true).contains(filterString)) { return true; } if (model.getAmount(item).contains(filterString)) { return true; } - if (model.getTradeFee(item).contains(filterString)) { + if (model.getTradeFee(item, true).contains(filterString)) { return true; } if (model.getTxFee(item).contains(filterString)) { @@ -607,7 +614,7 @@ public TableCell call( public void updateItem(final ClosedTradableListItem item, boolean empty) { super.updateItem(item, empty); if (item != null) - setGraphic(new AutoTooltipLabel(model.getVolume(item))); + setGraphic(new AutoTooltipLabel(model.getVolume(item, true))); else setGraphic(null); } @@ -663,7 +670,7 @@ public TableCell call( @Override public void updateItem(final ClosedTradableListItem item, boolean empty) { super.updateItem(item, empty); - setGraphic(new AutoTooltipLabel(model.getTradeFee(item))); + setGraphic(new AutoTooltipLabel(model.getTradeFee(item, true))); } }; } diff --git a/desktop/src/main/java/bisq/desktop/main/portfolio/closedtrades/ClosedTradesViewModel.java b/desktop/src/main/java/bisq/desktop/main/portfolio/closedtrades/ClosedTradesViewModel.java index 0bcee0d900e..25b3f08b737 100644 --- a/desktop/src/main/java/bisq/desktop/main/portfolio/closedtrades/ClosedTradesViewModel.java +++ b/desktop/src/main/java/bisq/desktop/main/portfolio/closedtrades/ClosedTradesViewModel.java @@ -26,6 +26,7 @@ import bisq.core.btc.wallet.BtcWalletService; import bisq.core.locale.CurrencyUtil; import bisq.core.locale.Res; +import bisq.core.monetary.Volume; import bisq.core.offer.Offer; import bisq.core.offer.OpenOffer; import bisq.core.trade.Tradable; @@ -36,12 +37,6 @@ import bisq.network.p2p.NodeAddress; -import bisq.common.config.Config; - -import org.bitcoinj.core.Address; -import org.bitcoinj.core.Transaction; -import org.bitcoinj.core.TransactionOutput; - import com.google.inject.Inject; import javax.inject.Named; @@ -83,8 +78,6 @@ String getTradeId(ClosedTradableListItem item) { String getAmount(ClosedTradableListItem item) { if (item != null && item.getTradable() instanceof Trade) return btcFormatter.formatCoin(((Trade) item.getTradable()).getTradeAmount()); - else if (item != null && item.getTradable() instanceof OpenOffer) - return "-"; else return ""; } @@ -110,13 +103,32 @@ String getPriceDeviation(ClosedTradableListItem item) { } } - String getVolume(ClosedTradableListItem item) { - if (item != null && item.getTradable() instanceof Trade) - return DisplayUtils.formatVolumeWithCode(((Trade) item.getTradable()).getTradeVolume()); - else if (item != null && item.getTradable() instanceof OpenOffer) - return "-"; - else + String getVolume(ClosedTradableListItem item, boolean appendCode) { + if (item == null) { return ""; + } + + if (item.getTradable() instanceof OpenOffer) { + return ""; + } + + Trade trade = (Trade) item.getTradable(); + return DisplayUtils.formatVolume(trade.getTradeVolume(), appendCode); + } + + String getVolumeCurrency(ClosedTradableListItem item) { + if (item == null) { + return ""; + } + Volume volume; + if (item.getTradable() instanceof OpenOffer) { + OpenOffer openOffer = (OpenOffer) item.getTradable(); + volume = openOffer.getOffer().getVolume(); + } else { + Trade trade = (Trade) item.getTradable(); + volume = trade.getTradeVolume(); + } + return volume != null ? volume.getCurrencyCode() : ""; } String getTxFee(ClosedTradableListItem item) { @@ -129,33 +141,44 @@ String getTxFee(ClosedTradableListItem item) { return btcFormatter.formatCoin(tradable.getOffer().getTxFee()); } - String getTradeFee(ClosedTradableListItem item) { + boolean isCurrencyForTradeFeeBtc(ClosedTradableListItem item) { if (item == null) { - return ""; + return false; } Tradable tradable = item.getTradable(); Offer offer = tradable.getOffer(); + if (wasMyOffer(tradable)) { + // I was maker so we use offer + return offer.isCurrencyForMakerFeeBtc(); + } else { + Trade trade = (Trade) tradable; + String takerFeeTxId = trade.getTakerFeeTxId(); + // If we find our tx in the bsq wallet its a BSQ trade fee tx + return bsqWalletService.getTransaction(takerFeeTxId) == null; + } + } + + String getTradeFee(ClosedTradableListItem item, boolean appendCode) { + if (item == null) { + return ""; + } - if (!wasMyOffer(tradable) && (tradable instanceof Trade)) { + Tradable tradable = item.getTradable(); + Offer offer = tradable.getOffer(); + if (wasMyOffer(tradable)) { + CoinFormatter formatter = offer.isCurrencyForMakerFeeBtc() ? btcFormatter : bsqFormatter; + return formatter.formatCoin(offer.getMakerFee(), appendCode); + } else { Trade trade = (Trade) tradable; - Transaction takerFeeTx = btcWalletService.getTransaction(trade.getTakerFeeTxId()); - if (takerFeeTx != null && takerFeeTx.getOutputs().size() > 1) { - // First output is fee receiver address. If its a BSQ (change) address of our own wallet its a BSQ fee - TransactionOutput output = takerFeeTx.getOutput(0); - Address address = output.getScriptPubKey().getToAddress(Config.baseCurrencyNetworkParameters()); - if (bsqWalletService.getWallet().findKeyFromAddress(address) != null) { - return bsqFormatter.formatCoinWithCode(trade.getTakerFee()); - } else { - return btcFormatter.formatCoinWithCode(trade.getTakerFee()); - } + String takerFeeTxId = trade.getTakerFeeTxId(); + if (bsqWalletService.getTransaction(takerFeeTxId) == null) { + // Was BTC fee + return btcFormatter.formatCoin(trade.getTakerFee(), appendCode); } else { - log.warn("takerFeeTx is null or has invalid structure. takerFeeTx={}", takerFeeTx); - return Res.get("shared.na"); + // BSQ fee + return bsqFormatter.formatCoin(trade.getTakerFee(), appendCode); } - } else { - CoinFormatter formatter = offer.isCurrencyForMakerFeeBtc() ? btcFormatter : bsqFormatter; - return formatter.formatCoinWithCode(offer.getMakerFee()); } } From e2a99a988a74a7da632bcd1259f8d10474bad356 Mon Sep 17 00:00:00 2001 From: chimp1984 Date: Mon, 15 Mar 2021 11:15:21 -0500 Subject: [PATCH 3/5] Fix incorrect taker trade fee. We had only displayed the fee for 1 tx, but taker pays 3 times that fee. The real miner fee is a bit higher in case we used BSQ as we add the burned BSQ to miner fee, we ignore that small difference as it would add more complexity and for that use case it't not that important to be exact. --- .../portfolio/closedtrades/ClosedTradesViewModel.java | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/desktop/src/main/java/bisq/desktop/main/portfolio/closedtrades/ClosedTradesViewModel.java b/desktop/src/main/java/bisq/desktop/main/portfolio/closedtrades/ClosedTradesViewModel.java index 25b3f08b737..b09b2e9f42d 100644 --- a/desktop/src/main/java/bisq/desktop/main/portfolio/closedtrades/ClosedTradesViewModel.java +++ b/desktop/src/main/java/bisq/desktop/main/portfolio/closedtrades/ClosedTradesViewModel.java @@ -135,10 +135,12 @@ String getTxFee(ClosedTradableListItem item) { if (item == null) return ""; Tradable tradable = item.getTradable(); - if (!wasMyOffer(tradable) && (tradable instanceof Trade)) - return btcFormatter.formatCoin(((Trade) tradable).getTxFee()); - else + if (!wasMyOffer(tradable) && (tradable instanceof Trade)) { + // taker pays for 3 transactions + return btcFormatter.formatCoin(((Trade) tradable).getTxFee().multiply(3)); + } else { return btcFormatter.formatCoin(tradable.getOffer().getTxFee()); + } } boolean isCurrencyForTradeFeeBtc(ClosedTradableListItem item) { From 1e593205d7e4ca216dd5603c0832b13145954425 Mon Sep 17 00:00:00 2001 From: chimp1984 Date: Mon, 15 Mar 2021 11:34:37 -0500 Subject: [PATCH 4/5] Fix taker trade fee amount. We need to use fee from trade not from offer --- .../bisq/desktop/main/overlays/windows/TradeDetailsWindow.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/desktop/src/main/java/bisq/desktop/main/overlays/windows/TradeDetailsWindow.java b/desktop/src/main/java/bisq/desktop/main/overlays/windows/TradeDetailsWindow.java index cf38de43335..be2d1d34891 100644 --- a/desktop/src/main/java/bisq/desktop/main/overlays/windows/TradeDetailsWindow.java +++ b/desktop/src/main/java/bisq/desktop/main/overlays/windows/TradeDetailsWindow.java @@ -228,7 +228,7 @@ private void addContent() { String txFee = Res.get("shared.makerTxFee", formatter.formatCoinWithCode(offer.getTxFee())) + " / " + - Res.get("shared.takerTxFee", formatter.formatCoinWithCode(offer.getTxFee().multiply(3L))); + Res.get("shared.takerTxFee", formatter.formatCoinWithCode(trade.getTxFee().multiply(3))); addConfirmationLabelLabel(gridPane, ++rowIndex, Res.get("tradeDetailsWindow.txFee"), txFee); NodeAddress arbitratorNodeAddress = trade.getArbitratorNodeAddress(); From 80c23883c81aaea42122e897c82561abd211a7a0 Mon Sep 17 00:00:00 2001 From: chimp1984 Date: Mon, 15 Mar 2021 16:30:11 -0500 Subject: [PATCH 5/5] Add popup for summary of trade history --- .../main/java/bisq/core/util/VolumeUtil.java | 15 ++ .../resources/i18n/displayStrings.properties | 12 ++ .../java/bisq/desktop/main/PriceUtil.java | 11 ++ .../windows/ClosedTradesSummaryWindow.java | 89 +++++++++++ .../closedtrades/ClosedTradesDataModel.java | 142 +++++++++++++++++- .../closedtrades/ClosedTradesView.fxml | 1 + .../closedtrades/ClosedTradesView.java | 7 +- .../closedtrades/ClosedTradesViewModel.java | 74 ++++++++- 8 files changed, 343 insertions(+), 8 deletions(-) create mode 100644 desktop/src/main/java/bisq/desktop/main/overlays/windows/ClosedTradesSummaryWindow.java diff --git a/core/src/main/java/bisq/core/util/VolumeUtil.java b/core/src/main/java/bisq/core/util/VolumeUtil.java index 71712bd3657..4150aaa8758 100644 --- a/core/src/main/java/bisq/core/util/VolumeUtil.java +++ b/core/src/main/java/bisq/core/util/VolumeUtil.java @@ -17,8 +17,15 @@ package bisq.core.util; +import bisq.core.monetary.Altcoin; +import bisq.core.monetary.AltcoinExchangeRate; +import bisq.core.monetary.Price; import bisq.core.monetary.Volume; +import org.bitcoinj.core.Coin; +import org.bitcoinj.utils.ExchangeRate; +import org.bitcoinj.utils.Fiat; + public class VolumeUtil { public static Volume getRoundedFiatVolume(Volume volumeByAmount) { @@ -47,4 +54,12 @@ public static Volume getAdjustedFiatVolume(Volume volumeByAmount, int factor) { roundedVolume = Math.max(factor, roundedVolume); return Volume.parse(String.valueOf(roundedVolume), volumeByAmount.getCurrencyCode()); } + + public static Volume getVolume(Coin amount, Price price) { + if (price.getMonetary() instanceof Altcoin) { + return new Volume(new AltcoinExchangeRate((Altcoin) price.getMonetary()).coinToAltcoin(amount)); + } else { + return new Volume(new ExchangeRate((Fiat) price.getMonetary()).coinToFiat(amount)); + } + } } diff --git a/core/src/main/resources/i18n/displayStrings.properties b/core/src/main/resources/i18n/displayStrings.properties index ad211c2e1ac..8596c3bbcc5 100644 --- a/core/src/main/resources/i18n/displayStrings.properties +++ b/core/src/main/resources/i18n/displayStrings.properties @@ -119,6 +119,7 @@ shared.sendingConfirmation=Sending confirmation... shared.sendingConfirmationAgain=Please send confirmation again shared.exportCSV=Export to CSV shared.exportJSON=Export to JSON +shared.summary=Show summary shared.noDateAvailable=No date available shared.noDetailsAvailable=No details available shared.notUsedYet=Not used yet @@ -2722,6 +2723,17 @@ txDetailsWindow.bsq.note=You have sent BSQ funds. \ txDetailsWindow.sentTo=Sent to txDetailsWindow.txId=TxId +closedTradesSummaryWindow.headline=Trade history summary +closedTradesSummaryWindow.totalAmount.title=Total trade amount +closedTradesSummaryWindow.totalAmount.value={0} ({1} with current market price) +closedTradesSummaryWindow.totalVolume.title=Total amount traded in {0} +closedTradesSummaryWindow.totalMinerFee.title=Sum of all miner fees +closedTradesSummaryWindow.totalMinerFee.value={0} ({1} of total trade amount) +closedTradesSummaryWindow.totalTradeFeeInBtc.title=Sum of all trade fees paid in BTC +closedTradesSummaryWindow.totalTradeFeeInBtc.value={0} ({1} of total trade amount) +closedTradesSummaryWindow.totalTradeFeeInBsq.title=Sum of all trade fees paid in BSQ +closedTradesSummaryWindow.totalTradeFeeInBsq.value={0} ({1} of total trade amount) + walletPasswordWindow.headline=Enter password to unlock torNetworkSettingWindow.header=Tor networks settings diff --git a/desktop/src/main/java/bisq/desktop/main/PriceUtil.java b/desktop/src/main/java/bisq/desktop/main/PriceUtil.java index a63e0086b19..7901c4689cc 100644 --- a/desktop/src/main/java/bisq/desktop/main/PriceUtil.java +++ b/desktop/src/main/java/bisq/desktop/main/PriceUtil.java @@ -106,6 +106,17 @@ public static InputValidator.ValidationResult isTriggerPriceValid(String trigger } } + public static Price marketPriceToPrice(MarketPrice marketPrice) { + String currencyCode = marketPrice.getCurrencyCode(); + double priceAsDouble = marketPrice.getPrice(); + int precision = CurrencyUtil.isCryptoCurrency(currencyCode) ? + Altcoin.SMALLEST_UNIT_EXPONENT : + Fiat.SMALLEST_UNIT_EXPONENT; + double scaled = MathUtils.scaleUpByPowerOf10(priceAsDouble, precision); + long roundedToLong = MathUtils.roundDoubleToLong(scaled); + return Price.valueOf(currencyCode, roundedToLong); + } + public void recalculateBsq30DayAveragePrice() { bsq30DayAveragePrice = null; bsq30DayAveragePrice = getBsq30DayAveragePrice(); diff --git a/desktop/src/main/java/bisq/desktop/main/overlays/windows/ClosedTradesSummaryWindow.java b/desktop/src/main/java/bisq/desktop/main/overlays/windows/ClosedTradesSummaryWindow.java new file mode 100644 index 00000000000..97099f3ad36 --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/main/overlays/windows/ClosedTradesSummaryWindow.java @@ -0,0 +1,89 @@ +/* + * This file is part of Bisq. + * + * Bisq is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bisq is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bisq. If not, see . + */ + +package bisq.desktop.main.overlays.windows; + +import bisq.desktop.main.overlays.Overlay; +import bisq.desktop.main.portfolio.closedtrades.ClosedTradesViewModel; +import bisq.desktop.util.Layout; + +import bisq.core.locale.Res; + +import org.bitcoinj.core.Coin; + +import javax.inject.Inject; + +import javafx.geometry.Insets; + +import java.util.Map; + +import static bisq.desktop.util.FormBuilder.addConfirmationLabelLabel; +import static bisq.desktop.util.FormBuilder.addTitledGroupBg; + +public class ClosedTradesSummaryWindow extends Overlay { + private final ClosedTradesViewModel model; + + @Inject + public ClosedTradesSummaryWindow(ClosedTradesViewModel model) { + this.model = model; + type = Type.Information; + } + + public void show() { + rowIndex = 0; + width = 900; + createGridPane(); + addContent(); + addButtons(); + display(); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Protected + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + protected void createGridPane() { + super.createGridPane(); + gridPane.setPadding(new Insets(35, 40, 30, 40)); + gridPane.getStyleClass().add("grid-pane"); + } + + private void addContent() { + Map totalVolumeByCurrency = model.getTotalVolumeByCurrency(); + int rowSpan = totalVolumeByCurrency.size() + 4; + addTitledGroupBg(gridPane, rowIndex, rowSpan, Res.get("closedTradesSummaryWindow.headline")); + Coin totalTradeAmount = model.getTotalTradeAmount(); + addConfirmationLabelLabel(gridPane, rowIndex, + Res.get("closedTradesSummaryWindow.totalAmount.title"), + model.getTotalAmountWithVolume(totalTradeAmount), Layout.TWICE_FIRST_ROW_DISTANCE); + totalVolumeByCurrency.entrySet().forEach(entry -> { + addConfirmationLabelLabel(gridPane, ++rowIndex, + Res.get("closedTradesSummaryWindow.totalVolume.title", entry.getKey()), entry.getValue()); + }); + addConfirmationLabelLabel(gridPane, ++rowIndex, + Res.get("closedTradesSummaryWindow.totalMinerFee.title"), + model.getTotalTxFee(totalTradeAmount)); + addConfirmationLabelLabel(gridPane, ++rowIndex, + Res.get("closedTradesSummaryWindow.totalTradeFeeInBtc.title"), + model.getTotalTradeFeeInBtc(totalTradeAmount)); + addConfirmationLabelLabel(gridPane, ++rowIndex, + Res.get("closedTradesSummaryWindow.totalTradeFeeInBsq.title") + " ", // lets give some extra space + model.getTotalTradeFeeInBsq(totalTradeAmount)); + } +} diff --git a/desktop/src/main/java/bisq/desktop/main/portfolio/closedtrades/ClosedTradesDataModel.java b/desktop/src/main/java/bisq/desktop/main/portfolio/closedtrades/ClosedTradesDataModel.java index 47eaf7e0189..2d24efe97c4 100644 --- a/desktop/src/main/java/bisq/desktop/main/portfolio/closedtrades/ClosedTradesDataModel.java +++ b/desktop/src/main/java/bisq/desktop/main/portfolio/closedtrades/ClosedTradesDataModel.java @@ -18,11 +18,27 @@ package bisq.desktop.main.portfolio.closedtrades; import bisq.desktop.common.model.ActivatableDataModel; +import bisq.desktop.main.PriceUtil; +import bisq.core.btc.wallet.BsqWalletService; +import bisq.core.monetary.Price; +import bisq.core.monetary.Volume; import bisq.core.offer.Offer; import bisq.core.offer.OfferPayload; +import bisq.core.provider.price.MarketPrice; +import bisq.core.provider.price.PriceFeedService; import bisq.core.trade.Tradable; +import bisq.core.trade.Trade; import bisq.core.trade.closed.ClosedTradableManager; +import bisq.core.trade.statistics.TradeStatisticsManager; +import bisq.core.user.Preferences; +import bisq.core.util.AveragePriceUtil; +import bisq.core.util.VolumeUtil; + +import bisq.common.util.Tuple2; + +import org.bitcoinj.core.Coin; +import org.bitcoinj.utils.Fiat; import com.google.inject.Inject; @@ -30,17 +46,33 @@ import javafx.collections.ListChangeListener; import javafx.collections.ObservableList; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; import java.util.stream.Collectors; class ClosedTradesDataModel extends ActivatableDataModel { final ClosedTradableManager closedTradableManager; + private final BsqWalletService bsqWalletService; + private final Preferences preferences; + private final TradeStatisticsManager tradeStatisticsManager; + private final PriceFeedService priceFeedService; private final ObservableList list = FXCollections.observableArrayList(); private final ListChangeListener tradesListChangeListener; @Inject - public ClosedTradesDataModel(ClosedTradableManager closedTradableManager) { + public ClosedTradesDataModel(ClosedTradableManager closedTradableManager, + BsqWalletService bsqWalletService, + Preferences preferences, + TradeStatisticsManager tradeStatisticsManager, + PriceFeedService priceFeedService) { this.closedTradableManager = closedTradableManager; + this.bsqWalletService = bsqWalletService; + this.preferences = preferences; + this.tradeStatisticsManager = tradeStatisticsManager; + this.priceFeedService = priceFeedService; tradesListChangeListener = change -> applyList(); } @@ -73,4 +105,112 @@ private void applyList() { list.sort((o1, o2) -> o2.getTradable().getDate().compareTo(o1.getTradable().getDate())); } + boolean wasMyOffer(Tradable tradable) { + return closedTradableManager.wasMyOffer(tradable.getOffer()); + } + + Coin getTotalAmount() { + return Coin.valueOf(getList().stream() + .map(ClosedTradableListItem::getTradable) + .filter(e -> e instanceof Trade) + .map(e -> (Trade) e) + .mapToLong(Trade::getTradeAmountAsLong) + .sum()); + } + + Map getTotalVolumeByCurrency() { + Map map = new HashMap<>(); + getList().stream() + .map(ClosedTradableListItem::getTradable) + .filter(e -> e instanceof Trade) + .map(e -> (Trade) e) + .map(Trade::getTradeVolume) + .filter(Objects::nonNull) + .forEach(volume -> { + String currencyCode = volume.getCurrencyCode(); + map.putIfAbsent(currencyCode, 0L); + map.put(currencyCode, volume.getValue() + map.get(currencyCode)); + }); + return map; + } + + public Optional getVolumeInUserFiatCurrency(Coin amount) { + return getVolume(amount, preferences.getPreferredTradeCurrency().getCode()); + } + + public Optional getVolume(Coin amount, String currencyCode) { + MarketPrice marketPrice = priceFeedService.getMarketPrice(currencyCode); + if (marketPrice == null) { + return Optional.empty(); + } + + Price price = PriceUtil.marketPriceToPrice(marketPrice); + return Optional.of(VolumeUtil.getVolume(amount, price)); + } + + public Volume getBsqVolumeInUsdWithAveragePrice(Coin amount) { + Tuple2 tuple = AveragePriceUtil.getAveragePriceTuple(preferences, tradeStatisticsManager, 30); + Price usdPrice = tuple.first; + long value = Math.round(amount.value * usdPrice.getValue() / 100d); + return new Volume(Fiat.valueOf("USD", value)); + } + + public Coin getTotalTxFee() { + return Coin.valueOf(getList().stream() + .map(ClosedTradableListItem::getTradable) + .mapToLong(tradable -> { + if (wasMyOffer(tradable)) { + return tradable.getOffer().getTxFee().value; + } else { + // taker pays for 3 transactions + return ((Trade) tradable).getTxFee().multiply(3).value; + } + }) + .sum()); + } + + public Coin getTotalTradeFee(boolean expectBtcFee) { + return Coin.valueOf(getList().stream() + .map(ClosedTradableListItem::getTradable) + .mapToLong(tradable -> getTradeFee(tradable, expectBtcFee)) + .sum()); + } + + protected long getTradeFee(Tradable tradable, boolean expectBtcFee) { + Offer offer = tradable.getOffer(); + if (wasMyOffer(tradable)) { + String makerFeeTxId = offer.getOfferFeePaymentTxId(); + boolean notInBsqWallet = bsqWalletService.getTransaction(makerFeeTxId) == null; + if (expectBtcFee) { + if (notInBsqWallet) { + return offer.getMakerFee().value; + } else { + return 0; + } + } else { + if (notInBsqWallet) { + return 0; + } else { + return offer.getMakerFee().value; + } + } + } else { + Trade trade = (Trade) tradable; + String takerFeeTxId = trade.getTakerFeeTxId(); + boolean notInBsqWallet = bsqWalletService.getTransaction(takerFeeTxId) == null; + if (expectBtcFee) { + if (notInBsqWallet) { + return trade.getTakerFee().value; + } else { + return 0; + } + } else { + if (notInBsqWallet) { + return 0; + } else { + return trade.getTakerFee().value; + } + } + } + } } diff --git a/desktop/src/main/java/bisq/desktop/main/portfolio/closedtrades/ClosedTradesView.fxml b/desktop/src/main/java/bisq/desktop/main/portfolio/closedtrades/ClosedTradesView.fxml index d94c6823734..5e0f1cd0f1a 100644 --- a/desktop/src/main/java/bisq/desktop/main/portfolio/closedtrades/ClosedTradesView.fxml +++ b/desktop/src/main/java/bisq/desktop/main/portfolio/closedtrades/ClosedTradesView.fxml @@ -62,6 +62,7 @@ diff --git a/desktop/src/main/java/bisq/desktop/main/portfolio/closedtrades/ClosedTradesView.java b/desktop/src/main/java/bisq/desktop/main/portfolio/closedtrades/ClosedTradesView.java index 2ec1fc97a80..50841a3afde 100644 --- a/desktop/src/main/java/bisq/desktop/main/portfolio/closedtrades/ClosedTradesView.java +++ b/desktop/src/main/java/bisq/desktop/main/portfolio/closedtrades/ClosedTradesView.java @@ -25,6 +25,7 @@ import bisq.desktop.components.HyperlinkWithIcon; import bisq.desktop.components.InputTextField; import bisq.desktop.components.PeerInfoIcon; +import bisq.desktop.main.overlays.windows.ClosedTradesSummaryWindow; import bisq.desktop.main.overlays.windows.OfferDetailsWindow; import bisq.desktop.main.overlays.windows.TradeDetailsWindow; import bisq.desktop.util.GUIUtil; @@ -124,7 +125,7 @@ public String toString() { @FXML Pane searchBoxSpacer; @FXML - AutoTooltipButton exportButton; + AutoTooltipButton exportButton, summaryButton; @FXML Label numItems; @FXML @@ -242,6 +243,7 @@ public void initialize() { HBox.setHgrow(footerSpacer, Priority.ALWAYS); HBox.setMargin(exportButton, new Insets(0, 10, 0, 0)); exportButton.updateText(Res.get("shared.exportCSV")); + summaryButton.updateText(Res.get("shared.summary")); } @Override @@ -291,6 +293,8 @@ protected void activate() { new ClosedTradableListItem(null), sortedList, (Stage) root.getScene().getWindow()); }); + summaryButton.setOnAction(event -> new ClosedTradesSummaryWindow(model).show()); + filterTextField.textProperty().addListener(filterTextFieldListener); applyFilteredListPredicate(filterTextField.getText()); root.widthProperty().addListener(widthListener); @@ -301,6 +305,7 @@ protected void activate() { protected void deactivate() { sortedList.comparatorProperty().unbind(); exportButton.setOnAction(null); + summaryButton.setOnAction(null); filterTextField.textProperty().removeListener(filterTextFieldListener); root.widthProperty().removeListener(widthListener); diff --git a/desktop/src/main/java/bisq/desktop/main/portfolio/closedtrades/ClosedTradesViewModel.java b/desktop/src/main/java/bisq/desktop/main/portfolio/closedtrades/ClosedTradesViewModel.java index b09b2e9f42d..a7d2e8d6cc7 100644 --- a/desktop/src/main/java/bisq/desktop/main/portfolio/closedtrades/ClosedTradesViewModel.java +++ b/desktop/src/main/java/bisq/desktop/main/portfolio/closedtrades/ClosedTradesViewModel.java @@ -23,9 +23,9 @@ import bisq.core.account.witness.AccountAgeWitnessService; import bisq.core.btc.wallet.BsqWalletService; -import bisq.core.btc.wallet.BtcWalletService; import bisq.core.locale.CurrencyUtil; import bisq.core.locale.Res; +import bisq.core.monetary.Altcoin; import bisq.core.monetary.Volume; import bisq.core.offer.Offer; import bisq.core.offer.OpenOffer; @@ -37,16 +37,20 @@ import bisq.network.p2p.NodeAddress; +import org.bitcoinj.core.Coin; +import org.bitcoinj.core.Monetary; +import org.bitcoinj.utils.Fiat; + import com.google.inject.Inject; import javax.inject.Named; import javafx.collections.ObservableList; +import java.util.Map; import java.util.stream.Collectors; -class ClosedTradesViewModel extends ActivatableWithDataModel implements ViewModel { - private final BtcWalletService btcWalletService; +public class ClosedTradesViewModel extends ActivatableWithDataModel implements ViewModel { private final BsqWalletService bsqWalletService; private final BsqFormatter bsqFormatter; private final CoinFormatter btcFormatter; @@ -55,13 +59,11 @@ class ClosedTradesViewModel extends ActivatableWithDataModel { + return Res.get("closedTradesSummaryWindow.totalAmount.value", + btcFormatter.formatCoin(totalTradeAmount, true), + DisplayUtils.formatVolumeWithCode(volume)); + }) + .orElse(""); + } + + public Map getTotalVolumeByCurrency() { + return dataModel.getTotalVolumeByCurrency().entrySet().stream() + .collect(Collectors.toMap(Map.Entry::getKey, + entry -> { + String currencyCode = entry.getKey(); + Monetary monetary; + if (CurrencyUtil.isCryptoCurrency(currencyCode)) { + monetary = Altcoin.valueOf(currencyCode, entry.getValue()); + } else { + monetary = Fiat.valueOf(currencyCode, entry.getValue()); + } + return DisplayUtils.formatVolumeWithCode(new Volume(monetary)); + } + )); + } + + public String getTotalTxFee(Coin totalTradeAmount) { + Coin totalTxFee = dataModel.getTotalTxFee(); + double percentage = ((double) totalTxFee.value) / totalTradeAmount.value; + return Res.get("closedTradesSummaryWindow.totalMinerFee.value", + btcFormatter.formatCoin(totalTxFee, true), + FormattingUtils.formatToPercentWithSymbol(percentage)); + } + + public String getTotalTradeFeeInBtc(Coin totalTradeAmount) { + Coin totalTradeFee = dataModel.getTotalTradeFee(true); + double percentage = ((double) totalTradeFee.value) / totalTradeAmount.value; + return Res.get("closedTradesSummaryWindow.totalTradeFeeInBtc.value", + btcFormatter.formatCoin(totalTradeFee, true), + FormattingUtils.formatToPercentWithSymbol(percentage)); + } + + public String getTotalTradeFeeInBsq(Coin totalTradeAmount) { + return dataModel.getVolume(totalTradeAmount, "USD") + .filter(v -> v.getValue() > 0) + .map(tradeAmountVolume -> { + Coin totalTradeFee = dataModel.getTotalTradeFee(false); + Volume bsqVolumeInUsd = dataModel.getBsqVolumeInUsdWithAveragePrice(totalTradeFee); // with 4 decimal + double percentage = ((double) bsqVolumeInUsd.getValue()) / tradeAmountVolume.getValue(); + return Res.get("closedTradesSummaryWindow.totalTradeFeeInBsq.value", + bsqFormatter.formatCoin(totalTradeFee, true), + FormattingUtils.formatToPercentWithSymbol(percentage)); + }) + .orElse(""); } }