Feat: OCO Offers

This commit is contained in:
jmacxx 2023-03-12 20:08:38 -05:00 committed by HenrikJannsen
parent d17749110a
commit 4f08f9f383
No known key found for this signature in database
GPG key ID: 02AA2BAE387C8307
12 changed files with 345 additions and 41 deletions

View file

@ -447,6 +447,7 @@ class CoreOffersService {
openOfferManager.placeOffer(offer,
buyerSecurityDepositPct,
useSavingsWallet,
false,
triggerPriceAsLong,
resultHandler::accept,
log::error);

View file

@ -42,6 +42,7 @@ import javafx.beans.property.SimpleObjectProperty;
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 @@ public class Balances {
}
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));
}

View file

@ -210,7 +210,17 @@ public final class AddressEntryList implements PersistableEnvelope, PersistedDat
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) {

View file

@ -630,6 +630,12 @@ public class BtcWalletService extends WalletService {
.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> addressEntry = getAddressEntryListAsImmutableList().stream()
.filter(e -> offerId.equals(e.getOfferId()))

View file

@ -381,20 +381,30 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
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 class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
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 class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
return;
}
if (isSpam(openOffer)) {
return;
}
Offer offer = openOffer.getOffer();
offerBookService.activateOffer(offer,
() -> {
@ -585,7 +600,8 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
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 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
// 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 class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
return openOffers.stream().filter(e -> e.getId().equals(offerId)).findFirst();
}
public List<OpenOffer> 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 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
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);
}
}

View file

@ -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 class PlaceOfferModel implements Model {
this.offer = offer;
this.reservedFundsForOffer = reservedFundsForOffer;
this.useSavingsWallet = useSavingsWallet;
this.useOco = useOco;
this.walletService = walletService;
this.tradeWalletService = tradeWalletService;
this.bsqWalletService = bsqWalletService;

View file

@ -19,6 +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.CreateMakerFeeTx;
import bisq.core.offer.placeoffer.bisq_v1.tasks.ValidateOffer;
import bisq.core.trade.bisq_v1.TransactionResultHandler;
@ -76,12 +77,20 @@ public class PlaceOfferProtocol {
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();
}

View file

@ -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 <http://www.gnu.org/licenses/>.
*/
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<PlaceOfferModel> {
@SuppressWarnings({"unused"})
public CloneMakerFeeOco(TaskRunner<PlaceOfferModel> 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<String> getTxIdFromAddress(BtcWalletService walletService, Address address) {
List<Transaction> 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();
}
}

View file

@ -334,6 +334,7 @@ public abstract class MutableOfferDataModel extends OfferDataModel implements Bs
openOfferManager.placeOffer(offer,
buyerSecurityDeposit.get(),
useSavingsWallet,
false,
triggerPrice,
resultHandler,
log::error);

View file

@ -134,6 +134,14 @@ class OpenOfferListItem implements FilterableListItem {
}
}
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()) {

View file

@ -50,6 +50,7 @@
<TableColumn fx:id="volumeColumn" minWidth="110"/>
<TableColumn fx:id="paymentMethodColumn" minWidth="120" maxWidth="170"/>
<TableColumn fx:id="directionColumn" minWidth="70"/>
<TableColumn fx:id="groupColumn" minWidth="50"/>
<TableColumn fx:id="deactivateItemColumn" minWidth="60" maxWidth="60" sortable="false"/>
<TableColumn fx:id="editItemColumn" minWidth="30" maxWidth="30" sortable="false"/>
<TableColumn fx:id="triggerIconColumn" minWidth="30" maxWidth="30" sortable="false"/>

View file

@ -39,6 +39,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.user.DontShowAgainLookup;
import com.googlecode.jcsv.writer.CSVEntryConverter;
@ -52,6 +54,7 @@ import javafx.fxml.FXML;
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.collections.transformation.SortedList;
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 @@ public class OpenOffersView extends ActivatableViewAndModel<VBox, OpenOffersView
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 class OpenOffersView extends ActivatableViewAndModel<VBox, OpenOffersView
TableView<OpenOfferListItem> tableView;
@FXML
TableColumn<OpenOfferListItem, OpenOfferListItem> 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 class OpenOffersView extends ActivatableViewAndModel<VBox, OpenOffersView
private final Navigation navigation;
private final OfferDetailsWindow offerDetailsWindow;
private final BsqSwapOfferDetailsWindow bsqSwapOfferDetailsWindow;
private final OpenOfferManager openOfferManager;
private SortedList<OpenOfferListItem> sortedList;
private PortfolioView.OpenOfferActionHandler openOfferActionHandler;
private ChangeListener<Number> widthListener;
@ -142,6 +150,7 @@ public class OpenOffersView extends ActivatableViewAndModel<VBox, OpenOffersView
@Inject
public OpenOffersView(OpenOffersViewModel model,
OpenOfferManager openOfferManager,
Navigation navigation,
OfferDetailsWindow offerDetailsWindow,
BsqSwapOfferDetailsWindow bsqSwapOfferDetailsWindow) {
@ -149,6 +158,7 @@ public class OpenOffersView extends ActivatableViewAndModel<VBox, OpenOffersView
this.navigation = navigation;
this.offerDetailsWindow = offerDetailsWindow;
this.bsqSwapOfferDetailsWindow = bsqSwapOfferDetailsWindow;
this.openOfferManager = openOfferManager;
}
@Override
@ -159,6 +169,7 @@ public class OpenOffersView extends ActivatableViewAndModel<VBox, OpenOffersView
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 class OpenOffersView extends ActivatableViewAndModel<VBox, OpenOffersView
setEditColumnCellFactory();
setTriggerIconColumnCellFactory();
setTriggerPriceColumnCellFactory();
setGroupColumnCellFactory();
setDuplicateColumnCellFactory();
setRemoveColumnCellFactory();
@ -197,11 +209,12 @@ public class OpenOffersView extends ActivatableViewAndModel<VBox, OpenOffersView
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 class OpenOffersView extends ActivatableViewAndModel<VBox, OpenOffersView
final TableRow<OpenOfferListItem> 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 @@ public class OpenOffersView extends ActivatableViewAndModel<VBox, OpenOffersView
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 @@ public class OpenOffersView extends ActivatableViewAndModel<VBox, OpenOffersView
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 @@ public class OpenOffersView extends ActivatableViewAndModel<VBox, OpenOffersView
}
private void onWidthChange(double width) {
triggerPriceColumn.setVisible(width > 1200);
triggerPriceColumn.setVisible(width > 1300);
}
private void onDeactivateOpenOffer(OpenOffer openOffer) {
@ -359,32 +387,37 @@ public class OpenOffersView extends ActivatableViewAndModel<VBox, OpenOffersView
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 @@ public class OpenOffersView extends ActivatableViewAndModel<VBox, OpenOffersView
}
}
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 class OpenOffersView extends ActivatableViewAndModel<VBox, OpenOffersView
});
}
private void setGroupColumnCellFactory() {
groupColumn.setCellValueFactory((offerListItem) -> new ReadOnlyObjectWrapper<>(offerListItem.getValue()));
groupColumn.setCellFactory(
new Callback<>() {
@Override
public TableCell<OpenOfferListItem, OpenOfferListItem> call(
TableColumn<OpenOfferListItem, OpenOfferListItem> 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 class OpenOffersView extends ActivatableViewAndModel<VBox, OpenOffersView
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) {