From 4f08f9f3837ae6d8b09ed91b1fa2e9bee9cb07dd Mon Sep 17 00:00:00 2001 From: jmacxx <47253594+jmacxx@users.noreply.github.com> Date: Sun, 12 Mar 2023 20:08:38 -0500 Subject: [PATCH 01/31] Feat: OCO Offers --- .../java/bisq/core/api/CoreOffersService.java | 1 + .../src/main/java/bisq/core/btc/Balances.java | 11 +- .../bisq/core/btc/model/AddressEntryList.java | 12 +- .../core/btc/wallet/BtcWalletService.java | 6 + .../bisq/core/offer/OpenOfferManager.java | 79 ++++++++- .../placeoffer/bisq_v1/PlaceOfferModel.java | 3 + .../bisq_v1/PlaceOfferProtocol.java | 21 ++- .../bisq_v1/tasks/CloneMakerFeeOco.java | 81 +++++++++ .../offer/bisq_v1/MutableOfferDataModel.java | 1 + .../openoffer/OpenOfferListItem.java | 8 + .../portfolio/openoffer/OpenOffersView.fxml | 1 + .../portfolio/openoffer/OpenOffersView.java | 162 +++++++++++++++--- 12 files changed, 345 insertions(+), 41 deletions(-) create mode 100644 core/src/main/java/bisq/core/offer/placeoffer/bisq_v1/tasks/CloneMakerFeeOco.java diff --git a/core/src/main/java/bisq/core/api/CoreOffersService.java b/core/src/main/java/bisq/core/api/CoreOffersService.java index 0405bbfe9dd..81feac117c0 100644 --- a/core/src/main/java/bisq/core/api/CoreOffersService.java +++ b/core/src/main/java/bisq/core/api/CoreOffersService.java @@ -447,6 +447,7 @@ private void placeOffer(Offer offer, openOfferManager.placeOffer(offer, buyerSecurityDepositPct, useSavingsWallet, + false, triggerPriceAsLong, resultHandler::accept, log::error); diff --git a/core/src/main/java/bisq/core/btc/Balances.java b/core/src/main/java/bisq/core/btc/Balances.java index d92a6bf0b40..cf372b37f35 100644 --- a/core/src/main/java/bisq/core/btc/Balances.java +++ b/core/src/main/java/bisq/core/btc/Balances.java @@ -42,6 +42,7 @@ import javafx.collections.ListChangeListener; import java.util.Objects; +import java.util.stream.Collectors; import java.util.stream.Stream; import lombok.Getter; @@ -110,11 +111,11 @@ private void updateAvailableBalance() { } private void updateReservedBalance() { - long sum = openOfferManager.getObservableList().stream() - .map(openOffer -> btcWalletService.getAddressEntry(openOffer.getId(), AddressEntry.Context.RESERVED_FOR_TRADE) - .orElse(null)) - .filter(Objects::nonNull) - .mapToLong(addressEntry -> btcWalletService.getBalanceForAddress(addressEntry.getAddress()).value) + long sum = btcWalletService.getAddressEntriesForOpenOffer().stream() + .collect(Collectors.toMap(AddressEntry::getAddress, p -> p, (p, q) -> p)) + .keySet() + .stream() + .mapToLong(address -> btcWalletService.getBalanceForAddress(address).value) .sum(); reservedBalance.set(Coin.valueOf(sum)); } diff --git a/core/src/main/java/bisq/core/btc/model/AddressEntryList.java b/core/src/main/java/bisq/core/btc/model/AddressEntryList.java index be9dd78fb4b..a1fc44d2e0e 100644 --- a/core/src/main/java/bisq/core/btc/model/AddressEntryList.java +++ b/core/src/main/java/bisq/core/btc/model/AddressEntryList.java @@ -210,7 +210,17 @@ public void swapToAvailable(AddressEntry addressEntry) { log.info("swapToAvailable addressEntry to swap={}", addressEntry); boolean setChangedByRemove = entrySet.remove(addressEntry); - boolean setChangedByAdd = entrySet.add(new AddressEntry(addressEntry.getKeyPair(), + + // check if the ADDRESS still has any existing entries, only if not do the add to available. + boolean entryWithSameContextAlreadyExist = entrySet.stream().anyMatch(e -> { + if (addressEntry.getAddressString() != null) { + return addressEntry.getAddressString().equals(e.getAddressString()) && + addressEntry.getContext() == addressEntry.getContext(); + } + return false; + }); + boolean setChangedByAdd = !entryWithSameContextAlreadyExist && entrySet.add( + new AddressEntry(addressEntry.getKeyPair(), AddressEntry.Context.AVAILABLE, addressEntry.isSegwit())); if (setChangedByRemove || setChangedByAdd) { diff --git a/core/src/main/java/bisq/core/btc/wallet/BtcWalletService.java b/core/src/main/java/bisq/core/btc/wallet/BtcWalletService.java index f1c5e7ad01e..c3498f2a7e6 100644 --- a/core/src/main/java/bisq/core/btc/wallet/BtcWalletService.java +++ b/core/src/main/java/bisq/core/btc/wallet/BtcWalletService.java @@ -630,6 +630,12 @@ public Optional getAddressEntry(String offerId, .findAny(); } + public AddressEntry createAddressEntryForOcoOffer(AddressEntry orgAddressEntry, String offerId) { + AddressEntry newEntry = new AddressEntry(orgAddressEntry.getKeyPair(), orgAddressEntry.getContext(), offerId, true); + addressEntryList.addAddressEntry(newEntry); + return newEntry; + } + public AddressEntry getOrCreateAddressEntry(String offerId, AddressEntry.Context context) { Optional addressEntry = getAddressEntryListAsImmutableList().stream() .filter(e -> offerId.equals(e.getOfferId())) diff --git a/core/src/main/java/bisq/core/offer/OpenOfferManager.java b/core/src/main/java/bisq/core/offer/OpenOfferManager.java index 2e6ac72488c..cca99295020 100644 --- a/core/src/main/java/bisq/core/offer/OpenOfferManager.java +++ b/core/src/main/java/bisq/core/offer/OpenOfferManager.java @@ -381,20 +381,30 @@ public void onAwakeFromStandby() { public void placeOffer(Offer offer, double buyerSecurityDeposit, boolean useSavingsWallet, + boolean useOco, long triggerPrice, TransactionResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) { checkNotNull(offer.getMakerFee(), "makerFee must not be null"); checkArgument(!offer.isBsqSwapOffer()); + int numClones = getOpenOffersByMakerFeeTxId(offer.getOfferFeePaymentTxId()).size(); + if (numClones > 10) { + errorMessageHandler.handleErrorMessage("PlaceOffer prevented because cloned OCO offers count is " + numClones); + return; + } + Coin reservedFundsForOffer = createOfferService.getReservedFundsForOffer(offer.getDirection(), offer.getAmount(), buyerSecurityDeposit, createOfferService.getSellerSecurityDepositAsDouble(buyerSecurityDeposit)); + offer.setPriceFeedService(priceFeedService); + PlaceOfferModel model = new PlaceOfferModel(offer, reservedFundsForOffer, useSavingsWallet, + useOco, btcWalletService, tradeWalletService, bsqWalletService, @@ -409,6 +419,7 @@ public void placeOffer(Offer offer, model, transaction -> { OpenOffer openOffer = new OpenOffer(offer, triggerPrice); + openOffer.setState(useOco ? OpenOffer.State.DEACTIVATED : OpenOffer.State.AVAILABLE); addOpenOfferToList(openOffer); if (!stopped) { startPeriodicRepublishOffersTimer(); @@ -465,6 +476,10 @@ public void activateOpenOffer(OpenOffer openOffer, return; } + if (isSpam(openOffer)) { + return; + } + Offer offer = openOffer.getOffer(); offerBookService.activateOffer(offer, () -> { @@ -585,7 +600,8 @@ private void onRemoved(OpenOffer openOffer, ResultHandler resultHandler, Offer o offer.setState(Offer.State.REMOVED); openOffer.setState(OpenOffer.State.CANCELED); removeOpenOfferFromList(openOffer); - if (!openOffer.getOffer().isBsqSwapOffer()) { + + if (!openOffer.getOffer().isBsqSwapOffer() && !safeRemovalOfOcoClone(openOffer)) { closedTradableManager.add(openOffer); btcWalletService.resetAddressEntriesForOpenOffer(offer.getId()); } @@ -595,13 +611,27 @@ private void onRemoved(OpenOffer openOffer, ResultHandler resultHandler, Offer o // Close openOffer after deposit published public void closeOpenOffer(Offer offer) { - getOpenOfferById(offer.getId()).ifPresent(openOffer -> { - removeOpenOfferFromList(openOffer); - openOffer.setState(OpenOffer.State.CLOSED); - offerBookService.removeOffer(openOffer.getOffer().getOfferPayloadBase(), - () -> log.trace("Successful removed offer"), - log::error); - }); + if (offer.isBsqSwapOffer()) { + getOpenOfferById(offer.getId()).ifPresent(openOffer -> { + removeOpenOfferFromList(openOffer); + openOffer.setState(OpenOffer.State.CLOSED); + offerBookService.removeOffer(openOffer.getOffer().getOfferPayloadBase(), + () -> log.trace("Successfully removed offer"), + log::error); + }); + } else { + // offer taken may have been OCO, in which case all its clones need to be removed + getOpenOffersByMakerFeeTxId(offer.getOfferFeePaymentTxId()).forEach(openOffer -> { + removeOpenOfferFromList(openOffer); + openOffer.setState(OpenOffer.State.CLOSED); + if (!offer.getId().equalsIgnoreCase(openOffer.getId())) { + btcWalletService.resetAddressEntriesForOpenOffer(openOffer.getId()); // cleanup OCO clone + } + offerBookService.removeOffer(openOffer.getOffer().getOfferPayloadBase(), + () -> log.trace("Successfully removed offer"), + log::error); + }); + } } public void reserveOpenOffer(OpenOffer openOffer) { @@ -626,6 +656,11 @@ public Optional getOpenOfferById(String offerId) { return openOffers.stream().filter(e -> e.getId().equals(offerId)).findFirst(); } + public List getOpenOffersByMakerFeeTxId(String makerFeeTxId) { + String safeSearch = makerFeeTxId == null ? "" : makerFeeTxId; + return openOffers.stream().filter(e -> !e.getOffer().isBsqSwapOffer() && e.getOffer().getOfferFeePaymentTxId().equals(safeSearch)).collect(Collectors.toList()); + } + /////////////////////////////////////////////////////////////////////////////////////////// // OfferPayload Availability @@ -1143,7 +1178,33 @@ private boolean isBsqSwapOfferLackingFunds(OpenOffer openOffer) { openOffer.isBsqSwapOfferHasMissingFunds(); } + public boolean isSpam(OpenOffer newOffer) { + // an offer is spam if the user has another open offer on the same ccy, payment method, and reserved UTXO + long matchingOffers = openOffers.stream() + .filter((openOffer -> !openOffer.getOffer().isBsqSwapOffer())) + .filter(openOffer -> !openOffer.isDeactivated()) + .filter(openOffer -> !openOffer.getShortId().equalsIgnoreCase(newOffer.getShortId())) + .filter(openOffer1 -> { + Offer newOffer1 = newOffer.getOffer(); + Offer offer1 = openOffer1.getOffer(); + return + offer1.getOfferFeePaymentTxId().equalsIgnoreCase(newOffer1.getOfferFeePaymentTxId()) && + offer1.getPaymentMethodId().equalsIgnoreCase(newOffer1.getPaymentMethodId()) && + offer1.getCounterCurrencyCode().equalsIgnoreCase(newOffer1.getCounterCurrencyCode()) && + offer1.getBaseCurrencyCode().equalsIgnoreCase(newOffer1.getBaseCurrencyCode()); + }) + .count(); + if (matchingOffers > 0) { + log.info("{} is considered spam", newOffer.getShortId()); + } + return matchingOffers > 0; + } + + public boolean safeRemovalOfOcoClone(OpenOffer openOffer) { + return getOpenOffersByMakerFeeTxId(openOffer.getOffer().getOfferFeePaymentTxId()).size() > 1; + } + private boolean preventedFromPublishing(OpenOffer openOffer) { - return openOffer.isDeactivated() || openOffer.isBsqSwapOfferHasMissingFunds(); + return openOffer.isDeactivated() || openOffer.isBsqSwapOfferHasMissingFunds() || isSpam(openOffer); } } diff --git a/core/src/main/java/bisq/core/offer/placeoffer/bisq_v1/PlaceOfferModel.java b/core/src/main/java/bisq/core/offer/placeoffer/bisq_v1/PlaceOfferModel.java index 0b44a8b6491..8355d472bbb 100644 --- a/core/src/main/java/bisq/core/offer/placeoffer/bisq_v1/PlaceOfferModel.java +++ b/core/src/main/java/bisq/core/offer/placeoffer/bisq_v1/PlaceOfferModel.java @@ -45,6 +45,7 @@ public class PlaceOfferModel implements Model { private final Offer offer; private final Coin reservedFundsForOffer; private final boolean useSavingsWallet; + private final boolean useOco; private final BtcWalletService walletService; private final TradeWalletService tradeWalletService; private final BsqWalletService bsqWalletService; @@ -66,6 +67,7 @@ public class PlaceOfferModel implements Model { public PlaceOfferModel(Offer offer, Coin reservedFundsForOffer, boolean useSavingsWallet, + boolean useOco, BtcWalletService walletService, TradeWalletService tradeWalletService, BsqWalletService bsqWalletService, @@ -79,6 +81,7 @@ public PlaceOfferModel(Offer offer, this.offer = offer; this.reservedFundsForOffer = reservedFundsForOffer; this.useSavingsWallet = useSavingsWallet; + this.useOco = useOco; this.walletService = walletService; this.tradeWalletService = tradeWalletService; this.bsqWalletService = bsqWalletService; diff --git a/core/src/main/java/bisq/core/offer/placeoffer/bisq_v1/PlaceOfferProtocol.java b/core/src/main/java/bisq/core/offer/placeoffer/bisq_v1/PlaceOfferProtocol.java index b3f50a95e55..05f6ccbdc96 100644 --- a/core/src/main/java/bisq/core/offer/placeoffer/bisq_v1/PlaceOfferProtocol.java +++ b/core/src/main/java/bisq/core/offer/placeoffer/bisq_v1/PlaceOfferProtocol.java @@ -19,6 +19,7 @@ import bisq.core.offer.placeoffer.bisq_v1.tasks.AddToOfferBook; import bisq.core.offer.placeoffer.bisq_v1.tasks.CheckNumberOfUnconfirmedTransactions; +import bisq.core.offer.placeoffer.bisq_v1.tasks.CloneMakerFeeOco; import bisq.core.offer.placeoffer.bisq_v1.tasks.CreateMakerFeeTx; import bisq.core.offer.placeoffer.bisq_v1.tasks.ValidateOffer; import bisq.core.trade.bisq_v1.TransactionResultHandler; @@ -76,12 +77,20 @@ public void placeOffer() { errorMessageHandler.handleErrorMessage(errorMessage); } ); - taskRunner.addTasks( - ValidateOffer.class, - CheckNumberOfUnconfirmedTransactions.class, - CreateMakerFeeTx.class, - AddToOfferBook.class - ); + + if (model.isUseOco()) { + taskRunner.addTasks( + ValidateOffer.class, + CloneMakerFeeOco.class + ); + } else { + taskRunner.addTasks( + ValidateOffer.class, + CheckNumberOfUnconfirmedTransactions.class, + CreateMakerFeeTx.class, + AddToOfferBook.class + ); + } taskRunner.run(); } diff --git a/core/src/main/java/bisq/core/offer/placeoffer/bisq_v1/tasks/CloneMakerFeeOco.java b/core/src/main/java/bisq/core/offer/placeoffer/bisq_v1/tasks/CloneMakerFeeOco.java new file mode 100644 index 00000000000..dc68b4a7628 --- /dev/null +++ b/core/src/main/java/bisq/core/offer/placeoffer/bisq_v1/tasks/CloneMakerFeeOco.java @@ -0,0 +1,81 @@ +/* + * 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.core.offer.placeoffer.bisq_v1.tasks; + +import bisq.core.btc.model.AddressEntry; +import bisq.core.btc.wallet.BtcWalletService; +import bisq.core.btc.wallet.WalletService; +import bisq.core.offer.Offer; +import bisq.core.offer.placeoffer.bisq_v1.PlaceOfferModel; + +import bisq.common.taskrunner.Task; +import bisq.common.taskrunner.TaskRunner; + +import org.bitcoinj.core.Address; +import org.bitcoinj.core.Transaction; +import org.bitcoinj.core.TransactionOutput; + +import java.util.List; +import java.util.Optional; + +public class CloneMakerFeeOco extends Task { + @SuppressWarnings({"unused"}) + public CloneMakerFeeOco(TaskRunner taskHandler, PlaceOfferModel model) { + super(taskHandler, model); + } + + @Override + protected void run() { + runInterceptHook(); + Offer newOcoOffer = model.getOffer(); + // newOcoOffer is cloned from an existing offer; + // the clone needs a unique AddressEntry record associating the offerId with the reserved amount. + BtcWalletService walletService = model.getWalletService(); + for (AddressEntry potentialOcoSource : walletService.getAddressEntries(AddressEntry.Context.RESERVED_FOR_TRADE)) { + getTxIdFromAddress(walletService, potentialOcoSource.getAddress()).ifPresent(txId -> { + if (txId.equalsIgnoreCase(newOcoOffer.getOfferFeePaymentTxId())) { + walletService.createAddressEntryForOcoOffer(potentialOcoSource, newOcoOffer.getId()); + newOcoOffer.setState(Offer.State.OFFER_FEE_PAID); + complete(); + } + }); + if (completed) { + return; + } + } + failed(); + } + + // AddressEntry and TxId are not linked, so do a reverse lookup + private Optional getTxIdFromAddress(BtcWalletService walletService, Address address) { + List txns = walletService.getRecentTransactions(10, false); + for (Transaction txn : txns) { + for (TransactionOutput output : txn.getOutputs()) { + if (walletService.isTransactionOutputMine(output) && WalletService.isOutputScriptConvertibleToAddress(output)) { + String addressString = WalletService.getAddressStringFromOutput(output); + assert addressString != null; + // make sure the output is still unspent + if (addressString.equalsIgnoreCase(address.toString()) && output.getSpentBy() == null) { + return Optional.of(txn.getTxId().toString()); + } + } + } + } + return Optional.empty(); + } +} diff --git a/desktop/src/main/java/bisq/desktop/main/offer/bisq_v1/MutableOfferDataModel.java b/desktop/src/main/java/bisq/desktop/main/offer/bisq_v1/MutableOfferDataModel.java index 68ff7444745..27181ef0c78 100644 --- a/desktop/src/main/java/bisq/desktop/main/offer/bisq_v1/MutableOfferDataModel.java +++ b/desktop/src/main/java/bisq/desktop/main/offer/bisq_v1/MutableOfferDataModel.java @@ -334,6 +334,7 @@ void onPlaceOffer(Offer offer, TransactionResultHandler resultHandler) { openOfferManager.placeOffer(offer, buyerSecurityDeposit.get(), useSavingsWallet, + false, triggerPrice, resultHandler, log::error); diff --git a/desktop/src/main/java/bisq/desktop/main/portfolio/openoffer/OpenOfferListItem.java b/desktop/src/main/java/bisq/desktop/main/portfolio/openoffer/OpenOfferListItem.java index 11cf4266284..f9d48804f4a 100644 --- a/desktop/src/main/java/bisq/desktop/main/portfolio/openoffer/OpenOfferListItem.java +++ b/desktop/src/main/java/bisq/desktop/main/portfolio/openoffer/OpenOfferListItem.java @@ -134,6 +134,14 @@ public String getTriggerPriceAsString() { } } + public String getOcoGroupAsString() { + Offer offer = getOffer(); + if (offer.isBsqSwapOffer()) { + return ""; + } + return offer.getOfferFeePaymentTxId().substring(0, 4); + } + @Override public boolean match(String filterString) { if (filterString.isEmpty()) { diff --git a/desktop/src/main/java/bisq/desktop/main/portfolio/openoffer/OpenOffersView.fxml b/desktop/src/main/java/bisq/desktop/main/portfolio/openoffer/OpenOffersView.fxml index 4a0e6201e85..ab650ea0180 100644 --- a/desktop/src/main/java/bisq/desktop/main/portfolio/openoffer/OpenOffersView.fxml +++ b/desktop/src/main/java/bisq/desktop/main/portfolio/openoffer/OpenOffersView.fxml @@ -50,6 +50,7 @@ + diff --git a/desktop/src/main/java/bisq/desktop/main/portfolio/openoffer/OpenOffersView.java b/desktop/src/main/java/bisq/desktop/main/portfolio/openoffer/OpenOffersView.java index 7a0d8d59c17..15fcd4aa7ad 100644 --- a/desktop/src/main/java/bisq/desktop/main/portfolio/openoffer/OpenOffersView.java +++ b/desktop/src/main/java/bisq/desktop/main/portfolio/openoffer/OpenOffersView.java @@ -39,6 +39,8 @@ import bisq.core.locale.Res; import bisq.core.offer.Offer; import bisq.core.offer.OpenOffer; +import bisq.core.offer.OpenOfferManager; +import bisq.core.offer.bisq_v1.OfferPayload; import bisq.core.user.DontShowAgainLookup; import com.googlecode.jcsv.writer.CSVEntryConverter; @@ -52,6 +54,7 @@ import javafx.stage.Stage; import javafx.scene.control.Button; +import javafx.scene.control.ContentDisplay; import javafx.scene.control.ContextMenu; import javafx.scene.control.Label; import javafx.scene.control.MenuItem; @@ -80,9 +83,12 @@ import javafx.util.Callback; import java.util.Comparator; +import java.util.Date; +import java.util.stream.Collectors; import org.jetbrains.annotations.NotNull; +import static bisq.core.offer.OfferUtil.getRandomOfferId; import static bisq.desktop.util.FormBuilder.getRegularIconButton; import static bisq.desktop.util.FormBuilder.getRegularIconForLabel; @@ -101,6 +107,7 @@ private enum ColumnNames { VOLUME(Res.get("shared.amountMinMax")), PAYMENT_METHOD(Res.get("shared.paymentMethod")), DIRECTION(Res.get("shared.offerType")), + GROUP("Group"), STATUS(Res.get("shared.state")); private final String text; @@ -119,7 +126,7 @@ public String toString() { TableView tableView; @FXML TableColumn priceColumn, deviationColumn, amountColumn, volumeColumn, - marketColumn, directionColumn, dateColumn, offerIdColumn, deactivateItemColumn, + marketColumn, directionColumn, dateColumn, offerIdColumn, deactivateItemColumn, groupColumn, removeItemColumn, editItemColumn, triggerPriceColumn, triggerIconColumn, paymentMethodColumn, duplicateItemColumn; @FXML FilterBox filterBox; @@ -135,6 +142,7 @@ public String toString() { private final Navigation navigation; private final OfferDetailsWindow offerDetailsWindow; private final BsqSwapOfferDetailsWindow bsqSwapOfferDetailsWindow; + private final OpenOfferManager openOfferManager; private SortedList sortedList; private PortfolioView.OpenOfferActionHandler openOfferActionHandler; private ChangeListener widthListener; @@ -142,6 +150,7 @@ public String toString() { @Inject public OpenOffersView(OpenOffersViewModel model, + OpenOfferManager openOfferManager, Navigation navigation, OfferDetailsWindow offerDetailsWindow, BsqSwapOfferDetailsWindow bsqSwapOfferDetailsWindow) { @@ -149,6 +158,7 @@ public OpenOffersView(OpenOffersViewModel model, this.navigation = navigation; this.offerDetailsWindow = offerDetailsWindow; this.bsqSwapOfferDetailsWindow = bsqSwapOfferDetailsWindow; + this.openOfferManager = openOfferManager; } @Override @@ -159,6 +169,7 @@ public void initialize() { deviationColumn.setGraphic(new AutoTooltipTableColumn<>(ColumnNames.DEVIATION.toString(), Res.get("portfolio.closedTrades.deviation.help")).getGraphic()); triggerPriceColumn.setGraphic(new AutoTooltipLabel(ColumnNames.TRIGGER_PRICE.toString())); + groupColumn.setGraphic(new AutoTooltipLabel(ColumnNames.GROUP.toString())); amountColumn.setGraphic(new AutoTooltipLabel(ColumnNames.AMOUNT.toString())); volumeColumn.setGraphic(new AutoTooltipLabel(ColumnNames.VOLUME.toString())); marketColumn.setGraphic(new AutoTooltipLabel(ColumnNames.MARKET.toString())); @@ -183,6 +194,7 @@ public void initialize() { setEditColumnCellFactory(); setTriggerIconColumnCellFactory(); setTriggerPriceColumnCellFactory(); + setGroupColumnCellFactory(); setDuplicateColumnCellFactory(); setRemoveColumnCellFactory(); @@ -197,11 +209,12 @@ public void initialize() { deviationColumn.setComparator(Comparator.comparing(OpenOfferListItem::getPriceDeviationAsDouble, Comparator.nullsFirst(Comparator.naturalOrder()))); triggerPriceColumn.setComparator(Comparator.comparing(o -> o.getOpenOffer().getTriggerPrice(), Comparator.nullsFirst(Comparator.naturalOrder()))); + groupColumn.setComparator(Comparator.comparing(OpenOfferListItem::getOcoGroupAsString)); volumeColumn.setComparator(Comparator.comparing(o -> o.getOffer().getVolume(), Comparator.nullsFirst(Comparator.naturalOrder()))); dateColumn.setComparator(Comparator.comparing(o -> o.getOffer().getDate())); paymentMethodColumn.setComparator(Comparator.comparing(o -> Res.get(o.getOffer().getPaymentMethod().getId()))); - dateColumn.setSortType(TableColumn.SortType.DESCENDING); + dateColumn.setSortType(TableColumn.SortType.ASCENDING); tableView.getSortOrder().add(dateColumn); tableView.setRowFactory( @@ -209,8 +222,14 @@ public void initialize() { final TableRow row = new TableRow<>(); final ContextMenu rowMenu = new ContextMenu(); MenuItem duplicateItem = new MenuItem(Res.get("portfolio.context.offerLikeThis")); - duplicateItem.setOnAction((event) -> onDuplicateOffer(row.getItem().getOffer())); + duplicateItem.setOnAction((event) -> onDuplicateOffer(row.getItem())); + MenuItem duplicateItemOco1 = new MenuItem("Duplicate as OCO"); + duplicateItemOco1.setOnAction((event) -> onDuplicateOfferOco(row.getItem(), 1)); + MenuItem duplicateItemOco5 = new MenuItem("Duplicate as OCO x5"); + duplicateItemOco5.setOnAction((event) -> onDuplicateOfferOco(row.getItem(), 5)); rowMenu.getItems().add(duplicateItem); + rowMenu.getItems().add(duplicateItemOco1); + rowMenu.getItems().add(duplicateItemOco5); row.contextMenuProperty().bind( Bindings.when(Bindings.isNotNull(row.itemProperty())) .then(rowMenu) @@ -281,6 +300,7 @@ protected void activate() { columns[ColumnNames.VOLUME.ordinal()] = item.getVolumeAsString(); columns[ColumnNames.PAYMENT_METHOD.ordinal()] = item.getPaymentMethodAsString(); columns[ColumnNames.DIRECTION.ordinal()] = item.getDirectionLabel(); + columns[ColumnNames.GROUP.ordinal()] = item.getOcoGroupAsString(); columns[ColumnNames.STATUS.ordinal()] = String.valueOf(!item.getOpenOffer().isDeactivated()); return columns; }; @@ -299,6 +319,14 @@ protected void activate() { private void updateNumberOfOffers() { numItems.setText(Res.get("shared.numItemsLabel", sortedList.size())); + groupColumn.setVisible(ocoIsInUse()); + } + + private boolean ocoIsInUse() + { + return sortedList.stream() + .collect(Collectors.groupingBy(OpenOfferListItem::getOcoGroupAsString, Collectors.counting())) + .values().stream().anyMatch(i -> i > 1); } @Override @@ -329,7 +357,7 @@ private void updateSelectToggleButtonState() { } private void onWidthChange(double width) { - triggerPriceColumn.setVisible(width > 1200); + triggerPriceColumn.setVisible(width > 1300); } private void onDeactivateOpenOffer(OpenOffer openOffer) { @@ -359,32 +387,37 @@ private void onActivateOpenOffer(OpenOffer openOffer) { private void onRemoveOpenOffer(OpenOfferListItem item) { OpenOffer openOffer = item.getOpenOffer(); if (model.isBootstrappedOrShowPopup()) { - String key = (openOffer.getOffer().isBsqSwapOffer() ? "RemoveBsqSwapWarning" : "RemoveOfferWarning"); - if (DontShowAgainLookup.showAgain(key)) { - String message = openOffer.getOffer().isBsqSwapOffer() ? - Res.get("popup.warning.removeNoFeeOffer") : - Res.get("popup.warning.removeOffer", item.getMakerFeeAsString()); - new Popup().warning(message) - .actionButtonText(Res.get("shared.removeOffer")) - .onAction(() -> doRemoveOpenOffer(openOffer)) - .closeButtonText(Res.get("shared.dontRemoveOffer")) - .dontShowAgainId(key) - .show(); - } else { + if (openOfferManager.safeRemovalOfOcoClone(openOffer)) { doRemoveOpenOffer(openOffer); + } else { + String key = (openOffer.getOffer().isBsqSwapOffer() ? "RemoveBsqSwapWarning" : "RemoveOfferWarning"); + if (DontShowAgainLookup.showAgain(key)) { + String message = openOffer.getOffer().isBsqSwapOffer() ? + Res.get("popup.warning.removeNoFeeOffer") : + Res.get("popup.warning.removeOffer", item.getMakerFeeAsString()); + new Popup().warning(message) + .actionButtonText(Res.get("shared.removeOffer")) + .onAction(() -> doRemoveOpenOffer(openOffer)) + .closeButtonText(Res.get("shared.dontRemoveOffer")) + .dontShowAgainId(key) + .show(); + } else { + doRemoveOpenOffer(openOffer); + } } updateSelectToggleButtonState(); } } private void doRemoveOpenOffer(OpenOffer openOffer) { + boolean isSafeRemovalOfOcoClone = openOfferManager.safeRemovalOfOcoClone(openOffer); model.onRemoveOpenOffer(openOffer, () -> { log.debug("Remove offer was successful"); tableView.refresh(); - if (openOffer.getOffer().isBsqSwapOffer()) { + if (openOffer.getOffer().isBsqSwapOffer() || isSafeRemovalOfOcoClone) { return; // nothing to withdraw when Bsq swap is canceled (issue #5956) } String key = "WithdrawFundsAfterRemoveOfferInfo"; @@ -408,9 +441,68 @@ private void onEditOpenOffer(OpenOffer openOffer) { } } - private void onDuplicateOffer(Offer offer) { + private void onDuplicateOffer(OpenOfferListItem item) { try { - PortfolioUtil.duplicateOffer(navigation, offer.getOfferPayloadBase()); + PortfolioUtil.duplicateOffer(navigation, item.getOffer().getOfferPayloadBase()); + } catch (NullPointerException e) { + log.warn("Unable to get offerPayload - {}", e.toString()); + } + } + + private void onDuplicateOfferOco(OpenOfferListItem item, int numDuplicates) { + try { + for (int i=0; i< numDuplicates; i++) { + OfferPayload original = item.getOffer().getOfferPayload().orElseThrow(); + log.info("Duplicating offer as OCO: {}", original.getId()); + String newOfferId = getRandomOfferId(); + OfferPayload offerPayload = new OfferPayload(newOfferId, + new Date().getTime(), + original.getOwnerNodeAddress(), + original.getPubKeyRing(), + original.getDirection(), + original.getPrice(), + original.getMarketPriceMargin(), + original.isUseMarketBasedPrice(), + original.getAmount(), + original.getMinAmount(), + original.getBaseCurrencyCode(), + original.getCounterCurrencyCode(), + original.getArbitratorNodeAddresses(), + original.getMediatorNodeAddresses(), + original.getPaymentMethodId(), + original.getMakerPaymentAccountId(), + original.getOfferFeePaymentTxId(), + original.getCountryCode(), + original.getAcceptedCountryCodes(), + original.getBankId(), + original.getAcceptedBankIds(), + original.getVersionNr(), + original.getBlockHeightAtOfferCreation(), + original.getTxFee(), + original.getMakerFee(), + original.isCurrencyForMakerFeeBtc(), + original.getBuyerSecurityDeposit(), + original.getSellerSecurityDeposit(), + original.getMaxTradeLimit(), + original.getMaxTradePeriod(), + original.isUseAutoClose(), + original.isUseReOpenAfterAutoClose(), + original.getLowerClosePrice(), + original.getUpperClosePrice(), + original.isPrivateOffer(), + original.getHashOfChallenge(), + original.getExtraDataMap(), + original.getProtocolVersion()); + Offer expandedOffer = new Offer(offerPayload); + openOfferManager.placeOffer(expandedOffer, + 0, + false, + true, + 0, + transaction -> { + }, + log::error); + } } catch (NullPointerException e) { log.warn("Unable to get offerPayload - {}", e.toString()); } @@ -581,6 +673,36 @@ public void updateItem(final OpenOfferListItem item, boolean empty) { }); } + private void setGroupColumnCellFactory() { + groupColumn.setCellValueFactory((offerListItem) -> new ReadOnlyObjectWrapper<>(offerListItem.getValue())); + groupColumn.setCellFactory( + new Callback<>() { + @Override + public TableCell call( + TableColumn column) { + return new TableCell<>() { + @Override + public void updateItem(final OpenOfferListItem item, boolean empty) { + super.updateItem(item, empty); + getStyleClass().removeAll("offer-disabled"); + if (item != null) { + if (item.isNotPublished()) getStyleClass().add("offer-disabled"); + Label label = new AutoTooltipLabel(item.getOcoGroupAsString()); + if (openOfferManager.isSpam(item.getOpenOffer())) { + Text icon = getRegularIconForLabel(MaterialDesignIcon.EYE_OFF, label, "opaque-icon"); + label.setContentDisplay(ContentDisplay.RIGHT); + Tooltip.install(icon, new Tooltip("Change ccy or payment method to enable offer.")); + } + setGraphic(label); + } else { + setGraphic(null); + } + } + }; + } + }); + } + private void setVolumeColumnCellFactory() { volumeColumn.setCellValueFactory((offer) -> new ReadOnlyObjectWrapper<>(offer.getValue())); volumeColumn.setCellFactory( @@ -785,7 +907,7 @@ public void updateItem(final OpenOfferListItem item, boolean empty) { button.setTooltip(new Tooltip(Res.get("shared.duplicateOffer"))); setGraphic(button); } - button.setOnAction(event -> onDuplicateOffer(item.getOffer())); + button.setOnAction(event -> onDuplicateOffer(item)); } else { setGraphic(null); if (button != null) { From 1cc42aef771f1e226202157f6e472982d24d309a Mon Sep 17 00:00:00 2001 From: jmacxx <47253594+jmacxx@users.noreply.github.com> Date: Mon, 27 Mar 2023 09:55:23 -0500 Subject: [PATCH 02/31] Bug fix. --- .../core/offer/placeoffer/bisq_v1/tasks/CloneMakerFeeOco.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/main/java/bisq/core/offer/placeoffer/bisq_v1/tasks/CloneMakerFeeOco.java b/core/src/main/java/bisq/core/offer/placeoffer/bisq_v1/tasks/CloneMakerFeeOco.java index dc68b4a7628..320ae75b990 100644 --- a/core/src/main/java/bisq/core/offer/placeoffer/bisq_v1/tasks/CloneMakerFeeOco.java +++ b/core/src/main/java/bisq/core/offer/placeoffer/bisq_v1/tasks/CloneMakerFeeOco.java @@ -63,7 +63,7 @@ protected void run() { // AddressEntry and TxId are not linked, so do a reverse lookup private Optional getTxIdFromAddress(BtcWalletService walletService, Address address) { - List txns = walletService.getRecentTransactions(10, false); + List txns = walletService.getRecentTransactions(0, false); for (Transaction txn : txns) { for (TransactionOutput output : txn.getOutputs()) { if (walletService.isTransactionOutputMine(output) && WalletService.isOutputScriptConvertibleToAddress(output)) { From 27dcec00e9b04e7755869b62822cf6aba3b3ecfb Mon Sep 17 00:00:00 2001 From: jmacxx <47253594+jmacxx@users.noreply.github.com> Date: Sun, 2 Apr 2023 18:30:44 -0500 Subject: [PATCH 03/31] Code review changes. --- .../bisq/core/btc/model/AddressEntryList.java | 20 ++++--- .../core/btc/wallet/BtcWalletService.java | 17 ++++-- .../bisq/core/offer/OpenOfferManager.java | 55 +++++++++---------- .../placeoffer/bisq_v1/PlaceOfferModel.java | 6 +- .../bisq_v1/PlaceOfferProtocol.java | 2 +- .../bisq_v1/tasks/CloneMakerFeeOco.java | 2 +- .../resources/i18n/displayStrings.properties | 3 + .../openoffer/OpenOfferListItem.java | 10 +++- .../portfolio/openoffer/OpenOffersView.java | 29 +++++----- 9 files changed, 80 insertions(+), 64 deletions(-) diff --git a/core/src/main/java/bisq/core/btc/model/AddressEntryList.java b/core/src/main/java/bisq/core/btc/model/AddressEntryList.java index a1fc44d2e0e..8b3c95b0e34 100644 --- a/core/src/main/java/bisq/core/btc/model/AddressEntryList.java +++ b/core/src/main/java/bisq/core/btc/model/AddressEntryList.java @@ -209,21 +209,25 @@ public void swapToAvailable(AddressEntry addressEntry) { } log.info("swapToAvailable addressEntry to swap={}", addressEntry); - boolean setChangedByRemove = entrySet.remove(addressEntry); - - // check if the ADDRESS still has any existing entries, only if not do the add to available. - boolean entryWithSameContextAlreadyExist = entrySet.stream().anyMatch(e -> { + if (entrySet.remove(addressEntry)) { + requestPersistence(); + } + // check if the address still has any existing entries, which would be OCO offers sharing the UTXO + boolean entryWithSameContextStillExists = entrySet.stream().anyMatch(e -> { if (addressEntry.getAddressString() != null) { return addressEntry.getAddressString().equals(e.getAddressString()) && - addressEntry.getContext() == addressEntry.getContext(); + addressEntry.getContext() == e.getContext(); } return false; }); - boolean setChangedByAdd = !entryWithSameContextAlreadyExist && entrySet.add( + if (entryWithSameContextStillExists) { + return; + } + // no other uses of the address context remain, so make it available + if (entrySet.add( new AddressEntry(addressEntry.getKeyPair(), AddressEntry.Context.AVAILABLE, - addressEntry.isSegwit())); - if (setChangedByRemove || setChangedByAdd) { + addressEntry.isSegwit()))) { requestPersistence(); } } diff --git a/core/src/main/java/bisq/core/btc/wallet/BtcWalletService.java b/core/src/main/java/bisq/core/btc/wallet/BtcWalletService.java index c3498f2a7e6..32c9bd71878 100644 --- a/core/src/main/java/bisq/core/btc/wallet/BtcWalletService.java +++ b/core/src/main/java/bisq/core/btc/wallet/BtcWalletService.java @@ -630,10 +630,19 @@ public Optional getAddressEntry(String offerId, .findAny(); } - public AddressEntry createAddressEntryForOcoOffer(AddressEntry orgAddressEntry, String offerId) { - AddressEntry newEntry = new AddressEntry(orgAddressEntry.getKeyPair(), orgAddressEntry.getContext(), offerId, true); - addressEntryList.addAddressEntry(newEntry); - return newEntry; + // when a new offer needs to share the reserved amount info from parent offer's address entry + public AddressEntry getOrCreateAddressEntry(AddressEntry orgAddressEntry, String offerId) { + Optional addressEntry = getAddressEntryListAsImmutableList().stream() + .filter(e -> offerId.equals(e.getOfferId())) + .filter(e -> orgAddressEntry.getContext() == e.getContext()) + .findAny(); + if (addressEntry.isPresent()) { + return addressEntry.get(); + } else { + AddressEntry newEntry = new AddressEntry(orgAddressEntry.getKeyPair(), orgAddressEntry.getContext(), offerId, true); + addressEntryList.addAddressEntry(newEntry); + return newEntry; + } } public AddressEntry getOrCreateAddressEntry(String offerId, AddressEntry.Context context) { diff --git a/core/src/main/java/bisq/core/offer/OpenOfferManager.java b/core/src/main/java/bisq/core/offer/OpenOfferManager.java index cca99295020..3c9748e78e0 100644 --- a/core/src/main/java/bisq/core/offer/OpenOfferManager.java +++ b/core/src/main/java/bisq/core/offer/OpenOfferManager.java @@ -381,7 +381,7 @@ public void onAwakeFromStandby() { public void placeOffer(Offer offer, double buyerSecurityDeposit, boolean useSavingsWallet, - boolean useOco, + boolean useBatchOfferOco, long triggerPrice, TransactionResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) { @@ -399,12 +399,14 @@ public void placeOffer(Offer offer, buyerSecurityDeposit, createOfferService.getSellerSecurityDepositAsDouble(buyerSecurityDeposit)); - offer.setPriceFeedService(priceFeedService); + if (useBatchOfferOco) { + offer.setPriceFeedService(priceFeedService); + } PlaceOfferModel model = new PlaceOfferModel(offer, reservedFundsForOffer, useSavingsWallet, - useOco, + useBatchOfferOco, btcWalletService, tradeWalletService, bsqWalletService, @@ -419,7 +421,9 @@ public void placeOffer(Offer offer, model, transaction -> { OpenOffer openOffer = new OpenOffer(offer, triggerPrice); - openOffer.setState(useOco ? OpenOffer.State.DEACTIVATED : OpenOffer.State.AVAILABLE); + if (useBatchOfferOco) { + openOffer.setState(OpenOffer.State.DEACTIVATED); + } addOpenOfferToList(openOffer); if (!stopped) { startPeriodicRepublishOffersTimer(); @@ -476,7 +480,8 @@ public void activateOpenOffer(OpenOffer openOffer, return; } - if (isSpam(openOffer)) { + if (!canBeEnabled(openOffer.getOffer())) { + log.info("{} cannot be enabled, as it has duplicate characteristics with another open offer", openOffer.getShortId()); return; } @@ -624,7 +629,7 @@ public void closeOpenOffer(Offer offer) { getOpenOffersByMakerFeeTxId(offer.getOfferFeePaymentTxId()).forEach(openOffer -> { removeOpenOfferFromList(openOffer); openOffer.setState(OpenOffer.State.CLOSED); - if (!offer.getId().equalsIgnoreCase(openOffer.getId())) { + if (!offer.getId().equals(openOffer.getId())) { btcWalletService.resetAddressEntriesForOpenOffer(openOffer.getId()); // cleanup OCO clone } offerBookService.removeOffer(openOffer.getOffer().getOfferPayloadBase(), @@ -657,8 +662,10 @@ public Optional getOpenOfferById(String offerId) { } public List getOpenOffersByMakerFeeTxId(String makerFeeTxId) { - String safeSearch = makerFeeTxId == null ? "" : makerFeeTxId; - return openOffers.stream().filter(e -> !e.getOffer().isBsqSwapOffer() && e.getOffer().getOfferFeePaymentTxId().equals(safeSearch)).collect(Collectors.toList()); + return openOffers.stream() + .filter(e -> !e.getOffer().isBsqSwapOffer() && e.getOffer().getOfferFeePaymentTxId() + .equals(makerFeeTxId == null ? "" : makerFeeTxId)) + .collect(Collectors.toList()); } @@ -1178,26 +1185,16 @@ private boolean isBsqSwapOfferLackingFunds(OpenOffer openOffer) { openOffer.isBsqSwapOfferHasMissingFunds(); } - public boolean isSpam(OpenOffer newOffer) { - // an offer is spam if the user has another open offer on the same ccy, payment method, and reserved UTXO - long matchingOffers = openOffers.stream() - .filter((openOffer -> !openOffer.getOffer().isBsqSwapOffer())) - .filter(openOffer -> !openOffer.isDeactivated()) - .filter(openOffer -> !openOffer.getShortId().equalsIgnoreCase(newOffer.getShortId())) - .filter(openOffer1 -> { - Offer newOffer1 = newOffer.getOffer(); - Offer offer1 = openOffer1.getOffer(); - return - offer1.getOfferFeePaymentTxId().equalsIgnoreCase(newOffer1.getOfferFeePaymentTxId()) && - offer1.getPaymentMethodId().equalsIgnoreCase(newOffer1.getPaymentMethodId()) && - offer1.getCounterCurrencyCode().equalsIgnoreCase(newOffer1.getCounterCurrencyCode()) && - offer1.getBaseCurrencyCode().equalsIgnoreCase(newOffer1.getBaseCurrencyCode()); - }) - .count(); - if (matchingOffers > 0) { - log.info("{} is considered spam", newOffer.getShortId()); - } - return matchingOffers > 0; + public boolean canBeEnabled(Offer newOffer) { + // does the user have another open offer on the same currency, payment method, and reserved UTXO? + return openOffers.stream().noneMatch(e -> + !e.getOffer().isBsqSwapOffer() && + !e.isDeactivated() && + !e.getId().equals(newOffer.getId()) && + e.getOffer().getOfferFeePaymentTxId().equals(newOffer.getOfferFeePaymentTxId()) && + e.getOffer().getPaymentMethodId().equalsIgnoreCase(newOffer.getPaymentMethodId()) && + e.getOffer().getCounterCurrencyCode().equalsIgnoreCase(newOffer.getCounterCurrencyCode()) && + e.getOffer().getBaseCurrencyCode().equalsIgnoreCase(newOffer.getBaseCurrencyCode())); } public boolean safeRemovalOfOcoClone(OpenOffer openOffer) { @@ -1205,6 +1202,6 @@ public boolean safeRemovalOfOcoClone(OpenOffer openOffer) { } private boolean preventedFromPublishing(OpenOffer openOffer) { - return openOffer.isDeactivated() || openOffer.isBsqSwapOfferHasMissingFunds() || isSpam(openOffer); + return openOffer.isDeactivated() || openOffer.isBsqSwapOfferHasMissingFunds() || !canBeEnabled(openOffer.getOffer()); } } diff --git a/core/src/main/java/bisq/core/offer/placeoffer/bisq_v1/PlaceOfferModel.java b/core/src/main/java/bisq/core/offer/placeoffer/bisq_v1/PlaceOfferModel.java index 8355d472bbb..a57fde42e67 100644 --- a/core/src/main/java/bisq/core/offer/placeoffer/bisq_v1/PlaceOfferModel.java +++ b/core/src/main/java/bisq/core/offer/placeoffer/bisq_v1/PlaceOfferModel.java @@ -45,7 +45,7 @@ public class PlaceOfferModel implements Model { private final Offer offer; private final Coin reservedFundsForOffer; private final boolean useSavingsWallet; - private final boolean useOco; + private final boolean useBatchOfferOco; private final BtcWalletService walletService; private final TradeWalletService tradeWalletService; private final BsqWalletService bsqWalletService; @@ -67,7 +67,7 @@ public class PlaceOfferModel implements Model { public PlaceOfferModel(Offer offer, Coin reservedFundsForOffer, boolean useSavingsWallet, - boolean useOco, + boolean useBatchOfferOco, BtcWalletService walletService, TradeWalletService tradeWalletService, BsqWalletService bsqWalletService, @@ -81,7 +81,7 @@ public PlaceOfferModel(Offer offer, this.offer = offer; this.reservedFundsForOffer = reservedFundsForOffer; this.useSavingsWallet = useSavingsWallet; - this.useOco = useOco; + this.useBatchOfferOco = useBatchOfferOco; this.walletService = walletService; this.tradeWalletService = tradeWalletService; this.bsqWalletService = bsqWalletService; diff --git a/core/src/main/java/bisq/core/offer/placeoffer/bisq_v1/PlaceOfferProtocol.java b/core/src/main/java/bisq/core/offer/placeoffer/bisq_v1/PlaceOfferProtocol.java index 05f6ccbdc96..a30a9c8e7eb 100644 --- a/core/src/main/java/bisq/core/offer/placeoffer/bisq_v1/PlaceOfferProtocol.java +++ b/core/src/main/java/bisq/core/offer/placeoffer/bisq_v1/PlaceOfferProtocol.java @@ -78,7 +78,7 @@ public void placeOffer() { } ); - if (model.isUseOco()) { + if (model.isUseBatchOfferOco()) { taskRunner.addTasks( ValidateOffer.class, CloneMakerFeeOco.class diff --git a/core/src/main/java/bisq/core/offer/placeoffer/bisq_v1/tasks/CloneMakerFeeOco.java b/core/src/main/java/bisq/core/offer/placeoffer/bisq_v1/tasks/CloneMakerFeeOco.java index 320ae75b990..175adbed7e5 100644 --- a/core/src/main/java/bisq/core/offer/placeoffer/bisq_v1/tasks/CloneMakerFeeOco.java +++ b/core/src/main/java/bisq/core/offer/placeoffer/bisq_v1/tasks/CloneMakerFeeOco.java @@ -49,7 +49,7 @@ protected void run() { for (AddressEntry potentialOcoSource : walletService.getAddressEntries(AddressEntry.Context.RESERVED_FOR_TRADE)) { getTxIdFromAddress(walletService, potentialOcoSource.getAddress()).ifPresent(txId -> { if (txId.equalsIgnoreCase(newOcoOffer.getOfferFeePaymentTxId())) { - walletService.createAddressEntryForOcoOffer(potentialOcoSource, newOcoOffer.getId()); + walletService.getOrCreateAddressEntry(potentialOcoSource, newOcoOffer.getId()); newOcoOffer.setState(Offer.State.OFFER_FEE_PAID); complete(); } diff --git a/core/src/main/resources/i18n/displayStrings.properties b/core/src/main/resources/i18n/displayStrings.properties index ccd4b2800f7..fed8371eb08 100644 --- a/core/src/main/resources/i18n/displayStrings.properties +++ b/core/src/main/resources/i18n/displayStrings.properties @@ -77,6 +77,7 @@ shared.deviation=Deviation shared.paymentMethod=Payment method shared.tradeCurrency=Trade currency shared.offerType=Offer type +shared.group=Group shared.details=Details shared.address=Address shared.balanceWithCur=Balance in {0} @@ -100,6 +101,7 @@ shared.removeOffer=Remove offer shared.dontRemoveOffer=Don't remove offer shared.editOffer=Edit offer shared.duplicateOffer=Duplicate offer +shared.duplicateOcoOffer=Duplicate as OCO shared.openLargeQRWindow=Open large QR code window shared.chooseTradingAccount=Choose trading account shared.faq=Visit FAQ page @@ -364,6 +366,7 @@ offerbook.timeSinceSigning.tooltip.checkmark.buyBtc=buy BTC from a signed accoun offerbook.timeSinceSigning.tooltip.checkmark.wait=wait a minimum of {0} days offerbook.timeSinceSigning.tooltip.learnMore=Learn more offerbook.xmrAutoConf=Is auto-confirm enabled +offerbook.toEnableOffer=Change ccy or payment method to enable offer. offerbook.timeSinceSigning.help=When you successfully complete a trade with a peer who has a signed payment account, your payment account is signed.\n\ {0} days later, the initial limit of {1} is lifted and your account can sign other peers'' payment accounts. diff --git a/desktop/src/main/java/bisq/desktop/main/portfolio/openoffer/OpenOfferListItem.java b/desktop/src/main/java/bisq/desktop/main/portfolio/openoffer/OpenOfferListItem.java index f9d48804f4a..480336f219b 100644 --- a/desktop/src/main/java/bisq/desktop/main/portfolio/openoffer/OpenOfferListItem.java +++ b/desktop/src/main/java/bisq/desktop/main/portfolio/openoffer/OpenOfferListItem.java @@ -134,12 +134,16 @@ public String getTriggerPriceAsString() { } } - public String getOcoGroupAsString() { + public String getOcoGroupForSorting() { Offer offer = getOffer(); if (offer.isBsqSwapOffer()) { - return ""; + return " "; } - return offer.getOfferFeePaymentTxId().substring(0, 4); + return offer.getOfferFeePaymentTxId(); + } + + public String getOcoGroupForDisplay() { + return getOcoGroupForSorting().substring(0, 4); } @Override diff --git a/desktop/src/main/java/bisq/desktop/main/portfolio/openoffer/OpenOffersView.java b/desktop/src/main/java/bisq/desktop/main/portfolio/openoffer/OpenOffersView.java index 15fcd4aa7ad..3d2555c1632 100644 --- a/desktop/src/main/java/bisq/desktop/main/portfolio/openoffer/OpenOffersView.java +++ b/desktop/src/main/java/bisq/desktop/main/portfolio/openoffer/OpenOffersView.java @@ -107,7 +107,7 @@ private enum ColumnNames { VOLUME(Res.get("shared.amountMinMax")), PAYMENT_METHOD(Res.get("shared.paymentMethod")), DIRECTION(Res.get("shared.offerType")), - GROUP("Group"), + GROUP(Res.get("shared.group")), STATUS(Res.get("shared.state")); private final String text; @@ -209,7 +209,7 @@ public void initialize() { deviationColumn.setComparator(Comparator.comparing(OpenOfferListItem::getPriceDeviationAsDouble, Comparator.nullsFirst(Comparator.naturalOrder()))); triggerPriceColumn.setComparator(Comparator.comparing(o -> o.getOpenOffer().getTriggerPrice(), Comparator.nullsFirst(Comparator.naturalOrder()))); - groupColumn.setComparator(Comparator.comparing(OpenOfferListItem::getOcoGroupAsString)); + groupColumn.setComparator(Comparator.comparing(OpenOfferListItem::getOcoGroupForSorting)); volumeColumn.setComparator(Comparator.comparing(o -> o.getOffer().getVolume(), Comparator.nullsFirst(Comparator.naturalOrder()))); dateColumn.setComparator(Comparator.comparing(o -> o.getOffer().getDate())); paymentMethodColumn.setComparator(Comparator.comparing(o -> Res.get(o.getOffer().getPaymentMethod().getId()))); @@ -223,9 +223,9 @@ public void initialize() { final ContextMenu rowMenu = new ContextMenu(); MenuItem duplicateItem = new MenuItem(Res.get("portfolio.context.offerLikeThis")); duplicateItem.setOnAction((event) -> onDuplicateOffer(row.getItem())); - MenuItem duplicateItemOco1 = new MenuItem("Duplicate as OCO"); + MenuItem duplicateItemOco1 = new MenuItem(Res.get("shared.duplicateOcoOffer")); duplicateItemOco1.setOnAction((event) -> onDuplicateOfferOco(row.getItem(), 1)); - MenuItem duplicateItemOco5 = new MenuItem("Duplicate as OCO x5"); + MenuItem duplicateItemOco5 = new MenuItem(Res.get("shared.duplicateOcoOffer") + " x5"); duplicateItemOco5.setOnAction((event) -> onDuplicateOfferOco(row.getItem(), 5)); rowMenu.getItems().add(duplicateItem); rowMenu.getItems().add(duplicateItemOco1); @@ -252,6 +252,7 @@ public void initialize() { c.next(); if (c.wasAdded() || c.wasRemoved()) { updateNumberOfOffers(); + updateGroupColumn(); } }; } @@ -266,7 +267,7 @@ protected void activate() { filterBox.initializeWithCallback(filteredList, tableView, this::updateNumberOfOffers); filterBox.activate(); - + updateGroupColumn(); updateSelectToggleButtonState(); selectToggleButton.setOnAction(event -> { @@ -300,7 +301,7 @@ protected void activate() { columns[ColumnNames.VOLUME.ordinal()] = item.getVolumeAsString(); columns[ColumnNames.PAYMENT_METHOD.ordinal()] = item.getPaymentMethodAsString(); columns[ColumnNames.DIRECTION.ordinal()] = item.getDirectionLabel(); - columns[ColumnNames.GROUP.ordinal()] = item.getOcoGroupAsString(); + columns[ColumnNames.GROUP.ordinal()] = item.getOcoGroupForDisplay(); columns[ColumnNames.STATUS.ordinal()] = String.valueOf(!item.getOpenOffer().isDeactivated()); return columns; }; @@ -319,14 +320,12 @@ protected void activate() { private void updateNumberOfOffers() { numItems.setText(Res.get("shared.numItemsLabel", sortedList.size())); - groupColumn.setVisible(ocoIsInUse()); } - private boolean ocoIsInUse() - { - return sortedList.stream() - .collect(Collectors.groupingBy(OpenOfferListItem::getOcoGroupAsString, Collectors.counting())) - .values().stream().anyMatch(i -> i > 1); + private void updateGroupColumn() { + groupColumn.setVisible(sortedList.stream() + .collect(Collectors.groupingBy(OpenOfferListItem::getOcoGroupForSorting, Collectors.counting())) + .values().stream().anyMatch(i -> i > 1)); } @Override @@ -687,11 +686,11 @@ public void updateItem(final OpenOfferListItem item, boolean empty) { getStyleClass().removeAll("offer-disabled"); if (item != null) { if (item.isNotPublished()) getStyleClass().add("offer-disabled"); - Label label = new AutoTooltipLabel(item.getOcoGroupAsString()); - if (openOfferManager.isSpam(item.getOpenOffer())) { + Label label = new AutoTooltipLabel(item.getOcoGroupForDisplay()); + if (!openOfferManager.canBeEnabled(item.getOpenOffer().getOffer())) { Text icon = getRegularIconForLabel(MaterialDesignIcon.EYE_OFF, label, "opaque-icon"); label.setContentDisplay(ContentDisplay.RIGHT); - Tooltip.install(icon, new Tooltip("Change ccy or payment method to enable offer.")); + Tooltip.install(icon, new Tooltip(Res.get("offerbook.toEnableOffer"))); } setGraphic(label); } else { From e66517fb8af6e3c67b0f2de0a6f5a4a9fe195501 Mon Sep 17 00:00:00 2001 From: jmacxx <47253594+jmacxx@users.noreply.github.com> Date: Fri, 7 Apr 2023 09:31:30 -0500 Subject: [PATCH 04/31] Code review changes. Pick a more user friendly name instead of OCO. Clean up code. --- .../resources/i18n/displayStrings.properties | 2 +- .../portfolio/openoffer/OpenOffersView.java | 21 ++++++++----------- 2 files changed, 10 insertions(+), 13 deletions(-) diff --git a/core/src/main/resources/i18n/displayStrings.properties b/core/src/main/resources/i18n/displayStrings.properties index fed8371eb08..a914172bf01 100644 --- a/core/src/main/resources/i18n/displayStrings.properties +++ b/core/src/main/resources/i18n/displayStrings.properties @@ -101,7 +101,7 @@ shared.removeOffer=Remove offer shared.dontRemoveOffer=Don't remove offer shared.editOffer=Edit offer shared.duplicateOffer=Duplicate offer -shared.duplicateOcoOffer=Duplicate as OCO +shared.cloneGroupedOfferOco=Clone as Grouped Offer shared.openLargeQRWindow=Open large QR code window shared.chooseTradingAccount=Choose trading account shared.faq=Visit FAQ page diff --git a/desktop/src/main/java/bisq/desktop/main/portfolio/openoffer/OpenOffersView.java b/desktop/src/main/java/bisq/desktop/main/portfolio/openoffer/OpenOffersView.java index 3d2555c1632..1b800694639 100644 --- a/desktop/src/main/java/bisq/desktop/main/portfolio/openoffer/OpenOffersView.java +++ b/desktop/src/main/java/bisq/desktop/main/portfolio/openoffer/OpenOffersView.java @@ -223,13 +223,13 @@ public void initialize() { final ContextMenu rowMenu = new ContextMenu(); MenuItem duplicateItem = new MenuItem(Res.get("portfolio.context.offerLikeThis")); duplicateItem.setOnAction((event) -> onDuplicateOffer(row.getItem())); - MenuItem duplicateItemOco1 = new MenuItem(Res.get("shared.duplicateOcoOffer")); - duplicateItemOco1.setOnAction((event) -> onDuplicateOfferOco(row.getItem(), 1)); - MenuItem duplicateItemOco5 = new MenuItem(Res.get("shared.duplicateOcoOffer") + " x5"); - duplicateItemOco5.setOnAction((event) -> onDuplicateOfferOco(row.getItem(), 5)); + MenuItem cloneGroupedOfferOco1 = new MenuItem(Res.get("shared.cloneGroupedOfferOco")); + cloneGroupedOfferOco1.setOnAction((event) -> onDuplicateOfferOco(row.getItem(), 1)); + MenuItem cloneGroupedOfferOco5 = new MenuItem(Res.get("shared.cloneGroupedOfferOco") + " x5"); + cloneGroupedOfferOco5.setOnAction((event) -> onDuplicateOfferOco(row.getItem(), 5)); rowMenu.getItems().add(duplicateItem); - rowMenu.getItems().add(duplicateItemOco1); - rowMenu.getItems().add(duplicateItemOco5); + rowMenu.getItems().add(cloneGroupedOfferOco1); + rowMenu.getItems().add(cloneGroupedOfferOco5); row.contextMenuProperty().bind( Bindings.when(Bindings.isNotNull(row.itemProperty())) .then(rowMenu) @@ -449,9 +449,8 @@ private void onDuplicateOffer(OpenOfferListItem item) { } private void onDuplicateOfferOco(OpenOfferListItem item, int numDuplicates) { - try { - for (int i=0; i< numDuplicates; i++) { - OfferPayload original = item.getOffer().getOfferPayload().orElseThrow(); + for (int i=0; i< numDuplicates; i++) { + item.getOffer().getOfferPayload().ifPresent(original -> { log.info("Duplicating offer as OCO: {}", original.getId()); String newOfferId = getRandomOfferId(); OfferPayload offerPayload = new OfferPayload(newOfferId, @@ -501,9 +500,7 @@ private void onDuplicateOfferOco(OpenOfferListItem item, int numDuplicates) { transaction -> { }, log::error); - } - } catch (NullPointerException e) { - log.warn("Unable to get offerPayload - {}", e.toString()); + }); } } From acbff2b11af83de4e5439fe3e19b473454e6a1a4 Mon Sep 17 00:00:00 2001 From: jmacxx <47253594+jmacxx@users.noreply.github.com> Date: Tue, 2 May 2023 11:20:33 -0500 Subject: [PATCH 05/31] Use domain data as requested by reviewer. --- .../bisq/desktop/main/portfolio/openoffer/OpenOffersView.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/desktop/src/main/java/bisq/desktop/main/portfolio/openoffer/OpenOffersView.java b/desktop/src/main/java/bisq/desktop/main/portfolio/openoffer/OpenOffersView.java index 1b800694639..2480d3210cb 100644 --- a/desktop/src/main/java/bisq/desktop/main/portfolio/openoffer/OpenOffersView.java +++ b/desktop/src/main/java/bisq/desktop/main/portfolio/openoffer/OpenOffersView.java @@ -323,7 +323,7 @@ private void updateNumberOfOffers() { } private void updateGroupColumn() { - groupColumn.setVisible(sortedList.stream() + groupColumn.setVisible(model.dataModel.getList().stream() .collect(Collectors.groupingBy(OpenOfferListItem::getOcoGroupForSorting, Collectors.counting())) .values().stream().anyMatch(i -> i > 1)); } From 966b9d8448bcef9a36efece51873848de7ad70b3 Mon Sep 17 00:00:00 2001 From: HenrikJannsen Date: Thu, 4 May 2023 11:26:02 +0700 Subject: [PATCH 06/31] Add getAllRecentTransactions method. The numTransactions param in getRecentTransactions delivers all transactions if it is 0 but that is not intuitive. Passing Integer.MAX_VALUE makes more sense. Signed-off-by: HenrikJannsen --- .../src/main/java/bisq/core/btc/wallet/WalletService.java | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/core/src/main/java/bisq/core/btc/wallet/WalletService.java b/core/src/main/java/bisq/core/btc/wallet/WalletService.java index 2511dd4c681..abbcc1d21a3 100644 --- a/core/src/main/java/bisq/core/btc/wallet/WalletService.java +++ b/core/src/main/java/bisq/core/btc/wallet/WalletService.java @@ -461,8 +461,8 @@ public TransactionConfidence getConfidenceForAddressFromBlockHeight(Address addr .map(tx -> getTransactionConfidence(tx, address)) .filter(Objects::nonNull) .filter(con -> con.getConfidenceType() == TransactionConfidence.ConfidenceType.PENDING || - (con.getConfidenceType() == TransactionConfidence.ConfidenceType.BUILDING && - con.getAppearedAtChainHeight() > targetHeight)) + (con.getConfidenceType() == TransactionConfidence.ConfidenceType.BUILDING && + con.getAppearedAtChainHeight() > targetHeight)) .collect(Collectors.toList())); } return getMostRecentConfidence(transactionConfidenceList); @@ -751,6 +751,10 @@ public boolean isEncrypted() { return wallet.isEncrypted(); } + public List getAllRecentTransactions(boolean includeDead) { + return getRecentTransactions(Integer.MAX_VALUE, includeDead); + } + public List getRecentTransactions(int numTransactions, boolean includeDead) { // Returns a list ordered by tx.getUpdateTime() desc return wallet.getRecentTransactions(numTransactions, includeDead); From f6c31aa479264e41160f8c1f3ad8b1dede68df41 Mon Sep 17 00:00:00 2001 From: HenrikJannsen Date: Thu, 4 May 2023 13:56:29 +0700 Subject: [PATCH 07/31] Remove added code with entryWithSameContextStillExists as it is not needed. Multiple calls will call add on the hashset but as the entry is the same it will not trigger any change of the hashset. Signed-off-by: HenrikJannsen --- .../bisq/core/btc/model/AddressEntryList.java | 24 ++++--------------- 1 file changed, 5 insertions(+), 19 deletions(-) diff --git a/core/src/main/java/bisq/core/btc/model/AddressEntryList.java b/core/src/main/java/bisq/core/btc/model/AddressEntryList.java index 8b3c95b0e34..ccdc54412bd 100644 --- a/core/src/main/java/bisq/core/btc/model/AddressEntryList.java +++ b/core/src/main/java/bisq/core/btc/model/AddressEntryList.java @@ -149,7 +149,7 @@ public void onWalletReady(Wallet wallet) { wallet.getIssuedReceiveAddresses().stream() .filter(this::isAddressNotInEntries) .forEach(address -> { - DeterministicKey key = (DeterministicKey) wallet.findKeyFromAddress(address); + DeterministicKey key = (DeterministicKey) wallet.findKeyFromAddress(address); if (key != null) { // Address will be derived from key in getAddress method log.info("Create AddressEntry for IssuedReceiveAddress. address={}", address.toString()); @@ -209,25 +209,11 @@ public void swapToAvailable(AddressEntry addressEntry) { } log.info("swapToAvailable addressEntry to swap={}", addressEntry); - if (entrySet.remove(addressEntry)) { - requestPersistence(); - } - // check if the address still has any existing entries, which would be OCO offers sharing the UTXO - boolean entryWithSameContextStillExists = entrySet.stream().anyMatch(e -> { - if (addressEntry.getAddressString() != null) { - return addressEntry.getAddressString().equals(e.getAddressString()) && - addressEntry.getContext() == e.getContext(); - } - return false; - }); - if (entryWithSameContextStillExists) { - return; - } - // no other uses of the address context remain, so make it available - if (entrySet.add( - new AddressEntry(addressEntry.getKeyPair(), + boolean setChangedByRemove = entrySet.remove(addressEntry); + boolean setChangedByAdd = entrySet.add(new AddressEntry(addressEntry.getKeyPair(), AddressEntry.Context.AVAILABLE, - addressEntry.isSegwit()))) { + addressEntry.isSegwit())); + if (setChangedByRemove || setChangedByAdd) { requestPersistence(); } } From cadf2073cff4ca7c08e715d37d0e93bbe8747813 Mon Sep 17 00:00:00 2001 From: HenrikJannsen Date: Thu, 4 May 2023 15:24:07 +0700 Subject: [PATCH 08/31] Fix updateReservedBalance. btcWalletService.getAddressEntriesForOpenOffer() contains also OFFER_FUNDING entries. This version minimizes the change by mapping to address and use distinct to avoid duplicate entries to be summed up. Signed-off-by: HenrikJannsen --- core/src/main/java/bisq/core/btc/Balances.java | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/core/src/main/java/bisq/core/btc/Balances.java b/core/src/main/java/bisq/core/btc/Balances.java index cf372b37f35..73326c6ab1f 100644 --- a/core/src/main/java/bisq/core/btc/Balances.java +++ b/core/src/main/java/bisq/core/btc/Balances.java @@ -42,7 +42,6 @@ import javafx.collections.ListChangeListener; import java.util.Objects; -import java.util.stream.Collectors; import java.util.stream.Stream; import lombok.Getter; @@ -111,10 +110,12 @@ private void updateAvailableBalance() { } private void updateReservedBalance() { - long sum = btcWalletService.getAddressEntriesForOpenOffer().stream() - .collect(Collectors.toMap(AddressEntry::getAddress, p -> p, (p, q) -> p)) - .keySet() - .stream() + long sum = openOfferManager.getObservableList().stream() + .map(openOffer -> btcWalletService.getAddressEntry(openOffer.getId(), AddressEntry.Context.RESERVED_FOR_TRADE) + .orElse(null)) + .filter(Objects::nonNull) + .map(AddressEntry::getAddress) + .distinct() .mapToLong(address -> btcWalletService.getBalanceForAddress(address).value) .sum(); reservedBalance.set(Coin.valueOf(sum)); @@ -124,7 +125,7 @@ private void updateLockedBalance() { Stream lockedTrades = Stream.concat(closedTradableManager.getTradesStreamWithFundsLockedIn(), failedTradesManager.getTradesStreamWithFundsLockedIn()); lockedTrades = Stream.concat(lockedTrades, tradeManager.getTradesStreamWithFundsLockedIn()); long sum = lockedTrades.map(trade -> btcWalletService.getAddressEntry(trade.getId(), AddressEntry.Context.MULTI_SIG) - .orElse(null)) + .orElse(null)) .filter(Objects::nonNull) .mapToLong(AddressEntry::getCoinLockedInMultiSig) .sum(); From 992854c9b92ce27a62caa76a8ac646c7ff93972d Mon Sep 17 00:00:00 2001 From: HenrikJannsen Date: Thu, 4 May 2023 18:30:09 +0700 Subject: [PATCH 09/31] Various refactorings and changes Signed-off-by: HenrikJannsen --- .../src/main/java/bisq/core/btc/Balances.java | 3 +- .../core/btc/wallet/BtcWalletService.java | 18 ++- .../main/java/bisq/core/offer/OpenOffer.java | 4 + .../bisq/core/offer/OpenOfferManager.java | 100 +++++++------ .../placeoffer/bisq_v1/PlaceOfferModel.java | 6 +- .../bisq_v1/PlaceOfferProtocol.java | 6 +- ...> CloneAddressEntryForSharedMakerFee.java} | 43 +++--- .../resources/i18n/displayStrings.properties | 6 +- .../i18n/displayStrings_zh-hant.properties | 2 +- .../closedtrades/ClosedTradesView.java | 2 +- .../openoffer/OpenOfferListItem.java | 2 + .../portfolio/openoffer/OpenOffersView.java | 141 +++++++++--------- .../pendingtrades/PendingTradesView.java | 2 +- 13 files changed, 181 insertions(+), 154 deletions(-) rename core/src/main/java/bisq/core/offer/placeoffer/bisq_v1/tasks/{CloneMakerFeeOco.java => CloneAddressEntryForSharedMakerFee.java} (55%) diff --git a/core/src/main/java/bisq/core/btc/Balances.java b/core/src/main/java/bisq/core/btc/Balances.java index 73326c6ab1f..dcc9e311724 100644 --- a/core/src/main/java/bisq/core/btc/Balances.java +++ b/core/src/main/java/bisq/core/btc/Balances.java @@ -124,7 +124,8 @@ private void updateReservedBalance() { private void updateLockedBalance() { Stream lockedTrades = Stream.concat(closedTradableManager.getTradesStreamWithFundsLockedIn(), failedTradesManager.getTradesStreamWithFundsLockedIn()); lockedTrades = Stream.concat(lockedTrades, tradeManager.getTradesStreamWithFundsLockedIn()); - long sum = lockedTrades.map(trade -> btcWalletService.getAddressEntry(trade.getId(), AddressEntry.Context.MULTI_SIG) + long sum = lockedTrades.map(trade -> btcWalletService.getAddressEntry(trade.getId(), + AddressEntry.Context.MULTI_SIG) .orElse(null)) .filter(Objects::nonNull) .mapToLong(AddressEntry::getCoinLockedInMultiSig) diff --git a/core/src/main/java/bisq/core/btc/wallet/BtcWalletService.java b/core/src/main/java/bisq/core/btc/wallet/BtcWalletService.java index 32c9bd71878..a3f67c039cc 100644 --- a/core/src/main/java/bisq/core/btc/wallet/BtcWalletService.java +++ b/core/src/main/java/bisq/core/btc/wallet/BtcWalletService.java @@ -630,18 +630,22 @@ public Optional getAddressEntry(String offerId, .findAny(); } - // when a new offer needs to share the reserved amount info from parent offer's address entry - public AddressEntry getOrCreateAddressEntry(AddressEntry orgAddressEntry, String offerId) { + // For offers with shared maker fee we create a new address entry with from the source entry + // and set the new offerId. + public AddressEntry getOrCloneAddressEntryWithOfferId(AddressEntry sourceAddressEntry, String offerId) { Optional addressEntry = getAddressEntryListAsImmutableList().stream() - .filter(e -> offerId.equals(e.getOfferId())) - .filter(e -> orgAddressEntry.getContext() == e.getContext()) + .filter(entry -> offerId.equals(entry.getOfferId())) + .filter(entry -> sourceAddressEntry.getContext() == entry.getContext()) .findAny(); if (addressEntry.isPresent()) { return addressEntry.get(); } else { - AddressEntry newEntry = new AddressEntry(orgAddressEntry.getKeyPair(), orgAddressEntry.getContext(), offerId, true); - addressEntryList.addAddressEntry(newEntry); - return newEntry; + AddressEntry cloneWithNewOfferId = new AddressEntry(sourceAddressEntry.getKeyPair(), + sourceAddressEntry.getContext(), + offerId, + sourceAddressEntry.isSegwit()); + addressEntryList.addAddressEntry(cloneWithNewOfferId); + return cloneWithNewOfferId; } } diff --git a/core/src/main/java/bisq/core/offer/OpenOffer.java b/core/src/main/java/bisq/core/offer/OpenOffer.java index 088a3e8f042..5dc8096b230 100644 --- a/core/src/main/java/bisq/core/offer/OpenOffer.java +++ b/core/src/main/java/bisq/core/offer/OpenOffer.java @@ -187,6 +187,10 @@ public boolean isDeactivated() { return state == State.DEACTIVATED; } + public boolean isActivated() { + return !isDeactivated(); + } + public boolean isCanceled() { return state == State.CANCELED; } diff --git a/core/src/main/java/bisq/core/offer/OpenOfferManager.java b/core/src/main/java/bisq/core/offer/OpenOfferManager.java index 3c9748e78e0..e2750cffc5c 100644 --- a/core/src/main/java/bisq/core/offer/OpenOfferManager.java +++ b/core/src/main/java/bisq/core/offer/OpenOfferManager.java @@ -381,16 +381,16 @@ public void onAwakeFromStandby() { public void placeOffer(Offer offer, double buyerSecurityDeposit, boolean useSavingsWallet, - boolean useBatchOfferOco, + boolean isSharedMakerFee, long triggerPrice, TransactionResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) { checkNotNull(offer.getMakerFee(), "makerFee must not be null"); checkArgument(!offer.isBsqSwapOffer()); - int numClones = getOpenOffersByMakerFeeTxId(offer.getOfferFeePaymentTxId()).size(); - if (numClones > 10) { - errorMessageHandler.handleErrorMessage("PlaceOffer prevented because cloned OCO offers count is " + numClones); + int numClones = getOpenOffersByMakerFee(offer.getOfferFeePaymentTxId()).size(); + if (numClones >= 10) { + errorMessageHandler.handleErrorMessage("Cannot create offer because maximum number of 10 cloned offers with shared maker fee is reached."); return; } @@ -399,14 +399,10 @@ public void placeOffer(Offer offer, buyerSecurityDeposit, createOfferService.getSellerSecurityDepositAsDouble(buyerSecurityDeposit)); - if (useBatchOfferOco) { - offer.setPriceFeedService(priceFeedService); - } - PlaceOfferModel model = new PlaceOfferModel(offer, reservedFundsForOffer, useSavingsWallet, - useBatchOfferOco, + isSharedMakerFee, btcWalletService, tradeWalletService, bsqWalletService, @@ -421,7 +417,7 @@ public void placeOffer(Offer offer, model, transaction -> { OpenOffer openOffer = new OpenOffer(offer, triggerPrice); - if (useBatchOfferOco) { + if (isSharedMakerFee) { openOffer.setState(OpenOffer.State.DEACTIVATED); } addOpenOfferToList(openOffer); @@ -471,6 +467,12 @@ public void activateOpenOffer(OpenOffer openOffer, return; } + if (cannotActivateOffer(openOffer.getOffer())) { + errorMessageHandler.handleErrorMessage("This cloned offer with shared maker fee cannot be activated because it uses the same payment method " + + "and currency as another active offer."); + return; + } + // If there is not enough funds for a BsqSwapOffer we do not publish the offer, but still apply the state change. // Once the wallet gets funded the offer gets published automatically. if (isBsqSwapOfferLackingFunds(openOffer)) { @@ -480,11 +482,6 @@ public void activateOpenOffer(OpenOffer openOffer, return; } - if (!canBeEnabled(openOffer.getOffer())) { - log.info("{} cannot be enabled, as it has duplicate characteristics with another open offer", openOffer.getShortId()); - return; - } - Offer offer = openOffer.getOffer(); offerBookService.activateOffer(offer, () -> { @@ -606,8 +603,14 @@ private void onRemoved(OpenOffer openOffer, ResultHandler resultHandler, Offer o openOffer.setState(OpenOffer.State.CANCELED); removeOpenOfferFromList(openOffer); - if (!openOffer.getOffer().isBsqSwapOffer() && !safeRemovalOfOcoClone(openOffer)) { - closedTradableManager.add(openOffer); + if (!openOffer.getOffer().isBsqSwapOffer()) { + // In case of an offer which has its maker fee shared with other offers, we do not add the openOffer + // to history. Only when the last offer with that maker fee txId got removed we add it. + // Only canceled offers which have lost maker fees are shown in history. + // For that reason we also do not add BSQ offers. + if (getOpenOffersByMakerFee(offer.getOfferFeePaymentTxId()).isEmpty()) { + closedTradableManager.add(openOffer); + } btcWalletService.resetAddressEntriesForOpenOffer(offer.getId()); } log.info("onRemoved offerId={}", offer.getId()); @@ -625,12 +628,15 @@ public void closeOpenOffer(Offer offer) { log::error); }); } else { - // offer taken may have been OCO, in which case all its clones need to be removed - getOpenOffersByMakerFeeTxId(offer.getOfferFeePaymentTxId()).forEach(openOffer -> { + getOpenOffersByMakerFee(offer.getOfferFeePaymentTxId()).forEach(openOffer -> { removeOpenOfferFromList(openOffer); - openOffer.setState(OpenOffer.State.CLOSED); - if (!offer.getId().equals(openOffer.getId())) { - btcWalletService.resetAddressEntriesForOpenOffer(openOffer.getId()); // cleanup OCO clone + + if (offer.getId().equals(openOffer.getId())) { + openOffer.setState(OpenOffer.State.CLOSED); + } else { + // We use CANCELED for the offers which have shared maker fee but have not been taken for the trade. + openOffer.setState(OpenOffer.State.CANCELED); + btcWalletService.resetAddressEntriesForOpenOffer(openOffer.getId()); } offerBookService.removeOffer(openOffer.getOffer().getOfferPayloadBase(), () -> log.trace("Successfully removed offer"), @@ -644,6 +650,24 @@ public void reserveOpenOffer(OpenOffer openOffer) { requestPersistence(); } + public boolean cannotActivateOffer(Offer offer) { + return openOffers.stream() + .filter(openOffer -> !openOffer.getOffer().isBsqSwapOffer()) // We only handle non-BSQ offers + .filter(openOffer -> !openOffer.getId().equals(offer.getId())) // our own offer gets skipped + .filter(OpenOffer::isActivated) // we only check with activated offers + .anyMatch(openOffer -> + // Offers which share our maker fee will get checked if they have the same payment method + // and currency. + openOffer.getOffer().getOfferFeePaymentTxId().equals(offer.getOfferFeePaymentTxId()) && + openOffer.getOffer().getPaymentMethodId().equalsIgnoreCase(offer.getPaymentMethodId()) && + openOffer.getOffer().getCounterCurrencyCode().equalsIgnoreCase(offer.getCounterCurrencyCode()) && + openOffer.getOffer().getBaseCurrencyCode().equalsIgnoreCase(offer.getBaseCurrencyCode())); + } + + public boolean isOfferWithSharedMakerFee(OpenOffer openOffer) { + return getOpenOffersByMakerFee(openOffer.getOffer().getOfferFeePaymentTxId()).size() > 1; + } + /////////////////////////////////////////////////////////////////////////////////////////// // Getters @@ -661,13 +685,6 @@ public Optional getOpenOfferById(String offerId) { return openOffers.stream().filter(e -> e.getId().equals(offerId)).findFirst(); } - public List getOpenOffersByMakerFeeTxId(String makerFeeTxId) { - return openOffers.stream() - .filter(e -> !e.getOffer().isBsqSwapOffer() && e.getOffer().getOfferFeePaymentTxId() - .equals(makerFeeTxId == null ? "" : makerFeeTxId)) - .collect(Collectors.toList()); - } - /////////////////////////////////////////////////////////////////////////////////////////// // OfferPayload Availability @@ -1185,23 +1202,16 @@ private boolean isBsqSwapOfferLackingFunds(OpenOffer openOffer) { openOffer.isBsqSwapOfferHasMissingFunds(); } - public boolean canBeEnabled(Offer newOffer) { - // does the user have another open offer on the same currency, payment method, and reserved UTXO? - return openOffers.stream().noneMatch(e -> - !e.getOffer().isBsqSwapOffer() && - !e.isDeactivated() && - !e.getId().equals(newOffer.getId()) && - e.getOffer().getOfferFeePaymentTxId().equals(newOffer.getOfferFeePaymentTxId()) && - e.getOffer().getPaymentMethodId().equalsIgnoreCase(newOffer.getPaymentMethodId()) && - e.getOffer().getCounterCurrencyCode().equalsIgnoreCase(newOffer.getCounterCurrencyCode()) && - e.getOffer().getBaseCurrencyCode().equalsIgnoreCase(newOffer.getBaseCurrencyCode())); - } - - public boolean safeRemovalOfOcoClone(OpenOffer openOffer) { - return getOpenOffersByMakerFeeTxId(openOffer.getOffer().getOfferFeePaymentTxId()).size() > 1; + private boolean preventedFromPublishing(OpenOffer openOffer) { + return openOffer.isDeactivated() || + openOffer.isBsqSwapOfferHasMissingFunds() || + cannotActivateOffer(openOffer.getOffer()); } - private boolean preventedFromPublishing(OpenOffer openOffer) { - return openOffer.isDeactivated() || openOffer.isBsqSwapOfferHasMissingFunds() || !canBeEnabled(openOffer.getOffer()); + private Set getOpenOffersByMakerFee(String makerFeeTxId) { + return openOffers.stream() + .filter(openOffer -> !openOffer.getOffer().isBsqSwapOffer() && + openOffer.getOffer().getOfferFeePaymentTxId().equals(makerFeeTxId)) + .collect(Collectors.toSet()); } } diff --git a/core/src/main/java/bisq/core/offer/placeoffer/bisq_v1/PlaceOfferModel.java b/core/src/main/java/bisq/core/offer/placeoffer/bisq_v1/PlaceOfferModel.java index a57fde42e67..8073a853128 100644 --- a/core/src/main/java/bisq/core/offer/placeoffer/bisq_v1/PlaceOfferModel.java +++ b/core/src/main/java/bisq/core/offer/placeoffer/bisq_v1/PlaceOfferModel.java @@ -45,7 +45,7 @@ public class PlaceOfferModel implements Model { private final Offer offer; private final Coin reservedFundsForOffer; private final boolean useSavingsWallet; - private final boolean useBatchOfferOco; + private final boolean isSharedMakerFee; private final BtcWalletService walletService; private final TradeWalletService tradeWalletService; private final BsqWalletService bsqWalletService; @@ -67,7 +67,7 @@ public class PlaceOfferModel implements Model { public PlaceOfferModel(Offer offer, Coin reservedFundsForOffer, boolean useSavingsWallet, - boolean useBatchOfferOco, + boolean isSharedMakerFee, BtcWalletService walletService, TradeWalletService tradeWalletService, BsqWalletService bsqWalletService, @@ -81,7 +81,7 @@ public PlaceOfferModel(Offer offer, this.offer = offer; this.reservedFundsForOffer = reservedFundsForOffer; this.useSavingsWallet = useSavingsWallet; - this.useBatchOfferOco = useBatchOfferOco; + this.isSharedMakerFee = isSharedMakerFee; this.walletService = walletService; this.tradeWalletService = tradeWalletService; this.bsqWalletService = bsqWalletService; diff --git a/core/src/main/java/bisq/core/offer/placeoffer/bisq_v1/PlaceOfferProtocol.java b/core/src/main/java/bisq/core/offer/placeoffer/bisq_v1/PlaceOfferProtocol.java index a30a9c8e7eb..03d709e96b4 100644 --- a/core/src/main/java/bisq/core/offer/placeoffer/bisq_v1/PlaceOfferProtocol.java +++ b/core/src/main/java/bisq/core/offer/placeoffer/bisq_v1/PlaceOfferProtocol.java @@ -19,7 +19,7 @@ import bisq.core.offer.placeoffer.bisq_v1.tasks.AddToOfferBook; import bisq.core.offer.placeoffer.bisq_v1.tasks.CheckNumberOfUnconfirmedTransactions; -import bisq.core.offer.placeoffer.bisq_v1.tasks.CloneMakerFeeOco; +import bisq.core.offer.placeoffer.bisq_v1.tasks.CloneAddressEntryForSharedMakerFee; import bisq.core.offer.placeoffer.bisq_v1.tasks.CreateMakerFeeTx; import bisq.core.offer.placeoffer.bisq_v1.tasks.ValidateOffer; import bisq.core.trade.bisq_v1.TransactionResultHandler; @@ -78,10 +78,10 @@ public void placeOffer() { } ); - if (model.isUseBatchOfferOco()) { + if (model.isSharedMakerFee()) { taskRunner.addTasks( ValidateOffer.class, - CloneMakerFeeOco.class + CloneAddressEntryForSharedMakerFee.class ); } else { taskRunner.addTasks( diff --git a/core/src/main/java/bisq/core/offer/placeoffer/bisq_v1/tasks/CloneMakerFeeOco.java b/core/src/main/java/bisq/core/offer/placeoffer/bisq_v1/tasks/CloneAddressEntryForSharedMakerFee.java similarity index 55% rename from core/src/main/java/bisq/core/offer/placeoffer/bisq_v1/tasks/CloneMakerFeeOco.java rename to core/src/main/java/bisq/core/offer/placeoffer/bisq_v1/tasks/CloneAddressEntryForSharedMakerFee.java index 175adbed7e5..3d620ee9e7a 100644 --- a/core/src/main/java/bisq/core/offer/placeoffer/bisq_v1/tasks/CloneMakerFeeOco.java +++ b/core/src/main/java/bisq/core/offer/placeoffer/bisq_v1/tasks/CloneAddressEntryForSharedMakerFee.java @@ -33,45 +33,44 @@ import java.util.List; import java.util.Optional; -public class CloneMakerFeeOco extends Task { +// +public class CloneAddressEntryForSharedMakerFee extends Task { @SuppressWarnings({"unused"}) - public CloneMakerFeeOco(TaskRunner taskHandler, PlaceOfferModel model) { + public CloneAddressEntryForSharedMakerFee(TaskRunner taskHandler, PlaceOfferModel model) { super(taskHandler, model); } @Override protected void run() { runInterceptHook(); - Offer newOcoOffer = model.getOffer(); - // newOcoOffer is cloned from an existing offer; - // the clone needs a unique AddressEntry record associating the offerId with the reserved amount. + + Offer offer = model.getOffer(); + String makerFeeTxId = offer.getOfferFeePaymentTxId(); BtcWalletService walletService = model.getWalletService(); - for (AddressEntry potentialOcoSource : walletService.getAddressEntries(AddressEntry.Context.RESERVED_FOR_TRADE)) { - getTxIdFromAddress(walletService, potentialOcoSource.getAddress()).ifPresent(txId -> { - if (txId.equalsIgnoreCase(newOcoOffer.getOfferFeePaymentTxId())) { - walletService.getOrCreateAddressEntry(potentialOcoSource, newOcoOffer.getId()); - newOcoOffer.setState(Offer.State.OFFER_FEE_PAID); - complete(); - } - }); - if (completed) { + for (AddressEntry reservedForTradeEntry : walletService.getAddressEntries(AddressEntry.Context.RESERVED_FOR_TRADE)) { + if (findTxId(reservedForTradeEntry.getAddress()) + .map(txId -> txId.equals(makerFeeTxId)) + .orElse(false)) { + walletService.getOrCloneAddressEntryWithOfferId(reservedForTradeEntry, offer.getId()); + complete(); return; } } failed(); } - // AddressEntry and TxId are not linked, so do a reverse lookup - private Optional getTxIdFromAddress(BtcWalletService walletService, Address address) { - List txns = walletService.getRecentTransactions(0, false); - for (Transaction txn : txns) { - for (TransactionOutput output : txn.getOutputs()) { + // We look up the most recent transaction with unspent outputs associated with the given address and return + // the txId if found. + private Optional findTxId(Address address) { + BtcWalletService walletService = model.getWalletService(); + List transactions = walletService.getAllRecentTransactions(false); + for (Transaction transaction : transactions) { + for (TransactionOutput output : transaction.getOutputs()) { if (walletService.isTransactionOutputMine(output) && WalletService.isOutputScriptConvertibleToAddress(output)) { String addressString = WalletService.getAddressStringFromOutput(output); - assert addressString != null; // make sure the output is still unspent - if (addressString.equalsIgnoreCase(address.toString()) && output.getSpentBy() == null) { - return Optional.of(txn.getTxId().toString()); + if (addressString != null && addressString.equals(address.toString()) && output.getSpentBy() == null) { + return Optional.of(transaction.getTxId().toString()); } } } diff --git a/core/src/main/resources/i18n/displayStrings.properties b/core/src/main/resources/i18n/displayStrings.properties index a914172bf01..301cec76dc5 100644 --- a/core/src/main/resources/i18n/displayStrings.properties +++ b/core/src/main/resources/i18n/displayStrings.properties @@ -101,7 +101,7 @@ shared.removeOffer=Remove offer shared.dontRemoveOffer=Don't remove offer shared.editOffer=Edit offer shared.duplicateOffer=Duplicate offer -shared.cloneGroupedOfferOco=Clone as Grouped Offer +shared.cloneOffer=Clone offer (share maker fee) shared.openLargeQRWindow=Open large QR code window shared.chooseTradingAccount=Choose trading account shared.faq=Visit FAQ page @@ -366,7 +366,8 @@ offerbook.timeSinceSigning.tooltip.checkmark.buyBtc=buy BTC from a signed accoun offerbook.timeSinceSigning.tooltip.checkmark.wait=wait a minimum of {0} days offerbook.timeSinceSigning.tooltip.learnMore=Learn more offerbook.xmrAutoConf=Is auto-confirm enabled -offerbook.toEnableOffer=Change ccy or payment method to enable offer. + +offerbook.toEnableOffer=Change payment method or currency to enable cloned offer. offerbook.timeSinceSigning.help=When you successfully complete a trade with a peer who has a signed payment account, your payment account is signed.\n\ {0} days later, the initial limit of {1} is lifted and your account can sign other peers'' payment accounts. @@ -657,7 +658,6 @@ portfolio.tab.bsqSwap=Unconfirmed BSQ swaps portfolio.tab.failed=Failed portfolio.tab.editOpenOffer=Edit offer portfolio.tab.duplicateOffer=Duplicate offer -portfolio.context.offerLikeThis=Create new offer like this... portfolio.context.notYourOffer=You can only duplicate offers where you were the maker. portfolio.closedTrades.deviation.help=Percentage price deviation from market diff --git a/core/src/main/resources/i18n/displayStrings_zh-hant.properties b/core/src/main/resources/i18n/displayStrings_zh-hant.properties index 92f32b59559..15f1bbb1830 100644 --- a/core/src/main/resources/i18n/displayStrings_zh-hant.properties +++ b/core/src/main/resources/i18n/displayStrings_zh-hant.properties @@ -604,7 +604,7 @@ portfolio.tab.bsqSwap=Unconfirmed BSQ swaps portfolio.tab.failed=失敗 portfolio.tab.editOpenOffer=編輯報價 portfolio.tab.duplicateOffer=Duplicate offer -portfolio.context.offerLikeThis=Create new offer like this... +portfolio.context.offerLikeThis=Duplicate offer portfolio.context.notYourOffer=You can only duplicate offers where you were the maker. portfolio.closedTrades.deviation.help=Percentage price deviation from market 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 c9291ec3a2c..d93ea742e39 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 @@ -264,7 +264,7 @@ public void initialize() { tableView -> { TableRow row = new TableRow<>(); ContextMenu rowMenu = new ContextMenu(); - MenuItem duplicateItem = new MenuItem(Res.get("portfolio.context.offerLikeThis")); + MenuItem duplicateItem = new MenuItem(Res.get("portfolio.tab.duplicateOffer")); duplicateItem.setOnAction((ActionEvent event) -> onDuplicateOffer(row.getItem().getTradable().getOffer())); rowMenu.getItems().add(duplicateItem); row.contextMenuProperty().bind( diff --git a/desktop/src/main/java/bisq/desktop/main/portfolio/openoffer/OpenOfferListItem.java b/desktop/src/main/java/bisq/desktop/main/portfolio/openoffer/OpenOfferListItem.java index 480336f219b..d90fa1aeae9 100644 --- a/desktop/src/main/java/bisq/desktop/main/portfolio/openoffer/OpenOfferListItem.java +++ b/desktop/src/main/java/bisq/desktop/main/portfolio/openoffer/OpenOfferListItem.java @@ -134,6 +134,7 @@ public String getTriggerPriceAsString() { } } + // public String getOcoGroupForSorting() { Offer offer = getOffer(); if (offer.isBsqSwapOffer()) { @@ -142,6 +143,7 @@ public String getOcoGroupForSorting() { return offer.getOfferFeePaymentTxId(); } + // public String getOcoGroupForDisplay() { return getOcoGroupForSorting().substring(0, 4); } diff --git a/desktop/src/main/java/bisq/desktop/main/portfolio/openoffer/OpenOffersView.java b/desktop/src/main/java/bisq/desktop/main/portfolio/openoffer/OpenOffersView.java index 2480d3210cb..d25fb4beae1 100644 --- a/desktop/src/main/java/bisq/desktop/main/portfolio/openoffer/OpenOffersView.java +++ b/desktop/src/main/java/bisq/desktop/main/portfolio/openoffer/OpenOffersView.java @@ -41,6 +41,7 @@ import bisq.core.offer.OpenOffer; import bisq.core.offer.OpenOfferManager; import bisq.core.offer.bisq_v1.OfferPayload; +import bisq.core.provider.price.PriceFeedService; import bisq.core.user.DontShowAgainLookup; import com.googlecode.jcsv.writer.CSVEntryConverter; @@ -139,6 +140,7 @@ public String toString() { @FXML AutoTooltipSlideToggleButton selectToggleButton; + private final PriceFeedService priceFeedService; private final Navigation navigation; private final OfferDetailsWindow offerDetailsWindow; private final BsqSwapOfferDetailsWindow bsqSwapOfferDetailsWindow; @@ -151,10 +153,12 @@ public String toString() { @Inject public OpenOffersView(OpenOffersViewModel model, OpenOfferManager openOfferManager, + PriceFeedService priceFeedService, Navigation navigation, OfferDetailsWindow offerDetailsWindow, BsqSwapOfferDetailsWindow bsqSwapOfferDetailsWindow) { super(model); + this.priceFeedService = priceFeedService; this.navigation = navigation; this.offerDetailsWindow = offerDetailsWindow; this.bsqSwapOfferDetailsWindow = bsqSwapOfferDetailsWindow; @@ -221,15 +225,13 @@ public void initialize() { tableView -> { final TableRow row = new TableRow<>(); final ContextMenu rowMenu = new ContextMenu(); - MenuItem duplicateItem = new MenuItem(Res.get("portfolio.context.offerLikeThis")); - duplicateItem.setOnAction((event) -> onDuplicateOffer(row.getItem())); - MenuItem cloneGroupedOfferOco1 = new MenuItem(Res.get("shared.cloneGroupedOfferOco")); - cloneGroupedOfferOco1.setOnAction((event) -> onDuplicateOfferOco(row.getItem(), 1)); - MenuItem cloneGroupedOfferOco5 = new MenuItem(Res.get("shared.cloneGroupedOfferOco") + " x5"); - cloneGroupedOfferOco5.setOnAction((event) -> onDuplicateOfferOco(row.getItem(), 5)); - rowMenu.getItems().add(duplicateItem); - rowMenu.getItems().add(cloneGroupedOfferOco1); - rowMenu.getItems().add(cloneGroupedOfferOco5); + MenuItem duplicateOfferMenuItem = new MenuItem(Res.get("portfolio.tab.duplicateOffer")); + duplicateOfferMenuItem.setOnAction((event) -> onDuplicateOffer(row.getItem())); + // + MenuItem cloneOfferMenuItem = new MenuItem(Res.get("shared.cloneOffer")); + cloneOfferMenuItem.setOnAction((event) -> onCloneOffer(row.getItem().getOpenOffer())); + rowMenu.getItems().add(duplicateOfferMenuItem); + rowMenu.getItems().add(cloneOfferMenuItem); row.contextMenuProperty().bind( Bindings.when(Bindings.isNotNull(row.itemProperty())) .then(rowMenu) @@ -386,7 +388,7 @@ private void onActivateOpenOffer(OpenOffer openOffer) { private void onRemoveOpenOffer(OpenOfferListItem item) { OpenOffer openOffer = item.getOpenOffer(); if (model.isBootstrappedOrShowPopup()) { - if (openOfferManager.safeRemovalOfOcoClone(openOffer)) { + if (openOfferManager.isOfferWithSharedMakerFee(openOffer)) { doRemoveOpenOffer(openOffer); } else { String key = (openOffer.getOffer().isBsqSwapOffer() ? "RemoveBsqSwapWarning" : "RemoveOfferWarning"); @@ -409,16 +411,17 @@ private void onRemoveOpenOffer(OpenOfferListItem item) { } private void doRemoveOpenOffer(OpenOffer openOffer) { - boolean isSafeRemovalOfOcoClone = openOfferManager.safeRemovalOfOcoClone(openOffer); model.onRemoveOpenOffer(openOffer, () -> { log.debug("Remove offer was successful"); tableView.refresh(); - if (openOffer.getOffer().isBsqSwapOffer() || isSafeRemovalOfOcoClone) { - return; // nothing to withdraw when Bsq swap is canceled (issue #5956) + // We do not show the popup if it's a BSQ offer or a cloned offer with shared maker fee + if (openOffer.getOffer().isBsqSwapOffer() || openOfferManager.isOfferWithSharedMakerFee(openOffer)) { + return; } + String key = "WithdrawFundsAfterRemoveOfferInfo"; if (DontShowAgainLookup.showAgain(key)) { new Popup().instruction(Res.get("offerbook.withdrawFundsHint", Res.get("navigation.funds.availableForWithdrawal"))) @@ -448,60 +451,64 @@ private void onDuplicateOffer(OpenOfferListItem item) { } } - private void onDuplicateOfferOco(OpenOfferListItem item, int numDuplicates) { - for (int i=0; i< numDuplicates; i++) { - item.getOffer().getOfferPayload().ifPresent(original -> { - log.info("Duplicating offer as OCO: {}", original.getId()); - String newOfferId = getRandomOfferId(); - OfferPayload offerPayload = new OfferPayload(newOfferId, - new Date().getTime(), - original.getOwnerNodeAddress(), - original.getPubKeyRing(), - original.getDirection(), - original.getPrice(), - original.getMarketPriceMargin(), - original.isUseMarketBasedPrice(), - original.getAmount(), - original.getMinAmount(), - original.getBaseCurrencyCode(), - original.getCounterCurrencyCode(), - original.getArbitratorNodeAddresses(), - original.getMediatorNodeAddresses(), - original.getPaymentMethodId(), - original.getMakerPaymentAccountId(), - original.getOfferFeePaymentTxId(), - original.getCountryCode(), - original.getAcceptedCountryCodes(), - original.getBankId(), - original.getAcceptedBankIds(), - original.getVersionNr(), - original.getBlockHeightAtOfferCreation(), - original.getTxFee(), - original.getMakerFee(), - original.isCurrencyForMakerFeeBtc(), - original.getBuyerSecurityDeposit(), - original.getSellerSecurityDeposit(), - original.getMaxTradeLimit(), - original.getMaxTradePeriod(), - original.isUseAutoClose(), - original.isUseReOpenAfterAutoClose(), - original.getLowerClosePrice(), - original.getUpperClosePrice(), - original.isPrivateOffer(), - original.getHashOfChallenge(), - original.getExtraDataMap(), - original.getProtocolVersion()); - Offer expandedOffer = new Offer(offerPayload); - openOfferManager.placeOffer(expandedOffer, - 0, - false, - true, - 0, - transaction -> { - }, - log::error); - }); + private void onCloneOffer(OpenOffer openOffer) { + if (openOffer == null || openOffer.getOffer() == null || openOffer.getOffer().getOfferPayload().isEmpty()) { + return; } + + Offer offer = openOffer.getOffer(); + OfferPayload sourceOfferPayload = offer.getOfferPayload().get(); + log.info("Clone offerPayload with shared maker fee: {}", sourceOfferPayload.getId()); + String newOfferId = getRandomOfferId(); + OfferPayload duplicatedOfferPayload = new OfferPayload(newOfferId, + new Date().getTime(), + sourceOfferPayload.getOwnerNodeAddress(), + sourceOfferPayload.getPubKeyRing(), + sourceOfferPayload.getDirection(), + sourceOfferPayload.getPrice(), + sourceOfferPayload.getMarketPriceMargin(), + sourceOfferPayload.isUseMarketBasedPrice(), + sourceOfferPayload.getAmount(), + sourceOfferPayload.getMinAmount(), + sourceOfferPayload.getBaseCurrencyCode(), + sourceOfferPayload.getCounterCurrencyCode(), + sourceOfferPayload.getArbitratorNodeAddresses(), + sourceOfferPayload.getMediatorNodeAddresses(), + sourceOfferPayload.getPaymentMethodId(), + sourceOfferPayload.getMakerPaymentAccountId(), + sourceOfferPayload.getOfferFeePaymentTxId(), + sourceOfferPayload.getCountryCode(), + sourceOfferPayload.getAcceptedCountryCodes(), + sourceOfferPayload.getBankId(), + sourceOfferPayload.getAcceptedBankIds(), + sourceOfferPayload.getVersionNr(), + sourceOfferPayload.getBlockHeightAtOfferCreation(), + sourceOfferPayload.getTxFee(), + sourceOfferPayload.getMakerFee(), + sourceOfferPayload.isCurrencyForMakerFeeBtc(), + sourceOfferPayload.getBuyerSecurityDeposit(), + sourceOfferPayload.getSellerSecurityDeposit(), + sourceOfferPayload.getMaxTradeLimit(), + sourceOfferPayload.getMaxTradePeriod(), + sourceOfferPayload.isUseAutoClose(), + sourceOfferPayload.isUseReOpenAfterAutoClose(), + sourceOfferPayload.getLowerClosePrice(), + sourceOfferPayload.getUpperClosePrice(), + sourceOfferPayload.isPrivateOffer(), + sourceOfferPayload.getHashOfChallenge(), + sourceOfferPayload.getExtraDataMap(), + sourceOfferPayload.getProtocolVersion()); + Offer clonedOffer = new Offer(duplicatedOfferPayload); + clonedOffer.setPriceFeedService(priceFeedService); + offer.setState(Offer.State.OFFER_FEE_PAID); + openOfferManager.placeOffer(clonedOffer, + clonedOffer.getBuyerSecurityDeposit().getValue(), + false, + true, + openOffer.getTriggerPrice(), + transaction -> { + }, + log::error); } private void setOfferIdColumnCellFactory() { @@ -684,7 +691,7 @@ public void updateItem(final OpenOfferListItem item, boolean empty) { if (item != null) { if (item.isNotPublished()) getStyleClass().add("offer-disabled"); Label label = new AutoTooltipLabel(item.getOcoGroupForDisplay()); - if (!openOfferManager.canBeEnabled(item.getOpenOffer().getOffer())) { + if (openOfferManager.cannotActivateOffer(item.getOpenOffer().getOffer())) { Text icon = getRegularIconForLabel(MaterialDesignIcon.EYE_OFF, label, "opaque-icon"); label.setContentDisplay(ContentDisplay.RIGHT); Tooltip.install(icon, new Tooltip(Res.get("offerbook.toEnableOffer"))); 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 bcb1e0b5836..978d2f10f36 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 @@ -234,7 +234,7 @@ public void initialize() { tableView -> { final TableRow row = new TableRow<>(); final ContextMenu rowMenu = new ContextMenu(); - MenuItem duplicateItem = new MenuItem(Res.get("portfolio.context.offerLikeThis")); + MenuItem duplicateItem = new MenuItem(Res.get("portfolio.tab.duplicateOffer")); duplicateItem.setOnAction((event) -> { try { OfferPayload offerPayload = row.getItem().getTrade().getOffer().getOfferPayload().orElseThrow(); From 3403b1662c4617788089eaf4d94103b0551f9bd8 Mon Sep 17 00:00:00 2001 From: HenrikJannsen Date: Thu, 4 May 2023 20:10:38 +0700 Subject: [PATCH 10/31] User maker fee as column name (instead of group) Signed-off-by: HenrikJannsen --- .../core/btc/wallet/BtcWalletService.java | 2 +- core/src/main/java/bisq/core/offer/Offer.java | 1 + .../bisq/core/offer/OpenOfferManager.java | 18 +- .../CloneAddressEntryForSharedMakerFee.java | 1 + .../resources/i18n/displayStrings.properties | 7 +- .../i18n/displayStrings_zh-hant.properties | 2 +- .../openoffer/OpenOfferListItem.java | 21 +-- .../portfolio/openoffer/OpenOffersView.fxml | 2 +- .../portfolio/openoffer/OpenOffersView.java | 170 +++++++++++------- 9 files changed, 134 insertions(+), 90 deletions(-) diff --git a/core/src/main/java/bisq/core/btc/wallet/BtcWalletService.java b/core/src/main/java/bisq/core/btc/wallet/BtcWalletService.java index a3f67c039cc..b6b3fbd24e1 100644 --- a/core/src/main/java/bisq/core/btc/wallet/BtcWalletService.java +++ b/core/src/main/java/bisq/core/btc/wallet/BtcWalletService.java @@ -630,7 +630,7 @@ public Optional getAddressEntry(String offerId, .findAny(); } - // For offers with shared maker fee we create a new address entry with from the source entry + // For cloned offers with shared maker fee we create a new address entry based on the source entry // and set the new offerId. public AddressEntry getOrCloneAddressEntryWithOfferId(AddressEntry sourceAddressEntry, String offerId) { Optional addressEntry = getAddressEntryListAsImmutableList().stream() diff --git a/core/src/main/java/bisq/core/offer/Offer.java b/core/src/main/java/bisq/core/offer/Offer.java index b14f9c2c589..1009434c3a6 100644 --- a/core/src/main/java/bisq/core/offer/Offer.java +++ b/core/src/main/java/bisq/core/offer/Offer.java @@ -502,6 +502,7 @@ public String getMakerPaymentAccountId() { return offerPayloadBase.getMakerPaymentAccountId(); } + @Nullable public String getOfferFeePaymentTxId() { return getOfferPayload().map(OfferPayload::getOfferFeePaymentTxId).orElse(null); } diff --git a/core/src/main/java/bisq/core/offer/OpenOfferManager.java b/core/src/main/java/bisq/core/offer/OpenOfferManager.java index e2750cffc5c..0dfce513de8 100644 --- a/core/src/main/java/bisq/core/offer/OpenOfferManager.java +++ b/core/src/main/java/bisq/core/offer/OpenOfferManager.java @@ -388,7 +388,7 @@ public void placeOffer(Offer offer, checkNotNull(offer.getMakerFee(), "makerFee must not be null"); checkArgument(!offer.isBsqSwapOffer()); - int numClones = getOpenOffersByMakerFee(offer.getOfferFeePaymentTxId()).size(); + int numClones = getOpenOffersByMakerFeeTxId(offer.getOfferFeePaymentTxId()).size(); if (numClones >= 10) { errorMessageHandler.handleErrorMessage("Cannot create offer because maximum number of 10 cloned offers with shared maker fee is reached."); return; @@ -608,7 +608,7 @@ private void onRemoved(OpenOffer openOffer, ResultHandler resultHandler, Offer o // to history. Only when the last offer with that maker fee txId got removed we add it. // Only canceled offers which have lost maker fees are shown in history. // For that reason we also do not add BSQ offers. - if (getOpenOffersByMakerFee(offer.getOfferFeePaymentTxId()).isEmpty()) { + if (getOpenOffersByMakerFeeTxId(offer.getOfferFeePaymentTxId()).isEmpty()) { closedTradableManager.add(openOffer); } btcWalletService.resetAddressEntriesForOpenOffer(offer.getId()); @@ -628,7 +628,7 @@ public void closeOpenOffer(Offer offer) { log::error); }); } else { - getOpenOffersByMakerFee(offer.getOfferFeePaymentTxId()).forEach(openOffer -> { + getOpenOffersByMakerFeeTxId(offer.getOfferFeePaymentTxId()).forEach(openOffer -> { removeOpenOfferFromList(openOffer); if (offer.getId().equals(openOffer.getId())) { @@ -664,8 +664,8 @@ public boolean cannotActivateOffer(Offer offer) { openOffer.getOffer().getBaseCurrencyCode().equalsIgnoreCase(offer.getBaseCurrencyCode())); } - public boolean isOfferWithSharedMakerFee(OpenOffer openOffer) { - return getOpenOffersByMakerFee(openOffer.getOffer().getOfferFeePaymentTxId()).size() > 1; + public boolean hasOfferSharedMakerFee(OpenOffer openOffer) { + return getOpenOffersByMakerFeeTxId(openOffer.getOffer().getOfferFeePaymentTxId()).size() > 1; } @@ -1208,10 +1208,12 @@ private boolean preventedFromPublishing(OpenOffer openOffer) { cannotActivateOffer(openOffer.getOffer()); } - private Set getOpenOffersByMakerFee(String makerFeeTxId) { - return openOffers.stream() + private Set getOpenOffersByMakerFeeTxId(String makerFeeTxId) { + Set collect = openOffers.stream() .filter(openOffer -> !openOffer.getOffer().isBsqSwapOffer() && - openOffer.getOffer().getOfferFeePaymentTxId().equals(makerFeeTxId)) + makerFeeTxId != null && + makerFeeTxId.equals(openOffer.getOffer().getOfferFeePaymentTxId())) .collect(Collectors.toSet()); + return collect; } } diff --git a/core/src/main/java/bisq/core/offer/placeoffer/bisq_v1/tasks/CloneAddressEntryForSharedMakerFee.java b/core/src/main/java/bisq/core/offer/placeoffer/bisq_v1/tasks/CloneAddressEntryForSharedMakerFee.java index 3d620ee9e7a..037f8c96c9a 100644 --- a/core/src/main/java/bisq/core/offer/placeoffer/bisq_v1/tasks/CloneAddressEntryForSharedMakerFee.java +++ b/core/src/main/java/bisq/core/offer/placeoffer/bisq_v1/tasks/CloneAddressEntryForSharedMakerFee.java @@ -56,6 +56,7 @@ protected void run() { return; } } + failed(); } diff --git a/core/src/main/resources/i18n/displayStrings.properties b/core/src/main/resources/i18n/displayStrings.properties index 301cec76dc5..95283c51683 100644 --- a/core/src/main/resources/i18n/displayStrings.properties +++ b/core/src/main/resources/i18n/displayStrings.properties @@ -77,7 +77,6 @@ shared.deviation=Deviation shared.paymentMethod=Payment method shared.tradeCurrency=Trade currency shared.offerType=Offer type -shared.group=Group shared.details=Details shared.address=Address shared.balanceWithCur=Balance in {0} @@ -101,7 +100,7 @@ shared.removeOffer=Remove offer shared.dontRemoveOffer=Don't remove offer shared.editOffer=Edit offer shared.duplicateOffer=Duplicate offer -shared.cloneOffer=Clone offer (share maker fee) +shared.cloneOffer=Clone offer (with shared maker fee) shared.openLargeQRWindow=Open large QR code window shared.chooseTradingAccount=Choose trading account shared.faq=Visit FAQ page @@ -367,7 +366,8 @@ offerbook.timeSinceSigning.tooltip.checkmark.wait=wait a minimum of {0} days offerbook.timeSinceSigning.tooltip.learnMore=Learn more offerbook.xmrAutoConf=Is auto-confirm enabled -offerbook.toEnableOffer=Change payment method or currency to enable cloned offer. +offerbook.clonedOffer.info=This is a cloned offer with shared maker fee transaction ID +offerbook.nonClonedOffer.info=Regular offer without shared maker fee transaction ID offerbook.timeSinceSigning.help=When you successfully complete a trade with a peer who has a signed payment account, your payment account is signed.\n\ {0} days later, the initial limit of {1} is lifted and your account can sign other peers'' payment accounts. @@ -611,6 +611,7 @@ takeOffer.tac=With taking this offer I agree to the trade conditions as defined #################################################################### openOffer.header.triggerPrice=Trigger price +openOffer.header.makerFeeTxId=Maker fee openOffer.triggerPrice=Trigger price {0} openOffer.triggered=The offer has been deactivated because the market price reached your trigger price.\n\ Please edit the offer to define a new trigger price diff --git a/core/src/main/resources/i18n/displayStrings_zh-hant.properties b/core/src/main/resources/i18n/displayStrings_zh-hant.properties index 15f1bbb1830..92f32b59559 100644 --- a/core/src/main/resources/i18n/displayStrings_zh-hant.properties +++ b/core/src/main/resources/i18n/displayStrings_zh-hant.properties @@ -604,7 +604,7 @@ portfolio.tab.bsqSwap=Unconfirmed BSQ swaps portfolio.tab.failed=失敗 portfolio.tab.editOpenOffer=編輯報價 portfolio.tab.duplicateOffer=Duplicate offer -portfolio.context.offerLikeThis=Duplicate offer +portfolio.context.offerLikeThis=Create new offer like this... portfolio.context.notYourOffer=You can only duplicate offers where you were the maker. portfolio.closedTrades.deviation.help=Percentage price deviation from market diff --git a/desktop/src/main/java/bisq/desktop/main/portfolio/openoffer/OpenOfferListItem.java b/desktop/src/main/java/bisq/desktop/main/portfolio/openoffer/OpenOfferListItem.java index d90fa1aeae9..b194334b7c3 100644 --- a/desktop/src/main/java/bisq/desktop/main/portfolio/openoffer/OpenOfferListItem.java +++ b/desktop/src/main/java/bisq/desktop/main/portfolio/openoffer/OpenOfferListItem.java @@ -50,7 +50,11 @@ class OpenOfferListItem implements FilterableListItem { private final OpenOfferManager openOfferManager; - OpenOfferListItem(OpenOffer openOffer, PriceUtil priceUtil, CoinFormatter btcFormatter, BsqFormatter bsqFormatter, OpenOfferManager openOfferManager) { + OpenOfferListItem(OpenOffer openOffer, + PriceUtil priceUtil, + CoinFormatter btcFormatter, + BsqFormatter bsqFormatter, + OpenOfferManager openOfferManager) { this.openOffer = openOffer; this.priceUtil = priceUtil; this.btcFormatter = btcFormatter; @@ -134,18 +138,9 @@ public String getTriggerPriceAsString() { } } - // - public String getOcoGroupForSorting() { - Offer offer = getOffer(); - if (offer.isBsqSwapOffer()) { - return " "; - } - return offer.getOfferFeePaymentTxId(); - } - - // - public String getOcoGroupForDisplay() { - return getOcoGroupForSorting().substring(0, 4); + String getMakerFeeTxId() { + String makerFeeTxId = getOffer().getOfferFeePaymentTxId(); + return makerFeeTxId != null ? makerFeeTxId : ""; } @Override diff --git a/desktop/src/main/java/bisq/desktop/main/portfolio/openoffer/OpenOffersView.fxml b/desktop/src/main/java/bisq/desktop/main/portfolio/openoffer/OpenOffersView.fxml index ab650ea0180..22f69af71fe 100644 --- a/desktop/src/main/java/bisq/desktop/main/portfolio/openoffer/OpenOffersView.fxml +++ b/desktop/src/main/java/bisq/desktop/main/portfolio/openoffer/OpenOffersView.fxml @@ -41,6 +41,7 @@ + @@ -50,7 +51,6 @@ - diff --git a/desktop/src/main/java/bisq/desktop/main/portfolio/openoffer/OpenOffersView.java b/desktop/src/main/java/bisq/desktop/main/portfolio/openoffer/OpenOffersView.java index d25fb4beae1..c13ce344d06 100644 --- a/desktop/src/main/java/bisq/desktop/main/portfolio/openoffer/OpenOffersView.java +++ b/desktop/src/main/java/bisq/desktop/main/portfolio/openoffer/OpenOffersView.java @@ -55,7 +55,6 @@ import javafx.stage.Stage; import javafx.scene.control.Button; -import javafx.scene.control.ContentDisplay; import javafx.scene.control.ContextMenu; import javafx.scene.control.Label; import javafx.scene.control.MenuItem; @@ -99,6 +98,7 @@ public class OpenOffersView extends ActivatableViewAndModel tableView; @FXML TableColumn priceColumn, deviationColumn, amountColumn, volumeColumn, - marketColumn, directionColumn, dateColumn, offerIdColumn, deactivateItemColumn, groupColumn, + marketColumn, directionColumn, dateColumn, offerIdColumn, deactivateItemColumn, makerFeeTxIdColumn, removeItemColumn, editItemColumn, triggerPriceColumn, triggerIconColumn, paymentMethodColumn, duplicateItemColumn; @FXML FilterBox filterBox; @@ -168,12 +167,12 @@ public OpenOffersView(OpenOffersViewModel model, @Override public void initialize() { widthListener = (observable, oldValue, newValue) -> onWidthChange((double) newValue); + makerFeeTxIdColumn.setGraphic(new AutoTooltipLabel(ColumnNames.MAKER_FEE_TX_ID.toString())); paymentMethodColumn.setGraphic(new AutoTooltipLabel(ColumnNames.PAYMENT_METHOD.toString())); priceColumn.setGraphic(new AutoTooltipLabel(ColumnNames.PRICE.toString())); deviationColumn.setGraphic(new AutoTooltipTableColumn<>(ColumnNames.DEVIATION.toString(), Res.get("portfolio.closedTrades.deviation.help")).getGraphic()); triggerPriceColumn.setGraphic(new AutoTooltipLabel(ColumnNames.TRIGGER_PRICE.toString())); - groupColumn.setGraphic(new AutoTooltipLabel(ColumnNames.GROUP.toString())); amountColumn.setGraphic(new AutoTooltipLabel(ColumnNames.AMOUNT.toString())); volumeColumn.setGraphic(new AutoTooltipLabel(ColumnNames.VOLUME.toString())); marketColumn.setGraphic(new AutoTooltipLabel(ColumnNames.MARKET.toString())); @@ -186,6 +185,7 @@ public void initialize() { removeItemColumn.setText(""); setOfferIdColumnCellFactory(); + setMakerFeeTxIdColumnCellFactory(); setDirectionColumnCellFactory(); setMarketColumnCellFactory(); setPriceColumnCellFactory(); @@ -198,7 +198,6 @@ public void initialize() { setEditColumnCellFactory(); setTriggerIconColumnCellFactory(); setTriggerPriceColumnCellFactory(); - setGroupColumnCellFactory(); setDuplicateColumnCellFactory(); setRemoveColumnCellFactory(); @@ -206,6 +205,7 @@ public void initialize() { tableView.setPlaceholder(new AutoTooltipLabel(Res.get("table.placeholder.noItems", Res.get("shared.openOffers")))); offerIdColumn.setComparator(Comparator.comparing(o -> o.getOffer().getId())); + makerFeeTxIdColumn.setComparator(Comparator.comparing(OpenOfferListItem::getMakerFeeTxId)); directionColumn.setComparator(Comparator.comparing(o -> o.getOffer().getDirection())); marketColumn.setComparator(Comparator.comparing(OpenOfferListItem::getMarketDescription)); amountColumn.setComparator(Comparator.comparing(o -> o.getOffer().getAmount())); @@ -213,7 +213,6 @@ public void initialize() { deviationColumn.setComparator(Comparator.comparing(OpenOfferListItem::getPriceDeviationAsDouble, Comparator.nullsFirst(Comparator.naturalOrder()))); triggerPriceColumn.setComparator(Comparator.comparing(o -> o.getOpenOffer().getTriggerPrice(), Comparator.nullsFirst(Comparator.naturalOrder()))); - groupColumn.setComparator(Comparator.comparing(OpenOfferListItem::getOcoGroupForSorting)); volumeColumn.setComparator(Comparator.comparing(o -> o.getOffer().getVolume(), Comparator.nullsFirst(Comparator.naturalOrder()))); dateColumn.setComparator(Comparator.comparing(o -> o.getOffer().getDate())); paymentMethodColumn.setComparator(Comparator.comparing(o -> Res.get(o.getOffer().getPaymentMethod().getId()))); @@ -225,13 +224,15 @@ public void initialize() { tableView -> { final TableRow row = new TableRow<>(); final ContextMenu rowMenu = new ContextMenu(); + MenuItem duplicateOfferMenuItem = new MenuItem(Res.get("portfolio.tab.duplicateOffer")); duplicateOfferMenuItem.setOnAction((event) -> onDuplicateOffer(row.getItem())); - // - MenuItem cloneOfferMenuItem = new MenuItem(Res.get("shared.cloneOffer")); - cloneOfferMenuItem.setOnAction((event) -> onCloneOffer(row.getItem().getOpenOffer())); rowMenu.getItems().add(duplicateOfferMenuItem); + + MenuItem cloneOfferMenuItem = new MenuItem(Res.get("shared.cloneOffer")); + cloneOfferMenuItem.setOnAction((event) -> onCloneOffer(row.getItem())); rowMenu.getItems().add(cloneOfferMenuItem); + row.contextMenuProperty().bind( Bindings.when(Bindings.isNotNull(row.itemProperty())) .then(rowMenu) @@ -254,7 +255,7 @@ public void initialize() { c.next(); if (c.wasAdded() || c.wasRemoved()) { updateNumberOfOffers(); - updateGroupColumn(); + updateMakerFeeTxIdColumn(); } }; } @@ -269,7 +270,7 @@ protected void activate() { filterBox.initializeWithCallback(filteredList, tableView, this::updateNumberOfOffers); filterBox.activate(); - updateGroupColumn(); + updateMakerFeeTxIdColumn(); updateSelectToggleButtonState(); selectToggleButton.setOnAction(event -> { @@ -294,6 +295,7 @@ protected void activate() { CSVEntryConverter contentConverter = item -> { String[] columns = new String[ColumnNames.values().length]; columns[ColumnNames.OFFER_ID.ordinal()] = item.getOffer().getShortId(); + columns[ColumnNames.MAKER_FEE_TX_ID.ordinal()] = item.getMakerFeeTxId(); columns[ColumnNames.DATE.ordinal()] = item.getDateAsString(); columns[ColumnNames.MARKET.ordinal()] = item.getMarketDescription(); columns[ColumnNames.PRICE.ordinal()] = item.getPriceAsString(); @@ -303,7 +305,6 @@ protected void activate() { columns[ColumnNames.VOLUME.ordinal()] = item.getVolumeAsString(); columns[ColumnNames.PAYMENT_METHOD.ordinal()] = item.getPaymentMethodAsString(); columns[ColumnNames.DIRECTION.ordinal()] = item.getDirectionLabel(); - columns[ColumnNames.GROUP.ordinal()] = item.getOcoGroupForDisplay(); columns[ColumnNames.STATUS.ordinal()] = String.valueOf(!item.getOpenOffer().isDeactivated()); return columns; }; @@ -324,9 +325,9 @@ private void updateNumberOfOffers() { numItems.setText(Res.get("shared.numItemsLabel", sortedList.size())); } - private void updateGroupColumn() { - groupColumn.setVisible(model.dataModel.getList().stream() - .collect(Collectors.groupingBy(OpenOfferListItem::getOcoGroupForSorting, Collectors.counting())) + private void updateMakerFeeTxIdColumn() { + makerFeeTxIdColumn.setVisible(model.dataModel.getList().stream() + .collect(Collectors.groupingBy(OpenOfferListItem::getMakerFeeTxId, Collectors.counting())) .values().stream().anyMatch(i -> i > 1)); } @@ -388,7 +389,7 @@ private void onActivateOpenOffer(OpenOffer openOffer) { private void onRemoveOpenOffer(OpenOfferListItem item) { OpenOffer openOffer = item.getOpenOffer(); if (model.isBootstrappedOrShowPopup()) { - if (openOfferManager.isOfferWithSharedMakerFee(openOffer)) { + if (openOfferManager.hasOfferSharedMakerFee(openOffer)) { doRemoveOpenOffer(openOffer); } else { String key = (openOffer.getOffer().isBsqSwapOffer() ? "RemoveBsqSwapWarning" : "RemoveOfferWarning"); @@ -418,7 +419,8 @@ private void doRemoveOpenOffer(OpenOffer openOffer) { tableView.refresh(); // We do not show the popup if it's a BSQ offer or a cloned offer with shared maker fee - if (openOffer.getOffer().isBsqSwapOffer() || openOfferManager.isOfferWithSharedMakerFee(openOffer)) { + if (openOffer.getOffer().isBsqSwapOffer() || + openOfferManager.hasOfferSharedMakerFee(openOffer)) { return; } @@ -444,14 +446,17 @@ private void onEditOpenOffer(OpenOffer openOffer) { } private void onDuplicateOffer(OpenOfferListItem item) { - try { - PortfolioUtil.duplicateOffer(navigation, item.getOffer().getOfferPayloadBase()); - } catch (NullPointerException e) { - log.warn("Unable to get offerPayload - {}", e.toString()); + if (item == null || item.getOffer().getOfferPayloadBase() == null) { + return; } + PortfolioUtil.duplicateOffer(navigation, item.getOffer().getOfferPayloadBase()); } - private void onCloneOffer(OpenOffer openOffer) { + private void onCloneOffer(OpenOfferListItem item) { + if (item == null) { + return; + } + OpenOffer openOffer = item.getOpenOffer(); if (openOffer == null || openOffer.getOffer() == null || openOffer.getOffer().getOfferPayload().isEmpty()) { return; } @@ -460,7 +465,7 @@ private void onCloneOffer(OpenOffer openOffer) { OfferPayload sourceOfferPayload = offer.getOfferPayload().get(); log.info("Clone offerPayload with shared maker fee: {}", sourceOfferPayload.getId()); String newOfferId = getRandomOfferId(); - OfferPayload duplicatedOfferPayload = new OfferPayload(newOfferId, + OfferPayload clonedOfferPayload = new OfferPayload(newOfferId, new Date().getTime(), sourceOfferPayload.getOwnerNodeAddress(), sourceOfferPayload.getPubKeyRing(), @@ -498,7 +503,7 @@ private void onCloneOffer(OpenOffer openOffer) { sourceOfferPayload.getHashOfChallenge(), sourceOfferPayload.getExtraDataMap(), sourceOfferPayload.getProtocolVersion()); - Offer clonedOffer = new Offer(duplicatedOfferPayload); + Offer clonedOffer = new Offer(clonedOfferPayload); clonedOffer.setPriceFeedService(priceFeedService); offer.setState(Offer.State.OFFER_FEE_PAID); openOfferManager.placeOffer(clonedOffer, @@ -508,7 +513,7 @@ private void onCloneOffer(OpenOffer openOffer) { openOffer.getTriggerPrice(), transaction -> { }, - log::error); + errorMessage -> new Popup().warning(errorMessage).show()); } private void setOfferIdColumnCellFactory() { @@ -516,19 +521,23 @@ private void setOfferIdColumnCellFactory() { offerIdColumn.getStyleClass().addAll("number-column", "first-column"); offerIdColumn.setCellFactory( new Callback<>() { - @Override public TableCell call(TableColumn column) { return new TableCell<>() { - private HyperlinkWithIcon field; + private HyperlinkWithIcon hyperlinkWithIcon; @Override public void updateItem(final OpenOfferListItem item, boolean empty) { super.updateItem(item, empty); if (item != null && !empty) { - field = new HyperlinkWithIcon(item.getOffer().getShortId()); - field.setOnAction(event -> { + hyperlinkWithIcon = new HyperlinkWithIcon(item.getOffer().getShortId()); + if (item.isNotPublished()) { + // getStyleClass().add("offer-disabled"); does not work with hyperlinkWithIcon;-( + hyperlinkWithIcon.setStyle("-fx-text-fill: -bs-color-gray-3;"); + hyperlinkWithIcon.getIcon().setOpacity(0.2); + } + hyperlinkWithIcon.setOnAction(event -> { if (item.getOffer().isBsqSwapOffer()) { bsqSwapOfferDetailsWindow.show(item.getOffer()); } else { @@ -536,12 +545,12 @@ public void updateItem(final OpenOfferListItem item, boolean empty) { } }); - field.setTooltip(new Tooltip(Res.get("tooltip.openPopupForDetails"))); - setGraphic(field); + hyperlinkWithIcon.setTooltip(new Tooltip(Res.get("tooltip.openPopupForDetails"))); + setGraphic(hyperlinkWithIcon); } else { setGraphic(null); - if (field != null) - field.setOnAction(null); + if (hyperlinkWithIcon != null) + hyperlinkWithIcon.setOnAction(null); } } }; @@ -549,6 +558,71 @@ public void updateItem(final OpenOfferListItem item, boolean empty) { }); } + private void setMakerFeeTxIdColumnCellFactory() { + makerFeeTxIdColumn.setCellValueFactory((offerListItem) -> new ReadOnlyObjectWrapper<>(offerListItem.getValue())); + makerFeeTxIdColumn.setCellFactory( + new Callback<>() { + @Override + public TableCell call( + TableColumn column) { + return new TableCell<>() { + private HyperlinkWithIcon hyperlinkWithIcon = null; + + @Override + public void updateItem(final OpenOfferListItem item, boolean empty) { + super.updateItem(item, empty); + if (item != null && !empty) { + hyperlinkWithIcon = new HyperlinkWithIcon(item.getMakerFeeTxId()); + if (item.isNotPublished()) { + // getStyleClass().add("offer-disabled"); does not work with hyperlinkWithIcon ;-( + hyperlinkWithIcon.setStyle("-fx-text-fill: -bs-color-gray-3;"); + hyperlinkWithIcon.getIcon().setOpacity(0.2); + } + hyperlinkWithIcon.setOnAction(event -> GUIUtil.openTxInBlockExplorer(item.getMakerFeeTxId())); + if (openOfferManager.hasOfferSharedMakerFee(item.getOpenOffer())) { + hyperlinkWithIcon.setTooltip(new Tooltip(Res.get("offerbook.clonedOffer.info"))); + } else { + hyperlinkWithIcon.setTooltip(new Tooltip(Res.get("offerbook.nonClonedOffer.info"))); + } + setGraphic(hyperlinkWithIcon); + } else { + setGraphic(null); + if (hyperlinkWithIcon != null) { + hyperlinkWithIcon.setOnAction(null); + } + } + } + }; + + + /*return new TableCell<>() { + @Override + public void updateItem(final OpenOfferListItem item, boolean empty) { + super.updateItem(item, empty); + getStyleClass().removeAll("offer-disabled"); + if (item != null) { + if (item.isNotPublished()) { + getStyleClass().add("offer-disabled"); + } + Label label = new AutoTooltipLabel(item.getMakerFeeTxId()); + log.error("{} {}",openOfferManager.hasOfferSharedMakerFee(item.getOpenOffer()),item.getOpenOffer().getId()); + if (openOfferManager.hasOfferSharedMakerFee(item.getOpenOffer())) { + Text icon = getRegularIconForLabel(MaterialDesignIcon.CALENDAR_QUESTION, label); + label.setContentDisplay(ContentDisplay.LEFT); + Tooltip.install(icon, new Tooltip(Res.get("offerbook.sharedMakerFeeTxId"))); + } else { + label.setGraphic(null); + } + setGraphic(label); + } else { + setGraphic(null); + } + } + };*/ + } + }); + } + private void setDateColumnCellFactory() { dateColumn.setCellValueFactory((openOfferListItem) -> new ReadOnlyObjectWrapper<>(openOfferListItem.getValue())); dateColumn.setCellFactory( @@ -676,36 +750,6 @@ public void updateItem(final OpenOfferListItem item, boolean empty) { }); } - private void setGroupColumnCellFactory() { - groupColumn.setCellValueFactory((offerListItem) -> new ReadOnlyObjectWrapper<>(offerListItem.getValue())); - groupColumn.setCellFactory( - new Callback<>() { - @Override - public TableCell call( - TableColumn column) { - return new TableCell<>() { - @Override - public void updateItem(final OpenOfferListItem item, boolean empty) { - super.updateItem(item, empty); - getStyleClass().removeAll("offer-disabled"); - if (item != null) { - if (item.isNotPublished()) getStyleClass().add("offer-disabled"); - Label label = new AutoTooltipLabel(item.getOcoGroupForDisplay()); - if (openOfferManager.cannotActivateOffer(item.getOpenOffer().getOffer())) { - Text icon = getRegularIconForLabel(MaterialDesignIcon.EYE_OFF, label, "opaque-icon"); - label.setContentDisplay(ContentDisplay.RIGHT); - Tooltip.install(icon, new Tooltip(Res.get("offerbook.toEnableOffer"))); - } - setGraphic(label); - } else { - setGraphic(null); - } - } - }; - } - }); - } - private void setVolumeColumnCellFactory() { volumeColumn.setCellValueFactory((offer) -> new ReadOnlyObjectWrapper<>(offer.getValue())); volumeColumn.setCellFactory( From 6036da352b13737c359d7878db92453a33712d6e Mon Sep 17 00:00:00 2001 From: HenrikJannsen Date: Thu, 4 May 2023 20:25:11 +0700 Subject: [PATCH 11/31] Add cloneItemColumn Hide trigger price column if none is in list Signed-off-by: HenrikJannsen --- .../portfolio/openoffer/OpenOffersView.fxml | 1 + .../portfolio/openoffer/OpenOffersView.java | 53 +++++++++++++++++-- 2 files changed, 50 insertions(+), 4 deletions(-) diff --git a/desktop/src/main/java/bisq/desktop/main/portfolio/openoffer/OpenOffersView.fxml b/desktop/src/main/java/bisq/desktop/main/portfolio/openoffer/OpenOffersView.fxml index 22f69af71fe..2a7d5f2f380 100644 --- a/desktop/src/main/java/bisq/desktop/main/portfolio/openoffer/OpenOffersView.fxml +++ b/desktop/src/main/java/bisq/desktop/main/portfolio/openoffer/OpenOffersView.fxml @@ -55,6 +55,7 @@ + diff --git a/desktop/src/main/java/bisq/desktop/main/portfolio/openoffer/OpenOffersView.java b/desktop/src/main/java/bisq/desktop/main/portfolio/openoffer/OpenOffersView.java index c13ce344d06..df1a056f2a2 100644 --- a/desktop/src/main/java/bisq/desktop/main/portfolio/openoffer/OpenOffersView.java +++ b/desktop/src/main/java/bisq/desktop/main/portfolio/openoffer/OpenOffersView.java @@ -127,7 +127,8 @@ public String toString() { @FXML TableColumn priceColumn, deviationColumn, amountColumn, volumeColumn, marketColumn, directionColumn, dateColumn, offerIdColumn, deactivateItemColumn, makerFeeTxIdColumn, - removeItemColumn, editItemColumn, triggerPriceColumn, triggerIconColumn, paymentMethodColumn, duplicateItemColumn; + removeItemColumn, editItemColumn, triggerPriceColumn, triggerIconColumn, paymentMethodColumn, duplicateItemColumn, + cloneItemColumn; @FXML FilterBox filterBox; @FXML @@ -182,6 +183,7 @@ public void initialize() { deactivateItemColumn.setGraphic(new AutoTooltipLabel(ColumnNames.STATUS.toString())); editItemColumn.setText(""); duplicateItemColumn.setText(""); + cloneItemColumn.setText(""); removeItemColumn.setText(""); setOfferIdColumnCellFactory(); @@ -199,6 +201,7 @@ public void initialize() { setTriggerIconColumnCellFactory(); setTriggerPriceColumnCellFactory(); setDuplicateColumnCellFactory(); + setCloneColumnCellFactory(); setRemoveColumnCellFactory(); tableView.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY); @@ -255,7 +258,8 @@ public void initialize() { c.next(); if (c.wasAdded() || c.wasRemoved()) { updateNumberOfOffers(); - updateMakerFeeTxIdColumn(); + updateMakerFeeTxIdColumnVisibility(); + updateTriggerColumnVisibility(); } }; } @@ -270,7 +274,8 @@ protected void activate() { filterBox.initializeWithCallback(filteredList, tableView, this::updateNumberOfOffers); filterBox.activate(); - updateMakerFeeTxIdColumn(); + updateMakerFeeTxIdColumnVisibility(); + updateTriggerColumnVisibility(); updateSelectToggleButtonState(); selectToggleButton.setOnAction(event -> { @@ -325,12 +330,18 @@ private void updateNumberOfOffers() { numItems.setText(Res.get("shared.numItemsLabel", sortedList.size())); } - private void updateMakerFeeTxIdColumn() { + private void updateMakerFeeTxIdColumnVisibility() { makerFeeTxIdColumn.setVisible(model.dataModel.getList().stream() .collect(Collectors.groupingBy(OpenOfferListItem::getMakerFeeTxId, Collectors.counting())) .values().stream().anyMatch(i -> i > 1)); } + private void updateTriggerColumnVisibility() { + triggerIconColumn.setVisible(model.dataModel.getList().stream() + .mapToLong(item -> item.getOpenOffer().getTriggerPrice()) + .sum() > 0); + } + @Override protected void deactivate() { sortedList.comparatorProperty().unbind(); @@ -968,6 +979,40 @@ public void updateItem(final OpenOfferListItem item, boolean empty) { }); } + private void setCloneColumnCellFactory() { + cloneItemColumn.getStyleClass().add("avatar-column"); + cloneItemColumn.setCellValueFactory((offerListItem) -> new ReadOnlyObjectWrapper<>(offerListItem.getValue())); + cloneItemColumn.setCellFactory( + new Callback<>() { + @Override + public TableCell call(TableColumn column) { + return new TableCell<>() { + Button button; + + @Override + public void updateItem(final OpenOfferListItem item, boolean empty) { + super.updateItem(item, empty); + + if (item != null && !empty) { + if (button == null) { + button = getRegularIconButton(MaterialDesignIcon.BOX_SHADOW); + button.setTooltip(new Tooltip(Res.get("shared.cloneOffer"))); + setGraphic(button); + } + button.setOnAction(event -> onCloneOffer(item)); + } else { + setGraphic(null); + if (button != null) { + button.setOnAction(null); + button = null; + } + } + } + }; + } + }); + } + private void setTriggerIconColumnCellFactory() { triggerIconColumn.setCellValueFactory((offerListItem) -> new ReadOnlyObjectWrapper<>(offerListItem.getValue())); triggerIconColumn.setCellFactory( From 8b91ed7a00ecf5d3b833b23887c18784da1166d5 Mon Sep 17 00:00:00 2001 From: HenrikJannsen Date: Fri, 5 May 2023 09:33:35 +0700 Subject: [PATCH 12/31] Only reset if there are no other shared maker fee offers Signed-off-by: HenrikJannsen --- .../main/java/bisq/core/offer/OpenOfferManager.java | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/core/src/main/java/bisq/core/offer/OpenOfferManager.java b/core/src/main/java/bisq/core/offer/OpenOfferManager.java index 0dfce513de8..34023303147 100644 --- a/core/src/main/java/bisq/core/offer/OpenOfferManager.java +++ b/core/src/main/java/bisq/core/offer/OpenOfferManager.java @@ -610,8 +610,12 @@ private void onRemoved(OpenOffer openOffer, ResultHandler resultHandler, Offer o // For that reason we also do not add BSQ offers. if (getOpenOffersByMakerFeeTxId(offer.getOfferFeePaymentTxId()).isEmpty()) { closedTradableManager.add(openOffer); + + // We only reset if there are no other offers with the shared maker fee as otherwise the + // address in the addressEntry would become available while it's still RESERVED_FOR_TRADE + // for the remaining offers. + btcWalletService.resetAddressEntriesForOpenOffer(offer.getId()); } - btcWalletService.resetAddressEntriesForOpenOffer(offer.getId()); } log.info("onRemoved offerId={}", offer.getId()); resultHandler.handleResult(); @@ -877,7 +881,7 @@ private void sendAckMessage(OfferAvailabilityRequest message, result, errorMessage); - final NodeAddress takersNodeAddress = sender; + NodeAddress takersNodeAddress = sender; PubKeyRing takersPubKeyRing = message.getPubKeyRing(); log.info("Send AckMessage for OfferAvailabilityRequest to peer {} with offerId {} and sourceUid {}", takersNodeAddress, offerId, ackMessage.getSourceUid()); @@ -1209,11 +1213,10 @@ private boolean preventedFromPublishing(OpenOffer openOffer) { } private Set getOpenOffersByMakerFeeTxId(String makerFeeTxId) { - Set collect = openOffers.stream() + return openOffers.stream() .filter(openOffer -> !openOffer.getOffer().isBsqSwapOffer() && makerFeeTxId != null && makerFeeTxId.equals(openOffer.getOffer().getOfferFeePaymentTxId())) .collect(Collectors.toSet()); - return collect; } } From a621973eed797e8ef13b73fc4c89d119cba6e611 Mon Sep 17 00:00:00 2001 From: HenrikJannsen Date: Fri, 5 May 2023 09:41:05 +0700 Subject: [PATCH 13/31] Add translation string Signed-off-by: HenrikJannsen --- core/src/main/java/bisq/core/offer/OpenOfferManager.java | 4 ++-- core/src/main/resources/i18n/displayStrings.properties | 3 +++ 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/core/src/main/java/bisq/core/offer/OpenOfferManager.java b/core/src/main/java/bisq/core/offer/OpenOfferManager.java index 34023303147..94eb41dc1b3 100644 --- a/core/src/main/java/bisq/core/offer/OpenOfferManager.java +++ b/core/src/main/java/bisq/core/offer/OpenOfferManager.java @@ -468,8 +468,7 @@ public void activateOpenOffer(OpenOffer openOffer, } if (cannotActivateOffer(openOffer.getOffer())) { - errorMessageHandler.handleErrorMessage("This cloned offer with shared maker fee cannot be activated because it uses the same payment method " + - "and currency as another active offer."); + errorMessageHandler.handleErrorMessage(Res.get("offerbook.cannotActivate.info")); return; } @@ -640,6 +639,7 @@ public void closeOpenOffer(Offer offer) { } else { // We use CANCELED for the offers which have shared maker fee but have not been taken for the trade. openOffer.setState(OpenOffer.State.CANCELED); + // We need to reset now those entries as well btcWalletService.resetAddressEntriesForOpenOffer(openOffer.getId()); } offerBookService.removeOffer(openOffer.getOffer().getOfferPayloadBase(), diff --git a/core/src/main/resources/i18n/displayStrings.properties b/core/src/main/resources/i18n/displayStrings.properties index 95283c51683..bf36034e6d8 100644 --- a/core/src/main/resources/i18n/displayStrings.properties +++ b/core/src/main/resources/i18n/displayStrings.properties @@ -368,6 +368,9 @@ offerbook.xmrAutoConf=Is auto-confirm enabled offerbook.clonedOffer.info=This is a cloned offer with shared maker fee transaction ID offerbook.nonClonedOffer.info=Regular offer without shared maker fee transaction ID +offerbook.cannotActivate.info=This cloned offer with shared maker fee cannot be activated because it uses \ + the same payment method and currency as another active offer. You need to edit the offer and change the \ + payment method or currency or deactivate the offer which has the same payment method and currency. offerbook.timeSinceSigning.help=When you successfully complete a trade with a peer who has a signed payment account, your payment account is signed.\n\ {0} days later, the initial limit of {1} is lifted and your account can sign other peers'' payment accounts. From 384173c8949b0ded2a077c7205c210789c180845 Mon Sep 17 00:00:00 2001 From: HenrikJannsen Date: Fri, 5 May 2023 10:01:57 +0700 Subject: [PATCH 14/31] Rename translation string Signed-off-by: HenrikJannsen --- core/src/main/resources/i18n/displayStrings.properties | 2 +- .../bisq/desktop/main/portfolio/openoffer/OpenOffersView.java | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/core/src/main/resources/i18n/displayStrings.properties b/core/src/main/resources/i18n/displayStrings.properties index bf36034e6d8..369f6c3a1da 100644 --- a/core/src/main/resources/i18n/displayStrings.properties +++ b/core/src/main/resources/i18n/displayStrings.properties @@ -100,7 +100,6 @@ shared.removeOffer=Remove offer shared.dontRemoveOffer=Don't remove offer shared.editOffer=Edit offer shared.duplicateOffer=Duplicate offer -shared.cloneOffer=Clone offer (with shared maker fee) shared.openLargeQRWindow=Open large QR code window shared.chooseTradingAccount=Choose trading account shared.faq=Visit FAQ page @@ -366,6 +365,7 @@ offerbook.timeSinceSigning.tooltip.checkmark.wait=wait a minimum of {0} days offerbook.timeSinceSigning.tooltip.learnMore=Learn more offerbook.xmrAutoConf=Is auto-confirm enabled +offerbook.cloneOffer=Clone offer (with shared maker fee) offerbook.clonedOffer.info=This is a cloned offer with shared maker fee transaction ID offerbook.nonClonedOffer.info=Regular offer without shared maker fee transaction ID offerbook.cannotActivate.info=This cloned offer with shared maker fee cannot be activated because it uses \ diff --git a/desktop/src/main/java/bisq/desktop/main/portfolio/openoffer/OpenOffersView.java b/desktop/src/main/java/bisq/desktop/main/portfolio/openoffer/OpenOffersView.java index df1a056f2a2..0f1aaad9b18 100644 --- a/desktop/src/main/java/bisq/desktop/main/portfolio/openoffer/OpenOffersView.java +++ b/desktop/src/main/java/bisq/desktop/main/portfolio/openoffer/OpenOffersView.java @@ -232,7 +232,7 @@ public void initialize() { duplicateOfferMenuItem.setOnAction((event) -> onDuplicateOffer(row.getItem())); rowMenu.getItems().add(duplicateOfferMenuItem); - MenuItem cloneOfferMenuItem = new MenuItem(Res.get("shared.cloneOffer")); + MenuItem cloneOfferMenuItem = new MenuItem(Res.get("offerbook.cloneOffer")); cloneOfferMenuItem.setOnAction((event) -> onCloneOffer(row.getItem())); rowMenu.getItems().add(cloneOfferMenuItem); @@ -996,7 +996,7 @@ public void updateItem(final OpenOfferListItem item, boolean empty) { if (item != null && !empty) { if (button == null) { button = getRegularIconButton(MaterialDesignIcon.BOX_SHADOW); - button.setTooltip(new Tooltip(Res.get("shared.cloneOffer"))); + button.setTooltip(new Tooltip(Res.get("offerbook.cloneOffer"))); setGraphic(button); } button.setOnAction(event -> onCloneOffer(item)); From 4ae89e748c8f5f1d5bb33ddb751de712dfab489e Mon Sep 17 00:00:00 2001 From: HenrikJannsen Date: Fri, 5 May 2023 11:11:58 +0700 Subject: [PATCH 15/31] Use link icon for cloned offers Signed-off-by: HenrikJannsen --- .../bisq/core/offer/OpenOfferManager.java | 4 +- .../resources/i18n/displayStrings.properties | 17 ++++- desktop/src/main/java/bisq/desktop/bisq.css | 4 ++ .../portfolio/openoffer/OpenOffersView.fxml | 2 +- .../portfolio/openoffer/OpenOffersView.java | 67 ++++++++----------- .../src/main/java/bisq/desktop/theme-dark.css | 4 ++ .../java/bisq/desktop/util/FormBuilder.java | 10 +-- 7 files changed, 58 insertions(+), 50 deletions(-) diff --git a/core/src/main/java/bisq/core/offer/OpenOfferManager.java b/core/src/main/java/bisq/core/offer/OpenOfferManager.java index 94eb41dc1b3..d28260cb0d0 100644 --- a/core/src/main/java/bisq/core/offer/OpenOfferManager.java +++ b/core/src/main/java/bisq/core/offer/OpenOfferManager.java @@ -463,12 +463,12 @@ public void activateOpenOffer(OpenOffer openOffer, ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) { if (offersToBeEdited.containsKey(openOffer.getId())) { - errorMessageHandler.handleErrorMessage("You can't activate an offer that is currently edited."); + errorMessageHandler.handleErrorMessage(Res.get("offerbook.cannotActivateEditedOffer.warning")); return; } if (cannotActivateOffer(openOffer.getOffer())) { - errorMessageHandler.handleErrorMessage(Res.get("offerbook.cannotActivate.info")); + errorMessageHandler.handleErrorMessage(Res.get("offerbook.cannotActivate.warning")); return; } diff --git a/core/src/main/resources/i18n/displayStrings.properties b/core/src/main/resources/i18n/displayStrings.properties index 369f6c3a1da..9a50123d9c4 100644 --- a/core/src/main/resources/i18n/displayStrings.properties +++ b/core/src/main/resources/i18n/displayStrings.properties @@ -366,11 +366,22 @@ offerbook.timeSinceSigning.tooltip.learnMore=Learn more offerbook.xmrAutoConf=Is auto-confirm enabled offerbook.cloneOffer=Clone offer (with shared maker fee) -offerbook.clonedOffer.info=This is a cloned offer with shared maker fee transaction ID -offerbook.nonClonedOffer.info=Regular offer without shared maker fee transaction ID -offerbook.cannotActivate.info=This cloned offer with shared maker fee cannot be activated because it uses \ +offerbook.clonedOffer.tooltip=This is a cloned offer with shared maker fee transaction ID.\n\Maker fee transaction ID: {0} +offerbook.nonClonedOffer.tooltip=Regular offer without shared maker fee transaction ID.\n\Maker fee transaction ID: {0} +offerbook.cannotActivate.warning=This cloned offer with shared maker fee cannot be activated because it uses \ the same payment method and currency as another active offer. You need to edit the offer and change the \ payment method or currency or deactivate the offer which has the same payment method and currency. +offerbook.cannotActivateEditedOffer.warning=You can't activate an offer that is currently edited. +offerbook.clonedOffer.info=By cloning an offer one creates a copy of the given offer with a new offer ID but using the same \ + maker fee transaction ID.\n\n\ + This means there is no extra maker fee needed to get paid and the funds reserved for that offer can \ + be re-used by the cloned offers. This reduces the liquidity requirements for market makers and allows them to post the \ + same offer in different markets or with different payment methods.\n\n\ + As a consequence if one of the offers sharing the same maker fee transaction is taken all the other offers \ + will get closed as well because the transaction output of that maker fee transaction is spent and would render the \ + other offers invalid. \n\n\ + This feature requires to use the same trade amount and security deposit and is only permitted for offers with different \ + payment methods or currencies. offerbook.timeSinceSigning.help=When you successfully complete a trade with a peer who has a signed payment account, your payment account is signed.\n\ {0} days later, the initial limit of {1} is lifted and your account can sign other peers'' payment accounts. diff --git a/desktop/src/main/java/bisq/desktop/bisq.css b/desktop/src/main/java/bisq/desktop/bisq.css index 02f81d5c992..a082485fbcc 100644 --- a/desktop/src/main/java/bisq/desktop/bisq.css +++ b/desktop/src/main/java/bisq/desktop/bisq.css @@ -832,6 +832,10 @@ tree-table-view:focused { -fx-text-fill: -bs-rd-error-red; } +.icon { + -fx-fill: -bs-text-color; +} + .opaque-icon { -fx-fill: -bs-color-gray-bbb; -fx-opacity: 1; diff --git a/desktop/src/main/java/bisq/desktop/main/portfolio/openoffer/OpenOffersView.fxml b/desktop/src/main/java/bisq/desktop/main/portfolio/openoffer/OpenOffersView.fxml index 2a7d5f2f380..3bda1667aca 100644 --- a/desktop/src/main/java/bisq/desktop/main/portfolio/openoffer/OpenOffersView.fxml +++ b/desktop/src/main/java/bisq/desktop/main/portfolio/openoffer/OpenOffersView.fxml @@ -41,7 +41,7 @@ - + diff --git a/desktop/src/main/java/bisq/desktop/main/portfolio/openoffer/OpenOffersView.java b/desktop/src/main/java/bisq/desktop/main/portfolio/openoffer/OpenOffersView.java index 0f1aaad9b18..a28d2427649 100644 --- a/desktop/src/main/java/bisq/desktop/main/portfolio/openoffer/OpenOffersView.java +++ b/desktop/src/main/java/bisq/desktop/main/portfolio/openoffer/OpenOffersView.java @@ -391,7 +391,7 @@ private void onActivateOpenOffer(OpenOffer openOffer) { () -> log.debug("Activate offer was successful"), (message) -> { log.error(message); - new Popup().warning(Res.get("offerbook.activateOffer.failed", message)).show(); + new Popup().warning(message).show(); }); updateSelectToggleButtonState(); } @@ -467,6 +467,20 @@ private void onCloneOffer(OpenOfferListItem item) { if (item == null) { return; } + + String key = "clonedOfferInfo"; + if (DontShowAgainLookup.showAgain(key)) { + new Popup().backgroundInfo(Res.get("offerbook.clonedOffer.info")) + .useIUnderstandButton() + .dontShowAgainId(key) + .onClose(() -> doCloneOffer(item)) + .show(); + } else { + doCloneOffer(item); + } + } + + private void doCloneOffer(OpenOfferListItem item) { OpenOffer openOffer = item.getOpenOffer(); if (openOffer == null || openOffer.getOffer() == null || openOffer.getOffer().getOfferPayload().isEmpty()) { return; @@ -576,60 +590,35 @@ private void setMakerFeeTxIdColumnCellFactory() { @Override public TableCell call( TableColumn column) { - return new TableCell<>() { - private HyperlinkWithIcon hyperlinkWithIcon = null; + return new TableCell<>() { @Override public void updateItem(final OpenOfferListItem item, boolean empty) { super.updateItem(item, empty); - if (item != null && !empty) { - hyperlinkWithIcon = new HyperlinkWithIcon(item.getMakerFeeTxId()); - if (item.isNotPublished()) { - // getStyleClass().add("offer-disabled"); does not work with hyperlinkWithIcon ;-( - hyperlinkWithIcon.setStyle("-fx-text-fill: -bs-color-gray-3;"); - hyperlinkWithIcon.getIcon().setOpacity(0.2); - } - hyperlinkWithIcon.setOnAction(event -> GUIUtil.openTxInBlockExplorer(item.getMakerFeeTxId())); + getStyleClass().removeAll("offer-disabled"); + if (item != null) { + + Label label = new Label(item.getMakerFeeTxId()); + Text icon; if (openOfferManager.hasOfferSharedMakerFee(item.getOpenOffer())) { - hyperlinkWithIcon.setTooltip(new Tooltip(Res.get("offerbook.clonedOffer.info"))); + icon = getRegularIconForLabel(MaterialDesignIcon.LINK, label, "icon"); + setTooltip(new Tooltip(Res.get("offerbook.clonedOffer.tooltip", item.getMakerFeeTxId()))); } else { - hyperlinkWithIcon.setTooltip(new Tooltip(Res.get("offerbook.nonClonedOffer.info"))); + icon = getRegularIconForLabel(MaterialDesignIcon.LINK_OFF, label, "icon"); + setTooltip(new Tooltip(Res.get("offerbook.nonClonedOffer.tooltip", item.getMakerFeeTxId()))); } - setGraphic(hyperlinkWithIcon); - } else { - setGraphic(null); - if (hyperlinkWithIcon != null) { - hyperlinkWithIcon.setOnAction(null); - } - } - } - }; + icon.setVisible(!item.getOffer().isBsqSwapOffer()); - - /*return new TableCell<>() { - @Override - public void updateItem(final OpenOfferListItem item, boolean empty) { - super.updateItem(item, empty); - getStyleClass().removeAll("offer-disabled"); - if (item != null) { if (item.isNotPublished()) { getStyleClass().add("offer-disabled"); - } - Label label = new AutoTooltipLabel(item.getMakerFeeTxId()); - log.error("{} {}",openOfferManager.hasOfferSharedMakerFee(item.getOpenOffer()),item.getOpenOffer().getId()); - if (openOfferManager.hasOfferSharedMakerFee(item.getOpenOffer())) { - Text icon = getRegularIconForLabel(MaterialDesignIcon.CALENDAR_QUESTION, label); - label.setContentDisplay(ContentDisplay.LEFT); - Tooltip.install(icon, new Tooltip(Res.get("offerbook.sharedMakerFeeTxId"))); - } else { - label.setGraphic(null); + icon.setOpacity(0.2); } setGraphic(label); } else { setGraphic(null); } } - };*/ + }; } }); } diff --git a/desktop/src/main/java/bisq/desktop/theme-dark.css b/desktop/src/main/java/bisq/desktop/theme-dark.css index d5d31af370f..1be24acf044 100644 --- a/desktop/src/main/java/bisq/desktop/theme-dark.css +++ b/desktop/src/main/java/bisq/desktop/theme-dark.css @@ -557,3 +557,7 @@ -fx-text-fill: -bs-text-color; -fx-fill: -bs-text-color; } + +.icon { + -fx-fill: #fff; +} diff --git a/desktop/src/main/java/bisq/desktop/util/FormBuilder.java b/desktop/src/main/java/bisq/desktop/util/FormBuilder.java index f91a71d2210..ecab5c5f65b 100644 --- a/desktop/src/main/java/bisq/desktop/util/FormBuilder.java +++ b/desktop/src/main/java/bisq/desktop/util/FormBuilder.java @@ -2248,12 +2248,12 @@ public static void removeRowsFromGridPane(GridPane gridPane, int fromGridRow, in // Icons /////////////////////////////////////////////////////////////////////////////////////////// - public static Text getIconForLabel(GlyphIcons icon, String iconSize, Label label, String style) { + public static Text getIconForLabel(GlyphIcons icon, String iconSize, Label label, String styleClass) { if (icon.fontFamily().equals(MATERIAL_DESIGN_ICONS)) { final Text textIcon = MaterialDesignIconFactory.get().createIcon(icon, iconSize); textIcon.setOpacity(0.7); - if (style != null) { - textIcon.getStyleClass().add(style); + if (styleClass != null) { + textIcon.getStyleClass().add(styleClass); } label.setContentDisplay(ContentDisplay.LEFT); label.setGraphic(textIcon); @@ -2279,8 +2279,8 @@ public static Text getRegularIconForLabel(GlyphIcons icon, Label label) { return getRegularIconForLabel(icon, label, null); } - public static Text getRegularIconForLabel(GlyphIcons icon, Label label, String style) { - return getIconForLabel(icon, "1.231em", label, style); + public static Text getRegularIconForLabel(GlyphIcons icon, Label label, String styleClass) { + return getIconForLabel(icon, "1.231em", label, styleClass); } public static Text getIcon(GlyphIcons icon) { From 21672a1bd05ad1550563047011a9f641d135cfbb Mon Sep 17 00:00:00 2001 From: HenrikJannsen Date: Fri, 5 May 2023 13:34:03 +0700 Subject: [PATCH 16/31] Add clone offer tab Change flow of cloning an offer: We open the clone offer tab similar like the duplicate/edit offer tab. When clicking the clone button we create and publish the cloned offer. if the clone would not have changed the payment method/currency we show a popup and deactivate the offer. At editOffer we check if the offer is using a shared maker fee and if so we check if the edit triggered same payment method/currency. If so we show a popup and deactivate the offer. Signed-off-by: HenrikJannsen --- .../main/java/bisq/core/offer/OpenOffer.java | 4 - .../bisq/core/offer/OpenOfferManager.java | 9 +- .../resources/i18n/displayStrings.properties | 20 +- .../desktop/main/portfolio/PortfolioView.java | 72 ++++- .../cloneoffer/CloneOfferDataModel.java | 255 +++++++++++++++++ .../portfolio/cloneoffer/CloneOfferView.fxml | 24 ++ .../portfolio/cloneoffer/CloneOfferView.java | 257 ++++++++++++++++++ .../cloneoffer/CloneOfferViewModel.java | 128 +++++++++ .../editoffer/EditOfferDataModel.java | 35 ++- .../portfolio/editoffer/EditOfferView.java | 73 ++--- .../portfolio/openoffer/OpenOffersView.java | 98 ++----- 11 files changed, 841 insertions(+), 134 deletions(-) create mode 100644 desktop/src/main/java/bisq/desktop/main/portfolio/cloneoffer/CloneOfferDataModel.java create mode 100644 desktop/src/main/java/bisq/desktop/main/portfolio/cloneoffer/CloneOfferView.fxml create mode 100644 desktop/src/main/java/bisq/desktop/main/portfolio/cloneoffer/CloneOfferView.java create mode 100644 desktop/src/main/java/bisq/desktop/main/portfolio/cloneoffer/CloneOfferViewModel.java diff --git a/core/src/main/java/bisq/core/offer/OpenOffer.java b/core/src/main/java/bisq/core/offer/OpenOffer.java index 5dc8096b230..088a3e8f042 100644 --- a/core/src/main/java/bisq/core/offer/OpenOffer.java +++ b/core/src/main/java/bisq/core/offer/OpenOffer.java @@ -187,10 +187,6 @@ public boolean isDeactivated() { return state == State.DEACTIVATED; } - public boolean isActivated() { - return !isDeactivated(); - } - public boolean isCanceled() { return state == State.CANCELED; } diff --git a/core/src/main/java/bisq/core/offer/OpenOfferManager.java b/core/src/main/java/bisq/core/offer/OpenOfferManager.java index d28260cb0d0..d36fe92c956 100644 --- a/core/src/main/java/bisq/core/offer/OpenOfferManager.java +++ b/core/src/main/java/bisq/core/offer/OpenOfferManager.java @@ -417,7 +417,7 @@ public void placeOffer(Offer offer, model, transaction -> { OpenOffer openOffer = new OpenOffer(offer, triggerPrice); - if (isSharedMakerFee) { + if (isSharedMakerFee && cannotActivateOffer(offer)) { openOffer.setState(OpenOffer.State.DEACTIVATED); } addOpenOfferToList(openOffer); @@ -592,8 +592,6 @@ public void editOpenOfferCancel(OpenOffer openOffer, } else { resultHandler.handleResult(); } - } else { - errorMessageHandler.handleErrorMessage("Editing of offer can't be canceled as it is not edited."); } } @@ -658,11 +656,12 @@ public boolean cannotActivateOffer(Offer offer) { return openOffers.stream() .filter(openOffer -> !openOffer.getOffer().isBsqSwapOffer()) // We only handle non-BSQ offers .filter(openOffer -> !openOffer.getId().equals(offer.getId())) // our own offer gets skipped - .filter(OpenOffer::isActivated) // we only check with activated offers + .filter(openOffer -> !openOffer.isDeactivated()) // we only check with activated offers .anyMatch(openOffer -> // Offers which share our maker fee will get checked if they have the same payment method // and currency. - openOffer.getOffer().getOfferFeePaymentTxId().equals(offer.getOfferFeePaymentTxId()) && + openOffer.getOffer().getOfferFeePaymentTxId() != null && + openOffer.getOffer().getOfferFeePaymentTxId().equals(offer.getOfferFeePaymentTxId()) && openOffer.getOffer().getPaymentMethodId().equalsIgnoreCase(offer.getPaymentMethodId()) && openOffer.getOffer().getCounterCurrencyCode().equalsIgnoreCase(offer.getCounterCurrencyCode()) && openOffer.getOffer().getBaseCurrencyCode().equalsIgnoreCase(offer.getBaseCurrencyCode())); diff --git a/core/src/main/resources/i18n/displayStrings.properties b/core/src/main/resources/i18n/displayStrings.properties index 9a50123d9c4..60373194a58 100644 --- a/core/src/main/resources/i18n/displayStrings.properties +++ b/core/src/main/resources/i18n/displayStrings.properties @@ -369,7 +369,8 @@ offerbook.cloneOffer=Clone offer (with shared maker fee) offerbook.clonedOffer.tooltip=This is a cloned offer with shared maker fee transaction ID.\n\Maker fee transaction ID: {0} offerbook.nonClonedOffer.tooltip=Regular offer without shared maker fee transaction ID.\n\Maker fee transaction ID: {0} offerbook.cannotActivate.warning=This cloned offer with shared maker fee cannot be activated because it uses \ - the same payment method and currency as another active offer. You need to edit the offer and change the \ + the same payment method and currency as another active offer.\n\n\ + You need to edit the offer and change the \ payment method or currency or deactivate the offer which has the same payment method and currency. offerbook.cannotActivateEditedOffer.warning=You can't activate an offer that is currently edited. offerbook.clonedOffer.info=By cloning an offer one creates a copy of the given offer with a new offer ID but using the same \ @@ -638,7 +639,21 @@ editOffer.publishOffer=Publishing your offer. editOffer.failed=Editing of offer failed:\n{0} editOffer.success=Your offer has been successfully edited. editOffer.invalidDeposit=The buyer's security deposit is not within the constraints defined by the Bisq DAO and can no longer be edited. - +editOffer.openTabWarning=You have already the \"Edit Offer\" tab open. +editOffer.cannotActivateOffer=You have edited an offer which uses a shared maker fee with another offer and your edit \ + made the payment method and currency now the same as that of another active cloned offer. Your edited offer will be \ + deactivated because it is not permitted to publish 2 offers sharing the same maker fee with the same payment method \ + and currency.\n\n\ + You can edit the offer again at \"Portfolio/My open offers\" to fulfill the requirements to activate it. + +cloneOffer.clone=Clone offer +cloneOffer.publishOffer=Publishing cloned offer. +cloneOffer.success=Your offer has been successfully cloned. +cloneOffer.cannotActivateOffer=You have not changed the payment method or the currency. You still can clone the offer, but it will \ + be deactivated and not published.\n\n\ + You can edit the offer later again at \"Portfolio/My open offers\" to fulfill the requirements to activate it.\n\n\ + Do you still want to clone the offer? +cloneOffer.openTabWarning=You have already the \"Clone Offer\" tab open. #################################################################### # BSQ Swap offer @@ -673,6 +688,7 @@ portfolio.tab.bsqSwap=Unconfirmed BSQ swaps portfolio.tab.failed=Failed portfolio.tab.editOpenOffer=Edit offer portfolio.tab.duplicateOffer=Duplicate offer +portfolio.tab.cloneOpenOffer=Clone offer portfolio.context.notYourOffer=You can only duplicate offers where you were the maker. portfolio.closedTrades.deviation.help=Percentage price deviation from market diff --git a/desktop/src/main/java/bisq/desktop/main/portfolio/PortfolioView.java b/desktop/src/main/java/bisq/desktop/main/portfolio/PortfolioView.java index 46cc33712ce..619288cbb5a 100644 --- a/desktop/src/main/java/bisq/desktop/main/portfolio/PortfolioView.java +++ b/desktop/src/main/java/bisq/desktop/main/portfolio/PortfolioView.java @@ -23,7 +23,9 @@ import bisq.desktop.common.view.FxmlView; import bisq.desktop.common.view.View; import bisq.desktop.main.MainView; +import bisq.desktop.main.overlays.popups.Popup; import bisq.desktop.main.portfolio.bsqswaps.UnconfirmedBsqSwapsView; +import bisq.desktop.main.portfolio.cloneoffer.CloneOfferView; import bisq.desktop.main.portfolio.closedtrades.ClosedTradesView; import bisq.desktop.main.portfolio.duplicateoffer.DuplicateOfferView; import bisq.desktop.main.portfolio.editoffer.EditOfferView; @@ -58,7 +60,7 @@ public class PortfolioView extends ActivatableView { @FXML Tab openOffersTab, pendingTradesTab, closedTradesTab, bsqSwapTradesTab; - private Tab editOpenOfferTab, duplicateOfferTab; + private Tab editOpenOfferTab, duplicateOfferTab, cloneOpenOfferTab; private final Tab failedTradesTab = new Tab(Res.get("portfolio.tab.failed").toUpperCase()); private Tab currentTab; private Navigation.Listener navigationListener; @@ -71,7 +73,8 @@ public class PortfolioView extends ActivatableView { private final OpenOfferManager openOfferManager; private EditOfferView editOfferView; private DuplicateOfferView duplicateOfferView; - private boolean editOpenOfferViewOpen; + private CloneOfferView cloneOfferView; + private boolean editOpenOfferViewOpen, cloneOpenOfferViewOpen; private OpenOffer openOffer; private OpenOffersView openOffersView; private int initialTabCount = 0; @@ -116,13 +119,16 @@ else if (newValue == editOpenOfferTab) navigation.navigateTo(MainView.class, PortfolioView.class, EditOfferView.class); else if (newValue == duplicateOfferTab) { navigation.navigateTo(MainView.class, PortfolioView.class, DuplicateOfferView.class); + } else if (newValue == cloneOpenOfferTab) { + navigation.navigateTo(MainView.class, PortfolioView.class, CloneOfferView.class); } if (oldValue != null && oldValue == editOpenOfferTab) editOfferView.onTabSelected(false); if (oldValue != null && oldValue == duplicateOfferTab) duplicateOfferView.onTabSelected(false); - + if (oldValue != null && oldValue == cloneOpenOfferTab) + cloneOfferView.onTabSelected(false); }; tabListChangeListener = change -> { @@ -132,6 +138,8 @@ else if (newValue == duplicateOfferTab) { onEditOpenOfferRemoved(); if (removedTabs.size() == 1 && removedTabs.get(0).equals(duplicateOfferTab)) onDuplicateOfferRemoved(); + if (removedTabs.size() == 1 && removedTabs.get(0).equals(cloneOpenOfferTab)) + onCloneOpenOfferRemoved(); }; } @@ -154,6 +162,16 @@ private void onDuplicateOfferRemoved() { navigation.navigateTo(MainView.class, this.getClass(), OpenOffersView.class); } + private void onCloneOpenOfferRemoved() { + cloneOpenOfferViewOpen = false; + if (cloneOfferView != null) { + cloneOfferView.onClose(); + cloneOfferView = null; + } + + navigation.navigateTo(MainView.class, this.getClass(), OpenOffersView.class); + } + @Override protected void activate() { failedTradesManager.getObservableList().addListener((ListChangeListener) c -> { @@ -183,6 +201,9 @@ else if (root.getSelectionModel().getSelectedItem() == editOpenOfferTab) { } else if (root.getSelectionModel().getSelectedItem() == duplicateOfferTab) { navigation.navigateTo(MainView.class, PortfolioView.class, DuplicateOfferView.class); if (duplicateOfferView != null) duplicateOfferView.onTabSelected(true); + } else if (root.getSelectionModel().getSelectedItem() == cloneOpenOfferTab) { + navigation.navigateTo(MainView.class, PortfolioView.class, CloneOfferView.class); + if (cloneOfferView != null) cloneOfferView.onTabSelected(true); } } @@ -254,6 +275,28 @@ private void loadView(Class viewClass, @Nullable Object data) { view = viewLoader.load(OpenOffersView.class); selectOpenOffersView((OpenOffersView) view); } + } else if (view instanceof CloneOfferView) { + if (data instanceof OpenOffer) { + openOffer = (OpenOffer) data; + } + if (openOffer != null) { + if (cloneOfferView == null) { + cloneOfferView = (CloneOfferView) view; + cloneOfferView.applyOpenOffer(openOffer); + cloneOpenOfferTab = new Tab(Res.get("portfolio.tab.cloneOpenOffer").toUpperCase()); + cloneOfferView.setCloseHandler(() -> { + root.getTabs().remove(cloneOpenOfferTab); + }); + root.getTabs().add(cloneOpenOfferTab); + } + if (currentTab != cloneOpenOfferTab) + cloneOfferView.onTabSelected(true); + + currentTab = cloneOpenOfferTab; + } else { + view = viewLoader.load(OpenOffersView.class); + selectOpenOffersView((OpenOffersView) view); + } } currentTab.setContent(view.getRoot()); @@ -264,20 +307,35 @@ private void selectOpenOffersView(OpenOffersView view) { openOffersView = view; currentTab = openOffersTab; - OpenOfferActionHandler openOfferActionHandler = openOffer -> { + EditOpenOfferHandler editOpenOfferHandler = openOffer -> { if (!editOpenOfferViewOpen) { editOpenOfferViewOpen = true; PortfolioView.this.openOffer = openOffer; navigation.navigateTo(MainView.class, PortfolioView.this.getClass(), EditOfferView.class); } else { - log.error("You have already a \"Edit Offer\" tab open."); + new Popup().warning(Res.get("editOffer.openTabWarning")).show(); + } + }; + openOffersView.setEditOpenOfferHandler(editOpenOfferHandler); + + CloneOpenOfferHandler cloneOpenOfferHandler = openOffer -> { + if (!cloneOpenOfferViewOpen) { + cloneOpenOfferViewOpen = true; + PortfolioView.this.openOffer = openOffer; + navigation.navigateTo(MainView.class, PortfolioView.this.getClass(), CloneOfferView.class); + } else { + new Popup().warning(Res.get("cloneOffer.openTabWarning")).show(); } }; - openOffersView.setOpenOfferActionHandler(openOfferActionHandler); + openOffersView.setCloneOpenOfferHandler(cloneOpenOfferHandler); } - public interface OpenOfferActionHandler { + public interface EditOpenOfferHandler { void onEditOpenOffer(OpenOffer openOffer); } + + public interface CloneOpenOfferHandler { + void onCloneOpenOffer(OpenOffer openOffer); + } } diff --git a/desktop/src/main/java/bisq/desktop/main/portfolio/cloneoffer/CloneOfferDataModel.java b/desktop/src/main/java/bisq/desktop/main/portfolio/cloneoffer/CloneOfferDataModel.java new file mode 100644 index 00000000000..3764e7024b1 --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/main/portfolio/cloneoffer/CloneOfferDataModel.java @@ -0,0 +1,255 @@ +/* + * 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.portfolio.cloneoffer; + + +import bisq.desktop.Navigation; +import bisq.desktop.main.offer.bisq_v1.MutableOfferDataModel; + +import bisq.core.account.witness.AccountAgeWitnessService; +import bisq.core.btc.wallet.BsqWalletService; +import bisq.core.btc.wallet.BtcWalletService; +import bisq.core.btc.wallet.Restrictions; +import bisq.core.locale.CurrencyUtil; +import bisq.core.locale.TradeCurrency; +import bisq.core.offer.Offer; +import bisq.core.offer.OfferDirection; +import bisq.core.offer.OfferUtil; +import bisq.core.offer.OpenOffer; +import bisq.core.offer.OpenOfferManager; +import bisq.core.offer.bisq_v1.CreateOfferService; +import bisq.core.offer.bisq_v1.OfferPayload; +import bisq.core.payment.PaymentAccount; +import bisq.core.proto.persistable.CorePersistenceProtoResolver; +import bisq.core.provider.fee.FeeService; +import bisq.core.provider.price.PriceFeedService; +import bisq.core.trade.statistics.TradeStatisticsManager; +import bisq.core.user.Preferences; +import bisq.core.user.User; +import bisq.core.util.FormattingUtils; +import bisq.core.util.coin.CoinFormatter; +import bisq.core.util.coin.CoinUtil; + +import bisq.network.p2p.P2PService; + +import bisq.common.handlers.ErrorMessageHandler; +import bisq.common.handlers.ResultHandler; + +import com.google.inject.Inject; + +import javax.inject.Named; + +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; + +class CloneOfferDataModel extends MutableOfferDataModel { + + private final CorePersistenceProtoResolver corePersistenceProtoResolver; + private OpenOffer sourceOpenOffer; + + @Inject + CloneOfferDataModel(CreateOfferService createOfferService, + OpenOfferManager openOfferManager, + OfferUtil offerUtil, + BtcWalletService btcWalletService, + BsqWalletService bsqWalletService, + Preferences preferences, + User user, + P2PService p2PService, + PriceFeedService priceFeedService, + AccountAgeWitnessService accountAgeWitnessService, + FeeService feeService, + @Named(FormattingUtils.BTC_FORMATTER_KEY) CoinFormatter btcFormatter, + CorePersistenceProtoResolver corePersistenceProtoResolver, + TradeStatisticsManager tradeStatisticsManager, + Navigation navigation) { + + super(createOfferService, + openOfferManager, + offerUtil, + btcWalletService, + bsqWalletService, + preferences, + user, + p2PService, + priceFeedService, + accountAgeWitnessService, + feeService, + btcFormatter, + tradeStatisticsManager, + navigation); + this.corePersistenceProtoResolver = corePersistenceProtoResolver; + } + + public void reset() { + direction = null; + tradeCurrency = null; + tradeCurrencyCode.set(null); + useMarketBasedPrice.set(false); + amount.set(null); + minAmount.set(null); + price.set(null); + volume.set(null); + minVolume.set(null); + buyerSecurityDeposit.set(0); + paymentAccounts.clear(); + paymentAccount = null; + marketPriceMargin = 0; + } + + public void applyOpenOffer(OpenOffer openOffer) { + this.sourceOpenOffer = openOffer; + + Offer offer = openOffer.getOffer(); + direction = offer.getDirection(); + CurrencyUtil.getTradeCurrency(offer.getCurrencyCode()) + .ifPresent(c -> this.tradeCurrency = c); + tradeCurrencyCode.set(offer.getCurrencyCode()); + + PaymentAccount tmpPaymentAccount = user.getPaymentAccount(openOffer.getOffer().getMakerPaymentAccountId()); + Optional optionalTradeCurrency = CurrencyUtil.getTradeCurrency(openOffer.getOffer().getCurrencyCode()); + if (optionalTradeCurrency.isPresent() && tmpPaymentAccount != null) { + TradeCurrency selectedTradeCurrency = optionalTradeCurrency.get(); + this.paymentAccount = PaymentAccount.fromProto(tmpPaymentAccount.toProtoMessage(), corePersistenceProtoResolver); + if (paymentAccount.getSingleTradeCurrency() != null) + paymentAccount.setSingleTradeCurrency(selectedTradeCurrency); + else + paymentAccount.setSelectedTradeCurrency(selectedTradeCurrency); + } + + // If the security deposit got bounded because it was below the coin amount limit, it can be bigger + // by percentage than the restriction. We can't determine the percentage originally entered at offer + // creation, so just use the default value as it doesn't matter anyway. + double buyerSecurityDepositPercent = CoinUtil.getAsPercentPerBtc(offer.getBuyerSecurityDeposit(), offer.getAmount()); + if (buyerSecurityDepositPercent > Restrictions.getMaxBuyerSecurityDepositAsPercent() + && offer.getBuyerSecurityDeposit().value == Restrictions.getMinBuyerSecurityDepositAsCoin().value) + buyerSecurityDeposit.set(Restrictions.getDefaultBuyerSecurityDepositAsPercent()); + else + buyerSecurityDeposit.set(buyerSecurityDepositPercent); + + allowAmountUpdate = false; + } + + @Override + public boolean initWithData(OfferDirection direction, TradeCurrency tradeCurrency) { + try { + return super.initWithData(direction, tradeCurrency); + } catch (NullPointerException e) { + if (e.getMessage().contains("tradeCurrency")) { + throw new IllegalArgumentException("Offers of removed assets cannot be edited. You can only cancel it.", e); + } + return false; + } + } + + @Override + protected Set getUserPaymentAccounts() { + return Objects.requireNonNull(user.getPaymentAccounts()).stream() + .filter(account -> !account.getPaymentMethod().isBsqSwap()) + .collect(Collectors.toSet()); + } + + @Override + protected PaymentAccount getPreselectedPaymentAccount() { + return paymentAccount; + } + + public void populateData() { + Offer offer = sourceOpenOffer.getOffer(); + // Min amount need to be set before amount as if minAmount is null it would be set by amount + setMinAmount(offer.getMinAmount()); + setAmount(offer.getAmount()); + setPrice(offer.getPrice()); + setVolume(offer.getVolume()); + setUseMarketBasedPrice(offer.isUseMarketBasedPrice()); + setTriggerPrice(sourceOpenOffer.getTriggerPrice()); + if (offer.isUseMarketBasedPrice()) { + setMarketPriceMargin(offer.getMarketPriceMargin()); + } + } + + public void onCloneOffer(ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) { + Offer clonedOffer = createClonedOffer(); + openOfferManager.placeOffer(clonedOffer, + clonedOffer.getBuyerSecurityDeposit().getValue(), + false, + true, + sourceOpenOffer.getTriggerPrice(), + transaction -> resultHandler.handleResult(), + errorMessageHandler); + } + + private Offer createClonedOffer() { + Offer sourceOffer = sourceOpenOffer.getOffer(); + OfferPayload sourceOfferPayload = sourceOffer.getOfferPayload().orElseThrow(); + // We create a new offer based on our source offer and the edited fields in the UI + Offer editedOffer = createAndGetOffer(); + OfferPayload editedOfferPayload = editedOffer.getOfferPayload().orElseThrow(); + // We clone the edited offer but use the maker tx ID from the source offer + String sharedMakerTxId = sourceOfferPayload.getOfferFeePaymentTxId(); + OfferPayload clonedOfferPayload = new OfferPayload(editedOfferPayload.getId(), + editedOfferPayload.getDate(), + editedOfferPayload.getOwnerNodeAddress(), + editedOfferPayload.getPubKeyRing(), + editedOfferPayload.getDirection(), + editedOfferPayload.getPrice(), + editedOfferPayload.getMarketPriceMargin(), + editedOfferPayload.isUseMarketBasedPrice(), + editedOfferPayload.getAmount(), + editedOfferPayload.getMinAmount(), + editedOfferPayload.getBaseCurrencyCode(), + editedOfferPayload.getCounterCurrencyCode(), + editedOfferPayload.getArbitratorNodeAddresses(), + editedOfferPayload.getMediatorNodeAddresses(), + editedOfferPayload.getPaymentMethodId(), + editedOfferPayload.getMakerPaymentAccountId(), + sharedMakerTxId, + editedOfferPayload.getCountryCode(), + editedOfferPayload.getAcceptedCountryCodes(), + editedOfferPayload.getBankId(), + editedOfferPayload.getAcceptedBankIds(), + editedOfferPayload.getVersionNr(), + editedOfferPayload.getBlockHeightAtOfferCreation(), + editedOfferPayload.getTxFee(), + editedOfferPayload.getMakerFee(), + editedOfferPayload.isCurrencyForMakerFeeBtc(), + editedOfferPayload.getBuyerSecurityDeposit(), + editedOfferPayload.getSellerSecurityDeposit(), + editedOfferPayload.getMaxTradeLimit(), + editedOfferPayload.getMaxTradePeriod(), + editedOfferPayload.isUseAutoClose(), + editedOfferPayload.isUseReOpenAfterAutoClose(), + editedOfferPayload.getLowerClosePrice(), + editedOfferPayload.getUpperClosePrice(), + editedOfferPayload.isPrivateOffer(), + editedOfferPayload.getHashOfChallenge(), + editedOfferPayload.getExtraDataMap(), + editedOfferPayload.getProtocolVersion()); + Offer clonedOffer = new Offer(clonedOfferPayload); + clonedOffer.setPriceFeedService(priceFeedService); + clonedOffer.setState(Offer.State.OFFER_FEE_PAID); + return clonedOffer; + } + + public boolean cannotActivateOffer() { + Offer clonedOffer = createClonedOffer(); + return openOfferManager.cannotActivateOffer(clonedOffer); + } +} diff --git a/desktop/src/main/java/bisq/desktop/main/portfolio/cloneoffer/CloneOfferView.fxml b/desktop/src/main/java/bisq/desktop/main/portfolio/cloneoffer/CloneOfferView.fxml new file mode 100644 index 00000000000..72089b91891 --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/main/portfolio/cloneoffer/CloneOfferView.fxml @@ -0,0 +1,24 @@ + + + + + + + + diff --git a/desktop/src/main/java/bisq/desktop/main/portfolio/cloneoffer/CloneOfferView.java b/desktop/src/main/java/bisq/desktop/main/portfolio/cloneoffer/CloneOfferView.java new file mode 100644 index 00000000000..9dd2c43d572 --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/main/portfolio/cloneoffer/CloneOfferView.java @@ -0,0 +1,257 @@ +/* + * 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.portfolio.cloneoffer; + +import bisq.desktop.Navigation; +import bisq.desktop.common.view.FxmlView; +import bisq.desktop.components.AutoTooltipButton; +import bisq.desktop.components.BusyAnimation; +import bisq.desktop.main.offer.bisq_v1.MutableOfferView; +import bisq.desktop.main.overlays.popups.Popup; +import bisq.desktop.main.overlays.windows.OfferDetailsWindow; + +import bisq.core.locale.CurrencyUtil; +import bisq.core.locale.Res; +import bisq.core.offer.OpenOffer; +import bisq.core.payment.PaymentAccount; +import bisq.core.user.DontShowAgainLookup; +import bisq.core.user.Preferences; +import bisq.core.util.FormattingUtils; +import bisq.core.util.coin.BsqFormatter; +import bisq.core.util.coin.CoinFormatter; + +import bisq.common.util.Tuple4; + +import com.google.inject.Inject; + +import javax.inject.Named; + +import javafx.scene.control.Button; +import javafx.scene.control.Label; +import javafx.scene.image.ImageView; +import javafx.scene.layout.GridPane; +import javafx.scene.layout.HBox; + +import javafx.geometry.HPos; +import javafx.geometry.Insets; +import javafx.geometry.Pos; + +import javafx.collections.ObservableList; + +import static bisq.desktop.util.FormBuilder.addButtonBusyAnimationLabelAfterGroup; + +@FxmlView +public class CloneOfferView extends MutableOfferView { + + private BusyAnimation busyAnimation; + private Button cloneButton; + private Button cancelButton; + private Label spinnerInfoLabel; + + /////////////////////////////////////////////////////////////////////////////////////////// + // Constructor, lifecycle + /////////////////////////////////////////////////////////////////////////////////////////// + + @Inject + private CloneOfferView(CloneOfferViewModel model, + Navigation navigation, + Preferences preferences, + OfferDetailsWindow offerDetailsWindow, + @Named(FormattingUtils.BTC_FORMATTER_KEY) CoinFormatter btcFormatter, + BsqFormatter bsqFormatter) { + super(model, navigation, preferences, offerDetailsWindow, btcFormatter, bsqFormatter); + } + + @Override + protected void initialize() { + super.initialize(); + + addCloneGroup(); + renameAmountGroup(); + } + + private void renameAmountGroup() { + amountTitledGroupBg.setText(Res.get("editOffer.setPrice")); + } + + @Override + protected void doSetFocus() { + // Don't focus in any field before data was set + } + + @Override + protected void doActivate() { + super.doActivate(); + + addBindings(); + + hideOptionsGroup(); + + // Lock amount field as it would require bigger changes to support increased amount values. + amountTextField.setDisable(true); + amountBtcLabel.setDisable(true); + minAmountTextField.setDisable(true); + minAmountBtcLabel.setDisable(true); + volumeTextField.setDisable(true); + volumeCurrencyLabel.setDisable(true); + + // Workaround to fix margin on top of amount group + gridPane.setPadding(new Insets(-20, 25, -1, 25)); + + updatePriceToggle(); + updateElementsWithDirection(); + + model.isNextButtonDisabled.setValue(false); + cancelButton.setDisable(false); + + model.onInvalidateMarketPriceMargin(); + model.onInvalidatePrice(); + + // To force re-validation of payment account validation + onPaymentAccountsComboBoxSelected(); + } + + @Override + public void onClose() { + } + + @Override + protected void deactivate() { + super.deactivate(); + + removeBindings(); + } + + /////////////////////////////////////////////////////////////////////////////////////////// + // API + /////////////////////////////////////////////////////////////////////////////////////////// + + public void applyOpenOffer(OpenOffer openOffer) { + model.applyOpenOffer(openOffer); + + initWithData(openOffer.getOffer().getDirection(), + CurrencyUtil.getTradeCurrency(openOffer.getOffer().getCurrencyCode()).get(), + null); + + if (!model.isSecurityDepositValid()) { + new Popup().warning(Res.get("editOffer.invalidDeposit")) + .onClose(this::close) + .show(); + } + } + + /////////////////////////////////////////////////////////////////////////////////////////// + // Bindings, Listeners + /////////////////////////////////////////////////////////////////////////////////////////// + + private void addBindings() { + cloneButton.disableProperty().bind(model.isNextButtonDisabled); + } + + private void removeBindings() { + cloneButton.disableProperty().unbind(); + } + + @Override + protected ObservableList filterPaymentAccounts(ObservableList paymentAccounts) { + return paymentAccounts; + } + + /////////////////////////////////////////////////////////////////////////////////////////// + // Build UI elements + /////////////////////////////////////////////////////////////////////////////////////////// + + private void addCloneGroup() { + int tmpGridRow = 4; + Tuple4 tuple4 = addButtonBusyAnimationLabelAfterGroup(gridPane, tmpGridRow++, Res.get("cloneOffer.clone")); + + HBox hBox = tuple4.fourth; + hBox.setAlignment(Pos.CENTER_LEFT); + GridPane.setHalignment(hBox, HPos.LEFT); + + cloneButton = tuple4.first; + cloneButton.setMinHeight(40); + cloneButton.setPadding(new Insets(0, 20, 0, 20)); + cloneButton.setGraphicTextGap(10); + + busyAnimation = tuple4.second; + spinnerInfoLabel = tuple4.third; + + cancelButton = new AutoTooltipButton(Res.get("shared.cancel")); + cancelButton.setDefaultButton(false); + cancelButton.setOnAction(event -> close()); + hBox.getChildren().add(cancelButton); + + cloneButton.setOnAction(e -> { + cloneButton.requestFocus(); // fix issue #5460 (when enter key used, focus is wrong) + onClone(); + }); + } + + private void onClone() { + if (model.dataModel.cannotActivateOffer()) { + new Popup().warning(Res.get("cloneOffer.cannotActivateOffer")) + .actionButtonText(Res.get("shared.yes")) + .onAction(this::doClone) + .closeButtonText(Res.get("shared.no")) + .show(); + } else { + doClone(); + } + } + + private void doClone() { + if (model.isPriceInRange()) { + model.isNextButtonDisabled.setValue(true); + cancelButton.setDisable(true); + busyAnimation.play(); + spinnerInfoLabel.setText(Res.get("cloneOffer.publishOffer")); + model.onCloneOffer(() -> { + String key = "cloneOfferSuccess"; + if (DontShowAgainLookup.showAgain(key)) { + new Popup() + .feedback(Res.get("cloneOffer.success")) + .dontShowAgainId(key) + .show(); + } + spinnerInfoLabel.setText(""); + busyAnimation.stop(); + close(); + }, + errorMessage -> { + log.error(errorMessage); + spinnerInfoLabel.setText(""); + busyAnimation.stop(); + model.isNextButtonDisabled.setValue(false); + cancelButton.setDisable(false); + new Popup().warning(errorMessage).show(); + }); + } + } + + /////////////////////////////////////////////////////////////////////////////////////////// + // Utils + /////////////////////////////////////////////////////////////////////////////////////////// + + private void updateElementsWithDirection() { + ImageView iconView = new ImageView(); + iconView.setId(model.isShownAsSellOffer() ? "image-sell-white" : "image-buy-white"); + cloneButton.setGraphic(iconView); + cloneButton.setId(model.isShownAsSellOffer() ? "sell-button-big" : "buy-button-big"); + } +} diff --git a/desktop/src/main/java/bisq/desktop/main/portfolio/cloneoffer/CloneOfferViewModel.java b/desktop/src/main/java/bisq/desktop/main/portfolio/cloneoffer/CloneOfferViewModel.java new file mode 100644 index 00000000000..855c2ae86b8 --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/main/portfolio/cloneoffer/CloneOfferViewModel.java @@ -0,0 +1,128 @@ +/* + * 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.portfolio.cloneoffer; + +import bisq.desktop.Navigation; +import bisq.desktop.main.offer.OfferViewUtil; +import bisq.desktop.main.offer.bisq_v1.MutableOfferViewModel; +import bisq.desktop.util.validation.BsqValidator; +import bisq.desktop.util.validation.BtcValidator; +import bisq.desktop.util.validation.FiatVolumeValidator; +import bisq.desktop.util.validation.SecurityDepositValidator; + +import bisq.core.account.witness.AccountAgeWitnessService; +import bisq.core.offer.OfferUtil; +import bisq.core.offer.OpenOffer; +import bisq.core.provider.price.PriceFeedService; +import bisq.core.user.Preferences; +import bisq.core.util.FormattingUtils; +import bisq.core.util.PriceUtil; +import bisq.core.util.coin.BsqFormatter; +import bisq.core.util.coin.CoinFormatter; +import bisq.core.util.validation.AltcoinValidator; +import bisq.core.util.validation.FiatPriceValidator; + +import bisq.common.handlers.ErrorMessageHandler; +import bisq.common.handlers.ResultHandler; + +import com.google.inject.Inject; + +import javax.inject.Named; + +class CloneOfferViewModel extends MutableOfferViewModel { + + @Inject + public CloneOfferViewModel(CloneOfferDataModel dataModel, + FiatVolumeValidator fiatVolumeValidator, + FiatPriceValidator fiatPriceValidator, + AltcoinValidator altcoinValidator, + BtcValidator btcValidator, + BsqValidator bsqValidator, + SecurityDepositValidator securityDepositValidator, + PriceFeedService priceFeedService, + AccountAgeWitnessService accountAgeWitnessService, + Navigation navigation, + Preferences preferences, + @Named(FormattingUtils.BTC_FORMATTER_KEY) CoinFormatter btcFormatter, + BsqFormatter bsqFormatter, + OfferUtil offerUtil) { + super(dataModel, + fiatVolumeValidator, + fiatPriceValidator, + altcoinValidator, + btcValidator, + bsqValidator, + securityDepositValidator, + priceFeedService, + accountAgeWitnessService, + navigation, + preferences, + btcFormatter, + bsqFormatter, + offerUtil); + syncMinAmountWithAmount = false; + } + + @Override + public void activate() { + super.activate(); + + dataModel.populateData(); + + long triggerPriceAsLong = dataModel.getTriggerPrice(); + dataModel.setTriggerPrice(triggerPriceAsLong); + if (triggerPriceAsLong > 0) { + triggerPrice.set(PriceUtil.formatMarketPrice(triggerPriceAsLong, dataModel.getCurrencyCode())); + } else { + triggerPrice.set(""); + } + onTriggerPriceTextFieldChanged(); + } + + public void applyOpenOffer(OpenOffer openOffer) { + dataModel.reset(); + dataModel.applyOpenOffer(openOffer); + } + + public void onCloneOffer(ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) { + dataModel.onCloneOffer(resultHandler, errorMessageHandler); + } + + public void onInvalidateMarketPriceMargin() { + marketPriceMargin.set(FormattingUtils.formatToPercent(dataModel.getMarketPriceMargin())); + } + + public void onInvalidatePrice() { + price.set(FormattingUtils.formatPrice(null)); + price.set(FormattingUtils.formatPrice(dataModel.getPrice().get())); + } + + public boolean isSecurityDepositValid() { + return securityDepositValidator.validate(buyerSecurityDeposit.get()).isValid; + } + + @Override + public void triggerFocusOutOnAmountFields() { + // do not update BTC Amount or minAmount here + // issue 2798: "after a few edits of offer the BTC amount has increased" + } + + public boolean isShownAsSellOffer() { + return OfferViewUtil.isShownAsSellOffer(getTradeCurrency(), dataModel.getDirection()); + } +} diff --git a/desktop/src/main/java/bisq/desktop/main/portfolio/editoffer/EditOfferDataModel.java b/desktop/src/main/java/bisq/desktop/main/portfolio/editoffer/EditOfferDataModel.java index 96223fe0659..6d529133061 100644 --- a/desktop/src/main/java/bisq/desktop/main/portfolio/editoffer/EditOfferDataModel.java +++ b/desktop/src/main/java/bisq/desktop/main/portfolio/editoffer/EditOfferDataModel.java @@ -63,8 +63,9 @@ class EditOfferDataModel extends MutableOfferDataModel { private final CorePersistenceProtoResolver corePersistenceProtoResolver; - private OpenOffer openOffer; + private OpenOffer originalOpenOffer; private OpenOffer.State initialState; + private Offer editedOffer; @Inject EditOfferDataModel(CreateOfferService createOfferService, @@ -117,7 +118,7 @@ public void reset() { } public void applyOpenOffer(OpenOffer openOffer) { - this.openOffer = openOffer; + this.originalOpenOffer = openOffer; Offer offer = openOffer.getOffer(); direction = offer.getDirection(); @@ -175,21 +176,21 @@ protected PaymentAccount getPreselectedPaymentAccount() { } public void populateData() { - Offer offer = openOffer.getOffer(); + Offer offer = originalOpenOffer.getOffer(); // Min amount need to be set before amount as if minAmount is null it would be set by amount setMinAmount(offer.getMinAmount()); setAmount(offer.getAmount()); setPrice(offer.getPrice()); setVolume(offer.getVolume()); setUseMarketBasedPrice(offer.isUseMarketBasedPrice()); - setTriggerPrice(openOffer.getTriggerPrice()); + setTriggerPrice(originalOpenOffer.getTriggerPrice()); if (offer.isUseMarketBasedPrice()) { setMarketPriceMargin(offer.getMarketPriceMargin()); } } public void onStartEditOffer(ErrorMessageHandler errorMessageHandler) { - openOfferManager.editOpenOfferStart(openOffer, () -> { + openOfferManager.editOpenOfferStart(originalOpenOffer, () -> { }, errorMessageHandler); } @@ -201,19 +202,33 @@ public void onPublishOffer(ResultHandler resultHandler, ErrorMessageHandler erro OfferPayload offerPayload = offer.getOfferPayload().orElseThrow(); var mutableOfferPayloadFields = new MutableOfferPayloadFields(offerPayload); - OfferPayload editedPayload = offerUtil.getMergedOfferPayload(openOffer, mutableOfferPayloadFields); - Offer editedOffer = new Offer(editedPayload); + OfferPayload editedPayload = offerUtil.getMergedOfferPayload(originalOpenOffer, mutableOfferPayloadFields); + editedOffer = new Offer(editedPayload); editedOffer.setPriceFeedService(priceFeedService); editedOffer.setState(Offer.State.AVAILABLE); openOfferManager.editOpenOfferPublish(editedOffer, triggerPrice, initialState, () -> { - openOffer = null; + if (cannotActivateOffer()) { + OpenOffer editedOpenOffer = openOfferManager.getOpenOfferById(editedOffer.getId()).orElseThrow(); + editedOpenOffer.setState(OpenOffer.State.DEACTIVATED); + } resultHandler.handleResult(); + originalOpenOffer = null; + editedOffer = null; }, errorMessageHandler); } public void onCancelEditOffer(ErrorMessageHandler errorMessageHandler) { - if (openOffer != null) - openOfferManager.editOpenOfferCancel(openOffer, initialState, () -> { + if (originalOpenOffer != null) + openOfferManager.editOpenOfferCancel(originalOpenOffer, initialState, () -> { }, errorMessageHandler); } + + public boolean cannotActivateOffer() { + // The cannotActivateOffer check considers only activated offers but at editing offer we have set the + // offer DEACTIVATED. We temporarily flip the state so that our cannotActivateOffer works as expected. + originalOpenOffer.setState(OpenOffer.State.AVAILABLE); + boolean result = openOfferManager.cannotActivateOffer(editedOffer); + originalOpenOffer.setState(OpenOffer.State.DEACTIVATED); + return result; + } } diff --git a/desktop/src/main/java/bisq/desktop/main/portfolio/editoffer/EditOfferView.java b/desktop/src/main/java/bisq/desktop/main/portfolio/editoffer/EditOfferView.java index 7f9db5a0c8c..61f1e7cf05c 100644 --- a/desktop/src/main/java/bisq/desktop/main/portfolio/editoffer/EditOfferView.java +++ b/desktop/src/main/java/bisq/desktop/main/portfolio/editoffer/EditOfferView.java @@ -59,8 +59,9 @@ public class EditOfferView extends MutableOfferView { private BusyAnimation busyAnimation; - private Button confirmButton; + private Button confirmEditButton; private Button cancelButton; + private Label spinnerInfoLabel; /////////////////////////////////////////////////////////////////////////////////////////// // Constructor, lifecycle @@ -170,11 +171,11 @@ public void applyOpenOffer(OpenOffer openOffer) { /////////////////////////////////////////////////////////////////////////////////////////// private void addBindings() { - confirmButton.disableProperty().bind(model.isNextButtonDisabled); + confirmEditButton.disableProperty().bind(model.isNextButtonDisabled); } private void removeBindings() { - confirmButton.disableProperty().unbind(); + confirmEditButton.disableProperty().unbind(); } @Override @@ -195,28 +196,36 @@ private void addConfirmEditGroup() { editOfferConfirmationBox.setAlignment(Pos.CENTER_LEFT); GridPane.setHalignment(editOfferConfirmationBox, HPos.LEFT); - confirmButton = editOfferTuple.first; - confirmButton.setMinHeight(40); - confirmButton.setPadding(new Insets(0, 20, 0, 20)); - confirmButton.setGraphicTextGap(10); + confirmEditButton = editOfferTuple.first; + confirmEditButton.setMinHeight(40); + confirmEditButton.setPadding(new Insets(0, 20, 0, 20)); + confirmEditButton.setGraphicTextGap(10); busyAnimation = editOfferTuple.second; - Label spinnerInfoLabel = editOfferTuple.third; + spinnerInfoLabel = editOfferTuple.third; cancelButton = new AutoTooltipButton(Res.get("shared.cancel")); cancelButton.setDefaultButton(false); cancelButton.setOnAction(event -> close()); editOfferConfirmationBox.getChildren().add(cancelButton); - confirmButton.setOnAction(e -> { - confirmButton.requestFocus(); // fix issue #5460 (when enter key used, focus is wrong) - if (model.isPriceInRange()) { - model.isNextButtonDisabled.setValue(true); - cancelButton.setDisable(true); - busyAnimation.play(); - spinnerInfoLabel.setText(Res.get("editOffer.publishOffer")); - //edit offer - model.onPublishOffer(() -> { + confirmEditButton.setOnAction(e -> { + confirmEditButton.requestFocus(); // fix issue #5460 (when enter key used, focus is wrong) + onConfirmEdit(); + }); + } + + private void onConfirmEdit() { + if (model.isPriceInRange()) { + model.isNextButtonDisabled.setValue(true); + cancelButton.setDisable(true); + busyAnimation.play(); + spinnerInfoLabel.setText(Res.get("editOffer.publishOffer")); + + model.onPublishOffer(() -> { + if (model.dataModel.cannotActivateOffer()) { + new Popup().warning(Res.get("editOffer.cannotActivateOffer")).show(); + } else { String key = "editOfferSuccess"; if (DontShowAgainLookup.showAgain(key)) { new Popup() @@ -224,19 +233,19 @@ private void addConfirmEditGroup() { .dontShowAgainId(key) .show(); } - spinnerInfoLabel.setText(""); - busyAnimation.stop(); - close(); - }, (message) -> { - log.error(message); - spinnerInfoLabel.setText(""); - busyAnimation.stop(); - model.isNextButtonDisabled.setValue(false); - cancelButton.setDisable(false); - new Popup().warning(Res.get("editOffer.failed", message)).show(); - }); - } - }); + } + spinnerInfoLabel.setText(""); + busyAnimation.stop(); + close(); + }, (message) -> { + log.error(message); + spinnerInfoLabel.setText(""); + busyAnimation.stop(); + model.isNextButtonDisabled.setValue(false); + cancelButton.setDisable(false); + new Popup().warning(Res.get("editOffer.failed", message)).show(); + }); + } } /////////////////////////////////////////////////////////////////////////////////////////// @@ -246,7 +255,7 @@ private void addConfirmEditGroup() { private void updateElementsWithDirection() { ImageView iconView = new ImageView(); iconView.setId(model.isShownAsSellOffer() ? "image-sell-white" : "image-buy-white"); - confirmButton.setGraphic(iconView); - confirmButton.setId(model.isShownAsSellOffer() ? "sell-button-big" : "buy-button-big"); + confirmEditButton.setGraphic(iconView); + confirmEditButton.setId(model.isShownAsSellOffer() ? "sell-button-big" : "buy-button-big"); } } diff --git a/desktop/src/main/java/bisq/desktop/main/portfolio/openoffer/OpenOffersView.java b/desktop/src/main/java/bisq/desktop/main/portfolio/openoffer/OpenOffersView.java index a28d2427649..1b75ed1fdf9 100644 --- a/desktop/src/main/java/bisq/desktop/main/portfolio/openoffer/OpenOffersView.java +++ b/desktop/src/main/java/bisq/desktop/main/portfolio/openoffer/OpenOffersView.java @@ -37,10 +37,8 @@ import bisq.desktop.util.GUIUtil; import bisq.core.locale.Res; -import bisq.core.offer.Offer; import bisq.core.offer.OpenOffer; import bisq.core.offer.OpenOfferManager; -import bisq.core.offer.bisq_v1.OfferPayload; import bisq.core.provider.price.PriceFeedService; import bisq.core.user.DontShowAgainLookup; @@ -83,19 +81,16 @@ import javafx.util.Callback; import java.util.Comparator; -import java.util.Date; import java.util.stream.Collectors; import org.jetbrains.annotations.NotNull; -import static bisq.core.offer.OfferUtil.getRandomOfferId; import static bisq.desktop.util.FormBuilder.getRegularIconButton; import static bisq.desktop.util.FormBuilder.getRegularIconForLabel; @FxmlView public class OpenOffersView extends ActivatableViewAndModel { - private enum ColumnNames { OFFER_ID(Res.get("shared.offerId")), MAKER_FEE_TX_ID(Res.get("openOffer.header.makerFeeTxId")), @@ -146,7 +141,8 @@ public String toString() { private final BsqSwapOfferDetailsWindow bsqSwapOfferDetailsWindow; private final OpenOfferManager openOfferManager; private SortedList sortedList; - private PortfolioView.OpenOfferActionHandler openOfferActionHandler; + private PortfolioView.EditOpenOfferHandler editOpenOfferHandler; + private PortfolioView.CloneOpenOfferHandler cloneOpenOfferHandler; private ChangeListener widthListener; private ListChangeListener sortedListeChangedListener; @@ -452,7 +448,7 @@ private void doRemoveOpenOffer(OpenOffer openOffer) { private void onEditOpenOffer(OpenOffer openOffer) { if (model.isBootstrappedOrShowPopup()) { - openOfferActionHandler.onEditOpenOffer(openOffer); + editOpenOfferHandler.onEditOpenOffer(openOffer); } } @@ -460,23 +456,26 @@ private void onDuplicateOffer(OpenOfferListItem item) { if (item == null || item.getOffer().getOfferPayloadBase() == null) { return; } - PortfolioUtil.duplicateOffer(navigation, item.getOffer().getOfferPayloadBase()); + if (model.isBootstrappedOrShowPopup()) { + PortfolioUtil.duplicateOffer(navigation, item.getOffer().getOfferPayloadBase()); + } } private void onCloneOffer(OpenOfferListItem item) { if (item == null) { return; } - - String key = "clonedOfferInfo"; - if (DontShowAgainLookup.showAgain(key)) { - new Popup().backgroundInfo(Res.get("offerbook.clonedOffer.info")) - .useIUnderstandButton() - .dontShowAgainId(key) - .onClose(() -> doCloneOffer(item)) - .show(); - } else { - doCloneOffer(item); + if (model.isBootstrappedOrShowPopup()) { + String key = "clonedOfferInfo"; + if (DontShowAgainLookup.showAgain(key)) { + new Popup().backgroundInfo(Res.get("offerbook.clonedOffer.info")) + .useIUnderstandButton() + .dontShowAgainId(key) + .onClose(() -> doCloneOffer(item)) + .show(); + } else { + doCloneOffer(item); + } } } @@ -485,60 +484,7 @@ private void doCloneOffer(OpenOfferListItem item) { if (openOffer == null || openOffer.getOffer() == null || openOffer.getOffer().getOfferPayload().isEmpty()) { return; } - - Offer offer = openOffer.getOffer(); - OfferPayload sourceOfferPayload = offer.getOfferPayload().get(); - log.info("Clone offerPayload with shared maker fee: {}", sourceOfferPayload.getId()); - String newOfferId = getRandomOfferId(); - OfferPayload clonedOfferPayload = new OfferPayload(newOfferId, - new Date().getTime(), - sourceOfferPayload.getOwnerNodeAddress(), - sourceOfferPayload.getPubKeyRing(), - sourceOfferPayload.getDirection(), - sourceOfferPayload.getPrice(), - sourceOfferPayload.getMarketPriceMargin(), - sourceOfferPayload.isUseMarketBasedPrice(), - sourceOfferPayload.getAmount(), - sourceOfferPayload.getMinAmount(), - sourceOfferPayload.getBaseCurrencyCode(), - sourceOfferPayload.getCounterCurrencyCode(), - sourceOfferPayload.getArbitratorNodeAddresses(), - sourceOfferPayload.getMediatorNodeAddresses(), - sourceOfferPayload.getPaymentMethodId(), - sourceOfferPayload.getMakerPaymentAccountId(), - sourceOfferPayload.getOfferFeePaymentTxId(), - sourceOfferPayload.getCountryCode(), - sourceOfferPayload.getAcceptedCountryCodes(), - sourceOfferPayload.getBankId(), - sourceOfferPayload.getAcceptedBankIds(), - sourceOfferPayload.getVersionNr(), - sourceOfferPayload.getBlockHeightAtOfferCreation(), - sourceOfferPayload.getTxFee(), - sourceOfferPayload.getMakerFee(), - sourceOfferPayload.isCurrencyForMakerFeeBtc(), - sourceOfferPayload.getBuyerSecurityDeposit(), - sourceOfferPayload.getSellerSecurityDeposit(), - sourceOfferPayload.getMaxTradeLimit(), - sourceOfferPayload.getMaxTradePeriod(), - sourceOfferPayload.isUseAutoClose(), - sourceOfferPayload.isUseReOpenAfterAutoClose(), - sourceOfferPayload.getLowerClosePrice(), - sourceOfferPayload.getUpperClosePrice(), - sourceOfferPayload.isPrivateOffer(), - sourceOfferPayload.getHashOfChallenge(), - sourceOfferPayload.getExtraDataMap(), - sourceOfferPayload.getProtocolVersion()); - Offer clonedOffer = new Offer(clonedOfferPayload); - clonedOffer.setPriceFeedService(priceFeedService); - offer.setState(Offer.State.OFFER_FEE_PAID); - openOfferManager.placeOffer(clonedOffer, - clonedOffer.getBuyerSecurityDeposit().getValue(), - false, - true, - openOffer.getTriggerPrice(), - transaction -> { - }, - errorMessage -> new Popup().warning(errorMessage).show()); + cloneOpenOfferHandler.onCloneOpenOffer(openOffer); } private void setOfferIdColumnCellFactory() { @@ -1092,8 +1038,12 @@ public void updateItem(final OpenOfferListItem item, boolean empty) { }); } - public void setOpenOfferActionHandler(PortfolioView.OpenOfferActionHandler openOfferActionHandler) { - this.openOfferActionHandler = openOfferActionHandler; + public void setEditOpenOfferHandler(PortfolioView.EditOpenOfferHandler editOpenOfferHandler) { + this.editOpenOfferHandler = editOpenOfferHandler; + } + + public void setCloneOpenOfferHandler(PortfolioView.CloneOpenOfferHandler cloneOpenOfferHandler) { + this.cloneOpenOfferHandler = cloneOpenOfferHandler; } } From d42895637f66365471ea0016ddda9f0d3f8937ec Mon Sep 17 00:00:00 2001 From: HenrikJannsen Date: Fri, 5 May 2023 14:19:31 +0700 Subject: [PATCH 17/31] Publish cloned offer if activated Signed-off-by: HenrikJannsen --- .../java/bisq/core/offer/OpenOfferManager.java | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/core/src/main/java/bisq/core/offer/OpenOfferManager.java b/core/src/main/java/bisq/core/offer/OpenOfferManager.java index d36fe92c956..0dc169ef29d 100644 --- a/core/src/main/java/bisq/core/offer/OpenOfferManager.java +++ b/core/src/main/java/bisq/core/offer/OpenOfferManager.java @@ -417,8 +417,18 @@ public void placeOffer(Offer offer, model, transaction -> { OpenOffer openOffer = new OpenOffer(offer, triggerPrice); - if (isSharedMakerFee && cannotActivateOffer(offer)) { - openOffer.setState(OpenOffer.State.DEACTIVATED); + if (isSharedMakerFee) { + if (cannotActivateOffer(offer)) { + openOffer.setState(OpenOffer.State.DEACTIVATED); + } else { + // We did not use the AddToOfferBook task for publishing because we + // do not have created the openOffer during the protocol and we need that to determine if the offer can be activated. + // So in case we have an activated cloned offer we do the publishing here. + model.getOfferBookService().addOffer(model.getOffer(), + () -> model.setOfferAddedToOfferBook(true), + errorMessage -> model.getOffer().setErrorMessage("Could not add offer to offerbook.\n" + + "Please check your network connection and try again.")); + } } addOpenOfferToList(openOffer); if (!stopped) { From 13bcfb31218f38773216dd03e047b710f6d9c1e9 Mon Sep 17 00:00:00 2001 From: HenrikJannsen Date: Fri, 5 May 2023 19:41:24 +0700 Subject: [PATCH 18/31] Use new offerId and fresh data at clone offer Signed-off-by: HenrikJannsen --- .../portfolio/cloneoffer/CloneOfferDataModel.java | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/desktop/src/main/java/bisq/desktop/main/portfolio/cloneoffer/CloneOfferDataModel.java b/desktop/src/main/java/bisq/desktop/main/portfolio/cloneoffer/CloneOfferDataModel.java index 3764e7024b1..98fcb9c4487 100644 --- a/desktop/src/main/java/bisq/desktop/main/portfolio/cloneoffer/CloneOfferDataModel.java +++ b/desktop/src/main/java/bisq/desktop/main/portfolio/cloneoffer/CloneOfferDataModel.java @@ -54,6 +54,7 @@ import javax.inject.Named; +import java.util.Date; import java.util.Objects; import java.util.Optional; import java.util.Set; @@ -112,6 +113,7 @@ public void reset() { paymentAccounts.clear(); paymentAccount = null; marketPriceMargin = 0; + sourceOpenOffer = null; } public void applyOpenOffer(OpenOffer openOffer) { @@ -202,10 +204,13 @@ private Offer createClonedOffer() { // We create a new offer based on our source offer and the edited fields in the UI Offer editedOffer = createAndGetOffer(); OfferPayload editedOfferPayload = editedOffer.getOfferPayload().orElseThrow(); - // We clone the edited offer but use the maker tx ID from the source offer + // We clone the edited offer but use the maker tx ID from the source offer as well as a new offerId and + // a fresh date. String sharedMakerTxId = sourceOfferPayload.getOfferFeePaymentTxId(); - OfferPayload clonedOfferPayload = new OfferPayload(editedOfferPayload.getId(), - editedOfferPayload.getDate(), + String newOfferId = OfferUtil.getRandomOfferId(); + long date = new Date().getTime(); + OfferPayload clonedOfferPayload = new OfferPayload(newOfferId, + date, editedOfferPayload.getOwnerNodeAddress(), editedOfferPayload.getPubKeyRing(), editedOfferPayload.getDirection(), From 2651a88d5ed55d8de9de776fa71bc729ec32502c Mon Sep 17 00:00:00 2001 From: HenrikJannsen Date: Fri, 5 May 2023 19:42:08 +0700 Subject: [PATCH 19/31] Do not reset addressEntries if its from cloned offers at cleanUpAddressEntries Signed-off-by: HenrikJannsen --- core/src/main/java/bisq/core/offer/OpenOfferManager.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/core/src/main/java/bisq/core/offer/OpenOfferManager.java b/core/src/main/java/bisq/core/offer/OpenOfferManager.java index 0dc169ef29d..0cdfd7976a2 100644 --- a/core/src/main/java/bisq/core/offer/OpenOfferManager.java +++ b/core/src/main/java/bisq/core/offer/OpenOfferManager.java @@ -227,7 +227,10 @@ public void onUpdatedDataReceived() { } private void cleanUpAddressEntries() { - Set openOffersIdSet = openOffers.getList().stream().map(OpenOffer::getId).collect(Collectors.toSet()); + Set openOffersIdSet = openOffers.getList().stream() + .filter(openOffer -> !hasOfferSharedMakerFee(openOffer)) + .map(OpenOffer::getId) + .collect(Collectors.toSet()); btcWalletService.getAddressEntriesForOpenOffer().stream() .filter(e -> !openOffersIdSet.contains(e.getOfferId())) .forEach(e -> { From 60fad3ab6cccd27f5d881848386f6b52e98fc31d Mon Sep 17 00:00:00 2001 From: HenrikJannsen Date: Wed, 10 May 2023 11:42:00 +0700 Subject: [PATCH 20/31] Fix incorrect filtering of offers with shared maker fee Signed-off-by: HenrikJannsen --- core/src/main/java/bisq/core/offer/OpenOfferManager.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/main/java/bisq/core/offer/OpenOfferManager.java b/core/src/main/java/bisq/core/offer/OpenOfferManager.java index 0cdfd7976a2..d275bf5fe3e 100644 --- a/core/src/main/java/bisq/core/offer/OpenOfferManager.java +++ b/core/src/main/java/bisq/core/offer/OpenOfferManager.java @@ -228,9 +228,9 @@ public void onUpdatedDataReceived() { private void cleanUpAddressEntries() { Set openOffersIdSet = openOffers.getList().stream() - .filter(openOffer -> !hasOfferSharedMakerFee(openOffer)) .map(OpenOffer::getId) .collect(Collectors.toSet()); + // We reset all AddressEntriesForOpenOffer which do not have a corresponding openOffer btcWalletService.getAddressEntriesForOpenOffer().stream() .filter(e -> !openOffersIdSet.contains(e.getOfferId())) .forEach(e -> { From f843345dcc1f864fb38f09d48350b08eb6c26091 Mon Sep 17 00:00:00 2001 From: HenrikJannsen Date: Fri, 12 May 2023 19:26:32 +0700 Subject: [PATCH 21/31] Use sourceOfferPayload when cloning the OfferPayload for the fields which must not get changed (like security deposit could get adjusted by the UI). Signed-off-by: HenrikJannsen --- .../cloneoffer/CloneOfferDataModel.java | 26 +++++++++---------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/desktop/src/main/java/bisq/desktop/main/portfolio/cloneoffer/CloneOfferDataModel.java b/desktop/src/main/java/bisq/desktop/main/portfolio/cloneoffer/CloneOfferDataModel.java index 98fcb9c4487..1186b8dc9a8 100644 --- a/desktop/src/main/java/bisq/desktop/main/portfolio/cloneoffer/CloneOfferDataModel.java +++ b/desktop/src/main/java/bisq/desktop/main/portfolio/cloneoffer/CloneOfferDataModel.java @@ -190,7 +190,7 @@ public void populateData() { public void onCloneOffer(ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) { Offer clonedOffer = createClonedOffer(); openOfferManager.placeOffer(clonedOffer, - clonedOffer.getBuyerSecurityDeposit().getValue(), + sourceOpenOffer.getOffer().getBuyerSecurityDeposit().getValue(), false, true, sourceOpenOffer.getTriggerPrice(), @@ -211,18 +211,18 @@ private Offer createClonedOffer() { long date = new Date().getTime(); OfferPayload clonedOfferPayload = new OfferPayload(newOfferId, date, - editedOfferPayload.getOwnerNodeAddress(), - editedOfferPayload.getPubKeyRing(), - editedOfferPayload.getDirection(), + sourceOfferPayload.getOwnerNodeAddress(), + sourceOfferPayload.getPubKeyRing(), + sourceOfferPayload.getDirection(), editedOfferPayload.getPrice(), editedOfferPayload.getMarketPriceMargin(), editedOfferPayload.isUseMarketBasedPrice(), - editedOfferPayload.getAmount(), - editedOfferPayload.getMinAmount(), + sourceOfferPayload.getAmount(), + sourceOfferPayload.getMinAmount(), editedOfferPayload.getBaseCurrencyCode(), editedOfferPayload.getCounterCurrencyCode(), - editedOfferPayload.getArbitratorNodeAddresses(), - editedOfferPayload.getMediatorNodeAddresses(), + sourceOfferPayload.getArbitratorNodeAddresses(), + sourceOfferPayload.getMediatorNodeAddresses(), editedOfferPayload.getPaymentMethodId(), editedOfferPayload.getMakerPaymentAccountId(), sharedMakerTxId, @@ -232,11 +232,11 @@ private Offer createClonedOffer() { editedOfferPayload.getAcceptedBankIds(), editedOfferPayload.getVersionNr(), editedOfferPayload.getBlockHeightAtOfferCreation(), - editedOfferPayload.getTxFee(), - editedOfferPayload.getMakerFee(), - editedOfferPayload.isCurrencyForMakerFeeBtc(), - editedOfferPayload.getBuyerSecurityDeposit(), - editedOfferPayload.getSellerSecurityDeposit(), + sourceOfferPayload.getTxFee(), + sourceOfferPayload.getMakerFee(), + sourceOfferPayload.isCurrencyForMakerFeeBtc(), + sourceOfferPayload.getBuyerSecurityDeposit(), + sourceOfferPayload.getSellerSecurityDeposit(), editedOfferPayload.getMaxTradeLimit(), editedOfferPayload.getMaxTradePeriod(), editedOfferPayload.isUseAutoClose(), From 63ca40db5ce82f40693156436ec33e6c3f1ba9c2 Mon Sep 17 00:00:00 2001 From: HenrikJannsen Date: Fri, 12 May 2023 19:30:27 +0700 Subject: [PATCH 22/31] Remove code for adjusting the security deposit. Signed-off-by: HenrikJannsen --- .../portfolio/cloneoffer/CloneOfferDataModel.java | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/desktop/src/main/java/bisq/desktop/main/portfolio/cloneoffer/CloneOfferDataModel.java b/desktop/src/main/java/bisq/desktop/main/portfolio/cloneoffer/CloneOfferDataModel.java index 1186b8dc9a8..e002f2e2560 100644 --- a/desktop/src/main/java/bisq/desktop/main/portfolio/cloneoffer/CloneOfferDataModel.java +++ b/desktop/src/main/java/bisq/desktop/main/portfolio/cloneoffer/CloneOfferDataModel.java @@ -24,7 +24,6 @@ import bisq.core.account.witness.AccountAgeWitnessService; import bisq.core.btc.wallet.BsqWalletService; import bisq.core.btc.wallet.BtcWalletService; -import bisq.core.btc.wallet.Restrictions; import bisq.core.locale.CurrencyUtil; import bisq.core.locale.TradeCurrency; import bisq.core.offer.Offer; @@ -43,7 +42,6 @@ import bisq.core.user.User; import bisq.core.util.FormattingUtils; import bisq.core.util.coin.CoinFormatter; -import bisq.core.util.coin.CoinUtil; import bisq.network.p2p.P2PService; @@ -136,16 +134,6 @@ public void applyOpenOffer(OpenOffer openOffer) { paymentAccount.setSelectedTradeCurrency(selectedTradeCurrency); } - // If the security deposit got bounded because it was below the coin amount limit, it can be bigger - // by percentage than the restriction. We can't determine the percentage originally entered at offer - // creation, so just use the default value as it doesn't matter anyway. - double buyerSecurityDepositPercent = CoinUtil.getAsPercentPerBtc(offer.getBuyerSecurityDeposit(), offer.getAmount()); - if (buyerSecurityDepositPercent > Restrictions.getMaxBuyerSecurityDepositAsPercent() - && offer.getBuyerSecurityDeposit().value == Restrictions.getMinBuyerSecurityDepositAsCoin().value) - buyerSecurityDeposit.set(Restrictions.getDefaultBuyerSecurityDepositAsPercent()); - else - buyerSecurityDeposit.set(buyerSecurityDepositPercent); - allowAmountUpdate = false; } From e6dc26a26a0a27d42b54a77a887cc24a31a86022 Mon Sep 17 00:00:00 2001 From: HenrikJannsen Date: Fri, 12 May 2023 21:02:19 +0700 Subject: [PATCH 23/31] Further restrict cloning Signed-off-by: HenrikJannsen --- .../cloneoffer/CloneOfferDataModel.java | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/desktop/src/main/java/bisq/desktop/main/portfolio/cloneoffer/CloneOfferDataModel.java b/desktop/src/main/java/bisq/desktop/main/portfolio/cloneoffer/CloneOfferDataModel.java index e002f2e2560..5e05081c484 100644 --- a/desktop/src/main/java/bisq/desktop/main/portfolio/cloneoffer/CloneOfferDataModel.java +++ b/desktop/src/main/java/bisq/desktop/main/portfolio/cloneoffer/CloneOfferDataModel.java @@ -219,7 +219,7 @@ private Offer createClonedOffer() { editedOfferPayload.getBankId(), editedOfferPayload.getAcceptedBankIds(), editedOfferPayload.getVersionNr(), - editedOfferPayload.getBlockHeightAtOfferCreation(), + sourceOfferPayload.getBlockHeightAtOfferCreation(), sourceOfferPayload.getTxFee(), sourceOfferPayload.getMakerFee(), sourceOfferPayload.isCurrencyForMakerFeeBtc(), @@ -227,14 +227,14 @@ private Offer createClonedOffer() { sourceOfferPayload.getSellerSecurityDeposit(), editedOfferPayload.getMaxTradeLimit(), editedOfferPayload.getMaxTradePeriod(), - editedOfferPayload.isUseAutoClose(), - editedOfferPayload.isUseReOpenAfterAutoClose(), - editedOfferPayload.getLowerClosePrice(), - editedOfferPayload.getUpperClosePrice(), - editedOfferPayload.isPrivateOffer(), - editedOfferPayload.getHashOfChallenge(), - editedOfferPayload.getExtraDataMap(), - editedOfferPayload.getProtocolVersion()); + sourceOfferPayload.isUseAutoClose(), + sourceOfferPayload.isUseReOpenAfterAutoClose(), + sourceOfferPayload.getLowerClosePrice(), + sourceOfferPayload.getUpperClosePrice(), + sourceOfferPayload.isPrivateOffer(), + sourceOfferPayload.getHashOfChallenge(), + sourceOfferPayload.getExtraDataMap(), + sourceOfferPayload.getProtocolVersion()); Offer clonedOffer = new Offer(clonedOfferPayload); clonedOffer.setPriceFeedService(priceFeedService); clonedOffer.setState(Offer.State.OFFER_FEE_PAID); From b11511e037543bc0d68b54242cfe0307d11ff62e Mon Sep 17 00:00:00 2001 From: HenrikJannsen Date: Sat, 13 May 2023 22:15:38 +0700 Subject: [PATCH 24/31] Do not use sourceOfferPayload for extraDataMap Signed-off-by: HenrikJannsen --- .../desktop/main/portfolio/cloneoffer/CloneOfferDataModel.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/desktop/src/main/java/bisq/desktop/main/portfolio/cloneoffer/CloneOfferDataModel.java b/desktop/src/main/java/bisq/desktop/main/portfolio/cloneoffer/CloneOfferDataModel.java index 5e05081c484..611d41b6761 100644 --- a/desktop/src/main/java/bisq/desktop/main/portfolio/cloneoffer/CloneOfferDataModel.java +++ b/desktop/src/main/java/bisq/desktop/main/portfolio/cloneoffer/CloneOfferDataModel.java @@ -233,7 +233,7 @@ private Offer createClonedOffer() { sourceOfferPayload.getUpperClosePrice(), sourceOfferPayload.isPrivateOffer(), sourceOfferPayload.getHashOfChallenge(), - sourceOfferPayload.getExtraDataMap(), + editedOfferPayload.getExtraDataMap(), sourceOfferPayload.getProtocolVersion()); Offer clonedOffer = new Offer(clonedOfferPayload); clonedOffer.setPriceFeedService(priceFeedService); From 4262e91f1f3f1bcf4a9455fc363244e501ef18eb Mon Sep 17 00:00:00 2001 From: HenrikJannsen Date: Sat, 13 May 2023 22:45:39 +0700 Subject: [PATCH 25/31] Reapply original code from @jmacxx in AddressEntryList Signed-off-by: HenrikJannsen --- .../bisq/core/btc/model/AddressEntryList.java | 25 +++++++++++++++---- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/core/src/main/java/bisq/core/btc/model/AddressEntryList.java b/core/src/main/java/bisq/core/btc/model/AddressEntryList.java index ccdc54412bd..61b32a6e5c6 100644 --- a/core/src/main/java/bisq/core/btc/model/AddressEntryList.java +++ b/core/src/main/java/bisq/core/btc/model/AddressEntryList.java @@ -209,11 +209,26 @@ public void swapToAvailable(AddressEntry addressEntry) { } log.info("swapToAvailable addressEntry to swap={}", addressEntry); - boolean setChangedByRemove = entrySet.remove(addressEntry); - boolean setChangedByAdd = entrySet.add(new AddressEntry(addressEntry.getKeyPair(), - AddressEntry.Context.AVAILABLE, - addressEntry.isSegwit())); - if (setChangedByRemove || setChangedByAdd) { + if (entrySet.remove(addressEntry)) { + requestPersistence(); + } + // If we have an address entry which shared the address with another one (shared maker fee offers use case) + // then we do not swap to available as we need to protect the address of the remaining entry. + boolean entryWithSameContextStillExists = entrySet.stream().anyMatch(entry -> { + if (addressEntry.getAddressString() != null) { + return addressEntry.getAddressString().equals(entry.getAddressString()) && + addressEntry.getContext() == entry.getContext(); + } + return false; + }); + if (entryWithSameContextStillExists) { + return; + } + // no other uses of the address context remain, so make it available + if (entrySet.add( + new AddressEntry(addressEntry.getKeyPair(), + AddressEntry.Context.AVAILABLE, + addressEntry.isSegwit()))) { requestPersistence(); } } From 13f7b39fbd39f58e6005f22cd9a6ba1bdb098e65 Mon Sep 17 00:00:00 2001 From: HenrikJannsen Date: Sat, 13 May 2023 22:55:03 +0700 Subject: [PATCH 26/31] Do not show BSQ accounts in CloneOfferView Signed-off-by: HenrikJannsen --- .../main/portfolio/cloneoffer/CloneOfferView.java | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/desktop/src/main/java/bisq/desktop/main/portfolio/cloneoffer/CloneOfferView.java b/desktop/src/main/java/bisq/desktop/main/portfolio/cloneoffer/CloneOfferView.java index 9dd2c43d572..a227b0eb7c4 100644 --- a/desktop/src/main/java/bisq/desktop/main/portfolio/cloneoffer/CloneOfferView.java +++ b/desktop/src/main/java/bisq/desktop/main/portfolio/cloneoffer/CloneOfferView.java @@ -24,6 +24,7 @@ import bisq.desktop.main.offer.bisq_v1.MutableOfferView; import bisq.desktop.main.overlays.popups.Popup; import bisq.desktop.main.overlays.windows.OfferDetailsWindow; +import bisq.desktop.util.GUIUtil; import bisq.core.locale.CurrencyUtil; import bisq.core.locale.Res; @@ -51,8 +52,11 @@ import javafx.geometry.Insets; import javafx.geometry.Pos; +import javafx.collections.FXCollections; import javafx.collections.ObservableList; +import java.util.stream.Collectors; + import static bisq.desktop.util.FormBuilder.addButtonBusyAnimationLabelAfterGroup; @FxmlView @@ -169,7 +173,11 @@ private void removeBindings() { @Override protected ObservableList filterPaymentAccounts(ObservableList paymentAccounts) { - return paymentAccounts; + // We do not allow cloning or BSQ as there is no maker fee and requirement for reserved funds. + return FXCollections.observableArrayList( + paymentAccounts.stream() + .filter(paymentAccount -> !GUIUtil.BSQ.equals(paymentAccount.getSingleTradeCurrency())) + .collect(Collectors.toList())); } /////////////////////////////////////////////////////////////////////////////////////////// From 8c37ddd57b422e02abd513f17d9449251f0e2aaa Mon Sep 17 00:00:00 2001 From: HenrikJannsen Date: Sat, 20 May 2023 06:36:38 +0200 Subject: [PATCH 27/31] Use trigger price from model instead from sourceOpenOffer Signed-off-by: HenrikJannsen --- .../desktop/main/portfolio/cloneoffer/CloneOfferDataModel.java | 2 +- .../bisq/desktop/main/portfolio/cloneoffer/CloneOfferView.java | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/desktop/src/main/java/bisq/desktop/main/portfolio/cloneoffer/CloneOfferDataModel.java b/desktop/src/main/java/bisq/desktop/main/portfolio/cloneoffer/CloneOfferDataModel.java index 611d41b6761..98ce733322b 100644 --- a/desktop/src/main/java/bisq/desktop/main/portfolio/cloneoffer/CloneOfferDataModel.java +++ b/desktop/src/main/java/bisq/desktop/main/portfolio/cloneoffer/CloneOfferDataModel.java @@ -181,7 +181,7 @@ public void onCloneOffer(ResultHandler resultHandler, ErrorMessageHandler errorM sourceOpenOffer.getOffer().getBuyerSecurityDeposit().getValue(), false, true, - sourceOpenOffer.getTriggerPrice(), + triggerPrice, transaction -> resultHandler.handleResult(), errorMessageHandler); } diff --git a/desktop/src/main/java/bisq/desktop/main/portfolio/cloneoffer/CloneOfferView.java b/desktop/src/main/java/bisq/desktop/main/portfolio/cloneoffer/CloneOfferView.java index a227b0eb7c4..d5e39ee27a5 100644 --- a/desktop/src/main/java/bisq/desktop/main/portfolio/cloneoffer/CloneOfferView.java +++ b/desktop/src/main/java/bisq/desktop/main/portfolio/cloneoffer/CloneOfferView.java @@ -185,8 +185,7 @@ protected ObservableList filterPaymentAccounts(ObservableList tuple4 = addButtonBusyAnimationLabelAfterGroup(gridPane, tmpGridRow++, Res.get("cloneOffer.clone")); + Tuple4 tuple4 = addButtonBusyAnimationLabelAfterGroup(gridPane, 4, Res.get("cloneOffer.clone")); HBox hBox = tuple4.fourth; hBox.setAlignment(Pos.CENTER_LEFT); From dbd1098ebb6c349c97bec5cdbe333de6d8a1b0ab Mon Sep 17 00:00:00 2001 From: HenrikJannsen Date: Sat, 20 May 2023 08:19:48 +0200 Subject: [PATCH 28/31] Add isCloneOfferViewActivated flag to avoid repeated code execution in doActivate. Do not create a new observableArrayList in filterPaymentAccounts. The reason why the wrong account gets selected is not completely clear to me. The selection handler gets called when the combobox gets filled and that overwrites the selected account from the data. It seems that the new observableArrayList in filterPaymentAccounts triggered that un-expected behaviour. --- .../portfolio/cloneoffer/CloneOfferView.java | 22 +++++++++++-------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/desktop/src/main/java/bisq/desktop/main/portfolio/cloneoffer/CloneOfferView.java b/desktop/src/main/java/bisq/desktop/main/portfolio/cloneoffer/CloneOfferView.java index d5e39ee27a5..702d8420420 100644 --- a/desktop/src/main/java/bisq/desktop/main/portfolio/cloneoffer/CloneOfferView.java +++ b/desktop/src/main/java/bisq/desktop/main/portfolio/cloneoffer/CloneOfferView.java @@ -52,9 +52,9 @@ import javafx.geometry.Insets; import javafx.geometry.Pos; -import javafx.collections.FXCollections; import javafx.collections.ObservableList; +import java.util.List; import java.util.stream.Collectors; import static bisq.desktop.util.FormBuilder.addButtonBusyAnimationLabelAfterGroup; @@ -102,6 +102,7 @@ protected void doSetFocus() { protected void doActivate() { super.doActivate(); + addBindings(); hideOptionsGroup(); @@ -130,10 +131,6 @@ protected void doActivate() { onPaymentAccountsComboBoxSelected(); } - @Override - public void onClose() { - } - @Override protected void deactivate() { super.deactivate(); @@ -141,6 +138,10 @@ protected void deactivate() { removeBindings(); } + @Override + public void onClose() { + } + /////////////////////////////////////////////////////////////////////////////////////////// // API /////////////////////////////////////////////////////////////////////////////////////////// @@ -174,12 +175,15 @@ private void removeBindings() { @Override protected ObservableList filterPaymentAccounts(ObservableList paymentAccounts) { // We do not allow cloning or BSQ as there is no maker fee and requirement for reserved funds. - return FXCollections.observableArrayList( - paymentAccounts.stream() - .filter(paymentAccount -> !GUIUtil.BSQ.equals(paymentAccount.getSingleTradeCurrency())) - .collect(Collectors.toList())); + // Do not create a new ObservableList as that would cause bugs with the selected account. + List toRemove = paymentAccounts.stream() + .filter(paymentAccount -> GUIUtil.BSQ.equals(paymentAccount.getSingleTradeCurrency())) + .collect(Collectors.toList()); + toRemove.forEach(paymentAccounts::remove); + return paymentAccounts; } + /////////////////////////////////////////////////////////////////////////////////////////// // Build UI elements /////////////////////////////////////////////////////////////////////////////////////////// From a7ade008d081825d7cc59f0b665586adfe514f46 Mon Sep 17 00:00:00 2001 From: HenrikJannsen Date: Sat, 20 May 2023 08:34:46 +0200 Subject: [PATCH 29/31] Do not reset content in case the currentTab is editOpenOfferTab, duplicateOfferTab or cloneOpenOfferTab Signed-off-by: HenrikJannsen --- .../desktop/main/portfolio/PortfolioView.java | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/desktop/src/main/java/bisq/desktop/main/portfolio/PortfolioView.java b/desktop/src/main/java/bisq/desktop/main/portfolio/PortfolioView.java index 619288cbb5a..0cae99934bd 100644 --- a/desktop/src/main/java/bisq/desktop/main/portfolio/PortfolioView.java +++ b/desktop/src/main/java/bisq/desktop/main/portfolio/PortfolioView.java @@ -216,10 +216,18 @@ protected void deactivate() { } private void loadView(Class viewClass, @Nullable Object data) { - // we want to get activate/deactivate called, so we remove the old view on tab change - // TODO Don't understand the check for currentTab != editOpenOfferTab - if (currentTab != null && currentTab != editOpenOfferTab) - currentTab.setContent(null); + // We want to get activate/deactivate triggered, so we remove the old view on tab change + // for the tab views which are not-closable by calling `currentTab.setContent(null)`. + // The closable tab views like editOpenOfferTab, duplicateOfferTab and cloneOpenOfferTab + // do not need to be triggered. + if (currentTab != null) { + boolean isClosableTabView = currentTab == editOpenOfferTab || + currentTab == duplicateOfferTab || + currentTab == cloneOpenOfferTab; + if (!isClosableTabView) { + currentTab.setContent(null); + } + } View view = viewLoader.load(viewClass); From 114af3fef471d918b7f3b5a691fc993d0d26f61b Mon Sep 17 00:00:00 2001 From: HenrikJannsen Date: Sun, 21 May 2023 19:33:50 +0200 Subject: [PATCH 30/31] Add link to wiki Signed-off-by: HenrikJannsen --- core/src/main/resources/i18n/displayStrings.properties | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/core/src/main/resources/i18n/displayStrings.properties b/core/src/main/resources/i18n/displayStrings.properties index 60373194a58..0d296bce71e 100644 --- a/core/src/main/resources/i18n/displayStrings.properties +++ b/core/src/main/resources/i18n/displayStrings.properties @@ -382,7 +382,8 @@ offerbook.clonedOffer.info=By cloning an offer one creates a copy of the given o will get closed as well because the transaction output of that maker fee transaction is spent and would render the \ other offers invalid. \n\n\ This feature requires to use the same trade amount and security deposit and is only permitted for offers with different \ - payment methods or currencies. + payment methods or currencies.\n\n\ + For more information about cloning an offer see: [HYPERLINK:https://bisq.wiki/Cloning_an_offer] offerbook.timeSinceSigning.help=When you successfully complete a trade with a peer who has a signed payment account, your payment account is signed.\n\ {0} days later, the initial limit of {1} is lifted and your account can sign other peers'' payment accounts. From 1cbdf043cd94c0c4f38a7ee8e81a3aa2f3dbc9b3 Mon Sep 17 00:00:00 2001 From: HenrikJannsen Date: Mon, 22 May 2023 19:17:22 +0200 Subject: [PATCH 31/31] Fix bug with selection of account combobox Signed-off-by: HenrikJannsen --- .../java/bisq/desktop/main/offer/bisq_v1/MutableOfferView.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/desktop/src/main/java/bisq/desktop/main/offer/bisq_v1/MutableOfferView.java b/desktop/src/main/java/bisq/desktop/main/offer/bisq_v1/MutableOfferView.java index bd3d9a19f3c..06b7b6da4d9 100644 --- a/desktop/src/main/java/bisq/desktop/main/offer/bisq_v1/MutableOfferView.java +++ b/desktop/src/main/java/bisq/desktop/main/offer/bisq_v1/MutableOfferView.java @@ -258,8 +258,7 @@ protected void doActivate() { currencyComboBox.getSelectionModel().select(model.getTradeCurrency()); paymentAccountsComboBox.setItems(getPaymentAccounts()); - paymentAccountsComboBox.getSelectionModel().select(model.getPaymentAccount()); - + UserThread.execute(() -> paymentAccountsComboBox.getSelectionModel().select(model.getPaymentAccount())); onPaymentAccountsComboBoxSelected(); balanceTextField.setTargetAmount(model.getDataModel().totalToPayAsCoinProperty().get());