Use new tx and trade fees. UI improvements, remove trade fee from provider data

This commit is contained in:
Manfred Karrer 2017-02-16 15:43:38 -05:00
parent e31dac1e49
commit 0580e73179
16 changed files with 164 additions and 124 deletions

View file

@ -31,28 +31,8 @@ public class FeePolicy {
// https://estimatefee.appspot.com/ // https://estimatefee.appspot.com/
// Average values are 10-100 satoshis/byte in january 2016 // Average values are 10-100 satoshis/byte in january 2016
// Average values are 60-140 satoshis/byte in february 2017 // Average values are 60-140 satoshis/byte in february 2017
//
// Our trade transactions have a fixed set of inputs and outputs making the size very predictable
// (as long the user does not do multiple funding transactions)
//
// trade fee tx: 226 bytes // 221 satoshi/byte
// deposit tx: 336 bytes // 148 satoshi/byte
// payout tx: 371 bytes // 134 satoshi/byte
// disputed payout tx: 408 bytes // 122 satoshi/byte
// We set a fixed fee to make the needed amounts in the trade predictable. private static Coin NON_TRADE_FEE_PER_KB = Coin.valueOf(150_000);
// We use 0.0005 BTC (0.5 EUR @ 1000 EUR/BTC) which is for our tx sizes about 120-220 satoshi/byte
// We cannot make that user defined as it need to be the same for both users, so we can only change that in
// software updates
// TODO before Beta we should get a good future proof guess as a change causes incompatible versions
// For non trade transactions (withdrawal) we use the default fee calculation
// To avoid issues with not getting into full blocks, we increase the fee/kb to 30 satoshi/byte
// The user can change that in the preferences
// 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 NON_TRADE_FEE_PER_KB = Coin.valueOf(40_000); // 0.0004 BTC about 0.16 EUR @ 400 EUR/BTC
public static void setNonTradeFeePerKb(Coin nonTradeFeePerKb) { public static void setNonTradeFeePerKb(Coin nonTradeFeePerKb) {
NON_TRADE_FEE_PER_KB = nonTradeFeePerKb; NON_TRADE_FEE_PER_KB = nonTradeFeePerKb;
@ -62,4 +42,8 @@ public class FeePolicy {
return NON_TRADE_FEE_PER_KB; return NON_TRADE_FEE_PER_KB;
} }
public static Coin getDefaultSecurityDeposit() {
return Coin.valueOf(3_000_000);
}
} }

View file

@ -7,12 +7,8 @@ public class FeeData {
private static final Logger log = LoggerFactory.getLogger(FeeData.class); private static final Logger log = LoggerFactory.getLogger(FeeData.class);
public final long txFeePerByte; public final long txFeePerByte;
public final long createOfferFee;
public final long takeOfferFee;
public FeeData(long txFeePerByte, long createOfferFee, long takeOfferFee) { public FeeData(long txFeePerByte) {
this.txFeePerByte = txFeePerByte; this.txFeePerByte = txFeePerByte;
this.createOfferFee = createOfferFee;
this.takeOfferFee = takeOfferFee;
} }
} }

View file

@ -14,6 +14,7 @@ import java.io.IOException;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
//TODO use protobuffer instead of json
public class FeeProvider extends HttpClientProvider { public class FeeProvider extends HttpClientProvider {
private static final Logger log = LoggerFactory.getLogger(FeeProvider.class); private static final Logger log = LoggerFactory.getLogger(FeeProvider.class);
@ -28,7 +29,7 @@ public class FeeProvider extends HttpClientProvider {
tsMap.put("bitcoinFeesTs", ((Double) linkedTreeMap.get("bitcoinFeesTs")).longValue()); tsMap.put("bitcoinFeesTs", ((Double) linkedTreeMap.get("bitcoinFeesTs")).longValue());
LinkedTreeMap<String, Double> dataMap = (LinkedTreeMap<String, Double>) linkedTreeMap.get("data"); LinkedTreeMap<String, Double> dataMap = (LinkedTreeMap<String, Double>) linkedTreeMap.get("data");
FeeData feeData = new FeeData(dataMap.get("txFee").longValue(), dataMap.get("createOfferFee").longValue(), dataMap.get("takeOfferFee").longValue()); FeeData feeData = new FeeData(dataMap.get("txFee").longValue());
return new Tuple2<>(tsMap, feeData); return new Tuple2<>(tsMap, feeData);
} }

View file

@ -21,7 +21,6 @@ import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.SettableFuture; import com.google.common.util.concurrent.SettableFuture;
import com.google.inject.Inject; import com.google.inject.Inject;
import io.bitsquare.app.Log;
import io.bitsquare.btc.provider.ProvidersRepository; import io.bitsquare.btc.provider.ProvidersRepository;
import io.bitsquare.common.UserThread; import io.bitsquare.common.UserThread;
import io.bitsquare.common.handlers.FaultHandler; import io.bitsquare.common.handlers.FaultHandler;
@ -33,6 +32,7 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import javax.annotation.Nullable; import javax.annotation.Nullable;
import java.time.Instant;
import java.util.Map; import java.util.Map;
import static com.google.common.base.Preconditions.checkNotNull; import static com.google.common.base.Preconditions.checkNotNull;
@ -40,22 +40,28 @@ import static com.google.common.base.Preconditions.checkNotNull;
public class FeeService { public class FeeService {
private static final Logger log = LoggerFactory.getLogger(FeeService.class); private static final Logger log = LoggerFactory.getLogger(FeeService.class);
public static final long MIN_TX_FEE = 40; // satoshi/byte public static final long MIN_TX_FEE = 60; // satoshi/byte
public static final long MAX_TX_FEE = 200; public static final long MAX_TX_FEE = 300;
public static final long DEFAULT_TX_FEE = 100; public static final long DEFAULT_TX_FEE = 150;
public static final long MIN_CREATE_OFFER_FEE = 10_000; public static final long MIN_CREATE_OFFER_FEE_IN_BTC = 10_000;
public static final long MAX_CREATE_OFFER_FEE = 500_000; public static final long MAX_CREATE_OFFER_FEE_IN_BTC = 500_000;
public static final long DEFAULT_CREATE_OFFER_FEE = 30_000; public static final long DEFAULT_CREATE_OFFER_FEE_IN_BTC = 30_000; // excluded mining fee
public static final long MIN_TAKE_OFFER_FEE = 10_000; public static final long MIN_TAKE_OFFER_FEE_IN_BTC = 10_000;
public static final long MAX_TAKE_OFFER_FEE = 1000_000; public static final long MAX_TAKE_OFFER_FEE_IN_BTC = 1000_000;
public static final long DEFAULT_TAKE_OFFER_FEE = 80_000; public static final long DEFAULT_TAKE_OFFER_FEE_IN_BTC = 30_000; // excluded mining fee
// 0.00216 btc is for 3 x tx fee for taker -> about 2 EUR!
public static final long MIN_PAUSE_BETWEEN_REQUESTS_IN_MIN = 10;
private final FeeProvider feeProvider; private final FeeProvider feeProvider;
@Nullable
private FeeData feeData; private FeeData feeData;
private Map<String, Long> timeStampMap; private Map<String, Long> timeStampMap;
private long epochInSecondAtLastRequest; private long epochInSecondAtLastRequest;
private long lastRequest;
/////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////
// Constructor // Constructor
@ -65,7 +71,6 @@ public class FeeService {
public FeeService(HttpClient httpClient, public FeeService(HttpClient httpClient,
ProvidersRepository providersRepository) { ProvidersRepository providersRepository) {
this.feeProvider = new FeeProvider(httpClient, providersRepository.getBaseUrl()); this.feeProvider = new FeeProvider(httpClient, providersRepository.getBaseUrl());
feeData = new FeeData(DEFAULT_TX_FEE, DEFAULT_CREATE_OFFER_FEE, DEFAULT_TAKE_OFFER_FEE);
} }
public void onAllServicesInitialized() { public void onAllServicesInitialized() {
@ -73,30 +78,39 @@ public class FeeService {
} }
public void requestFees(@Nullable Runnable resultHandler, @Nullable FaultHandler faultHandler) { public void requestFees(@Nullable Runnable resultHandler, @Nullable FaultHandler faultHandler) {
//TODO add throttle long now = Instant.now().getEpochSecond();
Log.traceCall(); if (feeData == null || now - lastRequest > MIN_PAUSE_BETWEEN_REQUESTS_IN_MIN * 60) {
FeeRequest feeRequest = new FeeRequest(); lastRequest = now;
SettableFuture<Tuple2<Map<String, Long>, FeeData>> future = feeRequest.getFees(feeProvider); FeeRequest feeRequest = new FeeRequest();
Futures.addCallback(future, new FutureCallback<Tuple2<Map<String, Long>, FeeData>>() { SettableFuture<Tuple2<Map<String, Long>, FeeData>> future = feeRequest.getFees(feeProvider);
@Override Futures.addCallback(future, new FutureCallback<Tuple2<Map<String, Long>, FeeData>>() {
public void onSuccess(@Nullable Tuple2<Map<String, Long>, FeeData> result) { @Override
UserThread.execute(() -> { public void onSuccess(@Nullable Tuple2<Map<String, Long>, FeeData> result) {
checkNotNull(result, "Result must not be null at getFees"); UserThread.execute(() -> {
timeStampMap = result.first; checkNotNull(result, "Result must not be null at getFees");
epochInSecondAtLastRequest = timeStampMap.get("bitcoinFeesTs"); timeStampMap = result.first;
feeData = result.second; epochInSecondAtLastRequest = timeStampMap.get("bitcoinFeesTs");
if (resultHandler != null) feeData = result.second;
resultHandler.run(); log.info("Tx fee: txFeePerByte=" + feeData.txFeePerByte);
}); if (resultHandler != null)
} resultHandler.run();
});
}
@Override @Override
public void onFailure(@NotNull Throwable throwable) { public void onFailure(@NotNull Throwable throwable) {
log.warn("Could not load fees. " + throwable.toString()); log.warn("Could not load fees. " + throwable.toString());
if (faultHandler != null) if (faultHandler != null)
UserThread.execute(() -> faultHandler.handleFault("Could not load fees", throwable)); UserThread.execute(() -> faultHandler.handleFault("Could not load fees", throwable));
} }
}); });
} else {
log.debug("We got a requestFees called again before min pause of {} minutes has passed.", MIN_PAUSE_BETWEEN_REQUESTS_IN_MIN);
UserThread.execute(() -> {
if (resultHandler != null)
resultHandler.run();
});
}
} }
public Coin getTxFee(int sizeInBytes) { public Coin getTxFee(int sizeInBytes) {
@ -104,16 +118,20 @@ public class FeeService {
} }
public Coin getTxFeePerByte() { public Coin getTxFeePerByte() {
log.info("txFeePerByte = " + feeData.txFeePerByte); if (feeData != null)
return Coin.valueOf(feeData.txFeePerByte); return Coin.valueOf(feeData.txFeePerByte);
else
return Coin.valueOf(DEFAULT_TX_FEE);
} }
public Coin getCreateOfferFee() { // TODO we will get that from the DAO voting
return Coin.valueOf(feeData.createOfferFee); public Coin getCreateOfferFeeInBtc() {
return Coin.valueOf(DEFAULT_CREATE_OFFER_FEE_IN_BTC);
} }
public Coin getTakeOfferFee() { // TODO we will get that from the DAO voting
return Coin.valueOf(feeData.takeOfferFee); public Coin getTakeOfferFeeInBtc() {
return Coin.valueOf(DEFAULT_TAKE_OFFER_FEE_IN_BTC);
} }
public Coin getCreateCompensationRequestFee() { public Coin getCreateCompensationRequestFee() {

View file

@ -73,18 +73,18 @@ abstract public class SquBlockchainService {
String genesisTxId) String genesisTxId)
throws SquBlockchainException { throws SquBlockchainException {
try { try {
log.info("blockCount=" + chainHeadHeight); //log.info("blockCount=" + chainHeadHeight);
long startTs = System.currentTimeMillis(); long startTs = System.currentTimeMillis();
for (int blockHeight = genesisBlockHeight; blockHeight <= chainHeadHeight; blockHeight++) { for (int blockHeight = genesisBlockHeight; blockHeight <= chainHeadHeight; blockHeight++) {
Block block = requestBlock(blockHeight); Block block = requestBlock(blockHeight);
log.info("blockHeight=" + blockHeight); //log.info("blockHeight=" + blockHeight);
parseBlock(new SquBlock(block.getTx(), block.getHeight()), parseBlock(new SquBlock(block.getTx(), block.getHeight()),
genesisBlockHeight, genesisBlockHeight,
genesisTxId, genesisTxId,
utxoByTxIdMap); utxoByTxIdMap);
} }
printUtxoMap(utxoByTxIdMap); printUtxoMap(utxoByTxIdMap);
log.info("Took {} ms", System.currentTimeMillis() - startTs); // log.info("Took {} ms", System.currentTimeMillis() - startTs);
} catch (BitcoindException | CommunicationException e) { } catch (BitcoindException | CommunicationException e) {
throw new SquBlockchainException(e.getMessage(), e); throw new SquBlockchainException(e.getMessage(), e);
} }
@ -157,7 +157,7 @@ abstract public class SquBlockchainService {
for (SquTransaction transaction : connectedTxs) { for (SquTransaction transaction : connectedTxs) {
verifyTransaction(transaction, blockHeight, utxoByTxIdMap); verifyTransaction(transaction, blockHeight, utxoByTxIdMap);
} }
log.info("orphanTxs " + orphanTxs); //log.info("orphanTxs " + orphanTxs);
if (!orphanTxs.isEmpty() && recursions < maxRecursions) if (!orphanTxs.isEmpty() && recursions < maxRecursions)
resolveConnectedTxs(orphanTxs, utxoByTxIdMap, blockHeight, ++recursions, maxRecursions); resolveConnectedTxs(orphanTxs, utxoByTxIdMap, blockHeight, ++recursions, maxRecursions);
} }
@ -286,6 +286,6 @@ abstract public class SquBlockchainService {
.append(a.getValue().toString()).append("}\n"); .append(a.getValue().toString()).append("}\n");
}); });
}); });
log.info(sb.toString()); //log.info(sb.toString());
} }
} }

View file

@ -22,6 +22,7 @@ import io.bitsquare.app.DevFlags;
import io.bitsquare.app.Version; import io.bitsquare.app.Version;
import io.bitsquare.arbitration.Arbitrator; import io.bitsquare.arbitration.Arbitrator;
import io.bitsquare.btc.AddressEntry; import io.bitsquare.btc.AddressEntry;
import io.bitsquare.btc.FeePolicy;
import io.bitsquare.btc.listeners.BalanceListener; import io.bitsquare.btc.listeners.BalanceListener;
import io.bitsquare.btc.provider.fee.FeeService; import io.bitsquare.btc.provider.fee.FeeService;
import io.bitsquare.btc.provider.price.PriceFeedService; import io.bitsquare.btc.provider.price.PriceFeedService;
@ -143,8 +144,8 @@ class CreateOfferDataModel extends ActivatableDataModel {
useMarketBasedPrice.set(preferences.getUsePercentageBasedPrice()); useMarketBasedPrice.set(preferences.getUsePercentageBasedPrice());
// TODO add ui for editing // TODO add ui for editing, use preferences
securityDepositAsCoin = Coin.valueOf(1_000_000); securityDepositAsCoin = FeePolicy.getDefaultSecurityDeposit();
balanceListener = new BalanceListener(getAddressEntry().getAddress()) { balanceListener = new BalanceListener(getAddressEntry().getAddress()) {
@Override @Override
@ -255,14 +256,14 @@ class CreateOfferDataModel extends ActivatableDataModel {
// not too many inputs. // not too many inputs.
// trade fee tx: 226 bytes (1 input) - 374 bytes (2 inputs) // trade fee tx: 226 bytes (1 input) - 374 bytes (2 inputs)
feeService.requestFees(() -> {
createOfferFeeAsCoin = feeService.getCreateOfferFee();
txFeeAsCoin = feeService.getTxFee(400);
calculateTotalToPay();
}, null);
createOfferFeeAsCoin = feeService.getCreateOfferFee(); // Set the default values (in rare cases if the fee request was not done yet we get the hard coded default values)
// But offer creation happens usually after that so we should have already the value from the estimation service.
txFeeAsCoin = feeService.getTxFee(400); txFeeAsCoin = feeService.getTxFee(400);
createOfferFeeAsCoin = feeService.getCreateOfferFeeInBtc();
// We request to get the actual estimated fee
requestTxFee();
calculateVolume(); calculateVolume();
calculateTotalToPay(); calculateTotalToPay();
@ -419,6 +420,13 @@ class CreateOfferDataModel extends ActivatableDataModel {
this.marketPriceMargin = marketPriceMargin; this.marketPriceMargin = marketPriceMargin;
} }
void requestTxFee() {
feeService.requestFees(() -> {
txFeeAsCoin = feeService.getTxFee(400);
createOfferFeeAsCoin = feeService.getCreateOfferFeeInBtc();
calculateTotalToPay();
}, null);
}
/////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////
// Getters // Getters

View file

@ -334,7 +334,7 @@ public class CreateOfferView extends ActivatableViewAndModel<AnchorPane, CreateO
.show(); .show();
key = "createOfferFundWalletInfo"; key = "createOfferFundWalletInfo";
String tradeAmountText = model.isSellOffer() ? "- Trade amount: " + model.tradeAmount.get() + "\n" : ""; String tradeAmountText = model.isSellOffer() ? "- Trade amount: " + model.getTradeAmount() + "\n" : "";
new Popup().headLine("Fund your offer").instruction("You need to deposit " + new Popup().headLine("Fund your offer").instruction("You need to deposit " +
model.totalToPay.get() + " to this offer.\n\n" + model.totalToPay.get() + " to this offer.\n\n" +
@ -345,7 +345,7 @@ public class CreateOfferView extends ActivatableViewAndModel<AnchorPane, CreateO
"The amount is the sum of:\n" + "The amount is the sum of:\n" +
tradeAmountText + tradeAmountText +
"- Security deposit: " + model.getSecurityDeposit() + "\n" + "- Security deposit: " + model.getSecurityDeposit() + "\n" +
"- Trading fee: " + model.getOfferFee() + "\n" + "- Trading fee: " + model.getCreateOfferFee() + "\n" +
"- Mining fee: " + model.getTxFee() + "\n\n" + "- Mining fee: " + model.getTxFee() + "\n\n" +
"You can choose between two options when funding your trade:\n" + "You can choose between two options when funding your trade:\n" +
@ -1078,7 +1078,7 @@ public class CreateOfferView extends ActivatableViewAndModel<AnchorPane, CreateO
addPayInfoEntry(infoGridPane, i++, BSResources.get("createOffer.fundsBox.tradeAmount"), model.tradeAmount.get()); addPayInfoEntry(infoGridPane, i++, BSResources.get("createOffer.fundsBox.tradeAmount"), model.tradeAmount.get());
addPayInfoEntry(infoGridPane, i++, BSResources.get("createOffer.fundsBox.securityDeposit"), model.getSecurityDeposit()); addPayInfoEntry(infoGridPane, i++, BSResources.get("createOffer.fundsBox.securityDeposit"), model.getSecurityDeposit());
addPayInfoEntry(infoGridPane, i++, BSResources.get("createOffer.fundsBox.offerFee"), model.getOfferFee()); addPayInfoEntry(infoGridPane, i++, BSResources.get("createOffer.fundsBox.offerFee"), model.getCreateOfferFee());
addPayInfoEntry(infoGridPane, i++, BSResources.get("createOffer.fundsBox.networkFee"), model.getTxFee()); addPayInfoEntry(infoGridPane, i++, BSResources.get("createOffer.fundsBox.networkFee"), model.getTxFee());
Separator separator = new Separator(); Separator separator = new Separator();
separator.setOrientation(Orientation.HORIZONTAL); separator.setOrientation(Orientation.HORIZONTAL);
@ -1099,6 +1099,7 @@ public class CreateOfferView extends ActivatableViewAndModel<AnchorPane, CreateO
private void addPayInfoEntry(GridPane infoGridPane, int row, String labelText, String value) { private void addPayInfoEntry(GridPane infoGridPane, int row, String labelText, String value) {
Label label = new Label(labelText); Label label = new Label(labelText);
TextField textField = new TextField(value); TextField textField = new TextField(value);
textField.setMinWidth(300);
textField.setEditable(false); textField.setEditable(false);
textField.setFocusTraversable(false); textField.setFocusTraversable(false);
textField.setId("payment-info"); textField.setId("payment-info");

View file

@ -36,6 +36,7 @@ import io.bitsquare.gui.main.overlays.popups.Popup;
import io.bitsquare.gui.main.settings.SettingsView; import io.bitsquare.gui.main.settings.SettingsView;
import io.bitsquare.gui.main.settings.preferences.PreferencesView; import io.bitsquare.gui.main.settings.preferences.PreferencesView;
import io.bitsquare.gui.util.BSFormatter; import io.bitsquare.gui.util.BSFormatter;
import io.bitsquare.gui.util.GUIUtil;
import io.bitsquare.gui.util.validation.BtcValidator; import io.bitsquare.gui.util.validation.BtcValidator;
import io.bitsquare.gui.util.validation.FiatValidator; import io.bitsquare.gui.util.validation.FiatValidator;
import io.bitsquare.gui.util.validation.InputValidator; import io.bitsquare.gui.util.validation.InputValidator;
@ -495,6 +496,7 @@ class CreateOfferViewModel extends ActivatableWithDataModel<CreateOfferDataModel
} }
void onShowPayFundsScreen() { void onShowPayFundsScreen() {
dataModel.requestTxFee();
showPayFundsScreenDisplayed.set(true); showPayFundsScreenDisplayed.set(true);
updateSpinnerInfo(); updateSpinnerInfo();
} }
@ -659,16 +661,23 @@ class CreateOfferViewModel extends ActivatableWithDataModel<CreateOfferDataModel
return dataModel.getTradeCurrency(); return dataModel.getTradeCurrency();
} }
public String getOfferFee() { public String getTradeAmount() {
return formatter.formatCoinWithCode(dataModel.getCreateOfferFeeAsCoin()); return formatter.formatCoinWithCode(dataModel.amount.get());
}
public String getTxFee() {
return formatter.formatCoinWithCode(dataModel.getTxFeeAsCoin());
} }
public String getSecurityDeposit() { public String getSecurityDeposit() {
return formatter.formatCoinWithCode(dataModel.getSecurityDepositAsCoin()); return formatter.formatCoinWithCode(dataModel.getSecurityDepositAsCoin()) +
GUIUtil.getPercentageOfTradeAmount(dataModel.getSecurityDepositAsCoin(), dataModel.amount.get(), formatter);
}
public String getCreateOfferFee() {
return formatter.formatCoinWithCode(dataModel.getCreateOfferFeeAsCoin()) +
GUIUtil.getPercentageOfTradeAmount(dataModel.getCreateOfferFeeAsCoin(), dataModel.amount.get(), formatter);
}
public String getTxFee() {
return formatter.formatCoinWithCode(dataModel.getTxFeeAsCoin()) +
GUIUtil.getPercentageOfTradeAmount(dataModel.getTxFeeAsCoin(), dataModel.amount.get(), formatter);
} }
public PaymentAccount getPaymentAccount() { public PaymentAccount getPaymentAccount() {

View file

@ -174,7 +174,7 @@ class TakeOfferDataModel extends ActivatableDataModel {
// Taker pays 2 times the tx fee because the mining fee might be different when offerer created the offer // Taker pays 2 times the tx fee because the mining fee might be different when offerer created the offer
// and reserved his funds, so that would not work well with dynamic fees. // and reserved his funds, so that would not work well with dynamic fees.
// The mining fee for the takeOfferFee tx is deducted from the createOfferFee and not visible to the trader // The mining fee for the takeOfferFee tx is deducted from the takeOfferFee and not visible to the trader
// The taker pays the mining fee for the trade fee tx and the trade txs. // The taker pays the mining fee for the trade fee tx and the trade txs.
// A typical trade fee tx has about 226 bytes (if one input). The trade txs has about 336-414 bytes. // A typical trade fee tx has about 226 bytes (if one input). The trade txs has about 336-414 bytes.
@ -190,19 +190,17 @@ class TakeOfferDataModel extends ActivatableDataModel {
// trade fee tx: 226 bytes (1 input) - 374 bytes (2 inputs) // trade fee tx: 226 bytes (1 input) - 374 bytes (2 inputs)
// deposit tx: 336 bytes (1 MS output+ OP_RETURN) - 414 bytes (1 MS output + OP_RETURN + change in case of smaller trade amount) // deposit tx: 336 bytes (1 MS output+ OP_RETURN) - 414 bytes (1 MS output + OP_RETURN + change in case of smaller trade amount)
// payout tx: 371 bytes // payout tx: 371 bytes
// disputed payout tx: 408 bytes // disputed payout tx: 408 bytes
feeService.requestFees(() -> {
//TODO update doubleTxFeeAsCoin and txFeeAsCoin in view with binding
takerFeeAsCoin = feeService.getTakeOfferFee();
txFeeAsCoin = feeService.getTxFee(400);
totalTxFeeAsCoin = txFeeAsCoin.multiply(3);
calculateTotalToPay();
}, null);
takerFeeAsCoin = feeService.getTakeOfferFee(); // 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.
takerFeeAsCoin = feeService.getTakeOfferFeeInBtc();
txFeeAsCoin = feeService.getTxFee(400); txFeeAsCoin = feeService.getTxFee(400);
totalTxFeeAsCoin = txFeeAsCoin.multiply(3); totalTxFeeAsCoin = txFeeAsCoin.multiply(3);
// We request to get the actual estimated fee
requestTxFee();
calculateVolume(); calculateVolume();
calculateTotalToPay(); calculateTotalToPay();
@ -244,6 +242,15 @@ class TakeOfferDataModel extends ActivatableDataModel {
priceFeedService.setCurrencyCode(offer.getCurrencyCode()); priceFeedService.setCurrencyCode(offer.getCurrencyCode());
} }
void requestTxFee() {
feeService.requestFees(() -> {
takerFeeAsCoin = feeService.getTakeOfferFeeInBtc();
txFeeAsCoin = feeService.getTxFee(400);
totalTxFeeAsCoin = txFeeAsCoin.multiply(3);
calculateTotalToPay();
}, null);
}
void onTabSelected(boolean isSelected) { void onTabSelected(boolean isSelected) {
this.isTabSelected = isSelected; this.isTabSelected = isSelected;
if (!preferences.getUseStickyMarketPrice() && isTabSelected) if (!preferences.getUseStickyMarketPrice() && isTabSelected)
@ -258,11 +265,12 @@ class TakeOfferDataModel extends ActivatableDataModel {
// errorMessageHandler is used only in the check availability phase. As soon we have a trade we write the error msg in the trade object as we want to // errorMessageHandler is used only in the check availability phase. As soon we have a trade we write the error msg in the trade object as we want to
// have it persisted as well. // have it persisted as well.
void onTakeOffer(TradeResultHandler tradeResultHandler) { void onTakeOffer(TradeResultHandler tradeResultHandler) {
Coin fundsNeededForTrade = totalToPayAsCoin.get().subtract(takerFeeAsCoin).subtract(txFeeAsCoin);
tradeManager.onTakeOffer(amountAsCoin.get(), tradeManager.onTakeOffer(amountAsCoin.get(),
txFeeAsCoin, txFeeAsCoin,
takerFeeAsCoin, takerFeeAsCoin,
tradePrice.getValue(), tradePrice.getValue(),
totalToPayAsCoin.get().subtract(takerFeeAsCoin).subtract(txFeeAsCoin), fundsNeededForTrade,
offer, offer,
paymentAccount.getId(), paymentAccount.getId(),
useSavingsWallet, useSavingsWallet,
@ -460,6 +468,10 @@ class TakeOfferDataModel extends ActivatableDataModel {
return totalTxFeeAsCoin; return totalTxFeeAsCoin;
} }
public Coin getTxFeeAsCoin() {
return txFeeAsCoin;
}
public AddressEntry getAddressEntry() { public AddressEntry getAddressEntry() {
return addressEntry; return addressEntry;
} }

View file

@ -340,7 +340,7 @@ public class TakeOfferView extends ActivatableViewAndModel<AnchorPane, TakeOffer
.show(); .show();
key = "takeOfferFundWalletInfo"; key = "takeOfferFundWalletInfo";
String tradeAmountText = model.isSeller() ? "- Trade amount: " + model.getAmount() + "\n" : ""; String tradeAmountText = model.isSeller() ? "- Trade amount: " + model.getTradeAmount() + "\n" : "";
new Popup().headLine("Fund your trade").instruction("You need to deposit " + new Popup().headLine("Fund your trade").instruction("You need to deposit " +
model.totalToPay.get() + " for taking this offer.\n\n" + model.totalToPay.get() + " for taking this offer.\n\n" +
@ -348,7 +348,7 @@ public class TakeOfferView extends ActivatableViewAndModel<AnchorPane, TakeOffer
tradeAmountText + tradeAmountText +
"- Security deposit: " + model.getSecurityDeposit() + "\n" + "- Security deposit: " + model.getSecurityDeposit() + "\n" +
"- Trading fee: " + model.getTakerFee() + "\n" + "- Trading fee: " + model.getTakerFee() + "\n" +
"- Bitcoin mining fee: " + model.getNetworkFee() + "\n\n" + "- Mining fee (3x): " + model.getTxFee() + "\n\n" +
"You can choose between two options when funding your trade:\n" + "You can choose between two options when funding your trade:\n" +
"- Use your Bitsquare wallet (convenient, but transactions may be linkable) OR\n" + "- Use your Bitsquare wallet (convenient, but transactions may be linkable) OR\n" +
@ -977,11 +977,11 @@ public class TakeOfferView extends ActivatableViewAndModel<AnchorPane, TakeOffer
int i = 0; int i = 0;
if (model.isSeller()) if (model.isSeller())
addPayInfoEntry(infoGridPane, i++, BSResources.get("takeOffer.fundsBox.tradeAmount"), model.getAmount()); addPayInfoEntry(infoGridPane, i++, BSResources.get("takeOffer.fundsBox.tradeAmount"), model.getTradeAmount());
addPayInfoEntry(infoGridPane, i++, BSResources.get("takeOffer.fundsBox.securityDeposit"), model.getSecurityDeposit()); addPayInfoEntry(infoGridPane, i++, BSResources.get("takeOffer.fundsBox.securityDeposit"), model.getSecurityDeposit());
addPayInfoEntry(infoGridPane, i++, BSResources.get("takeOffer.fundsBox.offerFee"), model.getTakerFee()); addPayInfoEntry(infoGridPane, i++, BSResources.get("takeOffer.fundsBox.offerFee"), model.getTakerFee());
addPayInfoEntry(infoGridPane, i++, BSResources.get("takeOffer.fundsBox.networkFee"), model.getNetworkFee()); addPayInfoEntry(infoGridPane, i++, BSResources.get("takeOffer.fundsBox.networkFee"), model.getTxFee());
Separator separator = new Separator(); Separator separator = new Separator();
separator.setOrientation(Orientation.HORIZONTAL); separator.setOrientation(Orientation.HORIZONTAL);
separator.setStyle("-fx-background: #666;"); separator.setStyle("-fx-background: #666;");
@ -1002,6 +1002,7 @@ public class TakeOfferView extends ActivatableViewAndModel<AnchorPane, TakeOffer
private void addPayInfoEntry(GridPane infoGridPane, int row, String labelText, String value) { private void addPayInfoEntry(GridPane infoGridPane, int row, String labelText, String value) {
Label label = new Label(labelText); Label label = new Label(labelText);
TextField textField = new TextField(value); TextField textField = new TextField(value);
textField.setMinWidth(300);
textField.setEditable(false); textField.setEditable(false);
textField.setFocusTraversable(false); textField.setFocusTraversable(false);
textField.setId("payment-info"); textField.setId("payment-info");

View file

@ -27,6 +27,7 @@ import io.bitsquare.gui.main.funds.FundsView;
import io.bitsquare.gui.main.funds.deposit.DepositView; import io.bitsquare.gui.main.funds.deposit.DepositView;
import io.bitsquare.gui.main.overlays.popups.Popup; import io.bitsquare.gui.main.overlays.popups.Popup;
import io.bitsquare.gui.util.BSFormatter; import io.bitsquare.gui.util.BSFormatter;
import io.bitsquare.gui.util.GUIUtil;
import io.bitsquare.gui.util.validation.BtcValidator; import io.bitsquare.gui.util.validation.BtcValidator;
import io.bitsquare.gui.util.validation.InputValidator; import io.bitsquare.gui.util.validation.InputValidator;
import io.bitsquare.locale.BSResources; import io.bitsquare.locale.BSResources;
@ -208,6 +209,7 @@ class TakeOfferViewModel extends ActivatableWithDataModel<TakeOfferDataModel> im
} }
public void onShowPayFundsScreen() { public void onShowPayFundsScreen() {
dataModel.requestTxFee();
showPayFundsScreenDisplayed.set(true); showPayFundsScreenDisplayed.set(true);
updateSpinnerInfo(); updateSpinnerInfo();
} }
@ -569,22 +571,26 @@ class TakeOfferViewModel extends ActivatableWithDataModel<TakeOfferDataModel> im
return amountDescription; return amountDescription;
} }
String getAmount() { String getTradeAmount() {
return formatter.formatCoinWithCode(dataModel.amountAsCoin.get()); return formatter.formatCoinWithCode(dataModel.amountAsCoin.get());
} }
String getTakerFee() {
return formatter.formatCoinWithCode(dataModel.getTakerFeeAsCoin());
}
String getNetworkFee() {
return formatter.formatCoinWithCode(dataModel.getTotalTxFeeAsCoin());
}
public String getSecurityDeposit() { public String getSecurityDeposit() {
return formatter.formatCoinWithCode(dataModel.getSecurityDepositAsCoin()); return formatter.formatCoinWithCode(dataModel.getSecurityDepositAsCoin()) +
GUIUtil.getPercentageOfTradeAmount(dataModel.getSecurityDepositAsCoin(), dataModel.amountAsCoin.get(), formatter);
} }
public String getTakerFee() {
return formatter.formatCoinWithCode(dataModel.getTakerFeeAsCoin()) +
GUIUtil.getPercentageOfTradeAmount(dataModel.getTakerFeeAsCoin(), dataModel.amountAsCoin.get(), formatter);
}
public String getTxFee() {
return formatter.formatCoinWithCode(dataModel.getTotalTxFeeAsCoin()) +
GUIUtil.getPercentageOfTradeAmount(dataModel.getTotalTxFeeAsCoin(), dataModel.amountAsCoin.get(), formatter);
}
public PaymentMethod getPaymentMethod() { public PaymentMethod getPaymentMethod() {
return dataModel.getPaymentMethod(); return dataModel.getPaymentMethod();
} }

View file

@ -18,7 +18,6 @@
package io.bitsquare.gui.main.overlays.windows; package io.bitsquare.gui.main.overlays.windows;
import io.bitsquare.arbitration.DisputeManager; import io.bitsquare.arbitration.DisputeManager;
import io.bitsquare.btc.FeePolicy;
import io.bitsquare.gui.components.TextFieldWithCopyIcon; import io.bitsquare.gui.components.TextFieldWithCopyIcon;
import io.bitsquare.gui.main.MainView; import io.bitsquare.gui.main.MainView;
import io.bitsquare.gui.main.overlays.Overlay; import io.bitsquare.gui.main.overlays.Overlay;
@ -131,7 +130,7 @@ public class TradeDetailsWindow extends Overlay<TradeDetailsWindow> {
addLabelTextField(gridPane, ++rowIndex, "Payment method:", BSResources.get(offer.getPaymentMethod().getId())); addLabelTextField(gridPane, ++rowIndex, "Payment method:", BSResources.get(offer.getPaymentMethod().getId()));
// second group // second group
rows = 5; rows = 6;
PaymentAccountContractData buyerPaymentAccountContractData = null; PaymentAccountContractData buyerPaymentAccountContractData = null;
PaymentAccountContractData sellerPaymentAccountContractData = null; PaymentAccountContractData sellerPaymentAccountContractData = null;
@ -173,6 +172,7 @@ public class TradeDetailsWindow extends Overlay<TradeDetailsWindow> {
addLabelTextFieldWithCopyIcon(gridPane, rowIndex, "Trade ID:", trade.getId(), Layout.FIRST_ROW_AND_GROUP_DISTANCE); addLabelTextFieldWithCopyIcon(gridPane, rowIndex, "Trade ID:", trade.getId(), Layout.FIRST_ROW_AND_GROUP_DISTANCE);
addLabelTextField(gridPane, ++rowIndex, "Trade date:", formatter.formatDateTime(trade.getDate())); addLabelTextField(gridPane, ++rowIndex, "Trade date:", formatter.formatDateTime(trade.getDate()));
addLabelTextField(gridPane, ++rowIndex, "Security deposit:", formatter.formatCoinWithCode(offer.getSecurityDeposit())); addLabelTextField(gridPane, ++rowIndex, "Security deposit:", formatter.formatCoinWithCode(offer.getSecurityDeposit()));
addLabelTextField(gridPane, ++rowIndex, "Tx fee:", formatter.formatCoinWithCode(trade.getTxFee()));
addLabelTextFieldWithCopyIcon(gridPane, ++rowIndex, "Selected arbitrator:", trade.getArbitratorNodeAddress().getFullAddress()); addLabelTextFieldWithCopyIcon(gridPane, ++rowIndex, "Selected arbitrator:", trade.getArbitratorNodeAddress().getFullAddress());
if (trade.getTradingPeerNodeAddress() != null) if (trade.getTradingPeerNodeAddress() != null)

View file

@ -39,6 +39,7 @@ import javafx.stage.DirectoryChooser;
import javafx.stage.FileChooser; import javafx.stage.FileChooser;
import javafx.stage.Stage; import javafx.stage.Stage;
import javafx.util.StringConverter; import javafx.util.StringConverter;
import org.bitcoinj.core.Coin;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@ -300,4 +301,9 @@ public class GUIUtil {
e.printStackTrace(); e.printStackTrace();
} }
} }
public static String getPercentageOfTradeAmount(Coin fee, Coin tradeAmount, BSFormatter formatter) {
return " (" + formatter.formatToPercentWithSymbol((double) fee.value / (double) tradeAmount.value) +
" of trade amount)";
}
} }

View file

@ -180,7 +180,7 @@ takeOffer.fundsBox.sell.info=For every offer there is a dedicated trade wallet.
takeOffer.fundsBox.tradeAmount=Amount to sell: takeOffer.fundsBox.tradeAmount=Amount to sell:
takeOffer.fundsBox.securityDeposit=Security deposit: takeOffer.fundsBox.securityDeposit=Security deposit:
takeOffer.fundsBox.offerFee=Take offer fee: takeOffer.fundsBox.offerFee=Take offer fee:
takeOffer.fundsBox.networkFee=Mining fee: takeOffer.fundsBox.networkFee=Mining fees (3x):
takeOffer.fundsBox.total=Total: takeOffer.fundsBox.total=Total:
takeOffer.fundsBox.showAdvanced=Show advanced settings takeOffer.fundsBox.showAdvanced=Show advanced settings
takeOffer.fundsBox.hideAdvanced=Hide advanced settings takeOffer.fundsBox.hideAdvanced=Hide advanced settings

View file

@ -32,6 +32,7 @@ import java.util.Timer;
import java.util.TimerTask; import java.util.TimerTask;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
//TODO use protobuffer instead of json
public class FeeRequestService { public class FeeRequestService {
private static final Logger log = LoggerFactory.getLogger(FeeRequestService.class); private static final Logger log = LoggerFactory.getLogger(FeeRequestService.class);
@ -47,10 +48,7 @@ public class FeeRequestService {
public FeeRequestService() throws IOException { public FeeRequestService() throws IOException {
btcFeesProvider = new BtcFeesProvider(); btcFeesProvider = new BtcFeesProvider();
allFeesMap.put("txFee", FeeService.DEFAULT_TX_FEE); writeToJson();
allFeesMap.put("createOfferFee", FeeService.DEFAULT_CREATE_OFFER_FEE);
allFeesMap.put("takeOfferFee", FeeService.DEFAULT_TAKE_OFFER_FEE);
startRequests(); startRequests();
} }
@ -101,5 +99,4 @@ public class FeeRequestService {
public String getJson() { public String getJson() {
return json; return json;
} }
} }

View file

@ -12,6 +12,7 @@ import java.io.IOException;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
//TODO use protobuffer instead of json
public class BtcFeesProvider { public class BtcFeesProvider {
private static final Logger log = LoggerFactory.getLogger(BtcFeesProvider.class); private static final Logger log = LoggerFactory.getLogger(BtcFeesProvider.class);
@ -23,7 +24,7 @@ public class BtcFeesProvider {
public Long getFee() throws IOException, HttpException { public Long getFee() throws IOException, HttpException {
String response = httpClient.requestWithGET("recommended", "User-Agent", ""); String response = httpClient.requestWithGET("recommended", "User-Agent", "");
log.debug("Get recommended fee response: " + response); log.info("Get recommended fee response: " + response);
Map<String, Long> map = new HashMap<>(); Map<String, Long> map = new HashMap<>();
LinkedTreeMap<String, Double> treeMap = new Gson().fromJson(response, LinkedTreeMap.class); LinkedTreeMap<String, Double> treeMap = new Gson().fromJson(response, LinkedTreeMap.class);
treeMap.entrySet().stream().forEach(e -> map.put(e.getKey(), e.getValue().longValue())); treeMap.entrySet().stream().forEach(e -> map.put(e.getKey(), e.getValue().longValue()));