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: "" + e + " | "
+ elem = lambda e: f"{e} | "
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 + "
"
+ 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"))