mirror of
https://github.com/bisq-network/bisq.git
synced 2025-02-24 23:18:17 +01:00
Feat: OCO Offers
This commit is contained in:
parent
d17749110a
commit
4f08f9f383
12 changed files with 345 additions and 41 deletions
|
@ -447,6 +447,7 @@ class CoreOffersService {
|
|||
openOfferManager.placeOffer(offer,
|
||||
buyerSecurityDepositPct,
|
||||
useSavingsWallet,
|
||||
false,
|
||||
triggerPriceAsLong,
|
||||
resultHandler::accept,
|
||||
log::error);
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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()))
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -334,6 +334,7 @@ public abstract class MutableOfferDataModel extends OfferDataModel implements Bs
|
|||
openOfferManager.placeOffer(offer,
|
||||
buyerSecurityDeposit.get(),
|
||||
useSavingsWallet,
|
||||
false,
|
||||
triggerPrice,
|
||||
resultHandler,
|
||||
log::error);
|
||||
|
|
|
@ -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()) {
|
||||
|
|
|
@ -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"/>
|
||||
|
|
|
@ -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) {
|
||||
|
|
Loading…
Add table
Reference in a new issue