diff --git a/core/src/main/java/bisq/core/app/AppOptionKeys.java b/core/src/main/java/bisq/core/app/AppOptionKeys.java index 6c44f7d4f81..c9036c38865 100644 --- a/core/src/main/java/bisq/core/app/AppOptionKeys.java +++ b/core/src/main/java/bisq/core/app/AppOptionKeys.java @@ -29,4 +29,6 @@ public class AppOptionKeys { public static final String IGNORE_DEV_MSG_KEY = "ignoreDevMsg"; public static final String USE_DEV_PRIVILEGE_KEYS = "useDevPrivilegeKeys"; public static final String REFERRAL_ID = "referralId"; + public static final String HTTP_API_HOST = "httpApiHost"; + public static final String HTTP_API_PORT = "httpApiPort"; } diff --git a/core/src/main/java/bisq/core/app/BisqEnvironment.java b/core/src/main/java/bisq/core/app/BisqEnvironment.java index b75074c5b51..5703beea70c 100644 --- a/core/src/main/java/bisq/core/app/BisqEnvironment.java +++ b/core/src/main/java/bisq/core/app/BisqEnvironment.java @@ -193,8 +193,10 @@ private static String appDataDir(String userDataDir, String appName) { protected final String btcNodes, seedNodes, ignoreDevMsg, useDevPrivilegeKeys, useDevMode, useTorForBtc, rpcUser, rpcPassword, rpcPort, rpcBlockNotificationPort, dumpBlockchainData, fullDaoNode, myAddress, banList, dumpStatistics, maxMemory, socks5ProxyBtcAddress, - socks5ProxyHttpAddress, useAllProvidedNodes, numConnectionForBtc, genesisTxId, genesisBlockHeight, referralId, daoActivated; - + socks5ProxyHttpAddress, useAllProvidedNodes, numConnectionForBtc, genesisTxId, genesisBlockHeight, + referralId, daoActivated; + @Getter + protected final String httpApiHost, httpApiPort; public BisqEnvironment(OptionSet options) { this(new JOptCommandLinePropertySource(BISQ_COMMANDLINE_PROPERTY_SOURCE_NAME, checkNotNull( @@ -237,6 +239,12 @@ public BisqEnvironment(PropertySource commandLineProperties) { referralId = commandLineProperties.containsProperty(AppOptionKeys.REFERRAL_ID) ? (String) commandLineProperties.getProperty(AppOptionKeys.REFERRAL_ID) : ""; + httpApiHost = commandLineProperties.containsProperty(AppOptionKeys.HTTP_API_HOST) ? + (String) commandLineProperties.getProperty(AppOptionKeys.HTTP_API_HOST) : + "127.0.0.1"; + httpApiPort = commandLineProperties.containsProperty(AppOptionKeys.HTTP_API_PORT) ? + (String) commandLineProperties.getProperty(AppOptionKeys.HTTP_API_PORT) : + "8080"; useDevMode = commandLineProperties.containsProperty(CommonOptionKeys.USE_DEV_MODE) ? (String) commandLineProperties.getProperty(CommonOptionKeys.USE_DEV_MODE) : ""; diff --git a/core/src/main/java/bisq/core/app/BisqExecutable.java b/core/src/main/java/bisq/core/app/BisqExecutable.java index 57c9dd4cd43..f328e5e2cc3 100644 --- a/core/src/main/java/bisq/core/app/BisqExecutable.java +++ b/core/src/main/java/bisq/core/app/BisqExecutable.java @@ -74,7 +74,7 @@ import static java.lang.String.join; @Slf4j -public abstract class BisqExecutable implements GracefulShutDownHandler { +public abstract class BisqExecutable implements GracefulShutDownHandler, BisqSetup.BisqSetupCompleteListener { static { Utilities.removeCryptographyRestrictions(); } @@ -244,9 +244,12 @@ protected void onApplicationStarted() { protected void startAppSetup() { BisqSetup bisqSetup = injector.getInstance(BisqSetup.class); + bisqSetup.addBisqSetupCompleteListener(this); bisqSetup.start(); } + public abstract void onSetupComplete(); + /////////////////////////////////////////////////////////////////////////////////////////// // GracefulShutDownHandler implementation @@ -365,6 +368,12 @@ protected void customizeOptionParsing(OptionParser parser) { parser.accepts(AppOptionKeys.REFERRAL_ID, description("Optional Referral ID (e.g. for API users or pro market makers)", "")) .withRequiredArg(); + parser.accepts(AppOptionKeys.HTTP_API_HOST, + description("Optional HTTP API host", "127.0.0.1")) + .withRequiredArg(); + parser.accepts(AppOptionKeys.HTTP_API_PORT, + description("Optional HTTP API port", "8080")) + .withRequiredArg(); parser.accepts(CommonOptionKeys.USE_DEV_MODE, description("Enables dev mode which is used for convenience for developer testing", false)) .withRequiredArg() diff --git a/core/src/main/java/bisq/core/app/BisqFacade.java b/core/src/main/java/bisq/core/app/BisqFacade.java index 235c5f01179..4a7b6997112 100644 --- a/core/src/main/java/bisq/core/app/BisqFacade.java +++ b/core/src/main/java/bisq/core/app/BisqFacade.java @@ -17,7 +17,7 @@ package bisq.core.app; -import bisq.core.btc.model.BalanceModel; +import bisq.core.btc.Balances; import bisq.core.presentation.BalancePresentation; import bisq.common.app.Version; @@ -29,12 +29,12 @@ * E.g. useful for different APIs to access data of different domains of Bisq. */ public class BisqFacade { - private final BalanceModel balanceModel; + private final Balances balances; private final BalancePresentation balancePresentation; @Inject - public BisqFacade(BalanceModel balanceModel, BalancePresentation balancePresentation) { - this.balanceModel = balanceModel; + public BisqFacade(Balances balances, BalancePresentation balancePresentation) { + this.balances = balances; this.balancePresentation = balancePresentation; } @@ -43,8 +43,7 @@ public String getVersion() { } public long getAvailableBalance() { - balanceModel.updateBalance(); - return balanceModel.getAvailableBalance().get().getValue(); + return balances.getAvailableBalance().get().getValue(); } public String getAvailableBalanceAsString() { diff --git a/core/src/main/java/bisq/core/app/BisqHeadlessApp.java b/core/src/main/java/bisq/core/app/BisqHeadlessApp.java index 4518da4ec94..acfcb380902 100644 --- a/core/src/main/java/bisq/core/app/BisqHeadlessApp.java +++ b/core/src/main/java/bisq/core/app/BisqHeadlessApp.java @@ -54,7 +54,6 @@ public BisqHeadlessApp() { public void startApplication() { try { bisqSetup = injector.getInstance(BisqSetup.class); - bisqSetup.addBisqSetupCompleteListener(this); corruptedDatabaseFilesHandler = injector.getInstance(CorruptedDatabaseFilesHandler.class); tradeManager = injector.getInstance(TradeManager.class); diff --git a/core/src/main/java/bisq/core/app/BisqHeadlessAppMain.java b/core/src/main/java/bisq/core/app/BisqHeadlessAppMain.java index 22a9543f81f..16b6acb1ab5 100644 --- a/core/src/main/java/bisq/core/app/BisqHeadlessAppMain.java +++ b/core/src/main/java/bisq/core/app/BisqHeadlessAppMain.java @@ -80,6 +80,11 @@ protected void onApplicationLaunched() { headlessApp.setGracefulShutDownHandler(this); } + @Override + public void onSetupComplete() { + log.info("onSetupComplete"); + } + /////////////////////////////////////////////////////////////////////////////////////////// // We continue with a series of synchronous execution tasks /////////////////////////////////////////////////////////////////////////////////////////// diff --git a/core/src/main/java/bisq/core/app/BisqSetup.java b/core/src/main/java/bisq/core/app/BisqSetup.java index e7511c6bd71..07daf67afdf 100644 --- a/core/src/main/java/bisq/core/app/BisqSetup.java +++ b/core/src/main/java/bisq/core/app/BisqSetup.java @@ -23,9 +23,8 @@ import bisq.core.alert.PrivateNotificationPayload; import bisq.core.arbitration.ArbitratorManager; import bisq.core.arbitration.DisputeManager; -import bisq.core.btc.listeners.BalanceListener; +import bisq.core.btc.Balances; import bisq.core.btc.model.AddressEntry; -import bisq.core.btc.model.BalanceModel; import bisq.core.btc.setup.WalletsSetup; import bisq.core.btc.wallet.BtcWalletService; import bisq.core.btc.wallet.WalletsManager; @@ -40,14 +39,12 @@ import bisq.core.notifications.alerts.TradeEvents; import bisq.core.notifications.alerts.market.MarketAlerts; import bisq.core.notifications.alerts.price.PriceAlert; -import bisq.core.offer.OpenOffer; import bisq.core.offer.OpenOfferManager; import bisq.core.payment.AccountAgeWitnessService; 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.trade.TradeManager; import bisq.core.trade.statistics.TradeStatisticsManager; import bisq.core.user.Preferences; @@ -71,7 +68,6 @@ import bisq.common.util.Utilities; import org.bitcoinj.core.Coin; -import org.bitcoinj.core.Transaction; import javax.inject.Inject; @@ -123,7 +119,7 @@ public interface BisqSetupCompleteListener { private final WalletsManager walletsManager; private final WalletsSetup walletsSetup; private final BtcWalletService btcWalletService; - private final BalanceModel balanceModel; + private final Balances balances; private final PriceFeedService priceFeedService; private final ArbitratorManager arbitratorManager; private final P2PService p2PService; @@ -196,7 +192,7 @@ public BisqSetup(P2PNetworkSetup p2PNetworkSetup, WalletsManager walletsManager, WalletsSetup walletsSetup, BtcWalletService btcWalletService, - BalanceModel balanceModel, + Balances balances, PriceFeedService priceFeedService, ArbitratorManager arbitratorManager, P2PService p2PService, @@ -232,7 +228,7 @@ public BisqSetup(P2PNetworkSetup p2PNetworkSetup, this.walletsManager = walletsManager; this.walletsSetup = walletsSetup; this.btcWalletService = btcWalletService; - this.balanceModel = balanceModel; + this.balances = balances; this.priceFeedService = priceFeedService; this.arbitratorManager = arbitratorManager; this.p2PService = p2PService; @@ -578,29 +574,12 @@ private void initDomainServices() { disputeManager.onAllServicesInitialized(); tradeManager.onAllServicesInitialized(); - tradeManager.getTradableList().addListener((ListChangeListener) change -> balanceModel.updateBalance()); - tradeManager.getAddressEntriesForAvailableBalanceStream() - .filter(addressEntry -> addressEntry.getOfferId() != null) - .forEach(addressEntry -> { - log.debug("swapPendingOfferFundingEntries, offerId={}, OFFER_FUNDING", addressEntry.getOfferId()); - btcWalletService.swapTradeEntryToAvailableEntry(addressEntry.getOfferId(), AddressEntry.Context.OFFER_FUNDING); - }); - - btcWalletService.addBalanceListener(new BalanceListener() { - @Override - public void onBalanceChanged(Coin balance, Transaction tx) { - balanceModel.updateBalance(); - } - }); if (walletsSetup.downloadPercentageProperty().get() == 1) checkForLockedUpFunds(); - balanceModel.updateBalance(); - - openOfferManager.getObservableList().addListener((ListChangeListener) c -> balanceModel.updateBalance()); openOfferManager.onAllServicesInitialized(); - + balances.onAllServicesInitialized(); arbitratorManager.onAllServicesInitialized(); alertManager.alertMessageProperty().addListener((observable, oldValue, newValue) -> diff --git a/core/src/main/java/bisq/core/app/misc/ExecutableForAppWithP2p.java b/core/src/main/java/bisq/core/app/misc/ExecutableForAppWithP2p.java index a1dbf242e2a..9042e385022 100644 --- a/core/src/main/java/bisq/core/app/misc/ExecutableForAppWithP2p.java +++ b/core/src/main/java/bisq/core/app/misc/ExecutableForAppWithP2p.java @@ -60,6 +60,11 @@ protected void configUserThread() { UserThread.setExecutor(Executors.newSingleThreadExecutor(threadFactory)); } + @Override + public void onSetupComplete() { + log.info("onSetupComplete"); + } + // We don't use the gracefulShutDown implementation of the super class as we have a limited set of modules @Override public void gracefulShutDown(ResultHandler resultHandler) { diff --git a/core/src/main/java/bisq/core/btc/BalanceUtil.java b/core/src/main/java/bisq/core/btc/BalanceUtil.java new file mode 100644 index 00000000000..24b0e79ecc1 --- /dev/null +++ b/core/src/main/java/bisq/core/btc/BalanceUtil.java @@ -0,0 +1,82 @@ +/* + * 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 . + */ + +package bisq.core.btc; + +import bisq.core.btc.model.AddressEntry; +import bisq.core.btc.wallet.BtcWalletService; +import bisq.core.offer.OpenOffer; +import bisq.core.offer.OpenOfferManager; +import bisq.core.trade.Trade; +import bisq.core.trade.TradeManager; +import bisq.core.trade.closed.ClosedTradableManager; +import bisq.core.trade.failed.FailedTradesManager; + +import bisq.common.util.Tuple2; + +import javax.inject.Inject; + +import java.util.Objects; +import java.util.stream.Stream; + +public class BalanceUtil { + private final TradeManager tradeManager; + private final BtcWalletService btcWalletService; + private final OpenOfferManager openOfferManager; + private final ClosedTradableManager closedTradableManager; + private final FailedTradesManager failedTradesManager; + + @Inject + public BalanceUtil(TradeManager tradeManager, BtcWalletService btcWalletService, OpenOfferManager openOfferManager, + ClosedTradableManager closedTradableManager, FailedTradesManager failedTradesManager) { + this.tradeManager = tradeManager; + this.btcWalletService = btcWalletService; + this.openOfferManager = openOfferManager; + this.closedTradableManager = closedTradableManager; + this.failedTradesManager = failedTradesManager; + } + + public Stream getAddressEntriesForAvailableFunds() { + return tradeManager.getAddressEntriesForAvailableBalanceStream(); + } + + public Stream getAddressEntriesForReservedFunds() { + return getOpenOfferAndAddressEntriesForReservedFunds().map(tuple2 -> tuple2.second); + } + + public Stream> getOpenOfferAndAddressEntriesForReservedFunds() { + return openOfferManager.getObservableList().stream() + .map(openOffer -> btcWalletService.getAddressEntry(openOffer.getId(), AddressEntry.Context.RESERVED_FOR_TRADE) + .map(addressEntry -> new Tuple2<>(openOffer, addressEntry)) + .orElse(null)) + .filter(Objects::nonNull); + } + + public Stream getAddressEntriesForLockedFunds() { + return getTradesAndAddressEntriesForLockedFunds().map(tuple2 -> tuple2.second); + } + + public Stream> getTradesAndAddressEntriesForLockedFunds() { + Stream lockedTrades = Stream.concat(closedTradableManager.getLockedTradesStream(), failedTradesManager.getLockedTradesStream()); + lockedTrades = Stream.concat(lockedTrades, tradeManager.getLockedTradesStream()); + return lockedTrades + .map(trade -> btcWalletService.getAddressEntry(trade.getId(), AddressEntry.Context.MULTI_SIG) + .map(addressEntry -> new Tuple2<>(trade, addressEntry)) + .orElse(null)) + .filter(Objects::nonNull); + } +} diff --git a/core/src/main/java/bisq/core/btc/Balances.java b/core/src/main/java/bisq/core/btc/Balances.java new file mode 100644 index 00000000000..87f86f722b6 --- /dev/null +++ b/core/src/main/java/bisq/core/btc/Balances.java @@ -0,0 +1,108 @@ +/* + * 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 . + */ + +package bisq.core.btc; + +import bisq.core.btc.listeners.BalanceListener; +import bisq.core.btc.wallet.BtcWalletService; +import bisq.core.offer.OpenOffer; +import bisq.core.offer.OpenOfferManager; +import bisq.core.trade.Trade; +import bisq.core.trade.TradeManager; +import bisq.core.trade.closed.ClosedTradableManager; +import bisq.core.trade.failed.FailedTradesManager; + +import bisq.common.UserThread; + +import org.bitcoinj.core.Coin; +import org.bitcoinj.core.Transaction; + +import javax.inject.Inject; + +import javafx.beans.property.ObjectProperty; +import javafx.beans.property.SimpleObjectProperty; + +import javafx.collections.ListChangeListener; + +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class Balances { + private final BalanceUtil balanceUtil; + private final TradeManager tradeManager; + private final BtcWalletService btcWalletService; + private final OpenOfferManager openOfferManager; + + @Getter + private final ObjectProperty availableBalance = new SimpleObjectProperty<>(); + @Getter + private final ObjectProperty reservedBalance = new SimpleObjectProperty<>(); + @Getter + private final ObjectProperty lockedBalance = new SimpleObjectProperty<>(); + + @Inject + public Balances(BalanceUtil balanceUtil, TradeManager tradeManager, BtcWalletService btcWalletService, OpenOfferManager openOfferManager, + ClosedTradableManager closedTradableManager, FailedTradesManager failedTradesManager) { + this.balanceUtil = balanceUtil; + this.tradeManager = tradeManager; + this.btcWalletService = btcWalletService; + this.openOfferManager = openOfferManager; + } + + public void onAllServicesInitialized() { + openOfferManager.getObservableList().addListener((ListChangeListener) c -> updateBalance()); + tradeManager.getTradableList().addListener((ListChangeListener) change -> updateBalance()); + btcWalletService.addBalanceListener(new BalanceListener() { + @Override + public void onBalanceChanged(Coin balance, Transaction tx) { + updateBalance(); + } + }); + updateBalance(); + } + + private void updateBalance() { + // Need to delay a bit to get the balances correct + UserThread.execute(() -> { + updateAvailableBalance(); + updateReservedBalance(); + updateLockedBalance(); + }); + } + + private void updateAvailableBalance() { + Coin sum = Coin.valueOf(balanceUtil.getAddressEntriesForAvailableFunds() + .mapToLong(addressEntry -> btcWalletService.getBalanceForAddress(addressEntry.getAddress()).value) + .sum()); + availableBalance.set(sum); + } + + private void updateReservedBalance() { + Coin sum = Coin.valueOf(balanceUtil.getAddressEntriesForReservedFunds() + .mapToLong(addressEntry -> btcWalletService.getBalanceForAddress(addressEntry.getAddress()).value) + .sum()); + reservedBalance.set(sum); + } + + private void updateLockedBalance() { + Coin sum = Coin.valueOf(balanceUtil.getAddressEntriesForLockedFunds() + .mapToLong(addressEntry -> addressEntry.getCoinLockedInMultiSig().getValue()) + .sum()); + lockedBalance.set(sum); + } +} diff --git a/core/src/main/java/bisq/core/btc/BitcoinModule.java b/core/src/main/java/bisq/core/btc/BitcoinModule.java index b9e365a3bbd..f2ab467e975 100644 --- a/core/src/main/java/bisq/core/btc/BitcoinModule.java +++ b/core/src/main/java/bisq/core/btc/BitcoinModule.java @@ -19,7 +19,6 @@ import bisq.core.app.AppOptionKeys; import bisq.core.btc.model.AddressEntryList; -import bisq.core.btc.model.BalanceModel; import bisq.core.btc.nodes.BtcNodes; import bisq.core.btc.setup.RegTestHost; import bisq.core.btc.setup.WalletsSetup; @@ -78,7 +77,8 @@ protected void configure() { bind(BsqCoinSelector.class).in(Singleton.class); bind(NonBsqCoinSelector.class).in(Singleton.class); bind(BtcNodes.class).in(Singleton.class); - bind(BalanceModel.class).in(Singleton.class); + bind(Balances.class).in(Singleton.class); + bind(BalanceUtil.class).in(Singleton.class); bind(PriceNodeHttpClient.class).in(Singleton.class); diff --git a/core/src/main/java/bisq/core/btc/model/AddressEntryList.java b/core/src/main/java/bisq/core/btc/model/AddressEntryList.java index 42a5bb0ee57..e3157ade487 100644 --- a/core/src/main/java/bisq/core/btc/model/AddressEntryList.java +++ b/core/src/main/java/bisq/core/btc/model/AddressEntryList.java @@ -77,11 +77,14 @@ public static AddressEntryList fromProto(PB.AddressEntryList proto) { @Override public Message toProtoMessage() { + // We clone here as we got ConcurrentModificationExceptions + ArrayList clone = new ArrayList<>(this.list); + List addressEntries = clone.stream() + .map(AddressEntry::toProtoMessage) + .collect(Collectors.toList()); return PB.PersistableEnvelope.newBuilder() .setAddressEntryList(PB.AddressEntryList.newBuilder() - .addAllAddressEntry(list.stream() - .map(AddressEntry::toProtoMessage) - .collect(Collectors.toList()))) + .addAllAddressEntry(addressEntries)) .build(); } diff --git a/core/src/main/java/bisq/core/btc/wallet/BtcWalletService.java b/core/src/main/java/bisq/core/btc/wallet/BtcWalletService.java index b58901d8d75..7c4f0828107 100644 --- a/core/src/main/java/bisq/core/btc/wallet/BtcWalletService.java +++ b/core/src/main/java/bisq/core/btc/wallet/BtcWalletService.java @@ -17,6 +17,7 @@ package bisq.core.btc.wallet; +import bisq.core.app.BisqEnvironment; import bisq.core.btc.exceptions.AddressEntryException; import bisq.core.btc.exceptions.InsufficientFundsException; import bisq.core.btc.exceptions.TransactionVerificationException; @@ -946,6 +947,24 @@ private boolean feeEstimationNotSatisfied(int counter, Transaction tx) { tx.getFee().value - targetFee > 1000); } + public int getEstimatedFeeTxSize(List outputValues, Coin txFee) + throws InsufficientMoneyException, AddressFormatException { + Transaction transaction = new Transaction(params); + Address dummyAddress = wallet.currentReceiveKey().toAddress(BisqEnvironment.getParameters()); + outputValues.forEach(outputValue -> transaction.addOutput(outputValue, dummyAddress)); + + SendRequest sendRequest = SendRequest.forTx(transaction); + sendRequest.shuffleOutputs = false; + sendRequest.aesKey = aesKey; + sendRequest.coinSelector = new BtcCoinSelector(walletsSetup.getAddressesByContext(AddressEntry.Context.AVAILABLE)); + sendRequest.fee = txFee; + sendRequest.feePerKb = Coin.ZERO; + sendRequest.ensureMinRequiredFee = false; + sendRequest.changeAddress = dummyAddress; + wallet.completeTx(sendRequest); + return transaction.bitcoinSerialize().length; + } + /////////////////////////////////////////////////////////////////////////////////////////// // Withdrawal Send diff --git a/core/src/main/java/bisq/core/btc/wallet/TradeWalletService.java b/core/src/main/java/bisq/core/btc/wallet/TradeWalletService.java index cc41d021959..69a5162568a 100644 --- a/core/src/main/java/bisq/core/btc/wallet/TradeWalletService.java +++ b/core/src/main/java/bisq/core/btc/wallet/TradeWalletService.java @@ -230,37 +230,6 @@ public Transaction createBtcTradingFeeTx(Address fundingAddress, } } - public Transaction estimateBtcTradingFeeTxSize(Address fundingAddress, - Address reservedForTradeAddress, - Address changeAddress, - Coin reservedFundsForOffer, - boolean useSavingsWallet, - Coin tradingFee, - Coin txFee, - String feeReceiverAddresses) - throws InsufficientMoneyException, AddressFormatException { - Transaction tradingFeeTx = new Transaction(params); - tradingFeeTx.addOutput(tradingFee, Address.fromBase58(params, feeReceiverAddresses)); - tradingFeeTx.addOutput(reservedFundsForOffer, reservedForTradeAddress); - - SendRequest sendRequest = SendRequest.forTx(tradingFeeTx); - sendRequest.shuffleOutputs = false; - sendRequest.aesKey = aesKey; - if (useSavingsWallet) - sendRequest.coinSelector = new BtcCoinSelector(walletsSetup.getAddressesByContext(AddressEntry.Context.AVAILABLE)); - else - sendRequest.coinSelector = new BtcCoinSelector(fundingAddress); - - sendRequest.fee = txFee; - sendRequest.feePerKb = Coin.ZERO; - sendRequest.ensureMinRequiredFee = false; - sendRequest.changeAddress = changeAddress; - checkNotNull(wallet, "Wallet must not be null"); - log.info("estimateBtcTradingFeeTxSize"); - wallet.completeTx(sendRequest); - return tradingFeeTx; - } - public Transaction completeBsqTradingFeeTx(Transaction preparedBsqTx, Address fundingAddress, Address reservedForTradeAddress, diff --git a/core/src/main/java/bisq/core/offer/OfferUtil.java b/core/src/main/java/bisq/core/offer/OfferUtil.java index bc87e11f4af..cc279dc98d1 100644 --- a/core/src/main/java/bisq/core/offer/OfferUtil.java +++ b/core/src/main/java/bisq/core/offer/OfferUtil.java @@ -20,23 +20,44 @@ import bisq.core.app.BisqEnvironment; import bisq.core.btc.wallet.BsqWalletService; import bisq.core.btc.wallet.Restrictions; +import bisq.core.filter.FilterManager; +import bisq.core.locale.Country; +import bisq.core.locale.CurrencyUtil; +import bisq.core.locale.Res; import bisq.core.monetary.Price; import bisq.core.monetary.Volume; +import bisq.core.payment.AccountAgeWitnessService; +import bisq.core.payment.BankAccount; +import bisq.core.payment.CountryBasedPaymentAccount; +import bisq.core.payment.F2FAccount; +import bisq.core.payment.PaymentAccount; +import bisq.core.payment.SameBankAccount; +import bisq.core.payment.SepaAccount; +import bisq.core.payment.SepaInstantAccount; +import bisq.core.payment.SpecificBanksAccount; import bisq.core.provider.fee.FeeService; +import bisq.core.trade.statistics.ReferralIdService; import bisq.core.user.Preferences; import bisq.core.util.CoinUtil; +import bisq.network.p2p.P2PService; + import bisq.common.util.MathUtils; import org.bitcoinj.core.Coin; import com.google.common.annotations.VisibleForTesting; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Map; + import lombok.extern.slf4j.Slf4j; import javax.annotation.Nullable; 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. @@ -121,7 +142,7 @@ public static Coin getMakerFee(boolean isCurrencyForMakerFeeBtc, @Nullable Coin * @return */ public static boolean isCurrencyForMakerFeeBtc(Preferences preferences, BsqWalletService bsqWalletService, Coin amount, boolean marketPriceAvailable, double marketPriceMargin) { - return preferences.getPayFeeInBtc() || + return preferences.isPayFeeInBtc() || !isBsqForFeeAvailable(bsqWalletService, amount, marketPriceAvailable, marketPriceMargin); } @@ -155,9 +176,8 @@ public static Volume getAdjustedVolumeForHalCash(Volume volumeByAmount) { } /** - * - * @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... + * @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 @@ -174,9 +194,9 @@ static Volume getAdjustedFiatVolume(Volume volumeByAmount, int factor) { * 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 ot that amount. - * @param maxTradeLimit The max. trade limit of the users account, in satoshis. + * @param amount Bitcoin amount which is a candidate for getting rounded. + * @param price Price used in relation ot 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) { @@ -191,11 +211,11 @@ public static Coin getAdjustedAmountForHalCash(Coin amount, Price price, long ma * 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 ot 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. + * @param amount Bitcoin amount which is a candidate for getting rounded. + * @param price Price used in relation ot 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 @@ -247,4 +267,101 @@ static Coin getAdjustedAmount(Coin amount, Price price, long maxTradeLimit, int adjustedAmount = Math.min(maxTradeLimit, adjustedAmount); return Coin.valueOf(adjustedAmount); } + + public static ArrayList getAcceptedCountryCodes(PaymentAccount paymentAccount) { + ArrayList acceptedCountryCodes = null; + if (paymentAccount instanceof SepaAccount) { + acceptedCountryCodes = new ArrayList<>(((SepaAccount) paymentAccount).getAcceptedCountryCodes()); + } else if (paymentAccount instanceof SepaInstantAccount) { + acceptedCountryCodes = new ArrayList<>(((SepaInstantAccount) paymentAccount).getAcceptedCountryCodes()); + } else if (paymentAccount instanceof CountryBasedPaymentAccount) { + acceptedCountryCodes = new ArrayList<>(); + Country country = ((CountryBasedPaymentAccount) paymentAccount).getCountry(); + if (country != null) + acceptedCountryCodes.add(country.code); + } + return acceptedCountryCodes; + } + + public static ArrayList getAcceptedBanks(PaymentAccount paymentAccount) { + ArrayList acceptedBanks = null; + if (paymentAccount instanceof SpecificBanksAccount) { + acceptedBanks = new ArrayList<>(((SpecificBanksAccount) paymentAccount).getAcceptedBanks()); + } else if (paymentAccount instanceof SameBankAccount) { + acceptedBanks = new ArrayList<>(); + acceptedBanks.add(((SameBankAccount) paymentAccount).getBankId()); + } + return acceptedBanks; + } + + public static String getBankId(PaymentAccount paymentAccount) { + return paymentAccount instanceof BankAccount ? ((BankAccount) paymentAccount).getBankId() : null; + } + + // That is optional and set to null if not supported (AltCoins, OKPay,...) + public static String getCountryCode(PaymentAccount paymentAccount) { + if (paymentAccount instanceof CountryBasedPaymentAccount) { + Country country = ((CountryBasedPaymentAccount) paymentAccount).getCountry(); + return country != null ? country.code : null; + } else { + return null; + } + } + + public static Map getExtraDataMap(AccountAgeWitnessService accountAgeWitnessService, + ReferralIdService referralIdService, + PaymentAccount paymentAccount, + String currencyCode) { + Map extraDataMap = null; + if (CurrencyUtil.isFiatCurrency(currencyCode)) { + extraDataMap = new HashMap<>(); + final String myWitnessHashAsHex = accountAgeWitnessService.getMyWitnessHashAsHex(paymentAccount.getPaymentAccountPayload()); + extraDataMap.put(OfferPayload.ACCOUNT_AGE_WITNESS_HASH, myWitnessHashAsHex); + } + + if (referralIdService.getOptionalReferralId().isPresent()) { + if (extraDataMap == null) + extraDataMap = new HashMap<>(); + extraDataMap.put(OfferPayload.REFERRAL_ID, referralIdService.getOptionalReferralId().get()); + } + + if (paymentAccount instanceof F2FAccount) { + if (extraDataMap == null) + extraDataMap = new HashMap<>(); + extraDataMap.put(OfferPayload.F2F_CITY, ((F2FAccount) paymentAccount).getCity()); + extraDataMap.put(OfferPayload.F2F_EXTRA_INFO, ((F2FAccount) paymentAccount).getExtraInfo()); + } + + return extraDataMap; + } + + + public static void validateOfferData(FilterManager filterManager, + P2PService p2PService, + Coin buyerSecurityDepositAsCoin, + PaymentAccount paymentAccount, + String currencyCode, Coin makerFeeAsCoin) { + checkArgument(buyerSecurityDepositAsCoin.compareTo(Restrictions.getMaxBuyerSecurityDeposit()) <= 0, + "securityDeposit must be not exceed " + + Restrictions.getMaxBuyerSecurityDeposit().toFriendlyString()); + checkArgument(buyerSecurityDepositAsCoin.compareTo(Restrictions.getMinBuyerSecurityDeposit()) >= 0, + "securityDeposit must be not be less than " + + Restrictions.getMinBuyerSecurityDeposit().toFriendlyString()); + + checkArgument(!filterManager.isCurrencyBanned(currencyCode), + Res.get("offerbook.warning.currencyBanned")); + checkArgument(!filterManager.isPaymentMethodBanned(paymentAccount.getPaymentMethod()), + Res.get("offerbook.warning.paymentMethodBanned")); + checkNotNull(makerFeeAsCoin, "makerFee must not be null"); + checkNotNull(p2PService.getAddress(), "Address must not be null"); + } + + 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; + } } diff --git a/core/src/main/java/bisq/core/offer/TakerUtil.java b/core/src/main/java/bisq/core/offer/TakerUtil.java new file mode 100644 index 00000000000..6da4f7f9b1b --- /dev/null +++ b/core/src/main/java/bisq/core/offer/TakerUtil.java @@ -0,0 +1,71 @@ +/* + * 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 . + */ + +package bisq.core.offer; + +import bisq.core.app.BisqEnvironment; +import bisq.core.btc.wallet.BsqWalletService; +import bisq.core.provider.fee.FeeService; +import bisq.core.user.Preferences; +import bisq.core.util.CoinUtil; + +import org.bitcoinj.core.Coin; + +import javax.annotation.Nullable; + +public class TakerUtil { + public static Coin getFundsNeededForTakeOffer(Coin tradeAmount, Coin txFeeForDepositTx, Coin txFeeForPayoutTx, Offer offer) { + boolean buyOffer = OfferUtil.isBuyOffer(offer.getDirection()); + Coin needed = buyOffer ? offer.getSellerSecurityDeposit() : offer.getBuyerSecurityDeposit(); + + if (buyOffer) + needed = needed.add(tradeAmount); + + needed = needed.add(txFeeForDepositTx).add(txFeeForPayoutTx); + + return needed; + } + + @Nullable + public static Coin getTakerFee(Coin amount, Preferences preferences, BsqWalletService bsqWalletService) { + boolean currencyForTakerFeeBtc = isCurrencyForTakerFeeBtc(amount, preferences, bsqWalletService); + return getTakerFee(currencyForTakerFeeBtc, amount); + } + + @Nullable + public static Coin getTakerFee(boolean isCurrencyForTakerFeeBtc, Coin amount) { + if (amount != null) { + // TODO write unit test for that + Coin feePerBtc = CoinUtil.getFeePerBtc(FeeService.getTakerFeePerBtc(isCurrencyForTakerFeeBtc), amount); + return CoinUtil.maxCoin(feePerBtc, FeeService.getMinTakerFee(isCurrencyForTakerFeeBtc)); + } else { + return null; + } + } + + public static boolean isCurrencyForTakerFeeBtc(Coin amount, Preferences preferences, BsqWalletService bsqWalletService) { + return preferences.isPayFeeInBtc() || !isBsqForFeeAvailable(amount, bsqWalletService); + } + + public static boolean isBsqForFeeAvailable(Coin amount, BsqWalletService bsqWalletService) { + Coin takerFee = getTakerFee(false, amount); + return BisqEnvironment.isBaseCurrencySupportingBsq() && + takerFee != null && + bsqWalletService.getAvailableBalance() != null && + !bsqWalletService.getAvailableBalance().subtract(takerFee).isNegative(); + } +} diff --git a/core/src/main/java/bisq/core/offer/TxFeeEstimation.java b/core/src/main/java/bisq/core/offer/TxFeeEstimation.java new file mode 100644 index 00000000000..44323a22d66 --- /dev/null +++ b/core/src/main/java/bisq/core/offer/TxFeeEstimation.java @@ -0,0 +1,137 @@ +/* + * 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 . + */ + +package bisq.core.offer; + +import bisq.core.btc.wallet.BtcWalletService; +import bisq.core.provider.fee.FeeService; +import bisq.core.user.Preferences; + +import bisq.common.util.Tuple2; + +import org.bitcoinj.core.Coin; +import org.bitcoinj.core.InsufficientMoneyException; + +import com.google.common.annotations.VisibleForTesting; + +import java.util.List; + +import lombok.extern.slf4j.Slf4j; + +import static com.google.common.base.Preconditions.checkArgument; + +/** + * Util class for getting the estimated tx fee for maker or taker fee tx. + */ +@Slf4j +public class TxFeeEstimation { + private static int counter; + + public static Tuple2 getEstimatedFeeAndTxSizeForTaker(Coin reservedFundsForOffer, + Coin tradeFee, + FeeService feeService, + BtcWalletService btcWalletService, + Preferences preferences) { + Coin txFeePerByte = feeService.getTxFeePerByte(); + // We start with min taker fee size of 260 + int estimatedTxSize = 260; + try { + estimatedTxSize = getEstimatedTxSize(List.of(tradeFee, reservedFundsForOffer), estimatedTxSize, txFeePerByte, btcWalletService); + } catch (InsufficientMoneyException e) { + // if we cannot do the estimation we use the payout tx size + estimatedTxSize = 380; + log.info("We cannot do the fee estimation because there are not enough funds in the wallet. This is expected " + + "if the user pays from an external wallet. In that case we use an estimated tx size of {} bytes.", estimatedTxSize); + } + + if (!preferences.isPayFeeInBtc()) { + // If we pay the fee in BSQ we have one input more which adds about 150 bytes + estimatedTxSize += 150; + } + + int averageSize = (estimatedTxSize + 320) / 2; + // We use at least the size of the payout tx to not underpay at payout. + int minSize = Math.max(380, averageSize); + Coin txFee = txFeePerByte.multiply(minSize); + log.info("Fee estimation resulted in a tx size of {} bytes.\n" + + "We use an average between the taker fee tx and the deposit tx (320 bytes) which results in {} bytes.\n" + + "The payout tx has 380 bytes so we use that as our min value which is {} bytes.\n" + + "The tx fee of {}", estimatedTxSize, averageSize, minSize, txFee.toFriendlyString()); + return new Tuple2<>(txFee, minSize); + } + + public static Tuple2 getEstimatedFeeAndTxSizeForMaker(Coin reservedFundsForOffer, + Coin tradeFee, + FeeService feeService, + BtcWalletService btcWalletService, + Preferences preferences) { + Coin txFeePerByte = feeService.getTxFeePerByte(); + // We start with min maker fee size of 260 + int estimatedTxSize = 260; + try { + estimatedTxSize = getEstimatedTxSize(List.of(tradeFee, reservedFundsForOffer), estimatedTxSize, txFeePerByte, btcWalletService); + } catch (InsufficientMoneyException e) { + log.info("We cannot do the fee estimation because there are not enough funds in the wallet. This is expected " + + "if the user pays from an external wallet. In that case we use an estimated tx size of {} bytes.", estimatedTxSize); + } + + if (!preferences.isPayFeeInBtc()) { + // If we pay the fee in BSQ we have one input more which adds about 150 bytes + estimatedTxSize += 150; + } + + Coin txFee = txFeePerByte.multiply(estimatedTxSize); + log.info("Fee estimation resulted in a tx size of {} bytes and a tx fee of {}", estimatedTxSize, txFee.toFriendlyString()); + return new Tuple2<>(txFee, estimatedTxSize); + } + + @VisibleForTesting + static int getEstimatedTxSize(List outputValues, + int initialEstimatedTxSize, + Coin txFeePerByte, + BtcWalletService btcWalletService) + throws InsufficientMoneyException { + boolean isInTolerance; + int estimatedTxSize = initialEstimatedTxSize; + int realTxSize; + do { + Coin txFee = txFeePerByte.multiply(estimatedTxSize); + realTxSize = btcWalletService.getEstimatedFeeTxSize(outputValues, txFee); + isInTolerance = isInTolerance(estimatedTxSize, realTxSize, 0.2); + if (!isInTolerance) { + estimatedTxSize = realTxSize; + } + counter++; + } + while (!isInTolerance && counter < 10); + if (!isInTolerance) { + log.warn("We could not find a tx which satisfies our tolerance requirement of 20%. " + + "realTxSize={}, estimatedTxSize={}", + realTxSize, estimatedTxSize); + } + return estimatedTxSize; + } + + @VisibleForTesting + static boolean isInTolerance(int estimatedSize, int txSize, double tolerance) { + checkArgument(estimatedSize > 0, "estimatedSize must be positive"); + checkArgument(txSize > 0, "txSize must be positive"); + checkArgument(tolerance > 0, "tolerance must be positive"); + double deviation = Math.abs(1 - ((double) estimatedSize / (double) txSize)); + return deviation <= tolerance; + } +} diff --git a/core/src/main/java/bisq/core/offer/placeoffer/tasks/ValidateOffer.java b/core/src/main/java/bisq/core/offer/placeoffer/tasks/ValidateOffer.java index efe6040d369..8216c924f21 100644 --- a/core/src/main/java/bisq/core/offer/placeoffer/tasks/ValidateOffer.java +++ b/core/src/main/java/bisq/core/offer/placeoffer/tasks/ValidateOffer.java @@ -98,6 +98,7 @@ protected void run() { "maxTradePeriod must be positive. maxTradePeriod=" + offer.getMaxTradePeriod()); // TODO check upper and lower bounds for fiat // TODO check rest of new parameters + // TODO check for account age witness base tradeLimit is missing complete(); } catch (Exception e) { diff --git a/core/src/main/java/bisq/core/payment/AccountAgeWitnessService.java b/core/src/main/java/bisq/core/payment/AccountAgeWitnessService.java index 38cac92d482..77f299123cd 100644 --- a/core/src/main/java/bisq/core/payment/AccountAgeWitnessService.java +++ b/core/src/main/java/bisq/core/payment/AccountAgeWitnessService.java @@ -262,9 +262,14 @@ public long getMyAccountAge(PaymentAccountPayload paymentAccountPayload) { } public long getMyTradeLimit(PaymentAccount paymentAccount, String currencyCode) { - final Optional witnessOptional = Optional.of(getMyWitness(paymentAccount.getPaymentAccountPayload())); - return getTradeLimit(paymentAccount.getPaymentMethod() - .getMaxTradeLimitAsCoin(currencyCode), currencyCode, witnessOptional, new Date()); + if (paymentAccount == null) + return 0; + + Optional witnessOptional = Optional.of(getMyWitness(paymentAccount.getPaymentAccountPayload())); + return getTradeLimit(paymentAccount.getPaymentMethod().getMaxTradeLimitAsCoin(currencyCode), + currencyCode, + witnessOptional, + new Date()); } diff --git a/core/src/main/java/bisq/core/presentation/BalancePresentation.java b/core/src/main/java/bisq/core/presentation/BalancePresentation.java index 311a8570ea2..3b42d19ac6a 100644 --- a/core/src/main/java/bisq/core/presentation/BalancePresentation.java +++ b/core/src/main/java/bisq/core/presentation/BalancePresentation.java @@ -17,7 +17,7 @@ package bisq.core.presentation; -import bisq.core.btc.model.BalanceModel; +import bisq.core.btc.Balances; import bisq.core.util.BSFormatter; import javax.inject.Inject; @@ -26,7 +26,9 @@ import javafx.beans.property.StringProperty; import lombok.Getter; +import lombok.extern.slf4j.Slf4j; +@Slf4j public class BalancePresentation { @Getter private final StringProperty availableBalance = new SimpleStringProperty(); @@ -36,8 +38,8 @@ public class BalancePresentation { private final StringProperty lockedBalance = new SimpleStringProperty(); @Inject - public BalancePresentation(BalanceModel balanceModel, BSFormatter formatter) { - balanceModel.getAvailableBalance().addListener((observable, oldValue, newValue) -> { + public BalancePresentation(Balances balances, BSFormatter formatter) { + balances.getAvailableBalance().addListener((observable, oldValue, newValue) -> { String value = formatter.formatCoinWithCode(newValue); // If we get full precision the BTC postfix breaks layout so we omit it if (value.length() > 11) @@ -45,10 +47,10 @@ public BalancePresentation(BalanceModel balanceModel, BSFormatter formatter) { availableBalance.set(value); }); - balanceModel.getReservedBalance().addListener((observable, oldValue, newValue) -> { + balances.getReservedBalance().addListener((observable, oldValue, newValue) -> { reservedBalance.set(formatter.formatCoinWithCode(newValue)); }); - balanceModel.getLockedBalance().addListener((observable, oldValue, newValue) -> { + balances.getLockedBalance().addListener((observable, oldValue, newValue) -> { lockedBalance.set(formatter.formatCoinWithCode(newValue)); }); } diff --git a/core/src/main/java/bisq/core/trade/TradeManager.java b/core/src/main/java/bisq/core/trade/TradeManager.java index e4cb74d467b..def150cb681 100644 --- a/core/src/main/java/bisq/core/trade/TradeManager.java +++ b/core/src/main/java/bisq/core/trade/TradeManager.java @@ -239,6 +239,13 @@ public void onUpdatedDataReceived() { tradableList.getList().addListener((ListChangeListener) change -> onTradesChanged()); onTradesChanged(); + + getAddressEntriesForAvailableBalanceStream() + .filter(addressEntry -> addressEntry.getOfferId() != null) + .forEach(addressEntry -> { + log.debug("swapPendingOfferFundingEntries, offerId={}, OFFER_FUNDING", addressEntry.getOfferId()); + btcWalletService.swapTradeEntryToAvailableEntry(addressEntry.getOfferId(), AddressEntry.Context.OFFER_FUNDING); + }); } public void shutDown() { diff --git a/core/src/main/java/bisq/core/user/Preferences.java b/core/src/main/java/bisq/core/user/Preferences.java index def763d6f1a..5a0b91b55ff 100644 --- a/core/src/main/java/bisq/core/user/Preferences.java +++ b/core/src/main/java/bisq/core/user/Preferences.java @@ -705,10 +705,6 @@ public Coin getBuyerSecurityDepositAsCoin() { return Coin.valueOf(prefPayload.getBuyerSecurityDepositAsLong()); } - public boolean getPayFeeInBtc() { - return prefPayload.isPayFeeInBtc(); - } - @Override @Nullable public List getBridgeAddresses() { diff --git a/core/src/test/java/bisq/core/offer/TxFeeEstimationTest.java b/core/src/test/java/bisq/core/offer/TxFeeEstimationTest.java new file mode 100644 index 00000000000..b9d4db6d2eb --- /dev/null +++ b/core/src/test/java/bisq/core/offer/TxFeeEstimationTest.java @@ -0,0 +1,124 @@ +/* + * 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 . + */ + +package bisq.core.offer; + +import bisq.core.btc.wallet.BtcWalletService; + +import org.bitcoinj.core.Coin; +import org.bitcoinj.core.InsufficientMoneyException; + +import java.util.List; + +import org.powermock.core.classloader.annotations.PowerMockIgnore; +import org.powermock.core.classloader.annotations.PrepareForTest; +import org.powermock.modules.junit4.PowerMockRunner; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +@RunWith(PowerMockRunner.class) +@PrepareForTest(BtcWalletService.class) +@PowerMockIgnore({"com.sun.org.apache.xerces.*", "javax.xml.*", "org.xml.*"}) +public class TxFeeEstimationTest { + + @Test + public void testGetEstimatedTxSize() throws InsufficientMoneyException { + List outputValues = List.of(Coin.valueOf(2000), Coin.valueOf(3000)); + int initialEstimatedTxSize; + Coin txFeePerByte; + BtcWalletService btcWalletService = mock(BtcWalletService.class); + int result; + int realTxSize; + Coin txFee; + + + initialEstimatedTxSize = 260; + txFeePerByte = Coin.valueOf(10); + realTxSize = 260; + + txFee = txFeePerByte.multiply(initialEstimatedTxSize); + when(btcWalletService.getEstimatedFeeTxSize(outputValues, txFee)).thenReturn(realTxSize); + result = TxFeeEstimation.getEstimatedTxSize(outputValues, initialEstimatedTxSize, txFeePerByte, btcWalletService); + assertEquals(260, result); + + + // TODO check how to use the mocking framework for repeated calls + // The btcWalletService.getEstimatedFeeTxSize returns 0 at repeated calls in the while loop.... + /* initialEstimatedTxSize = 260; + txFeePerByte = Coin.valueOf(10); + realTxSize = 2600; + + txFee = txFeePerByte.multiply(initialEstimatedTxSize); + when(btcWalletService.getEstimatedFeeTxSize(outputValues, txFee)).thenReturn(realTxSize); + result = TxFeeEstimation.getEstimatedTxSize(outputValues, initialEstimatedTxSize, txFeePerByte, btcWalletService); + assertEquals(2600, result); + + initialEstimatedTxSize = 2600; + txFeePerByte = Coin.valueOf(10); + realTxSize = 260; + + txFee = txFeePerByte.multiply(initialEstimatedTxSize); + when(btcWalletService.getEstimatedFeeTxSize(outputValues, txFee)).thenReturn(realTxSize); + result = TxFeeEstimation.getEstimatedTxSize(outputValues, initialEstimatedTxSize, txFeePerByte, btcWalletService); + assertEquals(260, result);*/ + } + + @Test + public void testIsInTolerance() { + int estimatedSize; + int txSize; + double tolerance; + boolean result; + + estimatedSize = 100; + txSize = 100; + tolerance = 0.0001; + result = TxFeeEstimation.isInTolerance(estimatedSize, txSize, tolerance); + assertTrue(result); + + estimatedSize = 100; + txSize = 200; + tolerance = 0.2; + result = TxFeeEstimation.isInTolerance(estimatedSize, txSize, tolerance); + assertFalse(result); + + estimatedSize = 120; + txSize = 100; + tolerance = 0.2; + result = TxFeeEstimation.isInTolerance(estimatedSize, txSize, tolerance); + assertTrue(result); + + estimatedSize = 200; + txSize = 100; + tolerance = 1; + result = TxFeeEstimation.isInTolerance(estimatedSize, txSize, tolerance); + assertTrue(result); + + estimatedSize = 201; + txSize = 100; + tolerance = 1; + result = TxFeeEstimation.isInTolerance(estimatedSize, txSize, tolerance); + assertFalse(result); + } +} diff --git a/desktop/src/main/java/bisq/desktop/app/BisqAppMain.java b/desktop/src/main/java/bisq/desktop/app/BisqAppMain.java index 1d519806758..d81cafb0a59 100644 --- a/desktop/src/main/java/bisq/desktop/app/BisqAppMain.java +++ b/desktop/src/main/java/bisq/desktop/app/BisqAppMain.java @@ -54,6 +54,10 @@ public static void main(String[] args) throws Exception { } } + @Override + public void onSetupComplete() { + log.info("onSetupComplete"); + } /////////////////////////////////////////////////////////////////////////////////////////// // First synchronous execution tasks diff --git a/desktop/src/main/java/bisq/desktop/main/funds/withdrawal/WithdrawalView.java b/desktop/src/main/java/bisq/desktop/main/funds/withdrawal/WithdrawalView.java index f1f9b3b4ad8..ad6f7e9946b 100644 --- a/desktop/src/main/java/bisq/desktop/main/funds/withdrawal/WithdrawalView.java +++ b/desktop/src/main/java/bisq/desktop/main/funds/withdrawal/WithdrawalView.java @@ -29,8 +29,8 @@ import bisq.core.btc.exceptions.AddressEntryException; import bisq.core.btc.exceptions.InsufficientFundsException; import bisq.core.btc.listeners.BalanceListener; -import bisq.core.btc.setup.WalletsSetup; import bisq.core.btc.model.AddressEntry; +import bisq.core.btc.setup.WalletsSetup; import bisq.core.btc.wallet.BtcWalletService; import bisq.core.btc.wallet.Restrictions; import bisq.core.locale.Res; diff --git a/desktop/src/main/java/bisq/desktop/main/offer/MutableOfferDataModel.java b/desktop/src/main/java/bisq/desktop/main/offer/MutableOfferDataModel.java index 6f02681699d..9457bad0c2c 100644 --- a/desktop/src/main/java/bisq/desktop/main/offer/MutableOfferDataModel.java +++ b/desktop/src/main/java/bisq/desktop/main/offer/MutableOfferDataModel.java @@ -25,7 +25,6 @@ import bisq.core.btc.wallet.BsqWalletService; import bisq.core.btc.wallet.BtcWalletService; import bisq.core.btc.wallet.Restrictions; -import bisq.core.btc.wallet.TradeWalletService; import bisq.core.filter.FilterManager; import bisq.core.locale.CurrencyUtil; import bisq.core.locale.Res; @@ -36,16 +35,10 @@ import bisq.core.offer.OfferPayload; import bisq.core.offer.OfferUtil; import bisq.core.offer.OpenOfferManager; +import bisq.core.offer.TxFeeEstimation; import bisq.core.payment.AccountAgeWitnessService; -import bisq.core.payment.BankAccount; -import bisq.core.payment.CountryBasedPaymentAccount; -import bisq.core.payment.F2FAccount; import bisq.core.payment.HalCashAccount; import bisq.core.payment.PaymentAccount; -import bisq.core.payment.SameBankAccount; -import bisq.core.payment.SepaAccount; -import bisq.core.payment.SepaInstantAccount; -import bisq.core.payment.SpecificBanksAccount; import bisq.core.provider.fee.FeeService; import bisq.core.provider.price.PriceFeedService; import bisq.core.trade.handlers.TransactionResultHandler; @@ -58,11 +51,10 @@ import bisq.common.app.Version; import bisq.common.crypto.KeyRing; +import bisq.common.util.Tuple2; import bisq.common.util.Utilities; -import org.bitcoinj.core.Address; import org.bitcoinj.core.Coin; -import org.bitcoinj.core.InsufficientMoneyException; import org.bitcoinj.core.Transaction; import com.google.inject.Inject; @@ -85,7 +77,6 @@ import java.util.ArrayList; import java.util.Date; -import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; @@ -106,7 +97,6 @@ public abstract class MutableOfferDataModel extends OfferDataModel implements Bs final String shortOfferId; private final FilterManager filterManager; private final AccountAgeWitnessService accountAgeWitnessService; - private final TradeWalletService tradeWalletService; private final FeeService feeService; private final ReferralIdService referralIdService; private final BSFormatter formatter; @@ -138,7 +128,6 @@ public abstract class MutableOfferDataModel extends OfferDataModel implements Bs protected Coin txFeeFromFeeService; protected boolean marketPriceAvailable; protected int feeTxSize = 260; // size of typical tx with 1 input - protected int feeTxSizeEstimationRecursionCounter; protected boolean allowAmountUpdate = true; @@ -150,8 +139,9 @@ public abstract class MutableOfferDataModel extends OfferDataModel implements Bs public MutableOfferDataModel(OpenOfferManager openOfferManager, BtcWalletService btcWalletService, BsqWalletService bsqWalletService, Preferences preferences, User user, KeyRing keyRing, P2PService p2PService, PriceFeedService priceFeedService, FilterManager filterManager, - AccountAgeWitnessService accountAgeWitnessService, TradeWalletService tradeWalletService, - FeeService feeService, ReferralIdService referralIdService, BSFormatter formatter) { + AccountAgeWitnessService accountAgeWitnessService, FeeService feeService, + ReferralIdService referralIdService, + BSFormatter formatter) { super(btcWalletService); this.openOfferManager = openOfferManager; @@ -163,7 +153,6 @@ public MutableOfferDataModel(OpenOfferManager openOfferManager, BtcWalletService this.priceFeedService = priceFeedService; this.filterManager = filterManager; this.accountAgeWitnessService = accountAgeWitnessService; - this.tradeWalletService = tradeWalletService; this.feeService = feeService; this.referralIdService = referralIdService; this.formatter = formatter; @@ -321,30 +310,14 @@ Offer createAndGetOffer() { long amount = this.amount.get() != null ? this.amount.get().getValue() : 0L; long minAmount = this.minAmount.get() != null ? this.minAmount.get().getValue() : 0L; - ArrayList acceptedCountryCodes = null; - if (paymentAccount instanceof SepaAccount) { - acceptedCountryCodes = new ArrayList<>(); - acceptedCountryCodes.addAll(((SepaAccount) paymentAccount).getAcceptedCountryCodes()); - } else if (paymentAccount instanceof SepaInstantAccount) { - acceptedCountryCodes = new ArrayList<>(); - acceptedCountryCodes.addAll(((SepaInstantAccount) paymentAccount).getAcceptedCountryCodes()); - } else if (paymentAccount instanceof CountryBasedPaymentAccount) { - acceptedCountryCodes = new ArrayList<>(); - acceptedCountryCodes.add(((CountryBasedPaymentAccount) paymentAccount).getCountry().code); - } + ArrayList acceptedCountryCodes = OfferUtil.getAcceptedCountryCodes(paymentAccount); - ArrayList acceptedBanks = null; - if (paymentAccount instanceof SpecificBanksAccount) { - acceptedBanks = new ArrayList<>(((SpecificBanksAccount) paymentAccount).getAcceptedBanks()); - } else if (paymentAccount instanceof SameBankAccount) { - acceptedBanks = new ArrayList<>(); - acceptedBanks.add(((SameBankAccount) paymentAccount).getBankId()); - } + ArrayList acceptedBanks = OfferUtil.getAcceptedBanks(paymentAccount); - String bankId = paymentAccount instanceof BankAccount ? ((BankAccount) paymentAccount).getBankId() : null; + String bankId = OfferUtil.getBankId(paymentAccount); // That is optional and set to null if not supported (AltCoins, OKPay,...) - String countryCode = paymentAccount instanceof CountryBasedPaymentAccount ? ((CountryBasedPaymentAccount) paymentAccount).getCountry().code : null; + String countryCode = OfferUtil.getCountryCode(paymentAccount); checkNotNull(p2PService.getAddress(), "Address must not be null"); checkNotNull(getMakerFee(), "makerFee must not be null"); @@ -360,25 +333,8 @@ Offer createAndGetOffer() { long lowerClosePrice = 0; long upperClosePrice = 0; String hashOfChallenge = null; - Map extraDataMap = null; - if (CurrencyUtil.isFiatCurrency(currencyCode)) { - extraDataMap = new HashMap<>(); - final String myWitnessHashAsHex = accountAgeWitnessService.getMyWitnessHashAsHex(paymentAccount.getPaymentAccountPayload()); - extraDataMap.put(OfferPayload.ACCOUNT_AGE_WITNESS_HASH, myWitnessHashAsHex); - } - - if (referralIdService.getOptionalReferralId().isPresent()) { - if (extraDataMap == null) - extraDataMap = new HashMap<>(); - extraDataMap.put(OfferPayload.REFERRAL_ID, referralIdService.getOptionalReferralId().get()); - } - if (paymentAccount instanceof F2FAccount) { - if (extraDataMap == null) - extraDataMap = new HashMap<>(); - extraDataMap.put(OfferPayload.F2F_CITY, ((F2FAccount) paymentAccount).getCity()); - extraDataMap.put(OfferPayload.F2F_EXTRA_INFO, ((F2FAccount) paymentAccount).getExtraInfo()); - } + Map extraDataMap = OfferUtil.getExtraDataMap(accountAgeWitnessService, referralIdService, paymentAccount, currencyCode); Coin buyerSecurityDepositAsCoin = buyerSecurityDeposit.get(); checkArgument(buyerSecurityDepositAsCoin.compareTo(Restrictions.getMaxBuyerSecurityDeposit()) <= 0, @@ -438,57 +394,14 @@ Offer createAndGetOffer() { // This works only if we have already funds in the wallet public void estimateTxSize() { - txFeeFromFeeService = feeService.getTxFee(feeTxSize); - Address fundingAddress = btcWalletService.getFreshAddressEntry().getAddress(); - Address reservedForTradeAddress = btcWalletService.getOrCreateAddressEntry(offerId, AddressEntry.Context.RESERVED_FOR_TRADE).getAddress(); - Address changeAddress = btcWalletService.getFreshAddressEntry().getAddress(); - - Coin reservedFundsForOffer = getSecurityDeposit(); - if (!isBuyOffer()) - reservedFundsForOffer = reservedFundsForOffer.add(amount.get()); - - checkNotNull(user.getAcceptedArbitrators(), "user.getAcceptedArbitrators() must not be null"); - checkArgument(!user.getAcceptedArbitrators().isEmpty(), "user.getAcceptedArbitrators() must not be empty"); - String dummyArbitratorAddress = user.getAcceptedArbitrators().get(0).getBtcAddress(); - try { - log.info("We create a dummy tx to see if our estimated size is in the accepted range. feeTxSize={}," + - " txFee based on feeTxSize: {}, recommended txFee is {} sat/byte", - feeTxSize, txFeeFromFeeService.toFriendlyString(), feeService.getTxFeePerByte()); - Transaction tradeFeeTx = tradeWalletService.estimateBtcTradingFeeTxSize( - fundingAddress, - reservedForTradeAddress, - changeAddress, - reservedFundsForOffer, - true, - getMakerFee(), - txFeeFromFeeService, - dummyArbitratorAddress); - - final int txSize = tradeFeeTx.bitcoinSerialize().length; - // use feeTxSizeEstimationRecursionCounter to avoid risk for endless loop - if (txSize > feeTxSize * 1.2 && feeTxSizeEstimationRecursionCounter < 10) { - feeTxSizeEstimationRecursionCounter++; - log.info("txSize is {} bytes but feeTxSize used for txFee calculation was {} bytes. We try again with an " + - "adjusted txFee to reach the target tx fee.", txSize, feeTxSize); - feeTxSize = txSize; - txFeeFromFeeService = feeService.getTxFee(feeTxSize); - // lets try again with the adjusted txSize and fee. - estimateTxSize(); - } else { - log.info("feeTxSize {} bytes", feeTxSize); - log.info("txFee based on estimated size: {}, recommended txFee is {} sat/byte", - txFeeFromFeeService.toFriendlyString(), feeService.getTxFeePerByte()); - } - } catch (InsufficientMoneyException e) { - // If we need to fund from an external wallet we can assume we only have 1 input (260 bytes). - log.warn("We cannot do the fee estimation because there are not enough funds in the wallet. This is expected " + - "if the user pays from an external wallet. In that case we use an estimated tx size of 260 bytes."); - feeTxSize = 260; - txFeeFromFeeService = feeService.getTxFee(feeTxSize); - log.info("feeTxSize {} bytes", feeTxSize); - log.info("txFee based on estimated size: {}, recommended txFee is {} sat/byte", - txFeeFromFeeService.toFriendlyString(), feeService.getTxFeePerByte()); - } + Coin fundsNeededForMaker = OfferUtil.getFundsNeededForOffer(amount.get(), buyerSecurityDeposit.get(), direction); + Tuple2 estimatedFeeAndTxSize = TxFeeEstimation.getEstimatedFeeAndTxSizeForMaker(fundsNeededForMaker, + getMakerFee(), + feeService, + btcWalletService, + preferences); + txFeeFromFeeService = estimatedFeeAndTxSize.first; + feeTxSize = estimatedFeeAndTxSize.second; } void onPlaceOffer(Offer offer, TransactionResultHandler resultHandler) { @@ -647,7 +560,7 @@ public double getMarketPriceMargin() { } boolean isMakerFeeValid() { - return preferences.getPayFeeInBtc() || isBsqForFeeAvailable(); + return preferences.isPayFeeInBtc() || isBsqForFeeAvailable(); } long getMaxTradeLimit() { @@ -727,7 +640,7 @@ Coin getSecurityDeposit() { } public boolean isBuyOffer() { - return OfferUtil.isBuyOffer(getDirection()); + return OfferUtil.isBuyOffer(direction); } public Coin getTxFee() { diff --git a/desktop/src/main/java/bisq/desktop/main/offer/createoffer/CreateOfferDataModel.java b/desktop/src/main/java/bisq/desktop/main/offer/createoffer/CreateOfferDataModel.java index e2250863431..cd9562515b0 100644 --- a/desktop/src/main/java/bisq/desktop/main/offer/createoffer/CreateOfferDataModel.java +++ b/desktop/src/main/java/bisq/desktop/main/offer/createoffer/CreateOfferDataModel.java @@ -3,16 +3,20 @@ * * 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 + * 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 + * 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 . + * along with Bisq. If not, +see . */ package bisq.desktop.main.offer.createoffer; @@ -21,7 +25,6 @@ import bisq.core.btc.wallet.BsqWalletService; import bisq.core.btc.wallet.BtcWalletService; -import bisq.core.btc.wallet.TradeWalletService; import bisq.core.filter.FilterManager; import bisq.core.offer.OpenOfferManager; import bisq.core.payment.AccountAgeWitnessService; @@ -46,7 +49,31 @@ class CreateOfferDataModel extends MutableOfferDataModel { @Inject - public CreateOfferDataModel(OpenOfferManager openOfferManager, BtcWalletService btcWalletService, BsqWalletService bsqWalletService, Preferences preferences, User user, KeyRing keyRing, P2PService p2PService, PriceFeedService priceFeedService, FilterManager filterManager, AccountAgeWitnessService accountAgeWitnessService, TradeWalletService tradeWalletService, FeeService feeService, ReferralIdService referralIdService, BSFormatter formatter) { - super(openOfferManager, btcWalletService, bsqWalletService, preferences, user, keyRing, p2PService, priceFeedService, filterManager, accountAgeWitnessService, tradeWalletService, feeService, referralIdService, formatter); + public CreateOfferDataModel(OpenOfferManager openOfferManager, + BtcWalletService btcWalletService, + BsqWalletService bsqWalletService, + Preferences preferences, + User user, + KeyRing keyRing, + P2PService p2PService, + PriceFeedService priceFeedService, + FilterManager filterManager, + AccountAgeWitnessService accountAgeWitnessService, + FeeService feeService, + ReferralIdService referralIdService, + BSFormatter formatter) { + super(openOfferManager, + btcWalletService, + bsqWalletService, + preferences, + user, + keyRing, + p2PService, + priceFeedService, + filterManager, + accountAgeWitnessService, + feeService, + referralIdService, + formatter); } } diff --git a/desktop/src/main/java/bisq/desktop/main/offer/takeoffer/TakeOfferDataModel.java b/desktop/src/main/java/bisq/desktop/main/offer/takeoffer/TakeOfferDataModel.java index 1e5bccee9b1..db4894b4c54 100644 --- a/desktop/src/main/java/bisq/desktop/main/offer/takeoffer/TakeOfferDataModel.java +++ b/desktop/src/main/java/bisq/desktop/main/offer/takeoffer/TakeOfferDataModel.java @@ -27,7 +27,6 @@ import bisq.core.btc.wallet.BsqWalletService; import bisq.core.btc.wallet.BtcWalletService; import bisq.core.btc.wallet.Restrictions; -import bisq.core.btc.wallet.TradeWalletService; import bisq.core.filter.FilterManager; import bisq.core.locale.CurrencyUtil; import bisq.core.locale.Res; @@ -36,6 +35,8 @@ import bisq.core.offer.Offer; import bisq.core.offer.OfferPayload; import bisq.core.offer.OfferUtil; +import bisq.core.offer.TakerUtil; +import bisq.core.offer.TxFeeEstimation; import bisq.core.payment.AccountAgeWitnessService; import bisq.core.payment.HalCashAccount; import bisq.core.payment.PaymentAccount; @@ -49,9 +50,9 @@ import bisq.core.user.User; import bisq.core.util.CoinUtil; -import org.bitcoinj.core.Address; +import bisq.common.util.Tuple2; + import org.bitcoinj.core.Coin; -import org.bitcoinj.core.InsufficientMoneyException; import org.bitcoinj.core.Transaction; import org.bitcoinj.wallet.Wallet; @@ -65,6 +66,8 @@ import java.util.List; +import org.jetbrains.annotations.NotNull; + import javax.annotation.Nullable; import static com.google.common.base.Preconditions.checkArgument; @@ -83,12 +86,10 @@ class TakeOfferDataModel extends OfferDataModel { private final FilterManager filterManager; private final Preferences preferences; private final PriceFeedService priceFeedService; - private final TradeWalletService tradeWalletService; private final AccountAgeWitnessService accountAgeWitnessService; private Coin txFeeFromFeeService; private Coin securityDeposit; - // Coin feeFromFundingTx = Coin.NEGATIVE_SATOSHI; private Offer offer; @@ -101,9 +102,8 @@ class TakeOfferDataModel extends OfferDataModel { private PaymentAccount paymentAccount; private boolean isTabSelected; Price tradePrice; - // 260 kb is size of typical trade fee tx with 1 input but trade tx (deposit and payout) are larger so we adjust to 320 - private int feeTxSize = 320; - private int feeTxSizeEstimationRecursionCounter; + // We take payout tx as the default value if we cannot do the fee estimation + private int feeTxSize = 380; private boolean freezeFee; private Coin txFeePerByteFromFeeService; @@ -117,7 +117,7 @@ class TakeOfferDataModel extends OfferDataModel { TakeOfferDataModel(TradeManager tradeManager, BtcWalletService btcWalletService, BsqWalletService bsqWalletService, User user, FeeService feeService, FilterManager filterManager, - Preferences preferences, PriceFeedService priceFeedService, TradeWalletService tradeWalletService, + Preferences preferences, PriceFeedService priceFeedService, AccountAgeWitnessService accountAgeWitnessService) { super(btcWalletService); @@ -128,10 +128,7 @@ class TakeOfferDataModel extends OfferDataModel { this.filterManager = filterManager; this.preferences = preferences; this.priceFeedService = priceFeedService; - this.tradeWalletService = tradeWalletService; this.accountAgeWitnessService = accountAgeWitnessService; - - // isMainNet.set(preferences.getBaseCryptoNetwork() == BitcoinNetwork.BTC_MAINNET); } @Override @@ -194,7 +191,7 @@ void initWithData(Offer offer) { // and batched txs would add more complexity to the trade protocol. // A typical trade fee tx has about 260 bytes (if one input). The trade txs has about 336-414 bytes. - // We use 320 as a average value. + // We use 380 as a average value. // trade fee tx: 260 bytes (1 input) // deposit tx: 336 bytes (1 MS output+ OP_RETURN) - 414 bytes (1 MS output + OP_RETURN + change in case of smaller trade amount) @@ -204,14 +201,15 @@ void initWithData(Offer offer) { // Set the default values (in rare cases if the fee request was not done yet we get the hard coded default values) // But the "take offer" happens usually after that so we should have already the value from the estimation service. txFeePerByteFromFeeService = feeService.getTxFeePerByte(); - txFeeFromFeeService = getTxFeeBySize(feeTxSize); + txFeeFromFeeService = txFeePerByteFromFeeService.multiply(feeTxSize); // We request to get the actual estimated fee log.info("Start requestTxFee: txFeeFromFeeService={}", txFeeFromFeeService); feeService.requestFees(() -> { if (!freezeFee) { txFeePerByteFromFeeService = feeService.getTxFeePerByte(); - txFeeFromFeeService = getTxFeeBySize(feeTxSize); + txFeeFromFeeService = txFeePerByteFromFeeService.multiply(feeTxSize); + calculateTotalToPay(); log.info("Completed requestTxFee: txFeeFromFeeService={}", txFeeFromFeeService); } else { @@ -287,7 +285,7 @@ void onTakeOffer(TradeResultHandler tradeResultHandler) { checkNotNull(txFeeFromFeeService, "txFeeFromFeeService must not be null"); checkNotNull(getTakerFee(), "takerFee must not be null"); - Coin fundsNeededForTrade = getSecurityDeposit().add(txFeeFromFeeService).add(txFeeFromFeeService); + Coin fundsNeededForTrade = getFundsNeededForTrade(); if (isBuyOffer()) fundsNeededForTrade = fundsNeededForTrade.add(amount.get()); @@ -329,83 +327,45 @@ void onTakeOffer(TradeResultHandler tradeResultHandler) { // and if funds get higher (if tx get larger) the user would get confused (adding small inputs would increase total required funds). // So that would require more thoughts how to deal with all those cases. public void estimateTxSize() { - Address fundingAddress = btcWalletService.getFreshAddressEntry().getAddress(); int txSize = 0; if (btcWalletService.getBalance(Wallet.BalanceType.AVAILABLE).isPositive()) { - txFeeFromFeeService = getTxFeeBySize(feeTxSize); - - Address reservedForTradeAddress = btcWalletService.getOrCreateAddressEntry(offer.getId(), AddressEntry.Context.RESERVED_FOR_TRADE).getAddress(); - Address changeAddress = btcWalletService.getFreshAddressEntry().getAddress(); - - Coin reservedFundsForOffer = getSecurityDeposit().add(txFeeFromFeeService).add(txFeeFromFeeService); - if (isBuyOffer()) - reservedFundsForOffer = reservedFundsForOffer.add(amount.get()); - - checkNotNull(user.getAcceptedArbitrators(), "user.getAcceptedArbitrators() must not be null"); - checkArgument(!user.getAcceptedArbitrators().isEmpty(), "user.getAcceptedArbitrators() must not be empty"); - String dummyArbitratorAddress = user.getAcceptedArbitrators().get(0).getBtcAddress(); - try { - log.debug("We create a dummy tx to see if our estimated size is in the accepted range. feeTxSize={}," + - " txFee based on feeTxSize: {}, recommended txFee is {} sat/byte", - feeTxSize, txFeeFromFeeService.toFriendlyString(), feeService.getTxFeePerByte()); - Transaction tradeFeeTx = tradeWalletService.estimateBtcTradingFeeTxSize( - fundingAddress, - reservedForTradeAddress, - changeAddress, - reservedFundsForOffer, - true, - getTakerFee(), - txFeeFromFeeService, - dummyArbitratorAddress); - - txSize = tradeFeeTx.bitcoinSerialize().length; - // use feeTxSizeEstimationRecursionCounter to avoid risk for endless loop - // We use the tx size for the trade fee tx as target for the fees. - // The deposit and payout txs are determined +/- 1 output but the trade fee tx can have either 1 or many inputs - // so we need to make sure the trade fee tx gets the correct fee to not get stuck. - // We use a 20% tolerance frm out default 320 byte size (typical for deposit and payout) and only if we get a - // larger size we increase the fee. Worst case is that we overpay for the other follow up txs, but better than - // use a too low fee and get stuck. - if (txSize > feeTxSize * 1.2 && feeTxSizeEstimationRecursionCounter < 10) { - feeTxSizeEstimationRecursionCounter++; - log.info("txSize is {} bytes but feeTxSize used for txFee calculation was {} bytes. We try again with an " + - "adjusted txFee to reach the target tx fee.", txSize, feeTxSize); - - feeTxSize = txSize; - txFeeFromFeeService = getTxFeeBySize(txSize); - - // lets try again with the adjusted txSize and fee. - estimateTxSize(); - } else { - // We are done with estimation iterations - if (feeTxSizeEstimationRecursionCounter < 10) - log.info("Fee estimation completed:\n" + - "txFee based on estimated size of {} bytes. Average tx size = {} bytes. Actual tx size = {} bytes. TxFee is {} ({} sat/byte)", - feeTxSize, getAverageSize(feeTxSize), txSize, txFeeFromFeeService.toFriendlyString(), feeService.getTxFeePerByte()); - else - log.warn("We could not estimate the fee as the feeTxSizeEstimationRecursionCounter exceeded our limit of 10 recursions.\n" + - "txFee based on estimated size of {} bytes. Average tx size = {} bytes. Actual tx size = {} bytes. " + - "TxFee is {} ({} sat/byte)", - feeTxSize, getAverageSize(feeTxSize), txSize, txFeeFromFeeService.toFriendlyString(), feeService.getTxFeePerByte()); - } - } catch (InsufficientMoneyException e) { - log.info("We cannot complete the fee estimation because there are not enough funds in the wallet.\n" + - "This is expected if the user has not sufficient funds yet.\n" + - "In that case we use the latest estimated tx size or the default if none has been calculated yet.\n" + - "txFee based on estimated size of {} bytes. Average tx size = {} bytes. Actual tx size = {} bytes. TxFee is {} ({} sat/byte)", - feeTxSize, getAverageSize(feeTxSize), txSize, txFeeFromFeeService.toFriendlyString(), feeService.getTxFeePerByte()); - } + Coin fundsNeededForTaker = TakerUtil.getFundsNeededForTakeOffer(amount.get(), getTxFeeForDepositTx(), getTxFeeForPayoutTx(), offer); + + // As taker we pay 3 times the fee and currently the fee is the same for all 3 txs (trade fee tx, deposit + // tx and payout tx). + // We should try to change that in future to have the deposit and payout tx with a fixed fee as the size is + // there more deterministic. + // The trade fee tx can be in the worst case very large if there are many inputs so if we take that tx alone + // for the fee estimation we would overpay a lot. + // On the other side if we have the best case of a 1 input tx fee tx then it is only 260 bytes but the + // other 2 txs are larger (320 and 380 bytes) and would get a lower fee/byte as intended. + // We apply following model to not overpay too much but be on the safe side as well. + // We sum the taker fee tx and the deposit tx together as it can be assumed that both be in the same block and + // as they are dependent txs the miner will pick both if the fee in total is good enough. + // We make sure that the fee is sufficient to meet our intended fee/byte for the larger payout tx with 380 bytes. + Tuple2 estimatedFeeAndTxSize = TxFeeEstimation.getEstimatedFeeAndTxSizeForTaker(fundsNeededForTaker, + getTakerFee(), + feeService, + btcWalletService, + preferences); + txFeeFromFeeService = estimatedFeeAndTxSize.first; + feeTxSize = estimatedFeeAndTxSize.second; } else { - feeTxSize = 320; - txFeeFromFeeService = getTxFeeBySize(feeTxSize); + feeTxSize = 380; + txFeeFromFeeService = txFeePerByteFromFeeService.multiply(feeTxSize); log.info("We cannot do the fee estimation because there are no funds in the wallet.\nThis is expected " + "if the user has not funded his wallet yet.\n" + - "In that case we use an estimated tx size of 320 bytes.\n" + - "txFee based on estimated size of {} bytes. Average tx size = {} bytes. Actual tx size = {} bytes. TxFee is {} ({} sat/byte)", - feeTxSize, getAverageSize(feeTxSize), txSize, txFeeFromFeeService.toFriendlyString(), feeService.getTxFeePerByte()); + "In that case we use an estimated tx size of 380 bytes.\n" + + "txFee based on estimated size of {} bytes. feeTxSize = {} bytes. Actual tx size = {} bytes. TxFee is {} ({} sat/byte)", + feeTxSize, feeTxSize, txSize, txFeeFromFeeService.toFriendlyString(), feeService.getTxFeePerByte()); } } + @NotNull + private Coin getFundsNeededForTrade() { + return getSecurityDeposit().add(getTxFeeForDepositTx()).add(getTxFeeForPayoutTx()); + } + public void onPaymentAccountSelected(PaymentAccount paymentAccount) { if (paymentAccount != null) { this.paymentAccount = paymentAccount; @@ -450,14 +410,19 @@ boolean hasAcceptedArbitrators() { } boolean isCurrencyForTakerFeeBtc() { - return preferences.getPayFeeInBtc() || !isBsqForFeeAvailable(); + // TODO do more testing before applying TakerUtil + //return TakerUtil.isCurrencyForTakerFeeBtc(amount.get(), preferences, bsqWalletService); + return preferences.isPayFeeInBtc() || !isBsqForFeeAvailable(); } boolean isTakerFeeValid() { - return preferences.getPayFeeInBtc() || isBsqForFeeAvailable(); + return preferences.isPayFeeInBtc() || isBsqForFeeAvailable(); } boolean isBsqForFeeAvailable() { + // TODO do more testing before applying TakerUtil + //return TakerUtil.isBsqForFeeAvailable(amount.get(), bsqWalletService); + final Coin takerFee = getTakerFee(false); return BisqEnvironment.isBaseCurrencySupportingBsq() && takerFee != null && @@ -537,6 +502,9 @@ private boolean isBuyOffer() { @Nullable Coin getTakerFee(boolean isCurrencyForTakerFeeBtc) { + // TODO do more testing before applying TakerUtil + // return TakerUtil.getTakerFee(isCurrencyForTakerFeeBtc, this.amount.get()); + Coin amount = this.amount.get(); if (amount != null) { // TODO write unit test for that @@ -557,23 +525,6 @@ public void swapTradeToSavings() { btcWalletService.resetAddressEntriesForOpenOffer(offer.getId()); } - // 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. - private int getAverageSize(int txSize) { - return (txSize + 320) / 2; - } - - private Coin getTxFeeBySize(int sizeInBytes) { - return txFeePerByteFromFeeService.multiply(getAverageSize(sizeInBytes)); - } - - /* private void setFeeFromFundingTx(Coin fee) { - feeFromFundingTx = fee; - isFeeFromFundingTxSufficient.set(feeFromFundingTx.compareTo(FeePolicy.getMinRequiredFeeForFundingTx()) >= 0); - }*/ - boolean isMinAmountLessOrEqualAmount() { //noinspection SimplifiableIfStatement if (offer != null && amount.get() != null) @@ -620,10 +571,25 @@ public String getCurrencyNameAndCode() { } public Coin getTotalTxFee() { + Coin totalTxTees = txFeeFromFeeService.add(getTxFeeForDepositTx()).add(getTxFeeForPayoutTx()); if (isCurrencyForTakerFeeBtc()) - return txFeeFromFeeService.multiply(3); + return totalTxTees; else - return txFeeFromFeeService.multiply(3).subtract(getTakerFee() != null ? getTakerFee() : Coin.ZERO); + return totalTxTees.subtract(getTakerFee() != null ? getTakerFee() : Coin.ZERO); + } + + private Coin getTxFeeForDepositTx() { + // Unfortunately we cannot change that to the correct fees as it would break backward compatibility + // We still might find a way with offer version or app version checks so lets keep that commented out + // code as that shows how it should be. + return txFeeFromFeeService; //feeService.getTxFee(320); + } + + private Coin getTxFeeForPayoutTx() { + // Unfortunately we cannot change that to the correct fees as it would break backward compatibility + // We still might find a way with offer version or app version checks so lets keep that commented out + // code as that shows how it should be. + return txFeeFromFeeService; //feeService.getTxFee(380); } public AddressEntry getAddressEntry() { diff --git a/desktop/src/main/java/bisq/desktop/main/portfolio/editoffer/EditOfferDataModel.java b/desktop/src/main/java/bisq/desktop/main/portfolio/editoffer/EditOfferDataModel.java index f0090d191c8..f1d27dd98c4 100644 --- a/desktop/src/main/java/bisq/desktop/main/portfolio/editoffer/EditOfferDataModel.java +++ b/desktop/src/main/java/bisq/desktop/main/portfolio/editoffer/EditOfferDataModel.java @@ -22,7 +22,6 @@ import bisq.core.btc.wallet.BsqWalletService; import bisq.core.btc.wallet.BtcWalletService; -import bisq.core.btc.wallet.TradeWalletService; import bisq.core.filter.FilterManager; import bisq.core.locale.CurrencyUtil; import bisq.core.locale.TradeCurrency; @@ -55,8 +54,32 @@ class EditOfferDataModel extends MutableOfferDataModel { private OpenOffer.State initialState; @Inject - EditOfferDataModel(OpenOfferManager openOfferManager, BtcWalletService btcWalletService, BsqWalletService bsqWalletService, Preferences preferences, User user, KeyRing keyRing, P2PService p2PService, PriceFeedService priceFeedService, FilterManager filterManager, AccountAgeWitnessService accountAgeWitnessService, TradeWalletService tradeWalletService, FeeService feeService, ReferralIdService referralIdService, BSFormatter formatter, CorePersistenceProtoResolver corePersistenceProtoResolver) { - super(openOfferManager, btcWalletService, bsqWalletService, preferences, user, keyRing, p2PService, priceFeedService, filterManager, accountAgeWitnessService, tradeWalletService, feeService, referralIdService, formatter); + EditOfferDataModel(OpenOfferManager openOfferManager, BtcWalletService btcWalletService, + BsqWalletService bsqWalletService, + Preferences preferences, + User user, + KeyRing keyRing, + P2PService p2PService, + PriceFeedService priceFeedService, + FilterManager filterManager, + AccountAgeWitnessService accountAgeWitnessService, + FeeService feeService, + ReferralIdService referralIdService, + BSFormatter formatter, + CorePersistenceProtoResolver corePersistenceProtoResolver) { + super(openOfferManager, + btcWalletService, + bsqWalletService, + preferences, + user, + keyRing, + p2PService, + priceFeedService, + filterManager, + accountAgeWitnessService, + feeService, + referralIdService, + formatter); this.corePersistenceProtoResolver = corePersistenceProtoResolver; } diff --git a/desktop/src/test/java/bisq/desktop/main/offer/createoffer/CreateOfferViewModelTest.java b/desktop/src/test/java/bisq/desktop/main/offer/createoffer/CreateOfferViewModelTest.java index 8ee590cf89f..e0e173b9ebd 100644 --- a/desktop/src/test/java/bisq/desktop/main/offer/createoffer/CreateOfferViewModelTest.java +++ b/desktop/src/test/java/bisq/desktop/main/offer/createoffer/CreateOfferViewModelTest.java @@ -103,7 +103,9 @@ public void setUp() { when(securityDepositValidator.validate(any())).thenReturn(new InputValidator.ValidationResult(false)); when(accountAgeWitnessService.getMyTradeLimit(any(), any())).thenReturn(100000000L); - CreateOfferDataModel dataModel = new CreateOfferDataModel(null, btcWalletService, bsqWalletService, empty, user, null, null, priceFeedService, null, accountAgeWitnessService, null, feeService, null, bsFormatter); + CreateOfferDataModel dataModel = new CreateOfferDataModel(null, btcWalletService, + bsqWalletService, empty, user, null, null, priceFeedService, + null, accountAgeWitnessService, feeService, null, bsFormatter); dataModel.initWithData(OfferPayload.Direction.BUY, new CryptoCurrency("BTC", "bitcoin")); dataModel.activate();