mirror of
https://github.com/bisq-network/bisq.git
synced 2024-11-19 01:41:11 +01:00
Merge pull request #6675 from HenrikJannsen/clone_offer_with_shared_maker_fee
Add feature for cloning an offer with shared maker fee
This commit is contained in:
commit
f4368b3185
@ -447,6 +447,7 @@ class CoreOffersService {
|
||||
openOfferManager.placeOffer(offer,
|
||||
buyerSecurityDepositPct,
|
||||
useSavingsWallet,
|
||||
false,
|
||||
triggerPriceAsLong,
|
||||
resultHandler::accept,
|
||||
log::error);
|
||||
|
@ -114,7 +114,9 @@ public class Balances {
|
||||
.map(openOffer -> btcWalletService.getAddressEntry(openOffer.getId(), AddressEntry.Context.RESERVED_FOR_TRADE)
|
||||
.orElse(null))
|
||||
.filter(Objects::nonNull)
|
||||
.mapToLong(addressEntry -> btcWalletService.getBalanceForAddress(addressEntry.getAddress()).value)
|
||||
.map(AddressEntry::getAddress)
|
||||
.distinct()
|
||||
.mapToLong(address -> btcWalletService.getBalanceForAddress(address).value)
|
||||
.sum();
|
||||
reservedBalance.set(Coin.valueOf(sum));
|
||||
}
|
||||
@ -122,8 +124,9 @@ public class Balances {
|
||||
private void updateLockedBalance() {
|
||||
Stream<Trade> 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))
|
||||
long sum = lockedTrades.map(trade -> btcWalletService.getAddressEntry(trade.getId(),
|
||||
AddressEntry.Context.MULTI_SIG)
|
||||
.orElse(null))
|
||||
.filter(Objects::nonNull)
|
||||
.mapToLong(AddressEntry::getCoinLockedInMultiSig)
|
||||
.sum();
|
||||
|
@ -149,7 +149,7 @@ public final class AddressEntryList implements PersistableEnvelope, PersistedDat
|
||||
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,11 +209,26 @@ 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(),
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
@ -630,6 +630,25 @@ public class BtcWalletService extends WalletService {
|
||||
.findAny();
|
||||
}
|
||||
|
||||
// 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> addressEntry = getAddressEntryListAsImmutableList().stream()
|
||||
.filter(entry -> offerId.equals(entry.getOfferId()))
|
||||
.filter(entry -> sourceAddressEntry.getContext() == entry.getContext())
|
||||
.findAny();
|
||||
if (addressEntry.isPresent()) {
|
||||
return addressEntry.get();
|
||||
} else {
|
||||
AddressEntry cloneWithNewOfferId = new AddressEntry(sourceAddressEntry.getKeyPair(),
|
||||
sourceAddressEntry.getContext(),
|
||||
offerId,
|
||||
sourceAddressEntry.isSegwit());
|
||||
addressEntryList.addAddressEntry(cloneWithNewOfferId);
|
||||
return cloneWithNewOfferId;
|
||||
}
|
||||
}
|
||||
|
||||
public AddressEntry getOrCreateAddressEntry(String offerId, AddressEntry.Context context) {
|
||||
Optional<AddressEntry> addressEntry = getAddressEntryListAsImmutableList().stream()
|
||||
.filter(e -> offerId.equals(e.getOfferId()))
|
||||
|
@ -461,8 +461,8 @@ public abstract class WalletService {
|
||||
.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 abstract class WalletService {
|
||||
return wallet.isEncrypted();
|
||||
}
|
||||
|
||||
public List<Transaction> getAllRecentTransactions(boolean includeDead) {
|
||||
return getRecentTransactions(Integer.MAX_VALUE, includeDead);
|
||||
}
|
||||
|
||||
public List<Transaction> getRecentTransactions(int numTransactions, boolean includeDead) {
|
||||
// Returns a list ordered by tx.getUpdateTime() desc
|
||||
return wallet.getRecentTransactions(numTransactions, includeDead);
|
||||
|
@ -502,6 +502,7 @@ public class Offer implements NetworkPayload, PersistablePayload {
|
||||
return offerPayloadBase.getMakerPaymentAccountId();
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public String getOfferFeePaymentTxId() {
|
||||
return getOfferPayload().map(OfferPayload::getOfferFeePaymentTxId).orElse(null);
|
||||
}
|
||||
|
@ -226,7 +226,10 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
|
||||
}
|
||||
|
||||
private void cleanUpAddressEntries() {
|
||||
Set<String> openOffersIdSet = openOffers.getList().stream().map(OpenOffer::getId).collect(Collectors.toSet());
|
||||
Set<String> openOffersIdSet = openOffers.getList().stream()
|
||||
.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 -> {
|
||||
@ -380,12 +383,19 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
|
||||
public void placeOffer(Offer offer,
|
||||
double buyerSecurityDeposit,
|
||||
boolean useSavingsWallet,
|
||||
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("Cannot create offer because maximum number of 10 cloned offers with shared maker fee is reached.");
|
||||
return;
|
||||
}
|
||||
|
||||
Coin reservedFundsForOffer = createOfferService.getReservedFundsForOffer(offer.getDirection(),
|
||||
offer.getAmount(),
|
||||
buyerSecurityDeposit,
|
||||
@ -394,6 +404,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
|
||||
PlaceOfferModel model = new PlaceOfferModel(offer,
|
||||
reservedFundsForOffer,
|
||||
useSavingsWallet,
|
||||
isSharedMakerFee,
|
||||
btcWalletService,
|
||||
tradeWalletService,
|
||||
bsqWalletService,
|
||||
@ -408,6 +419,19 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
|
||||
model,
|
||||
transaction -> {
|
||||
OpenOffer openOffer = new OpenOffer(offer, triggerPrice);
|
||||
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) {
|
||||
startPeriodicRepublishOffersTimer();
|
||||
@ -451,7 +475,12 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
|
||||
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.warning"));
|
||||
return;
|
||||
}
|
||||
|
||||
@ -575,8 +604,6 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
|
||||
} else {
|
||||
resultHandler.handleResult();
|
||||
}
|
||||
} else {
|
||||
errorMessageHandler.handleErrorMessage("Editing of offer can't be canceled as it is not edited.");
|
||||
}
|
||||
}
|
||||
|
||||
@ -584,9 +611,20 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
|
||||
offer.setState(Offer.State.REMOVED);
|
||||
openOffer.setState(OpenOffer.State.CANCELED);
|
||||
removeOpenOfferFromList(openOffer);
|
||||
|
||||
if (!openOffer.getOffer().isBsqSwapOffer()) {
|
||||
closedTradableManager.add(openOffer);
|
||||
btcWalletService.resetAddressEntriesForOpenOffer(offer.getId());
|
||||
// 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 (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());
|
||||
}
|
||||
}
|
||||
log.info("onRemoved offerId={}", offer.getId());
|
||||
resultHandler.handleResult();
|
||||
@ -594,13 +632,31 @@ 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 {
|
||||
getOpenOffersByMakerFeeTxId(offer.getOfferFeePaymentTxId()).forEach(openOffer -> {
|
||||
removeOpenOfferFromList(openOffer);
|
||||
|
||||
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);
|
||||
// We need to reset now those entries as well
|
||||
btcWalletService.resetAddressEntriesForOpenOffer(openOffer.getId());
|
||||
}
|
||||
offerBookService.removeOffer(openOffer.getOffer().getOfferPayloadBase(),
|
||||
() -> log.trace("Successfully removed offer"),
|
||||
log::error);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public void reserveOpenOffer(OpenOffer openOffer) {
|
||||
@ -608,6 +664,25 @@ 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 -> !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() != 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()));
|
||||
}
|
||||
|
||||
public boolean hasOfferSharedMakerFee(OpenOffer openOffer) {
|
||||
return getOpenOffersByMakerFeeTxId(openOffer.getOffer().getOfferFeePaymentTxId()).size() > 1;
|
||||
}
|
||||
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
// Getters
|
||||
@ -815,7 +890,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
|
||||
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());
|
||||
@ -1141,6 +1216,16 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
|
||||
}
|
||||
|
||||
private boolean preventedFromPublishing(OpenOffer openOffer) {
|
||||
return openOffer.isDeactivated() || openOffer.isBsqSwapOfferHasMissingFunds();
|
||||
return openOffer.isDeactivated() ||
|
||||
openOffer.isBsqSwapOfferHasMissingFunds() ||
|
||||
cannotActivateOffer(openOffer.getOffer());
|
||||
}
|
||||
|
||||
private Set<OpenOffer> getOpenOffersByMakerFeeTxId(String makerFeeTxId) {
|
||||
return openOffers.stream()
|
||||
.filter(openOffer -> !openOffer.getOffer().isBsqSwapOffer() &&
|
||||
makerFeeTxId != null &&
|
||||
makerFeeTxId.equals(openOffer.getOffer().getOfferFeePaymentTxId()))
|
||||
.collect(Collectors.toSet());
|
||||
}
|
||||
}
|
||||
|
@ -45,6 +45,7 @@ public class PlaceOfferModel implements Model {
|
||||
private final Offer offer;
|
||||
private final Coin reservedFundsForOffer;
|
||||
private final boolean useSavingsWallet;
|
||||
private final boolean isSharedMakerFee;
|
||||
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 isSharedMakerFee,
|
||||
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.isSharedMakerFee = isSharedMakerFee;
|
||||
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.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;
|
||||
@ -76,12 +77,20 @@ public class PlaceOfferProtocol {
|
||||
errorMessageHandler.handleErrorMessage(errorMessage);
|
||||
}
|
||||
);
|
||||
taskRunner.addTasks(
|
||||
ValidateOffer.class,
|
||||
CheckNumberOfUnconfirmedTransactions.class,
|
||||
CreateMakerFeeTx.class,
|
||||
AddToOfferBook.class
|
||||
);
|
||||
|
||||
if (model.isSharedMakerFee()) {
|
||||
taskRunner.addTasks(
|
||||
ValidateOffer.class,
|
||||
CloneAddressEntryForSharedMakerFee.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 CloneAddressEntryForSharedMakerFee extends Task<PlaceOfferModel> {
|
||||
@SuppressWarnings({"unused"})
|
||||
public CloneAddressEntryForSharedMakerFee(TaskRunner<PlaceOfferModel> taskHandler, PlaceOfferModel model) {
|
||||
super(taskHandler, model);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void run() {
|
||||
runInterceptHook();
|
||||
|
||||
Offer offer = model.getOffer();
|
||||
String makerFeeTxId = offer.getOfferFeePaymentTxId();
|
||||
BtcWalletService walletService = model.getWalletService();
|
||||
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();
|
||||
}
|
||||
|
||||
// We look up the most recent transaction with unspent outputs associated with the given address and return
|
||||
// the txId if found.
|
||||
private Optional<String> findTxId(Address address) {
|
||||
BtcWalletService walletService = model.getWalletService();
|
||||
List<Transaction> 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);
|
||||
// make sure the output is still unspent
|
||||
if (addressString != null && addressString.equals(address.toString()) && output.getSpentBy() == null) {
|
||||
return Optional.of(transaction.getTxId().toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
@ -365,6 +365,26 @@ 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.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.\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 \
|
||||
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.\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.
|
||||
offerbook.timeSinceSigning.notSigned=Not signed yet
|
||||
@ -607,6 +627,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
|
||||
@ -619,7 +640,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
|
||||
@ -654,7 +689,7 @@ 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.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
|
||||
|
@ -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;
|
||||
|
@ -336,6 +336,7 @@ public abstract class MutableOfferDataModel extends OfferDataModel implements Bs
|
||||
openOfferManager.placeOffer(offer,
|
||||
buyerSecurityDeposit.get(),
|
||||
useSavingsWallet,
|
||||
false,
|
||||
triggerPrice,
|
||||
resultHandler,
|
||||
log::error);
|
||||
|
@ -258,8 +258,7 @@ public abstract class MutableOfferView<M extends MutableOfferViewModel<?>> exten
|
||||
|
||||
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());
|
||||
|
@ -23,7 +23,9 @@ import bisq.desktop.common.view.CachingViewLoader;
|
||||
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<TabPane, Void> {
|
||||
|
||||
@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<TabPane, Void> {
|
||||
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 @@ public class PortfolioView extends ActivatableView<TabPane, Void> {
|
||||
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 @@ public class PortfolioView extends ActivatableView<TabPane, Void> {
|
||||
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 @@ public class PortfolioView extends ActivatableView<TabPane, Void> {
|
||||
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<Trade>) c -> {
|
||||
@ -183,6 +201,9 @@ public class PortfolioView extends ActivatableView<TabPane, Void> {
|
||||
} 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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -195,10 +216,18 @@ public class PortfolioView extends ActivatableView<TabPane, Void> {
|
||||
}
|
||||
|
||||
private void loadView(Class<? extends View> 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);
|
||||
|
||||
@ -254,6 +283,28 @@ public class PortfolioView extends ActivatableView<TabPane, Void> {
|
||||
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 +315,35 @@ public class PortfolioView extends ActivatableView<TabPane, Void> {
|
||||
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.setOpenOfferActionHandler(openOfferActionHandler);
|
||||
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.setCloneOpenOfferHandler(cloneOpenOfferHandler);
|
||||
}
|
||||
|
||||
public interface OpenOfferActionHandler {
|
||||
public interface EditOpenOfferHandler {
|
||||
void onEditOpenOffer(OpenOffer openOffer);
|
||||
}
|
||||
|
||||
public interface CloneOpenOfferHandler {
|
||||
void onCloneOpenOffer(OpenOffer openOffer);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -0,0 +1,248 @@
|
||||
/*
|
||||
* 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.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.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.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.Date;
|
||||
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;
|
||||
sourceOpenOffer = null;
|
||||
}
|
||||
|
||||
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<TradeCurrency> 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);
|
||||
}
|
||||
|
||||
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<PaymentAccount> 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,
|
||||
sourceOpenOffer.getOffer().getBuyerSecurityDeposit().getValue(),
|
||||
false,
|
||||
true,
|
||||
triggerPrice,
|
||||
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 as well as a new offerId and
|
||||
// a fresh date.
|
||||
String sharedMakerTxId = sourceOfferPayload.getOfferFeePaymentTxId();
|
||||
String newOfferId = OfferUtil.getRandomOfferId();
|
||||
long date = new Date().getTime();
|
||||
OfferPayload clonedOfferPayload = new OfferPayload(newOfferId,
|
||||
date,
|
||||
sourceOfferPayload.getOwnerNodeAddress(),
|
||||
sourceOfferPayload.getPubKeyRing(),
|
||||
sourceOfferPayload.getDirection(),
|
||||
editedOfferPayload.getPrice(),
|
||||
editedOfferPayload.getMarketPriceMargin(),
|
||||
editedOfferPayload.isUseMarketBasedPrice(),
|
||||
sourceOfferPayload.getAmount(),
|
||||
sourceOfferPayload.getMinAmount(),
|
||||
editedOfferPayload.getBaseCurrencyCode(),
|
||||
editedOfferPayload.getCounterCurrencyCode(),
|
||||
sourceOfferPayload.getArbitratorNodeAddresses(),
|
||||
sourceOfferPayload.getMediatorNodeAddresses(),
|
||||
editedOfferPayload.getPaymentMethodId(),
|
||||
editedOfferPayload.getMakerPaymentAccountId(),
|
||||
sharedMakerTxId,
|
||||
editedOfferPayload.getCountryCode(),
|
||||
editedOfferPayload.getAcceptedCountryCodes(),
|
||||
editedOfferPayload.getBankId(),
|
||||
editedOfferPayload.getAcceptedBankIds(),
|
||||
editedOfferPayload.getVersionNr(),
|
||||
sourceOfferPayload.getBlockHeightAtOfferCreation(),
|
||||
sourceOfferPayload.getTxFee(),
|
||||
sourceOfferPayload.getMakerFee(),
|
||||
sourceOfferPayload.isCurrencyForMakerFeeBtc(),
|
||||
sourceOfferPayload.getBuyerSecurityDeposit(),
|
||||
sourceOfferPayload.getSellerSecurityDeposit(),
|
||||
editedOfferPayload.getMaxTradeLimit(),
|
||||
editedOfferPayload.getMaxTradePeriod(),
|
||||
sourceOfferPayload.isUseAutoClose(),
|
||||
sourceOfferPayload.isUseReOpenAfterAutoClose(),
|
||||
sourceOfferPayload.getLowerClosePrice(),
|
||||
sourceOfferPayload.getUpperClosePrice(),
|
||||
sourceOfferPayload.isPrivateOffer(),
|
||||
sourceOfferPayload.getHashOfChallenge(),
|
||||
editedOfferPayload.getExtraDataMap(),
|
||||
sourceOfferPayload.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);
|
||||
}
|
||||
}
|
@ -0,0 +1,24 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
|
||||
<!--
|
||||
~ 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/>.
|
||||
-->
|
||||
|
||||
<?import javafx.scene.layout.AnchorPane?>
|
||||
<AnchorPane fx:id="root" fx:controller="bisq.desktop.main.portfolio.cloneoffer.CloneOfferView"
|
||||
xmlns:fx="http://javafx.com/fxml">
|
||||
|
||||
</AnchorPane>
|
@ -0,0 +1,268 @@
|
||||
/*
|
||||
* 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.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.desktop.util.GUIUtil;
|
||||
|
||||
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 java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static bisq.desktop.util.FormBuilder.addButtonBusyAnimationLabelAfterGroup;
|
||||
|
||||
@FxmlView
|
||||
public class CloneOfferView extends MutableOfferView<CloneOfferViewModel> {
|
||||
|
||||
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
|
||||
protected void deactivate() {
|
||||
super.deactivate();
|
||||
|
||||
removeBindings();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClose() {
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
// 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<PaymentAccount> filterPaymentAccounts(ObservableList<PaymentAccount> paymentAccounts) {
|
||||
// We do not allow cloning or BSQ as there is no maker fee and requirement for reserved funds.
|
||||
// Do not create a new ObservableList as that would cause bugs with the selected account.
|
||||
List<PaymentAccount> toRemove = paymentAccounts.stream()
|
||||
.filter(paymentAccount -> GUIUtil.BSQ.equals(paymentAccount.getSingleTradeCurrency()))
|
||||
.collect(Collectors.toList());
|
||||
toRemove.forEach(paymentAccounts::remove);
|
||||
return paymentAccounts;
|
||||
}
|
||||
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
// Build UI elements
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
private void addCloneGroup() {
|
||||
Tuple4<Button, BusyAnimation, Label, HBox> tuple4 = addButtonBusyAnimationLabelAfterGroup(gridPane, 4, 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");
|
||||
}
|
||||
}
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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<CloneOfferDataModel> {
|
||||
|
||||
@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());
|
||||
}
|
||||
}
|
@ -264,7 +264,7 @@ public class ClosedTradesView extends ActivatableViewAndModel<VBox, ClosedTrades
|
||||
tableView -> {
|
||||
TableRow<ClosedTradesListItem> 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(
|
||||
|
@ -63,8 +63,9 @@ import java.util.stream.Collectors;
|
||||
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 @@ class EditOfferDataModel extends MutableOfferDataModel {
|
||||
}
|
||||
|
||||
public void applyOpenOffer(OpenOffer openOffer) {
|
||||
this.openOffer = openOffer;
|
||||
this.originalOpenOffer = openOffer;
|
||||
|
||||
Offer offer = openOffer.getOffer();
|
||||
direction = offer.getDirection();
|
||||
@ -175,21 +176,21 @@ class EditOfferDataModel extends MutableOfferDataModel {
|
||||
}
|
||||
|
||||
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 @@ class EditOfferDataModel extends MutableOfferDataModel {
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
@ -59,8 +59,9 @@ import static bisq.desktop.util.FormBuilder.addButtonBusyAnimationLabelAfterGrou
|
||||
public class EditOfferView extends MutableOfferView<EditOfferViewModel> {
|
||||
|
||||
private BusyAnimation busyAnimation;
|
||||
private Button confirmButton;
|
||||
private Button confirmEditButton;
|
||||
private Button cancelButton;
|
||||
private Label spinnerInfoLabel;
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
// Constructor, lifecycle
|
||||
@ -170,11 +171,11 @@ public class EditOfferView extends MutableOfferView<EditOfferViewModel> {
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
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 @@ public class EditOfferView extends MutableOfferView<EditOfferViewModel> {
|
||||
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 @@ public class EditOfferView extends MutableOfferView<EditOfferViewModel> {
|
||||
.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 @@ public class EditOfferView extends MutableOfferView<EditOfferViewModel> {
|
||||
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");
|
||||
}
|
||||
}
|
||||
|
@ -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,6 +138,11 @@ class OpenOfferListItem implements FilterableListItem {
|
||||
}
|
||||
}
|
||||
|
||||
String getMakerFeeTxId() {
|
||||
String makerFeeTxId = getOffer().getOfferFeePaymentTxId();
|
||||
return makerFeeTxId != null ? makerFeeTxId : "";
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean match(String filterString) {
|
||||
if (filterString.isEmpty()) {
|
||||
|
@ -41,6 +41,7 @@
|
||||
<TableView fx:id="tableView" VBox.vgrow="ALWAYS">
|
||||
<columns>
|
||||
<TableColumn fx:id="offerIdColumn" minWidth="110" maxWidth="120"/>
|
||||
<TableColumn fx:id="makerFeeTxIdColumn" minWidth="70"/>
|
||||
<TableColumn fx:id="dateColumn" minWidth="170"/>
|
||||
<TableColumn fx:id="marketColumn" minWidth="75"/>
|
||||
<TableColumn fx:id="priceColumn" minWidth="100"/>
|
||||
@ -54,6 +55,7 @@
|
||||
<TableColumn fx:id="editItemColumn" minWidth="30" maxWidth="30" sortable="false"/>
|
||||
<TableColumn fx:id="triggerIconColumn" minWidth="30" maxWidth="30" sortable="false"/>
|
||||
<TableColumn fx:id="duplicateItemColumn" minWidth="30" maxWidth="30" sortable="false"/>
|
||||
<TableColumn fx:id="cloneItemColumn" minWidth="30" maxWidth="30" sortable="false"/>
|
||||
<TableColumn fx:id="removeItemColumn" minWidth="30" maxWidth="30" sortable="false"/>
|
||||
</columns>
|
||||
</TableView>
|
||||
|
@ -37,8 +37,9 @@ import bisq.desktop.main.portfolio.presentation.PortfolioUtil;
|
||||
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.provider.price.PriceFeedService;
|
||||
import bisq.core.user.DontShowAgainLookup;
|
||||
|
||||
import com.googlecode.jcsv.writer.CSVEntryConverter;
|
||||
@ -80,6 +81,7 @@ import javafx.collections.transformation.SortedList;
|
||||
import javafx.util.Callback;
|
||||
|
||||
import java.util.Comparator;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
@ -89,9 +91,9 @@ import static bisq.desktop.util.FormBuilder.getRegularIconForLabel;
|
||||
@FxmlView
|
||||
public class OpenOffersView extends ActivatableViewAndModel<VBox, OpenOffersViewModel> {
|
||||
|
||||
|
||||
private enum ColumnNames {
|
||||
OFFER_ID(Res.get("shared.offerId")),
|
||||
MAKER_FEE_TX_ID(Res.get("openOffer.header.makerFeeTxId")),
|
||||
DATE(Res.get("shared.dateTime")),
|
||||
MARKET(Res.get("shared.market")),
|
||||
PRICE(Res.get("shared.price")),
|
||||
@ -119,8 +121,9 @@ public class OpenOffersView extends ActivatableViewAndModel<VBox, OpenOffersView
|
||||
TableView<OpenOfferListItem> tableView;
|
||||
@FXML
|
||||
TableColumn<OpenOfferListItem, OpenOfferListItem> priceColumn, deviationColumn, amountColumn, volumeColumn,
|
||||
marketColumn, directionColumn, dateColumn, offerIdColumn, deactivateItemColumn,
|
||||
removeItemColumn, editItemColumn, triggerPriceColumn, triggerIconColumn, paymentMethodColumn, duplicateItemColumn;
|
||||
marketColumn, directionColumn, dateColumn, offerIdColumn, deactivateItemColumn, makerFeeTxIdColumn,
|
||||
removeItemColumn, editItemColumn, triggerPriceColumn, triggerIconColumn, paymentMethodColumn, duplicateItemColumn,
|
||||
cloneItemColumn;
|
||||
@FXML
|
||||
FilterBox filterBox;
|
||||
@FXML
|
||||
@ -132,28 +135,36 @@ public class OpenOffersView extends ActivatableViewAndModel<VBox, OpenOffersView
|
||||
@FXML
|
||||
AutoTooltipSlideToggleButton selectToggleButton;
|
||||
|
||||
private final PriceFeedService priceFeedService;
|
||||
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 PortfolioView.EditOpenOfferHandler editOpenOfferHandler;
|
||||
private PortfolioView.CloneOpenOfferHandler cloneOpenOfferHandler;
|
||||
private ChangeListener<Number> widthListener;
|
||||
private ListChangeListener<OpenOfferListItem> sortedListeChangedListener;
|
||||
|
||||
@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;
|
||||
this.openOfferManager = openOfferManager;
|
||||
}
|
||||
|
||||
@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(),
|
||||
@ -168,9 +179,11 @@ public class OpenOffersView extends ActivatableViewAndModel<VBox, OpenOffersView
|
||||
deactivateItemColumn.setGraphic(new AutoTooltipLabel(ColumnNames.STATUS.toString()));
|
||||
editItemColumn.setText("");
|
||||
duplicateItemColumn.setText("");
|
||||
cloneItemColumn.setText("");
|
||||
removeItemColumn.setText("");
|
||||
|
||||
setOfferIdColumnCellFactory();
|
||||
setMakerFeeTxIdColumnCellFactory();
|
||||
setDirectionColumnCellFactory();
|
||||
setMarketColumnCellFactory();
|
||||
setPriceColumnCellFactory();
|
||||
@ -184,12 +197,14 @@ public class OpenOffersView extends ActivatableViewAndModel<VBox, OpenOffersView
|
||||
setTriggerIconColumnCellFactory();
|
||||
setTriggerPriceColumnCellFactory();
|
||||
setDuplicateColumnCellFactory();
|
||||
setCloneColumnCellFactory();
|
||||
setRemoveColumnCellFactory();
|
||||
|
||||
tableView.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY);
|
||||
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()));
|
||||
@ -201,16 +216,22 @@ public class OpenOffersView extends ActivatableViewAndModel<VBox, OpenOffersView
|
||||
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(
|
||||
tableView -> {
|
||||
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()));
|
||||
rowMenu.getItems().add(duplicateItem);
|
||||
|
||||
MenuItem duplicateOfferMenuItem = new MenuItem(Res.get("portfolio.tab.duplicateOffer"));
|
||||
duplicateOfferMenuItem.setOnAction((event) -> onDuplicateOffer(row.getItem()));
|
||||
rowMenu.getItems().add(duplicateOfferMenuItem);
|
||||
|
||||
MenuItem cloneOfferMenuItem = new MenuItem(Res.get("offerbook.cloneOffer"));
|
||||
cloneOfferMenuItem.setOnAction((event) -> onCloneOffer(row.getItem()));
|
||||
rowMenu.getItems().add(cloneOfferMenuItem);
|
||||
|
||||
row.contextMenuProperty().bind(
|
||||
Bindings.when(Bindings.isNotNull(row.itemProperty()))
|
||||
.then(rowMenu)
|
||||
@ -233,6 +254,8 @@ public class OpenOffersView extends ActivatableViewAndModel<VBox, OpenOffersView
|
||||
c.next();
|
||||
if (c.wasAdded() || c.wasRemoved()) {
|
||||
updateNumberOfOffers();
|
||||
updateMakerFeeTxIdColumnVisibility();
|
||||
updateTriggerColumnVisibility();
|
||||
}
|
||||
};
|
||||
}
|
||||
@ -247,7 +270,8 @@ public class OpenOffersView extends ActivatableViewAndModel<VBox, OpenOffersView
|
||||
|
||||
filterBox.initializeWithCallback(filteredList, tableView, this::updateNumberOfOffers);
|
||||
filterBox.activate();
|
||||
|
||||
updateMakerFeeTxIdColumnVisibility();
|
||||
updateTriggerColumnVisibility();
|
||||
updateSelectToggleButtonState();
|
||||
|
||||
selectToggleButton.setOnAction(event -> {
|
||||
@ -272,6 +296,7 @@ public class OpenOffersView extends ActivatableViewAndModel<VBox, OpenOffersView
|
||||
CSVEntryConverter<OpenOfferListItem> 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();
|
||||
@ -301,6 +326,18 @@ public class OpenOffersView extends ActivatableViewAndModel<VBox, OpenOffersView
|
||||
numItems.setText(Res.get("shared.numItemsLabel", sortedList.size()));
|
||||
}
|
||||
|
||||
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();
|
||||
@ -329,7 +366,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) {
|
||||
@ -350,7 +387,7 @@ public class OpenOffersView extends ActivatableViewAndModel<VBox, OpenOffersView
|
||||
() -> 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();
|
||||
}
|
||||
@ -359,19 +396,23 @@ 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.hasOfferSharedMakerFee(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();
|
||||
}
|
||||
@ -384,9 +425,12 @@ public class OpenOffersView extends ActivatableViewAndModel<VBox, OpenOffersView
|
||||
|
||||
tableView.refresh();
|
||||
|
||||
if (openOffer.getOffer().isBsqSwapOffer()) {
|
||||
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.hasOfferSharedMakerFee(openOffer)) {
|
||||
return;
|
||||
}
|
||||
|
||||
String key = "WithdrawFundsAfterRemoveOfferInfo";
|
||||
if (DontShowAgainLookup.showAgain(key)) {
|
||||
new Popup().instruction(Res.get("offerbook.withdrawFundsHint", Res.get("navigation.funds.availableForWithdrawal")))
|
||||
@ -404,16 +448,43 @@ public class OpenOffersView extends ActivatableViewAndModel<VBox, OpenOffersView
|
||||
|
||||
private void onEditOpenOffer(OpenOffer openOffer) {
|
||||
if (model.isBootstrappedOrShowPopup()) {
|
||||
openOfferActionHandler.onEditOpenOffer(openOffer);
|
||||
editOpenOfferHandler.onEditOpenOffer(openOffer);
|
||||
}
|
||||
}
|
||||
|
||||
private void onDuplicateOffer(Offer offer) {
|
||||
try {
|
||||
PortfolioUtil.duplicateOffer(navigation, offer.getOfferPayloadBase());
|
||||
} catch (NullPointerException e) {
|
||||
log.warn("Unable to get offerPayload - {}", e.toString());
|
||||
private void onDuplicateOffer(OpenOfferListItem item) {
|
||||
if (item == null || item.getOffer().getOfferPayloadBase() == null) {
|
||||
return;
|
||||
}
|
||||
if (model.isBootstrappedOrShowPopup()) {
|
||||
PortfolioUtil.duplicateOffer(navigation, item.getOffer().getOfferPayloadBase());
|
||||
}
|
||||
}
|
||||
|
||||
private void onCloneOffer(OpenOfferListItem item) {
|
||||
if (item == null) {
|
||||
return;
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void doCloneOffer(OpenOfferListItem item) {
|
||||
OpenOffer openOffer = item.getOpenOffer();
|
||||
if (openOffer == null || openOffer.getOffer() == null || openOffer.getOffer().getOfferPayload().isEmpty()) {
|
||||
return;
|
||||
}
|
||||
cloneOpenOfferHandler.onCloneOpenOffer(openOffer);
|
||||
}
|
||||
|
||||
private void setOfferIdColumnCellFactory() {
|
||||
@ -421,19 +492,23 @@ public class OpenOffersView extends ActivatableViewAndModel<VBox, OpenOffersView
|
||||
offerIdColumn.getStyleClass().addAll("number-column", "first-column");
|
||||
offerIdColumn.setCellFactory(
|
||||
new Callback<>() {
|
||||
|
||||
@Override
|
||||
public TableCell<OpenOfferListItem, OpenOfferListItem> call(TableColumn<OpenOfferListItem,
|
||||
OpenOfferListItem> 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 {
|
||||
@ -441,12 +516,52 @@ public class OpenOffersView extends ActivatableViewAndModel<VBox, OpenOffersView
|
||||
}
|
||||
});
|
||||
|
||||
field.setTooltip(new Tooltip(Res.get("tooltip.openPopupForDetails")));
|
||||
setGraphic(field);
|
||||
hyperlinkWithIcon.setTooltip(new Tooltip(Res.get("tooltip.openPopupForDetails")));
|
||||
setGraphic(hyperlinkWithIcon);
|
||||
} else {
|
||||
setGraphic(null);
|
||||
if (hyperlinkWithIcon != null)
|
||||
hyperlinkWithIcon.setOnAction(null);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void setMakerFeeTxIdColumnCellFactory() {
|
||||
makerFeeTxIdColumn.setCellValueFactory((offerListItem) -> new ReadOnlyObjectWrapper<>(offerListItem.getValue()));
|
||||
makerFeeTxIdColumn.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) {
|
||||
|
||||
Label label = new Label(item.getMakerFeeTxId());
|
||||
Text icon;
|
||||
if (openOfferManager.hasOfferSharedMakerFee(item.getOpenOffer())) {
|
||||
icon = getRegularIconForLabel(MaterialDesignIcon.LINK, label, "icon");
|
||||
setTooltip(new Tooltip(Res.get("offerbook.clonedOffer.tooltip", item.getMakerFeeTxId())));
|
||||
} else {
|
||||
icon = getRegularIconForLabel(MaterialDesignIcon.LINK_OFF, label, "icon");
|
||||
setTooltip(new Tooltip(Res.get("offerbook.nonClonedOffer.tooltip", item.getMakerFeeTxId())));
|
||||
}
|
||||
icon.setVisible(!item.getOffer().isBsqSwapOffer());
|
||||
|
||||
if (item.isNotPublished()) {
|
||||
getStyleClass().add("offer-disabled");
|
||||
icon.setOpacity(0.2);
|
||||
}
|
||||
setGraphic(label);
|
||||
} else {
|
||||
setGraphic(null);
|
||||
if (field != null)
|
||||
field.setOnAction(null);
|
||||
}
|
||||
}
|
||||
};
|
||||
@ -785,7 +900,41 @@ 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) {
|
||||
button.setOnAction(null);
|
||||
button = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void setCloneColumnCellFactory() {
|
||||
cloneItemColumn.getStyleClass().add("avatar-column");
|
||||
cloneItemColumn.setCellValueFactory((offerListItem) -> new ReadOnlyObjectWrapper<>(offerListItem.getValue()));
|
||||
cloneItemColumn.setCellFactory(
|
||||
new Callback<>() {
|
||||
@Override
|
||||
public TableCell<OpenOfferListItem, OpenOfferListItem> call(TableColumn<OpenOfferListItem, OpenOfferListItem> 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("offerbook.cloneOffer")));
|
||||
setGraphic(button);
|
||||
}
|
||||
button.setOnAction(event -> onCloneOffer(item));
|
||||
} else {
|
||||
setGraphic(null);
|
||||
if (button != null) {
|
||||
@ -889,8 +1038,12 @@ public class OpenOffersView extends ActivatableViewAndModel<VBox, OpenOffersView
|
||||
});
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -234,7 +234,7 @@ public class PendingTradesView extends ActivatableViewAndModel<VBox, PendingTrad
|
||||
tableView -> {
|
||||
final TableRow<PendingTradesListItem> 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();
|
||||
|
@ -557,3 +557,7 @@
|
||||
-fx-text-fill: -bs-text-color;
|
||||
-fx-fill: -bs-text-color;
|
||||
}
|
||||
|
||||
.icon {
|
||||
-fx-fill: #fff;
|
||||
}
|
||||
|
@ -2248,12 +2248,12 @@ public class FormBuilder {
|
||||
// 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 class FormBuilder {
|
||||
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) {
|
||||
|
Loading…
Reference in New Issue
Block a user