From e6dd3e6ad84c5b2978e6aff8a07c4e8e348c62ba Mon Sep 17 00:00:00 2001 From: ThomasV Date: Thu, 18 Jan 2018 11:56:21 +0100 Subject: [PATCH 1/6] allow to save unbroadcasted transactions in wallet --- gui/qt/history_list.py | 1 + lib/commands.py | 9 ++++++ lib/synchronizer.py | 2 +- lib/wallet.py | 66 ++++++++++++++++++++++-------------------- 4 files changed, 46 insertions(+), 32 deletions(-) diff --git a/gui/qt/history_list.py b/gui/qt/history_list.py index 6e17037ba..4126662d5 100644 --- a/gui/qt/history_list.py +++ b/gui/qt/history_list.py @@ -37,6 +37,7 @@ TX_ICONS = [ "warning.png", "unconfirmed.png", "unconfirmed.png", + "warning.png", "clock1.png", "clock2.png", "clock3.png", diff --git a/lib/commands.py b/lib/commands.py index bb10161c6..c52b4637f 100644 --- 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 index b6534e386..70e13e529 100644 --- 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 index a9dc646bc..93c9ff316 100644 --- a/lib/wallet.py +++ b/lib/wallet.py @@ -69,6 +69,7 @@ TX_STATUS = [ _('Low fee'), _('Unconfirmed'), _('Not Verified'), + _('Local'), ] @@ -404,28 +405,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) @@ -520,7 +523,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) @@ -529,6 +532,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 @@ -550,7 +556,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: @@ -649,9 +655,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, {}) @@ -748,10 +759,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: @@ -844,10 +854,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: @@ -855,9 +867,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): @@ -967,14 +979,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: From 5e9d901794f8e8205281833d9ffe2a78baaba23e Mon Sep 17 00:00:00 2001 From: Johann Bauer Date: Tue, 23 Jan 2018 19:11:12 +0100 Subject: [PATCH 2/6] Allow to remove local transactions from the GUI --- gui/qt/history_list.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/gui/qt/history_list.py b/gui/qt/history_list.py index 4126662d5..7a554b083 100644 --- a/gui/qt/history_list.py +++ b/gui/qt/history_list.py @@ -159,11 +159,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: @@ -177,3 +181,20 @@ 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, tx_hash): + answer = QMessageBox.question(self.parent, + _("Please confirm"), + _("Are you sure you want to remove this transaction?"), + QMessageBox.Yes, + QMessageBox.No) + if answer == QMessageBox.No: + return + self.wallet.remove_transaction(tx_hash) + root = self.invisibleRootItem() + child_count = root.childCount() + for i in range(child_count): + item = root.child(i) + if item.data(0, Qt.UserRole) == tx_hash: + root.removeChild(item) + return From 887e06eebb75161d68c7f51f7ad3f1feda64561e Mon Sep 17 00:00:00 2001 From: Johann Bauer Date: Tue, 23 Jan 2018 23:50:02 +0100 Subject: [PATCH 3/6] Set icon for offline transactions --- gui/qt/history_list.py | 2 +- icons.qrc | 1 + icons/offline_tx.png | Bin 0 -> 2451 bytes lib/wallet.py | 2 +- 4 files changed, 3 insertions(+), 2 deletions(-) create mode 100644 icons/offline_tx.png diff --git a/gui/qt/history_list.py b/gui/qt/history_list.py index 7a554b083..a5ca141b3 100644 --- a/gui/qt/history_list.py +++ b/gui/qt/history_list.py @@ -37,7 +37,7 @@ TX_ICONS = [ "warning.png", "unconfirmed.png", "unconfirmed.png", - "warning.png", + "offline_tx.png", "clock1.png", "clock2.png", "clock3.png", diff --git a/icons.qrc b/icons.qrc index 92e19a1a1..195e9af9d 100644 --- a/icons.qrc +++ b/icons.qrc @@ -23,6 +23,7 @@ icons/lock.png icons/microphone.png icons/network.png + icons/offline_tx.png icons/qrcode.png icons/qrcode_white.png icons/preferences.png diff --git a/icons/offline_tx.png b/icons/offline_tx.png new file mode 100644 index 0000000000000000000000000000000000000000..55d2b2a471ed3f96214b239dba9c6e18f1437d17 GIT binary patch literal 2451 zcmV;E32gR>P)& z&=rwK}t!M1dgkNnMnT|vp4U_;JW_{Na+XAU=vZgd&P{_p<)XJBt7D*dyx{1HPz0W zb}ld1op#zkAVDTPIc9{9aYk(nbtR=FA12g2H)5FQ7kL@JiK>zIY=4(Hzs9{WmlgTkF1ED%shVF63Rt$WCPiWN@Au6q z@VgX-vllcdoOVb;J{~Ga!z5c`rszCovi^pmbWd)& z4_#p$FaEnREDUS5p~gxvM2)lek!j~lT`YWlcS9%`HIO^pkK(I_yIf#T#{n*Qyk?|; zT()phwh90*S2adWA=bUKviR6#?QSW_+^1@aows(bD*ni@u<4cR(_sLZ|MQViIwClIE@IFyU*DUgx}r2lsND4R&+i8*CAn?m;eRmD{kJ?> zTOMa}b*?#iHWX{_jKJ^aC@xB63MCZ)K$mRY$|Ufck_j0u0N7F29I|b(dPiVVv^T+= zwFj~&*z@y?$AZ!C(cMy#1sjjNG%V4*Wbv$VlyS1H{OP(8eN|juuA7z}cI8W!rm3UL}$UUuepbJzMQS7oYWvr@63xPXFOT6f>F z{?IkTvhFIqF5j1(o`{JB846`&tnIKBDWykQ;y zIMLDrx5}!V)hl2Bey+5|;_C`B6#zK+X-kL#591Qv$#)-~_6Z1Cx#xq%2mnkf%2K2) zmpS9Mub2_?DD=RImL34Wgi*s>3XwNcH)k`)WF#m6aP&+t8qv)+Kf(>=m^Vije=C7*_E!=pxvTRMwSZA78G`2Yy);gQdcf+l6 z6y&4`2ENGY6V}8V$G!|k0iZA|LG`G-gi2c$j!1R`0Gd0(rY-EFaiv$-vb+&VUH~|L zx;-j{+`Mh+Qt{vJCPxWKKBHvT?Iq*=cb84{Pb$p3i7V`b-cAI7>OiSwS@tuZo$1g3 zAU`wN>sHyFam5hgqo($-4geX+9@w@VM=eXHCi^dbr^7LeDT?cKT#POA(uVl}04?o3 z!i22s8?>S{sHn8V=XFU@pT zQv3*pH2iDd=Ll;C#*a?tmTi|fecfDs8PhE{0C?RBOktHba0QEREP@j_2{{!*VG+4)V?YCnV#v*6FKRB-i8O8URE!6AGhUPP@>PlH4kb z0YJvxivhBex`lrYZ9a?+C&0B8?JKq*c+b1|FcCwjRAfMCP`XQYD?N*daN8UP?C z&96{KCpv=+(|ql8TXz%yuFgyMa!xCjY~9Lw?YmYNpU{Ol;Yj`2y)V4|k@jlkG0hNG z6$tqRAftQN$&x@7=bSDdKRV3~0F7sZQOyvqImd#O*X5@9TmWz`7y~7=o@=Jqcd9Kk zCN`olT?u1Ykq2&z_YtngGOA zwq#RPvP*aG0x+3MR^{>W*OUG3)STf-&UpA@|}(qrSCAi|S?- z6~f;8$(ai}0F28{L`=74Eess-_6EKEXz^E|Z0nKwR+9iInV9Q?kZW&Vb2x5^=&$~} zAY0gfo<1SR2LMOvTTMdA>&~M@NubIf)y?VSvJ(LS$D4wt1-h3e<+mK_j+x?ow+64u zQ8Y5q5*~54b1>b~SAE>jW}ayaV%OnQ5zg2XdsaZmAbEc`0 zl}=x;*K<$N$Rx|Fa-8joAsiF>^w^XS>9(ay1(kTI>R6ir0E?#OBruLAoK@VtVn(YW z z+1*@Edt^*zqAw@ahp=YhV11`0B-so&7SdZc)IE1YVcM_nxN)>67&Y+t)=$G>-I!gu zqPXr-f&Q1Ajf1a`wA`i{Za3femu1%`Bzsl-^T3%%&FAf}?s$0W{ea_4VutL84mNb? zr`keD_NusV?pVLV*zW$jk{_Z0#|N+BDyJ%HOvErQ<>kZHA4jl4D9Yx=OR**+F$$o4Z7P{5;(4I3tVjs6`WIpHI8!u|H0d#%b{ Date: Wed, 24 Jan 2018 18:17:50 +0100 Subject: [PATCH 4/6] Also remove child transactions --- gui/qt/history_list.py | 27 +++++++++++++++++---------- lib/wallet.py | 10 ++++++++++ 2 files changed, 27 insertions(+), 10 deletions(-) diff --git a/gui/qt/history_list.py b/gui/qt/history_list.py index a5ca141b3..32aec4982 100644 --- a/gui/qt/history_list.py +++ b/gui/qt/history_list.py @@ -182,19 +182,26 @@ class HistoryList(MyTreeWidget): menu.addAction(_("View on block explorer"), lambda: webbrowser.open(tx_URL)) menu.exec_(self.viewport().mapToGlobal(position)) - def remove_local_tx(self, tx_hash): - answer = QMessageBox.question(self.parent, - _("Please confirm"), - _("Are you sure you want to remove this transaction?"), - QMessageBox.Yes, - QMessageBox.No) + 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 - self.wallet.remove_transaction(tx_hash) + for tx in to_delete: + self.wallet.remove_transaction(tx) root = self.invisibleRootItem() child_count = root.childCount() + _offset = 0 for i in range(child_count): - item = root.child(i) - if item.data(0, Qt.UserRole) == tx_hash: + item = root.child(i - _offset) + if item.data(0, Qt.UserRole) in to_delete: root.removeChild(item) - return + _offset += 1 diff --git a/lib/wallet.py b/lib/wallet.py index 8a7770d82..ae2c2a745 100644 --- a/lib/wallet.py +++ b/lib/wallet.py @@ -1377,6 +1377,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 From 95da5a8bed7136bf0defd36b60732d3bfe0680b3 Mon Sep 17 00:00:00 2001 From: Johann Bauer Date: Wed, 24 Jan 2018 21:32:51 +0100 Subject: [PATCH 5/6] Enable adding transactions from file through Drag and Drop --- gui/qt/history_list.py | 12 +++++++++++- gui/qt/util.py | 34 ++++++++++++++++++++++++++++++++++ 2 files changed, 45 insertions(+), 1 deletion(-) diff --git a/gui/qt/history_list.py b/gui/qt/history_list.py index 32aec4982..06d68f2ef 100644 --- a/gui/qt/history_list.py +++ b/gui/qt/history_list.py @@ -47,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) @@ -205,3 +206,12 @@ class HistoryList(MyTreeWidget): 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() + self.on_update() + + diff --git a/gui/qt/util.py b/gui/qt/util.py index 2d2626a18..cd552e405 100644 --- 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")) From e184ac888f85c0bea1e97d58471713dc68ef1c30 Mon Sep 17 00:00:00 2001 From: Johann Bauer Date: Wed, 24 Jan 2018 21:41:35 +0100 Subject: [PATCH 6/6] Make sure to save changes to transactions on disk --- gui/qt/history_list.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/gui/qt/history_list.py b/gui/qt/history_list.py index 06d68f2ef..d270a1af1 100644 --- a/gui/qt/history_list.py +++ b/gui/qt/history_list.py @@ -198,6 +198,7 @@ class HistoryList(MyTreeWidget, AcceptFileDragDrop): 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 @@ -211,7 +212,5 @@ class HistoryList(MyTreeWidget, AcceptFileDragDrop): with open(fn) as f: tx = self.parent.tx_from_text(f.read()) self.wallet.add_transaction(tx.txid(), tx) - self.wallet.save_transactions() + self.wallet.save_transactions(write=True) self.on_update() - -