Browse Source

qt history view custom fiat input fixes

previously, when you submitted a fiat value with thousands separator,
it would be discarded.
3.3.3.1
Janus 6 years ago
parent
commit
37b009a342
  1. 6
      electrum/exchange_rate.py
  2. 6
      electrum/gui/qt/history_list.py
  3. 71
      electrum/tests/test_wallet.py
  4. 27
      electrum/util.py
  5. 70
      electrum/wallet.py

6
electrum/exchange_rate.py

@ -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:

6
electrum/gui/qt/history_list.py

@ -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):

71
electrum/tests/test_wallet.py

@ -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)

27
electrum/util.py

@ -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():

70
electrum/wallet.py

@ -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…
Cancel
Save