dynamic fees

This commit is contained in:
ThomasV 2015-08-04 07:15:54 +02:00
parent 959620db46
commit 43880d452e
12 changed files with 81 additions and 36 deletions

View File

@ -2,6 +2,7 @@
* Use ssl.PROTOCOL_TLSv1 * Use ssl.PROTOCOL_TLSv1
* Fix DNSSEC issues with ECDSA signatures * Fix DNSSEC issues with ECDSA signatures
* Replace TLSLite dependency with minimal RSA implementation * Replace TLSLite dependency with minimal RSA implementation
* Dynamic fees, using estimatefee value returned by server
# Release 2.4 # Release 2.4
* Payment to DNS names storing a Bitcoin addresses (OpenAlias) is * Payment to DNS names storing a Bitcoin addresses (OpenAlias) is

View File

@ -444,7 +444,7 @@ def pay_to(recipient, amount, label):
droid.dialogShow() droid.dialogShow()
try: try:
tx = wallet.mktx([('address', recipient, amount)], password) tx = wallet.mktx([('address', recipient, amount)], password, config)
except Exception as e: except Exception as e:
modal_dialog('error', e.message) modal_dialog('error', e.message)
droid.dialogDismiss() droid.dialogDismiss()
@ -895,12 +895,14 @@ menu_commands = ["send", "receive", "settings", "contacts", "main"]
wallet = None wallet = None
network = None network = None
contacts = None contacts = None
config = None
class ElectrumGui: class ElectrumGui:
def __init__(self, config, _network): def __init__(self, _config, _network):
global wallet, network, contacts global wallet, network, contacts
network = _network network = _network
config = _config
network.register_callback('updated', update_callback) network.register_callback('updated', update_callback)
network.register_callback('connected', update_callback) network.register_callback('connected', update_callback)
network.register_callback('disconnected', update_callback) network.register_callback('disconnected', update_callback)

View File

@ -690,7 +690,7 @@ class ElectrumWindow:
return return
coins = self.wallet.get_spendable_coins() coins = self.wallet.get_spendable_coins()
try: try:
tx = self.wallet.make_unsigned_transaction(coins, [('op_return', 'dummy_tx', amount)], fee) tx = self.wallet.make_unsigned_transaction(coins, [('op_return', 'dummy_tx', amount)], self.config, fee)
self.funds_error = False self.funds_error = False
except NotEnoughFunds: except NotEnoughFunds:
self.funds_error = True self.funds_error = True
@ -812,7 +812,7 @@ class ElectrumWindow:
password = None password = None
try: try:
tx = self.wallet.mktx( [(to_address, amount)], password, fee ) tx = self.wallet.mktx( [(to_address, amount)], password, self.config, fee)
except Exception as e: except Exception as e:
self.show_message(str(e)) self.show_message(str(e))
return return

View File

@ -99,3 +99,7 @@ class BTCAmountEdit(AmountEdit):
self.setText("") self.setText("")
else: else:
self.setText(format_satoshis_plain(amount, self.decimal_point())) self.setText(format_satoshis_plain(amount, self.decimal_point()))
class BTCkBEdit(BTCAmountEdit):
def _base_unit(self):
return BTCAmountEdit._base_unit(self) + '/kB'

View File

@ -46,7 +46,7 @@ from electrum import Imported_Wallet
from electrum import paymentrequest from electrum import paymentrequest
from electrum.contacts import Contacts from electrum.contacts import Contacts
from amountedit import AmountEdit, BTCAmountEdit, MyLineEdit from amountedit import AmountEdit, BTCAmountEdit, MyLineEdit, BTCkBEdit
from network_dialog import NetworkDialog from network_dialog import NetworkDialog
from qrcodewidget import QRCodeWidget, QRDialog from qrcodewidget import QRCodeWidget, QRDialog
from qrtextedit import ScanQRTextEdit, ShowQRTextEdit from qrtextedit import ScanQRTextEdit, ShowQRTextEdit
@ -980,7 +980,8 @@ class ElectrumWindow(QMainWindow):
output = ('address', addr, sendable) output = ('address', addr, sendable)
dummy_tx = Transaction.from_io(inputs, [output]) dummy_tx = Transaction.from_io(inputs, [output])
if not self.fee_e.isModified(): if not self.fee_e.isModified():
self.fee_e.setAmount(self.wallet.estimated_fee(dummy_tx)) fee_per_kb = self.wallet.fee_per_kb(self.config)
self.fee_e.setAmount(self.wallet.estimated_fee(dummy_tx, fee_per_kb))
self.amount_e.setAmount(max(0, sendable - self.fee_e.get_amount())) self.amount_e.setAmount(max(0, sendable - self.fee_e.get_amount()))
self.amount_e.textEdited.emit("") self.amount_e.textEdited.emit("")
@ -1059,7 +1060,7 @@ class ElectrumWindow(QMainWindow):
addr = self.payto_e.payto_address if self.payto_e.payto_address else self.dummy_address addr = self.payto_e.payto_address if self.payto_e.payto_address else self.dummy_address
outputs = [('address', addr, amount)] outputs = [('address', addr, amount)]
try: try:
tx = self.wallet.make_unsigned_transaction(self.get_coins(), outputs, fee) tx = self.wallet.make_unsigned_transaction(self.get_coins(), outputs, self.config, fee)
self.not_enough_funds = False self.not_enough_funds = False
except NotEnoughFunds: except NotEnoughFunds:
self.not_enough_funds = True self.not_enough_funds = True
@ -1195,7 +1196,7 @@ class ElectrumWindow(QMainWindow):
return return
outputs, fee, tx_desc, coins = r outputs, fee, tx_desc, coins = r
try: try:
tx = self.wallet.make_unsigned_transaction(coins, outputs, fee) tx = self.wallet.make_unsigned_transaction(coins, outputs, self.config, fee)
if not tx: if not tx:
raise BaseException(_("Insufficient funds")) raise BaseException(_("Insufficient funds"))
except Exception as e: except Exception as e:
@ -2477,7 +2478,7 @@ class ElectrumWindow(QMainWindow):
if not d.exec_(): if not d.exec_():
return return
fee = self.wallet.fee_per_kb fee = self.wallet.fee_per_kb(self.config)
tx = Transaction.sweep(get_pk(), self.network, get_address(), fee) tx = Transaction.sweep(get_pk(), self.network, get_address(), fee)
self.show_transaction(tx) self.show_transaction(tx)
@ -2563,21 +2564,44 @@ class ElectrumWindow(QMainWindow):
nz.valueChanged.connect(on_nz) nz.valueChanged.connect(on_nz)
gui_widgets.append((nz_label, nz)) gui_widgets.append((nz_label, nz))
fee_help = _('Fee per kilobyte of transaction.') + '\n' \ msg = _('Fee per kilobyte of transaction.') + '\n' \
+ _('Recommended value') + ': ' + self.format_amount(bitcoin.RECOMMENDED_FEE) + ' ' + self.base_unit() + _('If you enable dynamic fees, your client will use a value recommended by the server, and this parameter will be used as upper bound.')
fee_label = HelpLabel(_('Transaction fee per kb') + ':', fee_help) fee_label = HelpLabel(_('Transaction fee per kb') + ':', msg)
fee_e = BTCAmountEdit(self.get_decimal_point) fee_e = BTCkBEdit(self.get_decimal_point)
fee_e.setAmount(self.wallet.fee_per_kb) fee_e.setAmount(self.config.get('fee_per_kb', bitcoin.RECOMMENDED_FEE))
if not self.config.is_modifiable('fee_per_kb'):
for w in [fee_e, fee_label]: w.setEnabled(False)
def on_fee(is_done): def on_fee(is_done):
self.wallet.set_fee(fee_e.get_amount() or 0, is_done) v = fee_e.get_amount() or 0
self.wallet.set_fee(v)
self.config.set_key('fee_per_kb', v, is_done)
if not is_done: if not is_done:
self.update_fee() self.update_fee()
fee_e.editingFinished.connect(lambda: on_fee(True)) fee_e.editingFinished.connect(lambda: on_fee(True))
fee_e.textEdited.connect(lambda: on_fee(False)) fee_e.textEdited.connect(lambda: on_fee(False))
tx_widgets.append((fee_label, fee_e)) tx_widgets.append((fee_label, fee_e))
dynfee_cb = QCheckBox(_('Dynamic fees'))
dynfee_cb.setChecked(self.config.get('dynamic_fees', False))
dynfee_sl = QSlider(Qt.Horizontal, self)
dynfee_sl.setValue(self.config.get('fee_factor', 50))
dynfee_sl.setToolTip("Fee Multiplier. Min = 50%, Max = 150%")
tx_widgets.append((dynfee_cb, dynfee_sl))
def update_feeperkb():
fee_e.setAmount(self.wallet.fee_per_kb(self.config))
b = self.config.get('dynamic_fees')
dynfee_sl.setHidden(not b)
fee_e.setEnabled(not b)
def fee_factor_changed(b):
self.config.set_key('fee_factor', b, False)
update_feeperkb()
def on_dynfee(x):
dynfee = x == Qt.Checked
self.config.set_key('dynamic_fees', dynfee)
update_feeperkb()
dynfee_cb.stateChanged.connect(on_dynfee)
dynfee_sl.valueChanged[int].connect(fee_factor_changed)
update_feeperkb()
msg = _('OpenAlias record, used to receive coins and to sign payment requests.') + '\n\n'\ msg = _('OpenAlias record, used to receive coins and to sign payment requests.') + '\n\n'\
+ _('The following alias providers are available:') + '\n'\ + _('The following alias providers are available:') + '\n'\
+ '\n'.join(['https://cryptoname.co/', 'http://xmr.link']) + '\n\n'\ + '\n'.join(['https://cryptoname.co/', 'http://xmr.link']) + '\n\n'\
@ -2644,7 +2668,7 @@ class ElectrumWindow(QMainWindow):
self.update_history_tab() self.update_history_tab()
self.update_receive_tab() self.update_receive_tab()
self.update_address_tab() self.update_address_tab()
fee_e.setAmount(self.wallet.fee_per_kb) fee_e.setAmount(self.wallet.fee_per_kb(self.config))
self.update_status() self.update_status()
unit_combo.currentIndexChanged.connect(on_unit) unit_combo.currentIndexChanged.connect(on_unit)
gui_widgets.append((unit_label, unit_combo)) gui_widgets.append((unit_label, unit_combo))

View File

@ -201,7 +201,7 @@ class ElectrumGui:
if c == "n": return if c == "n": return
try: try:
tx = self.wallet.mktx( [(self.str_recipient, amount)], password, fee) tx = self.wallet.mktx( [(self.str_recipient, amount)], password, self.config, fee)
except Exception as e: except Exception as e:
print(str(e)) print(str(e))
return return

View File

@ -314,7 +314,7 @@ class ElectrumGui:
password = None password = None
try: try:
tx = self.wallet.mktx( [(self.str_recipient, amount)], password, fee) tx = self.wallet.mktx( [(self.str_recipient, amount)], password, self.config, fee)
except Exception as e: except Exception as e:
self.show_message(str(e)) self.show_message(str(e))
return return

View File

@ -396,7 +396,7 @@ class Commands:
final_outputs.append(('address', address, amount)) final_outputs.append(('address', address, amount))
coins = self.wallet.get_spendable_coins(domain) coins = self.wallet.get_spendable_coins(domain)
tx = self.wallet.make_unsigned_transaction(coins, final_outputs, fee, change_addr) tx = self.wallet.make_unsigned_transaction(coins, final_outputs, self.config, fee, change_addr)
str(tx) #this serializes str(tx) #this serializes
if not unsigned: if not unsigned:
self.wallet.sign_transaction(tx, self.password) self.wallet.sign_transaction(tx, self.password)

View File

@ -235,12 +235,15 @@ class Network(util.DaemonThread):
self.interface.send_request({'method':'blockchain.address.subscribe','params':[addr]}) self.interface.send_request({'method':'blockchain.address.subscribe','params':[addr]})
self.interface.send_request({'method':'server.banner','params':[]}) self.interface.send_request({'method':'server.banner','params':[]})
self.interface.send_request({'method':'server.peers.subscribe','params':[]}) self.interface.send_request({'method':'server.peers.subscribe','params':[]})
self.interface.send_request({'method':'blockchain.estimatefee','params':[2]})
def get_status_value(self, key): def get_status_value(self, key):
if key == 'status': if key == 'status':
value = self.connection_status value = self.connection_status
elif key == 'banner': elif key == 'banner':
value = self.banner value = self.banner
elif key == 'fee':
value = self.fee
elif key == 'updated': elif key == 'updated':
value = (self.get_local_height(), self.get_server_height()) value = (self.get_local_height(), self.get_server_height())
elif key == 'servers': elif key == 'servers':
@ -425,6 +428,11 @@ class Network(util.DaemonThread):
elif method == 'server.banner': elif method == 'server.banner':
self.banner = result self.banner = result
self.notify('banner') self.notify('banner')
elif method == 'blockchain.estimatefee':
from bitcoin import COIN
self.fee = int(result * COIN)
self.print_error("recommended fee", self.fee)
self.notify('fee')
elif method == 'blockchain.address.subscribe': elif method == 'blockchain.address.subscribe':
addr = response.get('params')[0] addr = response.get('params')[0]
self.addr_responses[addr] = result self.addr_responses[addr] = result

View File

@ -62,6 +62,8 @@ class NetworkProxy(util.DaemonThread):
self.server_height = 0 self.server_height = 0
self.interfaces = [] self.interfaces = []
self.jobs = [] self.jobs = []
# value returned by estimatefee
self.fee = None
def run(self): def run(self):
@ -90,6 +92,8 @@ class NetworkProxy(util.DaemonThread):
self.status = value self.status = value
elif key == 'banner': elif key == 'banner':
self.banner = value self.banner = value
elif key == 'fee':
self.fee = value
elif key == 'updated': elif key == 'updated':
self.blockchain_height, self.server_height = value self.blockchain_height, self.server_height = value
elif key == 'servers': elif key == 'servers':

View File

@ -143,6 +143,7 @@ class Abstract_Wallet(object):
""" """
def __init__(self, storage): def __init__(self, storage):
self.storage = storage self.storage = storage
self.network = None
self.electrum_version = ELECTRUM_VERSION self.electrum_version = ELECTRUM_VERSION
self.gap_limit_for_change = 6 # constant self.gap_limit_for_change = 6 # constant
# saved fields # saved fields
@ -153,9 +154,7 @@ class Abstract_Wallet(object):
self.labels = storage.get('labels', {}) self.labels = storage.get('labels', {})
self.frozen_addresses = set(storage.get('frozen_addresses',[])) self.frozen_addresses = set(storage.get('frozen_addresses',[]))
self.stored_height = storage.get('stored_height', 0) # last known height (for offline mode) self.stored_height = storage.get('stored_height', 0) # last known height (for offline mode)
self.history = storage.get('addr_history',{}) # address -> list(txid, height) self.history = storage.get('addr_history',{}) # address -> list(txid, height)
self.fee_per_kb = int(storage.get('fee_per_kb', RECOMMENDED_FEE))
# This attribute is set when wallet.start_threads is called. # This attribute is set when wallet.start_threads is called.
self.synchronizer = None self.synchronizer = None
@ -674,10 +673,6 @@ class Abstract_Wallet(object):
xx += x xx += x
return cc, uu, xx return cc, uu, xx
def set_fee(self, fee, save = True):
self.fee_per_kb = fee
self.storage.put('fee_per_kb', self.fee_per_kb, save)
def get_address_history(self, address): def get_address_history(self, address):
with self.lock: with self.lock:
return self.history.get(address, []) return self.history.get(address, [])
@ -873,23 +868,30 @@ class Abstract_Wallet(object):
return ', '.join(labels) return ', '.join(labels)
return '' return ''
def fee_per_kb(self, config):
b = config.get('dynamic_fees')
f = config.get('fee_factor', 50)
F = config.get('fee_per_kb', bitcoin.RECOMMENDED_FEE)
return min(F, self.network.fee*(50 + f)/100) if b and self.network and self.network.fee else F
def get_tx_fee(self, tx): def get_tx_fee(self, tx):
# this method can be overloaded # this method can be overloaded
return tx.get_fee() return tx.get_fee()
def estimated_fee(self, tx): def estimated_fee(self, tx, fee_per_kb):
estimated_size = len(tx.serialize(-1))/2 estimated_size = len(tx.serialize(-1))/2
fee = int(self.fee_per_kb*estimated_size/1000.) fee = int(fee_per_kb * estimated_size / 1000.)
if fee < MIN_RELAY_TX_FEE: # and tx.requires_fee(self): if fee < MIN_RELAY_TX_FEE: # and tx.requires_fee(self):
fee = MIN_RELAY_TX_FEE fee = MIN_RELAY_TX_FEE
return fee return fee
def make_unsigned_transaction(self, coins, outputs, fixed_fee=None, change_addr=None): def make_unsigned_transaction(self, coins, outputs, config, fixed_fee=None, change_addr=None):
# check outputs # check outputs
for type, data, value in outputs: for type, data, value in outputs:
if type == 'address': if type == 'address':
assert is_address(data), "Address " + data + " is invalid!" assert is_address(data), "Address " + data + " is invalid!"
fee_per_kb = self.fee_per_kb(config)
amount = sum(map(lambda x:x[2], outputs)) amount = sum(map(lambda x:x[2], outputs))
total = fee = 0 total = fee = 0
inputs = [] inputs = []
@ -903,7 +905,7 @@ class Abstract_Wallet(object):
# no need to estimate fee until we have reached desired amount # no need to estimate fee until we have reached desired amount
if total < amount: if total < amount:
continue continue
fee = fixed_fee if fixed_fee is not None else self.estimated_fee(tx) fee = fixed_fee if fixed_fee is not None else self.estimated_fee(tx, fee_per_kb)
if total >= amount + fee: if total >= amount + fee:
break break
else: else:
@ -914,7 +916,7 @@ class Abstract_Wallet(object):
if total - v >= amount + fee: if total - v >= amount + fee:
tx.inputs.remove(item) tx.inputs.remove(item)
total -= v total -= v
fee = fixed_fee if fixed_fee is not None else self.estimated_fee(tx) fee = fixed_fee if fixed_fee is not None else self.estimated_fee(tx, fee_per_kb)
else: else:
break break
print_error("using %d inputs"%len(tx.inputs)) print_error("using %d inputs"%len(tx.inputs))
@ -943,7 +945,7 @@ class Abstract_Wallet(object):
elif change_amount > DUST_THRESHOLD: elif change_amount > DUST_THRESHOLD:
tx.outputs.append(('address', change_addr, change_amount)) tx.outputs.append(('address', change_addr, change_amount))
# recompute fee including change output # recompute fee including change output
fee = self.estimated_fee(tx) fee = self.estimated_fee(tx, fee_per_kb)
# remove change output # remove change output
tx.outputs.pop() tx.outputs.pop()
# if change is still above dust threshold, re-add change output. # if change is still above dust threshold, re-add change output.
@ -962,9 +964,9 @@ class Abstract_Wallet(object):
run_hook('make_unsigned_transaction', tx) run_hook('make_unsigned_transaction', tx)
return tx return tx
def mktx(self, outputs, password, fee=None, change_addr=None, domain=None): def mktx(self, outputs, password, config, fee=None, change_addr=None, domain=None):
coins = self.get_spendable_coins(domain) coins = self.get_spendable_coins(domain)
tx = self.make_unsigned_transaction(coins, outputs, fee, change_addr) tx = self.make_unsigned_transaction(coins, outputs, config, fee, change_addr)
self.sign_transaction(tx, password) self.sign_transaction(tx, password)
return tx return tx

View File

@ -1,7 +1,7 @@
#!/usr/bin/env python #!/usr/bin/env python
import util, json import util, json
peers = util.get_peers() peers = util.get_peers()
results = util.send_request(peers, {'method':'blockchain.estimatefee','params':[1]}) results = util.send_request(peers, {'method':'blockchain.estimatefee','params':[2]})
print json.dumps(results, indent=4) print json.dumps(results, indent=4)