mirror of
https://github.com/haveno-dex/haveno.git
synced 2025-07-22 06:38:55 -04:00
Fix fee calculation, add adjustable non-trade mining fee
This commit is contained in:
parent
2abc2cd5bc
commit
565c44d94c
12 changed files with 230 additions and 144 deletions
|
@ -50,14 +50,14 @@ public class FeePolicy {
|
|||
// The BitcoinJ fee calculation use kb so a tx size < 1kb will still pay the fee for a kb tx.
|
||||
// Our payout tx has about 370 bytes so we get a fee/kb value of about 90 satoshi/byte making it high priority
|
||||
// Other payout transactions (E.g. arbitrators many collected transactions) will go with 30 satoshi/byte if > 1kb
|
||||
private static Coin FEE_PER_KB = Coin.valueOf(20_000); // 0.0002 BTC about 0.08 EUR @ 400 EUR/BTC
|
||||
private static Coin NON_TRADE_FEE_PER_KB = Coin.valueOf(10_000); // 0.0001 BTC about 0.04 EUR @ 400 EUR/BTC
|
||||
|
||||
public static void setFeePerKb(Coin feePerKb) {
|
||||
FEE_PER_KB = feePerKb;
|
||||
public static void setNonTradeFeePerKb(Coin nonTradeFeePerKb) {
|
||||
NON_TRADE_FEE_PER_KB = nonTradeFeePerKb;
|
||||
}
|
||||
|
||||
public static Coin getFeePerKb() {
|
||||
return FEE_PER_KB;
|
||||
public static Coin getNonTradeFeePerKb() {
|
||||
return NON_TRADE_FEE_PER_KB;
|
||||
}
|
||||
|
||||
// Some wallets don't support manual fees. Most use at least 0.0001 BTC (0.04 EUR @ 400 EUR/BTC)
|
||||
|
|
|
@ -38,7 +38,10 @@ import org.bitcoinj.kits.WalletAppKit;
|
|||
import org.bitcoinj.params.MainNetParams;
|
||||
import org.bitcoinj.params.RegTestParams;
|
||||
import org.bitcoinj.params.TestNet3Params;
|
||||
import org.bitcoinj.script.Script;
|
||||
import org.bitcoinj.utils.Threading;
|
||||
import org.bitcoinj.wallet.CoinSelection;
|
||||
import org.bitcoinj.wallet.CoinSelector;
|
||||
import org.bitcoinj.wallet.DeterministicSeed;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.slf4j.Logger;
|
||||
|
@ -347,6 +350,10 @@ public class WalletService {
|
|||
return addressEntryList.addAddressEntry(new AddressEntry(wallet.freshReceiveKey(), wallet.getParams(), context));
|
||||
}
|
||||
|
||||
public AddressEntry createAddressEntry(AddressEntry.Context context) {
|
||||
return addressEntryList.addAddressEntry(new AddressEntry(wallet.freshReceiveKey(), wallet.getParams(), context));
|
||||
}
|
||||
|
||||
public Optional<AddressEntry> findAddressEntry(String address, AddressEntry.Context context) {
|
||||
return getAddressEntryListAsImmutableList().stream()
|
||||
.filter(e -> address.equals(e.getAddressString()))
|
||||
|
@ -521,45 +528,156 @@ public class WalletService {
|
|||
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
// Withdrawal
|
||||
// Withdrawal Fee calculation
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
public Coin getRequiredFee(String fromAddress,
|
||||
String toAddress,
|
||||
Coin amount,
|
||||
@Nullable KeyParameter aesKey,
|
||||
AddressEntry.Context context) throws AddressFormatException, AddressEntryException {
|
||||
Coin fee;
|
||||
try {
|
||||
wallet.completeTx(getSendRequest(fromAddress, toAddress, amount, aesKey, context));
|
||||
// We use the min fee for now as the mix of savingswallet/trade wallet has some nasty edge cases...
|
||||
fee = FeePolicy.getFixedTxFeeForTrades();
|
||||
} catch (InsufficientMoneyException e) {
|
||||
log.info("The amount to be transferred is not enough to pay the transaction fees of {}. " +
|
||||
"We subtract that fee from the receivers amount to make the transaction possible.");
|
||||
fee = e.missing;
|
||||
}
|
||||
return fee;
|
||||
Optional<AddressEntry> addressEntry = findAddressEntry(fromAddress, context);
|
||||
if (!addressEntry.isPresent())
|
||||
throw new AddressEntryException("WithdrawFromAddress is not found in our wallet.");
|
||||
|
||||
checkNotNull(addressEntry.get().getAddress(), "addressEntry.get().getAddress() must nto be null");
|
||||
CoinSelector selector = new TradeWalletCoinSelector(params, addressEntry.get().getAddress());
|
||||
return getFee(toAddress,
|
||||
amount,
|
||||
selector);
|
||||
}
|
||||
|
||||
public Coin getRequiredFeeForMultipleAddresses(Set<String> fromAddresses,
|
||||
String toAddress,
|
||||
Coin amount,
|
||||
@Nullable KeyParameter aesKey) throws AddressFormatException,
|
||||
Coin amount) throws AddressFormatException,
|
||||
AddressEntryException {
|
||||
Coin fee;
|
||||
try {
|
||||
wallet.completeTx(getSendRequestForMultipleAddresses(fromAddresses, toAddress, amount, null, aesKey));
|
||||
// We use the min fee for now as the mix of savingswallet/trade wallet has some nasty edge cases...
|
||||
fee = FeePolicy.getFixedTxFeeForTrades();
|
||||
} catch (InsufficientMoneyException e) {
|
||||
log.info("The amount to be transferred is not enough to pay the transaction fees of {}. " +
|
||||
"We subtract that fee from the receivers amount to make the transaction possible.");
|
||||
fee = e.missing;
|
||||
}
|
||||
Set<AddressEntry> addressEntries = fromAddresses.stream()
|
||||
.map(address -> {
|
||||
Optional<AddressEntry> addressEntryOptional = findAddressEntry(address, AddressEntry.Context.AVAILABLE);
|
||||
if (!addressEntryOptional.isPresent())
|
||||
addressEntryOptional = findAddressEntry(address, AddressEntry.Context.OFFER_FUNDING);
|
||||
if (!addressEntryOptional.isPresent())
|
||||
addressEntryOptional = findAddressEntry(address, AddressEntry.Context.TRADE_PAYOUT);
|
||||
if (!addressEntryOptional.isPresent())
|
||||
addressEntryOptional = findAddressEntry(address, AddressEntry.Context.ARBITRATOR);
|
||||
return addressEntryOptional;
|
||||
})
|
||||
.filter(Optional::isPresent)
|
||||
.map(Optional::get)
|
||||
.collect(Collectors.toSet());
|
||||
if (addressEntries.isEmpty())
|
||||
throw new AddressEntryException("No Addresses for withdraw found in our wallet");
|
||||
|
||||
CoinSelector selector = new MultiAddressesCoinSelector(params, addressEntries);
|
||||
return getFee(toAddress,
|
||||
amount,
|
||||
selector);
|
||||
}
|
||||
|
||||
private Coin getFee(String toAddress,
|
||||
Coin amount,
|
||||
CoinSelector selector) throws AddressFormatException, AddressEntryException {
|
||||
List<TransactionOutput> candidates = wallet.calculateAllSpendCandidates();
|
||||
CoinSelection bestCoinSelection = selector.select(params.getMaxMoney(), candidates);
|
||||
Transaction tx = new Transaction(params);
|
||||
tx.addOutput(amount, new Address(params, toAddress));
|
||||
if (!adjustOutputDownwardsForFee(tx, bestCoinSelection, Coin.ZERO, FeePolicy.getNonTradeFeePerKb()))
|
||||
throw new Wallet.CouldNotAdjustDownwards();
|
||||
|
||||
Coin fee = amount.subtract(tx.getOutput(0).getValue());
|
||||
log.info("Required fee " + fee);
|
||||
return fee;
|
||||
}
|
||||
|
||||
private boolean adjustOutputDownwardsForFee(Transaction tx, CoinSelection coinSelection, Coin baseFee, Coin feePerKb) {
|
||||
TransactionOutput output = tx.getOutput(0);
|
||||
// Check if we need additional fee due to the transaction's size
|
||||
int size = tx.bitcoinSerialize().length;
|
||||
size += estimateBytesForSigning(coinSelection);
|
||||
Coin fee = baseFee.add(feePerKb.multiply((size / 1000) + 1));
|
||||
output.setValue(output.getValue().subtract(fee));
|
||||
// Check if we need additional fee due to the output's value
|
||||
if (output.getValue().compareTo(Coin.CENT) < 0 && fee.compareTo(Transaction.REFERENCE_DEFAULT_MIN_TX_FEE) < 0)
|
||||
output.setValue(output.getValue().subtract(Transaction.REFERENCE_DEFAULT_MIN_TX_FEE.subtract(fee)));
|
||||
return output.getMinNonDustValue().compareTo(output.getValue()) <= 0;
|
||||
}
|
||||
|
||||
private int estimateBytesForSigning(CoinSelection selection) {
|
||||
int size = 0;
|
||||
for (TransactionOutput output : selection.gathered) {
|
||||
try {
|
||||
Script script = output.getScriptPubKey();
|
||||
ECKey key = null;
|
||||
Script redeemScript = null;
|
||||
if (script.isSentToAddress()) {
|
||||
key = wallet.findKeyFromPubHash(script.getPubKeyHash());
|
||||
checkNotNull(key, "Coin selection includes unspendable outputs");
|
||||
} else if (script.isPayToScriptHash()) {
|
||||
redeemScript = wallet.findRedeemDataFromScriptHash(script.getPubKeyHash()).redeemScript;
|
||||
checkNotNull(redeemScript, "Coin selection includes unspendable outputs");
|
||||
}
|
||||
size += script.getNumberOfBytesRequiredToSpend(key, redeemScript);
|
||||
} catch (ScriptException e) {
|
||||
// If this happens it means an output script in a wallet tx could not be understood. That should never
|
||||
// happen, if it does it means the wallet has got into an inconsistent state.
|
||||
throw new IllegalStateException(e);
|
||||
}
|
||||
}
|
||||
return size;
|
||||
}
|
||||
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
// Withdrawal Send
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
public String sendFunds(String fromAddress,
|
||||
String toAddress,
|
||||
Coin receiverAmount,
|
||||
@Nullable KeyParameter aesKey,
|
||||
AddressEntry.Context context,
|
||||
FutureCallback<Transaction> callback) throws AddressFormatException,
|
||||
AddressEntryException, InsufficientMoneyException {
|
||||
Wallet.SendResult sendResult = wallet.sendCoins(getSendRequest(fromAddress, toAddress, receiverAmount, aesKey, context));
|
||||
Futures.addCallback(sendResult.broadcastComplete, callback);
|
||||
|
||||
printTxWithInputs("sendFunds", sendResult.tx);
|
||||
return sendResult.tx.getHashAsString();
|
||||
}
|
||||
|
||||
public String sendFundsForMultipleAddresses(Set<String> fromAddresses,
|
||||
String toAddress,
|
||||
Coin receiverAmount,
|
||||
@Nullable String changeAddress,
|
||||
@Nullable KeyParameter aesKey,
|
||||
FutureCallback<Transaction> callback) throws AddressFormatException,
|
||||
AddressEntryException, InsufficientMoneyException {
|
||||
Wallet.SendResult sendResult = wallet.sendCoins(getSendRequestForMultipleAddresses(fromAddresses, toAddress,
|
||||
receiverAmount, changeAddress, aesKey));
|
||||
Futures.addCallback(sendResult.broadcastComplete, callback);
|
||||
|
||||
printTxWithInputs("sendFunds", sendResult.tx);
|
||||
return sendResult.tx.getHashAsString();
|
||||
}
|
||||
|
||||
public void emptyWallet(String toAddress, KeyParameter aesKey, ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler)
|
||||
throws InsufficientMoneyException, AddressFormatException {
|
||||
Wallet.SendRequest sendRequest = Wallet.SendRequest.emptyWallet(new Address(params, toAddress));
|
||||
sendRequest.aesKey = aesKey;
|
||||
Wallet.SendResult sendResult = wallet.sendCoins(sendRequest);
|
||||
sendRequest.feePerKb = FeePolicy.getNonTradeFeePerKb();
|
||||
Futures.addCallback(sendResult.broadcastComplete, new FutureCallback<Transaction>() {
|
||||
@Override
|
||||
public void onSuccess(Transaction result) {
|
||||
resultHandler.handleResult();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(@NotNull Throwable t) {
|
||||
errorMessageHandler.handleErrorMessage(t.getMessage());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private Wallet.SendRequest getSendRequest(String fromAddress,
|
||||
String toAddress,
|
||||
Coin amount,
|
||||
|
@ -581,7 +699,7 @@ public class WalletService {
|
|||
checkNotNull(addressEntry.get().getAddress(), "addressEntry.get().getAddress() must nto be null");
|
||||
sendRequest.coinSelector = new TradeWalletCoinSelector(params, addressEntry.get().getAddress());
|
||||
sendRequest.changeAddress = addressEntry.get().getAddress();
|
||||
sendRequest.feePerKb = FeePolicy.getFeePerKb();
|
||||
sendRequest.feePerKb = FeePolicy.getNonTradeFeePerKb();
|
||||
return sendRequest;
|
||||
}
|
||||
|
||||
|
@ -606,6 +724,8 @@ public class WalletService {
|
|||
addressEntryOptional = findAddressEntry(address, AddressEntry.Context.OFFER_FUNDING);
|
||||
if (!addressEntryOptional.isPresent())
|
||||
addressEntryOptional = findAddressEntry(address, AddressEntry.Context.TRADE_PAYOUT);
|
||||
if (!addressEntryOptional.isPresent())
|
||||
addressEntryOptional = findAddressEntry(address, AddressEntry.Context.ARBITRATOR);
|
||||
return addressEntryOptional;
|
||||
})
|
||||
.filter(Optional::isPresent)
|
||||
|
@ -629,60 +749,10 @@ public class WalletService {
|
|||
}
|
||||
checkNotNull(changeAddressAddressEntry, "change address must not be null");
|
||||
sendRequest.changeAddress = changeAddressAddressEntry.getAddress();
|
||||
sendRequest.feePerKb = FeePolicy.getFeePerKb();
|
||||
sendRequest.feePerKb = FeePolicy.getNonTradeFeePerKb();
|
||||
return sendRequest;
|
||||
}
|
||||
|
||||
public String sendFunds(String fromAddress,
|
||||
String toAddress,
|
||||
Coin amount,
|
||||
@Nullable KeyParameter aesKey,
|
||||
AddressEntry.Context context,
|
||||
FutureCallback<Transaction> callback) throws AddressFormatException,
|
||||
AddressEntryException, InsufficientMoneyException {
|
||||
Coin fee = getRequiredFee(fromAddress, toAddress, amount, aesKey, context);
|
||||
Wallet.SendResult sendResult = wallet.sendCoins(getSendRequest(fromAddress, toAddress, amount.subtract(fee), aesKey, context));
|
||||
Futures.addCallback(sendResult.broadcastComplete, callback);
|
||||
|
||||
printTxWithInputs("sendFunds", sendResult.tx);
|
||||
return sendResult.tx.getHashAsString();
|
||||
}
|
||||
|
||||
public String sendFundsForMultipleAddresses(Set<String> fromAddresses,
|
||||
String toAddress,
|
||||
Coin amount,
|
||||
@Nullable String changeAddress,
|
||||
@Nullable KeyParameter aesKey,
|
||||
FutureCallback<Transaction> callback) throws AddressFormatException,
|
||||
AddressEntryException, InsufficientMoneyException {
|
||||
Coin fee = getRequiredFeeForMultipleAddresses(fromAddresses, toAddress, amount, aesKey);
|
||||
Wallet.SendResult sendResult = wallet.sendCoins(getSendRequestForMultipleAddresses(fromAddresses, toAddress,
|
||||
amount.subtract(fee), changeAddress, aesKey));
|
||||
Futures.addCallback(sendResult.broadcastComplete, callback);
|
||||
|
||||
printTxWithInputs("sendFunds", sendResult.tx);
|
||||
return sendResult.tx.getHashAsString();
|
||||
}
|
||||
|
||||
public void emptyWallet(String toAddress, KeyParameter aesKey, ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler)
|
||||
throws InsufficientMoneyException, AddressFormatException {
|
||||
Wallet.SendRequest sendRequest = Wallet.SendRequest.emptyWallet(new Address(params, toAddress));
|
||||
sendRequest.aesKey = aesKey;
|
||||
Wallet.SendResult sendResult = wallet.sendCoins(sendRequest);
|
||||
sendRequest.feePerKb = FeePolicy.getFeePerKb();
|
||||
Futures.addCallback(sendResult.broadcastComplete, new FutureCallback<Transaction>() {
|
||||
@Override
|
||||
public void onSuccess(Transaction result) {
|
||||
resultHandler.handleResult();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(@NotNull Throwable t) {
|
||||
errorMessageHandler.handleErrorMessage(t.getMessage());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
// Getters
|
||||
|
|
|
@ -306,7 +306,7 @@ public class TradeManager {
|
|||
// Trade
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
public void onWithdrawRequest(String toAddress, KeyParameter aesKey, Trade trade, ResultHandler resultHandler, FaultHandler faultHandler) {
|
||||
public void onWithdrawRequest(String toAddress, Coin receiverAmount, KeyParameter aesKey, Trade trade, ResultHandler resultHandler, FaultHandler faultHandler) {
|
||||
String fromAddress = walletService.getOrCreateAddressEntry(trade.getId(), AddressEntry.Context.TRADE_PAYOUT).getAddressString();
|
||||
|
||||
FutureCallback<Transaction> callback = new FutureCallback<Transaction>() {
|
||||
|
@ -328,7 +328,7 @@ public class TradeManager {
|
|||
}
|
||||
};
|
||||
try {
|
||||
walletService.sendFunds(fromAddress, toAddress, trade.getPayoutAmount(), aesKey, AddressEntry.Context.TRADE_PAYOUT, callback);
|
||||
walletService.sendFunds(fromAddress, toAddress, receiverAmount, aesKey, AddressEntry.Context.TRADE_PAYOUT, callback);
|
||||
} catch (AddressFormatException | InsufficientMoneyException | AddressEntryException e) {
|
||||
e.printStackTrace();
|
||||
log.error(e.getMessage());
|
||||
|
|
|
@ -105,7 +105,7 @@ public final class Preferences implements Persistable {
|
|||
private boolean showOwnOffersInOfferBook;
|
||||
private Locale preferredLocale;
|
||||
private TradeCurrency preferredTradeCurrency;
|
||||
private long txFeePerKB = FeePolicy.getFeePerKb().value;
|
||||
private long nonTradeTxFeePerKB = FeePolicy.getNonTradeFeePerKb().value;
|
||||
private double maxPriceDistanceInPercent;
|
||||
|
||||
// Observable wrappers
|
||||
|
@ -162,7 +162,7 @@ public final class Preferences implements Persistable {
|
|||
maxPriceDistanceInPercent = 0.2;
|
||||
|
||||
try {
|
||||
setTxFeePerKB(persisted.getTxFeePerKB());
|
||||
setNonTradeTxFeePerKB(persisted.getNonTradeTxFeePerKB());
|
||||
} catch (Exception e) {
|
||||
// leave default value
|
||||
}
|
||||
|
@ -306,15 +306,15 @@ public final class Preferences implements Persistable {
|
|||
}
|
||||
}
|
||||
|
||||
public void setTxFeePerKB(long txFeePerKB) throws Exception {
|
||||
if (txFeePerKB < Transaction.REFERENCE_DEFAULT_MIN_TX_FEE.value)
|
||||
public void setNonTradeTxFeePerKB(long nonTradeTxFeePerKB) throws Exception {
|
||||
if (nonTradeTxFeePerKB < Transaction.REFERENCE_DEFAULT_MIN_TX_FEE.value)
|
||||
throw new Exception("Transaction fee must be at least 5 satoshi/byte");
|
||||
|
||||
if (txFeePerKB < Transaction.REFERENCE_DEFAULT_MIN_TX_FEE.value)
|
||||
throw new Exception("Transaction fee must be at least 5 satoshi/byte");
|
||||
if (nonTradeTxFeePerKB > 500_000)
|
||||
throw new Exception("Transaction fee is in the range of 10-100 satoshi/byte. Your input is above any reasonable value (>500 satoshi/byte).");
|
||||
|
||||
this.txFeePerKB = txFeePerKB;
|
||||
FeePolicy.setFeePerKb(Coin.valueOf(txFeePerKB));
|
||||
this.nonTradeTxFeePerKB = nonTradeTxFeePerKB;
|
||||
FeePolicy.setNonTradeFeePerKb(Coin.valueOf(nonTradeTxFeePerKB));
|
||||
storage.queueUpForSave();
|
||||
}
|
||||
|
||||
|
@ -435,8 +435,8 @@ public final class Preferences implements Persistable {
|
|||
return preferredTradeCurrency;
|
||||
}
|
||||
|
||||
public long getTxFeePerKB() {
|
||||
return Math.max(Transaction.REFERENCE_DEFAULT_MIN_TX_FEE.value, txFeePerKB);
|
||||
public long getNonTradeTxFeePerKB() {
|
||||
return Math.max(Transaction.REFERENCE_DEFAULT_MIN_TX_FEE.value, nonTradeTxFeePerKB);
|
||||
}
|
||||
|
||||
public boolean getUseTorForBitcoinJ() {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue