mirror of
https://github.com/LBRYFoundation/LBRY-Vault.git
synced 2025-08-28 16:01:30 +00:00
Merge branch 'local_tx'
This commit is contained in:
commit
0e7e7e3dc5
7 changed files with 129 additions and 33 deletions
|
@ -37,6 +37,7 @@ TX_ICONS = [
|
||||||
"warning.png",
|
"warning.png",
|
||||||
"unconfirmed.png",
|
"unconfirmed.png",
|
||||||
"unconfirmed.png",
|
"unconfirmed.png",
|
||||||
|
"offline_tx.png",
|
||||||
"clock1.png",
|
"clock1.png",
|
||||||
"clock2.png",
|
"clock2.png",
|
||||||
"clock3.png",
|
"clock3.png",
|
||||||
|
@ -46,11 +47,12 @@ TX_ICONS = [
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
class HistoryList(MyTreeWidget):
|
class HistoryList(MyTreeWidget, AcceptFileDragDrop):
|
||||||
filter_columns = [2, 3, 4] # Date, Description, Amount
|
filter_columns = [2, 3, 4] # Date, Description, Amount
|
||||||
|
|
||||||
def __init__(self, parent=None):
|
def __init__(self, parent=None):
|
||||||
MyTreeWidget.__init__(self, parent, self.create_menu, [], 3)
|
MyTreeWidget.__init__(self, parent, self.create_menu, [], 3)
|
||||||
|
AcceptFileDragDrop.__init__(self, ".txn")
|
||||||
self.refresh_headers()
|
self.refresh_headers()
|
||||||
self.setColumnHidden(1, True)
|
self.setColumnHidden(1, True)
|
||||||
|
|
||||||
|
@ -158,11 +160,15 @@ class HistoryList(MyTreeWidget):
|
||||||
|
|
||||||
menu = QMenu()
|
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))
|
menu.addAction(_("Copy %s")%column_title, lambda: self.parent.app.clipboard().setText(column_data))
|
||||||
if column in self.editable_columns:
|
if column in self.editable_columns:
|
||||||
menu.addAction(_("Edit %s")%column_title, lambda: self.editItem(item, column))
|
menu.addAction(_("Edit %s")%column_title, lambda: self.editItem(item, column))
|
||||||
|
|
||||||
menu.addAction(_("Details"), lambda: self.parent.show_transaction(tx))
|
menu.addAction(_("Details"), lambda: self.parent.show_transaction(tx))
|
||||||
|
|
||||||
if is_unconfirmed and tx:
|
if is_unconfirmed and tx:
|
||||||
rbf = is_mine and not tx.is_final()
|
rbf = is_mine and not tx.is_final()
|
||||||
if rbf:
|
if rbf:
|
||||||
|
@ -176,3 +182,35 @@ class HistoryList(MyTreeWidget):
|
||||||
if tx_URL:
|
if tx_URL:
|
||||||
menu.addAction(_("View on block explorer"), lambda: webbrowser.open(tx_URL))
|
menu.addAction(_("View on block explorer"), lambda: webbrowser.open(tx_URL))
|
||||||
menu.exec_(self.viewport().mapToGlobal(position))
|
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()
|
||||||
|
|
|
@ -635,6 +635,40 @@ class ColorScheme:
|
||||||
if ColorScheme.has_dark_background(widget):
|
if ColorScheme.has_dark_background(widget):
|
||||||
ColorScheme.dark_scheme = True
|
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__":
|
if __name__ == "__main__":
|
||||||
app = QApplication([])
|
app = QApplication([])
|
||||||
t = WaitingDialog(None, 'testing ...', lambda: [time.sleep(1)], lambda x: QMessageBox.information(None, 'done', "done"))
|
t = WaitingDialog(None, 'testing ...', lambda: [time.sleep(1)], lambda x: QMessageBox.information(None, 'done', "done"))
|
||||||
|
|
|
@ -23,6 +23,7 @@
|
||||||
<file>icons/lock.png</file>
|
<file>icons/lock.png</file>
|
||||||
<file>icons/microphone.png</file>
|
<file>icons/microphone.png</file>
|
||||||
<file>icons/network.png</file>
|
<file>icons/network.png</file>
|
||||||
|
<file>icons/offline_tx.png</file>
|
||||||
<file>icons/qrcode.png</file>
|
<file>icons/qrcode.png</file>
|
||||||
<file>icons/qrcode_white.png</file>
|
<file>icons/qrcode_white.png</file>
|
||||||
<file>icons/preferences.png</file>
|
<file>icons/preferences.png</file>
|
||||||
|
|
BIN
icons/offline_tx.png
Normal file
BIN
icons/offline_tx.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.4 KiB |
|
@ -629,6 +629,15 @@ class Commands:
|
||||||
out = self.wallet.get_payment_request(addr, self.config)
|
out = self.wallet.get_payment_request(addr, self.config)
|
||||||
return self._format_request(out)
|
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')
|
@command('wp')
|
||||||
def signrequest(self, address, password=None):
|
def signrequest(self, address, password=None):
|
||||||
"Sign payment request with an OpenAlias"
|
"Sign payment request with an OpenAlias"
|
||||||
|
|
|
@ -88,7 +88,7 @@ class Synchronizer(ThreadJob):
|
||||||
if not params:
|
if not params:
|
||||||
return
|
return
|
||||||
addr = params[0]
|
addr = params[0]
|
||||||
history = self.wallet.get_address_history(addr)
|
history = self.wallet.history.get(addr, [])
|
||||||
if self.get_status(history) != result:
|
if self.get_status(history) != result:
|
||||||
if self.requested_histories.get(addr) is None:
|
if self.requested_histories.get(addr) is None:
|
||||||
self.requested_histories[addr] = result
|
self.requested_histories[addr] = result
|
||||||
|
|
|
@ -69,6 +69,7 @@ TX_STATUS = [
|
||||||
_('Low fee'),
|
_('Low fee'),
|
||||||
_('Unconfirmed'),
|
_('Unconfirmed'),
|
||||||
_('Not Verified'),
|
_('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)
|
return self.network.get_local_height() if self.network else self.storage.get('stored_height', 0)
|
||||||
|
|
||||||
def get_tx_height(self, tx_hash):
|
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:
|
with self.lock:
|
||||||
if tx_hash in self.verified_tx:
|
if tx_hash in self.verified_tx:
|
||||||
height, timestamp, pos = self.verified_tx[tx_hash]
|
height, timestamp, pos = self.verified_tx[tx_hash]
|
||||||
conf = max(self.get_local_height() - height + 1, 0)
|
conf = max(self.get_local_height() - height + 1, 0)
|
||||||
return height, conf, timestamp
|
return height, conf, timestamp
|
||||||
else:
|
elif tx_hash in self.unverified_tx:
|
||||||
height = self.unverified_tx[tx_hash]
|
height = self.unverified_tx[tx_hash]
|
||||||
return height, 0, False
|
return height, 0, False
|
||||||
|
else:
|
||||||
|
# local transaction
|
||||||
|
return -2, 0, False
|
||||||
|
|
||||||
def get_txpos(self, tx_hash):
|
def get_txpos(self, tx_hash):
|
||||||
"return position, even if the tx is unverified"
|
"return position, even if the tx is unverified"
|
||||||
with self.lock:
|
with self.lock:
|
||||||
x = self.verified_tx.get(tx_hash)
|
if tx_hash in self.verified_tx:
|
||||||
y = self.unverified_tx.get(tx_hash)
|
height, timestamp, pos = self.verified_tx[tx_hash]
|
||||||
if x:
|
|
||||||
height, timestamp, pos = x
|
|
||||||
return height, pos
|
return height, pos
|
||||||
elif y > 0:
|
elif tx_hash in self.unverified_tx:
|
||||||
return y, 0
|
height = self.unverified_tx[tx_hash]
|
||||||
|
return (height, 0) if height>0 else (1e9 - height), 0
|
||||||
else:
|
else:
|
||||||
return 1e12 - y, 0
|
return (1e9+1, 0)
|
||||||
|
|
||||||
def is_found(self):
|
def is_found(self):
|
||||||
return self.history.values() != [[]] * len(self.history)
|
return self.history.values() != [[]] * len(self.history)
|
||||||
|
@ -521,7 +524,7 @@ class Abstract_Wallet(PrintError):
|
||||||
status = _("%d confirmations") % conf
|
status = _("%d confirmations") % conf
|
||||||
else:
|
else:
|
||||||
status = _('Not verified')
|
status = _('Not verified')
|
||||||
else:
|
elif height in [-1,0]:
|
||||||
status = _('Unconfirmed')
|
status = _('Unconfirmed')
|
||||||
if fee is None:
|
if fee is None:
|
||||||
fee = self.tx_fees.get(tx_hash)
|
fee = self.tx_fees.get(tx_hash)
|
||||||
|
@ -530,6 +533,9 @@ class Abstract_Wallet(PrintError):
|
||||||
fee_per_kb = fee * 1000 / size
|
fee_per_kb = fee * 1000 / size
|
||||||
exp_n = self.network.config.reverse_dynfee(fee_per_kb)
|
exp_n = self.network.config.reverse_dynfee(fee_per_kb)
|
||||||
can_bump = is_mine and not tx.is_final()
|
can_bump = is_mine and not tx.is_final()
|
||||||
|
else:
|
||||||
|
status = _('Local')
|
||||||
|
can_broadcast = self.network is not None
|
||||||
else:
|
else:
|
||||||
status = _("Signed")
|
status = _("Signed")
|
||||||
can_broadcast = self.network is not None
|
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
|
return tx_hash, status, label, can_broadcast, can_bump, amount, fee, height, conf, timestamp, exp_n
|
||||||
|
|
||||||
def get_addr_io(self, address):
|
def get_addr_io(self, address):
|
||||||
h = self.history.get(address, [])
|
h = self.get_address_history(address)
|
||||||
received = {}
|
received = {}
|
||||||
sent = {}
|
sent = {}
|
||||||
for tx_hash, height in h:
|
for tx_hash, height in h:
|
||||||
|
@ -650,9 +656,14 @@ class Abstract_Wallet(PrintError):
|
||||||
xx += x
|
xx += x
|
||||||
return cc, uu, xx
|
return cc, uu, xx
|
||||||
|
|
||||||
def get_address_history(self, address):
|
def get_address_history(self, addr):
|
||||||
with self.lock:
|
h = []
|
||||||
return self.history.get(address, [])
|
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):
|
def find_pay_to_pubkey_address(self, prevout_hash, prevout_n):
|
||||||
dd = self.txo.get(prevout_hash, {})
|
dd = self.txo.get(prevout_hash, {})
|
||||||
|
@ -749,10 +760,9 @@ class Abstract_Wallet(PrintError):
|
||||||
old_hist = self.history.get(addr, [])
|
old_hist = self.history.get(addr, [])
|
||||||
for tx_hash, height in old_hist:
|
for tx_hash, height in old_hist:
|
||||||
if (tx_hash, height) not in hist:
|
if (tx_hash, height) not in hist:
|
||||||
# remove tx if it's not referenced in histories
|
# make tx local
|
||||||
self.tx_addr_hist[tx_hash].remove(addr)
|
self.unverified_tx.pop(tx_hash, None)
|
||||||
if not self.tx_addr_hist[tx_hash]:
|
self.verified_tx.pop(tx_hash, None)
|
||||||
self.remove_transaction(tx_hash)
|
|
||||||
self.history[addr] = hist
|
self.history[addr] = hist
|
||||||
|
|
||||||
for tx_hash, tx_height in hist:
|
for tx_hash, tx_height in hist:
|
||||||
|
@ -845,10 +855,12 @@ class Abstract_Wallet(PrintError):
|
||||||
is_lowfee = fee < low_fee * 0.5
|
is_lowfee = fee < low_fee * 0.5
|
||||||
else:
|
else:
|
||||||
is_lowfee = False
|
is_lowfee = False
|
||||||
if height==0 and not is_final:
|
if height == -2:
|
||||||
status = 0
|
status = 5
|
||||||
elif height < 0:
|
elif height == -1:
|
||||||
status = 1
|
status = 1
|
||||||
|
elif height==0 and not is_final:
|
||||||
|
status = 0
|
||||||
elif height == 0 and is_lowfee:
|
elif height == 0 and is_lowfee:
|
||||||
status = 2
|
status = 2
|
||||||
elif height == 0:
|
elif height == 0:
|
||||||
|
@ -856,9 +868,9 @@ class Abstract_Wallet(PrintError):
|
||||||
else:
|
else:
|
||||||
status = 4
|
status = 4
|
||||||
else:
|
else:
|
||||||
status = 4 + min(conf, 6)
|
status = 5 + min(conf, 6)
|
||||||
time_str = format_time(timestamp) if timestamp else _("unknown")
|
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
|
return status, status_str
|
||||||
|
|
||||||
def relayfee(self):
|
def relayfee(self):
|
||||||
|
@ -968,14 +980,6 @@ class Abstract_Wallet(PrintError):
|
||||||
# add it in case it was previously unconfirmed
|
# add it in case it was previously unconfirmed
|
||||||
self.add_unverified_tx(tx_hash, tx_height)
|
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):
|
def start_threads(self, network):
|
||||||
self.network = network
|
self.network = network
|
||||||
if self.network is not None:
|
if self.network is not None:
|
||||||
|
@ -1374,6 +1378,16 @@ class Abstract_Wallet(PrintError):
|
||||||
index = self.get_address_index(addr)
|
index = self.get_address_index(addr)
|
||||||
return self.keystore.decrypt_message(index, message, password)
|
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):
|
class Simple_Wallet(Abstract_Wallet):
|
||||||
# wallet with a single keystore
|
# wallet with a single keystore
|
||||||
|
|
Loading…
Add table
Reference in a new issue