mirror of
https://github.com/LBRYFoundation/LBRY-Vault.git
synced 2025-09-03 02:35:20 +00:00
qt history view custom fiat input fixes
previously, when you submitted a fiat value with thousands separator, it would be discarded.
This commit is contained in:
parent
a53dded50f
commit
37b009a342
5 changed files with 133 additions and 47 deletions
|
@ -464,9 +464,13 @@ class FxThread(ThreadJob):
|
||||||
d = get_exchanges_by_ccy(history)
|
d = get_exchanges_by_ccy(history)
|
||||||
return d.get(ccy, [])
|
return d.get(ccy, [])
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def remove_thousands_separator(text):
|
||||||
|
return text.replace(',', '') # FIXME use THOUSAND_SEPARATOR in util
|
||||||
|
|
||||||
def ccy_amount_str(self, amount, commas):
|
def ccy_amount_str(self, amount, commas):
|
||||||
prec = CCY_PRECISIONS.get(self.ccy, 2)
|
prec = CCY_PRECISIONS.get(self.ccy, 2)
|
||||||
fmt_str = "{:%s.%df}" % ("," if commas else "", max(0, prec))
|
fmt_str = "{:%s.%df}" % ("," if commas else "", max(0, prec)) # FIXME use util.THOUSAND_SEPARATOR and util.DECIMAL_POINT
|
||||||
try:
|
try:
|
||||||
rounded_amount = round(amount, prec)
|
rounded_amount = round(amount, prec)
|
||||||
except decimal.InvalidOperation:
|
except decimal.InvalidOperation:
|
||||||
|
|
|
@ -275,10 +275,11 @@ class HistoryList(MyTreeWidget, AcceptFileDragDrop):
|
||||||
if value and value < 0:
|
if value and value < 0:
|
||||||
item.setForeground(3, red_brush)
|
item.setForeground(3, red_brush)
|
||||||
item.setForeground(4, red_brush)
|
item.setForeground(4, red_brush)
|
||||||
if fiat_value and not tx_item['fiat_default']:
|
if fiat_value is not None and not tx_item['fiat_default']:
|
||||||
item.setForeground(6, blue_brush)
|
item.setForeground(6, blue_brush)
|
||||||
if tx_hash:
|
if tx_hash:
|
||||||
item.setData(0, Qt.UserRole, tx_hash)
|
item.setData(0, Qt.UserRole, tx_hash)
|
||||||
|
item.setData(0, Qt.UserRole+1, value)
|
||||||
self.insertTopLevelItem(0, item)
|
self.insertTopLevelItem(0, item)
|
||||||
if current_tx == tx_hash:
|
if current_tx == tx_hash:
|
||||||
self.setCurrentItem(item)
|
self.setCurrentItem(item)
|
||||||
|
@ -286,6 +287,7 @@ class HistoryList(MyTreeWidget, AcceptFileDragDrop):
|
||||||
def on_edited(self, item, column, prior):
|
def on_edited(self, item, column, prior):
|
||||||
'''Called only when the text actually changes'''
|
'''Called only when the text actually changes'''
|
||||||
key = item.data(0, Qt.UserRole)
|
key = item.data(0, Qt.UserRole)
|
||||||
|
value = item.data(0, Qt.UserRole+1)
|
||||||
text = item.text(column)
|
text = item.text(column)
|
||||||
# fixme
|
# fixme
|
||||||
if column == 3:
|
if column == 3:
|
||||||
|
@ -293,7 +295,7 @@ class HistoryList(MyTreeWidget, AcceptFileDragDrop):
|
||||||
self.update_labels()
|
self.update_labels()
|
||||||
self.parent.update_completions()
|
self.parent.update_completions()
|
||||||
elif column == 6:
|
elif column == 6:
|
||||||
self.parent.wallet.set_fiat_value(key, self.parent.fx.ccy, text)
|
self.parent.wallet.set_fiat_value(key, self.parent.fx.ccy, text, self.parent.fx, value)
|
||||||
self.on_update()
|
self.on_update()
|
||||||
|
|
||||||
def on_doubleclick(self, item, column):
|
def on_doubleclick(self, item, column):
|
||||||
|
|
|
@ -3,9 +3,16 @@ import tempfile
|
||||||
import sys
|
import sys
|
||||||
import os
|
import os
|
||||||
import json
|
import json
|
||||||
|
from decimal import Decimal
|
||||||
|
from unittest import TestCase
|
||||||
|
import time
|
||||||
|
|
||||||
from io import StringIO
|
from io import StringIO
|
||||||
from electrum.storage import WalletStorage, FINAL_SEED_VERSION
|
from electrum.storage import WalletStorage, FINAL_SEED_VERSION
|
||||||
|
from electrum.wallet import Abstract_Wallet
|
||||||
|
from electrum.exchange_rate import ExchangeBase, FxThread
|
||||||
|
from electrum.util import TxMinedStatus
|
||||||
|
from electrum.bitcoin import COIN
|
||||||
|
|
||||||
from . import SequentialTestCase
|
from . import SequentialTestCase
|
||||||
|
|
||||||
|
@ -68,3 +75,67 @@ class TestWalletStorage(WalletTestCase):
|
||||||
with open(self.wallet_path, "r") as f:
|
with open(self.wallet_path, "r") as f:
|
||||||
contents = f.read()
|
contents = f.read()
|
||||||
self.assertEqual(some_dict, json.loads(contents))
|
self.assertEqual(some_dict, json.loads(contents))
|
||||||
|
|
||||||
|
class FakeExchange(ExchangeBase):
|
||||||
|
def __init__(self, rate):
|
||||||
|
super().__init__(lambda self: None, lambda self: None)
|
||||||
|
self.quotes = {'TEST': rate}
|
||||||
|
|
||||||
|
class FakeFxThread:
|
||||||
|
def __init__(self, exchange):
|
||||||
|
self.exchange = exchange
|
||||||
|
self.ccy = 'TEST'
|
||||||
|
|
||||||
|
remove_thousands_separator = staticmethod(FxThread.remove_thousands_separator)
|
||||||
|
timestamp_rate = FxThread.timestamp_rate
|
||||||
|
ccy_amount_str = FxThread.ccy_amount_str
|
||||||
|
history_rate = FxThread.history_rate
|
||||||
|
|
||||||
|
class FakeWallet:
|
||||||
|
def __init__(self, fiat_value):
|
||||||
|
super().__init__()
|
||||||
|
self.fiat_value = fiat_value
|
||||||
|
self.transactions = self.verified_tx = {'abc': 'Tx'}
|
||||||
|
|
||||||
|
def get_tx_height(self, txid):
|
||||||
|
# because we use a current timestamp, and history is empty,
|
||||||
|
# FxThread.history_rate will use spot prices
|
||||||
|
return TxMinedStatus(height=10, conf=10, timestamp=time.time(), header_hash='def')
|
||||||
|
|
||||||
|
default_fiat_value = Abstract_Wallet.default_fiat_value
|
||||||
|
price_at_timestamp = Abstract_Wallet.price_at_timestamp
|
||||||
|
class storage:
|
||||||
|
put = lambda self, x: None
|
||||||
|
|
||||||
|
txid = 'abc'
|
||||||
|
ccy = 'TEST'
|
||||||
|
|
||||||
|
class TestFiat(TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
self.value_sat = COIN
|
||||||
|
self.fiat_value = {}
|
||||||
|
self.wallet = FakeWallet(fiat_value=self.fiat_value)
|
||||||
|
self.fx = FakeFxThread(FakeExchange(Decimal('1000.001')))
|
||||||
|
default_fiat = Abstract_Wallet.default_fiat_value(self.wallet, txid, self.fx, self.value_sat)
|
||||||
|
self.assertEqual(Decimal('1000.001'), default_fiat)
|
||||||
|
self.assertEqual('1,000.00', self.fx.ccy_amount_str(default_fiat, commas=True))
|
||||||
|
|
||||||
|
def test_save_fiat_and_reset(self):
|
||||||
|
self.assertEqual(False, Abstract_Wallet.set_fiat_value(self.wallet, txid, ccy, '1000.01', self.fx, self.value_sat))
|
||||||
|
saved = self.fiat_value[ccy][txid]
|
||||||
|
self.assertEqual('1,000.01', self.fx.ccy_amount_str(Decimal(saved), commas=True))
|
||||||
|
self.assertEqual(True, Abstract_Wallet.set_fiat_value(self.wallet, txid, ccy, '', self.fx, self.value_sat))
|
||||||
|
self.assertNotIn(txid, self.fiat_value[ccy])
|
||||||
|
# even though we are not setting it to the exact fiat value according to the exchange rate, precision is truncated away
|
||||||
|
self.assertEqual(True, Abstract_Wallet.set_fiat_value(self.wallet, txid, ccy, '1,000.002', self.fx, self.value_sat))
|
||||||
|
|
||||||
|
def test_too_high_precision_value_resets_with_no_saved_value(self):
|
||||||
|
self.assertEqual(True, Abstract_Wallet.set_fiat_value(self.wallet, txid, ccy, '1,000.001', self.fx, self.value_sat))
|
||||||
|
|
||||||
|
def test_empty_resets(self):
|
||||||
|
self.assertEqual(True, Abstract_Wallet.set_fiat_value(self.wallet, txid, ccy, '', self.fx, self.value_sat))
|
||||||
|
self.assertNotIn(ccy, self.fiat_value)
|
||||||
|
|
||||||
|
def test_save_garbage(self):
|
||||||
|
self.assertEqual(False, Abstract_Wallet.set_fiat_value(self.wallet, txid, ccy, 'garbage', self.fx, self.value_sat))
|
||||||
|
self.assertNotIn(ccy, self.fiat_value)
|
||||||
|
|
|
@ -39,6 +39,7 @@ import urllib.request, urllib.parse, urllib.error
|
||||||
import builtins
|
import builtins
|
||||||
import json
|
import json
|
||||||
import time
|
import time
|
||||||
|
from typing import NamedTuple, Optional
|
||||||
|
|
||||||
import aiohttp
|
import aiohttp
|
||||||
from aiohttp_socks import SocksConnector, SocksVer
|
from aiohttp_socks import SocksConnector, SocksVer
|
||||||
|
@ -129,31 +130,15 @@ class UserCancelled(Exception):
|
||||||
'''An exception that is suppressed from the user'''
|
'''An exception that is suppressed from the user'''
|
||||||
pass
|
pass
|
||||||
|
|
||||||
class Satoshis(object):
|
class Satoshis(NamedTuple):
|
||||||
__slots__ = ('value',)
|
value: int
|
||||||
|
|
||||||
def __new__(cls, value):
|
|
||||||
self = super(Satoshis, cls).__new__(cls)
|
|
||||||
self.value = value
|
|
||||||
return self
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
return 'Satoshis(%d)'%self.value
|
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return format_satoshis(self.value) + " BTC"
|
return format_satoshis(self.value) + " BTC"
|
||||||
|
|
||||||
class Fiat(object):
|
class Fiat(NamedTuple):
|
||||||
__slots__ = ('value', 'ccy')
|
value: Optional[Decimal]
|
||||||
|
ccy: str
|
||||||
def __new__(cls, value, ccy):
|
|
||||||
self = super(Fiat, cls).__new__(cls)
|
|
||||||
self.ccy = ccy
|
|
||||||
self.value = value
|
|
||||||
return self
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
return 'Fiat(%s)'% self.__str__()
|
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
if self.value is None or self.value.is_nan():
|
if self.value is None or self.value.is_nan():
|
||||||
|
|
|
@ -247,24 +247,37 @@ class Abstract_Wallet(AddressSynchronizer):
|
||||||
self.storage.put('labels', self.labels)
|
self.storage.put('labels', self.labels)
|
||||||
return changed
|
return changed
|
||||||
|
|
||||||
def set_fiat_value(self, txid, ccy, text):
|
def set_fiat_value(self, txid, ccy, text, fx, value):
|
||||||
if txid not in self.transactions:
|
if txid not in self.transactions:
|
||||||
return
|
return
|
||||||
if not text:
|
# since fx is inserting the thousands separator,
|
||||||
|
# and not util, also have fx remove it
|
||||||
|
text = fx.remove_thousands_separator(text)
|
||||||
|
def_fiat = self.default_fiat_value(txid, fx, value)
|
||||||
|
formatted = fx.ccy_amount_str(def_fiat, commas=False)
|
||||||
|
def_fiat_rounded = Decimal(formatted)
|
||||||
|
reset = not text
|
||||||
|
if not reset:
|
||||||
|
try:
|
||||||
|
text_dec = Decimal(text)
|
||||||
|
text_dec_rounded = Decimal(fx.ccy_amount_str(text_dec, commas=False))
|
||||||
|
reset = text_dec_rounded == def_fiat_rounded
|
||||||
|
except:
|
||||||
|
# garbage. not resetting, but not saving either
|
||||||
|
return False
|
||||||
|
if reset:
|
||||||
d = self.fiat_value.get(ccy, {})
|
d = self.fiat_value.get(ccy, {})
|
||||||
if d and txid in d:
|
if d and txid in d:
|
||||||
d.pop(txid)
|
d.pop(txid)
|
||||||
else:
|
else:
|
||||||
return
|
# avoid saving empty dict
|
||||||
else:
|
return True
|
||||||
try:
|
|
||||||
Decimal(text)
|
|
||||||
except:
|
|
||||||
return
|
|
||||||
if ccy not in self.fiat_value:
|
if ccy not in self.fiat_value:
|
||||||
self.fiat_value[ccy] = {}
|
self.fiat_value[ccy] = {}
|
||||||
self.fiat_value[ccy][txid] = text
|
if not reset:
|
||||||
|
self.fiat_value[ccy][txid] = text
|
||||||
self.storage.put('fiat_value', self.fiat_value)
|
self.storage.put('fiat_value', self.fiat_value)
|
||||||
|
return reset
|
||||||
|
|
||||||
def get_fiat_value(self, txid, ccy):
|
def get_fiat_value(self, txid, ccy):
|
||||||
fiat_value = self.fiat_value.get(ccy, {}).get(txid)
|
fiat_value = self.fiat_value.get(ccy, {}).get(txid)
|
||||||
|
@ -423,21 +436,11 @@ class Abstract_Wallet(AddressSynchronizer):
|
||||||
income += value
|
income += value
|
||||||
# fiat computations
|
# fiat computations
|
||||||
if fx and fx.is_enabled() and fx.get_history_config():
|
if fx and fx.is_enabled() and fx.get_history_config():
|
||||||
fiat_value = self.get_fiat_value(tx_hash, fx.ccy)
|
fiat_fields = self.get_tx_item_fiat(tx_hash, value, fx, tx_fee)
|
||||||
fiat_default = fiat_value is None
|
fiat_value = fiat_fields['fiat_value'].value
|
||||||
fiat_rate = self.price_at_timestamp(tx_hash, fx.timestamp_rate)
|
item.update(fiat_fields)
|
||||||
fiat_value = fiat_value if fiat_value is not None else value / Decimal(COIN) * fiat_rate
|
|
||||||
fiat_fee = tx_fee / Decimal(COIN) * fiat_rate if tx_fee is not None else None
|
|
||||||
item['fiat_value'] = Fiat(fiat_value, fx.ccy)
|
|
||||||
item['fiat_fee'] = Fiat(fiat_fee, fx.ccy) if fiat_fee else None
|
|
||||||
item['fiat_default'] = fiat_default
|
|
||||||
if value < 0:
|
if value < 0:
|
||||||
acquisition_price = - value / Decimal(COIN) * self.average_price(tx_hash, fx.timestamp_rate, fx.ccy)
|
capital_gains += fiat_fields['capital_gain'].value
|
||||||
liquidation_price = - fiat_value
|
|
||||||
item['acquisition_price'] = Fiat(acquisition_price, fx.ccy)
|
|
||||||
cg = liquidation_price - acquisition_price
|
|
||||||
item['capital_gain'] = Fiat(cg, fx.ccy)
|
|
||||||
capital_gains += cg
|
|
||||||
fiat_expenditures += -fiat_value
|
fiat_expenditures += -fiat_value
|
||||||
else:
|
else:
|
||||||
fiat_income += fiat_value
|
fiat_income += fiat_value
|
||||||
|
@ -478,6 +481,27 @@ class Abstract_Wallet(AddressSynchronizer):
|
||||||
'summary': summary
|
'summary': summary
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def default_fiat_value(self, tx_hash, fx, value):
|
||||||
|
return value / Decimal(COIN) * self.price_at_timestamp(tx_hash, fx.timestamp_rate)
|
||||||
|
|
||||||
|
def get_tx_item_fiat(self, tx_hash, value, fx, tx_fee):
|
||||||
|
item = {}
|
||||||
|
fiat_value = self.get_fiat_value(tx_hash, fx.ccy)
|
||||||
|
fiat_default = fiat_value is None
|
||||||
|
fiat_rate = self.price_at_timestamp(tx_hash, fx.timestamp_rate)
|
||||||
|
fiat_value = fiat_value if fiat_value is not None else self.default_fiat_value(tx_hash, fx, value)
|
||||||
|
fiat_fee = tx_fee / Decimal(COIN) * fiat_rate if tx_fee is not None else None
|
||||||
|
item['fiat_value'] = Fiat(fiat_value, fx.ccy)
|
||||||
|
item['fiat_fee'] = Fiat(fiat_fee, fx.ccy) if fiat_fee else None
|
||||||
|
item['fiat_default'] = fiat_default
|
||||||
|
if value < 0:
|
||||||
|
acquisition_price = - value / Decimal(COIN) * self.average_price(tx_hash, fx.timestamp_rate, fx.ccy)
|
||||||
|
liquidation_price = - fiat_value
|
||||||
|
item['acquisition_price'] = Fiat(acquisition_price, fx.ccy)
|
||||||
|
cg = liquidation_price - acquisition_price
|
||||||
|
item['capital_gain'] = Fiat(cg, fx.ccy)
|
||||||
|
return item
|
||||||
|
|
||||||
def get_label(self, tx_hash):
|
def get_label(self, tx_hash):
|
||||||
label = self.labels.get(tx_hash, '')
|
label = self.labels.get(tx_hash, '')
|
||||||
if label is '':
|
if label is '':
|
||||||
|
|
Loading…
Add table
Reference in a new issue