commit 0e7e7e3dc5fad0705bf64ead8350c12c1f432d0c
parent 603345a17223814d4e8ad719b36e49e413abed0e
Author: ThomasV <thomasv@electrum.org>
Date: Tue, 30 Jan 2018 00:18:44 +0100
Merge branch 'local_tx'
Diffstat:
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