diff --git a/RELEASE-NOTES b/RELEASE-NOTES index 86db7b1b..9134ba56 100644 --- a/RELEASE-NOTES +++ b/RELEASE-NOTES @@ -1,3 +1,15 @@ + +# Release 1.7.2: + +* Transactions that are in the same block are displayed in chronological order in the history. +* The client computes transaction priority and rejects zero-fee transactions that need a fee. +* The default fee was lowered to 200 uBTC per kb. +* Due to an internal format change, your history may be pruned when + you open your wallet for the first time after upgrading to 1.7.2. If + this is the case, please visit a full server to restore your full + history. You will only need to do that once. + + # Release 1.7.1: bugfixes. diff --git a/app.fil b/app.fil index 3ebc5174..1eaee6cf 100644 --- a/app.fil +++ b/app.fil @@ -2,3 +2,8 @@ gui/gui_gtk.py gui/gui_classic.py gui/gui_lite.py gui/history_widget.py +plugins/aliases.py +plugins/pointofsale.py +plugins/labels.py +plugins/qrscanner.py +plugins/virtualkeyboard.py diff --git a/electrum b/electrum index 28e30533..15fcb60d 100755 --- a/electrum +++ b/electrum @@ -72,7 +72,7 @@ def arg_parser(): parser.add_option("-a", "--all", action="store_true", dest="show_all", default=False, help="show all addresses") parser.add_option("-b", "--balance", action="store_true", dest="show_balance", default=False, help="show the balance of listed addresses") parser.add_option("-l", "--labels", action="store_true", dest="show_labels", default=False, help="show the labels of listed addresses") - parser.add_option("-f", "--fee", dest="tx_fee", default="0.005", help="set tx fee") + parser.add_option("-f", "--fee", dest="tx_fee", default=None, help="set tx fee") parser.add_option("-F", "--fromaddr", dest="from_addr", default=None, help="set source address for payto/mktx. if it isn't in the wallet, it will ask for the private key unless supplied in the format public_key:private_key. It's not saved in the wallet.") parser.add_option("-c", "--changeaddr", dest="change_addr", default=None, help="set the change address for payto/mktx. default is a spare address, or the source address if it's not in the wallet") parser.add_option("-s", "--server", dest="server", default=None, help="set server host:port:protocol, where protocol is t or h") diff --git a/gui/gui_classic.py b/gui/gui_classic.py index 5c48205d..585c4b4d 100644 --- a/gui/gui_classic.py +++ b/gui/gui_classic.py @@ -32,6 +32,7 @@ from PyQt4.QtCore import * import PyQt4.QtCore as QtCore import PyQt4.QtGui as QtGui from electrum.interface import DEFAULT_SERVERS +from electrum.bitcoin import MIN_RELAY_TX_FEE try: import icons_rc @@ -795,6 +796,10 @@ class ElectrumWindow(QMainWindow): self.show_message(str(e)) return + if tx.requires_fee(self.wallet.verifier) and fee < MIN_RELAY_TX_FEE: + QMessageBox.warning(self, _('Error'), _("This transaction requires a higher fee, or it will not be propagated by the network."), _('OK')) + return + self.run_hook('send_tx', tx) if label: @@ -1928,11 +1933,11 @@ class ElectrumWindow(QMainWindow): fee_e = QLineEdit() fee_e.setText("%s"% str( Decimal( self.wallet.fee)/100000000 ) ) grid_wallet.addWidget(fee_e, 0, 2) - msg = _('Fee per transaction input. Transactions involving multiple inputs tend to require a higher fee.') + ' ' \ - + _('Recommended value') + ': 0.001' + msg = _('Fee per kilobyte of transaction.') + ' ' \ + + _('Recommended value') + ': 0.0001' grid_wallet.addWidget(HelpButton(msg), 0, 3) fee_e.textChanged.connect(lambda: numbify(fee_e,False)) - if not self.config.is_modifiable('fee'): + if not self.config.is_modifiable('fee_per_kb'): for w in [fee_e, fee_label]: w.setEnabled(False) usechange_label = QLabel(_('Use change addresses')) diff --git a/gui/gui_gtk.py b/gui/gui_gtk.py index 8568f7f5..2cec19b7 100644 --- a/gui/gui_gtk.py +++ b/gui/gui_gtk.py @@ -35,6 +35,7 @@ MONOSPACE_FONT = 'Lucida Console' if platform.system() == 'Windows' else 'monosp from electrum.util import format_satoshis from electrum.interface import DEFAULT_SERVERS +from electrum.bitcoin import MIN_RELAY_TX_FEE def numbify(entry, is_int = False): text = entry.get_text().strip() @@ -198,7 +199,7 @@ def run_settings_dialog(wallet, parent): fee_entry.connect('changed', numbify, False) fee_entry.show() fee.pack_start(fee_entry,False,False, 10) - add_help_button(fee, 'Fee per transaction input. Transactions involving multiple inputs tend to have a higher fee. Recommended value:0.0005') + add_help_button(fee, 'Fee per kilobyte of transaction. Recommended value:0.0001') fee.show() vbox.pack_start(fee, False,False, 5) @@ -843,6 +844,11 @@ class ElectrumWindow: except BaseException, e: self.show_message(str(e)) return + + if tx.requires_fee(self.wallet.verifier) and fee < MIN_RELAY_TX_FEE: + self.show_message( "This transaction requires a higher fee, or it will not be propagated by the network." ) + return + if label: self.wallet.labels[tx.hash()] = label diff --git a/gui/gui_text.py b/gui/gui_text.py index 77d70bed..58c67329 100644 --- a/gui/gui_text.py +++ b/gui/gui_text.py @@ -318,14 +318,14 @@ class ElectrumGui: def settings_dialog(self): out = self.run_dialog('Settings', [ {'label':'Default GUI', 'type':'list', 'choices':['classic','lite','gtk','text'], 'value':self.config.get('gui')}, - {'label':'Default fee', 'type':'satoshis', 'value': format_satoshis(self.config.get('fee')).strip() } + {'label':'Default fee', 'type':'satoshis', 'value': format_satoshis(self.config.get('fee_per_kb')).strip() } ], buttons = 1) if out: if out.get('Default GUI'): self.config.set_key('gui', out['Default GUI'], True) if out.get('Default fee'): fee = int ( Decimal( out['Default fee']) *10000000 ) - self.config.set_key('fee', fee, True) + self.config.set_key('fee_per_kb', fee, True) def password_dialog(self): diff --git a/lib/bitcoin.py b/lib/bitcoin.py index 6a8c4864..41f93959 100644 --- a/lib/bitcoin.py +++ b/lib/bitcoin.py @@ -577,6 +577,7 @@ class BIP32Sequence: ################################## transactions +MIN_RELAY_TX_FEE = 10000 class Transaction: @@ -876,6 +877,26 @@ class Transaction: return out + def requires_fee(self, verifier): + # see https://en.bitcoin.it/wiki/Transaction_fees + threshold = 57600000 + size = len(self.raw)/2 + if size >= 10000: + return True + + for o in self.outputs: + value = o[1] + if value < 1000000: + return True + sum = 0 + for i in self.inputs: + age = verifier.get_confirmations(i["tx_hash"])[0] + sum += i["value"] * age + priority = sum / size + print_error(priority, threshold) + return priority < threshold + + def test_bip32(): diff --git a/lib/deserialize.py b/lib/deserialize.py index 96972ce3..85ab97c4 100644 --- a/lib/deserialize.py +++ b/lib/deserialize.py @@ -323,7 +323,12 @@ def match_decoded(decoded, to_match): return True def get_address_from_input_script(bytes): - decoded = [ x for x in script_GetOp(bytes) ] + try: + decoded = [ x for x in script_GetOp(bytes) ] + except: + # coinbase transactions raise an exception + print_error("cannot find address in input script", bytes.encode('hex')) + return [], [], "(None)" # non-generated TxIn transactions push a signature # (seventy-something bytes) and then their public key diff --git a/lib/verifier.py b/lib/verifier.py index 81a76619..913aa2f0 100644 --- a/lib/verifier.py +++ b/lib/verifier.py @@ -35,7 +35,7 @@ class WalletVerifier(threading.Thread): self.transactions = {} # requested verifications (with height sent by the requestor) self.interface.register_channel('verifier') - self.verified_tx = config.get('verified_tx2',{}) # height, timestamp of verified transactions + self.verified_tx = config.get('verified_tx3',{}) # height, timestamp of verified transactions self.merkle_roots = config.get('merkle_roots',{}) # hashed by me self.targets = config.get('targets',{}) # compute targets @@ -50,7 +50,7 @@ class WalletVerifier(threading.Thread): """ return the number of confirmations of a monitored transaction. """ with self.lock: if tx in self.verified_tx: - height, timestamp = self.verified_tx[tx] + height, timestamp, pos = self.verified_tx[tx] conf = (self.local_height - height + 1) else: conf = 0 @@ -183,7 +183,8 @@ class WalletVerifier(threading.Thread): def verify_merkle(self, tx_hash, result): tx_height = result.get('block_height') - self.merkle_roots[tx_hash] = self.hash_merkle_root(result['merkle'], tx_hash, result.get('pos')) + pos = result.get('pos') + self.merkle_roots[tx_hash] = self.hash_merkle_root(result['merkle'], tx_hash, pos) header = self.read_header(tx_height) if not header: return assert header.get('merkle_root') == self.merkle_roots[tx_hash] @@ -191,9 +192,9 @@ class WalletVerifier(threading.Thread): header = self.read_header(tx_height) timestamp = header.get('timestamp') with self.lock: - self.verified_tx[tx_hash] = (tx_height, timestamp) + self.verified_tx[tx_hash] = (tx_height, timestamp, pos) print_error("verified %s"%tx_hash) - self.config.set_key('verified_tx2', self.verified_tx, True) + self.config.set_key('verified_tx3', self.verified_tx, True) self.interface.trigger_callback('updated') diff --git a/lib/version.py b/lib/version.py index 2c577e67..b6dfd318 100644 --- a/lib/version.py +++ b/lib/version.py @@ -1,4 +1,4 @@ -ELECTRUM_VERSION = "1.7.1" # version of the client package +ELECTRUM_VERSION = "1.7.2" # version of the client package PROTOCOL_VERSION = '0.6' # protocol version requested SEED_VERSION = 4 # bump this every time the seed generation is modified -TRANSLATION_ID = 3992 # version of the wiki page +TRANSLATION_ID = 4012 # version of the wiki page diff --git a/lib/wallet.py b/lib/wallet.py index fee23d32..cfb27376 100644 --- a/lib/wallet.py +++ b/lib/wallet.py @@ -74,7 +74,7 @@ class Wallet: self.seed_version = config.get('seed_version', SEED_VERSION) self.gap_limit = config.get('gap_limit', 5) self.use_change = config.get('use_change',True) - self.fee = int(config.get('fee',100000)) + self.fee = int(config.get('fee_per_kb',20000)) self.num_zeros = int(config.get('num_zeros',0)) self.use_encryption = config.get('use_encryption', False) self.seed = config.get('seed', '') # encrypted @@ -103,7 +103,6 @@ class Wallet: # not saved self.prevout_values = {} # my own transaction outputs self.spent_outputs = [] - self.receipt = None # next receipt # spv self.verifier = None @@ -114,6 +113,7 @@ class Wallet: self.up_to_date = False self.lock = threading.Lock() + self.transaction_lock = threading.Lock() self.tx_event = threading.Event() if self.seed_version != SEED_VERSION: @@ -429,13 +429,10 @@ class Wallet: def update_tx_outputs(self, tx_hash): tx = self.transactions.get(tx_hash) - i = 0 - for item in tx.outputs: - addr, value = item + + for i, (addr, value) in enumerate(tx.outputs): key = tx_hash+ ':%d'%i - with self.lock: - self.prevout_values[key] = value - i += 1 + self.prevout_values[key] = value for item in tx.inputs: if self.is_mine(item.get('address')): @@ -453,13 +450,11 @@ class Wallet: for tx_hash, tx_height in h: tx = self.transactions.get(tx_hash) if not tx: continue - i = 0 - for item in tx.outputs: - addr, value = item + + for i, (addr, value) in enumerate(tx.outputs): if addr == address: key = tx_hash + ':%d'%i received_coins.append(key) - i +=1 for tx_hash, tx_height in h: tx = self.transactions.get(tx_hash) @@ -474,13 +469,10 @@ class Wallet: if key in received_coins: v -= value - i = 0 - for item in tx.outputs: - addr, value = item + for i, (addr, value) in enumerate(tx.outputs): key = tx_hash + ':%d'%i if addr == address: v += value - i += 1 if tx_height: c += v @@ -565,14 +557,20 @@ class Wallet: total += v inputs.append( item ) - fee = self.fee*len(inputs) if fixed_fee is None else fixed_fee + if fixed_fee is None: + estimated_size = len(inputs) * 180 + 80 # this assumes non-compressed keys + fee = self.fee * int(round(estimated_size/1024.)) + if fee == 0: fee = self.fee + else: + fee = fixed_fee if total >= amount + fee: break else: - #print "not enough funds: %s %s"%(format_satoshis(total), format_satoshis(fee)) inputs = [] return inputs, total, fee + + def add_tx_change( self, outputs, amount, fee, total, change_addr=None ): change_amount = total - ( amount + fee ) if change_amount != 0: @@ -602,19 +600,17 @@ class Wallet: def receive_tx_callback(self, tx_hash, tx, tx_height): + if not self.check_new_tx(tx_hash, tx): # may happen due to pruning print_error("received transaction that is no longer referenced in history", tx_hash) return - with self.lock: + with self.transaction_lock: self.transactions[tx_hash] = tx - - #tx_height = tx.get('height') - if self.verifier and tx_height>0: - self.verifier.add(tx_hash, tx_height) - - self.update_tx_outputs(tx_hash) + if self.verifier and tx_height>0: + self.verifier.add(tx_hash, tx_height) + self.update_tx_outputs(tx_hash) self.save() @@ -636,29 +632,29 @@ class Wallet: def get_tx_history(self): - with self.lock: + with self.transaction_lock: history = self.transactions.items() - history.sort(key = lambda x: self.verifier.get_height(x[0]) if self.verifier.get_height(x[0]) else 1e12) - result = [] + history.sort(key = lambda x: self.verifier.verified_tx.get(x[0]) if self.verifier.verified_tx.get(x[0]) else (1e12,0,0)) + result = [] - balance = 0 - for tx_hash, tx in history: - is_mine, v, fee = self.get_tx_value(tx) - if v is not None: balance += v - c, u = self.get_balance() + balance = 0 + for tx_hash, tx in history: + is_mine, v, fee = self.get_tx_value(tx) + if v is not None: balance += v + c, u = self.get_balance() - if balance != c+u: - v_str = format_satoshis( c+u - balance, True, self.num_zeros) - result.append( ('', 1000, 0, c+u-balance, None, c+u-balance, None ) ) + if balance != c+u: + #v_str = format_satoshis( c+u - balance, True, self.num_zeros) + result.append( ('', 1000, 0, c+u-balance, None, c+u-balance, None ) ) - balance = c + u - balance - for tx_hash, tx in history: - conf, timestamp = self.verifier.get_confirmations(tx_hash) if self.verifier else (None, None) - is_mine, value, fee = self.get_tx_value(tx) - if value is not None: - balance += value + balance = c + u - balance + for tx_hash, tx in history: + conf, timestamp = self.verifier.get_confirmations(tx_hash) if self.verifier else (None, None) + is_mine, value, fee = self.get_tx_value(tx) + if value is not None: + balance += value - result.append( (tx_hash, conf, is_mine, value, fee, balance, timestamp) ) + result.append( (tx_hash, conf, is_mine, value, fee, balance, timestamp) ) return result @@ -768,9 +764,6 @@ class Wallet: out = self.tx_result if out != tx_hash: return False, "error: " + out - if self.receipt: - self.receipts[tx_hash] = self.receipt - self.receipt = None return True, out @@ -831,7 +824,7 @@ class Wallet: s = { 'use_encryption': self.use_encryption, 'use_change': self.use_change, - 'fee': self.fee, + 'fee_per_kb': self.fee, 'accounts': self.accounts, 'addr_history': self.history, 'labels': self.labels, @@ -859,6 +852,12 @@ class Wallet: self.verifier.add(tx_hash, tx_height) + # if we are on a pruning server, remove unverified transactions + vr = self.verifier.transactions.keys() + self.verifier.verified_tx.keys() + for tx_hash in self.transactions.keys(): + if tx_hash not in vr: + self.transactions.pop(tx_hash) + def check_new_history(self, addr, hist): @@ -903,6 +902,7 @@ class Wallet: ext_requests.append( ('blockchain.address.get_history', [_addr]) ) ext_h = self.interface.synchronous_get(ext_requests) + print_error("sync:", ext_requests, ext_h) height = None for h in ext_h: if h == ['*']: continue diff --git a/make_packages b/make_packages index 73abfebc..0d6b85b1 100755 --- a/make_packages +++ b/make_packages @@ -25,7 +25,8 @@ if __name__ == '__main__': shutil.copytree("aes",'dist/e4a-%s/aes'%version) shutil.copytree("lib",'dist/e4a-%s/lib'%version) os.mkdir('dist/e4a-%s/gui'%version) - shutil.copy("gui/gui_android.py",'dist/e4a-%s/gui'%version) + for n in ['gui_android.py', 'pyqrnative.py', 'bmp.py']: + shutil.copy("gui/%s"%n,'dist/e4a-%s/gui'%version) open('dist/e4a-%s/gui/__init__.py'%version,'w').close() os.chdir("dist")