From 565c44d94cb6d4d23e62d8ef5df5f646165e2990 Mon Sep 17 00:00:00 2001 From: Manfred Karrer Date: Wed, 6 Apr 2016 23:26:27 +0200 Subject: [PATCH] Fix fee calculation, add adjustable non-trade mining fee --- .../main/java/io/bitsquare/btc/FeePolicy.java | 10 +- .../java/io/bitsquare/btc/WalletService.java | 224 ++++++++++++------ .../java/io/bitsquare/trade/TradeManager.java | 4 +- .../java/io/bitsquare/user/Preferences.java | 20 +- .../gui/main/funds/deposit/DepositView.java | 2 +- .../main/funds/withdrawal/WithdrawalView.java | 4 +- .../offer/takeoffer/TakeOfferDataModel.java | 2 +- .../bitsquare/gui/main/overlays/Overlay.java | 8 +- .../pendingtrades/PendingTradesDataModel.java | 9 +- .../steps/buyer/BuyerStep5View.java | 51 ++-- .../settings/preferences/PreferencesView.java | 39 +-- .../io/bitsquare/p2p/PeerServiceTest.java | 1 + 12 files changed, 230 insertions(+), 144 deletions(-) diff --git a/core/src/main/java/io/bitsquare/btc/FeePolicy.java b/core/src/main/java/io/bitsquare/btc/FeePolicy.java index abcc134c61..57eccee178 100644 --- a/core/src/main/java/io/bitsquare/btc/FeePolicy.java +++ b/core/src/main/java/io/bitsquare/btc/FeePolicy.java @@ -50,14 +50,14 @@ public class FeePolicy { // The BitcoinJ fee calculation use kb so a tx size < 1kb will still pay the fee for a kb tx. // Our payout tx has about 370 bytes so we get a fee/kb value of about 90 satoshi/byte making it high priority // Other payout transactions (E.g. arbitrators many collected transactions) will go with 30 satoshi/byte if > 1kb - private static Coin FEE_PER_KB = Coin.valueOf(20_000); // 0.0002 BTC about 0.08 EUR @ 400 EUR/BTC + private static Coin NON_TRADE_FEE_PER_KB = Coin.valueOf(10_000); // 0.0001 BTC about 0.04 EUR @ 400 EUR/BTC - public static void setFeePerKb(Coin feePerKb) { - FEE_PER_KB = feePerKb; + public static void setNonTradeFeePerKb(Coin nonTradeFeePerKb) { + NON_TRADE_FEE_PER_KB = nonTradeFeePerKb; } - public static Coin getFeePerKb() { - return FEE_PER_KB; + public static Coin getNonTradeFeePerKb() { + return NON_TRADE_FEE_PER_KB; } // Some wallets don't support manual fees. Most use at least 0.0001 BTC (0.04 EUR @ 400 EUR/BTC) diff --git a/core/src/main/java/io/bitsquare/btc/WalletService.java b/core/src/main/java/io/bitsquare/btc/WalletService.java index e50dc11b5e..bee8ad32d7 100644 --- a/core/src/main/java/io/bitsquare/btc/WalletService.java +++ b/core/src/main/java/io/bitsquare/btc/WalletService.java @@ -38,7 +38,10 @@ import org.bitcoinj.kits.WalletAppKit; import org.bitcoinj.params.MainNetParams; import org.bitcoinj.params.RegTestParams; import org.bitcoinj.params.TestNet3Params; +import org.bitcoinj.script.Script; import org.bitcoinj.utils.Threading; +import org.bitcoinj.wallet.CoinSelection; +import org.bitcoinj.wallet.CoinSelector; import org.bitcoinj.wallet.DeterministicSeed; import org.jetbrains.annotations.NotNull; import org.slf4j.Logger; @@ -347,6 +350,10 @@ public class WalletService { return addressEntryList.addAddressEntry(new AddressEntry(wallet.freshReceiveKey(), wallet.getParams(), context)); } + public AddressEntry createAddressEntry(AddressEntry.Context context) { + return addressEntryList.addAddressEntry(new AddressEntry(wallet.freshReceiveKey(), wallet.getParams(), context)); + } + public Optional findAddressEntry(String address, AddressEntry.Context context) { return getAddressEntryListAsImmutableList().stream() .filter(e -> address.equals(e.getAddressString())) @@ -521,45 +528,156 @@ public class WalletService { /////////////////////////////////////////////////////////////////////////////////////////// - // Withdrawal + // Withdrawal Fee calculation /////////////////////////////////////////////////////////////////////////////////////////// public Coin getRequiredFee(String fromAddress, String toAddress, Coin amount, - @Nullable KeyParameter aesKey, AddressEntry.Context context) throws AddressFormatException, AddressEntryException { - Coin fee; - try { - wallet.completeTx(getSendRequest(fromAddress, toAddress, amount, aesKey, context)); - // We use the min fee for now as the mix of savingswallet/trade wallet has some nasty edge cases... - fee = FeePolicy.getFixedTxFeeForTrades(); - } catch (InsufficientMoneyException e) { - log.info("The amount to be transferred is not enough to pay the transaction fees of {}. " + - "We subtract that fee from the receivers amount to make the transaction possible."); - fee = e.missing; - } - return fee; + Optional addressEntry = findAddressEntry(fromAddress, context); + if (!addressEntry.isPresent()) + throw new AddressEntryException("WithdrawFromAddress is not found in our wallet."); + + checkNotNull(addressEntry.get().getAddress(), "addressEntry.get().getAddress() must nto be null"); + CoinSelector selector = new TradeWalletCoinSelector(params, addressEntry.get().getAddress()); + return getFee(toAddress, + amount, + selector); } public Coin getRequiredFeeForMultipleAddresses(Set fromAddresses, String toAddress, - Coin amount, - @Nullable KeyParameter aesKey) throws AddressFormatException, + Coin amount) throws AddressFormatException, AddressEntryException { - Coin fee; - try { - wallet.completeTx(getSendRequestForMultipleAddresses(fromAddresses, toAddress, amount, null, aesKey)); - // We use the min fee for now as the mix of savingswallet/trade wallet has some nasty edge cases... - fee = FeePolicy.getFixedTxFeeForTrades(); - } catch (InsufficientMoneyException e) { - log.info("The amount to be transferred is not enough to pay the transaction fees of {}. " + - "We subtract that fee from the receivers amount to make the transaction possible."); - fee = e.missing; - } + Set addressEntries = fromAddresses.stream() + .map(address -> { + Optional addressEntryOptional = findAddressEntry(address, AddressEntry.Context.AVAILABLE); + if (!addressEntryOptional.isPresent()) + addressEntryOptional = findAddressEntry(address, AddressEntry.Context.OFFER_FUNDING); + if (!addressEntryOptional.isPresent()) + addressEntryOptional = findAddressEntry(address, AddressEntry.Context.TRADE_PAYOUT); + if (!addressEntryOptional.isPresent()) + addressEntryOptional = findAddressEntry(address, AddressEntry.Context.ARBITRATOR); + return addressEntryOptional; + }) + .filter(Optional::isPresent) + .map(Optional::get) + .collect(Collectors.toSet()); + if (addressEntries.isEmpty()) + throw new AddressEntryException("No Addresses for withdraw found in our wallet"); + + CoinSelector selector = new MultiAddressesCoinSelector(params, addressEntries); + return getFee(toAddress, + amount, + selector); + } + + private Coin getFee(String toAddress, + Coin amount, + CoinSelector selector) throws AddressFormatException, AddressEntryException { + List candidates = wallet.calculateAllSpendCandidates(); + CoinSelection bestCoinSelection = selector.select(params.getMaxMoney(), candidates); + Transaction tx = new Transaction(params); + tx.addOutput(amount, new Address(params, toAddress)); + if (!adjustOutputDownwardsForFee(tx, bestCoinSelection, Coin.ZERO, FeePolicy.getNonTradeFeePerKb())) + throw new Wallet.CouldNotAdjustDownwards(); + + Coin fee = amount.subtract(tx.getOutput(0).getValue()); + log.info("Required fee " + fee); return fee; } + private boolean adjustOutputDownwardsForFee(Transaction tx, CoinSelection coinSelection, Coin baseFee, Coin feePerKb) { + TransactionOutput output = tx.getOutput(0); + // Check if we need additional fee due to the transaction's size + int size = tx.bitcoinSerialize().length; + size += estimateBytesForSigning(coinSelection); + Coin fee = baseFee.add(feePerKb.multiply((size / 1000) + 1)); + output.setValue(output.getValue().subtract(fee)); + // Check if we need additional fee due to the output's value + if (output.getValue().compareTo(Coin.CENT) < 0 && fee.compareTo(Transaction.REFERENCE_DEFAULT_MIN_TX_FEE) < 0) + output.setValue(output.getValue().subtract(Transaction.REFERENCE_DEFAULT_MIN_TX_FEE.subtract(fee))); + return output.getMinNonDustValue().compareTo(output.getValue()) <= 0; + } + + private int estimateBytesForSigning(CoinSelection selection) { + int size = 0; + for (TransactionOutput output : selection.gathered) { + try { + Script script = output.getScriptPubKey(); + ECKey key = null; + Script redeemScript = null; + if (script.isSentToAddress()) { + key = wallet.findKeyFromPubHash(script.getPubKeyHash()); + checkNotNull(key, "Coin selection includes unspendable outputs"); + } else if (script.isPayToScriptHash()) { + redeemScript = wallet.findRedeemDataFromScriptHash(script.getPubKeyHash()).redeemScript; + checkNotNull(redeemScript, "Coin selection includes unspendable outputs"); + } + size += script.getNumberOfBytesRequiredToSpend(key, redeemScript); + } catch (ScriptException e) { + // If this happens it means an output script in a wallet tx could not be understood. That should never + // happen, if it does it means the wallet has got into an inconsistent state. + throw new IllegalStateException(e); + } + } + return size; + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Withdrawal Send + /////////////////////////////////////////////////////////////////////////////////////////// + + public String sendFunds(String fromAddress, + String toAddress, + Coin receiverAmount, + @Nullable KeyParameter aesKey, + AddressEntry.Context context, + FutureCallback callback) throws AddressFormatException, + AddressEntryException, InsufficientMoneyException { + Wallet.SendResult sendResult = wallet.sendCoins(getSendRequest(fromAddress, toAddress, receiverAmount, aesKey, context)); + Futures.addCallback(sendResult.broadcastComplete, callback); + + printTxWithInputs("sendFunds", sendResult.tx); + return sendResult.tx.getHashAsString(); + } + + public String sendFundsForMultipleAddresses(Set fromAddresses, + String toAddress, + Coin receiverAmount, + @Nullable String changeAddress, + @Nullable KeyParameter aesKey, + FutureCallback callback) throws AddressFormatException, + AddressEntryException, InsufficientMoneyException { + Wallet.SendResult sendResult = wallet.sendCoins(getSendRequestForMultipleAddresses(fromAddresses, toAddress, + receiverAmount, changeAddress, aesKey)); + Futures.addCallback(sendResult.broadcastComplete, callback); + + printTxWithInputs("sendFunds", sendResult.tx); + return sendResult.tx.getHashAsString(); + } + + public void emptyWallet(String toAddress, KeyParameter aesKey, ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) + throws InsufficientMoneyException, AddressFormatException { + Wallet.SendRequest sendRequest = Wallet.SendRequest.emptyWallet(new Address(params, toAddress)); + sendRequest.aesKey = aesKey; + Wallet.SendResult sendResult = wallet.sendCoins(sendRequest); + sendRequest.feePerKb = FeePolicy.getNonTradeFeePerKb(); + Futures.addCallback(sendResult.broadcastComplete, new FutureCallback() { + @Override + public void onSuccess(Transaction result) { + resultHandler.handleResult(); + } + + @Override + public void onFailure(@NotNull Throwable t) { + errorMessageHandler.handleErrorMessage(t.getMessage()); + } + }); + } + private Wallet.SendRequest getSendRequest(String fromAddress, String toAddress, Coin amount, @@ -581,7 +699,7 @@ public class WalletService { checkNotNull(addressEntry.get().getAddress(), "addressEntry.get().getAddress() must nto be null"); sendRequest.coinSelector = new TradeWalletCoinSelector(params, addressEntry.get().getAddress()); sendRequest.changeAddress = addressEntry.get().getAddress(); - sendRequest.feePerKb = FeePolicy.getFeePerKb(); + sendRequest.feePerKb = FeePolicy.getNonTradeFeePerKb(); return sendRequest; } @@ -606,6 +724,8 @@ public class WalletService { addressEntryOptional = findAddressEntry(address, AddressEntry.Context.OFFER_FUNDING); if (!addressEntryOptional.isPresent()) addressEntryOptional = findAddressEntry(address, AddressEntry.Context.TRADE_PAYOUT); + if (!addressEntryOptional.isPresent()) + addressEntryOptional = findAddressEntry(address, AddressEntry.Context.ARBITRATOR); return addressEntryOptional; }) .filter(Optional::isPresent) @@ -629,60 +749,10 @@ public class WalletService { } checkNotNull(changeAddressAddressEntry, "change address must not be null"); sendRequest.changeAddress = changeAddressAddressEntry.getAddress(); - sendRequest.feePerKb = FeePolicy.getFeePerKb(); + sendRequest.feePerKb = FeePolicy.getNonTradeFeePerKb(); return sendRequest; } - public String sendFunds(String fromAddress, - String toAddress, - Coin amount, - @Nullable KeyParameter aesKey, - AddressEntry.Context context, - FutureCallback callback) throws AddressFormatException, - AddressEntryException, InsufficientMoneyException { - Coin fee = getRequiredFee(fromAddress, toAddress, amount, aesKey, context); - Wallet.SendResult sendResult = wallet.sendCoins(getSendRequest(fromAddress, toAddress, amount.subtract(fee), aesKey, context)); - Futures.addCallback(sendResult.broadcastComplete, callback); - - printTxWithInputs("sendFunds", sendResult.tx); - return sendResult.tx.getHashAsString(); - } - - public String sendFundsForMultipleAddresses(Set fromAddresses, - String toAddress, - Coin amount, - @Nullable String changeAddress, - @Nullable KeyParameter aesKey, - FutureCallback callback) throws AddressFormatException, - AddressEntryException, InsufficientMoneyException { - Coin fee = getRequiredFeeForMultipleAddresses(fromAddresses, toAddress, amount, aesKey); - Wallet.SendResult sendResult = wallet.sendCoins(getSendRequestForMultipleAddresses(fromAddresses, toAddress, - amount.subtract(fee), changeAddress, aesKey)); - Futures.addCallback(sendResult.broadcastComplete, callback); - - printTxWithInputs("sendFunds", sendResult.tx); - return sendResult.tx.getHashAsString(); - } - - public void emptyWallet(String toAddress, KeyParameter aesKey, ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) - throws InsufficientMoneyException, AddressFormatException { - Wallet.SendRequest sendRequest = Wallet.SendRequest.emptyWallet(new Address(params, toAddress)); - sendRequest.aesKey = aesKey; - Wallet.SendResult sendResult = wallet.sendCoins(sendRequest); - sendRequest.feePerKb = FeePolicy.getFeePerKb(); - Futures.addCallback(sendResult.broadcastComplete, new FutureCallback() { - @Override - public void onSuccess(Transaction result) { - resultHandler.handleResult(); - } - - @Override - public void onFailure(@NotNull Throwable t) { - errorMessageHandler.handleErrorMessage(t.getMessage()); - } - }); - } - /////////////////////////////////////////////////////////////////////////////////////////// // Getters diff --git a/core/src/main/java/io/bitsquare/trade/TradeManager.java b/core/src/main/java/io/bitsquare/trade/TradeManager.java index d2ab532ed6..434be6ba43 100644 --- a/core/src/main/java/io/bitsquare/trade/TradeManager.java +++ b/core/src/main/java/io/bitsquare/trade/TradeManager.java @@ -306,7 +306,7 @@ public class TradeManager { // Trade /////////////////////////////////////////////////////////////////////////////////////////// - public void onWithdrawRequest(String toAddress, KeyParameter aesKey, Trade trade, ResultHandler resultHandler, FaultHandler faultHandler) { + public void onWithdrawRequest(String toAddress, Coin receiverAmount, KeyParameter aesKey, Trade trade, ResultHandler resultHandler, FaultHandler faultHandler) { String fromAddress = walletService.getOrCreateAddressEntry(trade.getId(), AddressEntry.Context.TRADE_PAYOUT).getAddressString(); FutureCallback callback = new FutureCallback() { @@ -328,7 +328,7 @@ public class TradeManager { } }; try { - walletService.sendFunds(fromAddress, toAddress, trade.getPayoutAmount(), aesKey, AddressEntry.Context.TRADE_PAYOUT, callback); + walletService.sendFunds(fromAddress, toAddress, receiverAmount, aesKey, AddressEntry.Context.TRADE_PAYOUT, callback); } catch (AddressFormatException | InsufficientMoneyException | AddressEntryException e) { e.printStackTrace(); log.error(e.getMessage()); diff --git a/core/src/main/java/io/bitsquare/user/Preferences.java b/core/src/main/java/io/bitsquare/user/Preferences.java index 95edb46c83..e3cd230452 100644 --- a/core/src/main/java/io/bitsquare/user/Preferences.java +++ b/core/src/main/java/io/bitsquare/user/Preferences.java @@ -105,7 +105,7 @@ public final class Preferences implements Persistable { private boolean showOwnOffersInOfferBook; private Locale preferredLocale; private TradeCurrency preferredTradeCurrency; - private long txFeePerKB = FeePolicy.getFeePerKb().value; + private long nonTradeTxFeePerKB = FeePolicy.getNonTradeFeePerKb().value; private double maxPriceDistanceInPercent; // Observable wrappers @@ -162,7 +162,7 @@ public final class Preferences implements Persistable { maxPriceDistanceInPercent = 0.2; try { - setTxFeePerKB(persisted.getTxFeePerKB()); + setNonTradeTxFeePerKB(persisted.getNonTradeTxFeePerKB()); } catch (Exception e) { // leave default value } @@ -306,15 +306,15 @@ public final class Preferences implements Persistable { } } - public void setTxFeePerKB(long txFeePerKB) throws Exception { - if (txFeePerKB < Transaction.REFERENCE_DEFAULT_MIN_TX_FEE.value) + public void setNonTradeTxFeePerKB(long nonTradeTxFeePerKB) throws Exception { + if (nonTradeTxFeePerKB < Transaction.REFERENCE_DEFAULT_MIN_TX_FEE.value) throw new Exception("Transaction fee must be at least 5 satoshi/byte"); - if (txFeePerKB < Transaction.REFERENCE_DEFAULT_MIN_TX_FEE.value) - throw new Exception("Transaction fee must be at least 5 satoshi/byte"); + if (nonTradeTxFeePerKB > 500_000) + throw new Exception("Transaction fee is in the range of 10-100 satoshi/byte. Your input is above any reasonable value (>500 satoshi/byte)."); - this.txFeePerKB = txFeePerKB; - FeePolicy.setFeePerKb(Coin.valueOf(txFeePerKB)); + this.nonTradeTxFeePerKB = nonTradeTxFeePerKB; + FeePolicy.setNonTradeFeePerKb(Coin.valueOf(nonTradeTxFeePerKB)); storage.queueUpForSave(); } @@ -435,8 +435,8 @@ public final class Preferences implements Persistable { return preferredTradeCurrency; } - public long getTxFeePerKB() { - return Math.max(Transaction.REFERENCE_DEFAULT_MIN_TX_FEE.value, txFeePerKB); + public long getNonTradeTxFeePerKB() { + return Math.max(Transaction.REFERENCE_DEFAULT_MIN_TX_FEE.value, nonTradeTxFeePerKB); } public boolean getUseTorForBitcoinJ() { diff --git a/gui/src/main/java/io/bitsquare/gui/main/funds/deposit/DepositView.java b/gui/src/main/java/io/bitsquare/gui/main/funds/deposit/DepositView.java index 16f9adb58f..1ef7d6ac42 100644 --- a/gui/src/main/java/io/bitsquare/gui/main/funds/deposit/DepositView.java +++ b/gui/src/main/java/io/bitsquare/gui/main/funds/deposit/DepositView.java @@ -186,7 +186,7 @@ public class DepositView extends ActivatableView { new Popup().warning("You have already at least one address which is not used yet in any transaction.\n" + "Please select in the address table an unused address.").show(); } else { - AddressEntry newSavingsAddressEntry = walletService.getOrCreateAddressEntry(AddressEntry.Context.AVAILABLE); + AddressEntry newSavingsAddressEntry = walletService.createAddressEntry(AddressEntry.Context.AVAILABLE); updateList(); observableList.stream() .filter(depositListItem -> depositListItem.getAddressString().equals(newSavingsAddressEntry.getAddressString())) diff --git a/gui/src/main/java/io/bitsquare/gui/main/funds/withdrawal/WithdrawalView.java b/gui/src/main/java/io/bitsquare/gui/main/funds/withdrawal/WithdrawalView.java index 8f5ae9920c..2e874dc575 100644 --- a/gui/src/main/java/io/bitsquare/gui/main/funds/withdrawal/WithdrawalView.java +++ b/gui/src/main/java/io/bitsquare/gui/main/funds/withdrawal/WithdrawalView.java @@ -196,10 +196,10 @@ public class WithdrawalView extends ActivatableView { }; try { Coin requiredFee = walletService.getRequiredFeeForMultipleAddresses(fromAddresses, - withdrawToTextField.getText(), senderAmount, null); + withdrawToTextField.getText(), senderAmount); Coin receiverAmount = senderAmount.subtract(requiredFee); if (BitsquareApp.DEV_MODE) { - doWithdraw(senderAmount, callback); + doWithdraw(receiverAmount, callback); } else { new Popup().headLine("Confirm withdrawal request") .confirmation("Sending: " + formatter.formatCoinWithCode(senderAmount) + "\n" + diff --git a/gui/src/main/java/io/bitsquare/gui/main/offer/takeoffer/TakeOfferDataModel.java b/gui/src/main/java/io/bitsquare/gui/main/offer/takeoffer/TakeOfferDataModel.java index 4db9dc990f..9136e7471e 100644 --- a/gui/src/main/java/io/bitsquare/gui/main/offer/takeoffer/TakeOfferDataModel.java +++ b/gui/src/main/java/io/bitsquare/gui/main/offer/takeoffer/TakeOfferDataModel.java @@ -408,7 +408,7 @@ class TakeOfferDataModel extends ActivatableDataModel { //noinspection SimplifiableIfStatement if (amountAsCoin.get() != null && offer != null) { Coin customAmount = offer.getAmount().subtract(amountAsCoin.get()); - Coin dustAndFee = FeePolicy.getFeePerKb().add(Transaction.MIN_NONDUST_OUTPUT); + Coin dustAndFee = FeePolicy.getFixedTxFeeForTrades().add(Transaction.MIN_NONDUST_OUTPUT); return customAmount.isPositive() && customAmount.isLessThan(dustAndFee); } else { return true; diff --git a/gui/src/main/java/io/bitsquare/gui/main/overlays/Overlay.java b/gui/src/main/java/io/bitsquare/gui/main/overlays/Overlay.java index 126c799815..3e98821b3f 100644 --- a/gui/src/main/java/io/bitsquare/gui/main/overlays/Overlay.java +++ b/gui/src/main/java/io/bitsquare/gui/main/overlays/Overlay.java @@ -186,9 +186,11 @@ public abstract class Overlay { Scene rootScene = owner.getScene(); if (rootScene != null) { Window window = rootScene.getWindow(); - window.xProperty().removeListener(positionListener); - window.yProperty().removeListener(positionListener); - window.widthProperty().removeListener(positionListener); + if (window != null && positionListener != null) { + window.xProperty().removeListener(positionListener); + window.yProperty().removeListener(positionListener); + window.widthProperty().removeListener(positionListener); + } } } diff --git a/gui/src/main/java/io/bitsquare/gui/main/portfolio/pendingtrades/PendingTradesDataModel.java b/gui/src/main/java/io/bitsquare/gui/main/portfolio/pendingtrades/PendingTradesDataModel.java index 6055189200..20f8d0d6d9 100644 --- a/gui/src/main/java/io/bitsquare/gui/main/portfolio/pendingtrades/PendingTradesDataModel.java +++ b/gui/src/main/java/io/bitsquare/gui/main/portfolio/pendingtrades/PendingTradesDataModel.java @@ -147,12 +147,12 @@ public class PendingTradesDataModel extends ActivatableDataModel { ((SellerTrade) getTrade()).onFiatPaymentReceived(resultHandler, errorMessageHandler); } - public void onWithdrawRequest(String toAddress, ResultHandler resultHandler, FaultHandler faultHandler) { + public void onWithdrawRequest(String toAddress, Coin receiverAmount, ResultHandler resultHandler, FaultHandler faultHandler) { checkNotNull(getTrade(), "trade must not be null"); if (walletService.getWallet().isEncrypted()) { - walletPasswordWindow.onAesKey(aesKey -> doWithdrawRequest(toAddress, aesKey, resultHandler, faultHandler)).show(); + walletPasswordWindow.onAesKey(aesKey -> doWithdrawRequest(toAddress, receiverAmount, aesKey, resultHandler, faultHandler)).show(); } else - doWithdrawRequest(toAddress, null, resultHandler, faultHandler); + doWithdrawRequest(toAddress, receiverAmount, null, resultHandler, faultHandler); } public void onOpenDispute() { @@ -279,10 +279,11 @@ public class PendingTradesDataModel extends ActivatableDataModel { selectedItemProperty.set(item); } - private void doWithdrawRequest(String toAddress, KeyParameter aesKey, ResultHandler resultHandler, FaultHandler faultHandler) { + private void doWithdrawRequest(String toAddress, Coin receiverAmount, KeyParameter aesKey, ResultHandler resultHandler, FaultHandler faultHandler) { if (toAddress != null && toAddress.length() > 0) { tradeManager.onWithdrawRequest( toAddress, + receiverAmount, aesKey, getTrade(), () -> { diff --git a/gui/src/main/java/io/bitsquare/gui/main/portfolio/pendingtrades/steps/buyer/BuyerStep5View.java b/gui/src/main/java/io/bitsquare/gui/main/portfolio/pendingtrades/steps/buyer/BuyerStep5View.java index edebc27778..1f05e795f6 100644 --- a/gui/src/main/java/io/bitsquare/gui/main/portfolio/pendingtrades/steps/buyer/BuyerStep5View.java +++ b/gui/src/main/java/io/bitsquare/gui/main/portfolio/pendingtrades/steps/buyer/BuyerStep5View.java @@ -168,19 +168,20 @@ public class BuyerStep5View extends TradeStepView { // TODO at some error situation it can be tha the funds are already paid out and we get stuck here // need handling to remove the trade (planned for next release) Coin balance = walletService.getBalanceForAddress(fromAddressesEntry.getAddress()); - if (balance.isZero()) { - new Popup().warning("Your funds have already been withdrawn.\nPlease check the transaction history.").show(); - model.dataModel.tradeManager.addTradeToClosedTrades(trade); - } else { - if (toAddresses.isEmpty()) { - validateWithdrawAddress(); - } else if (Restrictions.isAboveFixedTxFeeAndDust(senderAmount)) { - try { + try { + Coin requiredFee = walletService.getRequiredFee(fromAddresses, toAddresses, senderAmount, AddressEntry.Context.TRADE_PAYOUT); + Coin receiverAmount = senderAmount.subtract(requiredFee); + if (balance.isZero()) { + new Popup().warning("Your funds have already been withdrawn.\nPlease check the transaction history.").show(); + model.dataModel.tradeManager.addTradeToClosedTrades(trade); + } else { + if (toAddresses.isEmpty()) { + validateWithdrawAddress(); + } else if (Restrictions.isAboveFixedTxFeeAndDust(senderAmount)) { + if (BitsquareApp.DEV_MODE) { - doWithdrawal(); + doWithdrawal(receiverAmount); } else { - Coin requiredFee = walletService.getRequiredFee(fromAddresses, toAddresses, senderAmount, null, AddressEntry.Context.TRADE_PAYOUT); - Coin receiverAmount = senderAmount.subtract(requiredFee); BSFormatter formatter = model.formatter; String key = "reviewWithdrawalAtTradeComplete"; if (!BitsquareApp.DEV_MODE && preferences.showAgain(key)) { @@ -197,34 +198,34 @@ public class BuyerStep5View extends TradeStepView { withdrawToExternalWalletButton.setDisable(false); }) .actionButtonText("Yes") - .onAction(() -> doWithdrawal()) + .onAction(() -> doWithdrawal(receiverAmount)) .dontShowAgainId(key, preferences) .show(); } else { - doWithdrawal(); + doWithdrawal(receiverAmount); } } - } catch (AddressFormatException e) { - validateWithdrawAddress(); - } catch (AddressEntryException e) { - log.error(e.getMessage()); + + } else { + new Popup() + .warning("The amount to transfer is lower than the transaction fee and the min. possible tx value (dust).") + .show(); } - } else { - new Popup() - .warning("The amount to transfer is lower than the transaction fee and the min. possible tx value (dust).") - .show(); } + } catch (AddressFormatException e) { + validateWithdrawAddress(); + } catch (AddressEntryException e) { + log.error(e.getMessage()); } } - private void doWithdrawal() { + private void doWithdrawal(Coin receiverAmount) { useSavingsWalletButton.setDisable(true); withdrawToExternalWalletButton.setDisable(true); model.dataModel.onWithdrawRequest(withdrawAddressTextField.getText(), - () -> { - handleTradeCompleted(); - }, + receiverAmount, + this::handleTradeCompleted, (errorMessage, throwable) -> { useSavingsWalletButton.setDisable(false); withdrawToExternalWalletButton.setDisable(false); diff --git a/gui/src/main/java/io/bitsquare/gui/main/settings/preferences/PreferencesView.java b/gui/src/main/java/io/bitsquare/gui/main/settings/preferences/PreferencesView.java index cb267de466..b325338d9a 100644 --- a/gui/src/main/java/io/bitsquare/gui/main/settings/preferences/PreferencesView.java +++ b/gui/src/main/java/io/bitsquare/gui/main/settings/preferences/PreferencesView.java @@ -31,8 +31,6 @@ import io.bitsquare.gui.util.Layout; import io.bitsquare.locale.*; import io.bitsquare.user.BlockChainExplorer; import io.bitsquare.user.Preferences; -import javafx.beans.property.SimpleStringProperty; -import javafx.beans.property.StringProperty; import javafx.beans.value.ChangeListener; import javafx.collections.FXCollections; import javafx.collections.ObservableList; @@ -44,6 +42,7 @@ import javafx.scene.layout.AnchorPane; import javafx.scene.layout.GridPane; import javafx.util.Callback; import javafx.util.StringConverter; +import org.jetbrains.annotations.NotNull; import javax.inject.Inject; import java.util.concurrent.TimeUnit; @@ -61,7 +60,7 @@ public class PreferencesView extends ActivatableViewAndModel transactionFeeFocusedListener; private final Preferences preferences; private BSFormatter formatter; @@ -75,7 +74,6 @@ public class PreferencesView extends ActivatableViewAndModel btcDenominations = FXCollections.observableArrayList(Preferences.getBtcDenominations()); final ObservableList blockExplorers; final ObservableList languageCodes; - final StringProperty transactionFeePerByte = new SimpleStringProperty(); public final ObservableList fiatCurrencies; public final ObservableList allFiatCurrencies; public final ObservableList cryptoCurrencies; @@ -288,7 +286,7 @@ public class PreferencesView extends ActivatableViewAndModel deviationInputTextField.setText(formatter.formatToPercent(preferences.getMaxPriceDistanceInPercent())), 100, TimeUnit.MILLISECONDS); }; - // TODO need a bit extra work to separate trade and non trade tx fees before it can be used - /*transactionFeeInputTextField = addLabelInputTextField(root, ++gridRow, "Transaction fee (satoshi/byte):").second; + transactionFeeInputTextField = addLabelInputTextField(root, ++gridRow, "Transaction fee (satoshi/byte):").second; transactionFeeFocusedListener = (o, oldValue, newValue) -> { - onFocusOutTransactionFeeTextField(oldValue, newValue); - };*/ + if (oldValue && !newValue) { + try { + int val = Integer.parseInt(transactionFeeInputTextField.getText()); + preferences.setNonTradeTxFeePerKB(val * 1000); + } catch (NumberFormatException t) { + new Popup().warning("Please enter integer numbers only.").show(); + transactionFeeInputTextField.setText(getNonTradeTxFeePerKB()); + } catch (Throwable t) { + new Popup().warning("Your input was not accepted.\n" + t.getMessage()).show(); + transactionFeeInputTextField.setText(getNonTradeTxFeePerKB()); + } + } + }; } private void initializeDisplayOptions() { @@ -378,7 +386,7 @@ public class PreferencesView extends ActivatableViewAndModel