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:
@@ -816,26 +816,26 @@ class Commands:
|
||||
await self.addtransaction(result, wallet=wallet)
|
||||
return result
|
||||
|
||||
@command('w')
|
||||
async def onchain_history(self, year=None, show_addresses=False, show_fiat=False, wallet: Abstract_Wallet = None,
|
||||
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,
|
||||
}
|
||||
def get_year_timestamps(self, year:int):
|
||||
kwargs = {}
|
||||
if year:
|
||||
import time
|
||||
start_date = datetime.datetime(year, 1, 1)
|
||||
end_date = datetime.datetime(year+1, 1, 1)
|
||||
kwargs['from_timestamp'] = time.mktime(start_date.timetuple())
|
||||
kwargs['to_timestamp'] = time.mktime(end_date.timetuple())
|
||||
if show_fiat:
|
||||
from .exchange_rate import FxThread
|
||||
kwargs['fx'] = self.daemon.fx if self.daemon else FxThread(config=self.config)
|
||||
return kwargs
|
||||
|
||||
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')
|
||||
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)
|
||||
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')
|
||||
async def lightning_history(self, show_fiat=False, wallet: Abstract_Wallet = None):
|
||||
""" lightning history """
|
||||
lightning_history = wallet.lnworker.get_history() if wallet.lnworker else []
|
||||
return json_normalize(lightning_history)
|
||||
async def lightning_history(self, wallet: Abstract_Wallet = None):
|
||||
""" lightning history. """
|
||||
lightning_history = wallet.lnworker.get_lightning_history() if wallet.lnworker else {}
|
||||
sorted_hist= sorted(lightning_history.values(), key=lambda x: x.timestamp)
|
||||
return json_normalize([x.to_dict() for x in sorted_hist])
|
||||
|
||||
@command('w')
|
||||
async def setlabel(self, key, label, wallet: Abstract_Wallet = None):
|
||||
|
||||
@@ -144,11 +144,13 @@ Pane {
|
||||
Layout.topMargin: constants.paddingSmall
|
||||
text: qsTr('Payment hash')
|
||||
color: Material.accentColor
|
||||
visible: lnpaymentdetails.paymentHash
|
||||
}
|
||||
|
||||
TextHighlightPane {
|
||||
Layout.columnSpan: 2
|
||||
Layout.fillWidth: true
|
||||
visible: lnpaymentdetails.paymentHash
|
||||
|
||||
RowLayout {
|
||||
width: parent.width
|
||||
@@ -177,11 +179,13 @@ Pane {
|
||||
Layout.topMargin: constants.paddingSmall
|
||||
text: qsTr('Preimage')
|
||||
color: Material.accentColor
|
||||
visible: lnpaymentdetails.preimage
|
||||
}
|
||||
|
||||
TextHighlightPane {
|
||||
Layout.columnSpan: 2
|
||||
Layout.fillWidth: true
|
||||
visible: lnpaymentdetails.preimage
|
||||
|
||||
RowLayout {
|
||||
width: parent.width
|
||||
|
||||
@@ -96,16 +96,16 @@ class QELnPaymentDetails(QObject):
|
||||
return
|
||||
|
||||
# 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._fee.msatsInt = 0 if not tx['fee_msat'] else int(tx['fee_msat'])
|
||||
self._amount.msatsInt = int(tx['amount_msat'])
|
||||
self._label = tx['label']
|
||||
self._date = format_time(tx['timestamp'])
|
||||
self._timestamp = tx['timestamp']
|
||||
self._fee.msatsInt = 0 if not tx.fee_msat else int(tx.fee_msat)
|
||||
self._amount.msatsInt = int(tx.amount_msat)
|
||||
self._label = tx.label
|
||||
self._date = format_time(tx.timestamp)
|
||||
self._timestamp = tx.timestamp
|
||||
self._status = 'settled' # TODO: other states? get_lightning_history is deciding the filter for us :(
|
||||
self._phash = tx['payment_hash']
|
||||
self._preimage = tx['preimage']
|
||||
self._phash = tx.payment_hash
|
||||
self._preimage = tx.preimage
|
||||
|
||||
self.detailsChanged.emit()
|
||||
|
||||
@@ -127,15 +127,14 @@ class QETransactionListModel(QAbstractListModel, QtEventListener):
|
||||
#self._logger.debug(str(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:
|
||||
item['lightning'] = False
|
||||
|
||||
if item['lightning']:
|
||||
item['value'] = QEAmount(amount_sat=item['value'].value, amount_msat=item['amount_msat'])
|
||||
if item['type'] == 'payment':
|
||||
item['incoming'] = True if item['direction'] == 'received' else False
|
||||
item['incoming'] = True if item['amount_msat'] > 0 else False
|
||||
item['confirmations'] = 0
|
||||
else:
|
||||
item['value'] = QEAmount(amount_sat=item['value'].value)
|
||||
|
||||
@@ -330,20 +330,13 @@ class QETxDetails(QObject, QtEventListener):
|
||||
self._sighash_danger = self._wallet.wallet.check_sighash(self._tx)
|
||||
|
||||
if self._wallet.wallet.lnworker:
|
||||
# Calling lnworker.get_onchain_history and wallet.get_full_history here
|
||||
# is inefficient. We should probably pass the tx_item to the constructor.
|
||||
lnworker_history = self._wallet.wallet.lnworker.get_onchain_history()
|
||||
if self._txid in lnworker_history:
|
||||
item = lnworker_history[self._txid]
|
||||
group_id = item.get('group_id')
|
||||
if group_id:
|
||||
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
|
||||
# Calling wallet.get_full_history here is inefficient.
|
||||
# We should probably pass the tx_item to the constructor.
|
||||
full_history = self._wallet.wallet.get_full_history()
|
||||
item = full_history.get('group:' + self._txid)
|
||||
self._lnamount.satsInt = int(item['ln_value'].value) if item else 0
|
||||
else:
|
||||
self._lnamount.satsInt = 0
|
||||
|
||||
self._is_complete = self._tx.is_complete()
|
||||
self._is_rbf_enabled = self._tx.is_rbf_enabled()
|
||||
|
||||
@@ -601,11 +601,10 @@ class HistoryList(MyTreeView, AcceptFileDragDrop):
|
||||
self.main_window.show_message(_("Enable fiat exchange rate with history."))
|
||||
return
|
||||
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,
|
||||
to_timestamp=time.mktime(self.end_date.timetuple()) if self.end_date else None,
|
||||
fx=fx)
|
||||
summary = h['summary']
|
||||
if not summary:
|
||||
self.main_window.show_message(_("Nothing to summarize."))
|
||||
return
|
||||
@@ -738,7 +737,7 @@ class HistoryList(MyTreeView, AcceptFileDragDrop):
|
||||
# can happen e.g. before list is populated for the first time
|
||||
return
|
||||
tx_item = idx.internalPointer().get_data()
|
||||
if tx_item.get('lightning') and tx_item['type'] == 'payment':
|
||||
if tx_item.get('lightning'):
|
||||
menu = QMenu()
|
||||
menu.addAction(_("Details"), lambda: self.main_window.show_lightning_transaction(tx_item))
|
||||
cc = self.add_copy_menu(menu, idx)
|
||||
|
||||
@@ -45,7 +45,6 @@ class LightningTxDialog(WindowModalDialog):
|
||||
WindowModalDialog.__init__(self, parent, _("Lightning Payment"))
|
||||
self.main_window = parent
|
||||
self.config = parent.config
|
||||
self.is_sent = tx_item['direction'] == PaymentDirection.SENT
|
||||
self.label = tx_item['label']
|
||||
self.timestamp = tx_item['timestamp']
|
||||
self.amount = Decimal(tx_item['amount_msat']) / 1000
|
||||
@@ -61,8 +60,8 @@ class LightningTxDialog(WindowModalDialog):
|
||||
self.setLayout(vbox)
|
||||
amount_str = self.main_window.format_amount_and_units(self.amount, timestamp=self.timestamp)
|
||||
vbox.addWidget(QLabel(_("Amount") + f": {amount_str}"))
|
||||
if self.is_sent:
|
||||
fee_msat = tx_item['fee_msat']
|
||||
fee_msat = tx_item.get('fee_msat')
|
||||
if fee_msat is not 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)
|
||||
vbox.addWidget(QLabel(_("Fee: {}").format(fee_str)))
|
||||
|
||||
@@ -808,14 +808,15 @@ class TxDialog(QDialog, MessageBoxMixin):
|
||||
if txid is not None and fx.is_enabled() and amount is not None:
|
||||
tx_item_fiat = self.wallet.get_tx_item_fiat(
|
||||
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:
|
||||
item = lnworker_history[txid]
|
||||
ln_amount = item['amount_msat'] / 1000
|
||||
if amount is None:
|
||||
tx_mined_status = self.wallet.adb.get_tx_height(txid)
|
||||
|
||||
if self.wallet.lnworker:
|
||||
# if it is a group, collect ln amount
|
||||
full_history = self.wallet.get_full_history()
|
||||
item = full_history.get('group:' + txid)
|
||||
ln_amount = item['ln_value'].value if item else None
|
||||
else:
|
||||
ln_amount = None
|
||||
|
||||
self.broadcast_button.setEnabled(tx_details.can_broadcast)
|
||||
can_sign = not self.tx.is_complete() and \
|
||||
(self.wallet.can_sign(self.tx) or bool(self.external_keypairs))
|
||||
|
||||
@@ -36,7 +36,7 @@ from .util import (
|
||||
profiler, OldTaskGroup, ESocksProxy, NetworkRetryManager, JsonRPCClient, NotEnoughFunds, EventListener,
|
||||
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,
|
||||
UnrelatedTransactionException
|
||||
UnrelatedTransactionException, LightningHistoryItem
|
||||
)
|
||||
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
|
||||
@@ -48,7 +48,6 @@ from .transaction import (
|
||||
from .crypto import (
|
||||
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 .lnpeer import Peer, LN_P2P_NETWORK_TIMEOUT
|
||||
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])
|
||||
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 = {}
|
||||
for payment_hash, plist in self.get_payments(status='settled').items():
|
||||
if len(plist) == 0:
|
||||
@@ -995,94 +998,94 @@ class LNWallet(LNWorker):
|
||||
if not label and direction == PaymentDirection.FORWARDING:
|
||||
label = _('Forwarding')
|
||||
preimage = self.get_preimage(payment_hash).hex()
|
||||
item = {
|
||||
'type': 'payment',
|
||||
'label': label,
|
||||
'timestamp': timestamp or 0,
|
||||
'date': timestamp_to_datetime(timestamp),
|
||||
'direction': direction,
|
||||
'amount_msat': amount_msat,
|
||||
'fee_msat': fee_msat,
|
||||
'payment_hash': key,
|
||||
'preimage': preimage,
|
||||
}
|
||||
item['group_id'] = self.swap_manager.get_group_id_for_payment_hash(payment_hash)
|
||||
group_id = self.swap_manager.get_group_id_for_payment_hash(payment_hash)
|
||||
item = LightningHistoryItem(
|
||||
type = 'payment',
|
||||
payment_hash = payment_hash.hex(),
|
||||
preimage = preimage,
|
||||
amount_msat = amount_msat,
|
||||
fee_msat = fee_msat,
|
||||
group_id = group_id,
|
||||
timestamp = timestamp or 0,
|
||||
label=label,
|
||||
)
|
||||
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
|
||||
|
||||
def get_onchain_history(self):
|
||||
out = {}
|
||||
def get_groups_for_onchain_history(self) -> Dict[str, str]:
|
||||
"""
|
||||
returns dict: txid -> group_id
|
||||
side effect: sets default labels
|
||||
"""
|
||||
groups = {}
|
||||
# add funding events
|
||||
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
|
||||
tx_height = self.wallet.adb.get_tx_height(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
|
||||
groups[funding_txid] = funding_txid
|
||||
item = chan.get_closing_height()
|
||||
if item is None:
|
||||
continue
|
||||
closing_txid, closing_height, closing_timestamp = item
|
||||
tx_height = self.wallet.adb.get_tx_height(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
|
||||
groups[closing_txid] = closing_txid
|
||||
|
||||
d = self.swap_manager.get_groups_for_onchain_history()
|
||||
for k,v in d.items():
|
||||
group_id = v.get('group_id')
|
||||
group_label = v.get('group_label')
|
||||
if group_id and group_label:
|
||||
self.wallet.set_default_label(group_id, group_label)
|
||||
out.update(d)
|
||||
return out
|
||||
for txid, v in d.items():
|
||||
group_id = v['group_id']
|
||||
label = v.get('label')
|
||||
group_label = v.get('group_label') or label
|
||||
groups[txid] = group_id
|
||||
if label:
|
||||
self.wallet.set_default_label(txid, label)
|
||||
if group_label:
|
||||
self.wallet.set_group_label(group_id, group_label)
|
||||
|
||||
def get_history(self):
|
||||
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
|
||||
return groups
|
||||
|
||||
def channel_peers(self) -> List[bytes]:
|
||||
node_ids = [chan.node_id for chan in self.channels.values() if not chan.is_closed()]
|
||||
|
||||
@@ -1234,8 +1234,6 @@ class SwapManager(Logger):
|
||||
label += f' (refundable in {-delta} blocks)' # fixme: only if unspent
|
||||
d[txid] = {
|
||||
'group_id': txid,
|
||||
'amount_msat': 0, # must be zero for onchain tx
|
||||
'type': 'swap',
|
||||
'label': label,
|
||||
'group_label': group_label,
|
||||
}
|
||||
@@ -1244,8 +1242,7 @@ class SwapManager(Logger):
|
||||
# to the group (see wallet.get_full_history)
|
||||
d[swap.spending_txid] = {
|
||||
'group_id': txid,
|
||||
'amount_msat': 0, # must be zero for onchain tx
|
||||
'type': 'swap',
|
||||
'group_label': group_label,
|
||||
'label': _('Refund transaction'),
|
||||
}
|
||||
return d
|
||||
@@ -1254,10 +1251,7 @@ class SwapManager(Logger):
|
||||
# add group_id to swap transactions
|
||||
swap = self.get_swap(payment_hash)
|
||||
if swap:
|
||||
if swap.is_reverse:
|
||||
return swap.spending_txid
|
||||
else:
|
||||
return swap.funding_txid
|
||||
return swap.spending_txid if swap.is_reverse else swap.funding_txid
|
||||
|
||||
|
||||
class SwapServerTransport(Logger):
|
||||
|
||||
@@ -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 = int.from_bytes(digest, 'big')
|
||||
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),
|
||||
}
|
||||
|
||||
@@ -88,6 +88,7 @@ from .util import read_json_file, write_json_file, UserFacingException, FileImpo
|
||||
from .util import EventListener, event_listener
|
||||
from . import descriptor
|
||||
from .descriptor import Descriptor
|
||||
from .util import OnchainHistoryItem, LightningHistoryItem
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .network import Network
|
||||
@@ -1012,11 +1013,11 @@ class Abstract_Wallet(ABC, Logger, EventListener):
|
||||
"""
|
||||
with self.lock, self.transaction_lock:
|
||||
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)
|
||||
# todo: get_full_history should return unconfirmed tx topologically sorted
|
||||
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)
|
||||
|
||||
result = self._tx_parents_cache.get(txid, None)
|
||||
@@ -1145,28 +1146,53 @@ class Abstract_Wallet(ABC, Logger, EventListener):
|
||||
# return last 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:
|
||||
domain = self.get_addresses()
|
||||
|
||||
now = time.time()
|
||||
transactions = OrderedDictWithIndex()
|
||||
monotonic_timestamp = 0
|
||||
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))
|
||||
d = {
|
||||
'txid': hist_item.txid,
|
||||
'fee_sat': hist_item.fee,
|
||||
'height': hist_item.tx_mined_status.height,
|
||||
'confirmations': hist_item.tx_mined_status.conf,
|
||||
'timestamp': hist_item.tx_mined_status.timestamp,
|
||||
'monotonic_timestamp': monotonic_timestamp,
|
||||
'incoming': True if hist_item.delta>0 else False,
|
||||
'bc_value': Satoshis(hist_item.delta),
|
||||
'bc_balance': Satoshis(hist_item.balance),
|
||||
'date': timestamp_to_datetime(hist_item.tx_mined_status.timestamp),
|
||||
'label': self.get_label_for_txid(hist_item.txid),
|
||||
'txpos_in_block': hist_item.tx_mined_status.txpos,
|
||||
'wanted_height': hist_item.tx_mined_status.wanted_height,
|
||||
}
|
||||
yield d
|
||||
timestamp = (hist_item.tx_mined_status.timestamp or TX_TIMESTAMP_INF)
|
||||
height = hist_item.tx_mined_status
|
||||
if from_timestamp and (timestamp or now) < from_timestamp:
|
||||
continue
|
||||
if to_timestamp and (timestamp or now) >= to_timestamp:
|
||||
continue
|
||||
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
|
||||
monotonic_timestamp = max(monotonic_timestamp, timestamp)
|
||||
txid = hist_item.txid
|
||||
group_id = groups.get(txid)
|
||||
label = self.get_label_for_txid(txid)
|
||||
tx_item = OnchainHistoryItem(
|
||||
txid=hist_item.txid,
|
||||
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:
|
||||
height = self.adb.get_local_height()
|
||||
@@ -1342,44 +1368,26 @@ class Abstract_Wallet(ABC, Logger, EventListener):
|
||||
return is_paid, conf_needed
|
||||
|
||||
@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()
|
||||
# add on-chain txns
|
||||
onchain_history = self.get_onchain_history(domain=onchain_domain)
|
||||
for tx_item in onchain_history:
|
||||
txid = tx_item['txid']
|
||||
transactions_tmp[txid] = tx_item
|
||||
# add lnworker onchain transactions to transactions_tmp
|
||||
# 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)
|
||||
for tx_item in onchain_history.values():
|
||||
txid = tx_item.txid
|
||||
transactions_tmp[txid] = tx_item.to_dict()
|
||||
transactions_tmp[txid]['lightning'] = False
|
||||
|
||||
# add lightning_transactions
|
||||
lightning_history = self.lnworker.get_lightning_history() if self.lnworker and include_lightning else {}
|
||||
for tx_item in lightning_history.values():
|
||||
txid = tx_item.get('txid')
|
||||
ln_value = Decimal(tx_item['amount_msat']) / 1000
|
||||
tx_item['lightning'] = True
|
||||
tx_item['ln_value'] = Satoshis(ln_value)
|
||||
key = tx_item.get('txid') or tx_item['payment_hash']
|
||||
transactions_tmp[key] = tx_item
|
||||
key = tx_item.payment_hash or 'ln:' + tx_item.group_id
|
||||
transactions_tmp[key] = tx_item.to_dict()
|
||||
transactions_tmp[key]['lightning'] = True
|
||||
|
||||
# sort on-chain and LN stuff into new dict, by timestamp
|
||||
# (we rely on this being a *stable* sort)
|
||||
def sort_key(x):
|
||||
@@ -1396,10 +1404,10 @@ class Abstract_Wallet(ABC, Logger, EventListener):
|
||||
else:
|
||||
key = 'group:' + group_id
|
||||
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:
|
||||
parent = {
|
||||
'label': label,
|
||||
'label': group_label,
|
||||
'fiat_value': Fiat(Decimal(0), fx.ccy) if fx else None,
|
||||
'bc_value': Satoshis(0),
|
||||
'ln_value': Satoshis(0),
|
||||
@@ -1455,20 +1463,12 @@ class Abstract_Wallet(ABC, Logger, EventListener):
|
||||
return transactions
|
||||
|
||||
@profiler
|
||||
def get_detailed_history(
|
||||
self,
|
||||
from_timestamp=None,
|
||||
to_timestamp=None,
|
||||
fx=None,
|
||||
show_addresses=False,
|
||||
from_height=None,
|
||||
to_height=None):
|
||||
def get_onchain_capital_gains(self, fx, **kwargs):
|
||||
# History with capital gains, using utxo pricing
|
||||
# FIXME: Lightning capital gains would requires FIFO
|
||||
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')
|
||||
|
||||
from_timestamp = kwargs.get('from_timestamp')
|
||||
to_timestamp = kwargs.get('to_timestamp')
|
||||
history = self.get_onchain_history(**kwargs)
|
||||
show_fiat = fx and fx.is_enabled() and fx.has_history()
|
||||
out = []
|
||||
income = 0
|
||||
@@ -1476,26 +1476,13 @@ class Abstract_Wallet(ABC, Logger, EventListener):
|
||||
capital_gains = Decimal(0)
|
||||
fiat_income = Decimal(0)
|
||||
fiat_expenditures = Decimal(0)
|
||||
now = time.time()
|
||||
for item in self.get_onchain_history():
|
||||
for txid, hitem in history.items():
|
||||
item = hitem.to_dict()
|
||||
if item['bc_value'].value == 0:
|
||||
continue
|
||||
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 = self.db.get_transaction(tx_hash)
|
||||
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
|
||||
value = item['bc_value'].value
|
||||
if value < 0:
|
||||
@@ -1506,7 +1493,6 @@ class Abstract_Wallet(ABC, Logger, EventListener):
|
||||
if show_fiat:
|
||||
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
|
||||
item.update(fiat_fields)
|
||||
if value < 0:
|
||||
capital_gains += fiat_fields['capital_gain'].value
|
||||
fiat_expenditures += -fiat_value
|
||||
@@ -1517,12 +1503,8 @@ class Abstract_Wallet(ABC, Logger, EventListener):
|
||||
if out:
|
||||
first_item = out[0]
|
||||
last_item = out[-1]
|
||||
if from_height or to_height:
|
||||
start_height = from_height
|
||||
end_height = to_height
|
||||
else:
|
||||
start_height = first_item['height'] - 1
|
||||
end_height = last_item['height']
|
||||
start_height = first_item['height'] - 1
|
||||
end_height = last_item['height']
|
||||
|
||||
b = first_item['bc_balance'].value
|
||||
v = first_item['bc_value'].value
|
||||
@@ -1583,10 +1565,7 @@ class Abstract_Wallet(ABC, Logger, EventListener):
|
||||
|
||||
else:
|
||||
summary = {}
|
||||
return {
|
||||
'transactions': out,
|
||||
'summary': summary
|
||||
}
|
||||
return summary
|
||||
|
||||
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))
|
||||
@@ -1644,6 +1623,12 @@ class Abstract_Wallet(ABC, Logger, EventListener):
|
||||
def _get_default_label_for_outpoint(self, outpoint: str) -> str:
|
||||
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:
|
||||
return self._labels.get(tx_hash) or self._get_default_label_for_txid(tx_hash)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user