1
0

simplify history-related commands:

- reduce number of methods
 - use nametuples instead of dicts
 - only two types: OnchainHistoryItem and LightningHistoryItem
 - channel open/closes are groups
 - move capital gains into separate RPC
This commit is contained in:
ThomasV
2025-02-16 16:59:19 +01:00
parent ae8bfdcb51
commit 392c219913
12 changed files with 284 additions and 231 deletions

View File

@@ -816,26 +816,26 @@ class Commands:
await self.addtransaction(result, wallet=wallet) await self.addtransaction(result, wallet=wallet)
return result return result
@command('w') def get_year_timestamps(self, year:int):
async def onchain_history(self, year=None, show_addresses=False, show_fiat=False, wallet: Abstract_Wallet = None, kwargs = {}
from_height=None, to_height=None):
"""Wallet onchain history. Returns the transaction history of your wallet."""
kwargs = {
'show_addresses': show_addresses,
'from_height': from_height,
'to_height': to_height,
}
if year: if year:
import time import time
start_date = datetime.datetime(year, 1, 1) start_date = datetime.datetime(year, 1, 1)
end_date = datetime.datetime(year+1, 1, 1) end_date = datetime.datetime(year+1, 1, 1)
kwargs['from_timestamp'] = time.mktime(start_date.timetuple()) kwargs['from_timestamp'] = time.mktime(start_date.timetuple())
kwargs['to_timestamp'] = time.mktime(end_date.timetuple()) kwargs['to_timestamp'] = time.mktime(end_date.timetuple())
if show_fiat: return kwargs
from .exchange_rate import FxThread
kwargs['fx'] = self.daemon.fx if self.daemon else FxThread(config=self.config)
return json_normalize(wallet.get_detailed_history(**kwargs)) @command('w')
async def onchain_capital_gains(self, year=None, wallet: Abstract_Wallet = None):
"""
Capital gains, using utxo pricing.
This cannot be used with lightning.
"""
kwargs = self.get_year_timestamps(year)
from .exchange_rate import FxThread
fx = self.daemon.fx if self.daemon else FxThread(config=self.config)
return json_normalize(wallet.get_onchain_capital_gains(fx, **kwargs))
@command('wp') @command('wp')
async def bumpfee(self, tx, new_fee_rate, from_coins=None, decrease_payment=False, password=None, unsigned=False, wallet: Abstract_Wallet = None): async def bumpfee(self, tx, new_fee_rate, from_coins=None, decrease_payment=False, password=None, unsigned=False, wallet: Abstract_Wallet = None):
@@ -867,11 +867,34 @@ class Commands:
wallet.sign_transaction(new_tx, password) wallet.sign_transaction(new_tx, password)
return new_tx.serialize() return new_tx.serialize()
@command('w')
async def onchain_history(self, show_fiat=False, year=None, show_addresses=False, wallet: Abstract_Wallet = None):
"""Wallet onchain history. Returns the transaction history of your wallet."""
kwargs = self.get_year_timestamps(year)
onchain_history = wallet.get_onchain_history(**kwargs)
out = [x.to_dict() for x in onchain_history.values()]
if show_fiat:
from .exchange_rate import FxThread
fx = self.daemon.fx if self.daemon else FxThread(config=self.config)
else:
fx = None
for item in out:
if show_addresses:
tx = wallet.db.get_transaction(item['txid'])
item['inputs'] = list(map(lambda x: x.to_json(), tx.inputs()))
item['outputs'] = list(map(lambda x: {'address': x.get_ui_address_str(), 'value_sat': x.value},
tx.outputs()))
if fx:
fiat_fields = wallet.get_tx_item_fiat(tx_hash=item['txid'], amount_sat=item['amount_sat'], fx=fx, tx_fee=item['fee_sat'])
item.update(fiat_fields)
return json_normalize(out)
@command('wl') @command('wl')
async def lightning_history(self, show_fiat=False, wallet: Abstract_Wallet = None): async def lightning_history(self, wallet: Abstract_Wallet = None):
""" lightning history """ """ lightning history. """
lightning_history = wallet.lnworker.get_history() if wallet.lnworker else [] lightning_history = wallet.lnworker.get_lightning_history() if wallet.lnworker else {}
return json_normalize(lightning_history) sorted_hist= sorted(lightning_history.values(), key=lambda x: x.timestamp)
return json_normalize([x.to_dict() for x in sorted_hist])
@command('w') @command('w')
async def setlabel(self, key, label, wallet: Abstract_Wallet = None): async def setlabel(self, key, label, wallet: Abstract_Wallet = None):

View File

@@ -144,11 +144,13 @@ Pane {
Layout.topMargin: constants.paddingSmall Layout.topMargin: constants.paddingSmall
text: qsTr('Payment hash') text: qsTr('Payment hash')
color: Material.accentColor color: Material.accentColor
visible: lnpaymentdetails.paymentHash
} }
TextHighlightPane { TextHighlightPane {
Layout.columnSpan: 2 Layout.columnSpan: 2
Layout.fillWidth: true Layout.fillWidth: true
visible: lnpaymentdetails.paymentHash
RowLayout { RowLayout {
width: parent.width width: parent.width
@@ -177,11 +179,13 @@ Pane {
Layout.topMargin: constants.paddingSmall Layout.topMargin: constants.paddingSmall
text: qsTr('Preimage') text: qsTr('Preimage')
color: Material.accentColor color: Material.accentColor
visible: lnpaymentdetails.preimage
} }
TextHighlightPane { TextHighlightPane {
Layout.columnSpan: 2 Layout.columnSpan: 2
Layout.fillWidth: true Layout.fillWidth: true
visible: lnpaymentdetails.preimage
RowLayout { RowLayout {
width: parent.width width: parent.width

View File

@@ -96,16 +96,16 @@ class QELnPaymentDetails(QObject):
return return
# TODO this is horribly inefficient. need a payment getter/query method # TODO this is horribly inefficient. need a payment getter/query method
tx = self._wallet.wallet.lnworker.get_lightning_history()[bfh(self._key)] tx = self._wallet.wallet.lnworker.get_lightning_history()[self._key]
self._logger.debug(str(tx)) self._logger.debug(str(tx))
self._fee.msatsInt = 0 if not tx['fee_msat'] else int(tx['fee_msat']) self._fee.msatsInt = 0 if not tx.fee_msat else int(tx.fee_msat)
self._amount.msatsInt = int(tx['amount_msat']) self._amount.msatsInt = int(tx.amount_msat)
self._label = tx['label'] self._label = tx.label
self._date = format_time(tx['timestamp']) self._date = format_time(tx.timestamp)
self._timestamp = tx['timestamp'] self._timestamp = tx.timestamp
self._status = 'settled' # TODO: other states? get_lightning_history is deciding the filter for us :( self._status = 'settled' # TODO: other states? get_lightning_history is deciding the filter for us :(
self._phash = tx['payment_hash'] self._phash = tx.payment_hash
self._preimage = tx['preimage'] self._preimage = tx.preimage
self.detailsChanged.emit() self.detailsChanged.emit()

View File

@@ -127,15 +127,14 @@ class QETransactionListModel(QAbstractListModel, QtEventListener):
#self._logger.debug(str(tx_item)) #self._logger.debug(str(tx_item))
item = tx_item item = tx_item
item['key'] = item['txid'] if 'txid' in item else item['payment_hash'] item['key'] = item.get('txid') or item.get('group_id') or item['payment_hash']
if 'lightning' not in item: if 'lightning' not in item:
item['lightning'] = False item['lightning'] = False
if item['lightning']: if item['lightning']:
item['value'] = QEAmount(amount_sat=item['value'].value, amount_msat=item['amount_msat']) item['value'] = QEAmount(amount_sat=item['value'].value, amount_msat=item['amount_msat'])
if item['type'] == 'payment': item['incoming'] = True if item['amount_msat'] > 0 else False
item['incoming'] = True if item['direction'] == 'received' else False
item['confirmations'] = 0 item['confirmations'] = 0
else: else:
item['value'] = QEAmount(amount_sat=item['value'].value) item['value'] = QEAmount(amount_sat=item['value'].value)

View File

@@ -330,20 +330,13 @@ class QETxDetails(QObject, QtEventListener):
self._sighash_danger = self._wallet.wallet.check_sighash(self._tx) self._sighash_danger = self._wallet.wallet.check_sighash(self._tx)
if self._wallet.wallet.lnworker: if self._wallet.wallet.lnworker:
# Calling lnworker.get_onchain_history and wallet.get_full_history here # Calling wallet.get_full_history here is inefficient.
# is inefficient. We should probably pass the tx_item to the constructor. # We should probably pass the tx_item to the constructor.
lnworker_history = self._wallet.wallet.lnworker.get_onchain_history() full_history = self._wallet.wallet.get_full_history()
if self._txid in lnworker_history: item = full_history.get('group:' + self._txid)
item = lnworker_history[self._txid] self._lnamount.satsInt = int(item['ln_value'].value) if item else 0
group_id = item.get('group_id') else:
if group_id: self._lnamount.satsInt = 0
full_history = self._wallet.wallet.get_full_history()
group_item = full_history['group:' + group_id]
self._lnamount.satsInt = int(group_item['ln_value'].value)
else:
self._lnamount.satsInt = int(item['amount_msat'] / 1000)
else:
self._lnamount.satsInt = 0
self._is_complete = self._tx.is_complete() self._is_complete = self._tx.is_complete()
self._is_rbf_enabled = self._tx.is_rbf_enabled() self._is_rbf_enabled = self._tx.is_rbf_enabled()

View File

@@ -601,11 +601,10 @@ class HistoryList(MyTreeView, AcceptFileDragDrop):
self.main_window.show_message(_("Enable fiat exchange rate with history.")) self.main_window.show_message(_("Enable fiat exchange rate with history."))
return return
fx = self.main_window.fx fx = self.main_window.fx
h = self.wallet.get_detailed_history( summary = self.wallet.get_onchain_capital_gains(
from_timestamp=time.mktime(self.start_date.timetuple()) if self.start_date else None, from_timestamp=time.mktime(self.start_date.timetuple()) if self.start_date else None,
to_timestamp=time.mktime(self.end_date.timetuple()) if self.end_date else None, to_timestamp=time.mktime(self.end_date.timetuple()) if self.end_date else None,
fx=fx) fx=fx)
summary = h['summary']
if not summary: if not summary:
self.main_window.show_message(_("Nothing to summarize.")) self.main_window.show_message(_("Nothing to summarize."))
return return
@@ -738,7 +737,7 @@ class HistoryList(MyTreeView, AcceptFileDragDrop):
# can happen e.g. before list is populated for the first time # can happen e.g. before list is populated for the first time
return return
tx_item = idx.internalPointer().get_data() tx_item = idx.internalPointer().get_data()
if tx_item.get('lightning') and tx_item['type'] == 'payment': if tx_item.get('lightning'):
menu = QMenu() menu = QMenu()
menu.addAction(_("Details"), lambda: self.main_window.show_lightning_transaction(tx_item)) menu.addAction(_("Details"), lambda: self.main_window.show_lightning_transaction(tx_item))
cc = self.add_copy_menu(menu, idx) cc = self.add_copy_menu(menu, idx)

View File

@@ -45,7 +45,6 @@ class LightningTxDialog(WindowModalDialog):
WindowModalDialog.__init__(self, parent, _("Lightning Payment")) WindowModalDialog.__init__(self, parent, _("Lightning Payment"))
self.main_window = parent self.main_window = parent
self.config = parent.config self.config = parent.config
self.is_sent = tx_item['direction'] == PaymentDirection.SENT
self.label = tx_item['label'] self.label = tx_item['label']
self.timestamp = tx_item['timestamp'] self.timestamp = tx_item['timestamp']
self.amount = Decimal(tx_item['amount_msat']) / 1000 self.amount = Decimal(tx_item['amount_msat']) / 1000
@@ -61,8 +60,8 @@ class LightningTxDialog(WindowModalDialog):
self.setLayout(vbox) self.setLayout(vbox)
amount_str = self.main_window.format_amount_and_units(self.amount, timestamp=self.timestamp) amount_str = self.main_window.format_amount_and_units(self.amount, timestamp=self.timestamp)
vbox.addWidget(QLabel(_("Amount") + f": {amount_str}")) vbox.addWidget(QLabel(_("Amount") + f": {amount_str}"))
if self.is_sent: fee_msat = tx_item.get('fee_msat')
fee_msat = tx_item['fee_msat'] if fee_msat is not None:
fee_sat = Decimal(fee_msat) / 1000 if fee_msat is not None else None fee_sat = Decimal(fee_msat) / 1000 if fee_msat is not None else None
fee_str = self.main_window.format_amount_and_units(fee_sat, timestamp=self.timestamp) fee_str = self.main_window.format_amount_and_units(fee_sat, timestamp=self.timestamp)
vbox.addWidget(QLabel(_("Fee: {}").format(fee_str))) vbox.addWidget(QLabel(_("Fee: {}").format(fee_str)))

View File

@@ -808,14 +808,15 @@ class TxDialog(QDialog, MessageBoxMixin):
if txid is not None and fx.is_enabled() and amount is not None: if txid is not None and fx.is_enabled() and amount is not None:
tx_item_fiat = self.wallet.get_tx_item_fiat( tx_item_fiat = self.wallet.get_tx_item_fiat(
tx_hash=txid, amount_sat=abs(amount), fx=fx, tx_fee=fee) tx_hash=txid, amount_sat=abs(amount), fx=fx, tx_fee=fee)
lnworker_history = self.wallet.lnworker.get_onchain_history() if self.wallet.lnworker else {}
if txid in lnworker_history: if self.wallet.lnworker:
item = lnworker_history[txid] # if it is a group, collect ln amount
ln_amount = item['amount_msat'] / 1000 full_history = self.wallet.get_full_history()
if amount is None: item = full_history.get('group:' + txid)
tx_mined_status = self.wallet.adb.get_tx_height(txid) ln_amount = item['ln_value'].value if item else None
else: else:
ln_amount = None ln_amount = None
self.broadcast_button.setEnabled(tx_details.can_broadcast) self.broadcast_button.setEnabled(tx_details.can_broadcast)
can_sign = not self.tx.is_complete() and \ can_sign = not self.tx.is_complete() and \
(self.wallet.can_sign(self.tx) or bool(self.external_keypairs)) (self.wallet.can_sign(self.tx) or bool(self.external_keypairs))

View File

@@ -36,7 +36,7 @@ from .util import (
profiler, OldTaskGroup, ESocksProxy, NetworkRetryManager, JsonRPCClient, NotEnoughFunds, EventListener, profiler, OldTaskGroup, ESocksProxy, NetworkRetryManager, JsonRPCClient, NotEnoughFunds, EventListener,
event_listener, bfh, InvoiceError, resolve_dns_srv, is_ip_address, log_exceptions, ignore_exceptions, event_listener, bfh, InvoiceError, resolve_dns_srv, is_ip_address, log_exceptions, ignore_exceptions,
make_aiohttp_session, timestamp_to_datetime, random_shuffled_copy, is_private_netaddress, make_aiohttp_session, timestamp_to_datetime, random_shuffled_copy, is_private_netaddress,
UnrelatedTransactionException UnrelatedTransactionException, LightningHistoryItem
) )
from .invoices import Invoice, PR_UNPAID, PR_PAID, PR_INFLIGHT, PR_FAILED, LN_EXPIRY_NEVER, BaseInvoice from .invoices import Invoice, PR_UNPAID, PR_PAID, PR_INFLIGHT, PR_FAILED, LN_EXPIRY_NEVER, BaseInvoice
from .bitcoin import COIN, opcodes, make_op_return, address_to_scripthash, DummyAddress from .bitcoin import COIN, opcodes, make_op_return, address_to_scripthash, DummyAddress
@@ -48,7 +48,6 @@ from .transaction import (
from .crypto import ( from .crypto import (
sha256, chacha20_encrypt, chacha20_decrypt, pw_encode_with_version_and_mac, pw_decode_with_version_and_mac sha256, chacha20_encrypt, chacha20_decrypt, pw_encode_with_version_and_mac, pw_decode_with_version_and_mac
) )
from .lntransport import LNTransport, LNResponderTransport, LNTransportBase, LNPeerAddr, split_host_port, extract_nodeid, ConnStringFormatError from .lntransport import LNTransport, LNResponderTransport, LNTransportBase, LNPeerAddr, split_host_port, extract_nodeid, ConnStringFormatError
from .lnpeer import Peer, LN_P2P_NETWORK_TIMEOUT from .lnpeer import Peer, LN_P2P_NETWORK_TIMEOUT
from .lnaddr import lnencode, LnAddr, lndecode from .lnaddr import lnencode, LnAddr, lndecode
@@ -983,7 +982,11 @@ class LNWallet(LNWorker):
timestamp = min([htlc_with_status.htlc.timestamp for htlc_with_status in plist]) timestamp = min([htlc_with_status.htlc.timestamp for htlc_with_status in plist])
return direction, amount_msat, fee_msat, timestamp return direction, amount_msat, fee_msat, timestamp
def get_lightning_history(self): def get_lightning_history(self) -> Dict[str, LightningHistoryItem]:
"""
side effect: sets defaults labels
note that the result is not ordered
"""
out = {} out = {}
for payment_hash, plist in self.get_payments(status='settled').items(): for payment_hash, plist in self.get_payments(status='settled').items():
if len(plist) == 0: if len(plist) == 0:
@@ -995,94 +998,94 @@ class LNWallet(LNWorker):
if not label and direction == PaymentDirection.FORWARDING: if not label and direction == PaymentDirection.FORWARDING:
label = _('Forwarding') label = _('Forwarding')
preimage = self.get_preimage(payment_hash).hex() preimage = self.get_preimage(payment_hash).hex()
item = { group_id = self.swap_manager.get_group_id_for_payment_hash(payment_hash)
'type': 'payment', item = LightningHistoryItem(
'label': label, type = 'payment',
'timestamp': timestamp or 0, payment_hash = payment_hash.hex(),
'date': timestamp_to_datetime(timestamp), preimage = preimage,
'direction': direction, amount_msat = amount_msat,
'amount_msat': amount_msat, fee_msat = fee_msat,
'fee_msat': fee_msat, group_id = group_id,
'payment_hash': key, timestamp = timestamp or 0,
'preimage': preimage, label=label,
} )
item['group_id'] = self.swap_manager.get_group_id_for_payment_hash(payment_hash)
out[payment_hash] = item out[payment_hash] = item
for chan in itertools.chain(self.channels.values(), self.channel_backups.values()): # type: AbstractChannel
item = chan.get_funding_height()
if item is None:
continue
funding_txid, funding_height, funding_timestamp = item
label = _('Open channel') + ' ' + chan.get_id_for_log()
self.wallet.set_default_label(funding_txid, label)
self.wallet.set_group_label(funding_txid, label)
item = LightningHistoryItem(
type = 'channel_opening',
label = label,
group_id = funding_txid,
timestamp = funding_timestamp,
amount_msat = chan.balance(LOCAL, ctn=0),
fee_msat = None,
payment_hash = None,
preimage = None,
)
out[funding_txid] = item
item = chan.get_closing_height()
if item is None:
continue
closing_txid, closing_height, closing_timestamp = item
label = _('Close channel') + ' ' + chan.get_id_for_log()
self.wallet.set_default_label(closing_txid, label)
self.wallet.set_group_label(closing_txid, label)
item = LightningHistoryItem(
type = 'channel_closing',
label = label,
group_id = closing_txid,
timestamp = closing_timestamp,
amount_msat = -chan.balance(LOCAL),
fee_msat = None,
payment_hash = None,
preimage = None,
)
out[closing_txid] = item
# sanity check
balance_msat = sum([x.amount_msat for x in out.values()])
lb = sum(chan.balance(LOCAL) if not chan.is_closed() else 0
for chan in self.channels.values())
assert balance_msat == lb
return out return out
def get_onchain_history(self): def get_groups_for_onchain_history(self) -> Dict[str, str]:
out = {} """
returns dict: txid -> group_id
side effect: sets default labels
"""
groups = {}
# add funding events # add funding events
for chan in itertools.chain(self.channels.values(), self.channel_backups.values()): # type: AbstractChannel for chan in itertools.chain(self.channels.values(), self.channel_backups.values()): # type: AbstractChannel
item = chan.get_funding_height() item = chan.get_funding_height()
if item is None: if item is None:
continue continue
funding_txid, funding_height, funding_timestamp = item funding_txid, funding_height, funding_timestamp = item
tx_height = self.wallet.adb.get_tx_height(funding_txid) groups[funding_txid] = funding_txid
self.wallet.set_default_label(chan.funding_outpoint.to_str(), _('Open channel') + ' ' + chan.get_id_for_log())
item = {
'channel_id': chan.channel_id.hex(),
'type': 'channel_opening',
'label': self.wallet.get_label_for_txid(funding_txid),
'txid': funding_txid,
'amount_msat': chan.balance(LOCAL, ctn=0),
'direction': PaymentDirection.RECEIVED,
'timestamp': tx_height.timestamp,
'monotonic_timestamp': tx_height.timestamp or TX_TIMESTAMP_INF,
'date': timestamp_to_datetime(tx_height.timestamp),
'fee_sat': None,
'fee_msat': None,
'height': tx_height.height,
'confirmations': tx_height.conf,
'txpos_in_block': tx_height.txpos,
} # FIXME this data structure needs to be kept in ~sync with wallet.get_onchain_history
out[funding_txid] = item
item = chan.get_closing_height() item = chan.get_closing_height()
if item is None: if item is None:
continue continue
closing_txid, closing_height, closing_timestamp = item closing_txid, closing_height, closing_timestamp = item
tx_height = self.wallet.adb.get_tx_height(closing_txid) groups[closing_txid] = closing_txid
self.wallet.set_default_label(closing_txid, _('Close channel') + ' ' + chan.get_id_for_log())
item = {
'channel_id': chan.channel_id.hex(),
'txid': closing_txid,
'label': self.wallet.get_label_for_txid(closing_txid),
'type': 'channel_closure',
'amount_msat': -chan.balance(LOCAL),
'direction': PaymentDirection.SENT,
'timestamp': tx_height.timestamp,
'monotonic_timestamp': tx_height.timestamp or TX_TIMESTAMP_INF,
'date': timestamp_to_datetime(tx_height.timestamp),
'fee_sat': None,
'fee_msat': None,
'height': tx_height.height,
'confirmations': tx_height.conf,
'txpos_in_block': tx_height.txpos,
} # FIXME this data structure needs to be kept in ~sync with wallet.get_onchain_history
out[closing_txid] = item
d = self.swap_manager.get_groups_for_onchain_history() d = self.swap_manager.get_groups_for_onchain_history()
for k,v in d.items(): for txid, v in d.items():
group_id = v.get('group_id') group_id = v['group_id']
group_label = v.get('group_label') label = v.get('label')
if group_id and group_label: group_label = v.get('group_label') or label
self.wallet.set_default_label(group_id, group_label) groups[txid] = group_id
out.update(d) if label:
return out self.wallet.set_default_label(txid, label)
if group_label:
self.wallet.set_group_label(group_id, group_label)
def get_history(self): return groups
out = list(self.get_lightning_history().values()) + list(self.get_onchain_history().values())
# sort by timestamp
out.sort(key=lambda x: (x.get('timestamp') or float("inf")))
balance_msat = 0
for item in out:
balance_msat += item['amount_msat']
item['balance_msat'] = balance_msat
lb = sum(chan.balance(LOCAL) if not chan.is_closed() else 0
for chan in self.channels.values())
assert balance_msat == lb
return out
def channel_peers(self) -> List[bytes]: def channel_peers(self) -> List[bytes]:
node_ids = [chan.node_id for chan in self.channels.values() if not chan.is_closed()] node_ids = [chan.node_id for chan in self.channels.values() if not chan.is_closed()]

View File

@@ -1234,8 +1234,6 @@ class SwapManager(Logger):
label += f' (refundable in {-delta} blocks)' # fixme: only if unspent label += f' (refundable in {-delta} blocks)' # fixme: only if unspent
d[txid] = { d[txid] = {
'group_id': txid, 'group_id': txid,
'amount_msat': 0, # must be zero for onchain tx
'type': 'swap',
'label': label, 'label': label,
'group_label': group_label, 'group_label': group_label,
} }
@@ -1244,8 +1242,7 @@ class SwapManager(Logger):
# to the group (see wallet.get_full_history) # to the group (see wallet.get_full_history)
d[swap.spending_txid] = { d[swap.spending_txid] = {
'group_id': txid, 'group_id': txid,
'amount_msat': 0, # must be zero for onchain tx 'group_label': group_label,
'type': 'swap',
'label': _('Refund transaction'), 'label': _('Refund transaction'),
} }
return d return d
@@ -1254,10 +1251,7 @@ class SwapManager(Logger):
# add group_id to swap transactions # add group_id to swap transactions
swap = self.get_swap(payment_hash) swap = self.get_swap(payment_hash)
if swap: if swap:
if swap.is_reverse: return swap.spending_txid if swap.is_reverse else swap.funding_txid
return swap.spending_txid
else:
return swap.funding_txid
class SwapServerTransport(Logger): class SwapServerTransport(Logger):

View File

@@ -2184,3 +2184,56 @@ def get_nostr_ann_pow_amount(nostr_pubk: bytes, nonce: Optional[int]) -> int:
digest = hash_function(hash_preimage + nonce.to_bytes(32, 'big')).digest() digest = hash_function(hash_preimage + nonce.to_bytes(32, 'big')).digest()
digest = int.from_bytes(digest, 'big') digest = int.from_bytes(digest, 'big')
return hash_len_bits - digest.bit_length() return hash_len_bits - digest.bit_length()
class OnchainHistoryItem(NamedTuple):
txid: str
amount_sat: int
fee_sat: int
balance_sat: int
tx_mined_status: TxMinedInfo
group_id: Optional[str]
label: str
monotonic_timestamp: int
group_id: Optional[str]
def to_dict(self):
return {
'txid': self.txid,
'amount_sat': self.amount_sat,
'fee_sat': self.fee_sat,
'height': self.tx_mined_status.height,
'confirmations': self.tx_mined_status.conf,
'timestamp': self.tx_mined_status.timestamp,
'monotonic_timestamp': self.monotonic_timestamp,
'incoming': True if self.amount_sat>0 else False,
'bc_value': Satoshis(self.amount_sat),
'bc_balance': Satoshis(self.balance_sat),
'date': timestamp_to_datetime(self.tx_mined_status.timestamp),
'txpos_in_block': self.tx_mined_status.txpos,
'wanted_height': self.tx_mined_status.wanted_height,
'label': self.label,
'group_id': self.group_id,
}
class LightningHistoryItem(NamedTuple):
payment_hash: str
preimage: str
amount_msat: int
fee_msat: Optional[int]
type: str
group_id: Optional[str]
timestamp: int
label: str
def to_dict(self):
return {
'type': self.type,
'label': self.label,
'timestamp': self.timestamp or 0,
'date': timestamp_to_datetime(self.timestamp),
'amount_msat': self.amount_msat,
'fee_msat': self.fee_msat,
'payment_hash': self.payment_hash,
'preimage': self.preimage,
'group_id': self.group_id,
'ln_value': Satoshis(Decimal(self.amount_msat) / 1000),
}

View File

@@ -88,6 +88,7 @@ from .util import read_json_file, write_json_file, UserFacingException, FileImpo
from .util import EventListener, event_listener from .util import EventListener, event_listener
from . import descriptor from . import descriptor
from .descriptor import Descriptor from .descriptor import Descriptor
from .util import OnchainHistoryItem, LightningHistoryItem
if TYPE_CHECKING: if TYPE_CHECKING:
from .network import Network from .network import Network
@@ -1012,11 +1013,11 @@ class Abstract_Wallet(ABC, Logger, EventListener):
""" """
with self.lock, self.transaction_lock: with self.lock, self.transaction_lock:
if self._last_full_history is None: if self._last_full_history is None:
self._last_full_history = self.get_full_history(None, include_lightning=False) self._last_full_history = self.get_onchain_history()
# populate cache in chronological order (confirmed tx only) # populate cache in chronological order (confirmed tx only)
# todo: get_full_history should return unconfirmed tx topologically sorted # todo: get_full_history should return unconfirmed tx topologically sorted
for _txid, tx_item in self._last_full_history.items(): for _txid, tx_item in self._last_full_history.items():
if tx_item['height'] > 0: if tx_item.tx_mined_status.height > 0:
self.get_tx_parents(_txid) self.get_tx_parents(_txid)
result = self._tx_parents_cache.get(txid, None) result = self._tx_parents_cache.get(txid, None)
@@ -1145,28 +1146,53 @@ class Abstract_Wallet(ABC, Logger, EventListener):
# return last balance # return last balance
return balance return balance
def get_onchain_history(self, *, domain=None): def get_onchain_history(
self, *,
domain=None,
from_timestamp=None,
to_timestamp=None,
from_height=None,
to_height=None) -> Dict[str, OnchainHistoryItem]:
# sanity check
if (from_timestamp is not None or to_timestamp is not None) \
and (from_height is not None or to_height is not None):
raise UserFacingException('timestamp and block height based filtering cannot be used together')
# call lnworker first, because it adds accounting addresses
groups = self.lnworker.get_groups_for_onchain_history() if self.lnworker else {}
if domain is None: if domain is None:
domain = self.get_addresses() domain = self.get_addresses()
now = time.time()
transactions = OrderedDictWithIndex()
monotonic_timestamp = 0 monotonic_timestamp = 0
for hist_item in self.adb.get_history(domain=domain): for hist_item in self.adb.get_history(domain=domain):
monotonic_timestamp = max(monotonic_timestamp, (hist_item.tx_mined_status.timestamp or TX_TIMESTAMP_INF)) timestamp = (hist_item.tx_mined_status.timestamp or TX_TIMESTAMP_INF)
d = { height = hist_item.tx_mined_status
'txid': hist_item.txid, if from_timestamp and (timestamp or now) < from_timestamp:
'fee_sat': hist_item.fee, continue
'height': hist_item.tx_mined_status.height, if to_timestamp and (timestamp or now) >= to_timestamp:
'confirmations': hist_item.tx_mined_status.conf, continue
'timestamp': hist_item.tx_mined_status.timestamp, if from_height is not None and from_height > height > 0:
'monotonic_timestamp': monotonic_timestamp, continue
'incoming': True if hist_item.delta>0 else False, if to_height is not None and (height >= to_height or height <= 0):
'bc_value': Satoshis(hist_item.delta), continue
'bc_balance': Satoshis(hist_item.balance), monotonic_timestamp = max(monotonic_timestamp, timestamp)
'date': timestamp_to_datetime(hist_item.tx_mined_status.timestamp), txid = hist_item.txid
'label': self.get_label_for_txid(hist_item.txid), group_id = groups.get(txid)
'txpos_in_block': hist_item.tx_mined_status.txpos, label = self.get_label_for_txid(txid)
'wanted_height': hist_item.tx_mined_status.wanted_height, tx_item = OnchainHistoryItem(
} txid=hist_item.txid,
yield d amount_sat=hist_item.delta,
fee_sat=hist_item.fee,
balance_sat=hist_item.balance,
tx_mined_status=hist_item.tx_mined_status,
label=label,
monotonic_timestamp=monotonic_timestamp,
group_id=group_id,
)
transactions[hist_item.txid] = tx_item
return transactions
def create_invoice(self, *, outputs: List[PartialTxOutput], message, pr, URI) -> Invoice: def create_invoice(self, *, outputs: List[PartialTxOutput], message, pr, URI) -> Invoice:
height = self.adb.get_local_height() height = self.adb.get_local_height()
@@ -1342,44 +1368,26 @@ class Abstract_Wallet(ABC, Logger, EventListener):
return is_paid, conf_needed return is_paid, conf_needed
@profiler @profiler
def get_full_history(self, fx=None, *, onchain_domain=None, include_lightning=True, include_fiat=False): def get_full_history(self, fx=None, *, onchain_domain=None, include_lightning=True, include_fiat=False) -> dict:
"""
includes both onchain and lightning
includes grouping information
"""
transactions_tmp = OrderedDictWithIndex() transactions_tmp = OrderedDictWithIndex()
# add on-chain txns # add on-chain txns
onchain_history = self.get_onchain_history(domain=onchain_domain) onchain_history = self.get_onchain_history(domain=onchain_domain)
for tx_item in onchain_history: for tx_item in onchain_history.values():
txid = tx_item['txid'] txid = tx_item.txid
transactions_tmp[txid] = tx_item transactions_tmp[txid] = tx_item.to_dict()
# add lnworker onchain transactions to transactions_tmp transactions_tmp[txid]['lightning'] = False
# add group_id to tx that are in a group
lnworker_history = self.lnworker.get_onchain_history() if self.lnworker and include_lightning else {}
for txid, item in lnworker_history.items():
if txid in transactions_tmp:
tx_item = transactions_tmp[txid]
tx_item['group_id'] = item.get('group_id') # for swaps
tx_item['label'] = item['label']
tx_item['type'] = item['type']
ln_value = Decimal(item['amount_msat']) / 1000 # for channel open/close tx
tx_item['ln_value'] = Satoshis(ln_value)
if channel_id := item.get('channel_id'):
tx_item['channel_id'] = channel_id
else:
if item['type'] == 'swap':
# swap items do not have all the fields. We can skip skip them
# because they will eventually be in onchain_history
# TODO: use attr.s objects instead of dicts
continue
transactions_tmp[txid] = item
ln_value = Decimal(item['amount_msat']) / 1000 # for channel open/close tx
item['ln_value'] = Satoshis(ln_value)
# add lightning_transactions # add lightning_transactions
lightning_history = self.lnworker.get_lightning_history() if self.lnworker and include_lightning else {} lightning_history = self.lnworker.get_lightning_history() if self.lnworker and include_lightning else {}
for tx_item in lightning_history.values(): for tx_item in lightning_history.values():
txid = tx_item.get('txid') key = tx_item.payment_hash or 'ln:' + tx_item.group_id
ln_value = Decimal(tx_item['amount_msat']) / 1000 transactions_tmp[key] = tx_item.to_dict()
tx_item['lightning'] = True transactions_tmp[key]['lightning'] = True
tx_item['ln_value'] = Satoshis(ln_value)
key = tx_item.get('txid') or tx_item['payment_hash']
transactions_tmp[key] = tx_item
# sort on-chain and LN stuff into new dict, by timestamp # sort on-chain and LN stuff into new dict, by timestamp
# (we rely on this being a *stable* sort) # (we rely on this being a *stable* sort)
def sort_key(x): def sort_key(x):
@@ -1396,10 +1404,10 @@ class Abstract_Wallet(ABC, Logger, EventListener):
else: else:
key = 'group:' + group_id key = 'group:' + group_id
parent = transactions.get(key) parent = transactions.get(key)
label = self.get_label_for_txid(group_id) group_label = self.get_label_for_group(group_id)
if parent is None: if parent is None:
parent = { parent = {
'label': label, 'label': group_label,
'fiat_value': Fiat(Decimal(0), fx.ccy) if fx else None, 'fiat_value': Fiat(Decimal(0), fx.ccy) if fx else None,
'bc_value': Satoshis(0), 'bc_value': Satoshis(0),
'ln_value': Satoshis(0), 'ln_value': Satoshis(0),
@@ -1455,20 +1463,12 @@ class Abstract_Wallet(ABC, Logger, EventListener):
return transactions return transactions
@profiler @profiler
def get_detailed_history( def get_onchain_capital_gains(self, fx, **kwargs):
self,
from_timestamp=None,
to_timestamp=None,
fx=None,
show_addresses=False,
from_height=None,
to_height=None):
# History with capital gains, using utxo pricing # History with capital gains, using utxo pricing
# FIXME: Lightning capital gains would requires FIFO # FIXME: Lightning capital gains would requires FIFO
if (from_timestamp is not None or to_timestamp is not None) \ from_timestamp = kwargs.get('from_timestamp')
and (from_height is not None or to_height is not None): to_timestamp = kwargs.get('to_timestamp')
raise UserFacingException('timestamp and block height based filtering cannot be used together') history = self.get_onchain_history(**kwargs)
show_fiat = fx and fx.is_enabled() and fx.has_history() show_fiat = fx and fx.is_enabled() and fx.has_history()
out = [] out = []
income = 0 income = 0
@@ -1476,26 +1476,13 @@ class Abstract_Wallet(ABC, Logger, EventListener):
capital_gains = Decimal(0) capital_gains = Decimal(0)
fiat_income = Decimal(0) fiat_income = Decimal(0)
fiat_expenditures = Decimal(0) fiat_expenditures = Decimal(0)
now = time.time() for txid, hitem in history.items():
for item in self.get_onchain_history(): item = hitem.to_dict()
if item['bc_value'].value == 0:
continue
timestamp = item['timestamp'] timestamp = item['timestamp']
if from_timestamp and (timestamp or now) < from_timestamp:
continue
if to_timestamp and (timestamp or now) >= to_timestamp:
continue
height = item['height']
if from_height is not None and from_height > height > 0:
continue
if to_height is not None and (height >= to_height or height <= 0):
continue
tx_hash = item['txid'] tx_hash = item['txid']
tx = self.db.get_transaction(tx_hash)
tx_fee = item['fee_sat'] tx_fee = item['fee_sat']
item['fee'] = Satoshis(tx_fee) if tx_fee is not None else None
if show_addresses:
item['inputs'] = list(map(lambda x: x.to_json(), tx.inputs()))
item['outputs'] = list(map(lambda x: {'address': x.get_ui_address_str(), 'value': Satoshis(x.value)},
tx.outputs()))
# fixme: use in and out values # fixme: use in and out values
value = item['bc_value'].value value = item['bc_value'].value
if value < 0: if value < 0:
@@ -1506,7 +1493,6 @@ class Abstract_Wallet(ABC, Logger, EventListener):
if show_fiat: if show_fiat:
fiat_fields = self.get_tx_item_fiat(tx_hash=tx_hash, amount_sat=value, fx=fx, tx_fee=tx_fee) fiat_fields = self.get_tx_item_fiat(tx_hash=tx_hash, amount_sat=value, fx=fx, tx_fee=tx_fee)
fiat_value = fiat_fields['fiat_value'].value fiat_value = fiat_fields['fiat_value'].value
item.update(fiat_fields)
if value < 0: if value < 0:
capital_gains += fiat_fields['capital_gain'].value capital_gains += fiat_fields['capital_gain'].value
fiat_expenditures += -fiat_value fiat_expenditures += -fiat_value
@@ -1517,12 +1503,8 @@ class Abstract_Wallet(ABC, Logger, EventListener):
if out: if out:
first_item = out[0] first_item = out[0]
last_item = out[-1] last_item = out[-1]
if from_height or to_height: start_height = first_item['height'] - 1
start_height = from_height end_height = last_item['height']
end_height = to_height
else:
start_height = first_item['height'] - 1
end_height = last_item['height']
b = first_item['bc_balance'].value b = first_item['bc_balance'].value
v = first_item['bc_value'].value v = first_item['bc_value'].value
@@ -1583,10 +1565,7 @@ class Abstract_Wallet(ABC, Logger, EventListener):
else: else:
summary = {} summary = {}
return { return summary
'transactions': out,
'summary': summary
}
def acquisition_price(self, coins, price_func, ccy): def acquisition_price(self, coins, price_func, ccy):
return Decimal(sum(self.coin_price(coin.prevout.txid.hex(), price_func, ccy, self.adb.get_txin_value(coin)) for coin in coins)) return Decimal(sum(self.coin_price(coin.prevout.txid.hex(), price_func, ccy, self.adb.get_txin_value(coin)) for coin in coins))
@@ -1644,6 +1623,12 @@ class Abstract_Wallet(ABC, Logger, EventListener):
def _get_default_label_for_outpoint(self, outpoint: str) -> str: def _get_default_label_for_outpoint(self, outpoint: str) -> str:
return self._default_labels.get(outpoint) return self._default_labels.get(outpoint)
def get_label_for_group(self, group_id: str) -> str:
return self._default_labels.get('group:' + group_id)
def set_group_label(self, group_id: str, label: str):
self._default_labels['group:' + group_id] = label
def get_label_for_txid(self, tx_hash: str) -> str: def get_label_for_txid(self, tx_hash: str) -> str:
return self._labels.get(tx_hash) or self._get_default_label_for_txid(tx_hash) return self._labels.get(tx_hash) or self._get_default_label_for_txid(tx_hash)