electrum

Electrum Bitcoin wallet
git clone https://git.parazyd.org/electrum
Log | Files | Refs | Submodules

commit 0e7e7e3dc5fad0705bf64ead8350c12c1f432d0c
parent 603345a17223814d4e8ad719b36e49e413abed0e
Author: ThomasV <thomasv@electrum.org>
Date:   Tue, 30 Jan 2018 00:18:44 +0100

Merge branch 'local_tx'

Diffstat:
Mgui/qt/history_list.py | 40+++++++++++++++++++++++++++++++++++++++-
Mgui/qt/util.py | 34++++++++++++++++++++++++++++++++++
Micons.qrc | 1+
Aicons/offline_tx.png | 0
Mlib/commands.py | 9+++++++++
Mlib/synchronizer.py | 2+-
Mlib/wallet.py | 76+++++++++++++++++++++++++++++++++++++++++++++-------------------------------
7 files changed, 129 insertions(+), 33 deletions(-)

diff --git a/gui/qt/history_list.py b/gui/qt/history_list.py @@ -37,6 +37,7 @@ TX_ICONS = [ "warning.png", "unconfirmed.png", "unconfirmed.png", + "offline_tx.png", "clock1.png", "clock2.png", "clock3.png", @@ -46,11 +47,12 @@ TX_ICONS = [ ] -class HistoryList(MyTreeWidget): +class HistoryList(MyTreeWidget, AcceptFileDragDrop): filter_columns = [2, 3, 4] # Date, Description, Amount def __init__(self, parent=None): MyTreeWidget.__init__(self, parent, self.create_menu, [], 3) + AcceptFileDragDrop.__init__(self, ".txn") self.refresh_headers() self.setColumnHidden(1, True) @@ -158,11 +160,15 @@ class HistoryList(MyTreeWidget): menu = QMenu() + if height == -2: + menu.addAction(_("Remove"), lambda: self.remove_local_tx(tx_hash)) + menu.addAction(_("Copy %s")%column_title, lambda: self.parent.app.clipboard().setText(column_data)) if column in self.editable_columns: menu.addAction(_("Edit %s")%column_title, lambda: self.editItem(item, column)) menu.addAction(_("Details"), lambda: self.parent.show_transaction(tx)) + if is_unconfirmed and tx: rbf = is_mine and not tx.is_final() if rbf: @@ -176,3 +182,35 @@ class HistoryList(MyTreeWidget): if tx_URL: menu.addAction(_("View on block explorer"), lambda: webbrowser.open(tx_URL)) menu.exec_(self.viewport().mapToGlobal(position)) + + def remove_local_tx(self, delete_tx): + to_delete = {delete_tx} + to_delete |= self.wallet.get_depending_transactions(delete_tx) + + question = _("Are you sure you want to remove this transaction?") + if len(to_delete) > 1: + question = _( + "Are you sure you want to remove this transaction and {} child transactions?".format(len(to_delete) - 1) + ) + + answer = QMessageBox.question(self.parent, _("Please confirm"), question, QMessageBox.Yes, QMessageBox.No) + if answer == QMessageBox.No: + return + for tx in to_delete: + self.wallet.remove_transaction(tx) + self.wallet.save_transactions(write=True) + root = self.invisibleRootItem() + child_count = root.childCount() + _offset = 0 + for i in range(child_count): + item = root.child(i - _offset) + if item.data(0, Qt.UserRole) in to_delete: + root.removeChild(item) + _offset += 1 + + def onFileAdded(self, fn): + with open(fn) as f: + tx = self.parent.tx_from_text(f.read()) + self.wallet.add_transaction(tx.txid(), tx) + self.wallet.save_transactions(write=True) + self.on_update() diff --git a/gui/qt/util.py b/gui/qt/util.py @@ -635,6 +635,40 @@ class ColorScheme: if ColorScheme.has_dark_background(widget): ColorScheme.dark_scheme = True + +class AcceptFileDragDrop: + def __init__(self, file_type=""): + assert isinstance(self, QWidget) + self.setAcceptDrops(True) + self.file_type = file_type + + def validateEvent(self, event): + if not event.mimeData().hasUrls(): + event.ignore() + return False + for url in event.mimeData().urls(): + if not url.toLocalFile().endswith(self.file_type): + event.ignore() + return False + event.accept() + return True + + def dragEnterEvent(self, event): + self.validateEvent(event) + + def dragMoveEvent(self, event): + if self.validateEvent(event): + event.setDropAction(Qt.CopyAction) + + def dropEvent(self, event): + if self.validateEvent(event): + for url in event.mimeData().urls(): + self.onFileAdded(url.toLocalFile()) + + def onFileAdded(self, fn): + raise NotImplementedError() + + if __name__ == "__main__": app = QApplication([]) t = WaitingDialog(None, 'testing ...', lambda: [time.sleep(1)], lambda x: QMessageBox.information(None, 'done', "done")) diff --git a/icons.qrc b/icons.qrc @@ -23,6 +23,7 @@ <file>icons/lock.png</file> <file>icons/microphone.png</file> <file>icons/network.png</file> + <file>icons/offline_tx.png</file> <file>icons/qrcode.png</file> <file>icons/qrcode_white.png</file> <file>icons/preferences.png</file> diff --git a/icons/offline_tx.png b/icons/offline_tx.png Binary files differ. diff --git a/lib/commands.py b/lib/commands.py @@ -629,6 +629,15 @@ class Commands: out = self.wallet.get_payment_request(addr, self.config) return self._format_request(out) + @command('w') + def addtransaction(self, tx): + """ Add a transaction to the wallet history """ + #fixme: we should ensure that tx is related to wallet + tx = Transaction(tx) + self.wallet.add_transaction(tx.txid(), tx) + self.wallet.save_transactions() + return tx.txid() + @command('wp') def signrequest(self, address, password=None): "Sign payment request with an OpenAlias" diff --git a/lib/synchronizer.py b/lib/synchronizer.py @@ -88,7 +88,7 @@ class Synchronizer(ThreadJob): if not params: return addr = params[0] - history = self.wallet.get_address_history(addr) + history = self.wallet.history.get(addr, []) if self.get_status(history) != result: if self.requested_histories.get(addr) is None: self.requested_histories[addr] = result diff --git a/lib/wallet.py b/lib/wallet.py @@ -69,6 +69,7 @@ TX_STATUS = [ _('Low fee'), _('Unconfirmed'), _('Not Verified'), + _('Local only'), ] @@ -405,28 +406,30 @@ class Abstract_Wallet(PrintError): return self.network.get_local_height() if self.network else self.storage.get('stored_height', 0) def get_tx_height(self, tx_hash): - """ return the height and timestamp of a verified transaction. """ + """ return the height and timestamp of a transaction. """ with self.lock: if tx_hash in self.verified_tx: height, timestamp, pos = self.verified_tx[tx_hash] conf = max(self.get_local_height() - height + 1, 0) return height, conf, timestamp - else: + elif tx_hash in self.unverified_tx: height = self.unverified_tx[tx_hash] return height, 0, False + else: + # local transaction + return -2, 0, False def get_txpos(self, tx_hash): "return position, even if the tx is unverified" with self.lock: - x = self.verified_tx.get(tx_hash) - y = self.unverified_tx.get(tx_hash) - if x: - height, timestamp, pos = x + if tx_hash in self.verified_tx: + height, timestamp, pos = self.verified_tx[tx_hash] return height, pos - elif y > 0: - return y, 0 + elif tx_hash in self.unverified_tx: + height = self.unverified_tx[tx_hash] + return (height, 0) if height>0 else (1e9 - height), 0 else: - return 1e12 - y, 0 + return (1e9+1, 0) def is_found(self): return self.history.values() != [[]] * len(self.history) @@ -521,7 +524,7 @@ class Abstract_Wallet(PrintError): status = _("%d confirmations") % conf else: status = _('Not verified') - else: + elif height in [-1,0]: status = _('Unconfirmed') if fee is None: fee = self.tx_fees.get(tx_hash) @@ -530,6 +533,9 @@ class Abstract_Wallet(PrintError): fee_per_kb = fee * 1000 / size exp_n = self.network.config.reverse_dynfee(fee_per_kb) can_bump = is_mine and not tx.is_final() + else: + status = _('Local') + can_broadcast = self.network is not None else: status = _("Signed") can_broadcast = self.network is not None @@ -551,7 +557,7 @@ class Abstract_Wallet(PrintError): return tx_hash, status, label, can_broadcast, can_bump, amount, fee, height, conf, timestamp, exp_n def get_addr_io(self, address): - h = self.history.get(address, []) + h = self.get_address_history(address) received = {} sent = {} for tx_hash, height in h: @@ -650,9 +656,14 @@ class Abstract_Wallet(PrintError): xx += x return cc, uu, xx - def get_address_history(self, address): - with self.lock: - return self.history.get(address, []) + def get_address_history(self, addr): + h = [] + with self.transaction_lock: + for tx_hash in self.transactions: + if addr in self.txi.get(tx_hash, []) or addr in self.txo.get(tx_hash, []): + tx_height = self.get_tx_height(tx_hash)[0] + h.append((tx_hash, tx_height)) + return h def find_pay_to_pubkey_address(self, prevout_hash, prevout_n): dd = self.txo.get(prevout_hash, {}) @@ -749,10 +760,9 @@ class Abstract_Wallet(PrintError): old_hist = self.history.get(addr, []) for tx_hash, height in old_hist: if (tx_hash, height) not in hist: - # remove tx if it's not referenced in histories - self.tx_addr_hist[tx_hash].remove(addr) - if not self.tx_addr_hist[tx_hash]: - self.remove_transaction(tx_hash) + # make tx local + self.unverified_tx.pop(tx_hash, None) + self.verified_tx.pop(tx_hash, None) self.history[addr] = hist for tx_hash, tx_height in hist: @@ -845,10 +855,12 @@ class Abstract_Wallet(PrintError): is_lowfee = fee < low_fee * 0.5 else: is_lowfee = False - if height==0 and not is_final: - status = 0 - elif height < 0: + if height == -2: + status = 5 + elif height == -1: status = 1 + elif height==0 and not is_final: + status = 0 elif height == 0 and is_lowfee: status = 2 elif height == 0: @@ -856,9 +868,9 @@ class Abstract_Wallet(PrintError): else: status = 4 else: - status = 4 + min(conf, 6) + status = 5 + min(conf, 6) time_str = format_time(timestamp) if timestamp else _("unknown") - status_str = TX_STATUS[status] if status < 5 else time_str + status_str = TX_STATUS[status] if status < 6 else time_str return status, status_str def relayfee(self): @@ -968,14 +980,6 @@ class Abstract_Wallet(PrintError): # add it in case it was previously unconfirmed self.add_unverified_tx(tx_hash, tx_height) - # if we are on a pruning server, remove unverified transactions - with self.lock: - vr = list(self.verified_tx.keys()) + list(self.unverified_tx.keys()) - for tx_hash in list(self.transactions): - if tx_hash not in vr: - self.print_error("removing transaction", tx_hash) - self.transactions.pop(tx_hash) - def start_threads(self, network): self.network = network if self.network is not None: @@ -1374,6 +1378,16 @@ class Abstract_Wallet(PrintError): index = self.get_address_index(addr) return self.keystore.decrypt_message(index, message, password) + def get_depending_transactions(self, tx_hash): + """Returns all (grand-)children of tx_hash in this wallet.""" + children = set() + for other_hash, tx in self.transactions.items(): + for input in (tx.inputs()): + if input["prevout_hash"] == tx_hash: + children.add(other_hash) + children |= self.get_depending_transactions(other_hash) + return children + class Simple_Wallet(Abstract_Wallet): # wallet with a single keystore