Merge pull request #10131 from SomberNight/202508_commands_onchain_history
commands: add back from_height/to_height params to onchain_history
This commit is contained in:
@@ -36,7 +36,7 @@ import inspect
|
|||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
from functools import wraps
|
from functools import wraps
|
||||||
from decimal import Decimal, InvalidOperation
|
from decimal import Decimal, InvalidOperation
|
||||||
from typing import Optional, TYPE_CHECKING, Dict, List
|
from typing import Optional, TYPE_CHECKING, Dict, List, Any
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
|
|
||||||
@@ -1011,7 +1011,7 @@ class Commands(Logger):
|
|||||||
await self.addtransaction(result, wallet=wallet)
|
await self.addtransaction(result, wallet=wallet)
|
||||||
return result
|
return result
|
||||||
|
|
||||||
def get_year_timestamps(self, year:int):
|
def get_year_timestamps(self, year: int) -> dict[str, Any]:
|
||||||
kwargs = {}
|
kwargs = {}
|
||||||
if year:
|
if year:
|
||||||
start_date = datetime.datetime(year, 1, 1)
|
start_date = datetime.datetime(year, 1, 1)
|
||||||
@@ -1071,21 +1071,26 @@ class Commands(Logger):
|
|||||||
return new_tx.serialize()
|
return new_tx.serialize()
|
||||||
|
|
||||||
@command('w')
|
@command('w')
|
||||||
async def onchain_history(self, show_fiat=False, year=None, show_addresses=False, wallet: Abstract_Wallet = None):
|
async def onchain_history(
|
||||||
|
self, show_fiat=False, year=None, show_addresses=False,
|
||||||
|
from_height=None, to_height=None,
|
||||||
|
wallet: Abstract_Wallet = None,
|
||||||
|
):
|
||||||
"""Wallet onchain history. Returns the transaction history of your wallet.
|
"""Wallet onchain history. Returns the transaction history of your wallet.
|
||||||
|
|
||||||
arg:bool:show_addresses:Show input and output addresses
|
arg:bool:show_addresses:Show input and output addresses
|
||||||
arg:bool:show_fiat:Show fiat value of transactions
|
arg:bool:show_fiat:Show fiat value of transactions
|
||||||
arg:bool:show_fees:Show miner fees paid by transactions
|
|
||||||
arg:int:year:Show history for a given year
|
arg:int:year:Show history for a given year
|
||||||
|
arg:int:from_height:Only show transactions that confirmed after(inclusive) given block height
|
||||||
|
arg:int:to_height:Only show transactions that confirmed before(exclusive) given block height
|
||||||
"""
|
"""
|
||||||
# trigger lnwatcher callbacks for their side effects: setting labels and accounting_addresses
|
# trigger lnwatcher callbacks for their side effects: setting labels and accounting_addresses
|
||||||
if not self.network and wallet.lnworker:
|
if not self.network and wallet.lnworker:
|
||||||
await wallet.lnworker.lnwatcher.trigger_callbacks(requires_synchronizer=False)
|
await wallet.lnworker.lnwatcher.trigger_callbacks(requires_synchronizer=False)
|
||||||
|
|
||||||
#'from_height': (None, "Only show transactions that confirmed after given block height"),
|
|
||||||
#'to_height': (None, "Only show transactions that confirmed before given block height"),
|
|
||||||
kwargs = self.get_year_timestamps(year)
|
kwargs = self.get_year_timestamps(year)
|
||||||
|
kwargs['from_height'] = from_height
|
||||||
|
kwargs['to_height'] = to_height
|
||||||
onchain_history = wallet.get_onchain_history(**kwargs)
|
onchain_history = wallet.get_onchain_history(**kwargs)
|
||||||
out = [x.to_dict() for x in onchain_history.values()]
|
out = [x.to_dict() for x in onchain_history.values()]
|
||||||
if show_fiat:
|
if show_fiat:
|
||||||
|
|||||||
@@ -325,7 +325,8 @@ class MyEncoder(json.JSONEncoder):
|
|||||||
if isinstance(obj, Decimal):
|
if isinstance(obj, Decimal):
|
||||||
return str(obj)
|
return str(obj)
|
||||||
if isinstance(obj, datetime):
|
if isinstance(obj, datetime):
|
||||||
return obj.isoformat(' ')[:-3]
|
# note: if there is a timezone specified, this will include the offset
|
||||||
|
return obj.isoformat(' ', timespec="minutes")
|
||||||
if isinstance(obj, set):
|
if isinstance(obj, set):
|
||||||
return list(obj)
|
return list(obj)
|
||||||
if isinstance(obj, bytes): # for nametuples in lnchannel
|
if isinstance(obj, bytes): # for nametuples in lnchannel
|
||||||
@@ -893,10 +894,11 @@ def quantize_feerate(fee) -> Union[None, Decimal, int]:
|
|||||||
return Decimal(fee).quantize(_feerate_quanta, rounding=decimal.ROUND_HALF_DOWN)
|
return Decimal(fee).quantize(_feerate_quanta, rounding=decimal.ROUND_HALF_DOWN)
|
||||||
|
|
||||||
|
|
||||||
|
DEFAULT_TIMEZONE = None # type: timezone | None # None means local OS timezone
|
||||||
def timestamp_to_datetime(timestamp: Union[int, float, None], *, utc: bool = False) -> Optional[datetime]:
|
def timestamp_to_datetime(timestamp: Union[int, float, None], *, utc: bool = False) -> Optional[datetime]:
|
||||||
if timestamp is None:
|
if timestamp is None:
|
||||||
return None
|
return None
|
||||||
tz = None
|
tz = DEFAULT_TIMEZONE
|
||||||
if utc:
|
if utc:
|
||||||
tz = timezone.utc
|
tz = timezone.utc
|
||||||
return datetime.fromtimestamp(timestamp, tz=tz)
|
return datetime.fromtimestamp(timestamp, tz=tz)
|
||||||
|
|||||||
@@ -1202,8 +1202,8 @@ class Abstract_Wallet(ABC, Logger, EventListener):
|
|||||||
domain=None,
|
domain=None,
|
||||||
from_timestamp=None,
|
from_timestamp=None,
|
||||||
to_timestamp=None,
|
to_timestamp=None,
|
||||||
from_height=None,
|
from_height=None, # [from_height, to_height[
|
||||||
to_height=None
|
to_height=None,
|
||||||
) -> Dict[str, OnchainHistoryItem]:
|
) -> Dict[str, OnchainHistoryItem]:
|
||||||
# sanity check
|
# sanity check
|
||||||
if (from_timestamp is not None or to_timestamp is not None) \
|
if (from_timestamp is not None or to_timestamp is not None) \
|
||||||
@@ -1220,7 +1220,7 @@ class Abstract_Wallet(ABC, Logger, EventListener):
|
|||||||
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):
|
||||||
timestamp = (hist_item.tx_mined_status.timestamp or TX_TIMESTAMP_INF)
|
timestamp = (hist_item.tx_mined_status.timestamp or TX_TIMESTAMP_INF)
|
||||||
height = hist_item.tx_mined_status
|
height = hist_item.tx_mined_status.height
|
||||||
if from_timestamp and (timestamp or now) < from_timestamp:
|
if from_timestamp and (timestamp or now) < from_timestamp:
|
||||||
continue
|
continue
|
||||||
if to_timestamp and (timestamp or now) >= to_timestamp:
|
if to_timestamp and (timestamp or now) >= to_timestamp:
|
||||||
|
|||||||
3002
tests/fiat_fx_data/BitFinex_EUR
Normal file
3002
tests/fiat_fx_data/BitFinex_EUR
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,9 +1,13 @@
|
|||||||
import binascii
|
import binascii
|
||||||
|
import datetime
|
||||||
|
import os.path
|
||||||
import unittest
|
import unittest
|
||||||
from unittest import mock
|
from unittest import mock
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
from os import urandom
|
from os import urandom
|
||||||
|
import shutil
|
||||||
|
|
||||||
|
import electrum
|
||||||
from electrum.commands import Commands, eval_bool
|
from electrum.commands import Commands, eval_bool
|
||||||
from electrum import storage, wallet
|
from electrum import storage, wallet
|
||||||
from electrum.lnworker import RecvMPPResolution
|
from electrum.lnworker import RecvMPPResolution
|
||||||
@@ -14,6 +18,8 @@ from electrum.transaction import Transaction, TxOutput, tx_from_any
|
|||||||
from electrum.util import UserFacingException, NotEnoughFunds
|
from electrum.util import UserFacingException, NotEnoughFunds
|
||||||
from electrum.crypto import sha256
|
from electrum.crypto import sha256
|
||||||
from electrum.lnaddr import lndecode
|
from electrum.lnaddr import lndecode
|
||||||
|
from electrum.daemon import Daemon
|
||||||
|
from electrum import json_db
|
||||||
|
|
||||||
from . import ElectrumTestCase
|
from . import ElectrumTestCase
|
||||||
from . import restore_wallet_from_text__for_unittest
|
from . import restore_wallet_from_text__for_unittest
|
||||||
@@ -171,6 +177,26 @@ class TestCommandsTestnet(ElectrumTestCase):
|
|||||||
def setUp(self):
|
def setUp(self):
|
||||||
super().setUp()
|
super().setUp()
|
||||||
self.config = SimpleConfig({'electrum_path': self.electrum_path})
|
self.config = SimpleConfig({'electrum_path': self.electrum_path})
|
||||||
|
self.config.NETWORK_OFFLINE = True
|
||||||
|
shutil.copytree(os.path.join(os.path.dirname(__file__), "fiat_fx_data"), os.path.join(self.electrum_path, "cache"))
|
||||||
|
self.config.FX_EXCHANGE = "BitFinex"
|
||||||
|
self.config.FX_CURRENCY = "EUR"
|
||||||
|
self._default_default_timezone = electrum.util.DEFAULT_TIMEZONE
|
||||||
|
electrum.util.DEFAULT_TIMEZONE = datetime.timezone.utc
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
electrum.util.DEFAULT_TIMEZONE = self._default_default_timezone
|
||||||
|
super().tearDown()
|
||||||
|
|
||||||
|
async def asyncSetUp(self):
|
||||||
|
await super().asyncSetUp()
|
||||||
|
self.daemon = Daemon(config=self.config, listen_jsonrpc=False)
|
||||||
|
assert self.daemon.network is None
|
||||||
|
|
||||||
|
async def asyncTearDown(self):
|
||||||
|
with mock.patch.object(wallet.Abstract_Wallet, 'save_db'):
|
||||||
|
await self.daemon.stop()
|
||||||
|
await super().asyncTearDown()
|
||||||
|
|
||||||
async def test_convert_xkey(self):
|
async def test_convert_xkey(self):
|
||||||
cmds = Commands(config=self.config)
|
cmds = Commands(config=self.config)
|
||||||
@@ -533,3 +559,84 @@ class TestCommandsTestnet(ElectrumTestCase):
|
|||||||
with self.assertRaises(AssertionError):
|
with self.assertRaises(AssertionError):
|
||||||
# cancelling a settled invoice should raise
|
# cancelling a settled invoice should raise
|
||||||
await cmds.cancel_hold_invoice(payment_hash=payment_hash, wallet=wallet)
|
await cmds.cancel_hold_invoice(payment_hash=payment_hash, wallet=wallet)
|
||||||
|
|
||||||
|
@mock.patch.object(storage.WalletStorage, 'write')
|
||||||
|
@mock.patch.object(storage.WalletStorage, 'append')
|
||||||
|
async def test_onchain_history(self, *mock_args):
|
||||||
|
cmds = Commands(config=self.config, daemon=self.daemon)
|
||||||
|
WALLET_FILES_DIR = os.path.join(os.path.dirname(__file__), "test_storage_upgrade")
|
||||||
|
wallet_path = os.path.join(WALLET_FILES_DIR, "client_3_3_8_xpub_with_realistic_history")
|
||||||
|
await cmds.load_wallet(wallet_path=wallet_path)
|
||||||
|
|
||||||
|
expected_last_history_item = {
|
||||||
|
"amount_sat": -500200,
|
||||||
|
"bc_balance": "0.75136687",
|
||||||
|
"bc_value": "-0.005002",
|
||||||
|
"confirmations": 968,
|
||||||
|
"date": "2020-07-02 11:57+00:00", # kind of a hack. normally, there is no timezone offset here
|
||||||
|
"fee_sat": 200,
|
||||||
|
"group_id": None,
|
||||||
|
"height": 1774910,
|
||||||
|
"incoming": False,
|
||||||
|
"label": "",
|
||||||
|
"monotonic_timestamp": 1593691025,
|
||||||
|
"timestamp": 1593691025,
|
||||||
|
"txid": "6db8ee1bf57bb6ff1c4447749079ba1bd5e47a948bf5700b114b37af3437b5fc",
|
||||||
|
"txpos_in_block": 44,
|
||||||
|
"wanted_height": None,
|
||||||
|
}
|
||||||
|
|
||||||
|
hist = await cmds.onchain_history(wallet_path=wallet_path)
|
||||||
|
self.assertEqual(len(hist), 89)
|
||||||
|
self.assertEqual(hist[-1], expected_last_history_item)
|
||||||
|
|
||||||
|
with self.subTest(msg="'show_addresses' param"):
|
||||||
|
hist = await cmds.onchain_history(wallet_path=wallet_path, show_addresses=True)
|
||||||
|
self.assertEqual(len(hist), 89)
|
||||||
|
self.assertEqual(
|
||||||
|
hist[-1],
|
||||||
|
expected_last_history_item | {
|
||||||
|
'inputs': [
|
||||||
|
{
|
||||||
|
'coinbase': False,
|
||||||
|
'nsequence': 4294967293,
|
||||||
|
'prevout_hash': 'd42f6de015d93e6cd573ec8ae5ef6f87c4deb3763b0310e006d26c30d8800c67',
|
||||||
|
'prevout_n': 0,
|
||||||
|
'scriptSig': '',
|
||||||
|
'witness': [
|
||||||
|
'3044022056e0a02c45b5e4f93dc533c7f3fa95296684b0f41019ae91b5b7b083a5b651c202200a0e0c56bdfa299f4af8c604d359033863c9ce0a7fdd35acfbda5cff4a6ffa3301',
|
||||||
|
'02eba8ba71542a884f2eec1f40594192be2628268f9fa141c9b12b026008dbb274'
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
'outputs': [
|
||||||
|
{'address': 'tb1qr5mf6sumdlhjrq9t6wlyvdm960zu0n0t5d60ug', 'value_sat': 500000},
|
||||||
|
{'address': 'tb1qp3p2d72gj2l7r6za056tgu4ezsurjphper4swh', 'value_sat': 762100}
|
||||||
|
],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
with self.subTest(msg="'from_height' / 'to_height' params"):
|
||||||
|
hist = await cmds.onchain_history(wallet_path=wallet_path, from_height=1638866, to_height=1665815)
|
||||||
|
self.assertEqual(len(hist), 8)
|
||||||
|
with self.subTest(msg="'year' param"):
|
||||||
|
hist = await cmds.onchain_history(wallet_path=wallet_path, year=2019)
|
||||||
|
self.assertEqual(len(hist), 23)
|
||||||
|
with self.subTest(msg="timestamp and block height based filtering cannot be used together"):
|
||||||
|
with self.assertRaises(UserFacingException):
|
||||||
|
hist = await cmds.onchain_history(wallet_path=wallet_path, year=2019, from_height=1638866, to_height=1665815)
|
||||||
|
with self.subTest(msg="'show_fiat' param"):
|
||||||
|
self.config.FX_USE_EXCHANGE_RATE = True
|
||||||
|
hist = await cmds.onchain_history(wallet_path=wallet_path, show_fiat=True)
|
||||||
|
self.assertEqual(len(hist), 89)
|
||||||
|
self.assertEqual(
|
||||||
|
hist[-1],
|
||||||
|
expected_last_history_item | {
|
||||||
|
"acquisition_price": "41.67",
|
||||||
|
"capital_gain": "-1.16",
|
||||||
|
"fiat_currency": "EUR",
|
||||||
|
"fiat_default": True,
|
||||||
|
"fiat_fee": "0.02",
|
||||||
|
"fiat_rate": "8097.91",
|
||||||
|
"fiat_value": "-40.51",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user