From 6e5cdc81aefca29d79f076139e9ec1b86bbb1193 Mon Sep 17 00:00:00 2001 From: Kristaps Kaupe Date: Wed, 28 Feb 2024 13:33:04 +0200 Subject: [PATCH] Refactor: move bitcoin unit conversion functions from ob-watcher to jmbitcoin --- scripts/obwatch/ob-watcher.py | 93 +++++++++++++++++++++------------- src/jmbitcoin/amount.py | 28 +++++++++- test/jmbitcoin/test_amounts.py | 36 +++++++++++++ 3 files changed, 121 insertions(+), 36 deletions(-) diff --git a/scripts/obwatch/ob-watcher.py b/scripts/obwatch/ob-watcher.py index 921994a..b4c247b 100755 --- a/scripts/obwatch/ob-watcher.py +++ b/scripts/obwatch/ob-watcher.py @@ -1,23 +1,26 @@ #!/usr/bin/env python3 from functools import cmp_to_key -import http.server import base64 +import hashlib +import html +import http.server import io import json +import os import threading import time -import hashlib -import os import sys -from urllib.parse import parse_qs +from datetime import datetime, timedelta from decimal import Decimal from optparse import OptionParser +from typing import Tuple, Union from twisted.internet import reactor -from datetime import datetime, timedelta +from urllib.parse import parse_qs -from jmbase.support import EXIT_FAILURE from jmbase import bintohex +from jmbase.support import EXIT_FAILURE +from jmbitcoin import bitcoin_unit_to_power, sat_to_unit, sat_to_unit_power from jmclient import FidelityBondMixin, get_interest_rate, check_and_start_tor from jmclient.fidelity_bond import FidelityBondProof @@ -55,7 +58,6 @@ filtered_offername_list = sw0offers rotateObform = '
' refresh_orderbook_form = '
' sorted_units = ('BTC', 'mBTC', 'μBTC', 'satoshi') -unit_to_power = {'BTC': 8, 'mBTC': 5, 'μBTC': 2, 'satoshi': 0} sorted_rel_units = ('%', '‱', 'ppm') rel_unit_to_factor = {'%': 100, '‱': 1e4, 'ppm': 1e6} @@ -82,24 +84,33 @@ def ordertype_display(ordertype, order, btc_unit, rel_unit): return ordertypes[ordertype] -def cjfee_display(cjfee, order, btc_unit, rel_unit): +def cjfee_display(cjfee: Union[Decimal, float, int], + order: dict, + btc_unit: str, + rel_unit: str) -> str: if order['ordertype'] in ['swabsoffer', 'sw0absoffer']: - return satoshi_to_unit(cjfee, order, btc_unit, rel_unit) + val = sat_to_unit(cjfee, html.unescape(btc_unit)) + if btc_unit == "BTC": + return "%.8f" % val + else: + return str(val) elif order['ordertype'] in ['reloffer', 'swreloffer', 'sw0reloffer']: return str(Decimal(cjfee) * Decimal(rel_unit_to_factor[rel_unit])) + rel_unit -def satoshi_to_unit_power(sat, power): - return ("%." + str(power) + "f") % float( - Decimal(sat) / Decimal(10 ** power)) - -def satoshi_to_unit(sat, order, btc_unit, rel_unit): - return satoshi_to_unit_power(sat, unit_to_power[btc_unit]) - def order_str(s, order, btc_unit, rel_unit): return str(s) +def bond_value_to_str(bond_value: Decimal, btc_unit: str) -> str: + if btc_unit == "BTC": + return "%.16f" % bond_value + elif btc_unit == "mBTC": + return "%.10f" % bond_value + else: + return str(bond_value) + + def create_offerbook_table_heading(btc_unit, rel_unit): col = ' {1}\n' # .format(field,label) tableheading = '\n ' + ''.join( @@ -319,7 +330,7 @@ class OrderbookPageRequestHeader(http.server.SimpleHTTPRequestHandler): return get_graph_html(fig) + ("
log scale" if bins == 30 else "
linear") - def create_fidelity_bond_table(self, btc_unit): + def create_fidelity_bond_table(self, btc_unit: str) -> Tuple[str, str]: if jm_single().bc_interface == None: with self.taker.dblock: fbonds = self.taker.db.execute("SELECT * FROM fidelitybonds;").fetchall() @@ -339,12 +350,12 @@ class OrderbookPageRequestHeader(http.server.SimpleHTTPRequestHandler): else: (fidelity_bond_data, fidelity_bond_values, bond_outpoint_conf_times) =\ get_fidelity_bond_data(self.taker) - total_btc_committed_str = satoshi_to_unit( + total_btc_committed_str = str(sat_to_unit( sum([utxo_data["value"] for _, utxo_data in fidelity_bond_data]), - None, btc_unit, 0) + html.unescape(btc_unit))) RETARGET_INTERVAL = 2016 - elem = lambda e: "" + elem = lambda e: f"" bondtable = "" for (bond_data, utxo_data), bond_value, conf_time in zip( fidelity_bond_data, fidelity_bond_values, bond_outpoint_conf_times): @@ -354,9 +365,11 @@ class OrderbookPageRequestHeader(http.server.SimpleHTTPRequestHandler): conf_time_str = "No data" utxo_value_str = "No data" else: - bond_value_str = satoshi_to_unit_power(bond_value, 2*unit_to_power[btc_unit]) + bond_value_str = bond_value_to_str(sat_to_unit_power(bond_value, + 2 * bitcoin_unit_to_power(html.unescape(btc_unit))), + html.unescape(btc_unit)) conf_time_str = str(datetime.utcfromtimestamp(0) + timedelta(seconds=conf_time)) - utxo_value_str = satoshi_to_unit(utxo_data["value"], None, btc_unit, 0) + utxo_value_str = sat_to_unit(utxo_data["value"], html.unescape(btc_unit)) bondtable += ("" + elem(bond_data.maker_nick) + elem(bintohex(bond_data.utxo[0]) + ":" + str(bond_data.utxo[1])) @@ -391,7 +404,7 @@ class OrderbookPageRequestHeader(http.server.SimpleHTTPRequestHandler): choose_units_form + create_bonds_table_heading(btc_unit) + bondtable + "
" + e + "{e}
" + decodescript_tip) - def create_sybil_resistance_page(self, btc_unit): + def create_sybil_resistance_page(self, btc_unit: str) -> Tuple[str, str]: if jm_single().bc_interface == None: return "", "Calculations unavailable, requires configured bitcoin node." @@ -412,7 +425,7 @@ class OrderbookPageRequestHeader(http.server.SimpleHTTPRequestHandler): mainbody += ("Assuming the makers in the offerbook right now are not sybil attackers, " + "how much would a sybil attacker starting now have to sacrifice to succeed in their" + " attack with 95% probability. Honest weight=" - + satoshi_to_unit_power(honest_weight, 2*unit_to_power[btc_unit]) + " " + btc_unit + + str(sat_to_unit_power(honest_weight, 2 * bitcoin_unit_to_power(html.unescape(btc_unit)))) + " " + btc_unit + "" + bond_exponent + "
Also assumes that takers " + "are not price-sensitive and that their max " + "coinjoin fee is configured high enough that they dont exclude any makers.") @@ -440,7 +453,7 @@ class OrderbookPageRequestHeader(http.server.SimpleHTTPRequestHandler): interest_rate, timelock) else: coins_per_sybil = sybil.weight_to_burned_coins(success_sybil_weight) - row += ("" + satoshi_to_unit(coins_per_sybil*makercount, None, btc_unit, 0) + row += ("" + str(sat_to_unit(coins_per_sybil * makercount, html.unescape(btc_unit))) + "") row += "" mainbody += row @@ -471,8 +484,10 @@ class OrderbookPageRequestHeader(http.server.SimpleHTTPRequestHandler): sacrificed_values = [sybil.weight_to_burned_coins(w) for w in weights[:makercount]] foregone_value = (sybil.coins_burned_to_weight(sum(sacrificed_values)) - total_sybil_weight) - mainbody += ("" + makercount_str + "" + str(round(success_prob*100.0, 5)) - + "%" + satoshi_to_unit_power(foregone_value, 2*unit_to_power[btc_unit]) + mainbody += ("" + makercount_str + "" + str(round(success_prob * 100.0, 5)) + + "%" + bond_value_to_str(sat_to_unit_power( + foregone_value, 2 * bitcoin_unit_to_power( + html.unescape(btc_unit))), html.unescape(btc_unit)) + "") if makercount == len(weights): break @@ -480,7 +495,7 @@ class OrderbookPageRequestHeader(http.server.SimpleHTTPRequestHandler): return heading2, mainbody - def create_orderbook_table(self, btc_unit, rel_unit): + def create_orderbook_table(self, btc_unit: str, rel_unit: str) -> Tuple[int, str]: result = '' try: self.taker.dblock.acquire(True) @@ -532,15 +547,25 @@ class OrderbookPageRequestHeader(http.server.SimpleHTTPRequestHandler): parsed_bond.locktime, mediantime, interest_rate) - row["bondvalue"] = satoshi_to_unit_power(bond_value, 2*unit_to_power[btc_unit]) + row["bondvalue"] = bond_value_to_str(sat_to_unit_power( + bond_value, + 2 * bitcoin_unit_to_power(html.unescape(btc_unit))), + html.unescape(btc_unit)) + + def _okd_satoshi_to_unit(sat, order, btc_unit, rel_unit): + val = sat_to_unit(sat, html.unescape(btc_unit)) + if btc_unit == "BTC": + return "%.8f" % val + else: + return str(val) order_keys_display = (('ordertype', ordertype_display), ('counterparty', do_nothing), ('oid', order_str), ('cjfee', cjfee_display), - ('txfee', satoshi_to_unit), - ('minsize', satoshi_to_unit), - ('maxsize', satoshi_to_unit), + ('txfee', _okd_satoshi_to_unit), + ('minsize', _okd_satoshi_to_unit), + ('maxsize', _okd_satoshi_to_unit), ('bondvalue', do_nothing)) def _cmp(x, y): @@ -561,8 +586,8 @@ class OrderbookPageRequestHeader(http.server.SimpleHTTPRequestHandler): for o in sorted(rows, key=cmp_to_key(orderby_cmp)): result += ' \n' for key, displayer in order_keys_display: - result += ' ' + displayer(o[key], o, btc_unit, - rel_unit) + '\n' + result += ' ' + str(displayer(o[key], o, btc_unit, + rel_unit)) + '\n' result += ' \n' return len(rows), result diff --git a/src/jmbitcoin/amount.py b/src/jmbitcoin/amount.py index fd19f69..d0ab290 100644 --- a/src/jmbitcoin/amount.py +++ b/src/jmbitcoin/amount.py @@ -3,12 +3,37 @@ from typing import Any, Tuple, Union import re +def bitcoin_unit_to_power(btc_unit: str) -> int: + # https://en.bitcoin.it/wiki/Units + unit_to_power = { + 'BTC': 8, + 'mBTC': 5, # milli-bitoin, 0.001 BTC + 'μBTC': 2, # micro-bitcoin (bit), 0.000001 BTC + 'bit': 2, + 'satoshi': 0, + 'sat': 0 + } + if btc_unit not in unit_to_power: + raise ValueError(f"Invalid bitcoin unit: {btc_unit}") + return unit_to_power[btc_unit] + + def btc_to_sat(btc: Union[int, str, Tuple, float, Decimal]) -> int: return int(Decimal(btc) * Decimal('1e8')) +def sat_to_unit_power(sat: int, power: int) -> Decimal: + return Decimal(f"%.{power}f" % float( + Decimal(sat) / Decimal(10 ** power))) + + +def sat_to_unit(sat: int, btc_unit: str) -> Decimal: + return sat_to_unit_power(sat, bitcoin_unit_to_power(btc_unit)) + + def sat_to_btc(sat: int) -> Decimal: - return Decimal(sat) / Decimal('1e8') + return sat_to_unit(sat, 'BTC') + # 1 = 0.00000001 BTC = 1sat # 1sat = 0.00000001 BTC = 1sat @@ -19,7 +44,6 @@ def sat_to_btc(sat: int) -> Decimal: # 1.12300000sat = 0.00000001 BTC = 1sat # 1btc = 1.00000000 BTC = 10000000sat - def amount_to_sat(amount_str: str) -> int: amount_str = str(amount_str).strip() if re.compile(r"^[0-9]{1,8}(\.)?([0-9]{1,8})?(btc|sat)?$").match( diff --git a/test/jmbitcoin/test_amounts.py b/test/jmbitcoin/test_amounts.py index 98f570c..e338a73 100644 --- a/test/jmbitcoin/test_amounts.py +++ b/test/jmbitcoin/test_amounts.py @@ -3,11 +3,47 @@ import pytest from decimal import Decimal +def test_bitcoin_unit_to_power() -> None: + assert(btc.bitcoin_unit_to_power("BTC") == 8) + assert(btc.bitcoin_unit_to_power("mBTC") == 5) + assert(btc.bitcoin_unit_to_power("μBTC") == 2) + assert(btc.bitcoin_unit_to_power("satoshi") == 0) + with pytest.raises(ValueError): + btc.bitcoin_unit_to_power("") + btc.bitcoin_unit_to_power("invalidunit") + + def test_btc_to_sat() -> None: assert(btc.btc_to_sat(Decimal("0.00000001")) == 1) assert(btc.btc_to_sat(Decimal("1.00000000")) == 100000000) +def test_sat_to_unit_power() -> None: + assert(btc.sat_to_unit_power(1, 8) == Decimal("0.00000001")) + assert(btc.sat_to_unit_power(100000000, 8) == Decimal("1.00000000")) + assert(btc.sat_to_unit_power(1, 5) == Decimal("0.00001")) + assert(btc.sat_to_unit_power(100000, 5) == Decimal("1.00000")) + assert(btc.sat_to_unit_power(1, 2) == Decimal("0.01")) + assert(btc.sat_to_unit_power(100, 2) == Decimal("1.00")) + assert(btc.sat_to_unit_power(1, 0) == Decimal("1")) + + +def test_sat_to_unit() -> None: + assert(btc.sat_to_unit(1, "BTC") == Decimal("0.00000001")) + assert(btc.sat_to_unit(100000000, "BTC") == Decimal("1.00000000")) + assert(btc.sat_to_unit(1, "mBTC") == Decimal("0.00001")) + assert(btc.sat_to_unit(100000, "mBTC") == Decimal("1.00000")) + assert(btc.sat_to_unit(1, "μBTC") == Decimal("0.01")) + assert(btc.sat_to_unit(100, "μBTC") == Decimal("1.00")) + assert(btc.sat_to_unit(1, "bit") == Decimal("0.01")) + assert(btc.sat_to_unit(100, "bit") == Decimal("1.00")) + assert(btc.sat_to_unit(1, "satoshi") == Decimal("1")) + assert(btc.sat_to_unit(1, "sat") == Decimal("1")) + with pytest.raises(ValueError): + btc.sat_to_unit(1, "") + btc.sat_to_unit(1, "invalidunit") + + def test_sat_to_btc() -> None: assert(btc.sat_to_btc(1) == Decimal("0.00000001")) assert(btc.sat_to_btc(100000000) == Decimal("1.00000000"))