Browse Source

Refactor: move bitcoin unit conversion functions from ob-watcher to jmbitcoin

master
Kristaps Kaupe 2 years ago
parent
commit
6e5cdc81ae
No known key found for this signature in database
GPG Key ID: 33E472FE870C7E5D
  1. 93
      scripts/obwatch/ob-watcher.py
  2. 28
      src/jmbitcoin/amount.py
  3. 36
      test/jmbitcoin/test_amounts.py

93
scripts/obwatch/ob-watcher.py

@ -1,23 +1,26 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
from functools import cmp_to_key from functools import cmp_to_key
import http.server
import base64 import base64
import hashlib
import html
import http.server
import io import io
import json import json
import os
import threading import threading
import time import time
import hashlib
import os
import sys import sys
from urllib.parse import parse_qs from datetime import datetime, timedelta
from decimal import Decimal from decimal import Decimal
from optparse import OptionParser from optparse import OptionParser
from typing import Tuple, Union
from twisted.internet import reactor 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 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 import FidelityBondMixin, get_interest_rate, check_and_start_tor
from jmclient.fidelity_bond import FidelityBondProof from jmclient.fidelity_bond import FidelityBondProof
@ -55,7 +58,6 @@ filtered_offername_list = sw0offers
rotateObform = '<form action="rotateOb" method="post"><input type="submit" value="Rotate orderbooks"/></form>' rotateObform = '<form action="rotateOb" method="post"><input type="submit" value="Rotate orderbooks"/></form>'
refresh_orderbook_form = '<form action="refreshorderbook" method="post"><input type="submit" value="Check for timed-out counterparties" /></form>' refresh_orderbook_form = '<form action="refreshorderbook" method="post"><input type="submit" value="Check for timed-out counterparties" /></form>'
sorted_units = ('BTC', 'mBTC', '&#956;BTC', 'satoshi') sorted_units = ('BTC', 'mBTC', '&#956;BTC', 'satoshi')
unit_to_power = {'BTC': 8, 'mBTC': 5, '&#956;BTC': 2, 'satoshi': 0}
sorted_rel_units = ('%', '&#8241;', 'ppm') sorted_rel_units = ('%', '&#8241;', 'ppm')
rel_unit_to_factor = {'%': 100, '&#8241;': 1e4, 'ppm': 1e6} rel_unit_to_factor = {'%': 100, '&#8241;': 1e4, 'ppm': 1e6}
@ -82,24 +84,33 @@ def ordertype_display(ordertype, order, btc_unit, rel_unit):
return ordertypes[ordertype] 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']: 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']: elif order['ordertype'] in ['reloffer', 'swreloffer', 'sw0reloffer']:
return str(Decimal(cjfee) * Decimal(rel_unit_to_factor[rel_unit])) + rel_unit 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): def order_str(s, order, btc_unit, rel_unit):
return str(s) 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): def create_offerbook_table_heading(btc_unit, rel_unit):
col = ' <th>{1}</th>\n' # .format(field,label) col = ' <th>{1}</th>\n' # .format(field,label)
tableheading = '<table class="tftable sortable" border="1">\n <tr>' + ''.join( tableheading = '<table class="tftable sortable" border="1">\n <tr>' + ''.join(
@ -319,7 +330,7 @@ class OrderbookPageRequestHeader(http.server.SimpleHTTPRequestHandler):
return get_graph_html(fig) + ("<br/><a href='?scale=log'>log scale</a>" if return get_graph_html(fig) + ("<br/><a href='?scale=log'>log scale</a>" if
bins == 30 else "<br/><a href='?'>linear</a>") bins == 30 else "<br/><a href='?'>linear</a>")
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: if jm_single().bc_interface == None:
with self.taker.dblock: with self.taker.dblock:
fbonds = self.taker.db.execute("SELECT * FROM fidelitybonds;").fetchall() fbonds = self.taker.db.execute("SELECT * FROM fidelitybonds;").fetchall()
@ -339,12 +350,12 @@ class OrderbookPageRequestHeader(http.server.SimpleHTTPRequestHandler):
else: else:
(fidelity_bond_data, fidelity_bond_values, bond_outpoint_conf_times) =\ (fidelity_bond_data, fidelity_bond_values, bond_outpoint_conf_times) =\
get_fidelity_bond_data(self.taker) 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]), sum([utxo_data["value"] for _, utxo_data in fidelity_bond_data]),
None, btc_unit, 0) html.unescape(btc_unit)))
RETARGET_INTERVAL = 2016 RETARGET_INTERVAL = 2016
elem = lambda e: "<td>" + e + "</td>" elem = lambda e: f"<td>{e}</td>"
bondtable = "" bondtable = ""
for (bond_data, utxo_data), bond_value, conf_time in zip( for (bond_data, utxo_data), bond_value, conf_time in zip(
fidelity_bond_data, fidelity_bond_values, bond_outpoint_conf_times): fidelity_bond_data, fidelity_bond_values, bond_outpoint_conf_times):
@ -354,9 +365,11 @@ class OrderbookPageRequestHeader(http.server.SimpleHTTPRequestHandler):
conf_time_str = "No data" conf_time_str = "No data"
utxo_value_str = "No data" utxo_value_str = "No data"
else: 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)) 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 += ("<tr>" bondtable += ("<tr>"
+ elem(bond_data.maker_nick) + elem(bond_data.maker_nick)
+ elem(bintohex(bond_data.utxo[0]) + ":" + str(bond_data.utxo[1])) + 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 + "</table>" choose_units_form + create_bonds_table_heading(btc_unit) + bondtable + "</table>"
+ decodescript_tip) + 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: if jm_single().bc_interface == None:
return "", "Calculations unavailable, requires configured bitcoin node." 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, " 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" + "how much would a sybil attacker starting now have to sacrifice to succeed in their"
+ " attack with 95% probability. Honest weight=" + " 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
+ "<sup>" + bond_exponent + "</sup><br/>Also assumes that takers " + "<sup>" + bond_exponent + "</sup><br/>Also assumes that takers "
+ "are not price-sensitive and that their max " + "are not price-sensitive and that their max "
+ "coinjoin fee is configured high enough that they dont exclude any makers.") + "coinjoin fee is configured high enough that they dont exclude any makers.")
@ -440,7 +453,7 @@ class OrderbookPageRequestHeader(http.server.SimpleHTTPRequestHandler):
interest_rate, timelock) interest_rate, timelock)
else: else:
coins_per_sybil = sybil.weight_to_burned_coins(success_sybil_weight) coins_per_sybil = sybil.weight_to_burned_coins(success_sybil_weight)
row += ("<td>" + satoshi_to_unit(coins_per_sybil*makercount, None, btc_unit, 0) row += ("<td>" + str(sat_to_unit(coins_per_sybil * makercount, html.unescape(btc_unit)))
+ "</td>") + "</td>")
row += "</tr>" row += "</tr>"
mainbody += 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]] sacrificed_values = [sybil.weight_to_burned_coins(w) for w in weights[:makercount]]
foregone_value = (sybil.coins_burned_to_weight(sum(sacrificed_values)) foregone_value = (sybil.coins_burned_to_weight(sum(sacrificed_values))
- total_sybil_weight) - total_sybil_weight)
mainbody += ("<tr><td>" + makercount_str + "</td><td>" + str(round(success_prob*100.0, 5)) mainbody += ("<tr><td>" + makercount_str + "</td><td>" + str(round(success_prob * 100.0, 5))
+ "%</td><td>" + satoshi_to_unit_power(foregone_value, 2*unit_to_power[btc_unit]) + "%</td><td>" + bond_value_to_str(sat_to_unit_power(
foregone_value, 2 * bitcoin_unit_to_power(
html.unescape(btc_unit))), html.unescape(btc_unit))
+ "</td></tr>") + "</td></tr>")
if makercount == len(weights): if makercount == len(weights):
break break
@ -480,7 +495,7 @@ class OrderbookPageRequestHeader(http.server.SimpleHTTPRequestHandler):
return heading2, mainbody 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 = '' result = ''
try: try:
self.taker.dblock.acquire(True) self.taker.dblock.acquire(True)
@ -532,15 +547,25 @@ class OrderbookPageRequestHeader(http.server.SimpleHTTPRequestHandler):
parsed_bond.locktime, parsed_bond.locktime,
mediantime, mediantime,
interest_rate) 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), order_keys_display = (('ordertype', ordertype_display),
('counterparty', do_nothing), ('counterparty', do_nothing),
('oid', order_str), ('oid', order_str),
('cjfee', cjfee_display), ('cjfee', cjfee_display),
('txfee', satoshi_to_unit), ('txfee', _okd_satoshi_to_unit),
('minsize', satoshi_to_unit), ('minsize', _okd_satoshi_to_unit),
('maxsize', satoshi_to_unit), ('maxsize', _okd_satoshi_to_unit),
('bondvalue', do_nothing)) ('bondvalue', do_nothing))
def _cmp(x, y): def _cmp(x, y):
@ -561,8 +586,8 @@ class OrderbookPageRequestHeader(http.server.SimpleHTTPRequestHandler):
for o in sorted(rows, key=cmp_to_key(orderby_cmp)): for o in sorted(rows, key=cmp_to_key(orderby_cmp)):
result += ' <tr>\n' result += ' <tr>\n'
for key, displayer in order_keys_display: for key, displayer in order_keys_display:
result += ' <td>' + displayer(o[key], o, btc_unit, result += ' <td>' + str(displayer(o[key], o, btc_unit,
rel_unit) + '</td>\n' rel_unit)) + '</td>\n'
result += ' </tr>\n' result += ' </tr>\n'
return len(rows), result return len(rows), result

28
src/jmbitcoin/amount.py

@ -3,12 +3,37 @@ from typing import Any, Tuple, Union
import re 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: def btc_to_sat(btc: Union[int, str, Tuple, float, Decimal]) -> int:
return int(Decimal(btc) * Decimal('1e8')) 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: def sat_to_btc(sat: int) -> Decimal:
return Decimal(sat) / Decimal('1e8') return sat_to_unit(sat, 'BTC')
# 1 = 0.00000001 BTC = 1sat # 1 = 0.00000001 BTC = 1sat
# 1sat = 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 # 1.12300000sat = 0.00000001 BTC = 1sat
# 1btc = 1.00000000 BTC = 10000000sat # 1btc = 1.00000000 BTC = 10000000sat
def amount_to_sat(amount_str: str) -> int: def amount_to_sat(amount_str: str) -> int:
amount_str = str(amount_str).strip() amount_str = str(amount_str).strip()
if re.compile(r"^[0-9]{1,8}(\.)?([0-9]{1,8})?(btc|sat)?$").match( if re.compile(r"^[0-9]{1,8}(\.)?([0-9]{1,8})?(btc|sat)?$").match(

36
test/jmbitcoin/test_amounts.py

@ -3,11 +3,47 @@ import pytest
from decimal import Decimal 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: def test_btc_to_sat() -> None:
assert(btc.btc_to_sat(Decimal("0.00000001")) == 1) assert(btc.btc_to_sat(Decimal("0.00000001")) == 1)
assert(btc.btc_to_sat(Decimal("1.00000000")) == 100000000) 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: def test_sat_to_btc() -> None:
assert(btc.sat_to_btc(1) == Decimal("0.00000001")) assert(btc.sat_to_btc(1) == Decimal("0.00000001"))
assert(btc.sat_to_btc(100000000) == Decimal("1.00000000")) assert(btc.sat_to_btc(100000000) == Decimal("1.00000000"))

Loading…
Cancel
Save