mirror of
https://github.com/LBRYFoundation/LBRY-Vault.git
synced 2025-08-23 17:47:31 +00:00
store deserialized tx in/out in wallet file for fast computation
This commit is contained in:
parent
d9b1271f65
commit
e3de121be9
11 changed files with 385 additions and 365 deletions
|
@ -1170,7 +1170,7 @@ class ElectrumWindow:
|
||||||
cursor = self.history_treeview.get_cursor()[0]
|
cursor = self.history_treeview.get_cursor()[0]
|
||||||
self.history_list.clear()
|
self.history_list.clear()
|
||||||
|
|
||||||
for item in self.wallet.get_tx_history():
|
for item in self.wallet.get_history():
|
||||||
tx_hash, conf, is_mine, value, fee, balance, timestamp = item
|
tx_hash, conf, is_mine, value, fee, balance, timestamp = item
|
||||||
if conf > 0:
|
if conf > 0:
|
||||||
try:
|
try:
|
||||||
|
@ -1199,7 +1199,7 @@ class ElectrumWindow:
|
||||||
import datetime
|
import datetime
|
||||||
if not tx_hash: return ''
|
if not tx_hash: return ''
|
||||||
tx = self.wallet.transactions.get(tx_hash)
|
tx = self.wallet.transactions.get(tx_hash)
|
||||||
is_relevant, is_mine, v, fee = self.wallet.get_tx_value(tx)
|
is_relevant, is_mine, v, fee = self.wallet.get_wallet_delta(tx)
|
||||||
conf, timestamp = self.wallet.verifier.get_confirmations(tx_hash)
|
conf, timestamp = self.wallet.verifier.get_confirmations(tx_hash)
|
||||||
|
|
||||||
if timestamp:
|
if timestamp:
|
||||||
|
|
|
@ -450,7 +450,7 @@ class MiniWindow(QDialog):
|
||||||
self.history_list.empty()
|
self.history_list.empty()
|
||||||
|
|
||||||
for item in tx_history[-10:]:
|
for item in tx_history[-10:]:
|
||||||
tx_hash, conf, is_mine, value, fee, balance, timestamp = item
|
tx_hash, conf, value, timestamp = item
|
||||||
label = self.actuator.g.wallet.get_label(tx_hash)[0]
|
label = self.actuator.g.wallet.get_label(tx_hash)[0]
|
||||||
v_str = self.actuator.g.format_amount(value, True)
|
v_str = self.actuator.g.format_amount(value, True)
|
||||||
self.history_list.append(label, v_str, age(timestamp))
|
self.history_list.append(label, v_str, age(timestamp))
|
||||||
|
@ -862,7 +862,7 @@ class MiniDriver(QObject):
|
||||||
self.window.update_completions(completions)
|
self.window.update_completions(completions)
|
||||||
|
|
||||||
def update_history(self):
|
def update_history(self):
|
||||||
tx_history = self.g.wallet.get_tx_history()
|
tx_history = self.g.wallet.get_history()
|
||||||
self.window.update_history(tx_history)
|
self.window.update_history(tx_history)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -568,7 +568,7 @@ class ElectrumWindow(QMainWindow):
|
||||||
for i,width in enumerate(self.column_widths['history']):
|
for i,width in enumerate(self.column_widths['history']):
|
||||||
l.setColumnWidth(i, width)
|
l.setColumnWidth(i, width)
|
||||||
l.setHeaderLabels( [ '', _('Date'), _('Description') , _('Amount'), _('Balance')] )
|
l.setHeaderLabels( [ '', _('Date'), _('Description') , _('Amount'), _('Balance')] )
|
||||||
l.itemDoubleClicked.connect(self.tx_label_clicked)
|
l.itemDoubleClicked.connect(self.edit_tx_label)
|
||||||
l.itemChanged.connect(self.tx_label_changed)
|
l.itemChanged.connect(self.tx_label_changed)
|
||||||
l.customContextMenuRequested.connect(self.create_history_menu)
|
l.customContextMenuRequested.connect(self.create_history_menu)
|
||||||
return l
|
return l
|
||||||
|
@ -593,7 +593,7 @@ class ElectrumWindow(QMainWindow):
|
||||||
menu = QMenu()
|
menu = QMenu()
|
||||||
menu.addAction(_("Copy ID to Clipboard"), lambda: self.app.clipboard().setText(tx_hash))
|
menu.addAction(_("Copy ID to Clipboard"), lambda: self.app.clipboard().setText(tx_hash))
|
||||||
menu.addAction(_("Details"), lambda: self.show_transaction(self.wallet.transactions.get(tx_hash)))
|
menu.addAction(_("Details"), lambda: self.show_transaction(self.wallet.transactions.get(tx_hash)))
|
||||||
menu.addAction(_("Edit description"), lambda: self.tx_label_clicked(item,2))
|
menu.addAction(_("Edit description"), lambda: self.edit_tx_label(item,2))
|
||||||
menu.addAction(_("View on block explorer"), lambda: webbrowser.open(block_explorer + tx_hash))
|
menu.addAction(_("View on block explorer"), lambda: webbrowser.open(block_explorer + tx_hash))
|
||||||
menu.exec_(self.contacts_list.viewport().mapToGlobal(position))
|
menu.exec_(self.contacts_list.viewport().mapToGlobal(position))
|
||||||
|
|
||||||
|
@ -603,21 +603,25 @@ class ElectrumWindow(QMainWindow):
|
||||||
d = transaction_dialog.TxDialog(tx, self)
|
d = transaction_dialog.TxDialog(tx, self)
|
||||||
d.exec_()
|
d.exec_()
|
||||||
|
|
||||||
def tx_label_clicked(self, item, column):
|
def edit_tx_label(self, item, column):
|
||||||
if column==2 and item.isSelected():
|
if column==2 and item.isSelected():
|
||||||
self.is_edit=True
|
text = unicode(item.text(column))
|
||||||
|
tx_hash = str(item.data(0, Qt.UserRole).toString())
|
||||||
|
self.is_edit = True
|
||||||
|
if text == self.wallet.get_default_label(tx_hash):
|
||||||
|
item.setText(column, '')
|
||||||
item.setFlags(Qt.ItemIsEditable|Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
|
item.setFlags(Qt.ItemIsEditable|Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
|
||||||
self.history_list.editItem( item, column )
|
self.history_list.editItem( item, column )
|
||||||
item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
|
item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
|
||||||
self.is_edit=False
|
self.is_edit = False
|
||||||
|
|
||||||
def tx_label_changed(self, item, column):
|
def tx_label_changed(self, item, column):
|
||||||
if self.is_edit:
|
if self.is_edit:
|
||||||
return
|
return
|
||||||
self.is_edit=True
|
self.is_edit = True
|
||||||
tx_hash = str(item.data(0, Qt.UserRole).toString())
|
tx_hash = str(item.data(0, Qt.UserRole).toString())
|
||||||
tx = self.wallet.transactions.get(tx_hash)
|
tx = self.wallet.transactions.get(tx_hash)
|
||||||
text = unicode( item.text(2) )
|
text = unicode(item.text(2))
|
||||||
self.wallet.set_label(tx_hash, text)
|
self.wallet.set_label(tx_hash, text)
|
||||||
if text:
|
if text:
|
||||||
item.setForeground(2, QBrush(QColor('black')))
|
item.setForeground(2, QBrush(QColor('black')))
|
||||||
|
@ -625,7 +629,7 @@ class ElectrumWindow(QMainWindow):
|
||||||
text = self.wallet.get_default_label(tx_hash)
|
text = self.wallet.get_default_label(tx_hash)
|
||||||
item.setText(2, text)
|
item.setText(2, text)
|
||||||
item.setForeground(2, QBrush(QColor('gray')))
|
item.setForeground(2, QBrush(QColor('gray')))
|
||||||
self.is_edit=False
|
self.is_edit = False
|
||||||
|
|
||||||
|
|
||||||
def edit_label(self, is_recv):
|
def edit_label(self, is_recv):
|
||||||
|
@ -682,8 +686,9 @@ class ElectrumWindow(QMainWindow):
|
||||||
def update_history_tab(self):
|
def update_history_tab(self):
|
||||||
|
|
||||||
self.history_list.clear()
|
self.history_list.clear()
|
||||||
for item in self.wallet.get_tx_history(self.current_account):
|
balance = 0
|
||||||
tx_hash, conf, is_mine, value, fee, balance, timestamp = item
|
for item in self.wallet.get_history(self.current_account):
|
||||||
|
tx_hash, conf, value, timestamp = item
|
||||||
time_str = _("unknown")
|
time_str = _("unknown")
|
||||||
if conf > 0:
|
if conf > 0:
|
||||||
time_str = self.format_time(timestamp)
|
time_str = self.format_time(timestamp)
|
||||||
|
@ -703,6 +708,7 @@ class ElectrumWindow(QMainWindow):
|
||||||
else:
|
else:
|
||||||
v_str = '--'
|
v_str = '--'
|
||||||
|
|
||||||
|
balance += value
|
||||||
balance_str = self.format_amount(balance, whitespaces=True)
|
balance_str = self.format_amount(balance, whitespaces=True)
|
||||||
|
|
||||||
if tx_hash:
|
if tx_hash:
|
||||||
|
@ -721,7 +727,7 @@ class ElectrumWindow(QMainWindow):
|
||||||
item.setData(0, Qt.UserRole, tx_hash)
|
item.setData(0, Qt.UserRole, tx_hash)
|
||||||
item.setToolTip(0, "%d %s\nTxId:%s" % (conf, _('Confirmations'), tx_hash) )
|
item.setToolTip(0, "%d %s\nTxId:%s" % (conf, _('Confirmations'), tx_hash) )
|
||||||
if is_default_label:
|
if is_default_label:
|
||||||
item.setForeground(2, QBrush(QColor('grey')))
|
item.setForeground(2, QBrush(QColor('lightgrey')))
|
||||||
|
|
||||||
item.setIcon(0, icon)
|
item.setIcon(0, icon)
|
||||||
self.history_list.insertTopLevelItem(0,item)
|
self.history_list.insertTopLevelItem(0,item)
|
||||||
|
@ -1020,7 +1026,7 @@ class ElectrumWindow(QMainWindow):
|
||||||
for i in inputs: self.wallet.add_input_info(i)
|
for i in inputs: self.wallet.add_input_info(i)
|
||||||
addr = self.payto_e.payto_address if self.payto_e.payto_address else self.dummy_address
|
addr = self.payto_e.payto_address if self.payto_e.payto_address else self.dummy_address
|
||||||
output = ('address', addr, sendable)
|
output = ('address', addr, sendable)
|
||||||
dummy_tx = Transaction(inputs, [output])
|
dummy_tx = Transaction.from_io(inputs, [output])
|
||||||
fee = self.wallet.estimated_fee(dummy_tx)
|
fee = self.wallet.estimated_fee(dummy_tx)
|
||||||
self.amount_e.setAmount(max(0,sendable-fee))
|
self.amount_e.setAmount(max(0,sendable-fee))
|
||||||
self.amount_e.textEdited.emit("")
|
self.amount_e.textEdited.emit("")
|
||||||
|
@ -2510,10 +2516,10 @@ class ElectrumWindow(QMainWindow):
|
||||||
|
|
||||||
|
|
||||||
def do_export_history(self, wallet, fileName, is_csv):
|
def do_export_history(self, wallet, fileName, is_csv):
|
||||||
history = wallet.get_tx_history()
|
history = wallet.get_history()
|
||||||
lines = []
|
lines = []
|
||||||
for item in history:
|
for item in history:
|
||||||
tx_hash, confirmations, is_mine, value, fee, balance, timestamp = item
|
tx_hash, confirmations, value, timestamp = item
|
||||||
if confirmations:
|
if confirmations:
|
||||||
if timestamp is not None:
|
if timestamp is not None:
|
||||||
try:
|
try:
|
||||||
|
@ -2531,27 +2537,21 @@ class ElectrumWindow(QMainWindow):
|
||||||
else:
|
else:
|
||||||
value_string = '--'
|
value_string = '--'
|
||||||
|
|
||||||
if fee is not None:
|
|
||||||
fee_string = format_satoshis(fee, True)
|
|
||||||
else:
|
|
||||||
fee_string = '0'
|
|
||||||
|
|
||||||
if tx_hash:
|
if tx_hash:
|
||||||
label, is_default_label = wallet.get_label(tx_hash)
|
label, is_default_label = wallet.get_label(tx_hash)
|
||||||
label = label.encode('utf-8')
|
label = label.encode('utf-8')
|
||||||
else:
|
else:
|
||||||
label = ""
|
label = ""
|
||||||
|
|
||||||
balance_string = format_satoshis(balance, False)
|
|
||||||
if is_csv:
|
if is_csv:
|
||||||
lines.append([tx_hash, label, confirmations, value_string, fee_string, balance_string, time_string])
|
lines.append([tx_hash, label, confirmations, value_string, time_string])
|
||||||
else:
|
else:
|
||||||
lines.append({'txid':tx_hash, 'date':"%16s"%time_string, 'label':label, 'value':value_string})
|
lines.append({'txid':tx_hash, 'date':"%16s"%time_string, 'label':label, 'value':value_string})
|
||||||
|
|
||||||
with open(fileName, "w+") as f:
|
with open(fileName, "w+") as f:
|
||||||
if is_csv:
|
if is_csv:
|
||||||
transaction = csv.writer(f, lineterminator='\n')
|
transaction = csv.writer(f, lineterminator='\n')
|
||||||
transaction.writerow(["transaction_hash","label", "confirmations", "value", "fee", "balance", "timestamp"])
|
transaction.writerow(["transaction_hash","label", "confirmations", "value", "timestamp"])
|
||||||
for line in lines:
|
for line in lines:
|
||||||
transaction.writerow(line)
|
transaction.writerow(line)
|
||||||
else:
|
else:
|
||||||
|
|
|
@ -134,8 +134,8 @@ class TxDialog(QDialog):
|
||||||
|
|
||||||
|
|
||||||
def update(self):
|
def update(self):
|
||||||
|
is_relevant, is_mine, v, fee = self.wallet.get_wallet_delta(self.tx)
|
||||||
is_relevant, is_mine, v, fee = self.wallet.get_tx_value(self.tx)
|
tx_hash = self.tx.hash()
|
||||||
if self.wallet.can_sign(self.tx):
|
if self.wallet.can_sign(self.tx):
|
||||||
self.sign_button.show()
|
self.sign_button.show()
|
||||||
else:
|
else:
|
||||||
|
@ -143,7 +143,6 @@ class TxDialog(QDialog):
|
||||||
|
|
||||||
if self.tx.is_complete():
|
if self.tx.is_complete():
|
||||||
status = _("Signed")
|
status = _("Signed")
|
||||||
tx_hash = self.tx.hash()
|
|
||||||
|
|
||||||
if tx_hash in self.wallet.transactions.keys():
|
if tx_hash in self.wallet.transactions.keys():
|
||||||
conf, timestamp = self.wallet.verifier.get_confirmations(tx_hash)
|
conf, timestamp = self.wallet.verifier.get_confirmations(tx_hash)
|
||||||
|
@ -182,10 +181,10 @@ class TxDialog(QDialog):
|
||||||
if is_relevant:
|
if is_relevant:
|
||||||
if is_mine:
|
if is_mine:
|
||||||
if fee is not None:
|
if fee is not None:
|
||||||
self.amount_label.setText(_("Amount sent:")+' %s'% self.parent.format_amount(v-fee) + ' ' + self.parent.base_unit())
|
self.amount_label.setText(_("Amount sent:")+' %s'% self.parent.format_amount(-v+fee) + ' ' + self.parent.base_unit())
|
||||||
self.fee_label.setText(_("Transaction fee")+': %s'% self.parent.format_amount(fee) + ' ' + self.parent.base_unit())
|
self.fee_label.setText(_("Transaction fee")+': %s'% self.parent.format_amount(-fee) + ' ' + self.parent.base_unit())
|
||||||
else:
|
else:
|
||||||
self.amount_label.setText(_("Amount sent:")+' %s'% self.parent.format_amount(v) + ' ' + self.parent.base_unit())
|
self.amount_label.setText(_("Amount sent:")+' %s'% self.parent.format_amount(-v) + ' ' + self.parent.base_unit())
|
||||||
self.fee_label.setText(_("Transaction fee")+': '+ _("unknown"))
|
self.fee_label.setText(_("Transaction fee")+': '+ _("unknown"))
|
||||||
else:
|
else:
|
||||||
self.amount_label.setText(_("Amount received:")+' %s'% self.parent.format_amount(v) + ' ' + self.parent.base_unit())
|
self.amount_label.setText(_("Amount received:")+' %s'% self.parent.format_amount(v) + ' ' + self.parent.base_unit())
|
||||||
|
|
|
@ -107,7 +107,7 @@ class ElectrumGui:
|
||||||
b = 0
|
b = 0
|
||||||
self.history = []
|
self.history = []
|
||||||
|
|
||||||
for item in self.wallet.get_tx_history():
|
for item in self.wallet.get_history():
|
||||||
tx_hash, conf, is_mine, value, fee, balance, timestamp = item
|
tx_hash, conf, is_mine, value, fee, balance, timestamp = item
|
||||||
if conf:
|
if conf:
|
||||||
try:
|
try:
|
||||||
|
|
|
@ -342,7 +342,7 @@ class Commands:
|
||||||
def history(self):
|
def history(self):
|
||||||
balance = 0
|
balance = 0
|
||||||
out = []
|
out = []
|
||||||
for item in self.wallet.get_tx_history():
|
for item in self.wallet.get_history():
|
||||||
tx_hash, conf, is_mine, value, fee, balance, timestamp = item
|
tx_hash, conf, is_mine, value, fee, balance, timestamp = item
|
||||||
try:
|
try:
|
||||||
time_str = datetime.datetime.fromtimestamp( timestamp).isoformat(' ')[:-3]
|
time_str = datetime.datetime.fromtimestamp( timestamp).isoformat(' ')[:-3]
|
||||||
|
|
|
@ -101,6 +101,7 @@ class WalletSynchronizer(util.DaemonThread):
|
||||||
if not self.wallet.is_up_to_date():
|
if not self.wallet.is_up_to_date():
|
||||||
self.wallet.set_up_to_date(True)
|
self.wallet.set_up_to_date(True)
|
||||||
self.was_updated = True
|
self.was_updated = True
|
||||||
|
self.wallet.save_transactions()
|
||||||
else:
|
else:
|
||||||
if self.wallet.is_up_to_date():
|
if self.wallet.is_up_to_date():
|
||||||
self.wallet.set_up_to_date(False)
|
self.wallet.set_up_to_date(False)
|
||||||
|
@ -127,7 +128,7 @@ class WalletSynchronizer(util.DaemonThread):
|
||||||
|
|
||||||
if method == 'blockchain.address.subscribe':
|
if method == 'blockchain.address.subscribe':
|
||||||
addr = params[0]
|
addr = params[0]
|
||||||
if self.wallet.get_status(self.wallet.get_history(addr)) != result:
|
if self.wallet.get_status(self.wallet.get_address_history(addr)) != result:
|
||||||
if requested_histories.get(addr) is None:
|
if requested_histories.get(addr) is None:
|
||||||
self.network.send([('blockchain.address.get_history', [addr])], self.queue.put)
|
self.network.send([('blockchain.address.get_history', [addr])], self.queue.put)
|
||||||
requested_histories[addr] = result
|
requested_histories[addr] = result
|
||||||
|
@ -135,41 +136,43 @@ class WalletSynchronizer(util.DaemonThread):
|
||||||
elif method == 'blockchain.address.get_history':
|
elif method == 'blockchain.address.get_history':
|
||||||
addr = params[0]
|
addr = params[0]
|
||||||
self.print_error("receiving history", addr, result)
|
self.print_error("receiving history", addr, result)
|
||||||
if result == ['*']:
|
hist = []
|
||||||
assert requested_histories.pop(addr) == '*'
|
# check that txids are unique
|
||||||
self.wallet.receive_history_callback(addr, result)
|
txids = []
|
||||||
else:
|
for item in result:
|
||||||
hist = []
|
tx_hash = item['tx_hash']
|
||||||
# check that txids are unique
|
if tx_hash not in txids:
|
||||||
txids = []
|
txids.append(tx_hash)
|
||||||
for item in result:
|
hist.append( (tx_hash, item['height']) )
|
||||||
tx_hash = item['tx_hash']
|
|
||||||
if tx_hash not in txids:
|
|
||||||
txids.append(tx_hash)
|
|
||||||
hist.append( (tx_hash, item['height']) )
|
|
||||||
|
|
||||||
if len(hist) != len(result):
|
if len(hist) != len(result):
|
||||||
raise Exception("error: server sent history with non-unique txid", result)
|
raise Exception("error: server sent history with non-unique txid", result)
|
||||||
|
|
||||||
# check that the status corresponds to what was announced
|
# check that the status corresponds to what was announced
|
||||||
rs = requested_histories.pop(addr)
|
rs = requested_histories.pop(addr)
|
||||||
if self.wallet.get_status(hist) != rs:
|
if self.wallet.get_status(hist) != rs:
|
||||||
raise Exception("error: status mismatch: %s"%addr)
|
raise Exception("error: status mismatch: %s"%addr)
|
||||||
|
|
||||||
# store received history
|
# store received history
|
||||||
self.wallet.receive_history_callback(addr, hist)
|
self.wallet.receive_history_callback(addr, hist)
|
||||||
|
|
||||||
# request transactions that we don't have
|
# request transactions that we don't have
|
||||||
for tx_hash, tx_height in hist:
|
for tx_hash, tx_height in hist:
|
||||||
if self.wallet.transactions.get(tx_hash) is None:
|
if self.wallet.transactions.get(tx_hash) is None:
|
||||||
if (tx_hash, tx_height) not in requested_tx and (tx_hash, tx_height) not in missing_tx:
|
if (tx_hash, tx_height) not in requested_tx and (tx_hash, tx_height) not in missing_tx:
|
||||||
missing_tx.append( (tx_hash, tx_height) )
|
missing_tx.append( (tx_hash, tx_height) )
|
||||||
|
|
||||||
elif method == 'blockchain.transaction.get':
|
elif method == 'blockchain.transaction.get':
|
||||||
tx_hash = params[0]
|
tx_hash = params[0]
|
||||||
tx_height = params[1]
|
tx_height = params[1]
|
||||||
assert tx_hash == bitcoin.hash_encode(bitcoin.Hash(result.decode('hex')))
|
assert tx_hash == bitcoin.hash_encode(bitcoin.Hash(result.decode('hex')))
|
||||||
tx = Transaction.deserialize(result)
|
tx = Transaction(result)
|
||||||
|
try:
|
||||||
|
tx.deserialize()
|
||||||
|
except Exception:
|
||||||
|
self.print_msg("Warning: Cannot deserialize transactions. skipping")
|
||||||
|
continue
|
||||||
|
|
||||||
self.wallet.receive_tx_callback(tx_hash, tx, tx_height)
|
self.wallet.receive_tx_callback(tx_hash, tx, tx_height)
|
||||||
self.was_updated = True
|
self.was_updated = True
|
||||||
requested_tx.remove( (tx_hash, tx_height) )
|
requested_tx.remove( (tx_hash, tx_height) )
|
||||||
|
|
|
@ -482,25 +482,24 @@ class Transaction:
|
||||||
self.raw = self.serialize()
|
self.raw = self.serialize()
|
||||||
return self.raw
|
return self.raw
|
||||||
|
|
||||||
def __init__(self, inputs, outputs, locktime=0):
|
def __init__(self, raw):
|
||||||
self.inputs = inputs
|
|
||||||
self.outputs = outputs
|
|
||||||
self.locktime = locktime
|
|
||||||
self.raw = None
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def deserialize(klass, raw):
|
|
||||||
self = klass([],[])
|
|
||||||
self.update(raw)
|
|
||||||
return self
|
|
||||||
|
|
||||||
def update(self, raw):
|
|
||||||
d = deserialize(raw)
|
|
||||||
self.raw = raw
|
self.raw = raw
|
||||||
|
|
||||||
|
def deserialize(self):
|
||||||
|
d = deserialize(self.raw)
|
||||||
self.inputs = d['inputs']
|
self.inputs = d['inputs']
|
||||||
self.outputs = [(x['type'], x['address'], x['value']) for x in d['outputs']]
|
self.outputs = [(x['type'], x['address'], x['value']) for x in d['outputs']]
|
||||||
self.locktime = d['lockTime']
|
self.locktime = d['lockTime']
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_io(klass, inputs, outputs, locktime=0):
|
||||||
|
self = klass(None)
|
||||||
|
self.inputs = inputs
|
||||||
|
self.outputs = outputs
|
||||||
|
self.locktime = locktime
|
||||||
|
#self.raw = self.serialize()
|
||||||
|
return self
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def sweep(klass, privkeys, network, to_address, fee):
|
def sweep(klass, privkeys, network, to_address, fee):
|
||||||
inputs = []
|
inputs = []
|
||||||
|
@ -526,7 +525,7 @@ class Transaction:
|
||||||
|
|
||||||
total = sum(i.get('value') for i in inputs) - fee
|
total = sum(i.get('value') for i in inputs) - fee
|
||||||
outputs = [('address', to_address, total)]
|
outputs = [('address', to_address, total)]
|
||||||
self = klass(inputs, outputs)
|
self = klass.from_io(inputs, outputs)
|
||||||
self.sign({ pubkey:privkey })
|
self.sign({ pubkey:privkey })
|
||||||
return self
|
return self
|
||||||
|
|
||||||
|
@ -736,16 +735,6 @@ class Transaction:
|
||||||
self.raw = self.serialize()
|
self.raw = self.serialize()
|
||||||
|
|
||||||
|
|
||||||
def add_pubkey_addresses(self, txdict):
|
|
||||||
for txin in self.inputs:
|
|
||||||
if txin.get('address') == "(pubkey)":
|
|
||||||
prev_tx = txdict.get(txin.get('prevout_hash'))
|
|
||||||
if prev_tx:
|
|
||||||
address, value = prev_tx.get_outputs()[txin.get('prevout_n')]
|
|
||||||
print_error("found pay-to-pubkey address:", address)
|
|
||||||
txin["address"] = address
|
|
||||||
|
|
||||||
|
|
||||||
def get_outputs(self):
|
def get_outputs(self):
|
||||||
"""convert pubkeys to addresses"""
|
"""convert pubkeys to addresses"""
|
||||||
o = []
|
o = []
|
||||||
|
@ -767,60 +756,8 @@ class Transaction:
|
||||||
return (addr in self.get_output_addresses()) or (addr in (tx.get("address") for tx in self.inputs))
|
return (addr in self.get_output_addresses()) or (addr in (tx.get("address") for tx in self.inputs))
|
||||||
|
|
||||||
|
|
||||||
def get_value(self, addresses, prevout_values):
|
|
||||||
# return the balance for that tx
|
|
||||||
is_relevant = False
|
|
||||||
is_send = False
|
|
||||||
is_pruned = False
|
|
||||||
is_partial = False
|
|
||||||
v_in = v_out = v_out_mine = 0
|
|
||||||
|
|
||||||
for item in self.inputs:
|
|
||||||
addr = item.get('address')
|
|
||||||
if addr in addresses:
|
|
||||||
is_send = True
|
|
||||||
is_relevant = True
|
|
||||||
key = item['prevout_hash'] + ':%d'%item['prevout_n']
|
|
||||||
value = prevout_values.get( key )
|
|
||||||
if value is None:
|
|
||||||
is_pruned = True
|
|
||||||
else:
|
|
||||||
v_in += value
|
|
||||||
else:
|
|
||||||
is_partial = True
|
|
||||||
|
|
||||||
if not is_send: is_partial = False
|
|
||||||
|
|
||||||
for addr, value in self.get_outputs():
|
|
||||||
v_out += value
|
|
||||||
if addr in addresses:
|
|
||||||
v_out_mine += value
|
|
||||||
is_relevant = True
|
|
||||||
|
|
||||||
if is_pruned:
|
|
||||||
# some inputs are mine:
|
|
||||||
fee = None
|
|
||||||
if is_send:
|
|
||||||
v = v_out_mine - v_out
|
|
||||||
else:
|
|
||||||
# no input is mine
|
|
||||||
v = v_out_mine
|
|
||||||
|
|
||||||
else:
|
|
||||||
v = v_out_mine - v_in
|
|
||||||
|
|
||||||
if is_partial:
|
|
||||||
# some inputs are mine, but not all
|
|
||||||
fee = None
|
|
||||||
is_send = v < 0
|
|
||||||
else:
|
|
||||||
# all inputs are mine
|
|
||||||
fee = v_out - v_in
|
|
||||||
|
|
||||||
return is_relevant, is_send, v, fee
|
|
||||||
|
|
||||||
|
|
||||||
def as_dict(self):
|
def as_dict(self):
|
||||||
|
self.deserialize()
|
||||||
import json
|
import json
|
||||||
out = {
|
out = {
|
||||||
"hex":str(self),
|
"hex":str(self),
|
||||||
|
|
476
lib/wallet.py
476
lib/wallet.py
|
@ -28,6 +28,7 @@ import json
|
||||||
import copy
|
import copy
|
||||||
|
|
||||||
from util import print_msg, print_error, NotEnoughFunds
|
from util import print_msg, print_error, NotEnoughFunds
|
||||||
|
from util import profiler
|
||||||
|
|
||||||
from bitcoin import *
|
from bitcoin import *
|
||||||
from account import *
|
from account import *
|
||||||
|
@ -173,9 +174,6 @@ class Abstract_Wallet(object):
|
||||||
|
|
||||||
self.load_transactions()
|
self.load_transactions()
|
||||||
|
|
||||||
# not saved
|
|
||||||
self.prevout_values = {} # my own transaction outputs
|
|
||||||
self.spent_outputs = []
|
|
||||||
# spv
|
# spv
|
||||||
self.verifier = None
|
self.verifier = None
|
||||||
# there is a difference between wallet.up_to_date and interface.is_up_to_date()
|
# there is a difference between wallet.up_to_date and interface.is_up_to_date()
|
||||||
|
@ -185,42 +183,35 @@ class Abstract_Wallet(object):
|
||||||
self.lock = threading.Lock()
|
self.lock = threading.Lock()
|
||||||
self.transaction_lock = threading.Lock()
|
self.transaction_lock = threading.Lock()
|
||||||
self.tx_event = threading.Event()
|
self.tx_event = threading.Event()
|
||||||
for tx_hash, tx in self.transactions.items():
|
|
||||||
self.update_tx_outputs(tx_hash)
|
|
||||||
|
|
||||||
# save wallet type the first time
|
# save wallet type the first time
|
||||||
if self.storage.get('wallet_type') is None:
|
if self.storage.get('wallet_type') is None:
|
||||||
self.storage.put('wallet_type', self.wallet_type, True)
|
self.storage.put('wallet_type', self.wallet_type, True)
|
||||||
|
|
||||||
|
@profiler
|
||||||
def load_transactions(self):
|
def load_transactions(self):
|
||||||
|
self.txi = self.storage.get('txi', {})
|
||||||
|
self.txo = self.storage.get('txo', {})
|
||||||
|
self.reverse_txo = self.storage.get('reverse_utxo', {})
|
||||||
|
tx_list = self.storage.get('transactions', {})
|
||||||
self.transactions = {}
|
self.transactions = {}
|
||||||
tx_list = self.storage.get('transactions',{})
|
for tx_hash, raw in tx_list.items():
|
||||||
for k, raw in tx_list.items():
|
tx = Transaction(raw)
|
||||||
try:
|
self.transactions[tx_hash] = tx
|
||||||
tx = Transaction.deserialize(raw)
|
if self.txi.get(tx_hash) is None and self.txo.get(tx_hash) is None:
|
||||||
except Exception:
|
print_error("removing unreferenced tx", tx_hash)
|
||||||
print_msg("Warning: Cannot deserialize transactions. skipping")
|
self.transactions.pop(tx_hash)
|
||||||
continue
|
|
||||||
self.add_pubkey_addresses(tx)
|
|
||||||
self.transactions[k] = tx
|
|
||||||
for h,tx in self.transactions.items():
|
|
||||||
if not self.check_new_tx(h, tx):
|
|
||||||
print_error("removing unreferenced tx", h)
|
|
||||||
self.transactions.pop(h)
|
|
||||||
|
|
||||||
def add_pubkey_addresses(self, tx):
|
@profiler
|
||||||
# find the address corresponding to pay-to-pubkey inputs
|
def save_transactions(self):
|
||||||
h = tx.hash()
|
with self.transaction_lock:
|
||||||
|
tx = {}
|
||||||
# inputs
|
for k,v in self.transactions.items():
|
||||||
tx.add_pubkey_addresses(self.transactions)
|
tx[k] = str(v)
|
||||||
|
self.storage.put('transactions', tx)
|
||||||
# outputs of tx: inputs of tx2
|
self.storage.put('txi', self.txi)
|
||||||
for type, x, v in tx.outputs:
|
self.storage.put('txo', self.txo)
|
||||||
if type == 'pubkey':
|
self.storage.put('reverse_txo', self.reverse_txo)
|
||||||
for tx2 in self.transactions.values():
|
|
||||||
tx2.add_pubkey_addresses({h:tx})
|
|
||||||
|
|
||||||
def get_action(self):
|
def get_action(self):
|
||||||
pass
|
pass
|
||||||
|
@ -362,7 +353,6 @@ class Abstract_Wallet(object):
|
||||||
account_id, sequence = self.get_address_index(address)
|
account_id, sequence = self.get_address_index(address)
|
||||||
return self.accounts[account_id].get_pubkeys(*sequence)
|
return self.accounts[account_id].get_pubkeys(*sequence)
|
||||||
|
|
||||||
|
|
||||||
def sign_message(self, address, message, password):
|
def sign_message(self, address, message, password):
|
||||||
keys = self.get_private_key(address, password)
|
keys = self.get_private_key(address, password)
|
||||||
assert len(keys) == 1
|
assert len(keys) == 1
|
||||||
|
@ -396,7 +386,7 @@ class Abstract_Wallet(object):
|
||||||
def fill_addressbook(self):
|
def fill_addressbook(self):
|
||||||
# todo: optimize this
|
# todo: optimize this
|
||||||
for tx_hash, tx in self.transactions.viewitems():
|
for tx_hash, tx in self.transactions.viewitems():
|
||||||
is_relevant, is_send, _, _ = self.get_tx_value(tx)
|
_, is_send, _, _ = self.get_tx_value(tx)
|
||||||
if is_send:
|
if is_send:
|
||||||
for addr in tx.get_output_addresses():
|
for addr in tx.get_output_addresses():
|
||||||
if not self.is_mine(addr) and addr not in self.addressbook:
|
if not self.is_mine(addr) and addr not in self.addressbook:
|
||||||
|
@ -405,66 +395,139 @@ class Abstract_Wallet(object):
|
||||||
# self.update_tx_labels()
|
# self.update_tx_labels()
|
||||||
|
|
||||||
def get_num_tx(self, address):
|
def get_num_tx(self, address):
|
||||||
n = 0
|
""" return number of transactions where address is involved """
|
||||||
for tx in self.transactions.values():
|
return len(self.history.get(address, []))
|
||||||
if address in tx.get_output_addresses(): n += 1
|
#n = 0
|
||||||
return n
|
#for tx in self.transactions.values():
|
||||||
|
# if address in tx.get_output_addresses(): n += 1
|
||||||
|
#return n
|
||||||
|
|
||||||
def get_tx_value(self, tx, account=None):
|
def get_tx_delta(self, tx_hash, address):
|
||||||
domain = self.get_account_addresses(account)
|
"effect of tx on address"
|
||||||
return tx.get_value(domain, self.prevout_values)
|
delta = 0
|
||||||
|
# substract the value of coins sent from address
|
||||||
def update_tx_outputs(self, tx_hash):
|
d = self.txi.get(tx_hash, {}).get(address, [])
|
||||||
tx = self.transactions.get(tx_hash)
|
for n, v in d:
|
||||||
|
delta -= v
|
||||||
for i, (addr, value) in enumerate(tx.get_outputs()):
|
# add the value of the coins received at address
|
||||||
key = tx_hash+ ':%d'%i
|
d = self.txo.get(tx_hash, {}).get(address, [])
|
||||||
self.prevout_values[key] = value
|
for n, v, cb in d:
|
||||||
|
delta += v
|
||||||
|
return delta
|
||||||
|
|
||||||
|
def get_wallet_delta(self, tx):
|
||||||
|
""" effect of tx on wallet """
|
||||||
|
addresses = self.addresses(True)
|
||||||
|
is_relevant = False
|
||||||
|
is_send = False
|
||||||
|
is_pruned = False
|
||||||
|
is_partial = False
|
||||||
|
v_in = v_out = v_out_mine = 0
|
||||||
for item in tx.inputs:
|
for item in tx.inputs:
|
||||||
if self.is_mine(item.get('address')):
|
addr = item.get('address')
|
||||||
key = item['prevout_hash'] + ':%d'%item['prevout_n']
|
if addr in addresses:
|
||||||
self.spent_outputs.append(key)
|
is_send = True
|
||||||
|
is_relevant = True
|
||||||
|
d = self.txo.get(item['prevout_hash'], {}).get(addr, [])
|
||||||
|
for n, v, cb in d:
|
||||||
|
if n == item['prevout_n']:
|
||||||
|
value = v
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
value = None
|
||||||
|
if value is None:
|
||||||
|
is_pruned = True
|
||||||
|
else:
|
||||||
|
v_in += value
|
||||||
|
else:
|
||||||
|
is_partial = True
|
||||||
|
if not is_send:
|
||||||
|
is_partial = False
|
||||||
|
for addr, value in tx.get_outputs():
|
||||||
|
v_out += value
|
||||||
|
if addr in addresses:
|
||||||
|
v_out_mine += value
|
||||||
|
is_relevant = True
|
||||||
|
if is_pruned:
|
||||||
|
# some inputs are mine:
|
||||||
|
fee = None
|
||||||
|
if is_send:
|
||||||
|
v = v_out_mine - v_out
|
||||||
|
else:
|
||||||
|
# no input is mine
|
||||||
|
v = v_out_mine
|
||||||
|
else:
|
||||||
|
v = v_out_mine - v_in
|
||||||
|
if is_partial:
|
||||||
|
# some inputs are mine, but not all
|
||||||
|
fee = None
|
||||||
|
is_send = v < 0
|
||||||
|
else:
|
||||||
|
# all inputs are mine
|
||||||
|
fee = v_out - v_in
|
||||||
|
return is_relevant, is_send, v, fee
|
||||||
|
|
||||||
def get_addr_balance(self, address):
|
def get_addr_balance(self, address):
|
||||||
'returns the confirmed balance and pending (unconfirmed) balance change of this bitcoin address'
|
"returns the confirmed balance and pending (unconfirmed) balance change of a bitcoin address"
|
||||||
#assert self.is_mine(address)
|
h = self.history.get(address, [])
|
||||||
h = self.history.get(address,[])
|
|
||||||
if h == ['*']: return 0,0
|
|
||||||
c = u = 0
|
c = u = 0
|
||||||
received_coins = [] # list of coins received at address
|
for tx_hash, height in h:
|
||||||
# go through all tx in history of this address and collect the coins arriving on this address
|
v = self.get_tx_delta(tx_hash, address)
|
||||||
for tx_hash, tx_height in h:
|
if height > 0:
|
||||||
tx = self.transactions.get(tx_hash)
|
c += v
|
||||||
if not tx: continue
|
|
||||||
|
|
||||||
for i, (addr, value) in enumerate(tx.get_outputs()):
|
|
||||||
if addr == address:
|
|
||||||
key = tx_hash + ':%d'%i
|
|
||||||
received_coins.append(key)
|
|
||||||
# go through all tx in history of this address again
|
|
||||||
for tx_hash, tx_height in h:
|
|
||||||
tx = self.transactions.get(tx_hash)
|
|
||||||
if not tx: continue
|
|
||||||
v = 0
|
|
||||||
# substract the value of coins leaving from this address
|
|
||||||
for item in tx.inputs:
|
|
||||||
addr = item.get('address')
|
|
||||||
if addr == address:
|
|
||||||
key = item['prevout_hash'] + ':%d'%item['prevout_n']
|
|
||||||
value = self.prevout_values.get( key )
|
|
||||||
if key in received_coins:
|
|
||||||
v -= value
|
|
||||||
# add the value of the coins arriving in this address
|
|
||||||
for i, (addr, value) in enumerate(tx.get_outputs()):
|
|
||||||
key = tx_hash + ':%d'%i
|
|
||||||
if addr == address:
|
|
||||||
v += value
|
|
||||||
|
|
||||||
if tx_height:
|
|
||||||
c += v # confirmed coins value
|
|
||||||
else:
|
else:
|
||||||
u += v # unconfirmed coins value
|
u += v
|
||||||
|
return c, u
|
||||||
|
|
||||||
|
def get_addr_utxo(self, address):
|
||||||
|
h = self.history.get(address, [])
|
||||||
|
coins = {}
|
||||||
|
for tx_hash, height in h:
|
||||||
|
l = self.txo.get(tx_hash, {}).get(address, [])
|
||||||
|
for n, v, is_cb in l:
|
||||||
|
coins[tx_hash + ':%d'%n] = (height, v, is_cb)
|
||||||
|
for tx_hash, height in h:
|
||||||
|
l = self.txi.get(tx_hash, {}).get(address, [])
|
||||||
|
for txi, v in l:
|
||||||
|
coins.pop(txi)
|
||||||
|
return coins.items()
|
||||||
|
|
||||||
|
def get_unspent_coins(self, domain=None):
|
||||||
|
coins = []
|
||||||
|
if domain is None:
|
||||||
|
domain = self.addresses(True)
|
||||||
|
for addr in domain:
|
||||||
|
c = self.get_addr_utxo(addr)
|
||||||
|
for txo, v in c:
|
||||||
|
tx_height, value, is_cb = v
|
||||||
|
prevout_hash, prevout_n = txo.split(':')
|
||||||
|
output = {
|
||||||
|
'address':addr,
|
||||||
|
'value':value,
|
||||||
|
'prevout_n':int(prevout_n),
|
||||||
|
'prevout_hash':prevout_hash,
|
||||||
|
'height':tx_height,
|
||||||
|
'coinbase':is_cb
|
||||||
|
}
|
||||||
|
coins.append((tx_height, output))
|
||||||
|
continue
|
||||||
|
# sort by age
|
||||||
|
if coins:
|
||||||
|
coins = sorted(coins)
|
||||||
|
if coins[-1][0] != 0:
|
||||||
|
while coins[0][0] == 0:
|
||||||
|
coins = coins[1:] + [ coins[0] ]
|
||||||
|
return [value for height, value in coins]
|
||||||
|
|
||||||
|
def get_addr_balance2(self, address):
|
||||||
|
"returns the confirmed balance and pending (unconfirmed) balance change of a bitcoin address"
|
||||||
|
coins = self.get_addr_utxo(address)
|
||||||
|
c = u = 0
|
||||||
|
for txo, v, height in coins:
|
||||||
|
if height > 0:
|
||||||
|
c += v
|
||||||
|
else:
|
||||||
|
u += v
|
||||||
return c, u
|
return c, u
|
||||||
|
|
||||||
def get_account_name(self, k):
|
def get_account_name(self, k):
|
||||||
|
@ -508,133 +571,170 @@ class Abstract_Wallet(object):
|
||||||
uu += u
|
uu += u
|
||||||
return cc, uu
|
return cc, uu
|
||||||
|
|
||||||
def get_unspent_coins(self, domain=None):
|
|
||||||
coins = []
|
|
||||||
if domain is None: domain = self.addresses(True)
|
|
||||||
for addr in domain:
|
|
||||||
h = self.history.get(addr, [])
|
|
||||||
if h == ['*']: continue
|
|
||||||
for tx_hash, tx_height in h:
|
|
||||||
tx = self.transactions.get(tx_hash)
|
|
||||||
if tx is None: raise Exception("Wallet not synchronized")
|
|
||||||
is_coinbase = tx.inputs[0].get('prevout_hash') == '0'*64
|
|
||||||
for i, (address, value) in enumerate(tx.get_outputs()):
|
|
||||||
output = {'address':address, 'value':value, 'prevout_n':i}
|
|
||||||
if address != addr: continue
|
|
||||||
key = tx_hash + ":%d"%i
|
|
||||||
if key in self.spent_outputs: continue
|
|
||||||
output['prevout_hash'] = tx_hash
|
|
||||||
output['height'] = tx_height
|
|
||||||
output['coinbase'] = is_coinbase
|
|
||||||
coins.append((tx_height, output))
|
|
||||||
|
|
||||||
# sort by age
|
|
||||||
if coins:
|
|
||||||
coins = sorted(coins)
|
|
||||||
if coins[-1][0] != 0:
|
|
||||||
while coins[0][0] == 0:
|
|
||||||
coins = coins[1:] + [ coins[0] ]
|
|
||||||
return [value for height, value in coins]
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def set_fee(self, fee):
|
def set_fee(self, fee):
|
||||||
if self.fee_per_kb != fee:
|
if self.fee_per_kb != fee:
|
||||||
self.fee_per_kb = fee
|
self.fee_per_kb = fee
|
||||||
self.storage.put('fee_per_kb', self.fee_per_kb, True)
|
self.storage.put('fee_per_kb', self.fee_per_kb, True)
|
||||||
|
|
||||||
|
def get_address_history(self, address):
|
||||||
def get_history(self, address):
|
|
||||||
with self.lock:
|
with self.lock:
|
||||||
return self.history.get(address)
|
return self.history.get(address, [])
|
||||||
|
|
||||||
def get_status(self, h):
|
def get_status(self, h):
|
||||||
if not h: return None
|
if not h:
|
||||||
if h == ['*']: return '*'
|
return None
|
||||||
status = ''
|
status = ''
|
||||||
for tx_hash, height in h:
|
for tx_hash, height in h:
|
||||||
status += tx_hash + ':%d:' % height
|
status += tx_hash + ':%d:' % height
|
||||||
return hashlib.sha256( status ).digest().encode('hex')
|
return hashlib.sha256( status ).digest().encode('hex')
|
||||||
|
|
||||||
def receive_tx_callback(self, tx_hash, tx, tx_height):
|
def add_transaction(self, tx_hash, tx, tx_height):
|
||||||
|
is_coinbase = tx.inputs[0].get('prevout_hash') == '0'*64
|
||||||
with self.transaction_lock:
|
with self.transaction_lock:
|
||||||
self.add_pubkey_addresses(tx)
|
# add inputs
|
||||||
if not self.check_new_tx(tx_hash, tx):
|
self.txi[tx_hash] = d = {}
|
||||||
# may happen due to pruning
|
for txi in tx.inputs:
|
||||||
print_error("received transaction that is no longer referenced in history", tx_hash)
|
addr = txi.get('address')
|
||||||
return
|
if addr and self.is_mine(addr):
|
||||||
self.transactions[tx_hash] = tx
|
prevout_hash = txi['prevout_hash']
|
||||||
self.network.pending_transactions_for_notifications.append(tx)
|
prevout_n = txi['prevout_n']
|
||||||
self.save_transactions()
|
ser = prevout_hash + ':%d'%prevout_n
|
||||||
if self.verifier and tx_height>0:
|
dd = self.txo.get(prevout_hash, {})
|
||||||
self.verifier.add(tx_hash, tx_height)
|
for n, v, is_cb in dd.get(addr, []):
|
||||||
self.update_tx_outputs(tx_hash)
|
if n == prevout_n:
|
||||||
|
if d.get(addr) is None:
|
||||||
|
d[addr] = []
|
||||||
|
d[addr].append((ser, v))
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
self.reverse_txo[ser] = tx_hash
|
||||||
|
elif addr == "(pubkey)":
|
||||||
|
prevout_hash = txi['prevout_hash']
|
||||||
|
prevout_n = txi['prevout_n']
|
||||||
|
ser = prevout_hash + ':%d'%prevout_n
|
||||||
|
dd = self.txo.get(prevout_hash, {})
|
||||||
|
found = False
|
||||||
|
for _addr, l in dd.items():
|
||||||
|
for n, v, is_cb in l:
|
||||||
|
if n == prevout_n:
|
||||||
|
print_error("found pay-to-pubkey address:", _addr)
|
||||||
|
if d.get(_addr) is None:
|
||||||
|
d[_addr] = []
|
||||||
|
d[_addr].append((ser, v))
|
||||||
|
found = True
|
||||||
|
if not found:
|
||||||
|
self.reverse_txo[ser] = tx_hash
|
||||||
|
|
||||||
|
# add outputs
|
||||||
|
self.txo[tx_hash] = d = {}
|
||||||
|
for n, txo in enumerate(tx.outputs):
|
||||||
|
ser = tx_hash + ':%d'%n
|
||||||
|
_type, x, v = txo
|
||||||
|
if _type == 'address':
|
||||||
|
addr = x
|
||||||
|
elif _type == 'pubkey':
|
||||||
|
addr = public_key_to_bc_address(x.decode('hex'))
|
||||||
|
else:
|
||||||
|
addr = None
|
||||||
|
if addr and self.is_mine(addr):
|
||||||
|
if d.get(addr) is None:
|
||||||
|
d[addr] = []
|
||||||
|
d[addr].append((n, v, is_coinbase))
|
||||||
|
|
||||||
|
next_tx = self.reverse_txo.get(ser)
|
||||||
|
if next_tx is not None:
|
||||||
|
self.reverse_txo.pop(ser)
|
||||||
|
dd = self.txi.get(next_tx, {})
|
||||||
|
if dd.get(addr) is None:
|
||||||
|
dd[addr] = []
|
||||||
|
dd[addr].append((ser, v))
|
||||||
|
# save
|
||||||
|
self.transactions[tx_hash] = tx
|
||||||
|
|
||||||
|
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
|
||||||
|
self.add_transaction(tx_hash, tx, tx_height)
|
||||||
|
#self.network.pending_transactions_for_notifications.append(tx)
|
||||||
|
if self.verifier and tx_height>0:
|
||||||
|
self.verifier.add(tx_hash, tx_height)
|
||||||
|
|
||||||
def save_transactions(self):
|
|
||||||
tx = {}
|
|
||||||
for k,v in self.transactions.items():
|
|
||||||
tx[k] = str(v)
|
|
||||||
self.storage.put('transactions', tx, True)
|
|
||||||
|
|
||||||
def receive_history_callback(self, addr, hist):
|
def receive_history_callback(self, addr, hist):
|
||||||
|
|
||||||
if not self.check_new_history(addr, hist):
|
#if not self.check_new_history(addr, hist):
|
||||||
raise Exception("error: received history for %s is not consistent with known transactions"%addr)
|
# raise Exception("error: received history for %s is not consistent with known transactions"%addr)
|
||||||
|
|
||||||
with self.lock:
|
with self.lock:
|
||||||
self.history[addr] = hist
|
self.history[addr] = hist
|
||||||
self.storage.put('addr_history', self.history, True)
|
self.storage.put('addr_history', self.history, True)
|
||||||
|
|
||||||
if hist != ['*']:
|
for tx_hash, tx_height in hist:
|
||||||
for tx_hash, tx_height in hist:
|
if tx_height>0:
|
||||||
if tx_height>0:
|
# add it in case it was previously unconfirmed
|
||||||
# add it in case it was previously unconfirmed
|
if self.verifier:
|
||||||
if self.verifier: self.verifier.add(tx_hash, tx_height)
|
self.verifier.add(tx_hash, tx_height)
|
||||||
|
|
||||||
def get_tx_history(self, account=None):
|
# if addr is new, we have to recompute txi and txo
|
||||||
if not self.verifier:
|
# fixme: bad interaction with server hist limit?
|
||||||
return []
|
tx = self.transactions.get(tx_hash)
|
||||||
|
if tx is not None and self.txi.get(tx_hash, {}).get(addr) is None and self.txo.get(tx_hash, {}).get(addr) is None:
|
||||||
|
tx.deserialize()
|
||||||
|
self.add_transaction(tx_hash, tx, tx_height)
|
||||||
|
|
||||||
with self.transaction_lock:
|
|
||||||
history = self.transactions.items()
|
|
||||||
history.sort(key = lambda x: self.verifier.get_txpos(x[0]))
|
|
||||||
result = []
|
|
||||||
|
|
||||||
|
def get_history(self, account=None):
|
||||||
|
# get domain
|
||||||
|
domain = self.get_account_addresses(account)
|
||||||
|
|
||||||
|
hh = []
|
||||||
|
# 1. Get the history of each address in the domain
|
||||||
|
for addr in domain:
|
||||||
|
h = self.get_address_history(addr)
|
||||||
|
for tx_hash, height in h:
|
||||||
|
delta = self.get_tx_delta(tx_hash, addr)
|
||||||
|
hh.append([addr, tx_hash, height, delta])
|
||||||
|
|
||||||
|
# 2. merge
|
||||||
|
# the delta of a tx on the domain is the sum of its deltas on addresses
|
||||||
|
merged = {}
|
||||||
|
for addr, tx_hash, height, delta in hh:
|
||||||
|
if tx_hash not in merged:
|
||||||
|
merged[tx_hash] = (height, delta)
|
||||||
|
else:
|
||||||
|
h, d = merged.get(tx_hash)
|
||||||
|
merged[tx_hash] = (h, d + delta)
|
||||||
|
|
||||||
|
# 3. create sorted list
|
||||||
|
history = []
|
||||||
|
for tx_hash, v in merged.items():
|
||||||
|
height, value = v
|
||||||
|
is_mine = 1
|
||||||
|
fee = 0
|
||||||
balance = 0
|
balance = 0
|
||||||
for tx_hash, tx in history:
|
conf, timestamp = self.verifier.get_confirmations(tx_hash) if self.verifier else (None, None)
|
||||||
is_relevant, is_mine, v, fee = self.get_tx_value(tx, account)
|
history.append( (tx_hash, conf, value, timestamp) )
|
||||||
if v is not None: balance += v
|
|
||||||
|
|
||||||
c, u = self.get_account_balance(account)
|
history.sort(key = lambda x: self.verifier.get_txpos(x[0]))
|
||||||
|
return history
|
||||||
|
|
||||||
if balance != c+u:
|
|
||||||
result.append( ('', 1000, 0, c+u-balance, None, c+u-balance, None ) )
|
|
||||||
|
|
||||||
balance = c + u - balance
|
|
||||||
for tx_hash, tx in history:
|
|
||||||
is_relevant, is_mine, value, fee = self.get_tx_value(tx, account)
|
|
||||||
if not is_relevant:
|
|
||||||
continue
|
|
||||||
if value is not None:
|
|
||||||
balance += value
|
|
||||||
|
|
||||||
conf, timestamp = self.verifier.get_confirmations(tx_hash) if self.verifier else (None, None)
|
|
||||||
result.append( (tx_hash, conf, is_mine, value, fee, balance, timestamp) )
|
|
||||||
|
|
||||||
return result
|
|
||||||
|
|
||||||
def get_label(self, tx_hash):
|
def get_label(self, tx_hash):
|
||||||
label = self.labels.get(tx_hash)
|
label = self.labels.get(tx_hash)
|
||||||
is_default = (label == '') or (label is None)
|
is_default = (label == '') or (label is None)
|
||||||
if is_default: label = self.get_default_label(tx_hash)
|
if is_default:
|
||||||
|
label = self.get_default_label(tx_hash)
|
||||||
return label, is_default
|
return label, is_default
|
||||||
|
|
||||||
def get_default_label(self, tx_hash):
|
def get_default_label(self, tx_hash):
|
||||||
|
return tx_hash
|
||||||
|
|
||||||
tx = self.transactions.get(tx_hash)
|
tx = self.transactions.get(tx_hash)
|
||||||
default_label = ''
|
default_label = ''
|
||||||
if tx:
|
if tx:
|
||||||
is_relevant, is_mine, _, _ = self.get_tx_value(tx)
|
_, is_mine, _, _ = self.get_wallet_delta(tx)
|
||||||
if is_mine:
|
if is_mine:
|
||||||
for o_addr in tx.get_output_addresses():
|
for o_addr in tx.get_output_addresses():
|
||||||
if not self.is_mine(o_addr):
|
if not self.is_mine(o_addr):
|
||||||
|
@ -702,7 +802,7 @@ class Abstract_Wallet(object):
|
||||||
amount = sum( map(lambda x:x[2], outputs) )
|
amount = sum( map(lambda x:x[2], outputs) )
|
||||||
total = fee = 0
|
total = fee = 0
|
||||||
inputs = []
|
inputs = []
|
||||||
tx = Transaction(inputs, outputs)
|
tx = Transaction.from_io(inputs, outputs)
|
||||||
for item in coins:
|
for item in coins:
|
||||||
if item.get('coinbase') and item.get('height') + COINBASE_MATURITY > self.network.get_local_height():
|
if item.get('coinbase') and item.get('height') + COINBASE_MATURITY > self.network.get_local_height():
|
||||||
continue
|
continue
|
||||||
|
@ -860,7 +960,6 @@ class Abstract_Wallet(object):
|
||||||
|
|
||||||
# review transactions that are in the history
|
# review transactions that are in the history
|
||||||
for addr, hist in self.history.items():
|
for addr, hist in self.history.items():
|
||||||
if hist == ['*']: continue
|
|
||||||
for tx_hash, tx_height in hist:
|
for tx_hash, tx_height in hist:
|
||||||
if tx_height>0:
|
if tx_height>0:
|
||||||
# add it in case it was previously unconfirmed
|
# add it in case it was previously unconfirmed
|
||||||
|
@ -874,23 +973,22 @@ class Abstract_Wallet(object):
|
||||||
|
|
||||||
def check_new_history(self, addr, hist):
|
def check_new_history(self, addr, hist):
|
||||||
# check that all tx in hist are relevant
|
# check that all tx in hist are relevant
|
||||||
if hist != ['*']:
|
for tx_hash, height in hist:
|
||||||
for tx_hash, height in hist:
|
tx = self.transactions.get(tx_hash)
|
||||||
tx = self.transactions.get(tx_hash)
|
if not tx:
|
||||||
if not tx: continue
|
continue
|
||||||
if not tx.has_address(addr):
|
if not tx.has_address(addr):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# check that we are not "orphaning" a transaction
|
# check that we are not "orphaning" a transaction
|
||||||
old_hist = self.history.get(addr,[])
|
old_hist = self.history.get(addr,[])
|
||||||
if old_hist == ['*']: return True
|
|
||||||
|
|
||||||
for tx_hash, height in old_hist:
|
for tx_hash, height in old_hist:
|
||||||
if tx_hash in map(lambda x:x[0], hist): continue
|
if tx_hash in map(lambda x:x[0], hist):
|
||||||
|
continue
|
||||||
found = False
|
found = False
|
||||||
for _addr, _hist in self.history.items():
|
for _addr, _hist in self.history.items():
|
||||||
if _addr == addr: continue
|
if _addr == addr:
|
||||||
if _hist == ['*']: continue
|
continue
|
||||||
_tx_hist = map(lambda x:x[0], _hist)
|
_tx_hist = map(lambda x:x[0], _hist)
|
||||||
if tx_hash in _tx_hist:
|
if tx_hash in _tx_hist:
|
||||||
found = True
|
found = True
|
||||||
|
@ -916,7 +1014,6 @@ class Abstract_Wallet(object):
|
||||||
print_error("sync:", ext_requests, ext_h)
|
print_error("sync:", ext_requests, ext_h)
|
||||||
height = None
|
height = None
|
||||||
for h in ext_h:
|
for h in ext_h:
|
||||||
if h == ['*']: continue
|
|
||||||
for item in h:
|
for item in h:
|
||||||
if item.get('tx_hash') == tx_hash:
|
if item.get('tx_hash') == tx_hash:
|
||||||
height = item.get('height')
|
height = item.get('height')
|
||||||
|
@ -933,7 +1030,6 @@ class Abstract_Wallet(object):
|
||||||
# 1 check that tx is referenced in addr_history.
|
# 1 check that tx is referenced in addr_history.
|
||||||
addresses = []
|
addresses = []
|
||||||
for addr, hist in self.history.items():
|
for addr, hist in self.history.items():
|
||||||
if hist == ['*']:continue
|
|
||||||
for txh, height in hist:
|
for txh, height in hist:
|
||||||
if txh == tx_hash:
|
if txh == tx_hash:
|
||||||
addresses.append(addr)
|
addresses.append(addr)
|
||||||
|
@ -996,8 +1092,6 @@ class Abstract_Wallet(object):
|
||||||
def address_is_old(self, address, age_limit=2):
|
def address_is_old(self, address, age_limit=2):
|
||||||
age = -1
|
age = -1
|
||||||
h = self.history.get(address, [])
|
h = self.history.get(address, [])
|
||||||
if h == ['*']:
|
|
||||||
return True
|
|
||||||
for tx_hash, tx_height in h:
|
for tx_hash, tx_height in h:
|
||||||
if tx_height == 0:
|
if tx_height == 0:
|
||||||
tx_age = 0
|
tx_age = 0
|
||||||
|
|
|
@ -499,9 +499,9 @@ class Plugin(BasePlugin):
|
||||||
@hook
|
@hook
|
||||||
def load_wallet(self, wallet):
|
def load_wallet(self, wallet):
|
||||||
tx_list = {}
|
tx_list = {}
|
||||||
for item in self.wallet.get_tx_history(self.wallet.storage.get("current_account", None)):
|
for item in self.wallet.get_history(self.wallet.storage.get("current_account", None)):
|
||||||
tx_hash, conf, is_mine, value, fee, balance, timestamp = item
|
tx_hash, conf, value, timestamp = item
|
||||||
tx_list[tx_hash] = {'value': value, 'timestamp': timestamp, 'balance': balance}
|
tx_list[tx_hash] = {'value': value, 'timestamp': timestamp }
|
||||||
|
|
||||||
self.tx_list = tx_list
|
self.tx_list = tx_list
|
||||||
self.cur_exchange = self.config.get('use_exchange', "Blockchain")
|
self.cur_exchange = self.config.get('use_exchange', "Blockchain")
|
||||||
|
@ -572,20 +572,21 @@ class Plugin(BasePlugin):
|
||||||
except Exception:
|
except Exception:
|
||||||
newtx = self.wallet.get_tx_history()
|
newtx = self.wallet.get_tx_history()
|
||||||
v = newtx[[x[0] for x in newtx].index(str(item.data(0, Qt.UserRole).toPyObject()))][3]
|
v = newtx[[x[0] for x in newtx].index(str(item.data(0, Qt.UserRole).toPyObject()))][3]
|
||||||
tx_info = {'timestamp':int(time.time()), 'value': v }
|
tx_info = {'timestamp':int(time.time()), 'value': v}
|
||||||
pass
|
pass
|
||||||
tx_time = int(tx_info['timestamp'])
|
tx_time = int(tx_info['timestamp'])
|
||||||
|
tx_value = Decimal(str(tx_info['value'])) / 100000000
|
||||||
if self.cur_exchange == "CoinDesk":
|
if self.cur_exchange == "CoinDesk":
|
||||||
tx_time_str = datetime.datetime.fromtimestamp(tx_time).strftime('%Y-%m-%d')
|
tx_time_str = datetime.datetime.fromtimestamp(tx_time).strftime('%Y-%m-%d')
|
||||||
try:
|
try:
|
||||||
tx_fiat_val = "%.2f %s" % (Decimal(str(tx_info['value'])) / 100000000 * Decimal(self.resp_hist['bpi'][tx_time_str]), "USD")
|
tx_fiat_val = "%.2f %s" % (value * Decimal(self.resp_hist['bpi'][tx_time_str]), "USD")
|
||||||
except KeyError:
|
except KeyError:
|
||||||
tx_fiat_val = "%.2f %s" % (self.btc_rate * Decimal(str(tx_info['value']))/100000000 , "USD")
|
tx_fiat_val = "%.2f %s" % (self.btc_rate * Decimal(str(tx_info['value']))/100000000 , "USD")
|
||||||
elif self.cur_exchange == "Winkdex":
|
elif self.cur_exchange == "Winkdex":
|
||||||
tx_time_str = datetime.datetime.fromtimestamp(tx_time).strftime('%Y-%m-%d') + "T16:00:00-04:00"
|
tx_time_str = datetime.datetime.fromtimestamp(tx_time).strftime('%Y-%m-%d') + "T16:00:00-04:00"
|
||||||
try:
|
try:
|
||||||
tx_rate = self.resp_hist[[x['timestamp'] for x in self.resp_hist].index(tx_time_str)]['price']
|
tx_rate = self.resp_hist[[x['timestamp'] for x in self.resp_hist].index(tx_time_str)]['price']
|
||||||
tx_fiat_val = "%.2f %s" % (Decimal(tx_info['value']) / 100000000 * Decimal(tx_rate)/Decimal("100.0"), "USD")
|
tx_fiat_val = "%.2f %s" % (tx_value * Decimal(tx_rate)/Decimal("100.0"), "USD")
|
||||||
except ValueError:
|
except ValueError:
|
||||||
tx_fiat_val = "%.2f %s" % (self.btc_rate * Decimal(tx_info['value'])/100000000 , "USD")
|
tx_fiat_val = "%.2f %s" % (self.btc_rate * Decimal(tx_info['value'])/100000000 , "USD")
|
||||||
except KeyError:
|
except KeyError:
|
||||||
|
@ -594,7 +595,7 @@ class Plugin(BasePlugin):
|
||||||
tx_time_str = datetime.datetime.fromtimestamp(tx_time).strftime('%Y-%m-%d')
|
tx_time_str = datetime.datetime.fromtimestamp(tx_time).strftime('%Y-%m-%d')
|
||||||
try:
|
try:
|
||||||
num = self.resp_hist[tx_time_str].replace(',','')
|
num = self.resp_hist[tx_time_str].replace(',','')
|
||||||
tx_fiat_val = "%.2f %s" % (Decimal(str(tx_info['value'])) / 100000000 * Decimal(num), self.fiat_unit())
|
tx_fiat_val = "%.2f %s" % (tx_value * Decimal(num), self.fiat_unit())
|
||||||
except KeyError:
|
except KeyError:
|
||||||
tx_fiat_val = _("No data")
|
tx_fiat_val = _("No data")
|
||||||
|
|
||||||
|
|
|
@ -42,30 +42,28 @@ class Plugin(BasePlugin):
|
||||||
@hook
|
@hook
|
||||||
def export_history_dialog(self, d,hbox):
|
def export_history_dialog(self, d,hbox):
|
||||||
self.wallet = d.wallet
|
self.wallet = d.wallet
|
||||||
|
history = self.wallet.get_history()
|
||||||
history = self.wallet.get_tx_history()
|
|
||||||
|
|
||||||
if len(history) > 0:
|
if len(history) > 0:
|
||||||
b = QPushButton(_("Preview plot"))
|
b = QPushButton(_("Preview plot"))
|
||||||
hbox.addWidget(b)
|
hbox.addWidget(b)
|
||||||
b.clicked.connect(lambda: self.do_plot(self.wallet))
|
b.clicked.connect(lambda: self.do_plot(self.wallet, history))
|
||||||
else:
|
else:
|
||||||
b = QPushButton(_("No history to plot"))
|
b = QPushButton(_("No history to plot"))
|
||||||
hbox.addWidget(b)
|
hbox.addWidget(b)
|
||||||
|
|
||||||
|
|
||||||
|
def do_plot(self, wallet, history):
|
||||||
def do_plot(self,wallet):
|
|
||||||
history = wallet.get_tx_history()
|
|
||||||
balance_Val=[]
|
balance_Val=[]
|
||||||
fee_val=[]
|
fee_val=[]
|
||||||
value_val=[]
|
value_val=[]
|
||||||
datenums=[]
|
datenums=[]
|
||||||
unknown_trans=0
|
unknown_trans = 0
|
||||||
pending_trans=0
|
pending_trans = 0
|
||||||
counter_trans=0
|
counter_trans = 0
|
||||||
|
balance = 0
|
||||||
for item in history:
|
for item in history:
|
||||||
tx_hash, confirmations, is_mine, value, fee, balance, timestamp = item
|
tx_hash, confirmations, value, timestamp = item
|
||||||
|
balance += value
|
||||||
if confirmations:
|
if confirmations:
|
||||||
if timestamp is not None:
|
if timestamp is not None:
|
||||||
try:
|
try:
|
||||||
|
@ -73,24 +71,15 @@ class Plugin(BasePlugin):
|
||||||
balance_string = format_satoshis(balance, False)
|
balance_string = format_satoshis(balance, False)
|
||||||
balance_Val.append(float((format_satoshis(balance,False)))*1000.0)
|
balance_Val.append(float((format_satoshis(balance,False)))*1000.0)
|
||||||
except [RuntimeError, TypeError, NameError] as reason:
|
except [RuntimeError, TypeError, NameError] as reason:
|
||||||
unknown_trans=unknown_trans+1
|
unknown_trans += 1
|
||||||
pass
|
pass
|
||||||
else:
|
else:
|
||||||
unknown_trans=unknown_trans+1
|
unknown_trans += 1
|
||||||
else:
|
else:
|
||||||
pending_trans=pending_trans+1
|
pending_trans += 1
|
||||||
|
|
||||||
if value is not None:
|
value_string = format_satoshis(value, True)
|
||||||
value_string = format_satoshis(value, True)
|
value_val.append(float(value_string)*1000.0)
|
||||||
value_val.append(float(value_string)*1000.0)
|
|
||||||
else:
|
|
||||||
value_string = '--'
|
|
||||||
|
|
||||||
if fee is not None:
|
|
||||||
fee_string = format_satoshis(fee, True)
|
|
||||||
fee_val.append(float(fee_string))
|
|
||||||
else:
|
|
||||||
fee_string = '0'
|
|
||||||
|
|
||||||
if tx_hash:
|
if tx_hash:
|
||||||
label, is_default_label = wallet.get_label(tx_hash)
|
label, is_default_label = wallet.get_label(tx_hash)
|
||||||
|
@ -139,12 +128,9 @@ class Plugin(BasePlugin):
|
||||||
|
|
||||||
xfmt = md.DateFormatter('%Y-%m-%d')
|
xfmt = md.DateFormatter('%Y-%m-%d')
|
||||||
ax.xaxis.set_major_formatter(xfmt)
|
ax.xaxis.set_major_formatter(xfmt)
|
||||||
axarr[1].plot(datenums,fee_val,marker='o',linestyle='-',color='red',label='Fee')
|
|
||||||
axarr[1].plot(datenums,value_val,marker='o',linestyle='-',color='green',label='Value')
|
axarr[1].plot(datenums,value_val,marker='o',linestyle='-',color='green',label='Value')
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
axarr[1].legend(loc='upper left')
|
axarr[1].legend(loc='upper left')
|
||||||
# plt.annotate('unknown transaction = %d \n pending transactions = %d' %(unknown_trans,pending_trans),xy=(0.7,0.05),xycoords='axes fraction',size=12)
|
# plt.annotate('unknown transaction = %d \n pending transactions = %d' %(unknown_trans,pending_trans),xy=(0.7,0.05),xycoords='axes fraction',size=12)
|
||||||
plt.show()
|
plt.show()
|
||||||
|
|
Loading…
Add table
Reference in a new issue