mirror of
https://github.com/bisq-network/bisq.git
synced 2025-03-03 18:56:59 +01:00
Merge pull request #4347 from sqrrm/deposit-improvements
Deposit improvements
This commit is contained in:
commit
cfc3252f7b
19 changed files with 299 additions and 34 deletions
|
@ -22,6 +22,10 @@ import com.google.common.math.DoubleMath;
|
|||
import java.math.BigDecimal;
|
||||
import java.math.RoundingMode;
|
||||
|
||||
import java.util.ArrayDeque;
|
||||
import java.util.Deque;
|
||||
import java.util.Optional;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
|
@ -107,4 +111,60 @@ public class MathUtils {
|
|||
}
|
||||
return median;
|
||||
}
|
||||
|
||||
public static class MovingAverage {
|
||||
Deque<Long> window;
|
||||
private int size;
|
||||
private long sum;
|
||||
private double outlier;
|
||||
|
||||
// Outlier as ratio
|
||||
public MovingAverage(int size, double outlier) {
|
||||
this.size = size;
|
||||
window = new ArrayDeque<>(size);
|
||||
this.outlier = outlier;
|
||||
sum = 0;
|
||||
}
|
||||
|
||||
public Optional<Double> next(long val) {
|
||||
var fullAtStart = isFull();
|
||||
if (fullAtStart) {
|
||||
if (outlier > 0) {
|
||||
// Return early if it's an outlier
|
||||
var avg = (double) sum / size;
|
||||
if (Math.abs(avg - val) / avg > outlier) {
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
sum -= window.remove();
|
||||
}
|
||||
window.add(val);
|
||||
sum += val;
|
||||
if (!fullAtStart && isFull() && outlier != 0) {
|
||||
removeInitialOutlier();
|
||||
}
|
||||
// When discarding outliers, the first n non discarded elements return Optional.empty()
|
||||
return outlier > 0 && !isFull() ? Optional.empty() : current();
|
||||
}
|
||||
|
||||
boolean isFull() {
|
||||
return window.size() == size;
|
||||
}
|
||||
|
||||
private void removeInitialOutlier() {
|
||||
var element = window.iterator();
|
||||
while (element.hasNext()) {
|
||||
var val = element.next();
|
||||
var avgExVal = (double) (sum - val) / (size - 1);
|
||||
if (Math.abs(avgExVal - val) / avgExVal > outlier) {
|
||||
element.remove();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public Optional<Double> current() {
|
||||
return window.size() == 0 ? Optional.empty() : Optional.of((double) sum / window.size());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
57
common/src/test/java/bisq/common/util/MathUtilsTest.java
Normal file
57
common/src/test/java/bisq/common/util/MathUtilsTest.java
Normal file
|
@ -0,0 +1,57 @@
|
|||
/*
|
||||
* 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.common.util;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
|
||||
public class MathUtilsTest {
|
||||
|
||||
|
||||
@SuppressWarnings("OptionalGetWithoutIsPresent")
|
||||
@Test
|
||||
public void testMovingAverageWithoutOutlierExclusion() {
|
||||
var values = new int[]{4, 5, 3, 1, 2, 4};
|
||||
// Moving average = 4, 4.5, 4, 3, 2, 7/3
|
||||
var movingAverage = new MathUtils.MovingAverage(3, 0);
|
||||
int i = 0;
|
||||
assertEquals(4, movingAverage.next(values[i++]).get(),0.001);
|
||||
assertEquals(4.5, movingAverage.next(values[i++]).get(),0.001);
|
||||
assertEquals(4, movingAverage.next(values[i++]).get(),0.001);
|
||||
assertEquals(3, movingAverage.next(values[i++]).get(),0.001);
|
||||
assertEquals(2, movingAverage.next(values[i++]).get(),0.001);
|
||||
assertEquals((double) 7 / 3, movingAverage.next(values[i]).get(),0.001);
|
||||
}
|
||||
|
||||
@SuppressWarnings("OptionalGetWithoutIsPresent")
|
||||
@Test
|
||||
public void testMovingAverageWithOutlierExclusion() {
|
||||
var values = new int[]{100, 102, 95, 101, 120, 115};
|
||||
// Moving average = N/A, N/A, 99, 99.333..., N/A, 103.666...
|
||||
var movingAverage = new MathUtils.MovingAverage(3, 0.2);
|
||||
int i = 0;
|
||||
assertFalse(movingAverage.next(values[i++]).isPresent());
|
||||
assertFalse(movingAverage.next(values[i++]).isPresent());
|
||||
assertEquals(99, movingAverage.next(values[i++]).get(),0.001);
|
||||
assertEquals(99.333, movingAverage.next(values[i++]).get(),0.001);
|
||||
assertFalse(movingAverage.next(values[i++]).isPresent());
|
||||
assertEquals(103.666, movingAverage.next(values[i]).get(),0.001);
|
||||
}
|
||||
}
|
|
@ -90,4 +90,9 @@ public class Restrictions {
|
|||
MIN_REFUND_AT_MEDIATED_DISPUTE = Coin.parseCoin("0.003"); // 0.003 BTC about 21 USD @ 7000 USD/BTC
|
||||
return MIN_REFUND_AT_MEDIATED_DISPUTE;
|
||||
}
|
||||
|
||||
public static int getLockTime(boolean isAsset) {
|
||||
// 10 days for altcoins, 20 days for other payment methods
|
||||
return isAsset ? 144 * 10 : 144 * 20;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -182,7 +182,7 @@ public class CreateOfferService {
|
|||
List<String> acceptedCountryCodes = PaymentAccountUtil.getAcceptedCountryCodes(paymentAccount);
|
||||
String bankId = PaymentAccountUtil.getBankId(paymentAccount);
|
||||
List<String> acceptedBanks = PaymentAccountUtil.getAcceptedBanks(paymentAccount);
|
||||
double sellerSecurityDeposit = getSellerSecurityDepositAsDouble();
|
||||
double sellerSecurityDeposit = getSellerSecurityDepositAsDouble(buyerSecurityDepositAsDouble);
|
||||
Coin txFeeFromFeeService = getEstimatedFeeAndTxSize(amount, direction, buyerSecurityDepositAsDouble, sellerSecurityDeposit).first;
|
||||
Coin txFeeToUse = txFee.isPositive() ? txFee : txFeeFromFeeService;
|
||||
Coin makerFeeAsCoin = getMakerFee(amount);
|
||||
|
@ -287,8 +287,9 @@ public class CreateOfferService {
|
|||
getSellerSecurityDeposit(amount, sellerSecurityDeposit);
|
||||
}
|
||||
|
||||
public double getSellerSecurityDepositAsDouble() {
|
||||
return Restrictions.getSellerSecurityDepositAsPercent();
|
||||
public double getSellerSecurityDepositAsDouble(double buyerSecurityDeposit) {
|
||||
return Preferences.USE_SYMMETRIC_SECURITY_DEPOSIT ? buyerSecurityDeposit :
|
||||
Restrictions.getSellerSecurityDepositAsPercent();
|
||||
}
|
||||
|
||||
public Coin getMakerFee(Coin amount) {
|
||||
|
|
|
@ -22,8 +22,8 @@ import bisq.core.btc.wallet.BtcWalletService;
|
|||
import bisq.core.btc.wallet.TradeWalletService;
|
||||
import bisq.core.dao.DaoFacade;
|
||||
import bisq.core.exceptions.TradePriceOutOfToleranceException;
|
||||
import bisq.core.locale.Res;
|
||||
import bisq.core.filter.FilterManager;
|
||||
import bisq.core.locale.Res;
|
||||
import bisq.core.offer.availability.DisputeAgentSelection;
|
||||
import bisq.core.offer.messages.OfferAvailabilityRequest;
|
||||
import bisq.core.offer.messages.OfferAvailabilityResponse;
|
||||
|
@ -353,7 +353,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
|
|||
Coin reservedFundsForOffer = createOfferService.getReservedFundsForOffer(offer.getDirection(),
|
||||
offer.getAmount(),
|
||||
buyerSecurityDeposit,
|
||||
createOfferService.getSellerSecurityDepositAsDouble());
|
||||
createOfferService.getSellerSecurityDepositAsDouble(buyerSecurityDeposit));
|
||||
|
||||
PlaceOfferModel model = new PlaceOfferModel(offer,
|
||||
reservedFundsForOffer,
|
||||
|
|
|
@ -17,6 +17,7 @@
|
|||
|
||||
package bisq.core.trade.protocol.tasks.maker;
|
||||
|
||||
import bisq.core.btc.wallet.Restrictions;
|
||||
import bisq.core.trade.Trade;
|
||||
import bisq.core.trade.protocol.tasks.TradeTask;
|
||||
|
||||
|
@ -38,7 +39,7 @@ public class MakerSetsLockTime extends TradeTask {
|
|||
runInterceptHook();
|
||||
|
||||
// 10 days for altcoins, 20 days for other payment methods
|
||||
int delay = processModel.getOffer().getPaymentMethod().isAsset() ? 144 * 10 : 144 * 20;
|
||||
int delay = Restrictions.getLockTime(processModel.getOffer().getPaymentMethod().isAsset());
|
||||
if (Config.baseCurrencyNetwork().isRegtest()) {
|
||||
delay = 5;
|
||||
}
|
||||
|
|
|
@ -120,6 +120,9 @@ public final class Preferences implements PersistedDataHost, BridgeAddressProvid
|
|||
new BlockChainExplorer("bsq.bisq.cc (@m52go)", "https://bsq.bisq.cc/tx.html?tx=", "https://bsq.bisq.cc/Address.html?addr=")
|
||||
));
|
||||
|
||||
public static final boolean USE_SYMMETRIC_SECURITY_DEPOSIT = true;
|
||||
|
||||
|
||||
// payload is initialized so the default values are available for Property initialization.
|
||||
@Setter
|
||||
@Delegate(excludes = ExcludesDelegateMethods.class)
|
||||
|
|
|
@ -211,6 +211,10 @@ public class FormattingUtils {
|
|||
return formatToPercent(value) + "%";
|
||||
}
|
||||
|
||||
public static String formatToRoundedPercentWithSymbol(double value) {
|
||||
return formatToPercent(value, new DecimalFormat("#")) + "%";
|
||||
}
|
||||
|
||||
public static String formatPercentagePrice(double value) {
|
||||
return formatToPercentWithSymbol(value);
|
||||
}
|
||||
|
@ -219,6 +223,11 @@ public class FormattingUtils {
|
|||
DecimalFormat decimalFormat = new DecimalFormat("#.##");
|
||||
decimalFormat.setMinimumFractionDigits(2);
|
||||
decimalFormat.setMaximumFractionDigits(2);
|
||||
|
||||
return formatToPercent(value, decimalFormat);
|
||||
}
|
||||
|
||||
public static String formatToPercent(double value, DecimalFormat decimalFormat) {
|
||||
return decimalFormat.format(MathUtils.roundDouble(value * 100.0, 2)).replace(",", ".");
|
||||
}
|
||||
|
||||
|
|
|
@ -357,6 +357,8 @@ shared.notSigned.noNeed=This account type doesn't use signing
|
|||
|
||||
offerbook.nrOffers=No. of offers: {0}
|
||||
offerbook.volume={0} (min - max)
|
||||
offerbook.deposit=Deposit BTC (%)
|
||||
offerbook.deposit.help=Deposit paid by each trader to guarantee the trade. Will be returned when the trade is completed.
|
||||
|
||||
offerbook.createOfferToBuy=Create new offer to buy {0}
|
||||
offerbook.createOfferToSell=Create new offer to sell {0}
|
||||
|
@ -480,6 +482,7 @@ createOffer.tac=With publishing this offer I agree to trade with any trader who
|
|||
createOffer.currencyForFee=Trade fee
|
||||
createOffer.setDeposit=Set buyer's security deposit (%)
|
||||
createOffer.setDepositAsBuyer=Set my security deposit as buyer (%)
|
||||
createOffer.setDepositForBothTraders=Set both traders' security deposit (%)
|
||||
createOffer.securityDepositInfo=Your buyer''s security deposit will be {0}
|
||||
createOffer.securityDepositInfoAsBuyer=Your security deposit as buyer will be {0}
|
||||
createOffer.minSecurityDepositUsed=Min. buyer security deposit is used
|
||||
|
|
|
@ -43,6 +43,8 @@ import bisq.core.payment.PaymentAccount;
|
|||
import bisq.core.provider.fee.FeeService;
|
||||
import bisq.core.provider.price.PriceFeedService;
|
||||
import bisq.core.trade.handlers.TransactionResultHandler;
|
||||
import bisq.core.trade.statistics.TradeStatistics2;
|
||||
import bisq.core.trade.statistics.TradeStatisticsManager;
|
||||
import bisq.core.user.Preferences;
|
||||
import bisq.core.user.User;
|
||||
import bisq.core.util.FormattingUtils;
|
||||
|
@ -79,8 +81,11 @@ import javafx.collections.FXCollections;
|
|||
import javafx.collections.ObservableList;
|
||||
import javafx.collections.SetChangeListener;
|
||||
|
||||
import java.util.Comparator;
|
||||
import java.util.Date;
|
||||
import java.util.HashSet;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
|
@ -126,6 +131,7 @@ public abstract class MutableOfferDataModel extends OfferDataModel implements Bs
|
|||
private boolean marketPriceAvailable;
|
||||
private int feeTxSize = TxFeeEstimationService.TYPICAL_TX_WITH_1_INPUT_SIZE;
|
||||
protected boolean allowAmountUpdate = true;
|
||||
private final TradeStatisticsManager tradeStatisticsManager;
|
||||
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
@ -145,6 +151,7 @@ public abstract class MutableOfferDataModel extends OfferDataModel implements Bs
|
|||
FeeService feeService,
|
||||
@Named(FormattingUtils.BTC_FORMATTER_KEY) CoinFormatter btcFormatter,
|
||||
MakerFeeProvider makerFeeProvider,
|
||||
TradeStatisticsManager tradeStatisticsManager,
|
||||
Navigation navigation) {
|
||||
super(btcWalletService);
|
||||
|
||||
|
@ -160,6 +167,7 @@ public abstract class MutableOfferDataModel extends OfferDataModel implements Bs
|
|||
this.btcFormatter = btcFormatter;
|
||||
this.makerFeeProvider = makerFeeProvider;
|
||||
this.navigation = navigation;
|
||||
this.tradeStatisticsManager = tradeStatisticsManager;
|
||||
|
||||
offerId = createOfferService.getRandomOfferId();
|
||||
shortOfferId = Utilities.getShortId(offerId);
|
||||
|
@ -257,6 +265,7 @@ public abstract class MutableOfferDataModel extends OfferDataModel implements Bs
|
|||
calculateVolume();
|
||||
calculateTotalToPay();
|
||||
updateBalance();
|
||||
setSuggestedSecurityDeposit(getPaymentAccount());
|
||||
|
||||
return true;
|
||||
}
|
||||
|
@ -294,7 +303,7 @@ public abstract class MutableOfferDataModel extends OfferDataModel implements Bs
|
|||
Tuple2<Coin, Integer> estimatedFeeAndTxSize = createOfferService.getEstimatedFeeAndTxSize(amount.get(),
|
||||
direction,
|
||||
buyerSecurityDeposit.get(),
|
||||
createOfferService.getSellerSecurityDepositAsDouble());
|
||||
createOfferService.getSellerSecurityDepositAsDouble(buyerSecurityDeposit.get()));
|
||||
txFeeFromFeeService = estimatedFeeAndTxSize.first;
|
||||
feeTxSize = estimatedFeeAndTxSize.second;
|
||||
}
|
||||
|
@ -317,14 +326,49 @@ public abstract class MutableOfferDataModel extends OfferDataModel implements Bs
|
|||
this.paymentAccount = paymentAccount;
|
||||
|
||||
setTradeCurrencyFromPaymentAccount(paymentAccount);
|
||||
|
||||
buyerSecurityDeposit.set(preferences.getBuyerSecurityDepositAsPercent(getPaymentAccount()));
|
||||
setSuggestedSecurityDeposit(getPaymentAccount());
|
||||
|
||||
if (amount.get() != null)
|
||||
this.amount.set(Coin.valueOf(Math.min(amount.get().value, getMaxTradeLimit())));
|
||||
}
|
||||
}
|
||||
|
||||
private void setSuggestedSecurityDeposit(PaymentAccount paymentAccount) {
|
||||
var minSecurityDeposit = preferences.getBuyerSecurityDepositAsPercent(getPaymentAccount());
|
||||
if (getTradeCurrency() == null) {
|
||||
setBuyerSecurityDeposit(minSecurityDeposit, false);
|
||||
return;
|
||||
}
|
||||
// Get average historic prices over for the prior trade period equaling the lock time
|
||||
var blocksRange = Restrictions.getLockTime(paymentAccount.getPaymentMethod().isAsset());
|
||||
var startDate = new Date(System.currentTimeMillis() - blocksRange * 10 * 60000);
|
||||
var sortedRangeData = tradeStatisticsManager.getObservableTradeStatisticsSet().stream()
|
||||
.filter(e -> e.getCurrencyCode().equals(getTradeCurrency().getCode()))
|
||||
.filter(e -> e.getTradeDate().compareTo(startDate) >= 0)
|
||||
.sorted(Comparator.comparing(TradeStatistics2::getTradeDate))
|
||||
.collect(Collectors.toList());
|
||||
var movingAverage = new MathUtils.MovingAverage(10, 0.2);
|
||||
double[] extremes = {Double.MAX_VALUE, Double.MIN_VALUE};
|
||||
sortedRangeData.forEach(e -> {
|
||||
var price = e.getTradePrice().getValue();
|
||||
movingAverage.next(price).ifPresent(val -> {
|
||||
if (val < extremes[0]) extremes[0] = val;
|
||||
if (val > extremes[1]) extremes[1] = val;
|
||||
});
|
||||
});
|
||||
var min = extremes[0];
|
||||
var max = extremes[1];
|
||||
if (min == 0d || max == 0d) {
|
||||
setBuyerSecurityDeposit(minSecurityDeposit, false);
|
||||
return;
|
||||
}
|
||||
// Suggested deposit is double the trade range over the previous lock time period, bounded by min/max deposit
|
||||
var suggestedSecurityDeposit =
|
||||
Math.min(2 * (max - min) / max, Restrictions.getMaxBuyerSecurityDepositAsPercent());
|
||||
buyerSecurityDeposit.set(Math.max(suggestedSecurityDeposit, minSecurityDeposit));
|
||||
}
|
||||
|
||||
|
||||
private void setTradeCurrencyFromPaymentAccount(PaymentAccount paymentAccount) {
|
||||
if (!paymentAccount.getTradeCurrencies().contains(tradeCurrency)) {
|
||||
if (paymentAccount.getSelectedTradeCurrency() != null)
|
||||
|
@ -591,10 +635,13 @@ public abstract class MutableOfferDataModel extends OfferDataModel implements Bs
|
|||
this.volume.set(volume);
|
||||
}
|
||||
|
||||
void setBuyerSecurityDeposit(double value) {
|
||||
void setBuyerSecurityDeposit(double value, boolean persist) {
|
||||
this.buyerSecurityDeposit.set(value);
|
||||
if (persist) {
|
||||
// Only expected to persist for manually changed deposit values
|
||||
preferences.setBuyerSecurityDepositAsPercent(value, getPaymentAccount());
|
||||
}
|
||||
}
|
||||
|
||||
protected boolean isUseMarketBasedPriceValue() {
|
||||
return marketPriceAvailable && useMarketBasedPrice.get() && !isHalCashAccount();
|
||||
|
@ -650,7 +697,8 @@ public abstract class MutableOfferDataModel extends OfferDataModel implements Bs
|
|||
if (amountAsCoin == null)
|
||||
amountAsCoin = Coin.ZERO;
|
||||
|
||||
Coin percentOfAmountAsCoin = CoinUtil.getPercentOfAmountAsCoin(createOfferService.getSellerSecurityDepositAsDouble(), amountAsCoin);
|
||||
Coin percentOfAmountAsCoin = CoinUtil.getPercentOfAmountAsCoin(
|
||||
createOfferService.getSellerSecurityDepositAsDouble(buyerSecurityDeposit.get()), amountAsCoin);
|
||||
return getBoundedSellerSecurityDepositAsCoin(percentOfAmountAsCoin);
|
||||
}
|
||||
|
||||
|
|
|
@ -421,7 +421,7 @@ public abstract class MutableOfferViewModel<M extends MutableOfferDataModel> ext
|
|||
securityDepositStringListener = (ov, oldValue, newValue) -> {
|
||||
if (!ignoreSecurityDepositStringListener) {
|
||||
if (securityDepositValidator.validate(newValue).isValid) {
|
||||
setBuyerSecurityDepositToModel();
|
||||
setBuyerSecurityDepositToModel(false);
|
||||
dataModel.calculateTotalToPay();
|
||||
}
|
||||
updateButtonDisableState();
|
||||
|
@ -898,7 +898,7 @@ public abstract class MutableOfferViewModel<M extends MutableOfferDataModel> ext
|
|||
.width(800)
|
||||
.actionButtonText(Res.get("createOffer.resetToDefault"))
|
||||
.onAction(() -> {
|
||||
dataModel.setBuyerSecurityDeposit(defaultSecurityDeposit);
|
||||
dataModel.setBuyerSecurityDeposit(defaultSecurityDeposit, false);
|
||||
ignoreSecurityDepositStringListener = true;
|
||||
buyerSecurityDeposit.set(FormattingUtils.formatToPercent(dataModel.getBuyerSecurityDeposit().get()));
|
||||
ignoreSecurityDepositStringListener = false;
|
||||
|
@ -915,7 +915,7 @@ public abstract class MutableOfferViewModel<M extends MutableOfferDataModel> ext
|
|||
}
|
||||
|
||||
private void applyBuyerSecurityDepositOnFocusOut() {
|
||||
setBuyerSecurityDepositToModel();
|
||||
setBuyerSecurityDepositToModel(true);
|
||||
ignoreSecurityDepositStringListener = true;
|
||||
buyerSecurityDeposit.set(FormattingUtils.formatToPercent(dataModel.getBuyerSecurityDeposit().get()));
|
||||
ignoreSecurityDepositStringListener = false;
|
||||
|
@ -966,7 +966,8 @@ public abstract class MutableOfferViewModel<M extends MutableOfferDataModel> ext
|
|||
}
|
||||
|
||||
public String getSecurityDepositLabel() {
|
||||
return dataModel.isBuyOffer() ? Res.get("createOffer.setDepositAsBuyer") : Res.get("createOffer.setDeposit");
|
||||
return Preferences.USE_SYMMETRIC_SECURITY_DEPOSIT ? Res.get("createOffer.setDepositForBothTraders") :
|
||||
dataModel.isBuyOffer() ? Res.get("createOffer.setDepositAsBuyer") : Res.get("createOffer.setDeposit");
|
||||
}
|
||||
|
||||
public String getSecurityDepositPopOverLabel(String depositInBTC) {
|
||||
|
@ -1145,11 +1146,13 @@ public abstract class MutableOfferViewModel<M extends MutableOfferDataModel> ext
|
|||
}
|
||||
}
|
||||
|
||||
private void setBuyerSecurityDepositToModel() {
|
||||
private void setBuyerSecurityDepositToModel(boolean persistPreference) {
|
||||
if (buyerSecurityDeposit.get() != null && !buyerSecurityDeposit.get().isEmpty()) {
|
||||
dataModel.setBuyerSecurityDeposit(ParsingUtils.parsePercentStringToDouble(buyerSecurityDeposit.get()));
|
||||
dataModel.setBuyerSecurityDeposit(ParsingUtils.parsePercentStringToDouble(buyerSecurityDeposit.get()),
|
||||
persistPreference);
|
||||
} else {
|
||||
dataModel.setBuyerSecurityDeposit(Restrictions.getDefaultBuyerSecurityDepositAsPercent());
|
||||
dataModel.setBuyerSecurityDeposit(Restrictions.getDefaultBuyerSecurityDepositAsPercent(),
|
||||
persistPreference);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1157,7 +1160,7 @@ public abstract class MutableOfferViewModel<M extends MutableOfferDataModel> ext
|
|||
// If the security deposit in the model is not valid percent
|
||||
String value = FormattingUtils.formatToPercent(dataModel.getBuyerSecurityDeposit().get());
|
||||
if (!securityDepositValidator.validate(value).isValid) {
|
||||
dataModel.setBuyerSecurityDeposit(Restrictions.getDefaultBuyerSecurityDepositAsPercent());
|
||||
dataModel.setBuyerSecurityDeposit(Restrictions.getDefaultBuyerSecurityDepositAsPercent(), false);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -32,6 +32,7 @@ import bisq.core.offer.CreateOfferService;
|
|||
import bisq.core.offer.OpenOfferManager;
|
||||
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;
|
||||
|
@ -63,6 +64,7 @@ class CreateOfferDataModel extends MutableOfferDataModel {
|
|||
FeeService feeService,
|
||||
@Named(FormattingUtils.BTC_FORMATTER_KEY) CoinFormatter btcFormatter,
|
||||
MakerFeeProvider makerFeeProvider,
|
||||
TradeStatisticsManager tradeStatisticsManager,
|
||||
Navigation navigation) {
|
||||
super(createOfferService,
|
||||
openOfferManager,
|
||||
|
@ -76,6 +78,7 @@ class CreateOfferDataModel extends MutableOfferDataModel {
|
|||
feeService,
|
||||
btcFormatter,
|
||||
makerFeeProvider,
|
||||
tradeStatisticsManager,
|
||||
navigation);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -38,7 +38,6 @@ import bisq.desktop.main.offer.OfferView;
|
|||
import bisq.desktop.main.overlays.popups.Popup;
|
||||
import bisq.desktop.main.overlays.windows.OfferDetailsWindow;
|
||||
import bisq.desktop.util.CssTheme;
|
||||
import bisq.desktop.util.DisplayUtils;
|
||||
import bisq.desktop.util.FormBuilder;
|
||||
import bisq.desktop.util.GUIUtil;
|
||||
import bisq.desktop.util.Layout;
|
||||
|
@ -130,7 +129,7 @@ public class OfferBookView extends ActivatableViewAndModel<GridPane, OfferBookVi
|
|||
private AutocompleteComboBox<PaymentMethod> paymentMethodComboBox;
|
||||
private AutoTooltipButton createOfferButton;
|
||||
private AutoTooltipTableColumn<OfferBookListItem, OfferBookListItem> amountColumn, volumeColumn, marketColumn,
|
||||
priceColumn, paymentMethodColumn, signingStateColumn, avatarColumn;
|
||||
priceColumn, paymentMethodColumn, depositColumn, signingStateColumn, avatarColumn;
|
||||
private TableView<OfferBookListItem> tableView;
|
||||
|
||||
private OfferView.OfferActionHandler offerActionHandler;
|
||||
|
@ -224,6 +223,8 @@ public class OfferBookView extends ActivatableViewAndModel<GridPane, OfferBookVi
|
|||
tableView.getColumns().add(volumeColumn);
|
||||
paymentMethodColumn = getPaymentMethodColumn();
|
||||
tableView.getColumns().add(paymentMethodColumn);
|
||||
depositColumn = getDepositColumn();
|
||||
tableView.getColumns().add(depositColumn);
|
||||
signingStateColumn = getSigningStateColumn();
|
||||
tableView.getColumns().add(signingStateColumn);
|
||||
avatarColumn = getAvatarColumn();
|
||||
|
@ -245,6 +246,14 @@ public class OfferBookView extends ActivatableViewAndModel<GridPane, OfferBookVi
|
|||
volumeColumn.setComparator(Comparator.comparing(o -> o.getOffer().getMinVolume(), Comparator.nullsFirst(Comparator.naturalOrder())));
|
||||
paymentMethodColumn.setComparator(Comparator.comparing(o -> o.getOffer().getPaymentMethod()));
|
||||
avatarColumn.setComparator(Comparator.comparing(o -> o.getOffer().getOwnerNodeAddress().getFullAddress()));
|
||||
depositColumn.setComparator(Comparator.comparing(o -> {
|
||||
var isSellOffer = o.getOffer().getDirection() == OfferPayload.Direction.SELL;
|
||||
var deposit = isSellOffer ? o.getOffer().getBuyerSecurityDeposit() :
|
||||
o.getOffer().getSellerSecurityDeposit();
|
||||
|
||||
return (deposit == null) ? 0.0 : deposit.getValue() / (double) o.getOffer().getAmount().getValue();
|
||||
|
||||
}, Comparator.nullsFirst(Comparator.naturalOrder())));
|
||||
|
||||
nrOfOffersLabel = new AutoTooltipLabel("");
|
||||
nrOfOffersLabel.setId("num-offers");
|
||||
|
@ -927,10 +936,56 @@ public class OfferBookView extends ActivatableViewAndModel<GridPane, OfferBookVi
|
|||
return column;
|
||||
}
|
||||
|
||||
|
||||
private AutoTooltipTableColumn<OfferBookListItem, OfferBookListItem> getDepositColumn() {
|
||||
AutoTooltipTableColumn<OfferBookListItem, OfferBookListItem> column = new AutoTooltipTableColumn<>(
|
||||
Res.get("offerbook.deposit"),
|
||||
Res.get("offerbook.deposit.help")) {
|
||||
{
|
||||
setMinWidth(70);
|
||||
setSortable(true);
|
||||
}
|
||||
};
|
||||
|
||||
column.getStyleClass().add("number-column");
|
||||
column.setCellValueFactory((offer) -> new ReadOnlyObjectWrapper<>(offer.getValue()));
|
||||
column.setCellFactory(
|
||||
new Callback<>() {
|
||||
@Override
|
||||
public TableCell<OfferBookListItem, OfferBookListItem> call(
|
||||
TableColumn<OfferBookListItem, OfferBookListItem> column) {
|
||||
return new TableCell<>() {
|
||||
@Override
|
||||
public void updateItem(final OfferBookListItem item, boolean empty) {
|
||||
super.updateItem(item, empty);
|
||||
if (item != null && !empty) {
|
||||
var isSellOffer = item.getOffer().getDirection() == OfferPayload.Direction.SELL;
|
||||
var deposit = isSellOffer ? item.getOffer().getBuyerSecurityDeposit() :
|
||||
item.getOffer().getSellerSecurityDeposit();
|
||||
if (deposit == null) {
|
||||
setText(Res.get("shared.na"));
|
||||
setGraphic(null);
|
||||
} else {
|
||||
setText("");
|
||||
setGraphic(new ColoredDecimalPlacesWithZerosText(model.formatDepositString(
|
||||
deposit, item.getOffer().getAmount().getValue()),
|
||||
GUIUtil.AMOUNT_DECIMALS_WITH_ZEROS));
|
||||
}
|
||||
} else {
|
||||
setText("");
|
||||
setGraphic(null);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
});
|
||||
return column;
|
||||
}
|
||||
|
||||
private TableColumn<OfferBookListItem, OfferBookListItem> getActionColumn() {
|
||||
TableColumn<OfferBookListItem, OfferBookListItem> column = new AutoTooltipTableColumn<>(Res.get("shared.actions")) {
|
||||
{
|
||||
setMinWidth(200);
|
||||
setMinWidth(180);
|
||||
setSortable(false);
|
||||
}
|
||||
};
|
||||
|
@ -1147,8 +1202,8 @@ public class OfferBookView extends ActivatableViewAndModel<GridPane, OfferBookVi
|
|||
private AutoTooltipTableColumn<OfferBookListItem, OfferBookListItem> getAvatarColumn() {
|
||||
AutoTooltipTableColumn<OfferBookListItem, OfferBookListItem> column = new AutoTooltipTableColumn<>(Res.get("offerbook.trader")) {
|
||||
{
|
||||
setMinWidth(80);
|
||||
setMaxWidth(80);
|
||||
setMinWidth(60);
|
||||
setMaxWidth(60);
|
||||
setSortable(true);
|
||||
}
|
||||
};
|
||||
|
|
|
@ -47,8 +47,8 @@ import bisq.core.trade.Trade;
|
|||
import bisq.core.trade.closed.ClosedTradableManager;
|
||||
import bisq.core.user.Preferences;
|
||||
import bisq.core.user.User;
|
||||
import bisq.core.util.coin.BsqFormatter;
|
||||
import bisq.core.util.FormattingUtils;
|
||||
import bisq.core.util.coin.BsqFormatter;
|
||||
import bisq.core.util.coin.CoinFormatter;
|
||||
|
||||
import bisq.network.p2p.NodeAddress;
|
||||
|
@ -168,9 +168,7 @@ class OfferBookViewModel extends ActivatableViewModel {
|
|||
this.filteredItems = new FilteredList<>(offerBook.getOfferBookListItems());
|
||||
this.sortedItems = new SortedList<>(filteredItems);
|
||||
|
||||
tradeCurrencyListChangeListener = c -> {
|
||||
fillAllTradeCurrencies();
|
||||
};
|
||||
tradeCurrencyListChangeListener = c -> fillAllTradeCurrencies();
|
||||
|
||||
filterItemsListener = c -> {
|
||||
final Optional<OfferBookListItem> highestAmountOffer = filteredItems.stream()
|
||||
|
@ -645,4 +643,9 @@ class OfferBookViewModel extends ActivatableViewModel {
|
|||
else
|
||||
return (direction == OfferPayload.Direction.SELL) ? Res.get("shared.buyingCurrency", currencyCode) : Res.get("shared.sellingCurrency", currencyCode);
|
||||
}
|
||||
|
||||
public String formatDepositString(Coin deposit, long amount) {
|
||||
var percentage = FormattingUtils.formatToRoundedPercentWithSymbol(deposit.getValue() / (double) amount);
|
||||
return btcFormatter.formatCoin(deposit) + " (" + percentage + ")";
|
||||
}
|
||||
}
|
||||
|
|
|
@ -37,6 +37,7 @@ 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;
|
||||
|
@ -74,6 +75,7 @@ class EditOfferDataModel extends MutableOfferDataModel {
|
|||
@Named(FormattingUtils.BTC_FORMATTER_KEY) CoinFormatter btcFormatter,
|
||||
CorePersistenceProtoResolver corePersistenceProtoResolver,
|
||||
MakerFeeProvider makerFeeProvider,
|
||||
TradeStatisticsManager tradeStatisticsManager,
|
||||
Navigation navigation) {
|
||||
super(createOfferService,
|
||||
openOfferManager,
|
||||
|
@ -87,6 +89,7 @@ class EditOfferDataModel extends MutableOfferDataModel {
|
|||
feeService,
|
||||
btcFormatter,
|
||||
makerFeeProvider,
|
||||
tradeStatisticsManager,
|
||||
navigation);
|
||||
this.corePersistenceProtoResolver = corePersistenceProtoResolver;
|
||||
}
|
||||
|
|
|
@ -15,11 +15,14 @@ import bisq.core.payment.PaymentAccount;
|
|||
import bisq.core.payment.RevolutAccount;
|
||||
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 org.bitcoinj.core.Coin;
|
||||
|
||||
import javafx.collections.FXCollections;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.UUID;
|
||||
|
||||
|
@ -52,17 +55,19 @@ public class CreateOfferDataModelTest {
|
|||
CreateOfferService createOfferService = mock(CreateOfferService.class);
|
||||
preferences = mock(Preferences.class);
|
||||
user = mock(User.class);
|
||||
var tradeStats = mock(TradeStatisticsManager.class);
|
||||
|
||||
when(btcWalletService.getOrCreateAddressEntry(anyString(), any())).thenReturn(addressEntry);
|
||||
when(preferences.isUsePercentageBasedPrice()).thenReturn(true);
|
||||
when(preferences.getBuyerSecurityDepositAsPercent(null)).thenReturn(0.01);
|
||||
when(createOfferService.getRandomOfferId()).thenReturn(UUID.randomUUID().toString());
|
||||
when(tradeStats.getObservableTradeStatisticsSet()).thenReturn(FXCollections.observableSet());
|
||||
|
||||
makerFeeProvider = mock(MakerFeeProvider.class);
|
||||
model = new CreateOfferDataModel(createOfferService, null, btcWalletService,
|
||||
null, preferences, user, null,
|
||||
priceFeedService, null,
|
||||
feeService, null, makerFeeProvider, null);
|
||||
feeService, null, makerFeeProvider, tradeStats, null);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
|
@ -34,14 +34,16 @@ import bisq.core.locale.Res;
|
|||
import bisq.core.offer.CreateOfferService;
|
||||
import bisq.core.offer.OfferPayload;
|
||||
import bisq.core.payment.PaymentAccount;
|
||||
import bisq.core.payment.payload.PaymentMethod;
|
||||
import bisq.core.provider.fee.FeeService;
|
||||
import bisq.core.provider.price.MarketPrice;
|
||||
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.coin.ImmutableCoinFormatter;
|
||||
import bisq.core.util.coin.BsqFormatter;
|
||||
import bisq.core.util.coin.CoinFormatter;
|
||||
import bisq.core.util.coin.ImmutableCoinFormatter;
|
||||
import bisq.core.util.validation.InputValidator;
|
||||
|
||||
import bisq.common.config.Config;
|
||||
|
@ -95,6 +97,7 @@ public class CreateOfferViewModelTest {
|
|||
SecurityDepositValidator securityDepositValidator = mock(SecurityDepositValidator.class);
|
||||
AccountAgeWitnessService accountAgeWitnessService = mock(AccountAgeWitnessService.class);
|
||||
CreateOfferService createOfferService = mock(CreateOfferService.class);
|
||||
var tradeStats = mock(TradeStatisticsManager.class);
|
||||
|
||||
when(btcWalletService.getOrCreateAddressEntry(anyString(), any())).thenReturn(addressEntry);
|
||||
when(btcWalletService.getBalanceForAddress(any())).thenReturn(Coin.valueOf(1000L));
|
||||
|
@ -102,6 +105,7 @@ public class CreateOfferViewModelTest {
|
|||
when(priceFeedService.getMarketPrice(anyString())).thenReturn(new MarketPrice("USD", 12684.0450, Instant.now().getEpochSecond(), true));
|
||||
when(feeService.getTxFee(anyInt())).thenReturn(Coin.valueOf(1000L));
|
||||
when(user.findFirstPaymentAccountWithCurrency(any())).thenReturn(paymentAccount);
|
||||
when(paymentAccount.getPaymentMethod()).thenReturn(mock(PaymentMethod.class));
|
||||
when(user.getPaymentAccountsAsObservable()).thenReturn(FXCollections.observableSet());
|
||||
when(securityDepositValidator.validate(any())).thenReturn(new InputValidator.ValidationResult(false));
|
||||
when(accountAgeWitnessService.getMyTradeLimit(any(), any(), any())).thenReturn(100000000L);
|
||||
|
@ -109,11 +113,12 @@ public class CreateOfferViewModelTest {
|
|||
when(bsqFormatter.formatCoin(any())).thenReturn("0");
|
||||
when(bsqWalletService.getAvailableConfirmedBalance()).thenReturn(Coin.ZERO);
|
||||
when(createOfferService.getRandomOfferId()).thenReturn(UUID.randomUUID().toString());
|
||||
when(tradeStats.getObservableTradeStatisticsSet()).thenReturn(FXCollections.observableSet());
|
||||
|
||||
CreateOfferDataModel dataModel = new CreateOfferDataModel(createOfferService, null, btcWalletService,
|
||||
bsqWalletService, empty, user, null, priceFeedService,
|
||||
accountAgeWitnessService, feeService,
|
||||
coinFormatter, mock(MakerFeeProvider.class), null);
|
||||
coinFormatter, mock(MakerFeeProvider.class), tradeStats, null);
|
||||
dataModel.initWithData(OfferPayload.Direction.BUY, new CryptoCurrency("BTC", "bitcoin"));
|
||||
dataModel.activate();
|
||||
|
||||
|
|
|
@ -19,6 +19,7 @@ import bisq.core.payment.PaymentAccount;
|
|||
import bisq.core.provider.fee.FeeService;
|
||||
import bisq.core.provider.price.MarketPrice;
|
||||
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.coin.BsqFormatter;
|
||||
|
@ -95,7 +96,7 @@ public class EditOfferDataModelTest {
|
|||
btcWalletService, bsqWalletService, empty, user,
|
||||
null, priceFeedService,
|
||||
accountAgeWitnessService, feeService, null, null,
|
||||
mock(MakerFeeProvider.class), null);
|
||||
mock(MakerFeeProvider.class), mock(TradeStatisticsManager.class), null);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
Loading…
Add table
Reference in a new issue