tdynamic fees - electrum - Electrum Bitcoin wallet HTML git clone https://git.parazyd.org/electrum DIR Log DIR Files DIR Refs DIR Submodules --- DIR commit 43880d452e9c0278ff114625d7871b06051647a0 DIR parent 959620db4646d4ae1d570fef661239197eb4c722 HTML Author: ThomasV <thomasv@gitorious> Date: Tue, 4 Aug 2015 07:15:54 +0200 dynamic fees Diffstat: M RELEASE-NOTES | 1 + M gui/android.py | 6 ++++-- M gui/gtk.py | 4 ++-- M gui/qt/amountedit.py | 4 ++++ M gui/qt/main_window.py | 52 ++++++++++++++++++++++--------- M gui/stdio.py | 2 +- M gui/text.py | 2 +- M lib/commands.py | 2 +- M lib/network.py | 8 ++++++++ M lib/network_proxy.py | 4 ++++ M lib/wallet.py | 30 ++++++++++++++++-------------- M scripts/estimate_fee | 2 +- 12 files changed, 81 insertions(+), 36 deletions(-) --- DIR diff --git a/RELEASE-NOTES b/RELEASE-NOTES t@@ -2,6 +2,7 @@ * Use ssl.PROTOCOL_TLSv1 * Fix DNSSEC issues with ECDSA signatures * Replace TLSLite dependency with minimal RSA implementation + * Dynamic fees, using estimatefee value returned by server # Release 2.4 * Payment to DNS names storing a Bitcoin addresses (OpenAlias) is DIR diff --git a/gui/android.py b/gui/android.py t@@ -444,7 +444,7 @@ def pay_to(recipient, amount, label): droid.dialogShow() try: - tx = wallet.mktx([('address', recipient, amount)], password) + tx = wallet.mktx([('address', recipient, amount)], password, config) except Exception as e: modal_dialog('error', e.message) droid.dialogDismiss() t@@ -895,12 +895,14 @@ menu_commands = ["send", "receive", "settings", "contacts", "main"] wallet = None network = None contacts = None +config = None class ElectrumGui: - def __init__(self, config, _network): + def __init__(self, _config, _network): global wallet, network, contacts network = _network + config = _config network.register_callback('updated', update_callback) network.register_callback('connected', update_callback) network.register_callback('disconnected', update_callback) DIR diff --git a/gui/gtk.py b/gui/gtk.py t@@ -690,7 +690,7 @@ class ElectrumWindow: return coins = self.wallet.get_spendable_coins() 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 except NotEnoughFunds: self.funds_error = True t@@ -812,7 +812,7 @@ class ElectrumWindow: password = None 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: self.show_message(str(e)) return DIR diff --git a/gui/qt/amountedit.py b/gui/qt/amountedit.py t@@ -99,3 +99,7 @@ class BTCAmountEdit(AmountEdit): self.setText("") else: self.setText(format_satoshis_plain(amount, self.decimal_point())) + +class BTCkBEdit(BTCAmountEdit): + def _base_unit(self): + return BTCAmountEdit._base_unit(self) + '/kB' DIR diff --git a/gui/qt/main_window.py b/gui/qt/main_window.py t@@ -46,7 +46,7 @@ from electrum import Imported_Wallet from electrum import paymentrequest from electrum.contacts import Contacts -from amountedit import AmountEdit, BTCAmountEdit, MyLineEdit +from amountedit import AmountEdit, BTCAmountEdit, MyLineEdit, BTCkBEdit from network_dialog import NetworkDialog from qrcodewidget import QRCodeWidget, QRDialog from qrtextedit import ScanQRTextEdit, ShowQRTextEdit t@@ -980,7 +980,8 @@ class ElectrumWindow(QMainWindow): output = ('address', addr, sendable) dummy_tx = Transaction.from_io(inputs, [output]) 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.textEdited.emit("") t@@ -1059,7 +1060,7 @@ class ElectrumWindow(QMainWindow): addr = self.payto_e.payto_address if self.payto_e.payto_address else self.dummy_address outputs = [('address', addr, amount)] 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 except NotEnoughFunds: self.not_enough_funds = True t@@ -1195,7 +1196,7 @@ class ElectrumWindow(QMainWindow): return outputs, fee, tx_desc, coins = r try: - tx = self.wallet.make_unsigned_transaction(coins, outputs, fee) + tx = self.wallet.make_unsigned_transaction(coins, outputs, self.config, fee) if not tx: raise BaseException(_("Insufficient funds")) except Exception as e: t@@ -2477,7 +2478,7 @@ class ElectrumWindow(QMainWindow): if not d.exec_(): 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) self.show_transaction(tx) t@@ -2563,21 +2564,44 @@ class ElectrumWindow(QMainWindow): nz.valueChanged.connect(on_nz) gui_widgets.append((nz_label, nz)) - fee_help = _('Fee per kilobyte of transaction.') + '\n' \ - + _('Recommended value') + ': ' + self.format_amount(bitcoin.RECOMMENDED_FEE) + ' ' + self.base_unit() - fee_label = HelpLabel(_('Transaction fee per kb') + ':', fee_help) - fee_e = BTCAmountEdit(self.get_decimal_point) - fee_e.setAmount(self.wallet.fee_per_kb) - if not self.config.is_modifiable('fee_per_kb'): - for w in [fee_e, fee_label]: w.setEnabled(False) + msg = _('Fee per kilobyte of transaction.') + '\n' \ + + _('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') + ':', msg) + fee_e = BTCkBEdit(self.get_decimal_point) + fee_e.setAmount(self.config.get('fee_per_kb', bitcoin.RECOMMENDED_FEE)) 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: self.update_fee() fee_e.editingFinished.connect(lambda: on_fee(True)) fee_e.textEdited.connect(lambda: on_fee(False)) 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'\ + _('The following alias providers are available:') + '\n'\ + '\n'.join(['https://cryptoname.co/', 'http://xmr.link']) + '\n\n'\ t@@ -2644,7 +2668,7 @@ class ElectrumWindow(QMainWindow): self.update_history_tab() self.update_receive_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() unit_combo.currentIndexChanged.connect(on_unit) gui_widgets.append((unit_label, unit_combo)) DIR diff --git a/gui/stdio.py b/gui/stdio.py t@@ -201,7 +201,7 @@ class ElectrumGui: if c == "n": return 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: print(str(e)) return DIR diff --git a/gui/text.py b/gui/text.py t@@ -314,7 +314,7 @@ class ElectrumGui: password = None 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: self.show_message(str(e)) return DIR diff --git a/lib/commands.py b/lib/commands.py t@@ -396,7 +396,7 @@ class Commands: final_outputs.append(('address', address, amount)) 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 if not unsigned: self.wallet.sign_transaction(tx, self.password) DIR diff --git a/lib/network.py b/lib/network.py t@@ -235,12 +235,15 @@ class Network(util.DaemonThread): self.interface.send_request({'method':'blockchain.address.subscribe','params':[addr]}) self.interface.send_request({'method':'server.banner','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): if key == 'status': value = self.connection_status elif key == 'banner': value = self.banner + elif key == 'fee': + value = self.fee elif key == 'updated': value = (self.get_local_height(), self.get_server_height()) elif key == 'servers': t@@ -425,6 +428,11 @@ class Network(util.DaemonThread): elif method == 'server.banner': self.banner = result 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': addr = response.get('params')[0] self.addr_responses[addr] = result DIR diff --git a/lib/network_proxy.py b/lib/network_proxy.py t@@ -62,6 +62,8 @@ class NetworkProxy(util.DaemonThread): self.server_height = 0 self.interfaces = [] self.jobs = [] + # value returned by estimatefee + self.fee = None def run(self): t@@ -90,6 +92,8 @@ class NetworkProxy(util.DaemonThread): self.status = value elif key == 'banner': self.banner = value + elif key == 'fee': + self.fee = value elif key == 'updated': self.blockchain_height, self.server_height = value elif key == 'servers': DIR diff --git a/lib/wallet.py b/lib/wallet.py t@@ -143,6 +143,7 @@ class Abstract_Wallet(object): """ def __init__(self, storage): self.storage = storage + self.network = None self.electrum_version = ELECTRUM_VERSION self.gap_limit_for_change = 6 # constant # saved fields t@@ -153,9 +154,7 @@ class Abstract_Wallet(object): self.labels = storage.get('labels', {}) self.frozen_addresses = set(storage.get('frozen_addresses',[])) 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.fee_per_kb = int(storage.get('fee_per_kb', RECOMMENDED_FEE)) # This attribute is set when wallet.start_threads is called. self.synchronizer = None t@@ -674,10 +673,6 @@ class Abstract_Wallet(object): xx += x 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): with self.lock: return self.history.get(address, []) t@@ -873,23 +868,30 @@ class Abstract_Wallet(object): return ', '.join(labels) 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): # this method can be overloaded return tx.get_fee() - def estimated_fee(self, tx): + def estimated_fee(self, tx, fee_per_kb): 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): fee = MIN_RELAY_TX_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 for type, data, value in outputs: if type == 'address': assert is_address(data), "Address " + data + " is invalid!" + fee_per_kb = self.fee_per_kb(config) amount = sum(map(lambda x:x[2], outputs)) total = fee = 0 inputs = [] t@@ -903,7 +905,7 @@ class Abstract_Wallet(object): # no need to estimate fee until we have reached desired amount if total < amount: 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: break else: t@@ -914,7 +916,7 @@ class Abstract_Wallet(object): if total - v >= amount + fee: tx.inputs.remove(item) 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: break print_error("using %d inputs"%len(tx.inputs)) t@@ -943,7 +945,7 @@ class Abstract_Wallet(object): elif change_amount > DUST_THRESHOLD: tx.outputs.append(('address', change_addr, change_amount)) # recompute fee including change output - fee = self.estimated_fee(tx) + fee = self.estimated_fee(tx, fee_per_kb) # remove change output tx.outputs.pop() # if change is still above dust threshold, re-add change output. t@@ -962,9 +964,9 @@ class Abstract_Wallet(object): run_hook('make_unsigned_transaction', 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) - 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) return tx DIR diff --git a/scripts/estimate_fee b/scripts/estimate_fee t@@ -1,7 +1,7 @@ #!/usr/bin/env python import util, json 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)