Refactor fee handling, add user defined fee to settings, UI fixes

This commit is contained in:
Manfred Karrer 2016-01-21 02:40:49 +01:00
parent 35d6612820
commit f723bf5737
36 changed files with 255 additions and 143 deletions

View file

@ -18,39 +18,67 @@
package io.bitsquare.btc; package io.bitsquare.btc;
import org.bitcoinj.core.Coin; import org.bitcoinj.core.Coin;
import org.bitcoinj.core.Wallet;
public class FeePolicy { public class FeePolicy {
// Official min. fee and fee per kiloByte dropped down to 0.00001 BTC / Coin.valueOf(1000) / 1000 satoshis, but as there are reported problems with // With block getting filled up the needed fee to get fast into a black has become more expensive and less predictable.
// confirmation we use a hgher value. // To see current fees check out:
// The should also help to avoid problems when the tx size is larger as the standard (e.g. if the user does not pay // https://tradeblock.com/blockchain
// in with one transaction but several tx). We don't do a dynamically fee calculation as we need predictable amounts, so that should help to get a larger // http://www.cointape.com
// headroom. // Average values are 10-100 satoshis/byte in january 2016
// Andreas Schildbach reported problems with confirmation and increased the fee/offered UI side fee setting. //
// Our trade transactions have a fixed set of inputs and outputs making the size very predictable
// http://www.cointape.com/ // (as long the user does not do multiple funding transactions)
// The fastest and cheapest transaction fee is currently 50 satoshis/byte, shown in green at the top. //
// For the average transaction size of 597 bytes, this results in a fee of 298 bits (0.298 mBTC). -> 0.0003 BTC or Coin.valueOf(30000);
// trade fee tx: 226 bytes // trade fee tx: 226 bytes
// deposit tx: 336 bytes // deposit tx: 336 bytes
// payout tx: 371 bytes // payout tx: 371 bytes
// disputed payout tx: 408 bytes -> 20400 satoshis with 50 satoshis/byte // disputed payout tx: 408 bytes
// Other good source is: https://tradeblock.com/blockchain 15-100 satoshis/byte // We set a fixed fee to make the needed amounts in the trade predictable.
// We use 0.0003 BTC (0.12 EUR @ 400 EUR/BTC) which is for our tx sizes about 75-150 satoshi/byte
public static final Coin TX_FEE = Coin.valueOf(30000); // 0.0003 BTC about 1.2 EUR @ 400 EUR/BTC: about 100 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
static { // TODO before Beta we should get a good future proof guess as a change causes incompatible versions
Wallet.SendRequest.DEFAULT_FEE_PER_KB = TX_FEE; public static Coin getFixedTxFeeForTrades() {
return Coin.valueOf(30_000);
} }
public static final Coin CREATE_OFFER_FEE = Coin.valueOf(100000); // 0.001 BTC 0.1% of 1 BTC about 0.4 EUR @ 400 EUR/BTC // For non trade transactions (withdrawal) we use the default fee calculation
public static final Coin TAKE_OFFER_FEE = CREATE_OFFER_FEE; // 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 FEE_PER_KB = Coin.valueOf(30_000); // 0.0003 BTC about 0.12 EUR @ 400 EUR/BTC
// TODO make final again later public static void setFeePerKb(Coin feePerKb) {
public static Coin SECURITY_DEPOSIT = Coin.valueOf(10000000); // 0.1 BTC; about 4 EUR @ 400 EUR/BTC FEE_PER_KB = feePerKb;
}
public static Coin getFeePerKb() {
return FEE_PER_KB;
}
// 0.001 BTC 0.1% of 1 BTC about 0.4 EUR @ 400 EUR/BTC
public static Coin getCreateOfferFee() {
return Coin.valueOf(100_000);
}
// Currently we use the same fee for both offerer and taker
public static Coin getTakeOfferFee() {
return getCreateOfferFee();
}
// TODO make final again later 100_000_000
// 0.1 BTC; about 4 EUR @ 400 EUR/BTC
private static Coin SECURITY_DEPOSIT = Coin.valueOf(10_000_000);
public static Coin getSecurityDeposit() {
return SECURITY_DEPOSIT;
}
// Called from WalletService to reduce SECURITY_DEPOSIT for mainnet to 0.01 btc // Called from WalletService to reduce SECURITY_DEPOSIT for mainnet to 0.01 btc
// TODO remove later when tested enough // TODO remove later when tested enough

View file

@ -34,7 +34,11 @@ public class Restrictions {
MAX_TRADE_AMOUNT = maxTradeAmount; MAX_TRADE_AMOUNT = maxTradeAmount;
} }
public static boolean isMinSpendableAmount(Coin amount) { public static boolean isAboveFixedTxFeeAndDust(Coin amount) {
return amount != null && amount.compareTo(FeePolicy.TX_FEE.add(Transaction.MIN_NONDUST_OUTPUT)) > 0; return amount != null && amount.compareTo(FeePolicy.getFixedTxFeeForTrades().add(Transaction.MIN_NONDUST_OUTPUT)) > 0;
}
public static boolean isAboveDust(Coin amount) {
return amount != null && amount.compareTo(Transaction.MIN_NONDUST_OUTPUT) > 0;
} }
} }

View file

@ -142,9 +142,9 @@ public class TradeWalletService {
public Transaction createTradingFeeTx(AddressEntry addressEntry, Coin tradingFee, String feeReceiverAddresses) public Transaction createTradingFeeTx(AddressEntry addressEntry, Coin tradingFee, String feeReceiverAddresses)
throws InsufficientMoneyException, AddressFormatException { throws InsufficientMoneyException, AddressFormatException {
Transaction tradingFeeTx = new Transaction(params); Transaction tradingFeeTx = new Transaction(params);
Preconditions.checkArgument(Restrictions.isMinSpendableAmount(tradingFee), Preconditions.checkArgument(Restrictions.isAboveFixedTxFeeAndDust(tradingFee),
"You cannot send an amount which are smaller than the fee + dust output."); "You cannot send an amount which are smaller than the fee + dust output.");
Coin outPutAmount = tradingFee.subtract(FeePolicy.TX_FEE); Coin outPutAmount = tradingFee.subtract(FeePolicy.getFixedTxFeeForTrades());
tradingFeeTx.addOutput(outPutAmount, new Address(params, feeReceiverAddresses)); tradingFeeTx.addOutput(outPutAmount, new Address(params, feeReceiverAddresses));
// we allow spending of unconfirmed tx (double spend risk is low and usability would suffer if we need to // we allow spending of unconfirmed tx (double spend risk is low and usability would suffer if we need to
@ -154,14 +154,13 @@ public class TradeWalletService {
sendRequest.shuffleOutputs = false; sendRequest.shuffleOutputs = false;
sendRequest.aesKey = aesKey; sendRequest.aesKey = aesKey;
sendRequest.coinSelector = new AddressBasedCoinSelector(params, addressEntry); sendRequest.coinSelector = new AddressBasedCoinSelector(params, addressEntry);
// We use a fixed fee
sendRequest.feePerKb = Coin.ZERO;
sendRequest.fee = FeePolicy.getFixedTxFeeForTrades();
// We use always the same address for all transactions in a trade to keep things simple. // We use always the same address for all transactions in a trade to keep things simple.
// To be discussed if that introduce any privacy issues. // To be discussed if that introduce any privacy issues.
sendRequest.changeAddress = addressEntry.getAddress(); sendRequest.changeAddress = addressEntry.getAddress();
// Wallet.SendRequest.DEFAULT_FEE_PER_KB is set in FeePolicy to our defined amount
// We don't want to risk delayed transactions so we set the fee rather high.
// Delayed tx will lead to a broken chain of the deposit transaction.
wallet.completeTx(sendRequest); wallet.completeTx(sendRequest);
printTxWithInputs("tradingFeeTx", tradingFeeTx); printTxWithInputs("tradingFeeTx", tradingFeeTx);
@ -212,7 +211,7 @@ public class TradeWalletService {
*/ */
// inputAmount includes the tx fee. So we subtract the fee to get the dummyOutputAmount. // inputAmount includes the tx fee. So we subtract the fee to get the dummyOutputAmount.
Coin dummyOutputAmount = inputAmount.subtract(FeePolicy.TX_FEE); Coin dummyOutputAmount = inputAmount.subtract(FeePolicy.getFixedTxFeeForTrades());
Transaction dummyTX = new Transaction(params); Transaction dummyTX = new Transaction(params);
// The output is just used to get the right inputs and change outputs, so we use an anonymous ECKey, as it will never be used for anything. // The output is just used to get the right inputs and change outputs, so we use an anonymous ECKey, as it will never be used for anything.
@ -303,7 +302,7 @@ public class TradeWalletService {
// First we construct a dummy TX to get the inputs and outputs we want to use for the real deposit tx. // First we construct a dummy TX to get the inputs and outputs we want to use for the real deposit tx.
// Similar to the way we did in the createTakerDepositTxInputs method. // Similar to the way we did in the createTakerDepositTxInputs method.
Transaction dummyTx = new Transaction(params); Transaction dummyTx = new Transaction(params);
Coin dummyOutputAmount = offererInputAmount.subtract(FeePolicy.TX_FEE); Coin dummyOutputAmount = offererInputAmount.subtract(FeePolicy.getFixedTxFeeForTrades());
TransactionOutput dummyOutput = new TransactionOutput(params, dummyTx, dummyOutputAmount, new ECKey().toAddress(params)); TransactionOutput dummyOutput = new TransactionOutput(params, dummyTx, dummyOutputAmount, new ECKey().toAddress(params));
dummyTx.addOutput(dummyOutput); dummyTx.addOutput(dummyOutput);
addAvailableInputsAndChangeOutputs(dummyTx, offererAddressInfo); addAvailableInputsAndChangeOutputs(dummyTx, offererAddressInfo);
@ -640,7 +639,6 @@ public class TradeWalletService {
return payoutTx; return payoutTx;
} }
/////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////
// Dispute // Dispute
/////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////
@ -694,12 +692,15 @@ public class TradeWalletService {
preparedPayoutTx.addOutput(buyerPayoutAmount, new Address(params, buyerAddressString)); preparedPayoutTx.addOutput(buyerPayoutAmount, new Address(params, buyerAddressString));
if (sellerPayoutAmount.isGreaterThan(Coin.ZERO)) if (sellerPayoutAmount.isGreaterThan(Coin.ZERO))
preparedPayoutTx.addOutput(sellerPayoutAmount, new Address(params, sellerAddressString)); preparedPayoutTx.addOutput(sellerPayoutAmount, new Address(params, sellerAddressString));
if (arbitratorPayoutAmount.isGreaterThan(Coin.ZERO)) if (arbitratorPayoutAmount.isGreaterThan(Coin.ZERO) && arbitratorAddressEntry.getAddressString() != null)
preparedPayoutTx.addOutput(arbitratorPayoutAmount, new Address(params, arbitratorAddressEntry.getAddressString())); preparedPayoutTx.addOutput(arbitratorPayoutAmount, new Address(params, arbitratorAddressEntry.getAddressString()));
// take care of sorting! // take care of sorting!
Script redeemScript = getMultiSigRedeemScript(buyerPubKey, sellerPubKey, arbitratorPubKey); Script redeemScript = getMultiSigRedeemScript(buyerPubKey, sellerPubKey, arbitratorPubKey);
Sha256Hash sigHash = preparedPayoutTx.hashForSignature(0, redeemScript, Transaction.SigHash.ALL, false); Sha256Hash sigHash = preparedPayoutTx.hashForSignature(0, redeemScript, Transaction.SigHash.ALL, false);
if (arbitratorAddressEntry.getKeyPair() == null)
throw new RuntimeException("Unexpected null value: arbitratorAddressEntry.getKeyPair() must not be null");
ECKey.ECDSASignature arbitratorSignature = arbitratorAddressEntry.getKeyPair().sign(sigHash, aesKey).toCanonicalised(); ECKey.ECDSASignature arbitratorSignature = arbitratorAddressEntry.getKeyPair().sign(sigHash, aesKey).toCanonicalised();
verifyTransaction(preparedPayoutTx); verifyTransaction(preparedPayoutTx);
@ -1026,6 +1027,9 @@ public class TradeWalletService {
Wallet.SendRequest sendRequest = Wallet.SendRequest.forTx(transaction); Wallet.SendRequest sendRequest = Wallet.SendRequest.forTx(transaction);
sendRequest.shuffleOutputs = false; sendRequest.shuffleOutputs = false;
sendRequest.aesKey = aesKey; sendRequest.aesKey = aesKey;
// We use a fixed fee
sendRequest.feePerKb = Coin.ZERO;
sendRequest.fee = FeePolicy.getFixedTxFeeForTrades();
// we allow spending of unconfirmed tx (double spend risk is low and usability would suffer if we need to wait for 1 confirmation) // we allow spending of unconfirmed tx (double spend risk is low and usability would suffer if we need to wait for 1 confirmation)
sendRequest.coinSelector = new AddressBasedCoinSelector(params, addressEntry); sendRequest.coinSelector = new AddressBasedCoinSelector(params, addressEntry);
// We use always the same address in a trade for all transactions // We use always the same address in a trade for all transactions
@ -1052,4 +1056,5 @@ public class TradeWalletService {
} }
return balance; return balance;
}*/ }*/
} }

View file

@ -438,15 +438,29 @@ public class WalletService {
// Withdrawal // Withdrawal
/////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////
public String sendFunds(String fromAddress, public Coin getRequiredFee(String fromAddress,
String toAddress, String toAddress,
Coin amount, Coin amount) throws AddressFormatException, IllegalArgumentException, InsufficientMoneyException {
KeyParameter aesKey, Coin fee;
FutureCallback<Transaction> callback) throws AddressFormatException, IllegalArgumentException, InsufficientMoneyException { try {
wallet.completeTx(getSendRequest(fromAddress, toAddress, amount, null));
fee = Coin.ZERO;
} 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;
}
public Wallet.SendRequest getSendRequest(String fromAddress,
String toAddress,
Coin amount,
@Nullable KeyParameter aesKey) throws AddressFormatException, IllegalArgumentException, InsufficientMoneyException {
Transaction tx = new Transaction(params); Transaction tx = new Transaction(params);
Preconditions.checkArgument(Restrictions.isMinSpendableAmount(amount), Preconditions.checkArgument(Restrictions.isAboveDust(amount),
"You cannot send an amount which are smaller than the fee + dust output."); "You cannot send an amount which are smaller than 546 satoshis.");
tx.addOutput(amount.subtract(FeePolicy.TX_FEE), new Address(params, toAddress)); tx.addOutput(amount, new Address(params, toAddress));
Wallet.SendRequest sendRequest = Wallet.SendRequest.forTx(tx); Wallet.SendRequest sendRequest = Wallet.SendRequest.forTx(tx);
sendRequest.aesKey = aesKey; sendRequest.aesKey = aesKey;
@ -457,13 +471,21 @@ public class WalletService {
sendRequest.coinSelector = new AddressBasedCoinSelector(params, addressEntry.get()); sendRequest.coinSelector = new AddressBasedCoinSelector(params, addressEntry.get());
sendRequest.changeAddress = addressEntry.get().getAddress(); sendRequest.changeAddress = addressEntry.get().getAddress();
Wallet.SendResult sendResult = wallet.sendCoins(sendRequest); sendRequest.feePerKb = FeePolicy.getFeePerKb();
return sendRequest;
}
public String sendFunds(String fromAddress,
String toAddress,
Coin amount,
KeyParameter aesKey,
FutureCallback<Transaction> callback) throws AddressFormatException, IllegalArgumentException, InsufficientMoneyException {
Coin fee = getRequiredFee(fromAddress, toAddress, amount);
Wallet.SendResult sendResult = wallet.sendCoins(getSendRequest(fromAddress, toAddress, amount.subtract(fee), aesKey));
Futures.addCallback(sendResult.broadcastComplete, callback); Futures.addCallback(sendResult.broadcastComplete, callback);
printTxWithInputs("sendFunds", tx); printTxWithInputs("sendFunds", sendResult.tx);
log.debug("tx=" + tx); return sendResult.tx.getHashAsString();
return tx.getHashAsString();
} }
public void emptyWallet(String toAddress, KeyParameter aesKey, ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) public void emptyWallet(String toAddress, KeyParameter aesKey, ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler)
@ -471,6 +493,7 @@ public class WalletService {
Wallet.SendRequest sendRequest = Wallet.SendRequest.emptyWallet(new Address(params, toAddress)); Wallet.SendRequest sendRequest = Wallet.SendRequest.emptyWallet(new Address(params, toAddress));
sendRequest.aesKey = aesKey; sendRequest.aesKey = aesKey;
Wallet.SendResult sendResult = wallet.sendCoins(sendRequest); Wallet.SendResult sendResult = wallet.sendCoins(sendRequest);
sendRequest.feePerKb = FeePolicy.getFeePerKb();
Futures.addCallback(sendResult.broadcastComplete, new FutureCallback<Transaction>() { Futures.addCallback(sendResult.broadcastComplete, new FutureCallback<Transaction>() {
@Override @Override
public void onSuccess(Transaction result) { public void onSuccess(Transaction result) {

View file

@ -79,7 +79,7 @@ public class BuyerAsOffererTrade extends BuyerTrade implements OffererTrade, Ser
public Coin getPayoutAmount() { public Coin getPayoutAmount() {
checkNotNull(getTradeAmount(), "Invalid state: getTradeAmount() = null"); checkNotNull(getTradeAmount(), "Invalid state: getTradeAmount() = null");
return FeePolicy.SECURITY_DEPOSIT.add(getTradeAmount()); return FeePolicy.getSecurityDeposit().add(getTradeAmount());
} }
} }

View file

@ -80,6 +80,6 @@ public class BuyerAsTakerTrade extends BuyerTrade implements TakerTrade, Seriali
public Coin getPayoutAmount() { public Coin getPayoutAmount() {
checkNotNull(getTradeAmount(), "Invalid state: getTradeAmount() = null"); checkNotNull(getTradeAmount(), "Invalid state: getTradeAmount() = null");
return FeePolicy.SECURITY_DEPOSIT.add(getTradeAmount()); return FeePolicy.getSecurityDeposit().add(getTradeAmount());
} }
} }

View file

@ -368,7 +368,7 @@ abstract public class Trade implements Tradable, Model, Serializable {
} }
public Coin getPayoutAmount() { public Coin getPayoutAmount() {
return FeePolicy.SECURITY_DEPOSIT; return FeePolicy.getSecurityDeposit();
} }
public ProcessModel getProcessModel() { public ProcessModel getProcessModel() {

View file

@ -131,7 +131,6 @@ public class TradeManager {
log.trace("onMailboxMessageAdded senderAddress: " + senderAddress); log.trace("onMailboxMessageAdded senderAddress: " + senderAddress);
Message message = decryptedMsgWithPubKey.message; Message message = decryptedMsgWithPubKey.message;
if (message instanceof PayDepositRequest) { if (message instanceof PayDepositRequest) {
//TODO is that used????
PayDepositRequest payDepositRequest = (PayDepositRequest) message; PayDepositRequest payDepositRequest = (PayDepositRequest) message;
log.trace("Received payDepositRequest: " + payDepositRequest); log.trace("Received payDepositRequest: " + payDepositRequest);
if (payDepositRequest.getSenderAddress().equals(senderAddress)) if (payDepositRequest.getSenderAddress().equals(senderAddress))
@ -163,12 +162,6 @@ public class TradeManager {
// Lifecycle // Lifecycle
/////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////
// When all services are initialized we create the protocols for our open offers and persisted pendingTrades
// OffererAsBuyerProtocol listens for take offer requests, so we need to instantiate it early.
public void onAllServicesInitialized() {
log.trace("onAllServicesInitialized");
}
private void initPendingTrades() { private void initPendingTrades() {
if (firstPeerAuthenticatedListener != null) p2PService.removeP2PServiceListener(firstPeerAuthenticatedListener); if (firstPeerAuthenticatedListener != null) p2PService.removeP2PServiceListener(firstPeerAuthenticatedListener);

View file

@ -44,7 +44,7 @@ public class BroadcastCreateOfferFeeTx extends Task<PlaceOfferModel> {
protected void run() { protected void run() {
try { try {
runInterceptHook(); runInterceptHook();
Coin totalsNeeded = FeePolicy.SECURITY_DEPOSIT.add(FeePolicy.CREATE_OFFER_FEE).add(FeePolicy.TX_FEE); Coin totalsNeeded = FeePolicy.getSecurityDeposit().add(FeePolicy.getCreateOfferFee()).add(FeePolicy.getFixedTxFeeForTrades());
AddressEntry addressEntry = model.walletService.getAddressEntryByOfferId(model.offer.getId()); AddressEntry addressEntry = model.walletService.getAddressEntryByOfferId(model.offer.getId());
Coin balance = model.walletService.getBalanceForAddress(addressEntry.getAddress()); Coin balance = model.walletService.getBalanceForAddress(addressEntry.getAddress());
if (balance.compareTo(totalsNeeded) >= 0) { if (balance.compareTo(totalsNeeded) >= 0) {

View file

@ -45,7 +45,7 @@ public class CreateOfferFeeTx extends Task<PlaceOfferModel> {
Arbitrator selectedArbitrator = model.user.getAcceptedArbitratorByAddress(selectedArbitratorAddress); Arbitrator selectedArbitrator = model.user.getAcceptedArbitratorByAddress(selectedArbitratorAddress);
Transaction transaction = model.tradeWalletService.createTradingFeeTx( Transaction transaction = model.tradeWalletService.createTradingFeeTx(
model.walletService.getAddressEntryByOfferId(model.offer.getId()), model.walletService.getAddressEntryByOfferId(model.offer.getId()),
FeePolicy.CREATE_OFFER_FEE, FeePolicy.getCreateOfferFee(),
selectedArbitrator.getBtcAddress()); selectedArbitrator.getBtcAddress());
// We assume there will be no tx malleability. We add a check later in case the published offer has a different hash. // We assume there will be no tx malleability. We add a check later in case the published offer has a different hash.

View file

@ -41,8 +41,8 @@ public class CreateAndSignDepositTxAsBuyer extends TradeTask {
try { try {
runInterceptHook(); runInterceptHook();
checkNotNull(trade.getTradeAmount(), "trade.getTradeAmount() must not be null"); checkNotNull(trade.getTradeAmount(), "trade.getTradeAmount() must not be null");
Coin buyerInputAmount = FeePolicy.SECURITY_DEPOSIT.add(FeePolicy.TX_FEE); Coin buyerInputAmount = FeePolicy.getSecurityDeposit().add(FeePolicy.getFixedTxFeeForTrades());
Coin msOutputAmount = buyerInputAmount.add(FeePolicy.SECURITY_DEPOSIT).add(trade.getTradeAmount()); Coin msOutputAmount = buyerInputAmount.add(FeePolicy.getSecurityDeposit()).add(trade.getTradeAmount());
log.info("\n\n------------------------------------------------------------\n" log.info("\n\n------------------------------------------------------------\n"
+ "Contract as json\n" + "Contract as json\n"

View file

@ -37,7 +37,7 @@ public class CreateDepositTxInputsAsBuyer extends TradeTask {
protected void run() { protected void run() {
try { try {
runInterceptHook(); runInterceptHook();
Coin takerInputAmount = FeePolicy.SECURITY_DEPOSIT.add(FeePolicy.TX_FEE); Coin takerInputAmount = FeePolicy.getSecurityDeposit().add(FeePolicy.getFixedTxFeeForTrades());
InputsAndChangeOutput result = processModel.getTradeWalletService().takerCreatesDepositsTxInputs(takerInputAmount, processModel.getAddressEntry()); InputsAndChangeOutput result = processModel.getTradeWalletService().takerCreatesDepositsTxInputs(takerInputAmount, processModel.getAddressEntry());
processModel.setRawInputs(result.rawInputs); processModel.setRawInputs(result.rawInputs);
processModel.setChangeOutputValue(result.changeOutputValue); processModel.setChangeOutputValue(result.changeOutputValue);

View file

@ -40,7 +40,7 @@ public class SignAndFinalizePayoutTx extends TradeTask {
try { try {
runInterceptHook(); runInterceptHook();
checkNotNull(trade.getTradeAmount(), "trade.getTradeAmount() must not be null"); checkNotNull(trade.getTradeAmount(), "trade.getTradeAmount() must not be null");
Coin sellerPayoutAmount = FeePolicy.SECURITY_DEPOSIT; Coin sellerPayoutAmount = FeePolicy.getSecurityDeposit();
Coin buyerPayoutAmount = sellerPayoutAmount.add(trade.getTradeAmount()); Coin buyerPayoutAmount = sellerPayoutAmount.add(trade.getTradeAmount());
Transaction transaction = processModel.getTradeWalletService().buyerSignsAndFinalizesPayoutTx( Transaction transaction = processModel.getTradeWalletService().buyerSignsAndFinalizesPayoutTx(

View file

@ -41,8 +41,8 @@ public class CreateAndSignDepositTxAsSeller extends TradeTask {
try { try {
runInterceptHook(); runInterceptHook();
checkNotNull(trade.getTradeAmount(), "trade.getTradeAmount() must not be null"); checkNotNull(trade.getTradeAmount(), "trade.getTradeAmount() must not be null");
Coin sellerInputAmount = FeePolicy.SECURITY_DEPOSIT.add(FeePolicy.TX_FEE).add(trade.getTradeAmount()); Coin sellerInputAmount = FeePolicy.getSecurityDeposit().add(FeePolicy.getFixedTxFeeForTrades()).add(trade.getTradeAmount());
Coin msOutputAmount = sellerInputAmount.add(FeePolicy.SECURITY_DEPOSIT); Coin msOutputAmount = sellerInputAmount.add(FeePolicy.getSecurityDeposit());
log.info("\n\n------------------------------------------------------------\n" log.info("\n\n------------------------------------------------------------\n"
+ "Contract as json\n" + "Contract as json\n"

View file

@ -38,7 +38,7 @@ public class CreateDepositTxInputsAsSeller extends TradeTask {
try { try {
runInterceptHook(); runInterceptHook();
if (trade.getTradeAmount() != null) { if (trade.getTradeAmount() != null) {
Coin takerInputAmount = FeePolicy.SECURITY_DEPOSIT.add(FeePolicy.TX_FEE).add(trade.getTradeAmount()); Coin takerInputAmount = FeePolicy.getSecurityDeposit().add(FeePolicy.getFixedTxFeeForTrades()).add(trade.getTradeAmount());
InputsAndChangeOutput result = processModel.getTradeWalletService().takerCreatesDepositsTxInputs(takerInputAmount, processModel InputsAndChangeOutput result = processModel.getTradeWalletService().takerCreatesDepositsTxInputs(takerInputAmount, processModel
.getAddressEntry()); .getAddressEntry());

View file

@ -39,7 +39,7 @@ public class SignPayoutTx extends TradeTask {
try { try {
runInterceptHook(); runInterceptHook();
checkNotNull(trade.getTradeAmount(), "trade.getTradeAmount() must not be null"); checkNotNull(trade.getTradeAmount(), "trade.getTradeAmount() must not be null");
Coin sellerPayoutAmount = FeePolicy.SECURITY_DEPOSIT; Coin sellerPayoutAmount = FeePolicy.getSecurityDeposit();
Coin buyerPayoutAmount = sellerPayoutAmount.add(trade.getTradeAmount()); Coin buyerPayoutAmount = sellerPayoutAmount.add(trade.getTradeAmount());
// We use the sellers LastBlockSeenHeight, which might be different to the buyers one. // We use the sellers LastBlockSeenHeight, which might be different to the buyers one.

View file

@ -47,7 +47,7 @@ public class CreateTakeOfferFeeTx extends TradeTask {
Arbitrator selectedArbitrator = user.getAcceptedArbitratorByAddress(selectedArbitratorAddress); Arbitrator selectedArbitrator = user.getAcceptedArbitratorByAddress(selectedArbitratorAddress);
Transaction createTakeOfferFeeTx = processModel.getTradeWalletService().createTradingFeeTx( Transaction createTakeOfferFeeTx = processModel.getTradeWalletService().createTradingFeeTx(
processModel.getAddressEntry(), processModel.getAddressEntry(),
FeePolicy.TAKE_OFFER_FEE, FeePolicy.getTakeOfferFee(),
selectedArbitrator.getBtcAddress()); selectedArbitrator.getBtcAddress());
processModel.setTakeOfferFeeTx(createTakeOfferFeeTx); processModel.setTakeOfferFeeTx(createTakeOfferFeeTx);

View file

@ -20,6 +20,7 @@ package io.bitsquare.user;
import io.bitsquare.app.BitsquareEnvironment; import io.bitsquare.app.BitsquareEnvironment;
import io.bitsquare.app.Version; import io.bitsquare.app.Version;
import io.bitsquare.btc.BitcoinNetwork; import io.bitsquare.btc.BitcoinNetwork;
import io.bitsquare.btc.FeePolicy;
import io.bitsquare.locale.CountryUtil; import io.bitsquare.locale.CountryUtil;
import io.bitsquare.locale.CurrencyUtil; import io.bitsquare.locale.CurrencyUtil;
import io.bitsquare.locale.TradeCurrency; import io.bitsquare.locale.TradeCurrency;
@ -31,6 +32,8 @@ import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty; import javafx.beans.property.StringProperty;
import javafx.collections.FXCollections; import javafx.collections.FXCollections;
import javafx.collections.ObservableList; import javafx.collections.ObservableList;
import org.bitcoinj.core.Coin;
import org.bitcoinj.core.Transaction;
import org.bitcoinj.utils.MonetaryFormat; import org.bitcoinj.utils.MonetaryFormat;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@ -100,6 +103,7 @@ public class Preferences implements Serializable {
private boolean tacAccepted; private boolean tacAccepted;
private Locale preferredLocale; private Locale preferredLocale;
private TradeCurrency preferredTradeCurrency; private TradeCurrency preferredTradeCurrency;
private long txFeePerKB = FeePolicy.getFeePerKb().value;
// Observable wrappers // Observable wrappers
transient private final StringProperty btcDenominationProperty = new SimpleStringProperty(btcDenomination); transient private final StringProperty btcDenominationProperty = new SimpleStringProperty(btcDenomination);
@ -146,6 +150,11 @@ public class Preferences implements Serializable {
defaultLocale = preferredLocale; defaultLocale = preferredLocale;
preferredTradeCurrency = persisted.getPreferredTradeCurrency(); preferredTradeCurrency = persisted.getPreferredTradeCurrency();
defaultTradeCurrency = preferredTradeCurrency; defaultTradeCurrency = preferredTradeCurrency;
try {
setTxFeePerKB(persisted.getTxFeePerKB());
} catch (Exception e) {
// leave default value
}
} else { } else {
setTradeCurrencies(CurrencyUtil.getAllSortedCurrencies()); setTradeCurrencies(CurrencyUtil.getAllSortedCurrencies());
tradeCurrencies = new ArrayList<>(tradeCurrenciesAsObservable); tradeCurrencies = new ArrayList<>(tradeCurrenciesAsObservable);
@ -162,6 +171,7 @@ public class Preferences implements Serializable {
preferredLocale = getDefaultLocale(); preferredLocale = getDefaultLocale();
preferredTradeCurrency = getDefaultTradeCurrency(); preferredTradeCurrency = getDefaultTradeCurrency();
storage.queueUpForSave(); storage.queueUpForSave();
} }
@ -272,6 +282,14 @@ public class Preferences implements Serializable {
storage.queueUpForSave(); storage.queueUpForSave();
} }
public void setTxFeePerKB(long txFeePerKB) throws Exception {
if (txFeePerKB < Transaction.REFERENCE_DEFAULT_MIN_TX_FEE.value)
throw new Exception("Transaction fee must be at least 5 satoshi/byte");
this.txFeePerKB = txFeePerKB;
FeePolicy.setFeePerKb(Coin.valueOf(txFeePerKB));
storage.queueUpForSave();
}
/////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////
// Getter // Getter
@ -387,4 +405,8 @@ public class Preferences implements Serializable {
public TradeCurrency getPreferredTradeCurrency() { public TradeCurrency getPreferredTradeCurrency() {
return preferredTradeCurrency; return preferredTradeCurrency;
} }
public long getTxFeePerKB() {
return Math.max(Transaction.REFERENCE_DEFAULT_MIN_TX_FEE.value, txFeePerKB);
}
} }

View file

@ -28,21 +28,21 @@ public class RestrictionsTest {
@Test @Test
public void testIsMinSpendableAmount() { public void testIsMinSpendableAmount() {
Coin amount = null; Coin amount = null;
assertFalse("tx unfunded, pending", Restrictions.isMinSpendableAmount(amount)); assertFalse("tx unfunded, pending", Restrictions.isAboveFixedTxFeeAndDust(amount));
amount = Coin.ZERO; amount = Coin.ZERO;
assertFalse("tx unfunded, pending", Restrictions.isMinSpendableAmount(amount)); assertFalse("tx unfunded, pending", Restrictions.isAboveFixedTxFeeAndDust(amount));
amount = FeePolicy.TX_FEE; amount = FeePolicy.getFixedTxFeeForTrades();
assertFalse("tx unfunded, pending", Restrictions.isMinSpendableAmount(amount)); assertFalse("tx unfunded, pending", Restrictions.isAboveFixedTxFeeAndDust(amount));
amount = Transaction.MIN_NONDUST_OUTPUT; amount = Transaction.MIN_NONDUST_OUTPUT;
assertFalse("tx unfunded, pending", Restrictions.isMinSpendableAmount(amount)); assertFalse("tx unfunded, pending", Restrictions.isAboveFixedTxFeeAndDust(amount));
amount = FeePolicy.TX_FEE.add(Transaction.MIN_NONDUST_OUTPUT); amount = FeePolicy.getFixedTxFeeForTrades().add(Transaction.MIN_NONDUST_OUTPUT);
assertFalse("tx unfunded, pending", Restrictions.isMinSpendableAmount(amount)); assertFalse("tx unfunded, pending", Restrictions.isAboveFixedTxFeeAndDust(amount));
amount = FeePolicy.TX_FEE.add(Transaction.MIN_NONDUST_OUTPUT).add(Coin.valueOf(1)); amount = FeePolicy.getFixedTxFeeForTrades().add(Transaction.MIN_NONDUST_OUTPUT).add(Coin.valueOf(1));
assertTrue("tx unfunded, pending", Restrictions.isMinSpendableAmount(amount)); assertTrue("tx unfunded, pending", Restrictions.isAboveFixedTxFeeAndDust(amount));
} }
} }

View file

@ -195,7 +195,7 @@ public class MainView extends InitializableView<StackPane, MainViewModel> {
private Tuple2<TextField, VBox> getBalanceBox(String text) { private Tuple2<TextField, VBox> getBalanceBox(String text) {
TextField textField = new TextField(); TextField textField = new TextField();
textField.setEditable(false); textField.setEditable(false);
textField.setPrefWidth(100); textField.setPrefWidth(120);
textField.setMouseTransparent(true); textField.setMouseTransparent(true);
textField.setFocusTraversable(false); textField.setFocusTraversable(false);
textField.setStyle("-fx-alignment: center; -fx-background-color: white;"); textField.setStyle("-fx-alignment: center; -fx-background-color: white;");

View file

@ -313,7 +313,6 @@ public class MainViewModel implements ViewModel {
}); });
pendingTradesChanged(); pendingTradesChanged();
addDisputeStateListeners(tradeManager.getTrades()); addDisputeStateListeners(tradeManager.getTrades());
tradeManager.onAllServicesInitialized();
// arbitratorManager // arbitratorManager

View file

@ -406,7 +406,7 @@ public class DisputeSummaryPopup extends Popup {
private void calculatePayoutAmounts(DisputeResult.FeePaymentPolicy feePayment) { private void calculatePayoutAmounts(DisputeResult.FeePaymentPolicy feePayment) {
Contract contract = dispute.getContract(); Contract contract = dispute.getContract();
Coin refund = FeePolicy.SECURITY_DEPOSIT; Coin refund = FeePolicy.getSecurityDeposit();
Coin winnerRefund; Coin winnerRefund;
Coin loserRefund; Coin loserRefund;
switch (feePayment) { switch (feePayment) {

View file

@ -105,28 +105,29 @@ public class ReservedListItem {
switch (phase) { switch (phase) {
case PREPARATION: case PREPARATION:
case TAKER_FEE_PAID: case TAKER_FEE_PAID:
balanceLabel.setText(formatter.formatCoinWithCode(balance) + " locked in deposit"); balanceLabel.setText(formatter.formatCoinWithCode(balance) + " (locally reserved)");
break; break;
case DEPOSIT_REQUESTED: case DEPOSIT_REQUESTED:
case DEPOSIT_PAID: case DEPOSIT_PAID:
case FIAT_SENT: case FIAT_SENT:
case FIAT_RECEIVED: case FIAT_RECEIVED:
// We ignore the tx fee as it will be paid by both (once deposit, once payout) // We ignore the tx fee as it will be paid by both (once deposit, once payout)
Coin balanceInDeposit = FeePolicy.SECURITY_DEPOSIT; Coin balanceInDeposit = FeePolicy.getSecurityDeposit();
// For the seller we add the trade amount // For the seller we add the trade amount
if (trade.getContract().getSellerAddress().equals(getAddress())) if (trade.getContract().getSellerAddress().equals(getAddress()))
balanceInDeposit.add(trade.getTradeAmount()); balanceInDeposit.add(trade.getTradeAmount());
balanceLabel.setText(formatter.formatCoinWithCode(balanceInDeposit) + " locked in deposit");
balanceLabel.setText(formatter.formatCoinWithCode(balance) + " (in MS escrow)");
break; break;
case PAYOUT_PAID: case PAYOUT_PAID:
balanceLabel.setText(formatter.formatCoinWithCode(balance) + " in wallet"); balanceLabel.setText(formatter.formatCoinWithCode(balance) + " (in local wallet)");
break; break;
case WITHDRAWN: case WITHDRAWN:
log.error("Invalid state at updateBalance (WITHDRAWN)"); log.error("Invalid state at updateBalance (WITHDRAWN)");
balanceLabel.setText(formatter.formatCoinWithCode(balance) + " already withdrawn"); balanceLabel.setText(formatter.formatCoinWithCode(balance) + " already withdrawn");
break; break;
case DISPUTE: case DISPUTE:
balanceLabel.setText(formatter.formatCoinWithCode(balance) + " locked because of open ticket"); balanceLabel.setText(formatter.formatCoinWithCode(balance) + " open dispute/ticket");
break; break;
default: default:
log.warn("Not supported tradePhase: " + phase); log.warn("Not supported tradePhase: " + phase);

View file

@ -20,7 +20,6 @@ package io.bitsquare.gui.main.funds.withdrawal;
import com.google.common.util.concurrent.FutureCallback; import com.google.common.util.concurrent.FutureCallback;
import io.bitsquare.app.BitsquareApp; import io.bitsquare.app.BitsquareApp;
import io.bitsquare.btc.AddressEntry; import io.bitsquare.btc.AddressEntry;
import io.bitsquare.btc.FeePolicy;
import io.bitsquare.btc.Restrictions; import io.bitsquare.btc.Restrictions;
import io.bitsquare.btc.WalletService; import io.bitsquare.btc.WalletService;
import io.bitsquare.btc.listeners.BalanceListener; import io.bitsquare.btc.listeners.BalanceListener;
@ -65,9 +64,12 @@ import java.util.stream.Stream;
@FxmlView @FxmlView
public class WithdrawalView extends ActivatableView<VBox, Void> { public class WithdrawalView extends ActivatableView<VBox, Void> {
@FXML Button withdrawButton; @FXML
@FXML TableView<WithdrawalListItem> table; Button withdrawButton;
@FXML TextField withdrawFromTextField, withdrawToTextField, amountTextField; @FXML
TableView<WithdrawalListItem> table;
@FXML
TextField withdrawFromTextField, withdrawToTextField, amountTextField;
@FXML @FXML
TableColumn<WithdrawalListItem, WithdrawalListItem> labelColumn, addressColumn, balanceColumn, confidenceColumn; TableColumn<WithdrawalListItem, WithdrawalListItem> labelColumn, addressColumn, balanceColumn, confidenceColumn;
@ -114,7 +116,7 @@ public class WithdrawalView extends ActivatableView<VBox, Void> {
setConfidenceColumnCellFactory(); setConfidenceColumnCellFactory();
if (BitsquareApp.DEV_MODE) if (BitsquareApp.DEV_MODE)
withdrawToTextField.setText("mwajQdfYnve1knXnmv7JdeiVpeogTsck6S"); withdrawToTextField.setText("mxAkWWaQBqwqcYstKzqLku3kzR6pbu2zHq");
} }
private boolean areInputsValid() { private boolean areInputsValid() {
@ -144,8 +146,7 @@ public class WithdrawalView extends ActivatableView<VBox, Void> {
if (Coin.ZERO.compareTo(newValue.getBalance()) <= 0) { if (Coin.ZERO.compareTo(newValue.getBalance()) <= 0) {
amountTextField.setText(newValue.getBalance().toPlainString()); amountTextField.setText(newValue.getBalance().toPlainString());
withdrawFromTextField.setText(newValue.getAddressEntry().getAddressString()); withdrawFromTextField.setText(newValue.getAddressEntry().getAddressString());
} } else {
else {
withdrawFromTextField.setText(""); withdrawFromTextField.setText("");
withdrawFromTextField.setPromptText("No fund to withdrawal on that address."); withdrawFromTextField.setPromptText("No fund to withdrawal on that address.");
amountTextField.setText(""); amountTextField.setText("");
@ -173,15 +174,14 @@ public class WithdrawalView extends ActivatableView<VBox, Void> {
@FXML @FXML
public void onWithdraw() { public void onWithdraw() {
Coin amount = formatter.parseToCoin(amountTextField.getText()); Coin senderAmount = formatter.parseToCoin(amountTextField.getText());
if (Restrictions.isMinSpendableAmount(amount)) { if (Restrictions.isAboveDust(senderAmount)) {
FutureCallback<Transaction> callback = new FutureCallback<Transaction>() { FutureCallback<Transaction> callback = new FutureCallback<Transaction>() {
@Override @Override
public void onSuccess(@javax.annotation.Nullable Transaction transaction) { public void onSuccess(@javax.annotation.Nullable Transaction transaction) {
if (transaction != null) { if (transaction != null) {
log.info("onWithdraw onSuccess tx ID:" + transaction.getHashAsString()); log.info("onWithdraw onSuccess tx ID:" + transaction.getHashAsString());
} } else {
else {
log.error("onWithdraw transaction is null"); log.error("onWithdraw transaction is null");
} }
} }
@ -191,26 +191,29 @@ public class WithdrawalView extends ActivatableView<VBox, Void> {
log.error("onWithdraw onFailure"); log.error("onWithdraw onFailure");
} }
}; };
try {
Coin requiredFee = walletService.getRequiredFee(withdrawFromTextField.getText(),
withdrawToTextField.getText(), senderAmount);
Coin receiverAmount = senderAmount.subtract(requiredFee);
if (BitsquareApp.DEV_MODE) {
doWithdraw(receiverAmount, callback);
} else {
new Popup().headLine("Confirm your withdrawal request")
.message("Sending: " + formatter.formatCoinWithCode(senderAmount) + "\n" +
"From address: " + withdrawFromTextField.getText() + "\n" +
"To receiving address: " + withdrawToTextField.getText() + ".\n\n" +
"Required transaction fee is: " + formatter.formatCoinWithCode(requiredFee) + "\n" +
"Recipient will receive: " + formatter.formatCoinWithCode(receiverAmount) + "\n\n" +
"Are you sure you want to withdraw that amount?")
.onAction(() -> doWithdraw(receiverAmount, callback))
.show();
if (BitsquareApp.DEV_MODE) { }
doWithdraw(amount, callback); } catch (AddressFormatException | InsufficientMoneyException e) {
} else { e.printStackTrace();
new Popup().headLine("Confirm your withdrawal request").message("Amount: " + amountTextField.getText() + " " + log.error(e.getMessage());
"BTC\n" +
"Sending" +
" address: " + withdrawFromTextField.getText() + "\n" + "Receiving address: " +
withdrawToTextField.getText() + "\n" + "Transaction fee: " +
formatter.formatCoinWithCode(FeePolicy.TX_FEE) + "\n" +
"Receivers amount: " +
formatter.formatCoinWithCode(amount.subtract(FeePolicy.TX_FEE)) + " BTC\n\n" +
"Are you sure you want to withdraw that amount?")
.onAction(() -> {
doWithdraw(amount, callback);
})
.show();
} }
} } else {
else {
new Popup().warning("The amount to transfer is lower than the transaction fee and the min. possible tx value.").show(); new Popup().warning("The amount to transfer is lower than the transaction fee and the min. possible tx value.").show();
} }
} }
@ -284,8 +287,7 @@ public class WithdrawalView extends ActivatableView<VBox, Void> {
hyperlink.setOnAction(event -> openDetails(item)); hyperlink.setOnAction(event -> openDetails(item));
} }
setGraphic(hyperlink); setGraphic(hyperlink);
} } else {
else {
setGraphic(null); setGraphic(null);
setId(null); setId(null);
} }
@ -363,8 +365,7 @@ public class WithdrawalView extends ActivatableView<VBox, Void> {
if (item != null && !empty) { if (item != null && !empty) {
setGraphic(item.getProgressIndicator()); setGraphic(item.getProgressIndicator());
} } else {
else {
setGraphic(null); setGraphic(null);
} }
} }

View file

@ -117,9 +117,9 @@ class CreateOfferDataModel extends ActivatableDataModel {
offerId = UUID.randomUUID().toString(); offerId = UUID.randomUUID().toString();
addressEntry = walletService.getAddressEntryByOfferId(offerId); addressEntry = walletService.getAddressEntryByOfferId(offerId);
offerFeeAsCoin = FeePolicy.CREATE_OFFER_FEE; offerFeeAsCoin = FeePolicy.getCreateOfferFee();
networkFeeAsCoin = FeePolicy.TX_FEE; networkFeeAsCoin = FeePolicy.getFixedTxFeeForTrades();
securityDepositAsCoin = FeePolicy.SECURITY_DEPOSIT; securityDepositAsCoin = FeePolicy.getSecurityDeposit();
balanceListener = new BalanceListener(getAddressEntry().getAddress()) { balanceListener = new BalanceListener(getAddressEntry().getAddress()) {
@Override @Override

View file

@ -436,7 +436,7 @@ public class CreateOfferView extends ActivatableViewAndModel<AnchorPane, CreateO
() -> { () -> {
new Popup().headLine(BSResources.get("createOffer.success.headline")) new Popup().headLine(BSResources.get("createOffer.success.headline"))
.message(BSResources.get("createOffer.success.info")) .message(BSResources.get("createOffer.success.info"))
.actionButtonText("Go to \"Open offers\"") .actionButtonText("Go to \"My offers\"")
.onAction(() -> { .onAction(() -> {
close(); close();
FxTimer.runLater(Duration.ofMillis(100), FxTimer.runLater(Duration.ofMillis(100),

View file

@ -20,7 +20,10 @@ package io.bitsquare.gui.main.offer.takeoffer;
import com.google.inject.Inject; import com.google.inject.Inject;
import io.bitsquare.app.BitsquareApp; import io.bitsquare.app.BitsquareApp;
import io.bitsquare.arbitration.Arbitrator; import io.bitsquare.arbitration.Arbitrator;
import io.bitsquare.btc.*; import io.bitsquare.btc.AddressEntry;
import io.bitsquare.btc.FeePolicy;
import io.bitsquare.btc.TradeWalletService;
import io.bitsquare.btc.WalletService;
import io.bitsquare.btc.listeners.BalanceListener; import io.bitsquare.btc.listeners.BalanceListener;
import io.bitsquare.common.handlers.ResultHandler; import io.bitsquare.common.handlers.ResultHandler;
import io.bitsquare.gui.common.model.ActivatableDataModel; import io.bitsquare.gui.common.model.ActivatableDataModel;
@ -94,9 +97,9 @@ class TakeOfferDataModel extends ActivatableDataModel {
this.walletPasswordPopup = walletPasswordPopup; this.walletPasswordPopup = walletPasswordPopup;
this.preferences = preferences; this.preferences = preferences;
offerFeeAsCoin = FeePolicy.CREATE_OFFER_FEE; offerFeeAsCoin = FeePolicy.getCreateOfferFee();
networkFeeAsCoin = FeePolicy.TX_FEE; networkFeeAsCoin = FeePolicy.getFixedTxFeeForTrades();
securityDepositAsCoin = FeePolicy.SECURITY_DEPOSIT; securityDepositAsCoin = FeePolicy.getSecurityDeposit();
} }
@Override @Override
@ -127,7 +130,7 @@ class TakeOfferDataModel extends ActivatableDataModel {
addressEntry = walletService.getAddressEntryByOfferId(offer.getId()); addressEntry = walletService.getAddressEntryByOfferId(offer.getId());
checkNotNull(addressEntry, "addressEntry must not be null"); checkNotNull(addressEntry, "addressEntry must not be null");
ObservableList<PaymentAccount> possiblePaymentAccounts = getPossiblePaymentAccounts(); ObservableList<PaymentAccount> possiblePaymentAccounts = getPossiblePaymentAccounts();
checkArgument(!possiblePaymentAccounts.isEmpty(), "possiblePaymentAccounts.isEmpty()"); checkArgument(!possiblePaymentAccounts.isEmpty(), "possiblePaymentAccounts.isEmpty()");
paymentAccount = possiblePaymentAccounts.get(0); paymentAccount = possiblePaymentAccounts.get(0);
@ -135,7 +138,7 @@ class TakeOfferDataModel extends ActivatableDataModel {
amountAsCoin.set(offer.getAmount()); amountAsCoin.set(offer.getAmount());
if (BitsquareApp.DEV_MODE) if (BitsquareApp.DEV_MODE)
amountAsCoin.set(Restrictions.MIN_TRADE_AMOUNT); amountAsCoin.set(offer.getAmount());
calculateVolume(); calculateVolume();
calculateTotalToPay(); calculateTotalToPay();
@ -274,7 +277,7 @@ class TakeOfferDataModel extends ActivatableDataModel {
boolean isMinAmountLessOrEqualAmount() { boolean isMinAmountLessOrEqualAmount() {
//noinspection SimplifiableIfStatement //noinspection SimplifiableIfStatement
if (offer != null && offer.getMinAmount() != null && amountAsCoin.get() != null) if (offer != null && offer.getMinAmount() != null && amountAsCoin.get() != null)
return !offer.getMinAmount().isGreaterThan(amountAsCoin.get()); return !offer.getMinAmount().isGreaterThan(amountAsCoin.get());
return true; return true;
} }

View file

@ -196,7 +196,7 @@ public class TakeOfferView extends ActivatableViewAndModel<AnchorPane, TakeOffer
model.isOfferAvailable, model.isOfferAvailable,
(a, b, c, d) -> a == null && b == null && !c && !d) (a, b, c, d) -> a == null && b == null && !c && !d)
.subscribe((observable, oldValue, newValue) -> { .subscribe((observable, oldValue, newValue) -> {
if (newValue) { if (!oldValue && newValue) {
isOfferAvailablePopup = new Popup().information(BSResources.get("takeOffer.fundsBox.isOfferAvailable")) isOfferAvailablePopup = new Popup().information(BSResources.get("takeOffer.fundsBox.isOfferAvailable"))
.show() .show()
.onClose(() -> { .onClose(() -> {

View file

@ -17,14 +17,15 @@
~ along with Bitsquare. If not, see <http://www.gnu.org/licenses/>. ~ along with Bitsquare. If not, see <http://www.gnu.org/licenses/>.
--> -->
<?import javafx.scene.control.*?> <?import javafx.scene.control.Tab?>
<?import javafx.scene.layout.*?> <?import javafx.scene.control.TabPane?>
<?import javafx.scene.layout.AnchorPane?>
<TabPane fx:id="root" fx:controller="io.bitsquare.gui.main.portfolio.PortfolioView" <TabPane fx:id="root" fx:controller="io.bitsquare.gui.main.portfolio.PortfolioView"
AnchorPane.bottomAnchor="0.0" AnchorPane.leftAnchor="0.0" AnchorPane.bottomAnchor="0.0" AnchorPane.leftAnchor="0.0"
AnchorPane.rightAnchor="0.0" AnchorPane.topAnchor="0.0" tabClosingPolicy="UNAVAILABLE" AnchorPane.rightAnchor="0.0" AnchorPane.topAnchor="0.0" tabClosingPolicy="UNAVAILABLE"
xmlns:fx="http://javafx.com/fxml"> xmlns:fx="http://javafx.com/fxml">
<Tab fx:id="openOffersTab" text="Open offers"/> <Tab fx:id="openOffersTab" text="My offers"/>
<Tab fx:id="pendingTradesTab" text="Open trades"/> <Tab fx:id="pendingTradesTab" text="Open trades"/>
<Tab fx:id="closedTradesTab" text="History"/> <Tab fx:id="closedTradesTab" text="History"/>

View file

@ -295,7 +295,7 @@ public class PendingTradesDataModel extends ActivatableDataModel {
} }
Coin getTotalFees() { Coin getTotalFees() {
return FeePolicy.TX_FEE.add(isOfferer() ? FeePolicy.CREATE_OFFER_FEE : FeePolicy.TAKE_OFFER_FEE); return FeePolicy.getFixedTxFeeForTrades().add(isOfferer() ? FeePolicy.getCreateOfferFee() : FeePolicy.getTakeOfferFee());
} }
PendingTradesListItem getSelectedItem() { PendingTradesListItem getSelectedItem() {

View file

@ -324,7 +324,7 @@ public class PendingTradesViewModel extends ActivatableWithDataModel<PendingTrad
} }
public String getSecurityDeposit() { public String getSecurityDeposit() {
return formatter.formatCoinWithCode(FeePolicy.SECURITY_DEPOSIT); return formatter.formatCoinWithCode(FeePolicy.getSecurityDeposit());
} }
public boolean isBlockChainMethod() { public boolean isBlockChainMethod() {

View file

@ -116,7 +116,7 @@ public class CompletedView extends TradeStepDetailsView {
}); });
if (BitsquareApp.DEV_MODE) if (BitsquareApp.DEV_MODE)
withdrawAddressTextField.setText("mwajQdfYnve1knXnmv7JdeiVpeogTsck6S"); withdrawAddressTextField.setText("mxAkWWaQBqwqcYstKzqLku3kzR6pbu2zHq");
} }
public void setBtcTradeAmountLabelText(String text) { public void setBtcTradeAmountLabelText(String text) {

View file

@ -19,10 +19,12 @@ package io.bitsquare.gui.main.settings.preferences;
import io.bitsquare.gui.common.view.ActivatableViewAndModel; import io.bitsquare.gui.common.view.ActivatableViewAndModel;
import io.bitsquare.gui.common.view.FxmlView; import io.bitsquare.gui.common.view.FxmlView;
import io.bitsquare.gui.components.InputTextField;
import io.bitsquare.gui.util.Layout; import io.bitsquare.gui.util.Layout;
import io.bitsquare.locale.LanguageUtil; import io.bitsquare.locale.LanguageUtil;
import io.bitsquare.locale.TradeCurrency; import io.bitsquare.locale.TradeCurrency;
import io.bitsquare.user.BlockChainExplorer; import io.bitsquare.user.BlockChainExplorer;
import javafx.beans.value.ChangeListener;
import javafx.scene.control.CheckBox; import javafx.scene.control.CheckBox;
import javafx.scene.control.ComboBox; import javafx.scene.control.ComboBox;
import javafx.scene.layout.GridPane; import javafx.scene.layout.GridPane;
@ -44,6 +46,8 @@ public class PreferencesView extends ActivatableViewAndModel<GridPane, Preferenc
private CheckBox useAnimationsCheckBox, useEffectsCheckBox, showPlaceOfferConfirmationCheckBox, showTakeOfferConfirmationCheckBox, private CheckBox useAnimationsCheckBox, useEffectsCheckBox, showPlaceOfferConfirmationCheckBox, showTakeOfferConfirmationCheckBox,
autoSelectArbitratorsCheckBox; autoSelectArbitratorsCheckBox;
private int gridRow = 0; private int gridRow = 0;
private InputTextField transactionFeeInputTextField;
private ChangeListener<Boolean> transactionFeeFocusedListener;
@Inject @Inject
public PreferencesView(PreferencesViewModel model) { public PreferencesView(PreferencesViewModel model) {
@ -52,12 +56,16 @@ public class PreferencesView extends ActivatableViewAndModel<GridPane, Preferenc
@Override @Override
public void initialize() { public void initialize() {
addTitledGroupBg(root, gridRow, 3, "Preferences"); addTitledGroupBg(root, gridRow, 4, "Preferences");
tradeCurrencyComboBox = addLabelComboBox(root, gridRow, "Preferred currency:", Layout.FIRST_ROW_DISTANCE).second; tradeCurrencyComboBox = addLabelComboBox(root, gridRow, "Preferred currency:", Layout.FIRST_ROW_DISTANCE).second;
languageComboBox = addLabelComboBox(root, ++gridRow, "Language:").second; languageComboBox = addLabelComboBox(root, ++gridRow, "Language:").second;
// btcDenominationComboBox = addLabelComboBox(root, ++gridRow, "Bitcoin denomination:").second; // btcDenominationComboBox = addLabelComboBox(root, ++gridRow, "Bitcoin denomination:").second;
blockExplorerComboBox = addLabelComboBox(root, ++gridRow, "Bitcoin block explorer:").second; blockExplorerComboBox = addLabelComboBox(root, ++gridRow, "Bitcoin block explorer:").second;
transactionFeeInputTextField = addLabelInputTextField(root, ++gridRow, "Transaction fee (satoshi/byte):").second;
transactionFeeFocusedListener = (o, oldValue, newValue) -> {
model.onFocusOutTransactionFeeTextField(oldValue, newValue, transactionFeeInputTextField.getText());
};
addTitledGroupBg(root, ++gridRow, 5, "Display options", Layout.GROUP_DISTANCE); addTitledGroupBg(root, ++gridRow, 5, "Display options", Layout.GROUP_DISTANCE);
useAnimationsCheckBox = addLabelCheckBox(root, gridRow, "Use animations:", "", Layout.FIRST_ROW_AND_GROUP_DISTANCE).second; useAnimationsCheckBox = addLabelCheckBox(root, gridRow, "Use animations:", "", Layout.FIRST_ROW_AND_GROUP_DISTANCE).second;
useEffectsCheckBox = addLabelCheckBox(root, ++gridRow, "Use effects:", "").second; useEffectsCheckBox = addLabelCheckBox(root, ++gridRow, "Use effects:", "").second;
@ -119,6 +127,8 @@ public class PreferencesView extends ActivatableViewAndModel<GridPane, Preferenc
}); });
blockExplorerComboBox.setOnAction(e -> model.onSelectBlockExplorer(blockExplorerComboBox.getSelectionModel().getSelectedItem())); blockExplorerComboBox.setOnAction(e -> model.onSelectBlockExplorer(blockExplorerComboBox.getSelectionModel().getSelectedItem()));
transactionFeeInputTextField.textProperty().bindBidirectional(model.transactionFeePerByte);
transactionFeeInputTextField.focusedProperty().addListener(transactionFeeFocusedListener);
useAnimationsCheckBox.setSelected(model.getUseAnimations()); useAnimationsCheckBox.setSelected(model.getUseAnimations());
useAnimationsCheckBox.setOnAction(e -> model.onSelectUseAnimations(useAnimationsCheckBox.isSelected())); useAnimationsCheckBox.setOnAction(e -> model.onSelectUseAnimations(useAnimationsCheckBox.isSelected()));
@ -142,6 +152,8 @@ public class PreferencesView extends ActivatableViewAndModel<GridPane, Preferenc
languageComboBox.setOnAction(null); languageComboBox.setOnAction(null);
tradeCurrencyComboBox.setOnAction(null); tradeCurrencyComboBox.setOnAction(null);
blockExplorerComboBox.setOnAction(null); blockExplorerComboBox.setOnAction(null);
transactionFeeInputTextField.textProperty().unbind();
transactionFeeInputTextField.focusedProperty().removeListener(transactionFeeFocusedListener);
useAnimationsCheckBox.setOnAction(null); useAnimationsCheckBox.setOnAction(null);
useEffectsCheckBox.setOnAction(null); useEffectsCheckBox.setOnAction(null);
showPlaceOfferConfirmationCheckBox.setOnAction(null); showPlaceOfferConfirmationCheckBox.setOnAction(null);

View file

@ -18,15 +18,20 @@
package io.bitsquare.gui.main.settings.preferences; package io.bitsquare.gui.main.settings.preferences;
import com.google.inject.Inject; import com.google.inject.Inject;
import io.bitsquare.common.UserThread;
import io.bitsquare.gui.common.model.ActivatableViewModel; import io.bitsquare.gui.common.model.ActivatableViewModel;
import io.bitsquare.gui.popups.Popup;
import io.bitsquare.locale.LanguageUtil; import io.bitsquare.locale.LanguageUtil;
import io.bitsquare.locale.TradeCurrency; import io.bitsquare.locale.TradeCurrency;
import io.bitsquare.user.BlockChainExplorer; import io.bitsquare.user.BlockChainExplorer;
import io.bitsquare.user.Preferences; import io.bitsquare.user.Preferences;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.collections.FXCollections; import javafx.collections.FXCollections;
import javafx.collections.ObservableList; import javafx.collections.ObservableList;
import java.util.Locale; import java.util.Locale;
import java.util.concurrent.TimeUnit;
class PreferencesViewModel extends ActivatableViewModel { class PreferencesViewModel extends ActivatableViewModel {
@ -35,7 +40,8 @@ class PreferencesViewModel extends ActivatableViewModel {
final ObservableList<BlockChainExplorer> blockExplorers; final ObservableList<BlockChainExplorer> blockExplorers;
final ObservableList<TradeCurrency> tradeCurrencies; final ObservableList<TradeCurrency> tradeCurrencies;
final ObservableList<String> languageCodes; final ObservableList<String> languageCodes;
final StringProperty transactionFeePerByte = new SimpleStringProperty();
/////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////
// Constructor, initialisation // Constructor, initialisation
@ -52,13 +58,13 @@ class PreferencesViewModel extends ActivatableViewModel {
@Override @Override
protected void activate() { protected void activate() {
transactionFeePerByte.set(String.valueOf(preferences.getTxFeePerKB() / 1000));
} }
@Override @Override
protected void deactivate() { protected void deactivate() {
} }
/////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////
// UI actions // UI actions
/////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////
@ -99,6 +105,19 @@ class PreferencesViewModel extends ActivatableViewModel {
preferences.setPreferredLocale(new Locale(code, preferences.getPreferredLocale().getCountry())); preferences.setPreferredLocale(new Locale(code, preferences.getPreferredLocale().getCountry()));
} }
public void onFocusOutTransactionFeeTextField(Boolean oldValue, Boolean newValue, String text) {
if (oldValue && !newValue) {
try {
preferences.setTxFeePerKB(Long.valueOf(transactionFeePerByte.get()) * 1000);
} catch (Exception e) {
new Popup().warning(e.getMessage())
.onClose(() -> UserThread.runAfter(
() -> transactionFeePerByte.set(String.valueOf(preferences.getTxFeePerKB() / 1000)),
100, TimeUnit.MILLISECONDS))
.show();
}
}
}
/////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////
// Getters // Getters
@ -139,4 +158,5 @@ class PreferencesViewModel extends ActivatableViewModel {
public TradeCurrency getTradeCurrency() { public TradeCurrency getTradeCurrency() {
return preferences.getPreferredTradeCurrency(); return preferences.getPreferredTradeCurrency();
} }
} }

View file

@ -99,7 +99,7 @@ public class EmptyWalletPopup extends Popup {
Tuple2<Label, InputTextField> tuple = addLabelInputTextField(gridPane, ++rowIndex, "Your destination address:"); Tuple2<Label, InputTextField> tuple = addLabelInputTextField(gridPane, ++rowIndex, "Your destination address:");
addressInputTextField = tuple.second; addressInputTextField = tuple.second;
emptyWalletButton = new Button("Empty wallet"); emptyWalletButton = new Button("Empty wallet");
boolean isBalanceSufficient = Restrictions.isMinSpendableAmount(totalBalance); boolean isBalanceSufficient = Restrictions.isAboveDust(totalBalance);
emptyWalletButton.setDefaultButton(isBalanceSufficient); emptyWalletButton.setDefaultButton(isBalanceSufficient);
closeButton.setDefaultButton(!isBalanceSufficient); closeButton.setDefaultButton(!isBalanceSufficient);
emptyWalletButton.setDisable(!isBalanceSufficient && addressInputTextField.getText().length() > 0); emptyWalletButton.setDisable(!isBalanceSufficient && addressInputTextField.getText().length() > 0);

View file

@ -76,7 +76,7 @@ createOffer.advancedBox.county=Payments account country:
createOffer.advancedBox.info=Your trading partners must fulfill your offer restrictions. You can edit the accepted countries, languages and arbitrators in the settings. The payments account details are used from your current selected payments account (if you have multiple payments accounts). createOffer.advancedBox.info=Your trading partners must fulfill your offer restrictions. You can edit the accepted countries, languages and arbitrators in the settings. The payments account details are used from your current selected payments account (if you have multiple payments accounts).
createOffer.success.headline=Your offer has been published to the P2P network. createOffer.success.headline=Your offer has been published to the P2P network.
createOffer.success.info=You can manage your open offers in the \"Portfolio\" screen under \"Open offers\". createOffer.success.info=You can manage your open offers in the \"Portfolio\" screen under \"My offers\".
createOffer.error.message=An error occurred when placing the offer.\n\n{0} createOffer.error.message=An error occurred when placing the offer.\n\n{0}