Merge branch 'master' into 03-add-txFeeRate-param

This commit is contained in:
ghubstan 2020-12-08 19:04:27 -03:00
commit 2842070afd
No known key found for this signature in database
GPG Key ID: E35592D6800A861E
17 changed files with 402 additions and 257 deletions

View File

@ -110,7 +110,11 @@ public class PersistenceManager<T extends PersistableEnvelope> {
// For Priority.HIGH data we want to write to disk in any case to be on the safe side if we might have missed
// a requestPersistence call after an important state update. Those are usually rather small data stores.
// Otherwise we only persist if requestPersistence was called since the last persist call.
if (persistenceManager.source.flushAtShutDown || persistenceManager.persistenceRequested) {
// We also check if we have called read already to avoid a very early write attempt before we have ever
// read the data, which would lead to a write of empty data
// (fixes https://github.com/bisq-network/bisq/issues/4844).
if (persistenceManager.readCalled.get() &&
(persistenceManager.source.flushAtShutDown || persistenceManager.persistenceRequested)) {
// We always get our completeHandler called even if exceptions happen. In case a file write fails
// we still call our shutdown and count down routine as the completeHandler is triggered in any case.
@ -184,6 +188,7 @@ public class PersistenceManager<T extends PersistableEnvelope> {
private Timer timer;
private ExecutorService writeToDiskExecutor;
public final AtomicBoolean initCalled = new AtomicBoolean(false);
public final AtomicBoolean readCalled = new AtomicBoolean(false);
///////////////////////////////////////////////////////////////////////////////////////////
@ -303,6 +308,8 @@ public class PersistenceManager<T extends PersistableEnvelope> {
return null;
}
readCalled.set(true);
File storageFile = new File(dir, fileName);
if (!storageFile.exists()) {
return null;

View File

@ -533,11 +533,19 @@ public class AccountAgeWitnessService {
Coin tradeAmount,
ErrorMessageHandler errorMessageHandler) {
checkNotNull(offer);
// In case we don't find the witness we check if the trade amount is above the
// TOLERATED_SMALL_TRADE_AMOUNT (0.01 BTC) and only in that case return false.
return findWitness(offer)
.map(witness -> verifyPeersTradeLimit(offer, tradeAmount, witness, new Date(), errorMessageHandler))
.orElse(false);
.orElse(isToleratedSmalleAmount(tradeAmount));
}
private boolean isToleratedSmalleAmount(Coin tradeAmount) {
return tradeAmount.value <= OfferRestrictions.TOLERATED_SMALL_TRADE_AMOUNT.value;
}
///////////////////////////////////////////////////////////////////////////////////////////
// Package scope verification subroutines
///////////////////////////////////////////////////////////////////////////////////////////

View File

@ -45,6 +45,8 @@ import java.util.function.Consumer;
import lombok.extern.slf4j.Slf4j;
import javax.annotation.Nullable;
/**
* Provides high level interface to functionality of core Bisq features.
* E.g. useful for different APIs to access data of different domains of Bisq.
@ -208,8 +210,8 @@ public class CoreApi {
coreTradesService.keepFunds(tradeId);
}
public void withdrawFunds(String tradeId, String address) {
coreTradesService.withdrawFunds(tradeId, address);
public void withdrawFunds(String tradeId, String address, @Nullable String memo) {
coreTradesService.withdrawFunds(tradeId, address, memo);
}
public Trade getTrade(String tradeId) {

View File

@ -41,6 +41,8 @@ import java.util.function.Consumer;
import lombok.extern.slf4j.Slf4j;
import javax.annotation.Nullable;
import static bisq.core.btc.model.AddressEntry.Context.TRADE_PAYOUT;
import static java.lang.String.format;
@ -154,7 +156,7 @@ class CoreTradesService {
tradeManager.onTradeCompleted(trade);
}
void withdrawFunds(String tradeId, String toAddress) {
void withdrawFunds(String tradeId, String toAddress, @Nullable String memo) {
// An encrypted wallet must be unlocked for this operation.
verifyTradeIsNotClosed(tradeId);
var trade = getOpenTrade(tradeId).orElseThrow(() ->
@ -184,6 +186,7 @@ class CoreTradesService {
fee,
coreWalletsService.getKey(),
trade,
memo,
() -> {
},
(errorMessage, throwable) -> {

View File

@ -254,8 +254,8 @@ public class BtcWalletService extends WalletService {
sendRequest.signInputs = false;
sendRequest.fee = txFeePerVbyte.multiply(txVsizeWithUnsignedInputs +
sigSizePerInput * numLegacyInputs +
sigSizePerInput * numSegwitInputs / 4);
sigSizePerInput * numLegacyInputs +
sigSizePerInput * numSegwitInputs / 4);
sendRequest.feePerKb = Coin.ZERO;
sendRequest.ensureMinRequiredFee = false;
@ -274,8 +274,8 @@ public class BtcWalletService extends WalletService {
numSegwitInputs = numInputs.second;
txVsizeWithUnsignedInputs = resultTx.getVsize();
long estimatedFeeAsLong = txFeePerVbyte.multiply(txVsizeWithUnsignedInputs +
sigSizePerInput * numLegacyInputs +
sigSizePerInput * numSegwitInputs / 4).value;
sigSizePerInput * numLegacyInputs +
sigSizePerInput * numSegwitInputs / 4).value;
// calculated fee must be inside of a tolerance range with tx fee
isFeeOutsideTolerance = Math.abs(resultTx.getFee().value - estimatedFeeAsLong) > 1000;
@ -374,8 +374,8 @@ public class BtcWalletService extends WalletService {
sendRequest.signInputs = false;
sendRequest.fee = txFeePerVbyte.multiply(txVsizeWithUnsignedInputs +
sigSizePerInput * numLegacyInputs +
sigSizePerInput * numSegwitInputs / 4);
sigSizePerInput * numLegacyInputs +
sigSizePerInput * numSegwitInputs / 4);
sendRequest.feePerKb = Coin.ZERO;
sendRequest.ensureMinRequiredFee = false;
@ -393,8 +393,8 @@ public class BtcWalletService extends WalletService {
numSegwitInputs = numInputs.second;
txVsizeWithUnsignedInputs = resultTx.getVsize();
final long estimatedFeeAsLong = txFeePerVbyte.multiply(txVsizeWithUnsignedInputs +
sigSizePerInput * numLegacyInputs +
sigSizePerInput * numSegwitInputs / 4).value;
sigSizePerInput * numLegacyInputs +
sigSizePerInput * numSegwitInputs / 4).value;
// calculated fee must be inside of a tolerance range with tx fee
isFeeOutsideTolerance = Math.abs(resultTx.getFee().value - estimatedFeeAsLong) > 1000;
}
@ -593,7 +593,7 @@ public class BtcWalletService extends WalletService {
for (TransactionInput input : tx.getInputs()) {
TransactionOutput connectedOutput = input.getConnectedOutput();
if (connectedOutput == null || ScriptPattern.isP2PKH(connectedOutput.getScriptPubKey()) ||
ScriptPattern.isP2PK(connectedOutput.getScriptPubKey())) {
ScriptPattern.isP2PK(connectedOutput.getScriptPubKey())) {
// If connectedOutput is null, we don't know here the input type. To avoid underpaying fees,
// we treat it as a legacy input which will result in a higher fee estimation.
numLegacyInputs++;
@ -1110,12 +1110,15 @@ public class BtcWalletService extends WalletService {
Coin fee,
@Nullable KeyParameter aesKey,
@SuppressWarnings("SameParameterValue") AddressEntry.Context context,
@Nullable String memo,
FutureCallback<Transaction> callback) throws AddressFormatException,
AddressEntryException, InsufficientMoneyException {
SendRequest sendRequest = getSendRequest(fromAddress, toAddress, receiverAmount, fee, aesKey, context);
Wallet.SendResult sendResult = wallet.sendCoins(sendRequest);
Futures.addCallback(sendResult.broadcastComplete, callback, MoreExecutors.directExecutor());
if (memo != null) {
sendResult.tx.setMemo(memo);
}
printTx("sendFunds", sendResult.tx);
return sendResult.tx.getTxId().toString();
}
@ -1126,13 +1129,16 @@ public class BtcWalletService extends WalletService {
Coin fee,
@Nullable String changeAddress,
@Nullable KeyParameter aesKey,
@Nullable String memo,
FutureCallback<Transaction> callback) throws AddressFormatException,
AddressEntryException, InsufficientMoneyException {
SendRequest request = getSendRequestForMultipleAddresses(fromAddresses, toAddress, receiverAmount, fee, changeAddress, aesKey);
Wallet.SendResult sendResult = wallet.sendCoins(request);
Futures.addCallback(sendResult.broadcastComplete, callback, MoreExecutors.directExecutor());
if (memo != null) {
sendResult.tx.setMemo(memo);
}
printTx("sendFunds", sendResult.tx);
return sendResult.tx;
}

View File

@ -30,8 +30,10 @@ import bisq.core.provider.fee.FeeService;
import bisq.core.provider.price.MarketPrice;
import bisq.core.provider.price.PriceFeedService;
import bisq.core.trade.statistics.ReferralIdService;
import bisq.core.trade.statistics.TradeStatisticsManager;
import bisq.core.user.AutoConfirmSettings;
import bisq.core.user.Preferences;
import bisq.core.util.AveragePriceUtil;
import bisq.core.util.coin.CoinFormatter;
import bisq.core.util.coin.CoinUtil;
@ -39,6 +41,7 @@ import bisq.network.p2p.P2PService;
import bisq.common.app.Capabilities;
import bisq.common.util.MathUtils;
import bisq.common.util.Tuple2;
import org.bitcoinj.core.Coin;
import org.bitcoinj.utils.Fiat;
@ -80,6 +83,7 @@ public class OfferUtil {
private final PriceFeedService priceFeedService;
private final P2PService p2PService;
private final ReferralIdService referralIdService;
private final TradeStatisticsManager tradeStatisticsManager;
private final Predicate<String> isValidFeePaymentCurrencyCode = (c) ->
c.equalsIgnoreCase("BSQ") || c.equalsIgnoreCase("BTC");
@ -91,7 +95,8 @@ public class OfferUtil {
Preferences preferences,
PriceFeedService priceFeedService,
P2PService p2PService,
ReferralIdService referralIdService) {
ReferralIdService referralIdService,
TradeStatisticsManager tradeStatisticsManager) {
this.accountAgeWitnessService = accountAgeWitnessService;
this.bsqWalletService = bsqWalletService;
this.filterManager = filterManager;
@ -99,6 +104,7 @@ public class OfferUtil {
this.priceFeedService = priceFeedService;
this.p2PService = p2PService;
this.referralIdService = referralIdService;
this.tradeStatisticsManager = tradeStatisticsManager;
}
public void maybeSetFeePaymentCurrencyPreference(String feeCurrencyCode) {
@ -286,8 +292,14 @@ public class OfferUtil {
public Optional<Volume> getFeeInUserFiatCurrency(Coin makerFee,
boolean isCurrencyForMakerFeeBtc,
CoinFormatter bsqFormatter) {
String countryCode = preferences.getUserCountry().code;
String userCurrencyCode = CurrencyUtil.getCurrencyByCountryCode(countryCode).getCode();
String userCurrencyCode = preferences.getPreferredTradeCurrency().getCode();
if (CurrencyUtil.isCryptoCurrency(userCurrencyCode)) {
// In case the user has selected a altcoin as preferredTradeCurrency
// we derive the fiat currency from the user country
String countryCode = preferences.getUserCountry().code;
userCurrencyCode = CurrencyUtil.getCurrencyByCountryCode(countryCode).getCode();
}
return getFeeInUserFiatCurrency(makerFee,
isCurrencyForMakerFeeBtc,
userCurrencyCode,
@ -347,8 +359,6 @@ public class OfferUtil {
boolean isCurrencyForMakerFeeBtc,
String userCurrencyCode,
CoinFormatter bsqFormatter) {
// We use the users currency derived from his selected country. We don't use the
// preferredTradeCurrency from preferences as that can be also set to an altcoin.
MarketPrice marketPrice = priceFeedService.getMarketPrice(userCurrencyCode);
if (marketPrice != null && makerFee != null) {
long marketPriceAsLong = roundDoubleToLong(
@ -358,16 +368,16 @@ public class OfferUtil {
if (isCurrencyForMakerFeeBtc) {
return Optional.of(userCurrencyPrice.getVolumeByAmount(makerFee));
} else {
Optional<Price> optionalBsqPrice = priceFeedService.getBsqPrice();
if (optionalBsqPrice.isPresent()) {
Price bsqPrice = optionalBsqPrice.get();
String inputValue = bsqFormatter.formatCoin(makerFee);
Volume makerFeeAsVolume = Volume.parse(inputValue, "BSQ");
Coin requiredBtc = bsqPrice.getAmountByVolume(makerFeeAsVolume);
return Optional.of(userCurrencyPrice.getVolumeByAmount(requiredBtc));
} else {
return Optional.empty();
}
// We use the current market price for the fiat currency and the 30 day average BSQ price
Tuple2<Price, Price> tuple = AveragePriceUtil.getAveragePriceTuple(preferences,
tradeStatisticsManager,
30);
Price bsqPrice = tuple.second;
String inputValue = bsqFormatter.formatCoin(makerFee);
Volume makerFeeAsVolume = Volume.parse(inputValue, "BSQ");
Coin requiredBtc = bsqPrice.getAmountByVolume(makerFeeAsVolume);
Volume volumeByAmount = userCurrencyPrice.getVolumeByAmount(requiredBtc);
return Optional.of(volumeByAmount);
}
} else {
return Optional.empty();

View File

@ -462,8 +462,14 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi
// Complete trade
///////////////////////////////////////////////////////////////////////////////////////////
public void onWithdrawRequest(String toAddress, Coin amount, Coin fee, KeyParameter aesKey,
Trade trade, ResultHandler resultHandler, FaultHandler faultHandler) {
public void onWithdrawRequest(String toAddress,
Coin amount,
Coin fee,
KeyParameter aesKey,
Trade trade,
@Nullable String memo,
ResultHandler resultHandler,
FaultHandler faultHandler) {
String fromAddress = btcWalletService.getOrCreateAddressEntry(trade.getId(),
AddressEntry.Context.TRADE_PAYOUT).getAddressString();
FutureCallback<Transaction> callback = new FutureCallback<>() {
@ -487,7 +493,8 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi
}
};
try {
btcWalletService.sendFunds(fromAddress, toAddress, amount, fee, aesKey, AddressEntry.Context.TRADE_PAYOUT, callback);
btcWalletService.sendFunds(fromAddress, toAddress, amount, fee, aesKey,
AddressEntry.Context.TRADE_PAYOUT, memo, callback);
} catch (AddressFormatException | InsufficientMoneyException | AddressEntryException e) {
e.printStackTrace();
log.error(e.getMessage());

View File

@ -0,0 +1,139 @@
/*
* 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.util;
import bisq.core.monetary.Altcoin;
import bisq.core.monetary.Price;
import bisq.core.trade.statistics.TradeStatistics3;
import bisq.core.trade.statistics.TradeStatisticsManager;
import bisq.core.user.Preferences;
import bisq.common.util.MathUtils;
import bisq.common.util.Tuple2;
import org.bitcoinj.utils.Fiat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Comparator;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.List;
import java.util.stream.Collectors;
public class AveragePriceUtil {
private static final double HOW_MANY_STD_DEVS_CONSTITUTE_OUTLIER = 10;
public static Tuple2<Price, Price> getAveragePriceTuple(Preferences preferences,
TradeStatisticsManager tradeStatisticsManager,
int days) {
double percentToTrim = Math.max(0, Math.min(49, preferences.getBsqAverageTrimThreshold() * 100));
Date pastXDays = getPastDate(days);
List<TradeStatistics3> bsqAllTradePastXDays = tradeStatisticsManager.getObservableTradeStatisticsSet().stream()
.filter(e -> e.getCurrency().equals("BSQ"))
.filter(e -> e.getDate().after(pastXDays))
.collect(Collectors.toList());
List<TradeStatistics3> bsqTradePastXDays = percentToTrim > 0 ?
removeOutliers(bsqAllTradePastXDays, percentToTrim) :
bsqAllTradePastXDays;
List<TradeStatistics3> usdAllTradePastXDays = tradeStatisticsManager.getObservableTradeStatisticsSet().stream()
.filter(e -> e.getCurrency().equals("USD"))
.filter(e -> e.getDate().after(pastXDays))
.collect(Collectors.toList());
List<TradeStatistics3> usdTradePastXDays = percentToTrim > 0 ?
removeOutliers(usdAllTradePastXDays, percentToTrim) :
usdAllTradePastXDays;
Price usdPrice = Price.valueOf("USD", getUSDAverage(bsqTradePastXDays, usdTradePastXDays));
Price bsqPrice = Price.valueOf("BSQ", getBTCAverage(bsqTradePastXDays));
return new Tuple2<>(usdPrice, bsqPrice);
}
private static List<TradeStatistics3> removeOutliers(List<TradeStatistics3> list, double percentToTrim) {
List<Double> yValues = list.stream()
.filter(TradeStatistics3::isValid)
.map(e -> (double) e.getPrice())
.collect(Collectors.toList());
Tuple2<Double, Double> tuple = InlierUtil.findInlierRange(yValues, percentToTrim, HOW_MANY_STD_DEVS_CONSTITUTE_OUTLIER);
double lowerBound = tuple.first;
double upperBound = tuple.second;
return list.stream()
.filter(e -> e.getPrice() > lowerBound)
.filter(e -> e.getPrice() < upperBound)
.collect(Collectors.toList());
}
private static long getBTCAverage(List<TradeStatistics3> list) {
long accumulatedVolume = 0;
long accumulatedAmount = 0;
for (TradeStatistics3 item : list) {
accumulatedVolume += item.getTradeVolume().getValue();
accumulatedAmount += item.getTradeAmount().getValue(); // Amount of BTC traded
}
long averagePrice;
double accumulatedAmountAsDouble = MathUtils.scaleUpByPowerOf10((double) accumulatedAmount, Altcoin.SMALLEST_UNIT_EXPONENT);
averagePrice = accumulatedVolume > 0 ? MathUtils.roundDoubleToLong(accumulatedAmountAsDouble / (double) accumulatedVolume) : 0;
return averagePrice;
}
private static long getUSDAverage(List<TradeStatistics3> bsqList, List<TradeStatistics3> usdList) {
// Use next USD/BTC print as price to calculate BSQ/USD rate
// Store each trade as amount of USD and amount of BSQ traded
List<Tuple2<Double, Double>> usdBsqList = new ArrayList<>(bsqList.size());
usdList.sort(Comparator.comparing(TradeStatistics3::getDateAsLong));
var usdBTCPrice = 10000d; // Default to 10000 USD per BTC if there is no USD feed at all
for (TradeStatistics3 item : bsqList) {
// Find usdprice for trade item
usdBTCPrice = usdList.stream()
.filter(usd -> usd.getDateAsLong() > item.getDateAsLong())
.map(usd -> MathUtils.scaleDownByPowerOf10((double) usd.getTradePrice().getValue(),
Fiat.SMALLEST_UNIT_EXPONENT))
.findFirst()
.orElse(usdBTCPrice);
var bsqAmount = MathUtils.scaleDownByPowerOf10((double) item.getTradeVolume().getValue(),
Altcoin.SMALLEST_UNIT_EXPONENT);
var btcAmount = MathUtils.scaleDownByPowerOf10((double) item.getTradeAmount().getValue(),
Altcoin.SMALLEST_UNIT_EXPONENT);
usdBsqList.add(new Tuple2<>(usdBTCPrice * btcAmount, bsqAmount));
}
long averagePrice;
var usdTraded = usdBsqList.stream()
.mapToDouble(item -> item.first)
.sum();
var bsqTraded = usdBsqList.stream()
.mapToDouble(item -> item.second)
.sum();
var averageAsDouble = bsqTraded > 0 ? usdTraded / bsqTraded : 0d;
var averageScaledUp = MathUtils.scaleUpByPowerOf10(averageAsDouble, Fiat.SMALLEST_UNIT_EXPONENT);
averagePrice = bsqTraded > 0 ? MathUtils.roundDoubleToLong(averageScaledUp) : 0;
return averagePrice;
}
private static Date getPastDate(int days) {
Calendar cal = new GregorianCalendar();
cal.setTime(new Date());
cal.add(Calendar.DAY_OF_MONTH, -1 * days);
return cal.getTime();
}
}

View File

@ -0,0 +1,140 @@
/*
* 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.util;
import bisq.common.util.DoubleSummaryStatisticsWithStdDev;
import bisq.common.util.Tuple2;
import javafx.collections.FXCollections;
import java.util.DoubleSummaryStatistics;
import java.util.List;
import java.util.stream.Collectors;
public class InlierUtil {
/* Finds the minimum and maximum inlier values. The returned values may be NaN.
* See `computeInlierThreshold` for the definition of inlier.
*/
public static Tuple2<Double, Double> findInlierRange(
List<Double> yValues,
double percentToTrim,
double howManyStdDevsConstituteOutlier
) {
Tuple2<Double, Double> inlierThreshold =
computeInlierThreshold(yValues, percentToTrim, howManyStdDevsConstituteOutlier);
DoubleSummaryStatistics inlierStatistics =
yValues
.stream()
.filter(y -> withinBounds(inlierThreshold, y))
.mapToDouble(Double::doubleValue)
.summaryStatistics();
var inlierMin = inlierStatistics.getMin();
var inlierMax = inlierStatistics.getMax();
return new Tuple2<>(inlierMin, inlierMax);
}
private static boolean withinBounds(Tuple2<Double, Double> bounds, double number) {
var lowerBound = bounds.first;
var upperBound = bounds.second;
return (lowerBound <= number) && (number <= upperBound);
}
/* Computes the lower and upper inlier thresholds. A point lying outside
* these thresholds is considered an outlier, and a point lying within
* is considered an inlier.
* The thresholds are found by trimming the dataset (see method `trim`),
* then adding or subtracting a multiple of its (trimmed) standard
* deviation from its (trimmed) mean.
*/
private static Tuple2<Double, Double> computeInlierThreshold(
List<Double> numbers, double percentToTrim, double howManyStdDevsConstituteOutlier
) {
if (howManyStdDevsConstituteOutlier <= 0) {
throw new IllegalArgumentException(
"howManyStdDevsConstituteOutlier should be a positive number");
}
List<Double> trimmed = trim(percentToTrim, numbers);
DoubleSummaryStatisticsWithStdDev summaryStatistics =
trimmed.stream()
.collect(
DoubleSummaryStatisticsWithStdDev::new,
DoubleSummaryStatisticsWithStdDev::accept,
DoubleSummaryStatisticsWithStdDev::combine);
double mean = summaryStatistics.getAverage();
double stdDev = summaryStatistics.getStandardDeviation();
var inlierLowerThreshold = mean - (stdDev * howManyStdDevsConstituteOutlier);
var inlierUpperThreshold = mean + (stdDev * howManyStdDevsConstituteOutlier);
return new Tuple2<>(inlierLowerThreshold, inlierUpperThreshold);
}
/* Sorts the data and discards given percentage from the left and right sides each.
* E.g. 5% trim implies a total of 10% (2x 5%) of elements discarded.
* Used in calculating trimmed mean (and in turn trimmed standard deviation),
* which is more robust to outliers than a simple mean.
*/
private static List<Double> trim(double percentToTrim, List<Double> numbers) {
var minPercentToTrim = 0;
var maxPercentToTrim = 50;
if (minPercentToTrim > percentToTrim || percentToTrim > maxPercentToTrim) {
throw new IllegalArgumentException(
String.format(
"The percentage of data points to trim must be in the range [%d,%d].",
minPercentToTrim, maxPercentToTrim));
}
var totalPercentTrim = percentToTrim * 2;
if (totalPercentTrim == 0) {
return numbers;
}
if (totalPercentTrim == 100) {
return FXCollections.emptyObservableList();
}
if (numbers.isEmpty()) {
return numbers;
}
var count = numbers.size();
int countToDropFromEachSide = (int) Math.round((count / 100d) * percentToTrim); // visada >= 0?
if (countToDropFromEachSide == 0) {
return numbers;
}
var sorted = numbers.stream().sorted();
var oneSideTrimmed = sorted.skip(countToDropFromEachSide);
// Here, having already trimmed the left-side, we are implicitly trimming
// the right-side by specifying a limit to the stream's length.
// An explicit right-side drop/trim/skip is not supported by the Stream API.
var countAfterTrim = count - (countToDropFromEachSide * 2); // visada > 0? ir <= count?
var bothSidesTrimmed = oneSideTrimmed.limit(countAfterTrim);
return bothSidesTrimmed.collect(Collectors.toList());
}
}

View File

@ -144,7 +144,8 @@ class GrpcTradesService extends TradesGrpc.TradesImplBase {
public void withdrawFunds(WithdrawFundsRequest req,
StreamObserver<WithdrawFundsReply> responseObserver) {
try {
coreApi.withdrawFunds(req.getTradeId(), req.getAddress());
//TODO @ghubstan Feel free to add a memo param for withdrawal requests (was just added in UI)
coreApi.withdrawFunds(req.getTradeId(), req.getAddress(), null);
var reply = WithdrawFundsReply.newBuilder().build();
responseObserver.onNext(reply);
responseObserver.onCompleted();

View File

@ -20,28 +20,25 @@ package bisq.desktop.main.dao.economy.dashboard;
import bisq.desktop.common.view.ActivatableView;
import bisq.desktop.common.view.FxmlView;
import bisq.desktop.components.TextFieldWithIcon;
import bisq.desktop.util.AxisInlierUtils;
import bisq.core.dao.DaoFacade;
import bisq.core.dao.state.DaoStateListener;
import bisq.core.dao.state.model.blockchain.Block;
import bisq.core.dao.state.model.governance.IssuanceType;
import bisq.core.locale.Res;
import bisq.core.monetary.Altcoin;
import bisq.core.monetary.Price;
import bisq.core.provider.price.PriceFeedService;
import bisq.core.trade.statistics.TradeStatistics3;
import bisq.core.trade.statistics.TradeStatisticsManager;
import bisq.core.user.Preferences;
import bisq.core.util.AveragePriceUtil;
import bisq.core.util.FormattingUtils;
import bisq.core.util.coin.BsqFormatter;
import bisq.common.util.MathUtils;
import bisq.common.util.Tuple2;
import bisq.common.util.Tuple3;
import org.bitcoinj.core.Coin;
import org.bitcoinj.utils.Fiat;
import javax.inject.Inject;
@ -75,11 +72,7 @@ import java.time.format.FormatStyle;
import java.time.temporal.TemporalAdjuster;
import java.time.temporal.TemporalAdjusters;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Comparator;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@ -114,7 +107,6 @@ public class BsqDashboardView extends ActivatableView<GridPane, Void> implements
private Coin availableAmount;
private int gridRow = 0;
double howManyStdDevsConstituteOutlier = 10;
private Price avg30DayUSDPrice;
@ -374,110 +366,21 @@ public class BsqDashboardView extends ActivatableView<GridPane, Void> implements
}
private long updateAveragePriceField(TextField textField, int days, boolean isUSDField) {
double percentToTrim = Math.max(0, Math.min(49, preferences.getBsqAverageTrimThreshold() * 100));
Date pastXDays = getPastDate(days);
List<TradeStatistics3> bsqAllTradePastXDays = tradeStatisticsManager.getObservableTradeStatisticsSet().stream()
.filter(e -> e.getCurrency().equals("BSQ"))
.filter(e -> e.getDate().after(pastXDays))
.collect(Collectors.toList());
List<TradeStatistics3> bsqTradePastXDays = percentToTrim > 0 ?
removeOutliers(bsqAllTradePastXDays, percentToTrim) :
bsqAllTradePastXDays;
Tuple2<Price, Price> tuple = AveragePriceUtil.getAveragePriceTuple(preferences, tradeStatisticsManager, days);
Price usdPrice = tuple.first;
Price bsqPrice = tuple.second;
List<TradeStatistics3> usdAllTradePastXDays = tradeStatisticsManager.getObservableTradeStatisticsSet().stream()
.filter(e -> e.getCurrency().equals("USD"))
.filter(e -> e.getDate().after(pastXDays))
.collect(Collectors.toList());
List<TradeStatistics3> usdTradePastXDays = percentToTrim > 0 ?
removeOutliers(usdAllTradePastXDays, percentToTrim) :
usdAllTradePastXDays;
long average = isUSDField ? getUSDAverage(bsqTradePastXDays, usdTradePastXDays) :
getBTCAverage(bsqTradePastXDays);
Price avgPrice = isUSDField ? Price.valueOf("USD", average) :
Price.valueOf("BSQ", average);
String avg = FormattingUtils.formatPrice(avgPrice);
if (isUSDField) {
textField.setText(avg + " USD/BSQ");
textField.setText(usdPrice + " USD/BSQ");
if (days == 30) {
avg30DayUSDPrice = avgPrice;
avg30DayUSDPrice = usdPrice;
}
} else {
textField.setText(avg + " BSQ/BTC");
textField.setText(bsqPrice + " BSQ/BTC");
}
return average;
}
private List<TradeStatistics3> removeOutliers(List<TradeStatistics3> list, double percentToTrim) {
List<Double> yValues = list.stream()
.filter(TradeStatistics3::isValid)
.map(e -> (double) e.getPrice())
.collect(Collectors.toList());
Tuple2<Double, Double> tuple = AxisInlierUtils.findInlierRange(yValues, percentToTrim, howManyStdDevsConstituteOutlier);
double lowerBound = tuple.first;
double upperBound = tuple.second;
return list.stream()
.filter(e -> e.getPrice() > lowerBound)
.filter(e -> e.getPrice() < upperBound)
.collect(Collectors.toList());
}
private long getBTCAverage(List<TradeStatistics3> list) {
long accumulatedVolume = 0;
long accumulatedAmount = 0;
for (TradeStatistics3 item : list) {
accumulatedVolume += item.getTradeVolume().getValue();
accumulatedAmount += item.getTradeAmount().getValue(); // Amount of BTC traded
}
long averagePrice;
double accumulatedAmountAsDouble = MathUtils.scaleUpByPowerOf10((double) accumulatedAmount, Altcoin.SMALLEST_UNIT_EXPONENT);
averagePrice = accumulatedVolume > 0 ? MathUtils.roundDoubleToLong(accumulatedAmountAsDouble / (double) accumulatedVolume) : 0;
return averagePrice;
}
private long getUSDAverage(List<TradeStatistics3> bsqList, List<TradeStatistics3> usdList) {
// Use next USD/BTC print as price to calculate BSQ/USD rate
// Store each trade as amount of USD and amount of BSQ traded
List<Tuple2<Double, Double>> usdBsqList = new ArrayList<>(bsqList.size());
usdList.sort(Comparator.comparing(TradeStatistics3::getDateAsLong));
var usdBTCPrice = 10000d; // Default to 10000 USD per BTC if there is no USD feed at all
for (TradeStatistics3 item : bsqList) {
// Find usdprice for trade item
usdBTCPrice = usdList.stream()
.filter(usd -> usd.getDateAsLong() > item.getDateAsLong())
.map(usd -> MathUtils.scaleDownByPowerOf10((double) usd.getTradePrice().getValue(),
Fiat.SMALLEST_UNIT_EXPONENT))
.findFirst()
.orElse(usdBTCPrice);
var bsqAmount = MathUtils.scaleDownByPowerOf10((double) item.getTradeVolume().getValue(),
Altcoin.SMALLEST_UNIT_EXPONENT);
var btcAmount = MathUtils.scaleDownByPowerOf10((double) item.getTradeAmount().getValue(),
Altcoin.SMALLEST_UNIT_EXPONENT);
usdBsqList.add(new Tuple2<>(usdBTCPrice * btcAmount, bsqAmount));
}
long averagePrice;
var usdTraded = usdBsqList.stream()
.mapToDouble(item -> item.first)
.sum();
var bsqTraded = usdBsqList.stream()
.mapToDouble(item -> item.second)
.sum();
var averageAsDouble = bsqTraded > 0 ? usdTraded / bsqTraded : 0d;
var averageScaledUp = MathUtils.scaleUpByPowerOf10(averageAsDouble, Fiat.SMALLEST_UNIT_EXPONENT);
averagePrice = bsqTraded > 0 ? MathUtils.roundDoubleToLong(averageScaledUp) : 0;
return averagePrice;
}
private Date getPastDate(int days) {
Calendar cal = new GregorianCalendar();
cal.setTime(new Date());
cal.add(Calendar.DAY_OF_MONTH, -1 * days);
return cal.getTime();
Price average = isUSDField ? usdPrice : bsqPrice;
return average.getValue();
}
}

View File

@ -494,14 +494,19 @@ public class WithdrawalView extends ActivatableView<VBox, Void> {
private void sendFunds(Coin amount, Coin fee, KeyParameter aesKey, FutureCallback<Transaction> callback) {
try {
String memo = withdrawMemoTextField.getText();
if (memo.isEmpty()) {
memo = null;
}
Transaction transaction = btcWalletService.sendFundsForMultipleAddresses(fromAddresses,
withdrawToTextField.getText(),
amount,
fee,
null,
aesKey,
memo,
callback);
transaction.setMemo(withdrawMemoTextField.getText());
reset();
updateList();
} catch (AddressFormatException e) {

View File

@ -209,6 +209,7 @@ public class PendingTradesDataModel extends ActivatableDataModel {
Coin amount,
Coin fee,
KeyParameter aesKey,
@Nullable String memo,
ResultHandler resultHandler,
FaultHandler faultHandler) {
checkNotNull(getTrade(), "trade must not be null");
@ -220,6 +221,7 @@ public class PendingTradesDataModel extends ActivatableDataModel {
fee,
aesKey,
getTrade(),
memo,
() -> {
resultHandler.handleResult();
selectBestItem();

View File

@ -74,7 +74,7 @@ import static bisq.desktop.util.FormBuilder.addTitledGroupBg;
public class BuyerStep4View extends TradeStepView {
// private final ChangeListener<Boolean> focusedPropertyListener;
private InputTextField withdrawAddressTextField;
private InputTextField withdrawAddressTextField, withdrawMemoTextField;
private Button withdrawToExternalWalletButton, useSavingsWalletButton;
private TitledGroupBg withdrawTitledGroupBg;
@ -131,10 +131,17 @@ public class BuyerStep4View extends TradeStepView {
withdrawTitledGroupBg = addTitledGroupBg(gridPane, ++gridRow, 1, Res.get("portfolio.pending.step5_buyer.withdrawBTC"), Layout.COMPACT_GROUP_DISTANCE);
withdrawTitledGroupBg.getStyleClass().add("last");
addCompactTopLabelTextField(gridPane, gridRow, Res.get("portfolio.pending.step5_buyer.amount"), model.getPayoutAmount(), Layout.FIRST_ROW_AND_GROUP_DISTANCE);
withdrawAddressTextField = addInputTextField(gridPane, ++gridRow, Res.get("portfolio.pending.step5_buyer.withdrawToAddress"));
withdrawAddressTextField.setManaged(false);
withdrawAddressTextField.setVisible(false);
withdrawMemoTextField = addInputTextField(gridPane, ++gridRow,
Res.get("funds.withdrawal.memoLabel", Res.getBaseCurrencyCode()));
withdrawMemoTextField.setPromptText(Res.get("funds.withdrawal.memo"));
withdrawMemoTextField.setManaged(false);
withdrawMemoTextField.setVisible(false);
HBox hBox = new HBox();
hBox.setSpacing(10);
useSavingsWalletButton = new AutoTooltipButton(Res.get("portfolio.pending.step5_buyer.moveToBisqWallet"));
@ -170,7 +177,9 @@ public class BuyerStep4View extends TradeStepView {
private void onWithdrawal() {
withdrawAddressTextField.setManaged(true);
withdrawAddressTextField.setVisible(true);
GridPane.setRowSpan(withdrawTitledGroupBg, 2);
withdrawMemoTextField.setManaged(true);
withdrawMemoTextField.setVisible(true);
GridPane.setRowSpan(withdrawTitledGroupBg, 3);
withdrawToExternalWalletButton.setDefaultButton(true);
useSavingsWalletButton.setDefaultButton(false);
withdrawToExternalWalletButton.getStyleClass().add("action-button");
@ -271,10 +280,15 @@ public class BuyerStep4View extends TradeStepView {
FaultHandler faultHandler) {
useSavingsWalletButton.setDisable(true);
withdrawToExternalWalletButton.setDisable(true);
String memo = withdrawMemoTextField.getText();
if (memo.isEmpty()) {
memo = null;
}
model.dataModel.onWithdrawRequest(toAddress,
amount,
fee,
aesKey,
memo,
resultHandler,
faultHandler);
}

View File

@ -17,7 +17,8 @@
package bisq.desktop.util;
import bisq.common.util.DoubleSummaryStatisticsWithStdDev;
import bisq.core.util.InlierUtil;
import bisq.common.util.Tuple2;
import javafx.scene.chart.NumberAxis;
@ -25,9 +26,7 @@ import javafx.scene.chart.XYChart;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
import javafx.collections.FXCollections;
import java.util.DoubleSummaryStatistics;
import java.util.List;
import java.util.stream.Collectors;
@ -76,7 +75,7 @@ public class AxisInlierUtils {
}
Tuple2<Double, Double> inlierRange =
findInlierRange(yValues, percentToTrim, howManyStdDevsConstituteOutlier);
InlierUtil.findInlierRange(yValues, percentToTrim, howManyStdDevsConstituteOutlier);
applyRange(yAxis, maxNumberOfTicks, inlierRange);
}
@ -88,115 +87,6 @@ public class AxisInlierUtils {
.collect(Collectors.toList());
}
/* Finds the minimum and maximum inlier values. The returned values may be NaN.
* See `computeInlierThreshold` for the definition of inlier.
*/
public static Tuple2<Double, Double> findInlierRange(
List<Double> yValues,
double percentToTrim,
double howManyStdDevsConstituteOutlier
) {
Tuple2<Double, Double> inlierThreshold =
computeInlierThreshold(yValues, percentToTrim, howManyStdDevsConstituteOutlier);
DoubleSummaryStatistics inlierStatistics =
yValues
.stream()
.filter(y -> withinBounds(inlierThreshold, y))
.mapToDouble(Double::doubleValue)
.summaryStatistics();
var inlierMin = inlierStatistics.getMin();
var inlierMax = inlierStatistics.getMax();
return new Tuple2<>(inlierMin, inlierMax);
}
private static boolean withinBounds(Tuple2<Double, Double> bounds, double number) {
var lowerBound = bounds.first;
var upperBound = bounds.second;
return (lowerBound <= number) && (number <= upperBound);
}
/* Computes the lower and upper inlier thresholds. A point lying outside
* these thresholds is considered an outlier, and a point lying within
* is considered an inlier.
* The thresholds are found by trimming the dataset (see method `trim`),
* then adding or subtracting a multiple of its (trimmed) standard
* deviation from its (trimmed) mean.
*/
private static Tuple2<Double, Double> computeInlierThreshold(
List<Double> numbers, double percentToTrim, double howManyStdDevsConstituteOutlier
) {
if (howManyStdDevsConstituteOutlier <= 0) {
throw new IllegalArgumentException(
"howManyStdDevsConstituteOutlier should be a positive number");
}
List<Double> trimmed = trim(percentToTrim, numbers);
DoubleSummaryStatisticsWithStdDev summaryStatistics =
trimmed.stream()
.collect(
DoubleSummaryStatisticsWithStdDev::new,
DoubleSummaryStatisticsWithStdDev::accept,
DoubleSummaryStatisticsWithStdDev::combine);
double mean = summaryStatistics.getAverage();
double stdDev = summaryStatistics.getStandardDeviation();
var inlierLowerThreshold = mean - (stdDev * howManyStdDevsConstituteOutlier);
var inlierUpperThreshold = mean + (stdDev * howManyStdDevsConstituteOutlier);
return new Tuple2<>(inlierLowerThreshold, inlierUpperThreshold);
}
/* Sorts the data and discards given percentage from the left and right sides each.
* E.g. 5% trim implies a total of 10% (2x 5%) of elements discarded.
* Used in calculating trimmed mean (and in turn trimmed standard deviation),
* which is more robust to outliers than a simple mean.
*/
private static List<Double> trim(double percentToTrim, List<Double> numbers) {
var minPercentToTrim = 0;
var maxPercentToTrim = 50;
if (minPercentToTrim > percentToTrim || percentToTrim > maxPercentToTrim) {
throw new IllegalArgumentException(
String.format(
"The percentage of data points to trim must be in the range [%d,%d].",
minPercentToTrim, maxPercentToTrim));
}
var totalPercentTrim = percentToTrim * 2;
if (totalPercentTrim == 0) {
return numbers;
}
if (totalPercentTrim == 100) {
return FXCollections.emptyObservableList();
}
if (numbers.isEmpty()) {
return numbers;
}
var count = numbers.size();
int countToDropFromEachSide = (int) Math.round((count / 100d) * percentToTrim); // visada >= 0?
if (countToDropFromEachSide == 0) {
return numbers;
}
var sorted = numbers.stream().sorted();
var oneSideTrimmed = sorted.skip(countToDropFromEachSide);
// Here, having already trimmed the left-side, we are implicitly trimming
// the right-side by specifying a limit to the stream's length.
// An explicit right-side drop/trim/skip is not supported by the Stream API.
var countAfterTrim = count - (countToDropFromEachSide * 2); // visada > 0? ir <= count?
var bothSidesTrimmed = oneSideTrimmed.limit(countAfterTrim);
return bothSidesTrimmed.collect(Collectors.toList());
}
/* On the given axis, sets the provided lower and upper bounds, and
* computes an appropriate major tick unit (distance between major ticks in data-space).
* External computation of tick unit is necessary, because JavaFX doesn't support automatic

View File

@ -30,6 +30,8 @@ import bisq.common.UserThread;
import bisq.common.app.AppModule;
import bisq.common.app.Capabilities;
import bisq.common.app.Capability;
import bisq.common.config.BaseCurrencyNetwork;
import bisq.common.config.Config;
import bisq.common.handlers.ResultHandler;
import lombok.extern.slf4j.Slf4j;
@ -147,6 +149,12 @@ public class SeedNodeMain extends ExecutableForAppWithP2p {
}
private void setupConnectionLossCheck() {
// For dev testing (usually on BTC_REGTEST) we don't want to get the seed shut
// down as it is normal that the seed is the only actively running node.
if (Config.baseCurrencyNetwork() == BaseCurrencyNetwork.BTC_REGTEST) {
return;
}
if (checkConnectionLossTime != null) {
return;
}