Refactor offer/trade related classes in core and desktop

These refactoring changes are for reducing existing and potential
duplication coming with the addition of new trading protocol support
in the gRPC API.  Some minor styling and logic simplification changes
are also include.

- Convert OfferUtil to injected singleton, and move various offer related
  utility methods into it.

- Delete both MakerFeeProvider classes, which were wrappers around the same
  static old OfferUtil method.

- Inject OfferUtil into CreateOfferDataModel, CreateOfferViewModel,
  TakeOfferDataModel, TakeOfferViewModel, MutableOfferDataModel,
  MutableOfferViewModel, OfferDataModel, EditOfferDataModel,
  EditOfferViewModel

- Refactor TakeOfferViewModel

	Use OfferUtil, remove unused fields & methods.
	Made minor logic simplification, style and formatting changes.

- MutableOfferDataModel

	Made minor logic simplification, style and formatting changes.

- MutableOfferView uses new paymentAccount.isHalCashAccount().

- MutableOfferViewModel

	Refactored to use new VolumeUtil, CoinUtil, OfferUtil.
	Removed unused fields & accessors.
	Made minor style change.

- Refactored OfferDataModel to use new OfferUtil

- Refactor CreateOfferService

	Inject and use OfferUtil
	Move some utility methods to OfferUtil
	Remove unused fields

- Offer

	Refactored to use new VolumeUtil for volume calculations.
	Made stateProperty and errorMessageProperty fields private.

- PaymentAccount

	Moved isHalCashAccount type check to this class.
	Moved getTradeCurrency logic to this class.

- Contract, radeStatistics2, TradeStatistics3

	Refactored to use new VolumeUtil for volume calculations.

- Trade

	Refactored to use new VolumeUtil for volume calculations.
	Made minor logic simplification, style and formatting changes.

- CoinUtil

	Moved some coin utility methods into this class

- CoinUtilTest

	Moved (coin related) tests from CoinCryptoUtilsTest and OfferUtilTest
	into CoinUtilTest, and deleted OfferUtilTest, CoinCryptoUtilsTest.

- Adjust create and edit offer tests to model refactoring
This commit is contained in:
ghubstan 2020-10-20 15:06:44 -03:00
parent 50e2c89c06
commit ab6be23516
No known key found for this signature in database
GPG key ID: E35592D6800A861E
27 changed files with 667 additions and 656 deletions

View file

@ -17,21 +17,16 @@
package bisq.core.offer;
import bisq.core.account.witness.AccountAgeWitnessService;
import bisq.core.btc.TxFeeEstimationService;
import bisq.core.btc.wallet.BsqWalletService;
import bisq.core.btc.wallet.BtcWalletService;
import bisq.core.btc.wallet.Restrictions;
import bisq.core.filter.FilterManager;
import bisq.core.locale.CurrencyUtil;
import bisq.core.locale.Res;
import bisq.core.monetary.Price;
import bisq.core.payment.HalCashAccount;
import bisq.core.payment.PaymentAccount;
import bisq.core.payment.PaymentAccountUtil;
import bisq.core.provider.price.MarketPrice;
import bisq.core.provider.price.PriceFeedService;
import bisq.core.trade.statistics.ReferralIdService;
import bisq.core.user.Preferences;
import bisq.core.user.User;
import bisq.core.util.coin.CoinUtil;
@ -62,14 +57,9 @@ import lombok.extern.slf4j.Slf4j;
@Slf4j
@Singleton
public class CreateOfferService {
private final OfferUtil offerUtil;
private final TxFeeEstimationService txFeeEstimationService;
private final MakerFeeProvider makerFeeProvider;
private final BsqWalletService bsqWalletService;
private final Preferences preferences;
private final PriceFeedService priceFeedService;
private final AccountAgeWitnessService accountAgeWitnessService;
private final ReferralIdService referralIdService;
private final FilterManager filterManager;
private final P2PService p2PService;
private final PubKeyRing pubKeyRing;
private final User user;
@ -81,26 +71,16 @@ public class CreateOfferService {
///////////////////////////////////////////////////////////////////////////////////////////
@Inject
public CreateOfferService(TxFeeEstimationService txFeeEstimationService,
MakerFeeProvider makerFeeProvider,
BsqWalletService bsqWalletService,
Preferences preferences,
public CreateOfferService(OfferUtil offerUtil,
TxFeeEstimationService txFeeEstimationService,
PriceFeedService priceFeedService,
AccountAgeWitnessService accountAgeWitnessService,
ReferralIdService referralIdService,
FilterManager filterManager,
P2PService p2PService,
PubKeyRing pubKeyRing,
User user,
BtcWalletService btcWalletService) {
this.offerUtil = offerUtil;
this.txFeeEstimationService = txFeeEstimationService;
this.makerFeeProvider = makerFeeProvider;
this.bsqWalletService = bsqWalletService;
this.preferences = preferences;
this.priceFeedService = priceFeedService;
this.accountAgeWitnessService = accountAgeWitnessService;
this.referralIdService = referralIdService;
this.filterManager = filterManager;
this.p2PService = p2PService;
this.pubKeyRing = pubKeyRing;
this.user = user;
@ -161,7 +141,7 @@ public class CreateOfferService {
NodeAddress makerAddress = p2PService.getAddress();
boolean useMarketBasedPriceValue = useMarketBasedPrice &&
isMarketPriceAvailable(currencyCode) &&
!isHalCashAccount(paymentAccount);
!paymentAccount.isHalCashAccount();
long priceAsLong = price != null && !useMarketBasedPriceValue ? price.getValue() : 0L;
double marketPriceMarginParam = useMarketBasedPriceValue ? marketPriceMargin : 0;
@ -185,11 +165,11 @@ public class CreateOfferService {
double sellerSecurityDeposit = getSellerSecurityDepositAsDouble(buyerSecurityDepositAsDouble);
Coin txFeeFromFeeService = getEstimatedFeeAndTxSize(amount, direction, buyerSecurityDepositAsDouble, sellerSecurityDeposit).first;
Coin txFeeToUse = txFee.isPositive() ? txFee : txFeeFromFeeService;
Coin makerFeeAsCoin = getMakerFee(amount);
boolean isCurrencyForMakerFeeBtc = OfferUtil.isCurrencyForMakerFeeBtc(preferences, bsqWalletService, amount);
Coin makerFeeAsCoin = offerUtil.getMakerFee(amount);
boolean isCurrencyForMakerFeeBtc = offerUtil.isCurrencyForMakerFeeBtc(amount);
Coin buyerSecurityDepositAsCoin = getBuyerSecurityDeposit(amount, buyerSecurityDepositAsDouble);
Coin sellerSecurityDepositAsCoin = getSellerSecurityDeposit(amount, sellerSecurityDeposit);
long maxTradeLimit = getMaxTradeLimit(paymentAccount, currencyCode, direction);
long maxTradeLimit = offerUtil.getMaxTradeLimit(paymentAccount, currencyCode, direction);
long maxTradePeriod = paymentAccount.getMaxTradePeriod();
// reserved for future use cases
@ -200,15 +180,11 @@ public class CreateOfferService {
long lowerClosePrice = 0;
long upperClosePrice = 0;
String hashOfChallenge = null;
Map<String, String> extraDataMap = OfferUtil.getExtraDataMap(accountAgeWitnessService,
referralIdService,
paymentAccount,
Map<String, String> extraDataMap = offerUtil.getExtraDataMap(paymentAccount,
currencyCode,
preferences,
direction);
OfferUtil.validateOfferData(filterManager,
p2PService,
offerUtil.validateOfferData(
buyerSecurityDepositAsDouble,
paymentAccount,
currencyCode,
@ -261,8 +237,12 @@ public class CreateOfferService {
OfferPayload.Direction direction,
double buyerSecurityDeposit,
double sellerSecurityDeposit) {
Coin reservedFundsForOffer = getReservedFundsForOffer(direction, amount, buyerSecurityDeposit, sellerSecurityDeposit);
return txFeeEstimationService.getEstimatedFeeAndTxSizeForMaker(reservedFundsForOffer, getMakerFee(amount));
Coin reservedFundsForOffer = getReservedFundsForOffer(direction,
amount,
buyerSecurityDeposit,
sellerSecurityDeposit);
return txFeeEstimationService.getEstimatedFeeAndTxSizeForMaker(reservedFundsForOffer,
offerUtil.getMakerFee(amount));
}
public Coin getReservedFundsForOffer(OfferPayload.Direction direction,
@ -274,7 +254,7 @@ public class CreateOfferService {
amount,
buyerSecurityDeposit,
sellerSecurityDeposit);
if (!isBuyOffer(direction))
if (!offerUtil.isBuyOffer(direction))
reservedFundsForOffer = reservedFundsForOffer.add(amount);
return reservedFundsForOffer;
@ -284,7 +264,7 @@ public class CreateOfferService {
Coin amount,
double buyerSecurityDeposit,
double sellerSecurityDeposit) {
return isBuyOffer(direction) ?
return offerUtil.isBuyOffer(direction) ?
getBuyerSecurityDeposit(amount, buyerSecurityDeposit) :
getSellerSecurityDeposit(amount, sellerSecurityDeposit);
}
@ -294,25 +274,6 @@ public class CreateOfferService {
Restrictions.getSellerSecurityDepositAsPercent();
}
public Coin getMakerFee(Coin amount) {
return makerFeeProvider.getMakerFee(bsqWalletService, preferences, amount);
}
public long getMaxTradeLimit(PaymentAccount paymentAccount,
String currencyCode,
OfferPayload.Direction direction) {
if (paymentAccount != null) {
return accountAgeWitnessService.getMyTradeLimit(paymentAccount, currencyCode, direction);
} else {
return 0;
}
}
public boolean isBuyOffer(OfferPayload.Direction direction) {
return OfferUtil.isBuyOffer(direction);
}
///////////////////////////////////////////////////////////////////////////////////////////
// Private
///////////////////////////////////////////////////////////////////////////////////////////
@ -322,20 +283,13 @@ public class CreateOfferService {
return marketPrice != null && marketPrice.isExternallyProvidedPrice();
}
private boolean isHalCashAccount(PaymentAccount paymentAccount) {
return paymentAccount instanceof HalCashAccount;
}
private Coin getBuyerSecurityDeposit(Coin amount, double buyerSecurityDeposit) {
Coin percentOfAmountAsCoin = CoinUtil.getPercentOfAmountAsCoin(buyerSecurityDeposit, amount);
return getBoundedBuyerSecurityDeposit(percentOfAmountAsCoin);
}
private Coin getSellerSecurityDeposit(Coin amount, double sellerSecurityDeposit) {
Coin amountAsCoin = amount;
if (amountAsCoin == null)
amountAsCoin = Coin.ZERO;
Coin amountAsCoin = (amount == null) ? Coin.ZERO : amount;
Coin percentOfAmountAsCoin = CoinUtil.getPercentOfAmountAsCoin(sellerSecurityDeposit, amountAsCoin);
return getBoundedSellerSecurityDeposit(percentOfAmountAsCoin);
}

View file

@ -1,29 +0,0 @@
/*
* 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;
import bisq.core.btc.wallet.BsqWalletService;
import bisq.core.user.Preferences;
import org.bitcoinj.core.Coin;
public class MakerFeeProvider {
public Coin getMakerFee(BsqWalletService bsqWalletService, Preferences preferences, Coin amount) {
return OfferUtil.getMakerFee(bsqWalletService, preferences, amount);
}
}

View file

@ -27,6 +27,7 @@ import bisq.core.offer.availability.OfferAvailabilityProtocol;
import bisq.core.payment.payload.PaymentMethod;
import bisq.core.provider.price.MarketPrice;
import bisq.core.provider.price.PriceFeedService;
import bisq.core.util.VolumeUtil;
import bisq.network.p2p.NodeAddress;
@ -96,13 +97,13 @@ public class Offer implements NetworkPayload, PersistablePayload {
private final OfferPayload offerPayload;
@JsonExclude
@Getter
transient private ObjectProperty<Offer.State> stateProperty = new SimpleObjectProperty<>(Offer.State.UNKNOWN);
final transient private ObjectProperty<Offer.State> stateProperty = new SimpleObjectProperty<>(Offer.State.UNKNOWN);
@JsonExclude
@Nullable
transient private OfferAvailabilityProtocol availabilityProtocol;
@JsonExclude
@Getter
transient private StringProperty errorMessageProperty = new SimpleStringProperty();
final transient private StringProperty errorMessageProperty = new SimpleStringProperty();
@JsonExclude
@Nullable
@Setter
@ -231,9 +232,9 @@ public class Offer implements NetworkPayload, PersistablePayload {
if (price != null && amount != null) {
Volume volumeByAmount = price.getVolumeByAmount(amount);
if (offerPayload.getPaymentMethodId().equals(PaymentMethod.HAL_CASH_ID))
volumeByAmount = OfferUtil.getAdjustedVolumeForHalCash(volumeByAmount);
volumeByAmount = VolumeUtil.getAdjustedVolumeForHalCash(volumeByAmount);
else if (CurrencyUtil.isFiatCurrency(offerPayload.getCurrencyCode()))
volumeByAmount = OfferUtil.getRoundedFiatVolume(volumeByAmount);
volumeByAmount = VolumeUtil.getRoundedFiatVolume(volumeByAmount);
return volumeByAmount;
} else {

View file

@ -19,7 +19,6 @@ package bisq.core.offer;
import bisq.core.account.witness.AccountAgeWitnessService;
import bisq.core.btc.wallet.BsqWalletService;
import bisq.core.btc.wallet.Restrictions;
import bisq.core.filter.FilterManager;
import bisq.core.locale.CurrencyUtil;
import bisq.core.locale.Res;
@ -44,7 +43,8 @@ import bisq.common.util.MathUtils;
import org.bitcoinj.core.Coin;
import org.bitcoinj.utils.Fiat;
import com.google.common.annotations.VisibleForTesting;
import javax.inject.Inject;
import javax.inject.Singleton;
import java.util.HashMap;
import java.util.Map;
@ -54,95 +54,174 @@ import lombok.extern.slf4j.Slf4j;
import javax.annotation.Nullable;
import static bisq.common.util.MathUtils.roundDoubleToLong;
import static bisq.common.util.MathUtils.scaleUpByPowerOf10;
import static bisq.core.btc.wallet.Restrictions.getMaxBuyerSecurityDepositAsPercent;
import static bisq.core.btc.wallet.Restrictions.getMinBuyerSecurityDepositAsPercent;
import static bisq.core.btc.wallet.Restrictions.getMinNonDustOutput;
import static bisq.core.btc.wallet.Restrictions.isDust;
import static bisq.core.offer.OfferPayload.*;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
/**
* This class holds utility methods for the creation of an Offer.
* Most of these are extracted here because they are used both in the GUI and in the API.
* <p>
* Long-term there could be a GUI-agnostic OfferService which provides these and other functionality to both the
* GUI and the API.
* This class holds utility methods for creating, editing and taking an Offer.
*/
@Slf4j
@Singleton
public class OfferUtil {
private final AccountAgeWitnessService accountAgeWitnessService;
private final BsqWalletService bsqWalletService;
private final FilterManager filterManager;
private final Preferences preferences;
private final PriceFeedService priceFeedService;
private final P2PService p2PService;
private final ReferralIdService referralIdService;
@Inject
public OfferUtil(AccountAgeWitnessService accountAgeWitnessService,
BsqWalletService bsqWalletService,
FilterManager filterManager,
Preferences preferences,
PriceFeedService priceFeedService,
P2PService p2PService,
ReferralIdService referralIdService) {
this.accountAgeWitnessService = accountAgeWitnessService;
this.bsqWalletService = bsqWalletService;
this.filterManager = filterManager;
this.preferences = preferences;
this.priceFeedService = priceFeedService;
this.p2PService = p2PService;
this.referralIdService = referralIdService;
}
/**
* Given the direction, is this a BUY?
*
* @param direction the offer direction
* @return {@code true} for an offer to buy BTC from the taker, {@code false} for an offer to sell BTC to the taker
* @return {@code true} for an offer to buy BTC from the taker, {@code false} for an
* offer to sell BTC to the taker
*/
public static boolean isBuyOffer(OfferPayload.Direction direction) {
return direction == OfferPayload.Direction.BUY;
public boolean isBuyOffer(Direction direction) {
return direction == Direction.BUY;
}
public long getMaxTradeLimit(PaymentAccount paymentAccount,
String currencyCode,
Direction direction) {
return paymentAccount != null
? accountAgeWitnessService.getMyTradeLimit(paymentAccount, currencyCode, direction)
: 0;
}
/**
* Return true if a balance can cover a cost.
*
* @param cost the cost of a trade
* @param balance a wallet balance
* @return true if balance >= cost
*/
public boolean isBalanceSufficient(Coin cost, Coin balance) {
return cost != null && balance.compareTo(cost) >= 0;
}
/**
* Return the wallet balance shortage for a given trade cost, or zero if there is
* no shortage.
*
* @param cost the cost of a trade
* @param balance a wallet balance
* @return the wallet balance shortage for the given cost, else zero.
*/
public Coin getBalanceShortage(Coin cost, Coin balance) {
if (cost != null) {
Coin shortage = cost.subtract(balance);
return shortage.isNegative() ? Coin.ZERO : shortage;
} else {
return Coin.ZERO;
}
}
/**
* Returns the usable BSQ balance.
*
* @return Coin the usable BSQ balance
*/
public Coin getUsableBsqBalance() {
// We have to keep a minimum amount of BSQ == bitcoin dust limit, otherwise there
// would be dust violations for change UTXOs; essentially means the minimum usable
// balance of BSQ is 5.46.
Coin usableBsqBalance = bsqWalletService.getAvailableConfirmedBalance().subtract(getMinNonDustOutput());
return usableBsqBalance.isNegative() ? Coin.ZERO : usableBsqBalance;
}
public double calculateManualPrice(double volumeAsDouble, double amountAsDouble) {
return volumeAsDouble / amountAsDouble;
}
public double calculateMarketPriceMargin(double manualPrice, double marketPrice) {
return MathUtils.roundDouble(manualPrice / marketPrice, 4);
}
/**
* Returns the makerFee as Coin, this can be priced in BTC or BSQ.
*
* @param bsqWalletService wallet service used to check if there is enough BSQ to pay the fee
* @param preferences preferences are used to see if the user indicated a preference for paying fees in BTC
* @param amount the amount of BTC to trade
* @return the maker fee for the given trade amount, or {@code null} if the amount is {@code null}
* @return the maker fee for the given trade amount, or {@code null} if the amount
* is {@code null}
*/
@Nullable
public static Coin getMakerFee(BsqWalletService bsqWalletService, Preferences preferences, @Nullable Coin amount) {
boolean isCurrencyForMakerFeeBtc = isCurrencyForMakerFeeBtc(preferences, bsqWalletService, amount);
return getMakerFee(isCurrencyForMakerFeeBtc, amount);
public Coin getMakerFee(@Nullable Coin amount) {
boolean isCurrencyForMakerFeeBtc = isCurrencyForMakerFeeBtc(amount);
return CoinUtil.getMakerFee(isCurrencyForMakerFeeBtc, amount);
}
public Coin getTxFeeBySize(Coin txFeePerByteFromFeeService, int sizeInBytes) {
return txFeePerByteFromFeeService.multiply(getAverageTakerFeeTxSize(sizeInBytes));
}
// We use the sum of the size of the trade fee and the deposit tx to get an average.
// Miners will take the trade fee tx if the total fee of both dependent txs are good
// enough. With that we avoid that we overpay in case that the trade fee has many
// inputs and we would apply that fee for the other 2 txs as well. We still might
// overpay a bit for the payout tx.
public int getAverageTakerFeeTxSize(int txSize) {
return (txSize + 320) / 2;
}
/**
* Calculates the maker fee for the given amount, marketPrice and marketPriceMargin.
* Checks if the maker fee should be paid in BTC, this can be the case due to user
* preference or because the user doesn't have enough BSQ.
*
* @param isCurrencyForMakerFeeBtc {@code true} to pay fee in BTC, {@code false} to pay fee in BSQ
* @param amount the amount of BTC to trade
* @return the maker fee for the given trade amount, or {@code null} if the amount is {@code null}
*/
@Nullable
public static Coin getMakerFee(boolean isCurrencyForMakerFeeBtc, @Nullable Coin amount) {
if (amount != null) {
Coin feePerBtc = CoinUtil.getFeePerBtc(FeeService.getMakerFeePerBtc(isCurrencyForMakerFeeBtc), amount);
return CoinUtil.maxCoin(feePerBtc, FeeService.getMinMakerFee(isCurrencyForMakerFeeBtc));
} else {
return null;
}
}
/**
* Checks if the maker fee should be paid in BTC, this can be the case due to user preference or because the user
* doesn't have enough BSQ.
*
* @param preferences preferences are used to see if the user indicated a preference for paying fees in BTC
* @param bsqWalletService wallet service used to check if there is enough BSQ to pay the fee
* @param amount the amount of BTC to trade
* @return {@code true} if BTC is preferred or the trade amount is nonnull and there isn't enough BSQ for it
* @return {@code true} if BTC is preferred or the trade amount is nonnull and there
* isn't enough BSQ for it.
*/
public static boolean isCurrencyForMakerFeeBtc(Preferences preferences,
BsqWalletService bsqWalletService,
@Nullable Coin amount) {
public boolean isCurrencyForMakerFeeBtc(@Nullable Coin amount) {
boolean payFeeInBtc = preferences.getPayFeeInBtc();
boolean bsqForFeeAvailable = isBsqForMakerFeeAvailable(bsqWalletService, amount);
boolean bsqForFeeAvailable = isBsqForMakerFeeAvailable(amount);
return payFeeInBtc || !bsqForFeeAvailable;
}
/**
* Checks if the available BSQ balance is sufficient to pay for the offer's maker fee.
*
* @param bsqWalletService wallet service used to check if there is enough BSQ to pay the fee
* @param amount the amount of BTC to trade
* @return {@code true} if the balance is sufficient, {@code false} otherwise
*/
public static boolean isBsqForMakerFeeAvailable(BsqWalletService bsqWalletService, @Nullable Coin amount) {
public boolean isBsqForMakerFeeAvailable(@Nullable Coin amount) {
Coin availableBalance = bsqWalletService.getAvailableConfirmedBalance();
Coin makerFee = getMakerFee(false, amount);
Coin makerFee = CoinUtil.getMakerFee(false, amount);
// If we don't know yet the maker fee (amount is not set) we return true, otherwise we would disable BSQ
// fee each time we open the create offer screen as there the amount is not set.
// If we don't know yet the maker fee (amount is not set) we return true,
// otherwise we would disable BSQ fee each time we open the create offer screen
// as there the amount is not set.
if (makerFee == null)
return true;
Coin surplusFunds = availableBalance.subtract(makerFee);
if (Restrictions.isDust(surplusFunds)) {
if (isDust(surplusFunds)) {
return false; // we can't be left with dust
}
return !availableBalance.subtract(makerFee).isNegative();
@ -150,7 +229,7 @@ public class OfferUtil {
@Nullable
public static Coin getTakerFee(boolean isCurrencyForTakerFeeBtc, @Nullable Coin amount) {
public Coin getTakerFee(boolean isCurrencyForTakerFeeBtc, @Nullable Coin amount) {
if (amount != null) {
Coin feePerBtc = CoinUtil.getFeePerBtc(FeeService.getTakerFeePerBtc(isCurrencyForTakerFeeBtc), amount);
return CoinUtil.maxCoin(feePerBtc, FeeService.getMinTakerFee(isCurrencyForTakerFeeBtc));
@ -159,156 +238,99 @@ public class OfferUtil {
}
}
public static boolean isCurrencyForTakerFeeBtc(Preferences preferences,
BsqWalletService bsqWalletService,
Coin amount) {
public boolean isCurrencyForTakerFeeBtc(Coin amount) {
boolean payFeeInBtc = preferences.getPayFeeInBtc();
boolean bsqForFeeAvailable = isBsqForTakerFeeAvailable(bsqWalletService, amount);
boolean bsqForFeeAvailable = isBsqForTakerFeeAvailable(amount);
return payFeeInBtc || !bsqForFeeAvailable;
}
public static boolean isBsqForTakerFeeAvailable(BsqWalletService bsqWalletService, @Nullable Coin amount) {
public boolean isBsqForTakerFeeAvailable(@Nullable Coin amount) {
Coin availableBalance = bsqWalletService.getAvailableConfirmedBalance();
Coin takerFee = getTakerFee(false, amount);
// If we don't know yet the maker fee (amount is not set) we return true, otherwise we would disable BSQ
// fee each time we open the create offer screen as there the amount is not set.
// If we don't know yet the maker fee (amount is not set) we return true,
// otherwise we would disable BSQ fee each time we open the create offer screen
// as there the amount is not set.
if (takerFee == null)
return true;
Coin surplusFunds = availableBalance.subtract(takerFee);
if (Restrictions.isDust(surplusFunds)) {
if (isDust(surplusFunds)) {
return false; // we can't be left with dust
}
return !availableBalance.subtract(takerFee).isNegative();
}
public static Volume getRoundedFiatVolume(Volume volumeByAmount) {
// We want to get rounded to 1 unit of the fiat currency, e.g. 1 EUR.
return getAdjustedFiatVolume(volumeByAmount, 1);
}
public static Volume getAdjustedVolumeForHalCash(Volume volumeByAmount) {
// EUR has precision 4 and we want multiple of 10 so we divide by 100000 then
// round and multiply with 10
return getAdjustedFiatVolume(volumeByAmount, 10);
}
/**
*
* @param volumeByAmount The volume generated from an amount
* @param factor The factor used for rounding. E.g. 1 means rounded to units of 1 EUR, 10 means rounded to 10 EUR...
* @return The adjusted Fiat volume
*/
@VisibleForTesting
static Volume getAdjustedFiatVolume(Volume volumeByAmount, int factor) {
// Fiat currencies use precision 4 and we want multiple of factor so we divide by 10000 * factor then
// round and multiply with factor
long roundedVolume = Math.round((double) volumeByAmount.getValue() / (10000d * factor)) * factor;
// Smallest allowed volume is factor (e.g. 10 EUR or 1 EUR,...)
roundedVolume = Math.max(factor, roundedVolume);
return Volume.parse(String.valueOf(roundedVolume), volumeByAmount.getCurrencyCode());
}
/**
* Calculate the possibly adjusted amount for {@code amount}, taking into account the
* {@code price} and {@code maxTradeLimit} and {@code factor}.
*
* @param amount Bitcoin amount which is a candidate for getting rounded.
* @param price Price used in relation to that amount.
* @param maxTradeLimit The max. trade limit of the users account, in satoshis.
* @return The adjusted amount
*/
public static Coin getRoundedFiatAmount(Coin amount, Price price, long maxTradeLimit) {
return getAdjustedAmount(amount, price, maxTradeLimit, 1);
}
public static Coin getAdjustedAmountForHalCash(Coin amount, Price price, long maxTradeLimit) {
return getAdjustedAmount(amount, price, maxTradeLimit, 10);
}
/**
* Calculate the possibly adjusted amount for {@code amount}, taking into account the
* {@code price} and {@code maxTradeLimit} and {@code factor}.
*
* @param amount Bitcoin amount which is a candidate for getting rounded.
* @param price Price used in relation to that amount.
* @param maxTradeLimit The max. trade limit of the users account, in satoshis.
* @param factor The factor used for rounding. E.g. 1 means rounded to units of
* 1 EUR, 10 means rounded to 10 EUR, etc.
* @return The adjusted amount
*/
@VisibleForTesting
static Coin getAdjustedAmount(Coin amount, Price price, long maxTradeLimit, int factor) {
checkArgument(
amount.getValue() >= 10_000,
"amount needs to be above minimum of 10k satoshis"
);
checkArgument(
factor > 0,
"factor needs to be positive"
);
// Amount must result in a volume of min factor units of the fiat currency, e.g. 1 EUR or
// 10 EUR in case of HalCash.
Volume smallestUnitForVolume = Volume.parse(String.valueOf(factor), price.getCurrencyCode());
if (smallestUnitForVolume.getValue() <= 0)
return Coin.ZERO;
Coin smallestUnitForAmount = price.getAmountByVolume(smallestUnitForVolume);
long minTradeAmount = Restrictions.getMinTradeAmount().value;
// We use 10 000 satoshi as min allowed amount
checkArgument(
minTradeAmount >= 10_000,
"MinTradeAmount must be at least 10k satoshis"
);
smallestUnitForAmount = Coin.valueOf(Math.max(minTradeAmount, smallestUnitForAmount.value));
// We don't allow smaller amount values than smallestUnitForAmount
if (amount.compareTo(smallestUnitForAmount) < 0)
amount = smallestUnitForAmount;
// We get the adjusted volume from our amount
Volume volume = getAdjustedFiatVolume(price.getVolumeByAmount(amount), factor);
if (volume.getValue() <= 0)
return Coin.ZERO;
// From that adjusted volume we calculate back the amount. It might be a bit different as
// the amount used as input before due rounding.
amount = price.getAmountByVolume(volume);
// For the amount we allow only 4 decimal places
long adjustedAmount = Math.round((double) amount.value / 10000d) * 10000;
// If we are above our trade limit we reduce the amount by the smallestUnitForAmount
while (adjustedAmount > maxTradeLimit) {
adjustedAmount -= smallestUnitForAmount.value;
}
adjustedAmount = Math.max(minTradeAmount, adjustedAmount);
adjustedAmount = Math.min(maxTradeLimit, adjustedAmount);
return Coin.valueOf(adjustedAmount);
}
public static Optional<Volume> getFeeInUserFiatCurrency(Coin makerFee, boolean isCurrencyForMakerFeeBtc,
Preferences preferences, PriceFeedService priceFeedService,
CoinFormatter bsqFormatter) {
public Optional<Volume> getFeeInUserFiatCurrency(Coin makerFee,
boolean isCurrencyForMakerFeeBtc,
CoinFormatter bsqFormatter) {
String countryCode = preferences.getUserCountry().code;
String userCurrencyCode = CurrencyUtil.getCurrencyByCountryCode(countryCode).getCode();
return getFeeInUserFiatCurrency(makerFee,
isCurrencyForMakerFeeBtc,
userCurrencyCode,
priceFeedService,
bsqFormatter);
}
private static Optional<Volume> getFeeInUserFiatCurrency(Coin makerFee, boolean isCurrencyForMakerFeeBtc,
String userCurrencyCode, PriceFeedService priceFeedService,
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.
public Map<String, String> getExtraDataMap(PaymentAccount paymentAccount,
String currencyCode,
Direction direction) {
Map<String, String> extraDataMap = new HashMap<>();
if (CurrencyUtil.isFiatCurrency(currencyCode)) {
String myWitnessHashAsHex = accountAgeWitnessService
.getMyWitnessHashAsHex(paymentAccount.getPaymentAccountPayload());
extraDataMap.put(ACCOUNT_AGE_WITNESS_HASH, myWitnessHashAsHex);
}
if (referralIdService.getOptionalReferralId().isPresent()) {
extraDataMap.put(REFERRAL_ID, referralIdService.getOptionalReferralId().get());
}
if (paymentAccount instanceof F2FAccount) {
extraDataMap.put(F2F_CITY, ((F2FAccount) paymentAccount).getCity());
extraDataMap.put(F2F_EXTRA_INFO, ((F2FAccount) paymentAccount).getExtraInfo());
}
extraDataMap.put(CAPABILITIES, Capabilities.app.toStringList());
if (currencyCode.equals("XMR") && direction == Direction.SELL) {
preferences.getAutoConfirmSettingsList().stream()
.filter(e -> e.getCurrencyCode().equals("XMR"))
.filter(AutoConfirmSettings::isEnabled)
.forEach(e -> extraDataMap.put(XMR_AUTO_CONF, XMR_AUTO_CONF_ENABLED_VALUE));
}
return extraDataMap.isEmpty() ? null : extraDataMap;
}
public void validateOfferData(double buyerSecurityDeposit,
PaymentAccount paymentAccount,
String currencyCode,
Coin makerFeeAsCoin) {
checkNotNull(makerFeeAsCoin, "makerFee must not be null");
checkNotNull(p2PService.getAddress(), "Address must not be null");
checkArgument(buyerSecurityDeposit <= getMaxBuyerSecurityDepositAsPercent(),
"securityDeposit must not exceed " +
getMaxBuyerSecurityDepositAsPercent());
checkArgument(buyerSecurityDeposit >= getMinBuyerSecurityDepositAsPercent(),
"securityDeposit must not be less than " +
getMinBuyerSecurityDepositAsPercent());
checkArgument(!filterManager.isCurrencyBanned(currencyCode),
Res.get("offerbook.warning.currencyBanned"));
checkArgument(!filterManager.isPaymentMethodBanned(paymentAccount.getPaymentMethod()),
Res.get("offerbook.warning.paymentMethodBanned"));
}
private Optional<Volume> getFeeInUserFiatCurrency(Coin makerFee,
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 = MathUtils.roundDoubleToLong(MathUtils.scaleUpByPowerOf10(marketPrice.getPrice(), Fiat.SMALLEST_UNIT_EXPONENT));
long marketPriceAsLong = roundDoubleToLong(
scaleUpByPowerOf10(marketPrice.getPrice(), Fiat.SMALLEST_UNIT_EXPONENT));
Price userCurrencyPrice = Price.valueOf(userCurrencyCode, marketPriceAsLong);
if (isCurrencyForMakerFeeBtc) {
@ -329,68 +351,4 @@ public class OfferUtil {
return Optional.empty();
}
}
public static Map<String, String> getExtraDataMap(AccountAgeWitnessService accountAgeWitnessService,
ReferralIdService referralIdService,
PaymentAccount paymentAccount,
String currencyCode,
Preferences preferences,
OfferPayload.Direction direction) {
Map<String, String> extraDataMap = new HashMap<>();
if (CurrencyUtil.isFiatCurrency(currencyCode)) {
String myWitnessHashAsHex = accountAgeWitnessService.getMyWitnessHashAsHex(paymentAccount.getPaymentAccountPayload());
extraDataMap.put(OfferPayload.ACCOUNT_AGE_WITNESS_HASH, myWitnessHashAsHex);
}
if (referralIdService.getOptionalReferralId().isPresent()) {
extraDataMap.put(OfferPayload.REFERRAL_ID, referralIdService.getOptionalReferralId().get());
}
if (paymentAccount instanceof F2FAccount) {
extraDataMap.put(OfferPayload.F2F_CITY, ((F2FAccount) paymentAccount).getCity());
extraDataMap.put(OfferPayload.F2F_EXTRA_INFO, ((F2FAccount) paymentAccount).getExtraInfo());
}
extraDataMap.put(OfferPayload.CAPABILITIES, Capabilities.app.toStringList());
if (currencyCode.equals("XMR") && direction == OfferPayload.Direction.SELL) {
preferences.getAutoConfirmSettingsList().stream()
.filter(e -> e.getCurrencyCode().equals("XMR"))
.filter(AutoConfirmSettings::isEnabled)
.forEach(e -> extraDataMap.put(OfferPayload.XMR_AUTO_CONF, OfferPayload.XMR_AUTO_CONF_ENABLED_VALUE));
}
return extraDataMap.isEmpty() ? null : extraDataMap;
}
public static void validateOfferData(FilterManager filterManager,
P2PService p2PService,
double buyerSecurityDeposit,
PaymentAccount paymentAccount,
String currencyCode,
Coin makerFeeAsCoin) {
checkNotNull(makerFeeAsCoin, "makerFee must not be null");
checkNotNull(p2PService.getAddress(), "Address must not be null");
checkArgument(buyerSecurityDeposit <= Restrictions.getMaxBuyerSecurityDepositAsPercent(),
"securityDeposit must not exceed " +
Restrictions.getMaxBuyerSecurityDepositAsPercent());
checkArgument(buyerSecurityDeposit >= Restrictions.getMinBuyerSecurityDepositAsPercent(),
"securityDeposit must not be less than " +
Restrictions.getMinBuyerSecurityDepositAsPercent());
checkArgument(!filterManager.isCurrencyBanned(currencyCode),
Res.get("offerbook.warning.currencyBanned"));
checkArgument(!filterManager.isPaymentMethodBanned(paymentAccount.getPaymentMethod()),
Res.get("offerbook.warning.paymentMethodBanned"));
}
// TODO no code duplication found in UI code (added for API)
/* public static Coin getFundsNeededForOffer(Coin tradeAmount, Coin buyerSecurityDeposit, OfferPayload.Direction direction) {
boolean buyOffer = isBuyOffer(direction);
Coin needed = buyOffer ? buyerSecurityDeposit : Restrictions.getSellerSecurityDeposit();
if (!buyOffer)
needed = needed.add(tradeAmount);
return needed;
}*/
}

View file

@ -126,8 +126,7 @@ public abstract class PaymentAccount implements PersistablePayload {
}
public void removeCurrency(TradeCurrency tradeCurrency) {
if (tradeCurrencies.contains(tradeCurrency))
tradeCurrencies.remove(tradeCurrency);
tradeCurrencies.remove(tradeCurrency);
}
public boolean hasMultipleCurrencies() {
@ -174,6 +173,30 @@ public abstract class PaymentAccount implements PersistablePayload {
return paymentAccountPayload.getOwnerId();
}
public boolean isHalCashAccount() {
return this instanceof HalCashAccount;
}
/**
* Return an Optional of the trade currency for this payment account, or
* Optional.empty() if none is found. If this payment account has a selected
* trade currency, that is returned, else its single trade currency is returned,
* else the first trade currency in the this payment account's tradeCurrencies
* list is returned.
*
* @return Optional of the trade currency for the given payment account
*/
public Optional<TradeCurrency> getTradeCurrency() {
if (this.getSelectedTradeCurrency() != null)
return Optional.of(this.getSelectedTradeCurrency());
else if (this.getSingleTradeCurrency() != null)
return Optional.of(this.getSingleTradeCurrency());
else if (!this.getTradeCurrencies().isEmpty())
return Optional.of(this.getTradeCurrencies().get(0));
else
return Optional.empty();
}
public void onAddToUser() {
// We are in the process to get added to the user. This is called just before saving the account and the
// last moment we could apply some special handling if needed (e.g. as it happens for Revolut)

View file

@ -21,10 +21,10 @@ import bisq.core.locale.CurrencyUtil;
import bisq.core.monetary.Price;
import bisq.core.monetary.Volume;
import bisq.core.offer.OfferPayload;
import bisq.core.offer.OfferUtil;
import bisq.core.payment.payload.PaymentAccountPayload;
import bisq.core.payment.payload.PaymentMethod;
import bisq.core.proto.CoreProtoResolver;
import bisq.core.util.VolumeUtil;
import bisq.network.p2p.NodeAddress;
@ -231,9 +231,9 @@ public final class Contract implements NetworkPayload {
Volume volumeByAmount = getTradePrice().getVolumeByAmount(getTradeAmount());
if (getPaymentMethodId().equals(PaymentMethod.HAL_CASH_ID))
volumeByAmount = OfferUtil.getAdjustedVolumeForHalCash(volumeByAmount);
volumeByAmount = VolumeUtil.getAdjustedVolumeForHalCash(volumeByAmount);
else if (CurrencyUtil.isFiatCurrency(getOfferPayload().getCurrencyCode()))
volumeByAmount = OfferUtil.getRoundedFiatVolume(volumeByAmount);
volumeByAmount = VolumeUtil.getRoundedFiatVolume(volumeByAmount);
return volumeByAmount;
}

View file

@ -22,7 +22,6 @@ import bisq.core.locale.CurrencyUtil;
import bisq.core.monetary.Price;
import bisq.core.monetary.Volume;
import bisq.core.offer.Offer;
import bisq.core.offer.OfferUtil;
import bisq.core.payment.payload.PaymentMethod;
import bisq.core.proto.CoreProtoResolver;
import bisq.core.support.dispute.arbitration.arbitrator.Arbitrator;
@ -32,6 +31,7 @@ import bisq.core.support.messages.ChatMessage;
import bisq.core.trade.protocol.ProcessModel;
import bisq.core.trade.protocol.ProcessModelServiceProvider;
import bisq.core.trade.txproof.AssetTxProofResult;
import bisq.core.util.VolumeUtil;
import bisq.network.p2p.NodeAddress;
@ -623,13 +623,11 @@ public abstract class Trade implements Tradable, Model {
arbitratorPubKeyRing = arbitrator.getPubKeyRing();
});
serviceProvider.getMediatorManager().getDisputeAgentByNodeAddress(mediatorNodeAddress).ifPresent(mediator -> {
mediatorPubKeyRing = mediator.getPubKeyRing();
});
serviceProvider.getMediatorManager().getDisputeAgentByNodeAddress(mediatorNodeAddress)
.ifPresent(mediator -> mediatorPubKeyRing = mediator.getPubKeyRing());
serviceProvider.getRefundAgentManager().getDisputeAgentByNodeAddress(refundAgentNodeAddress).ifPresent(refundAgent -> {
refundAgentPubKeyRing = refundAgent.getPubKeyRing();
});
serviceProvider.getRefundAgentManager().getDisputeAgentByNodeAddress(refundAgentNodeAddress)
.ifPresent(refundAgent -> refundAgentPubKeyRing = refundAgent.getPubKeyRing());
isInitialized = true;
}
@ -831,9 +829,9 @@ public abstract class Trade implements Tradable, Model {
Volume volumeByAmount = getTradePrice().getVolumeByAmount(getTradeAmount());
if (offer != null) {
if (offer.getPaymentMethod().getId().equals(PaymentMethod.HAL_CASH_ID))
volumeByAmount = OfferUtil.getAdjustedVolumeForHalCash(volumeByAmount);
volumeByAmount = VolumeUtil.getAdjustedVolumeForHalCash(volumeByAmount);
else if (CurrencyUtil.isFiatCurrency(offer.getCurrencyCode()))
volumeByAmount = OfferUtil.getRoundedFiatVolume(volumeByAmount);
volumeByAmount = VolumeUtil.getRoundedFiatVolume(volumeByAmount);
}
return volumeByAmount;
} else {
@ -864,15 +862,15 @@ public abstract class Trade implements Tradable, Model {
if (depositTx.getConfidence().getDepthInBlocks() > 0) {
final long tradeTime = getTakeOfferDate().getTime();
// Use tx.getIncludedInBestChainAt() when available, otherwise use tx.getUpdateTime()
long blockTime = depositTx.getIncludedInBestChainAt() != null ? depositTx.getIncludedInBestChainAt().getTime() : depositTx.getUpdateTime().getTime();
long blockTime = depositTx.getIncludedInBestChainAt() != null
? depositTx.getIncludedInBestChainAt().getTime()
: depositTx.getUpdateTime().getTime();
// If block date is in future (Date in Bitcoin blocks can be off by +/- 2 hours) we use our current date.
// If block date is earlier than our trade date we use our trade date.
if (blockTime > now)
startTime = now;
else if (blockTime < tradeTime)
startTime = tradeTime;
else
startTime = blockTime;
startTime = Math.max(blockTime, tradeTime);
log.debug("We set the start for the trade period to {}. Trade started at: {}. Block got mined at: {}",
new Date(startTime), new Date(tradeTime), new Date(blockTime));
@ -929,13 +927,9 @@ public abstract class Trade implements Tradable, Model {
// In refund agent case the funds are spent anyway with the time locked payout. We do not consider that as
// locked in funds.
if (disputeState == DisputeState.REFUND_REQUESTED ||
disputeState == DisputeState.REFUND_REQUEST_STARTED_BY_PEER ||
disputeState == DisputeState.REFUND_REQUEST_CLOSED) {
return false;
}
return true;
return disputeState != DisputeState.REFUND_REQUESTED &&
disputeState != DisputeState.REFUND_REQUEST_STARTED_BY_PEER &&
disputeState != DisputeState.REFUND_REQUEST_CLOSED;
}
public boolean isDepositConfirmed() {

View file

@ -23,8 +23,8 @@ import bisq.core.monetary.Price;
import bisq.core.monetary.Volume;
import bisq.core.offer.Offer;
import bisq.core.offer.OfferPayload;
import bisq.core.offer.OfferUtil;
import bisq.core.trade.Trade;
import bisq.core.util.VolumeUtil;
import bisq.network.p2p.NodeAddress;
import bisq.network.p2p.storage.payload.CapabilityRequiringPayload;
@ -310,7 +310,7 @@ public final class TradeStatistics2 implements ProcessOncePersistableNetworkPayl
return new Volume(new AltcoinExchangeRate((Altcoin) getTradePrice().getMonetary()).coinToAltcoin(getTradeAmount()));
} else {
Volume volume = new Volume(new ExchangeRate((Fiat) getTradePrice().getMonetary()).coinToFiat(getTradeAmount()));
return OfferUtil.getRoundedFiatVolume(volume);
return VolumeUtil.getRoundedFiatVolume(volume);
}
}

View file

@ -23,8 +23,8 @@ import bisq.core.monetary.Price;
import bisq.core.monetary.Volume;
import bisq.core.offer.Offer;
import bisq.core.offer.OfferPayload;
import bisq.core.offer.OfferUtil;
import bisq.core.trade.Trade;
import bisq.core.util.VolumeUtil;
import bisq.network.p2p.NodeAddress;
import bisq.network.p2p.storage.payload.CapabilityRequiringPayload;
@ -355,7 +355,7 @@ public final class TradeStatistics3 implements ProcessOncePersistableNetworkPayl
return new Volume(new AltcoinExchangeRate((Altcoin) getTradePrice().getMonetary()).coinToAltcoin(getTradeAmount()));
} else {
Volume volume = new Volume(new ExchangeRate((Fiat) getTradePrice().getMonetary()).coinToFiat(getTradeAmount()));
return OfferUtil.getRoundedFiatVolume(volume);
return VolumeUtil.getRoundedFiatVolume(volume);
}
}

View file

@ -0,0 +1,50 @@
/*
* 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.Volume;
public class VolumeUtil {
public static Volume getRoundedFiatVolume(Volume volumeByAmount) {
// We want to get rounded to 1 unit of the fiat currency, e.g. 1 EUR.
return getAdjustedFiatVolume(volumeByAmount, 1);
}
public static Volume getAdjustedVolumeForHalCash(Volume volumeByAmount) {
// EUR has precision 4 and we want multiple of 10 so we divide by 100000 then
// round and multiply with 10
return getAdjustedFiatVolume(volumeByAmount, 10);
}
/**
*
* @param volumeByAmount The volume generated from an amount
* @param factor The factor used for rounding. E.g. 1 means rounded to
* units of 1 EUR, 10 means rounded to 10 EUR.
* @return The adjusted Fiat volume
*/
public static Volume getAdjustedFiatVolume(Volume volumeByAmount, int factor) {
// Fiat currencies use precision 4 and we want multiple of factor so we divide by 10000 * factor then
// round and multiply with factor
long roundedVolume = Math.round((double) volumeByAmount.getValue() / (10000d * factor)) * factor;
// Smallest allowed volume is factor (e.g. 10 EUR or 1 EUR,...)
roundedVolume = Math.max(factor, roundedVolume);
return Volume.parse(String.valueOf(roundedVolume), volumeByAmount.getCurrencyCode());
}
}

View file

@ -17,10 +17,22 @@
package bisq.core.util.coin;
import bisq.core.btc.wallet.Restrictions;
import bisq.core.monetary.Price;
import bisq.core.monetary.Volume;
import bisq.core.provider.fee.FeeService;
import bisq.common.util.MathUtils;
import org.bitcoinj.core.Coin;
import com.google.common.annotations.VisibleForTesting;
import javax.annotation.Nullable;
import static bisq.core.util.VolumeUtil.getAdjustedFiatVolume;
import static com.google.common.base.Preconditions.checkArgument;
public class CoinUtil {
// Get the fee per amount
@ -75,4 +87,101 @@ public class CoinUtil {
double amountAsDouble = amount != null ? (double) amount.value : 0;
return Coin.valueOf(Math.round(percent * amountAsDouble));
}
/**
* Calculates the maker fee for the given amount, marketPrice and marketPriceMargin.
*
* @param isCurrencyForMakerFeeBtc {@code true} to pay fee in BTC, {@code false} to pay fee in BSQ
* @param amount the amount of BTC to trade
* @return the maker fee for the given trade amount, or {@code null} if the amount is {@code null}
*/
@Nullable
public static Coin getMakerFee(boolean isCurrencyForMakerFeeBtc, @Nullable Coin amount) {
if (amount != null) {
Coin feePerBtc = getFeePerBtc(FeeService.getMakerFeePerBtc(isCurrencyForMakerFeeBtc), amount);
return maxCoin(feePerBtc, FeeService.getMinMakerFee(isCurrencyForMakerFeeBtc));
} else {
return null;
}
}
/**
* Calculate the possibly adjusted amount for {@code amount}, taking into account the
* {@code price} and {@code maxTradeLimit} and {@code factor}.
*
* @param amount Bitcoin amount which is a candidate for getting rounded.
* @param price Price used in relation to that amount.
* @param maxTradeLimit The max. trade limit of the users account, in satoshis.
* @return The adjusted amount
*/
public static Coin getRoundedFiatAmount(Coin amount, Price price, long maxTradeLimit) {
return getAdjustedAmount(amount, price, maxTradeLimit, 1);
}
public static Coin getAdjustedAmountForHalCash(Coin amount, Price price, long maxTradeLimit) {
return getAdjustedAmount(amount, price, maxTradeLimit, 10);
}
/**
* Calculate the possibly adjusted amount for {@code amount}, taking into account the
* {@code price} and {@code maxTradeLimit} and {@code factor}.
*
* @param amount Bitcoin amount which is a candidate for getting rounded.
* @param price Price used in relation to that amount.
* @param maxTradeLimit The max. trade limit of the users account, in satoshis.
* @param factor The factor used for rounding. E.g. 1 means rounded to units of
* 1 EUR, 10 means rounded to 10 EUR, etc.
* @return The adjusted amount
*/
@VisibleForTesting
static Coin getAdjustedAmount(Coin amount, Price price, long maxTradeLimit, int factor) {
checkArgument(
amount.getValue() >= 10_000,
"amount needs to be above minimum of 10k satoshis"
);
checkArgument(
factor > 0,
"factor needs to be positive"
);
// Amount must result in a volume of min factor units of the fiat currency, e.g. 1 EUR or
// 10 EUR in case of HalCash.
Volume smallestUnitForVolume = Volume.parse(String.valueOf(factor), price.getCurrencyCode());
if (smallestUnitForVolume.getValue() <= 0)
return Coin.ZERO;
Coin smallestUnitForAmount = price.getAmountByVolume(smallestUnitForVolume);
long minTradeAmount = Restrictions.getMinTradeAmount().value;
// We use 10 000 satoshi as min allowed amount
checkArgument(
minTradeAmount >= 10_000,
"MinTradeAmount must be at least 10k satoshis"
);
smallestUnitForAmount = Coin.valueOf(Math.max(minTradeAmount, smallestUnitForAmount.value));
// We don't allow smaller amount values than smallestUnitForAmount
boolean useSmallestUnitForAmount = amount.compareTo(smallestUnitForAmount) < 0;
// We get the adjusted volume from our amount
Volume volume = useSmallestUnitForAmount
? getAdjustedFiatVolume(price.getVolumeByAmount(smallestUnitForAmount), factor)
: getAdjustedFiatVolume(price.getVolumeByAmount(amount), factor);
if (volume.getValue() <= 0)
return Coin.ZERO;
// From that adjusted volume we calculate back the amount. It might be a bit different as
// the amount used as input before due rounding.
Coin amountByVolume = price.getAmountByVolume(volume);
// For the amount we allow only 4 decimal places
long adjustedAmount = Math.round((double) amountByVolume.value / 10000d) * 10000;
// If we are above our trade limit we reduce the amount by the smallestUnitForAmount
while (adjustedAmount > maxTradeLimit) {
adjustedAmount -= smallestUnitForAmount.value;
}
adjustedAmount = Math.max(minTradeAmount, adjustedAmount);
adjustedAmount = Math.min(maxTradeLimit, adjustedAmount);
return Coin.valueOf(adjustedAmount);
}
}

View file

@ -1,60 +0,0 @@
/*
* 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.util.coin.CoinUtil;
import org.bitcoinj.core.Coin;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.junit.Test;
import static org.junit.Assert.assertEquals;
public class CoinCryptoUtilsTest {
private static final Logger log = LoggerFactory.getLogger(CoinCryptoUtilsTest.class);
@Test
public void testGetFeePerBtc() {
assertEquals(Coin.parseCoin("1"), CoinUtil.getFeePerBtc(Coin.parseCoin("1"), Coin.parseCoin("1")));
assertEquals(Coin.parseCoin("0.1"), CoinUtil.getFeePerBtc(Coin.parseCoin("0.1"), Coin.parseCoin("1")));
assertEquals(Coin.parseCoin("0.01"), CoinUtil.getFeePerBtc(Coin.parseCoin("0.1"), Coin.parseCoin("0.1")));
assertEquals(Coin.parseCoin("0.015"), CoinUtil.getFeePerBtc(Coin.parseCoin("0.3"), Coin.parseCoin("0.05")));
}
@Test
public void testMinCoin() {
assertEquals(Coin.parseCoin("1"), CoinUtil.minCoin(Coin.parseCoin("1"), Coin.parseCoin("1")));
assertEquals(Coin.parseCoin("0.1"), CoinUtil.minCoin(Coin.parseCoin("0.1"), Coin.parseCoin("1")));
assertEquals(Coin.parseCoin("0.01"), CoinUtil.minCoin(Coin.parseCoin("0.1"), Coin.parseCoin("0.01")));
assertEquals(Coin.parseCoin("0"), CoinUtil.minCoin(Coin.parseCoin("0"), Coin.parseCoin("0.05")));
assertEquals(Coin.parseCoin("0"), CoinUtil.minCoin(Coin.parseCoin("0.05"), Coin.parseCoin("0")));
}
@Test
public void testMaxCoin() {
assertEquals(Coin.parseCoin("1"), CoinUtil.maxCoin(Coin.parseCoin("1"), Coin.parseCoin("1")));
assertEquals(Coin.parseCoin("1"), CoinUtil.maxCoin(Coin.parseCoin("0.1"), Coin.parseCoin("1")));
assertEquals(Coin.parseCoin("0.1"), CoinUtil.maxCoin(Coin.parseCoin("0.1"), Coin.parseCoin("0.01")));
assertEquals(Coin.parseCoin("0.05"), CoinUtil.maxCoin(Coin.parseCoin("0"), Coin.parseCoin("0.05")));
assertEquals(Coin.parseCoin("0.05"), CoinUtil.maxCoin(Coin.parseCoin("0.05"), Coin.parseCoin("0")));
}
}

View file

@ -15,62 +15,90 @@
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.core.offer;
package bisq.core.util.coin;
import bisq.core.monetary.Price;
import org.bitcoinj.core.Coin;
import org.junit.Assert;
import org.junit.Test;
public class OfferUtilTest {
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.fail;
public class CoinUtilTest {
@Test
public void testGetFeePerBtc() {
assertEquals(Coin.parseCoin("1"), CoinUtil.getFeePerBtc(Coin.parseCoin("1"), Coin.parseCoin("1")));
assertEquals(Coin.parseCoin("0.1"), CoinUtil.getFeePerBtc(Coin.parseCoin("0.1"), Coin.parseCoin("1")));
assertEquals(Coin.parseCoin("0.01"), CoinUtil.getFeePerBtc(Coin.parseCoin("0.1"), Coin.parseCoin("0.1")));
assertEquals(Coin.parseCoin("0.015"), CoinUtil.getFeePerBtc(Coin.parseCoin("0.3"), Coin.parseCoin("0.05")));
}
@Test
public void testMinCoin() {
assertEquals(Coin.parseCoin("1"), CoinUtil.minCoin(Coin.parseCoin("1"), Coin.parseCoin("1")));
assertEquals(Coin.parseCoin("0.1"), CoinUtil.minCoin(Coin.parseCoin("0.1"), Coin.parseCoin("1")));
assertEquals(Coin.parseCoin("0.01"), CoinUtil.minCoin(Coin.parseCoin("0.1"), Coin.parseCoin("0.01")));
assertEquals(Coin.parseCoin("0"), CoinUtil.minCoin(Coin.parseCoin("0"), Coin.parseCoin("0.05")));
assertEquals(Coin.parseCoin("0"), CoinUtil.minCoin(Coin.parseCoin("0.05"), Coin.parseCoin("0")));
}
@Test
public void testMaxCoin() {
assertEquals(Coin.parseCoin("1"), CoinUtil.maxCoin(Coin.parseCoin("1"), Coin.parseCoin("1")));
assertEquals(Coin.parseCoin("1"), CoinUtil.maxCoin(Coin.parseCoin("0.1"), Coin.parseCoin("1")));
assertEquals(Coin.parseCoin("0.1"), CoinUtil.maxCoin(Coin.parseCoin("0.1"), Coin.parseCoin("0.01")));
assertEquals(Coin.parseCoin("0.05"), CoinUtil.maxCoin(Coin.parseCoin("0"), Coin.parseCoin("0.05")));
assertEquals(Coin.parseCoin("0.05"), CoinUtil.maxCoin(Coin.parseCoin("0.05"), Coin.parseCoin("0")));
}
@Test
public void testGetAdjustedAmount() {
Coin result = OfferUtil.getAdjustedAmount(
Coin result = CoinUtil.getAdjustedAmount(
Coin.valueOf(100_000),
Price.valueOf("USD", 1000_0000),
20_000_000,
1);
Assert.assertEquals(
assertEquals(
"Minimum trade amount allowed should be adjusted to the smallest trade allowed.",
"0.001 BTC",
result.toFriendlyString()
);
try {
OfferUtil.getAdjustedAmount(
CoinUtil.getAdjustedAmount(
Coin.ZERO,
Price.valueOf("USD", 1000_0000),
20_000_000,
1);
Assert.fail("Expected IllegalArgumentException to be thrown when amount is too low.");
fail("Expected IllegalArgumentException to be thrown when amount is too low.");
} catch (IllegalArgumentException iae) {
Assert.assertEquals(
assertEquals(
"Unexpected exception message.",
"amount needs to be above minimum of 10k satoshis",
iae.getMessage()
);
}
result = OfferUtil.getAdjustedAmount(
result = CoinUtil.getAdjustedAmount(
Coin.valueOf(1_000_000),
Price.valueOf("USD", 1000_0000),
20_000_000,
1);
Assert.assertEquals(
assertEquals(
"Minimum allowed trade amount should not be adjusted.",
"0.01 BTC",
result.toFriendlyString()
);
result = OfferUtil.getAdjustedAmount(
result = CoinUtil.getAdjustedAmount(
Coin.valueOf(100_000),
Price.valueOf("USD", 1000_0000),
1_000_000,
1);
Assert.assertEquals(
assertEquals(
"Minimum trade amount allowed should respect maxTradeLimit and factor, if possible.",
"0.001 BTC",
result.toFriendlyString()
@ -81,12 +109,12 @@ public class OfferUtilTest {
// max trade limit is 5k sat = 0.00005 BTC. But the returned amount 0.00005 BTC, or
// 0.05 USD worth, which is below the factor of 1 USD, but does respect the maxTradeLimit.
// Basically the given constraints (maxTradeLimit vs factor) are impossible to both fulfill..
result = OfferUtil.getAdjustedAmount(
result = CoinUtil.getAdjustedAmount(
Coin.valueOf(100_000),
Price.valueOf("USD", 1000_0000),
5_000,
1);
Assert.assertEquals(
assertEquals(
"Minimum trade amount allowed with low maxTradeLimit should still respect that limit, even if result does not respect the factor specified.",
"0.00005 BTC",
result.toFriendlyString()

View file

@ -1,13 +0,0 @@
package bisq.desktop.main.offer;
import bisq.core.btc.wallet.BsqWalletService;
import bisq.core.offer.OfferUtil;
import bisq.core.user.Preferences;
import org.bitcoinj.core.Coin;
public class MakerFeeProvider {
public Coin getMakerFee(BsqWalletService bsqWalletService, Preferences preferences, Coin amount) {
return OfferUtil.getMakerFee(bsqWalletService, preferences, amount);
}
}

View file

@ -38,7 +38,6 @@ import bisq.core.offer.Offer;
import bisq.core.offer.OfferPayload;
import bisq.core.offer.OfferUtil;
import bisq.core.offer.OpenOfferManager;
import bisq.core.payment.HalCashAccount;
import bisq.core.payment.PaymentAccount;
import bisq.core.provider.fee.FeeService;
import bisq.core.provider.price.PriceFeedService;
@ -48,6 +47,7 @@ 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.VolumeUtil;
import bisq.core.util.coin.CoinFormatter;
import bisq.core.util.coin.CoinUtil;
@ -85,6 +85,7 @@ import java.util.Comparator;
import java.util.Date;
import java.util.HashSet;
import java.util.Optional;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import javax.annotation.Nullable;
@ -103,7 +104,6 @@ public abstract class MutableOfferDataModel extends OfferDataModel implements Bs
private final AccountAgeWitnessService accountAgeWitnessService;
private final FeeService feeService;
private final CoinFormatter btcFormatter;
private final MakerFeeProvider makerFeeProvider;
private final Navigation navigation;
private final String offerId;
private final BalanceListener btcBalanceListener;
@ -133,6 +133,9 @@ public abstract class MutableOfferDataModel extends OfferDataModel implements Bs
protected boolean allowAmountUpdate = true;
private final TradeStatisticsManager tradeStatisticsManager;
private final Predicate<ObjectProperty<Coin>> isPositiveAmount = (c) -> c.get() != null && !c.get().isZero();
private final Predicate<ObjectProperty<Price>> isPositivePrice = (p) -> p.get() != null && !p.get().isZero();
private final Predicate<ObjectProperty<Volume>> isPositiveVolume = (v) -> v.get() != null && !v.get().isZero();
///////////////////////////////////////////////////////////////////////////////////////////
// Constructor, lifecycle
@ -141,6 +144,7 @@ public abstract class MutableOfferDataModel extends OfferDataModel implements Bs
@Inject
public MutableOfferDataModel(CreateOfferService createOfferService,
OpenOfferManager openOfferManager,
OfferUtil offerUtil,
BtcWalletService btcWalletService,
BsqWalletService bsqWalletService,
Preferences preferences,
@ -150,10 +154,9 @@ public abstract class MutableOfferDataModel extends OfferDataModel implements Bs
AccountAgeWitnessService accountAgeWitnessService,
FeeService feeService,
@Named(FormattingUtils.BTC_FORMATTER_KEY) CoinFormatter btcFormatter,
MakerFeeProvider makerFeeProvider,
TradeStatisticsManager tradeStatisticsManager,
Navigation navigation) {
super(btcWalletService);
super(btcWalletService, offerUtil);
this.createOfferService = createOfferService;
this.openOfferManager = openOfferManager;
@ -165,7 +168,6 @@ public abstract class MutableOfferDataModel extends OfferDataModel implements Bs
this.accountAgeWitnessService = accountAgeWitnessService;
this.feeService = feeService;
this.btcFormatter = btcFormatter;
this.makerFeeProvider = makerFeeProvider;
this.navigation = navigation;
this.tradeStatisticsManager = tradeStatisticsManager;
@ -373,16 +375,9 @@ public abstract class MutableOfferDataModel extends OfferDataModel implements Bs
}
}
private void setTradeCurrencyFromPaymentAccount(PaymentAccount paymentAccount) {
if (!paymentAccount.getTradeCurrencies().contains(tradeCurrency)) {
if (paymentAccount.getSelectedTradeCurrency() != null)
tradeCurrency = paymentAccount.getSelectedTradeCurrency();
else if (paymentAccount.getSingleTradeCurrency() != null)
tradeCurrency = paymentAccount.getSingleTradeCurrency();
else if (!paymentAccount.getTradeCurrencies().isEmpty())
tradeCurrency = paymentAccount.getTradeCurrencies().get(0);
}
if (!paymentAccount.getTradeCurrencies().contains(tradeCurrency))
tradeCurrency = paymentAccount.getTradeCurrency().orElse(tradeCurrency);
checkNotNull(tradeCurrency, "tradeCurrency must not be null");
tradeCurrencyCode.set(tradeCurrency.getCode());
@ -406,7 +401,8 @@ public abstract class MutableOfferDataModel extends OfferDataModel implements Bs
priceFeedService.setCurrencyCode(code);
Optional<TradeCurrency> tradeCurrencyOptional = preferences.getTradeCurrenciesAsObservable().stream().filter(e -> e.getCode().equals(code)).findAny();
Optional<TradeCurrency> tradeCurrencyOptional = preferences.getTradeCurrenciesAsObservable()
.stream().filter(e -> e.getCode().equals(code)).findAny();
if (!tradeCurrencyOptional.isPresent()) {
if (CurrencyUtil.isCryptoCurrency(code)) {
CurrencyUtil.getCryptoCurrency(code).ifPresent(preferences::addCryptoCurrency);
@ -512,8 +508,8 @@ public abstract class MutableOfferDataModel extends OfferDataModel implements Bs
///////////////////////////////////////////////////////////////////////////////////////////
double calculateMarketPriceManual(double marketPrice, double volumeAsDouble, double amountAsDouble) {
double manualPriceAsDouble = volumeAsDouble / amountAsDouble;
double percentage = MathUtils.roundDouble(manualPriceAsDouble / marketPrice, 4);
double manualPriceAsDouble = offerUtil.calculateManualPrice(volumeAsDouble, amountAsDouble);
double percentage = offerUtil.calculateMarketPriceMargin(manualPriceAsDouble, marketPrice);
setMarketPriceMargin(percentage);
@ -521,10 +517,7 @@ public abstract class MutableOfferDataModel extends OfferDataModel implements Bs
}
void calculateVolume() {
if (price.get() != null &&
amount.get() != null &&
!amount.get().isZero() &&
!price.get().isZero()) {
if (isPositivePrice.test(price) && isPositiveAmount.test(amount)) {
try {
Volume volumeByAmount = calculateVolumeForAmount(amount);
@ -540,10 +533,7 @@ public abstract class MutableOfferDataModel extends OfferDataModel implements Bs
}
void calculateMinVolume() {
if (price.get() != null &&
minAmount.get() != null &&
!minAmount.get().isZero() &&
!price.get().isZero()) {
if (isPositivePrice.test(price) && isPositiveAmount.test(minAmount)) {
try {
Volume volumeByAmount = calculateVolumeForAmount(minAmount);
@ -559,25 +549,21 @@ public abstract class MutableOfferDataModel extends OfferDataModel implements Bs
Volume volumeByAmount = price.get().getVolumeByAmount(minAmount.get());
// For HalCash we want multiple of 10 EUR
if (isHalCashAccount())
volumeByAmount = OfferUtil.getAdjustedVolumeForHalCash(volumeByAmount);
if (paymentAccount.isHalCashAccount())
volumeByAmount = VolumeUtil.getAdjustedVolumeForHalCash(volumeByAmount);
else if (CurrencyUtil.isFiatCurrency(tradeCurrencyCode.get()))
volumeByAmount = OfferUtil.getRoundedFiatVolume(volumeByAmount);
volumeByAmount = VolumeUtil.getRoundedFiatVolume(volumeByAmount);
return volumeByAmount;
}
void calculateAmount() {
if (volume.get() != null &&
price.get() != null &&
!volume.get().isZero() &&
!price.get().isZero() &&
allowAmountUpdate) {
if (isPositivePrice.test(price) && isPositiveVolume.test(volume) && allowAmountUpdate) {
try {
Coin value = DisplayUtils.reduceTo4Decimals(price.get().getAmountByVolume(volume.get()), btcFormatter);
if (isHalCashAccount())
value = OfferUtil.getAdjustedAmountForHalCash(value, price.get(), getMaxTradeLimit());
if (paymentAccount.isHalCashAccount())
value = CoinUtil.getAdjustedAmountForHalCash(value, price.get(), getMaxTradeLimit());
else if (CurrencyUtil.isFiatCurrency(tradeCurrencyCode.get()))
value = OfferUtil.getRoundedFiatAmount(value, price.get(), getMaxTradeLimit());
value = CoinUtil.getRoundedFiatAmount(value, price.get(), getMaxTradeLimit());
calculateVolume();
@ -608,8 +594,8 @@ public abstract class MutableOfferDataModel extends OfferDataModel implements Bs
return isBuyOffer() ? getBuyerSecurityDepositAsCoin() : getSellerSecurityDepositAsCoin();
}
public boolean isBuyOffer() {
return OfferUtil.isBuyOffer(getDirection());
boolean isBuyOffer() {
return offerUtil.isBuyOffer(getDirection());
}
public Coin getTxFee() {
@ -645,7 +631,7 @@ public abstract class MutableOfferDataModel extends OfferDataModel implements Bs
}
protected boolean isUseMarketBasedPriceValue() {
return marketPriceAvailable && useMarketBasedPrice.get() && !isHalCashAccount();
return marketPriceAvailable && useMarketBasedPrice.get() && !paymentAccount.isHalCashAccount();
}
///////////////////////////////////////////////////////////////////////////////////////////
@ -720,13 +706,7 @@ public abstract class MutableOfferDataModel extends OfferDataModel implements Bs
}
Coin getUsableBsqBalance() {
// we have to keep a minimum amount of BSQ == bitcoin dust limit
// otherwise there would be dust violations for change UTXOs
// essentially means the minimum usable balance of BSQ is 5.46
Coin usableBsqBalance = bsqWalletService.getAvailableConfirmedBalance().subtract(Restrictions.getMinNonDustOutput());
if (usableBsqBalance.isNegative())
usableBsqBalance = Coin.ZERO;
return usableBsqBalance;
return offerUtil.getUsableBsqBalance();
}
public void setMarketPriceAvailable(boolean marketPriceAvailable) {
@ -734,23 +714,23 @@ public abstract class MutableOfferDataModel extends OfferDataModel implements Bs
}
public Coin getMakerFee(boolean isCurrencyForMakerFeeBtc) {
return OfferUtil.getMakerFee(isCurrencyForMakerFeeBtc, amount.get());
return CoinUtil.getMakerFee(isCurrencyForMakerFeeBtc, amount.get());
}
public Coin getMakerFee() {
return makerFeeProvider.getMakerFee(bsqWalletService, preferences, amount.get());
return offerUtil.getMakerFee(amount.get());
}
public Coin getMakerFeeInBtc() {
return OfferUtil.getMakerFee(true, amount.get());
return CoinUtil.getMakerFee(true, amount.get());
}
public Coin getMakerFeeInBsq() {
return OfferUtil.getMakerFee(false, amount.get());
return CoinUtil.getMakerFee(false, amount.get());
}
public boolean isCurrencyForMakerFeeBtc() {
return OfferUtil.isCurrencyForMakerFeeBtc(preferences, bsqWalletService, amount.get());
return offerUtil.isCurrencyForMakerFeeBtc(amount.get());
}
boolean isPreferredFeeCurrencyBtc() {
@ -758,11 +738,7 @@ public abstract class MutableOfferDataModel extends OfferDataModel implements Bs
}
boolean isBsqForFeeAvailable() {
return OfferUtil.isBsqForMakerFeeAvailable(bsqWalletService, amount.get());
}
public boolean isHalCashAccount() {
return paymentAccount instanceof HalCashAccount;
return offerUtil.isBsqForMakerFeeAvailable(amount.get());
}
boolean canPlaceOffer() {

View file

@ -874,7 +874,7 @@ public abstract class MutableOfferView<M extends MutableOfferViewModel<?>> exten
int marketPriceAvailableValue = model.marketPriceAvailableProperty.get();
if (marketPriceAvailableValue > -1) {
boolean showPriceToggle = marketPriceAvailableValue == 1 &&
!model.getDataModel().isHalCashAccount();
!model.getDataModel().paymentAccount.isHalCashAccount();
percentagePriceBox.setVisible(showPriceToggle);
priceTypeToggleButton.setVisible(showPriceToggle);
boolean fixedPriceSelected = !model.getDataModel().getUseMarketBasedPrice().get() || !showPriceToggle;

View file

@ -54,8 +54,10 @@ import bisq.core.provider.price.PriceFeedService;
import bisq.core.user.Preferences;
import bisq.core.util.FormattingUtils;
import bisq.core.util.ParsingUtils;
import bisq.core.util.VolumeUtil;
import bisq.core.util.coin.BsqFormatter;
import bisq.core.util.coin.CoinFormatter;
import bisq.core.util.coin.CoinUtil;
import bisq.core.util.validation.InputValidator;
import bisq.common.Timer;
@ -63,7 +65,6 @@ import bisq.common.UserThread;
import bisq.common.app.DevEnv;
import bisq.common.util.MathUtils;
import org.bitcoinj.core.Address;
import org.bitcoinj.core.Coin;
import org.bitcoinj.utils.Fiat;
@ -96,7 +97,7 @@ public abstract class MutableOfferViewModel<M extends MutableOfferDataModel> ext
private final BsqValidator bsqValidator;
protected final SecurityDepositValidator securityDepositValidator;
private final PriceFeedService priceFeedService;
private AccountAgeWitnessService accountAgeWitnessService;
private final AccountAgeWitnessService accountAgeWitnessService;
private final Navigation navigation;
private final Preferences preferences;
protected final CoinFormatter btcFormatter;
@ -104,9 +105,9 @@ public abstract class MutableOfferViewModel<M extends MutableOfferDataModel> ext
private final FiatVolumeValidator fiatVolumeValidator;
private final FiatPriceValidator fiatPriceValidator;
private final AltcoinValidator altcoinValidator;
protected final OfferUtil offerUtil;
private String amountDescription;
private String directionLabel;
private String addressAsString;
private final String paymentLabel;
private boolean createOfferRequested;
@ -156,9 +157,6 @@ public abstract class MutableOfferViewModel<M extends MutableOfferDataModel> ext
final ObjectProperty<InputValidator.ValidationResult> volumeValidationResult = new SimpleObjectProperty<>();
final ObjectProperty<InputValidator.ValidationResult> buyerSecurityDepositValidationResult = new SimpleObjectProperty<>();
// Those are needed for the addressTextField
private final ObjectProperty<Address> address = new SimpleObjectProperty<>();
private ChangeListener<String> amountStringListener;
private ChangeListener<String> minAmountStringListener;
private ChangeListener<String> priceStringListener, marketPriceMarginStringListener;
@ -172,7 +170,6 @@ public abstract class MutableOfferViewModel<M extends MutableOfferDataModel> ext
private ChangeListener<Number> securityDepositAsDoubleListener;
private ChangeListener<Boolean> isWalletFundedListener;
//private ChangeListener<Coin> feeFromFundingTxListener;
private ChangeListener<String> errorMessageListener;
private Offer offer;
private Timer timeoutTimer;
@ -201,7 +198,8 @@ public abstract class MutableOfferViewModel<M extends MutableOfferDataModel> ext
Navigation navigation,
Preferences preferences,
@Named(FormattingUtils.BTC_FORMATTER_KEY) CoinFormatter btcFormatter,
BsqFormatter bsqFormatter) {
BsqFormatter bsqFormatter,
OfferUtil offerUtil) {
super(dataModel);
this.fiatVolumeValidator = fiatVolumeValidator;
@ -216,12 +214,12 @@ public abstract class MutableOfferViewModel<M extends MutableOfferDataModel> ext
this.preferences = preferences;
this.btcFormatter = btcFormatter;
this.bsqFormatter = bsqFormatter;
this.offerUtil = offerUtil;
paymentLabel = Res.get("createOffer.fundsBox.paymentLabel", dataModel.shortOfferId);
if (dataModel.getAddressEntry() != null) {
addressAsString = dataModel.getAddressEntry().getAddressString();
address.set(dataModel.getAddressEntry().getAddress());
}
createListeners();
}
@ -498,8 +496,9 @@ public abstract class MutableOfferViewModel<M extends MutableOfferDataModel> ext
tradeFee.set(getFormatterForMakerFee().formatCoin(makerFeeAsCoin));
Coin makerFeeInBtc = dataModel.getMakerFeeInBtc();
Optional<Volume> optionalBtcFeeInFiat = OfferUtil.getFeeInUserFiatCurrency(makerFeeInBtc,
true, preferences, priceFeedService, bsqFormatter);
Optional<Volume> optionalBtcFeeInFiat = offerUtil.getFeeInUserFiatCurrency(makerFeeInBtc,
true,
bsqFormatter);
String btcFeeWithFiatAmount = DisplayUtils.getFeeWithFiatAmount(makerFeeInBtc, optionalBtcFeeInFiat, btcFormatter);
if (DevEnv.isDaoActivated()) {
tradeFeeInBtcWithFiat.set(btcFeeWithFiatAmount);
@ -508,9 +507,12 @@ public abstract class MutableOfferViewModel<M extends MutableOfferDataModel> ext
}
Coin makerFeeInBsq = dataModel.getMakerFeeInBsq();
Optional<Volume> optionalBsqFeeInFiat = OfferUtil.getFeeInUserFiatCurrency(makerFeeInBsq,
false, preferences, priceFeedService, bsqFormatter);
String bsqFeeWithFiatAmount = DisplayUtils.getFeeWithFiatAmount(makerFeeInBsq, optionalBsqFeeInFiat, bsqFormatter);
Optional<Volume> optionalBsqFeeInFiat = offerUtil.getFeeInUserFiatCurrency(makerFeeInBsq,
false,
bsqFormatter);
String bsqFeeWithFiatAmount = DisplayUtils.getFeeWithFiatAmount(makerFeeInBsq,
optionalBsqFeeInFiat,
bsqFormatter);
if (DevEnv.isDaoActivated()) {
tradeFeeInBsqWithFiat.set(bsqFeeWithFiatAmount);
} else {
@ -604,7 +606,6 @@ public abstract class MutableOfferViewModel<M extends MutableOfferDataModel> ext
btcValidator.setMinValue(Restrictions.getMinTradeAmount());
final boolean isBuy = dataModel.getDirection() == OfferPayload.Direction.BUY;
directionLabel = isBuy ? Res.get("shared.buyBitcoin") : Res.get("shared.sellBitcoin");
amountDescription = Res.get("createOffer.amountPriceBox.amountDescription",
isBuy ? Res.get("shared.buy") : Res.get("shared.sell"));
@ -833,9 +834,7 @@ public abstract class MutableOfferViewModel<M extends MutableOfferDataModel> ext
}
// We want to trigger a recalculation of the volume
UserThread.execute(() -> {
onFocusOutVolumeTextField(true, false);
});
UserThread.execute(() -> onFocusOutVolumeTextField(true, false));
}
void onFocusOutVolumeTextField(boolean oldValue, boolean newValue) {
@ -849,10 +848,10 @@ public abstract class MutableOfferViewModel<M extends MutableOfferDataModel> ext
Volume volume = dataModel.getVolume().get();
if (volume != null) {
// For HalCash we want multiple of 10 EUR
if (dataModel.isHalCashAccount())
volume = OfferUtil.getAdjustedVolumeForHalCash(volume);
if (dataModel.paymentAccount.isHalCashAccount())
volume = VolumeUtil.getAdjustedVolumeForHalCash(volume);
else if (CurrencyUtil.isFiatCurrency(tradeCurrencyCode.get()))
volume = OfferUtil.getRoundedFiatVolume(volume);
volume = VolumeUtil.getRoundedFiatVolume(volume);
this.volume.set(DisplayUtils.formatVolume(volume));
}
@ -1045,10 +1044,6 @@ public abstract class MutableOfferViewModel<M extends MutableOfferDataModel> ext
return amountDescription;
}
public String getDirectionLabel() {
return directionLabel;
}
public String getAddressAsString() {
return addressAsString;
}
@ -1057,10 +1052,6 @@ public abstract class MutableOfferViewModel<M extends MutableOfferDataModel> ext
return paymentLabel;
}
public String formatCoin(Coin coin) {
return btcFormatter.formatCoin(coin);
}
public Offer createAndGetOffer() {
offer = dataModel.createAndGetOffer();
return offer;
@ -1086,10 +1077,10 @@ public abstract class MutableOfferViewModel<M extends MutableOfferDataModel> ext
long maxTradeLimit = dataModel.getMaxTradeLimit();
Price price = dataModel.getPrice().get();
if (price != null) {
if (dataModel.isHalCashAccount())
amount = OfferUtil.getAdjustedAmountForHalCash(amount, price, maxTradeLimit);
if (dataModel.paymentAccount.isHalCashAccount())
amount = CoinUtil.getAdjustedAmountForHalCash(amount, price, maxTradeLimit);
else if (CurrencyUtil.isFiatCurrency(tradeCurrencyCode.get()))
amount = OfferUtil.getRoundedFiatAmount(amount, price, maxTradeLimit);
amount = CoinUtil.getRoundedFiatAmount(amount, price, maxTradeLimit);
}
dataModel.setAmount(amount);
if (syncMinAmountWithAmount ||
@ -1110,10 +1101,10 @@ public abstract class MutableOfferViewModel<M extends MutableOfferDataModel> ext
Price price = dataModel.getPrice().get();
long maxTradeLimit = dataModel.getMaxTradeLimit();
if (price != null) {
if (dataModel.isHalCashAccount())
minAmount = OfferUtil.getAdjustedAmountForHalCash(minAmount, price, maxTradeLimit);
if (dataModel.paymentAccount.isHalCashAccount())
minAmount = CoinUtil.getAdjustedAmountForHalCash(minAmount, price, maxTradeLimit);
else if (CurrencyUtil.isFiatCurrency(tradeCurrencyCode.get()))
minAmount = OfferUtil.getRoundedFiatAmount(minAmount, price, maxTradeLimit);
minAmount = CoinUtil.getRoundedFiatAmount(minAmount, price, maxTradeLimit);
}
dataModel.setMinAmount(minAmount);

View file

@ -21,6 +21,7 @@ import bisq.desktop.common.model.ActivatableDataModel;
import bisq.core.btc.model.AddressEntry;
import bisq.core.btc.wallet.BtcWalletService;
import bisq.core.offer.OfferUtil;
import org.bitcoinj.core.Coin;
@ -31,13 +32,17 @@ import javafx.beans.property.SimpleObjectProperty;
import lombok.Getter;
import static bisq.core.util.coin.CoinUtil.minCoin;
/**
* Domain for that UI element.
* Note that the create offer domain has a deeper scope in the application domain (TradeManager).
* That model is just responsible for the domain specific parts displayed needed in that UI element.
* Note that the create offer domain has a deeper scope in the application domain
* (TradeManager). That model is just responsible for the domain specific parts displayed
* needed in that UI element.
*/
public abstract class OfferDataModel extends ActivatableDataModel {
protected final BtcWalletService btcWalletService;
protected final OfferUtil offerUtil;
@Getter
protected final BooleanProperty isBtcWalletFunded = new SimpleBooleanProperty();
@ -54,8 +59,9 @@ public abstract class OfferDataModel extends ActivatableDataModel {
protected AddressEntry addressEntry;
protected boolean useSavingsWallet;
public OfferDataModel(BtcWalletService btcWalletService) {
public OfferDataModel(BtcWalletService btcWalletService, OfferUtil offerUtil) {
this.btcWalletService = btcWalletService;
this.offerUtil = offerUtil;
}
protected void updateBalance() {
@ -64,28 +70,15 @@ public abstract class OfferDataModel extends ActivatableDataModel {
Coin savingWalletBalance = btcWalletService.getSavingWalletBalance();
totalAvailableBalance = savingWalletBalance.add(tradeWalletBalance);
if (totalToPayAsCoin.get() != null) {
if (totalAvailableBalance.compareTo(totalToPayAsCoin.get()) > 0)
balance.set(totalToPayAsCoin.get());
else
balance.set(totalAvailableBalance);
balance.set(minCoin(totalToPayAsCoin.get(), totalAvailableBalance));
}
} else {
balance.set(tradeWalletBalance);
}
if (totalToPayAsCoin.get() != null) {
Coin missing = totalToPayAsCoin.get().subtract(balance.get());
if (missing.isNegative())
missing = Coin.ZERO;
missingCoin.set(missing);
}
isBtcWalletFunded.set(isBalanceSufficient(balance.get()));
missingCoin.set(offerUtil.getBalanceShortage(totalToPayAsCoin.get(), balance.get()));
isBtcWalletFunded.set(offerUtil.isBalanceSufficient(totalToPayAsCoin.get(), balance.get()));
if (totalToPayAsCoin.get() != null && isBtcWalletFunded.get() && !showWalletFundedNotification.get()) {
showWalletFundedNotification.set(true);
}
}
private boolean isBalanceSufficient(Coin balance) {
return totalToPayAsCoin.get() != null && balance.compareTo(totalToPayAsCoin.get()) >= 0;
}
}

View file

@ -22,13 +22,13 @@ see <http://www.gnu.org/licenses/>.
package bisq.desktop.main.offer.createoffer;
import bisq.desktop.Navigation;
import bisq.desktop.main.offer.MakerFeeProvider;
import bisq.desktop.main.offer.MutableOfferDataModel;
import bisq.core.account.witness.AccountAgeWitnessService;
import bisq.core.btc.wallet.BsqWalletService;
import bisq.core.btc.wallet.BtcWalletService;
import bisq.core.offer.CreateOfferService;
import bisq.core.offer.OfferUtil;
import bisq.core.offer.OpenOfferManager;
import bisq.core.provider.fee.FeeService;
import bisq.core.provider.price.PriceFeedService;
@ -54,6 +54,7 @@ class CreateOfferDataModel extends MutableOfferDataModel {
@Inject
public CreateOfferDataModel(CreateOfferService createOfferService,
OpenOfferManager openOfferManager,
OfferUtil offerUtil,
BtcWalletService btcWalletService,
BsqWalletService bsqWalletService,
Preferences preferences,
@ -63,11 +64,11 @@ class CreateOfferDataModel extends MutableOfferDataModel {
AccountAgeWitnessService accountAgeWitnessService,
FeeService feeService,
@Named(FormattingUtils.BTC_FORMATTER_KEY) CoinFormatter btcFormatter,
MakerFeeProvider makerFeeProvider,
TradeStatisticsManager tradeStatisticsManager,
Navigation navigation) {
super(createOfferService,
openOfferManager,
offerUtil,
btcWalletService,
bsqWalletService,
preferences,
@ -77,7 +78,6 @@ class CreateOfferDataModel extends MutableOfferDataModel {
accountAgeWitnessService,
feeService,
btcFormatter,
makerFeeProvider,
tradeStatisticsManager,
navigation);
}

View file

@ -28,6 +28,7 @@ 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.provider.price.PriceFeedService;
import bisq.core.user.Preferences;
import bisq.core.util.FormattingUtils;
@ -53,7 +54,8 @@ class CreateOfferViewModel extends MutableOfferViewModel<CreateOfferDataModel> i
Navigation navigation,
Preferences preferences,
@Named(FormattingUtils.BTC_FORMATTER_KEY) CoinFormatter btcFormatter,
BsqFormatter bsqFormatter) {
BsqFormatter bsqFormatter,
OfferUtil offerUtil) {
super(dataModel,
fiatVolumeValidator,
fiatPriceValidator,
@ -65,6 +67,8 @@ class CreateOfferViewModel extends MutableOfferViewModel<CreateOfferDataModel> i
accountAgeWitnessService,
navigation,
preferences,
btcFormatter, bsqFormatter);
btcFormatter,
bsqFormatter,
offerUtil);
}
}

View file

@ -38,7 +38,6 @@ import bisq.core.monetary.Volume;
import bisq.core.offer.Offer;
import bisq.core.offer.OfferPayload;
import bisq.core.offer.OfferUtil;
import bisq.core.payment.HalCashAccount;
import bisq.core.payment.PaymentAccount;
import bisq.core.payment.PaymentAccountUtil;
import bisq.core.payment.payload.PaymentMethod;
@ -48,6 +47,7 @@ import bisq.core.trade.TradeManager;
import bisq.core.trade.handlers.TradeResultHandler;
import bisq.core.user.Preferences;
import bisq.core.user.User;
import bisq.core.util.VolumeUtil;
import bisq.core.util.coin.CoinUtil;
import bisq.network.p2p.P2PService;
@ -123,6 +123,7 @@ class TakeOfferDataModel extends OfferDataModel {
@Inject
TakeOfferDataModel(TradeManager tradeManager,
OfferBook offerBook,
OfferUtil offerUtil,
BtcWalletService btcWalletService,
BsqWalletService bsqWalletService,
User user, FeeService feeService,
@ -134,7 +135,7 @@ class TakeOfferDataModel extends OfferDataModel {
Navigation navigation,
P2PService p2PService
) {
super(btcWalletService);
super(btcWalletService, offerUtil);
this.tradeManager = tradeManager;
this.offerBook = offerBook;
@ -463,9 +464,9 @@ class TakeOfferDataModel extends OfferDataModel {
!amount.get().isZero()) {
Volume volumeByAmount = tradePrice.getVolumeByAmount(amount.get());
if (offer.getPaymentMethod().getId().equals(PaymentMethod.HAL_CASH_ID))
volumeByAmount = OfferUtil.getAdjustedVolumeForHalCash(volumeByAmount);
volumeByAmount = VolumeUtil.getAdjustedVolumeForHalCash(volumeByAmount);
else if (CurrencyUtil.isFiatCurrency(getCurrencyCode()))
volumeByAmount = OfferUtil.getRoundedFiatVolume(volumeByAmount);
volumeByAmount = VolumeUtil.getRoundedFiatVolume(volumeByAmount);
volume.set(volumeByAmount);
@ -643,11 +644,11 @@ class TakeOfferDataModel extends OfferDataModel {
}
public boolean isHalCashAccount() {
return paymentAccount instanceof HalCashAccount;
return paymentAccount.isHalCashAccount();
}
public boolean isCurrencyForTakerFeeBtc() {
return OfferUtil.isCurrencyForTakerFeeBtc(preferences, bsqWalletService, amount.get());
return offerUtil.isCurrencyForTakerFeeBtc(amount.get());
}
public void setPreferredCurrencyForTakerFeeBtc(boolean isCurrencyForTakerFeeBtc) {
@ -659,18 +660,18 @@ class TakeOfferDataModel extends OfferDataModel {
}
public Coin getTakerFeeInBtc() {
return OfferUtil.getTakerFee(true, amount.get());
return offerUtil.getTakerFee(true, amount.get());
}
public Coin getTakerFeeInBsq() {
return OfferUtil.getTakerFee(false, amount.get());
return offerUtil.getTakerFee(false, amount.get());
}
boolean isTakerFeeValid() {
return preferences.getPayFeeInBtc() || OfferUtil.isBsqForTakerFeeAvailable(bsqWalletService, amount.get());
return preferences.getPayFeeInBtc() || offerUtil.isBsqForTakerFeeAvailable(amount.get());
}
public boolean isBsqForFeeAvailable() {
return OfferUtil.isBsqForTakerFeeAvailable(bsqWalletService, amount.get());
return offerUtil.isBsqForTakerFeeAvailable(amount.get());
}
}

View file

@ -41,12 +41,11 @@ import bisq.core.offer.OfferUtil;
import bisq.core.payment.PaymentAccount;
import bisq.core.payment.payload.PaymentMethod;
import bisq.core.provider.fee.FeeService;
import bisq.core.provider.price.PriceFeedService;
import bisq.core.trade.Trade;
import bisq.core.user.Preferences;
import bisq.core.util.FormattingUtils;
import bisq.core.util.coin.BsqFormatter;
import bisq.core.util.coin.CoinFormatter;
import bisq.core.util.coin.CoinUtil;
import bisq.core.util.validation.InputValidator;
import bisq.network.p2p.P2PService;
@ -86,11 +85,10 @@ import static javafx.beans.binding.Bindings.createStringBinding;
class TakeOfferViewModel extends ActivatableWithDataModel<TakeOfferDataModel> implements ViewModel {
final TakeOfferDataModel dataModel;
private final OfferUtil offerUtil;
private final BtcValidator btcValidator;
private final P2PService p2PService;
private final Preferences preferences;
private final PriceFeedService priceFeedService;
private AccountAgeWitnessService accountAgeWitnessService;
private final AccountAgeWitnessService accountAgeWitnessService;
private final Navigation navigation;
private final CoinFormatter btcFormatter;
private final BsqFormatter bsqFormatter;
@ -101,7 +99,6 @@ class TakeOfferViewModel extends ActivatableWithDataModel<TakeOfferDataModel> im
private Trade trade;
private Offer offer;
private String price;
private String directionLabel;
private String amountDescription;
final StringProperty amount = new SimpleStringProperty();
@ -146,21 +143,18 @@ class TakeOfferViewModel extends ActivatableWithDataModel<TakeOfferDataModel> im
@Inject
public TakeOfferViewModel(TakeOfferDataModel dataModel,
OfferUtil offerUtil,
BtcValidator btcValidator,
P2PService p2PService,
Preferences preferences,
PriceFeedService priceFeedService,
AccountAgeWitnessService accountAgeWitnessService,
Navigation navigation,
@Named(FormattingUtils.BTC_FORMATTER_KEY) CoinFormatter btcFormatter,
BsqFormatter bsqFormatter) {
super(dataModel);
this.dataModel = dataModel;
this.offerUtil = offerUtil;
this.btcValidator = btcValidator;
this.p2PService = p2PService;
this.preferences = preferences;
this.priceFeedService = priceFeedService;
this.accountAgeWitnessService = accountAgeWitnessService;
this.navigation = navigation;
this.btcFormatter = btcFormatter;
@ -207,13 +201,9 @@ class TakeOfferViewModel extends ActivatableWithDataModel<TakeOfferDataModel> im
dataModel.initWithData(offer);
this.offer = offer;
if (offer.isBuyOffer()) {
directionLabel = Res.get("shared.sellBitcoin");
amountDescription = Res.get("takeOffer.amountPriceBox.buy.amountDescription");
} else {
directionLabel = Res.get("shared.buyBitcoin");
amountDescription = Res.get("takeOffer.amountPriceBox.sell.amountDescription");
}
amountDescription = offer.isBuyOffer()
? Res.get("takeOffer.amountPriceBox.buy.amountDescription")
: Res.get("takeOffer.amountPriceBox.sell.amountDescription");
amountRange = btcFormatter.formatCoin(offer.getMinAmount()) + " - " + btcFormatter.formatCoin(offer.getAmount());
price = FormattingUtils.formatPrice(dataModel.tradePrice);
@ -296,8 +286,9 @@ class TakeOfferViewModel extends ActivatableWithDataModel<TakeOfferDataModel> im
tradeFee.set(getFormatterForTakerFee().formatCoin(takerFeeAsCoin));
Coin makerFeeInBtc = dataModel.getTakerFeeInBtc();
Optional<Volume> optionalBtcFeeInFiat = OfferUtil.getFeeInUserFiatCurrency(makerFeeInBtc,
true, preferences, priceFeedService, bsqFormatter);
Optional<Volume> optionalBtcFeeInFiat = offerUtil.getFeeInUserFiatCurrency(makerFeeInBtc,
true,
bsqFormatter);
String btcFeeWithFiatAmount = DisplayUtils.getFeeWithFiatAmount(makerFeeInBtc, optionalBtcFeeInFiat, btcFormatter);
if (DevEnv.isDaoActivated()) {
tradeFeeInBtcWithFiat.set(btcFeeWithFiatAmount);
@ -306,8 +297,9 @@ class TakeOfferViewModel extends ActivatableWithDataModel<TakeOfferDataModel> im
}
Coin makerFeeInBsq = dataModel.getTakerFeeInBsq();
Optional<Volume> optionalBsqFeeInFiat = OfferUtil.getFeeInUserFiatCurrency(makerFeeInBsq,
false, preferences, priceFeedService, bsqFormatter);
Optional<Volume> optionalBsqFeeInFiat = offerUtil.getFeeInUserFiatCurrency(makerFeeInBsq,
false,
bsqFormatter);
String bsqFeeWithFiatAmount = DisplayUtils.getFeeWithFiatAmount(makerFeeInBsq, optionalBsqFeeInFiat, bsqFormatter);
if (DevEnv.isDaoActivated()) {
tradeFeeInBsqWithFiat.set(bsqFeeWithFiatAmount);
@ -355,7 +347,7 @@ class TakeOfferViewModel extends ActivatableWithDataModel<TakeOfferDataModel> im
Price tradePrice = dataModel.tradePrice;
long maxTradeLimit = dataModel.getMaxTradeLimit();
if (dataModel.getPaymentMethod().getId().equals(PaymentMethod.HAL_CASH_ID)) {
Coin adjustedAmountForHalCash = OfferUtil.getAdjustedAmountForHalCash(dataModel.getAmount().get(),
Coin adjustedAmountForHalCash = CoinUtil.getAdjustedAmountForHalCash(dataModel.getAmount().get(),
tradePrice,
maxTradeLimit);
dataModel.applyAmount(adjustedAmountForHalCash);
@ -364,7 +356,7 @@ class TakeOfferViewModel extends ActivatableWithDataModel<TakeOfferDataModel> im
if (!isAmountEqualMinAmount(dataModel.getAmount().get()) && (!isAmountEqualMaxAmount(dataModel.getAmount().get()))) {
// We only apply the rounding if the amount is variable (minAmount is lower as amount).
// Otherwise we could get an amount lower then the minAmount set by rounding
Coin roundedAmount = OfferUtil.getRoundedFiatAmount(dataModel.getAmount().get(), tradePrice,
Coin roundedAmount = CoinUtil.getRoundedFiatAmount(dataModel.getAmount().get(), tradePrice,
maxTradeLimit);
dataModel.applyAmount(roundedAmount);
}
@ -638,12 +630,12 @@ class TakeOfferViewModel extends ActivatableWithDataModel<TakeOfferDataModel> im
Price price = dataModel.tradePrice;
if (price != null) {
if (dataModel.isHalCashAccount()) {
amount = OfferUtil.getAdjustedAmountForHalCash(amount, price, maxTradeLimit);
amount = CoinUtil.getAdjustedAmountForHalCash(amount, price, maxTradeLimit);
} else if (CurrencyUtil.isFiatCurrency(dataModel.getCurrencyCode())
&& !isAmountEqualMinAmount(amount) && !isAmountEqualMaxAmount(amount)) {
// We only apply the rounding if the amount is variable (minAmount is lower as amount).
// Otherwise we could get an amount lower then the minAmount set by rounding
amount = OfferUtil.getRoundedFiatAmount(amount, price, maxTradeLimit);
amount = CoinUtil.getRoundedFiatAmount(amount, price, maxTradeLimit);
}
}
dataModel.applyAmount(amount);
@ -694,10 +686,6 @@ class TakeOfferViewModel extends ActivatableWithDataModel<TakeOfferDataModel> im
return price;
}
public String getDirectionLabel() {
return directionLabel;
}
public String getAmountDescription() {
return amountDescription;
}
@ -757,10 +745,6 @@ class TakeOfferViewModel extends ActivatableWithDataModel<TakeOfferDataModel> im
return GUIUtil.getPercentage(txFeeAsCoin, dataModel.getAmount().get());
}
public PaymentMethod getPaymentMethod() {
return dataModel.getPaymentMethod();
}
ObservableList<PaymentAccount> getPossiblePaymentAccounts() {
return dataModel.getPossiblePaymentAccounts();
}
@ -781,14 +765,6 @@ class TakeOfferViewModel extends ActivatableWithDataModel<TakeOfferDataModel> im
offer.setErrorMessage(null);
}
public String getBuyerSecurityDeposit() {
return btcFormatter.formatCoin(dataModel.getBuyerSecurityDeposit());
}
public String getSellerSecurityDeposit() {
return btcFormatter.formatCoin(dataModel.getSellerSecurityDeposit());
}
private CoinFormatter getFormatterForTakerFee() {
return dataModel.isCurrencyForTakerFeeBtc() ? btcFormatter : bsqFormatter;
}

View file

@ -19,7 +19,6 @@ package bisq.desktop.main.portfolio.editoffer;
import bisq.desktop.Navigation;
import bisq.desktop.main.offer.MakerFeeProvider;
import bisq.desktop.main.offer.MutableOfferDataModel;
import bisq.core.account.witness.AccountAgeWitnessService;
@ -31,6 +30,7 @@ import bisq.core.locale.TradeCurrency;
import bisq.core.offer.CreateOfferService;
import bisq.core.offer.Offer;
import bisq.core.offer.OfferPayload;
import bisq.core.offer.OfferUtil;
import bisq.core.offer.OpenOffer;
import bisq.core.offer.OpenOfferManager;
import bisq.core.payment.PaymentAccount;
@ -64,6 +64,7 @@ class EditOfferDataModel extends MutableOfferDataModel {
@Inject
EditOfferDataModel(CreateOfferService createOfferService,
OpenOfferManager openOfferManager,
OfferUtil offerUtil,
BtcWalletService btcWalletService,
BsqWalletService bsqWalletService,
Preferences preferences,
@ -74,11 +75,12 @@ class EditOfferDataModel extends MutableOfferDataModel {
FeeService feeService,
@Named(FormattingUtils.BTC_FORMATTER_KEY) CoinFormatter btcFormatter,
CorePersistenceProtoResolver corePersistenceProtoResolver,
MakerFeeProvider makerFeeProvider,
TradeStatisticsManager tradeStatisticsManager,
Navigation navigation) {
super(createOfferService,
openOfferManager,
offerUtil,
btcWalletService,
bsqWalletService,
preferences,
@ -88,7 +90,6 @@ class EditOfferDataModel extends MutableOfferDataModel {
accountAgeWitnessService,
feeService,
btcFormatter,
makerFeeProvider,
tradeStatisticsManager,
navigation);
this.corePersistenceProtoResolver = corePersistenceProtoResolver;

View file

@ -27,6 +27,7 @@ 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;
@ -56,7 +57,8 @@ class EditOfferViewModel extends MutableOfferViewModel<EditOfferDataModel> {
Navigation navigation,
Preferences preferences,
@Named(FormattingUtils.BTC_FORMATTER_KEY) CoinFormatter btcFormatter,
BsqFormatter bsqFormatter) {
BsqFormatter bsqFormatter,
OfferUtil offerUtil) {
super(dataModel,
fiatVolumeValidator,
fiatPriceValidator,
@ -68,7 +70,9 @@ class EditOfferViewModel extends MutableOfferViewModel<EditOfferDataModel> {
accountAgeWitnessService,
navigation,
preferences,
btcFormatter, bsqFormatter);
btcFormatter,
bsqFormatter,
offerUtil);
syncMinAmountWithAmount = false;
}

View file

@ -1,7 +1,5 @@
package bisq.desktop.main.offer.createoffer;
import bisq.desktop.main.offer.MakerFeeProvider;
import bisq.core.btc.model.AddressEntry;
import bisq.core.btc.wallet.BtcWalletService;
import bisq.core.locale.CryptoCurrency;
@ -9,7 +7,7 @@ import bisq.core.locale.FiatCurrency;
import bisq.core.locale.GlobalSettings;
import bisq.core.locale.Res;
import bisq.core.offer.CreateOfferService;
import bisq.core.offer.OfferPayload;
import bisq.core.offer.OfferUtil;
import bisq.core.payment.ClearXchangeAccount;
import bisq.core.payment.PaymentAccount;
import bisq.core.payment.RevolutAccount;
@ -29,6 +27,7 @@ import java.util.UUID;
import org.junit.Before;
import org.junit.Test;
import static bisq.core.offer.OfferPayload.Direction;
import static org.junit.Assert.assertEquals;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
@ -40,7 +39,7 @@ public class CreateOfferDataModelTest {
private CreateOfferDataModel model;
private User user;
private Preferences preferences;
private MakerFeeProvider makerFeeProvider;
private OfferUtil offerUtil;
@Before
public void setUp() {
@ -54,6 +53,7 @@ public class CreateOfferDataModelTest {
FeeService feeService = mock(FeeService.class);
CreateOfferService createOfferService = mock(CreateOfferService.class);
preferences = mock(Preferences.class);
offerUtil = mock(OfferUtil.class);
user = mock(User.class);
var tradeStats = mock(TradeStatisticsManager.class);
@ -63,11 +63,20 @@ public class CreateOfferDataModelTest {
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, tradeStats, null);
model = new CreateOfferDataModel(createOfferService,
null,
offerUtil,
btcWalletService,
null,
preferences,
user,
null,
priceFeedService,
null,
feeService,
null,
tradeStats,
null);
}
@Test
@ -84,9 +93,9 @@ public class CreateOfferDataModelTest {
when(user.getPaymentAccounts()).thenReturn(paymentAccounts);
when(preferences.getSelectedPaymentAccountForCreateOffer()).thenReturn(revolutAccount);
when(makerFeeProvider.getMakerFee(any(), any(), any())).thenReturn(Coin.ZERO);
when(offerUtil.getMakerFee(any())).thenReturn(Coin.ZERO);
model.initWithData(OfferPayload.Direction.BUY, new FiatCurrency("USD"));
model.initWithData(Direction.BUY, new FiatCurrency("USD"));
assertEquals("USD", model.getTradeCurrencyCode().get());
}
@ -104,10 +113,9 @@ public class CreateOfferDataModelTest {
when(user.getPaymentAccounts()).thenReturn(paymentAccounts);
when(user.findFirstPaymentAccountWithCurrency(new FiatCurrency("USD"))).thenReturn(zelleAccount);
when(preferences.getSelectedPaymentAccountForCreateOffer()).thenReturn(revolutAccount);
when(makerFeeProvider.getMakerFee(any(), any(), any())).thenReturn(Coin.ZERO);
when(offerUtil.getMakerFee(any())).thenReturn(Coin.ZERO);
model.initWithData(OfferPayload.Direction.BUY, new FiatCurrency("USD"));
model.initWithData(Direction.BUY, new FiatCurrency("USD"));
assertEquals("USD", model.getTradeCurrencyCode().get());
}
}

View file

@ -17,7 +17,6 @@
package bisq.desktop.main.offer.createoffer;
import bisq.desktop.main.offer.MakerFeeProvider;
import bisq.desktop.util.validation.AltcoinValidator;
import bisq.desktop.util.validation.BtcValidator;
import bisq.desktop.util.validation.FiatPriceValidator;
@ -32,7 +31,7 @@ import bisq.core.locale.CryptoCurrency;
import bisq.core.locale.GlobalSettings;
import bisq.core.locale.Res;
import bisq.core.offer.CreateOfferService;
import bisq.core.offer.OfferPayload;
import bisq.core.offer.OfferUtil;
import bisq.core.payment.PaymentAccount;
import bisq.core.payment.payload.PaymentMethod;
import bisq.core.provider.fee.FeeService;
@ -61,6 +60,7 @@ import java.util.UUID;
import org.junit.Before;
import org.junit.Test;
import static bisq.core.offer.OfferPayload.Direction;
import static bisq.desktop.maker.PreferenceMakers.empty;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
@ -73,7 +73,8 @@ import static org.mockito.Mockito.when;
public class CreateOfferViewModelTest {
private CreateOfferViewModel model;
private final CoinFormatter coinFormatter = new ImmutableCoinFormatter(Config.baseCurrencyNetworkParameters().getMonetaryFormat());
private final CoinFormatter coinFormatter = new ImmutableCoinFormatter(
Config.baseCurrencyNetworkParameters().getMonetaryFormat());
@Before
public void setUp() {
@ -97,12 +98,17 @@ public class CreateOfferViewModelTest {
SecurityDepositValidator securityDepositValidator = mock(SecurityDepositValidator.class);
AccountAgeWitnessService accountAgeWitnessService = mock(AccountAgeWitnessService.class);
CreateOfferService createOfferService = mock(CreateOfferService.class);
OfferUtil offerUtil = mock(OfferUtil.class);
var tradeStats = mock(TradeStatisticsManager.class);
when(btcWalletService.getOrCreateAddressEntry(anyString(), any())).thenReturn(addressEntry);
when(btcWalletService.getBalanceForAddress(any())).thenReturn(Coin.valueOf(1000L));
when(priceFeedService.updateCounterProperty()).thenReturn(new SimpleIntegerProperty());
when(priceFeedService.getMarketPrice(anyString())).thenReturn(new MarketPrice("USD", 12684.0450, Instant.now().getEpochSecond(), true));
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));
@ -115,16 +121,37 @@ public class CreateOfferViewModelTest {
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), tradeStats, null);
dataModel.initWithData(OfferPayload.Direction.BUY, new CryptoCurrency("BTC", "bitcoin"));
CreateOfferDataModel dataModel = new CreateOfferDataModel(createOfferService,
null,
offerUtil,
btcWalletService,
bsqWalletService,
empty,
user,
null,
priceFeedService,
accountAgeWitnessService,
feeService,
coinFormatter,
tradeStats,
null);
dataModel.initWithData(Direction.BUY, new CryptoCurrency("BTC", "bitcoin"));
dataModel.activate();
model = new CreateOfferViewModel(dataModel, null, fiatPriceValidator, altcoinValidator,
btcValidator, null, securityDepositValidator, priceFeedService, null, null,
preferences, coinFormatter, bsqFormatter);
model = new CreateOfferViewModel(dataModel,
null,
fiatPriceValidator,
altcoinValidator,
btcValidator,
null,
securityDepositValidator,
priceFeedService,
null,
null,
preferences,
coinFormatter,
bsqFormatter,
offerUtil);
model.activate();
}

View file

@ -1,6 +1,5 @@
package bisq.desktop.main.portfolio.editoffer;
import bisq.desktop.main.offer.MakerFeeProvider;
import bisq.desktop.util.validation.SecurityDepositValidator;
import bisq.core.account.witness.AccountAgeWitnessService;
@ -13,6 +12,7 @@ import bisq.core.locale.GlobalSettings;
import bisq.core.locale.Res;
import bisq.core.offer.CreateOfferService;
import bisq.core.offer.OfferPayload;
import bisq.core.offer.OfferUtil;
import bisq.core.offer.OpenOffer;
import bisq.core.payment.CryptoCurrencyAccount;
import bisq.core.payment.PaymentAccount;
@ -77,11 +77,16 @@ public class EditOfferDataModelTest {
SecurityDepositValidator securityDepositValidator = mock(SecurityDepositValidator.class);
AccountAgeWitnessService accountAgeWitnessService = mock(AccountAgeWitnessService.class);
CreateOfferService createOfferService = mock(CreateOfferService.class);
OfferUtil offerUtil = mock(OfferUtil.class);
when(btcWalletService.getOrCreateAddressEntry(anyString(), any())).thenReturn(addressEntry);
when(btcWalletService.getBalanceForAddress(any())).thenReturn(Coin.valueOf(1000L));
when(priceFeedService.updateCounterProperty()).thenReturn(new SimpleIntegerProperty());
when(priceFeedService.getMarketPrice(anyString())).thenReturn(new MarketPrice("USD", 12684.0450, Instant.now().getEpochSecond(), true));
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(user.getPaymentAccountsAsObservable()).thenReturn(FXCollections.observableSet());
@ -92,11 +97,21 @@ public class EditOfferDataModelTest {
when(bsqWalletService.getAvailableConfirmedBalance()).thenReturn(Coin.ZERO);
when(createOfferService.getRandomOfferId()).thenReturn(UUID.randomUUID().toString());
model = new EditOfferDataModel(createOfferService, null,
btcWalletService, bsqWalletService, empty, user,
null, priceFeedService,
accountAgeWitnessService, feeService, null, null,
mock(MakerFeeProvider.class), mock(TradeStatisticsManager.class), null);
model = new EditOfferDataModel(createOfferService,
null,
offerUtil,
btcWalletService,
bsqWalletService,
empty,
user,
null,
priceFeedService,
accountAgeWitnessService,
feeService,
null,
null,
mock(TradeStatisticsManager.class),
null);
}
@Test