From 992854c9b92ce27a62caa76a8ac646c7ff93972d Mon Sep 17 00:00:00 2001 From: HenrikJannsen Date: Thu, 4 May 2023 18:30:09 +0700 Subject: [PATCH] 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 | 102 +++++++------ .../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, 182 insertions(+), 155 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 73326c6ab1..dcc9e31172 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 @@ public class Balances { 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 32c9bd7187..a3f67c039c 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 class BtcWalletService extends WalletService { .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 088a3e8f04..5dc8096b23 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 final class OpenOffer implements Tradable { 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 3c9748e78e..e2750cffc5 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 class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe 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 class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe 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 class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe model, transaction -> { OpenOffer openOffer = new OpenOffer(offer, triggerPrice); - if (useBatchOfferOco) { + if (isSharedMakerFee) { openOffer.setState(OpenOffer.State.DEACTIVATED); } addOpenOfferToList(openOffer); @@ -471,6 +467,12 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe 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 class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe 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 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe 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 class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe 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 class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe 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 class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe 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 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe 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() || !canBeEnabled(openOffer.getOffer()); + return openOffer.isDeactivated() || + openOffer.isBsqSwapOfferHasMissingFunds() || + cannotActivateOffer(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 a57fde42e6..8073a85312 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 class PlaceOfferModel implements Model { 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 a30a9c8e7e..03d709e96b 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 @@ package bisq.core.offer.placeoffer.bisq_v1; 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 class PlaceOfferProtocol { } ); - 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 175adbed7e..3d620ee9e7 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 org.bitcoinj.core.TransactionOutput; 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 a914172bf0..301cec76dc 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 92f32b5955..15f1bbb183 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 c9291ec3a2..d93ea742e3 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 class ClosedTradesView extends ActivatableViewAndModel { 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 480336f219..d90fa1aeae 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 @@ class OpenOfferListItem implements FilterableListItem { } } + // public String getOcoGroupForSorting() { Offer offer = getOffer(); if (offer.isBsqSwapOffer()) { @@ -142,6 +143,7 @@ class OpenOfferListItem implements FilterableListItem { 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 2480d3210c..d25fb4beae 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.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; import com.googlecode.jcsv.writer.CSVEntryConverter; @@ -139,6 +140,7 @@ public class OpenOffersView extends ActivatableViewAndModel { 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 @@ public class OpenOffersView extends ActivatableViewAndModel { 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 @@ public class OpenOffersView extends ActivatableViewAndModel { - 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 class OpenOffersView extends ActivatableViewAndModel { 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();