Skip to content

Commit

Permalink
[REF] Use Value in Transaction class, fix issues, add unittests
Browse files Browse the repository at this point in the history
  • Loading branch information
mccwdev committed Nov 18, 2020
1 parent 0e0906f commit cd3245a
Show file tree
Hide file tree
Showing 5 changed files with 100 additions and 27 deletions.
15 changes: 12 additions & 3 deletions bitcoinlib/transactions.py
Original file line number Diff line number Diff line change
Expand Up @@ -682,7 +682,7 @@ def __init__(self, prev_txid, output_n, keys=None, signatures=None, public_hash=
:param index_n: Index of input in transaction. Used by Transaction class.
:type index_n: int
:param value: Value of input in smallest denominator, i.e. sathosis
:type value: int
:type value: int, Value, str
:param double_spend: Is this input also spend in another transaction
:type double_spend: bool
:param locktime_cltv: Check Lock Time Verify value. Script level absolute time lock for this input
Expand Down Expand Up @@ -721,6 +721,10 @@ def __init__(self, prev_txid, output_n, keys=None, signatures=None, public_hash=
if not isinstance(network, Network):
self.network = Network(network)
self.index_n = index_n
if isinstance(value, str):
value = Value(value).value_sat
elif isinstance(value, Value):
value = value.value_sat
self.value = value
if not keys:
keys = []
Expand Down Expand Up @@ -1058,6 +1062,10 @@ def __init__(self, value, address='', public_hash=b'', public_key=b'', lock_scri
raise TransactionError("Please specify address, lock_script, public key or public key hash when "
"creating output")

if isinstance(value, str):
value = Value(value).value_sat
elif isinstance(value, Value):
value = value.value_sat
self.value = value
self.lock_script = b'' if lock_script is None else to_bytes(lock_script)
self.public_hash = to_bytes(public_hash)
Expand Down Expand Up @@ -1412,7 +1420,7 @@ def info(self):
print("Inputs")
replace_by_fee = False
for ti in self.inputs:
print("-", ti.address, Value.from_satoshi(ti.value, self.network).str(1), ti.prev_txid.hex(),
print("-", ti.address, Value.from_satoshi(ti.value, network=self.network).str(1), ti.prev_txid.hex(),
ti.output_n_int)
validstr = "not validated"
if ti.valid:
Expand Down Expand Up @@ -1451,7 +1459,8 @@ def info(self):
spent_str = 'S'
elif to.spent is False:
spent_str = 'U'
print("-", to.address, Value.from_satoshi(to.value, self.network).str(1), to.script_type, spent_str)
print("-", to.address, Value.from_satoshi(to.value, network=self.network).str(1), to.script_type,
spent_str)
if replace_by_fee:
print("Replace by fee: Enabled")
print("Size: %s" % self.size)
Expand Down
53 changes: 34 additions & 19 deletions bitcoinlib/values.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,22 +28,32 @@ class Value:
"""

@classmethod
def from_satoshi(cls, value, network=DEFAULT_NETWORK):
def from_satoshi(cls, value, denominator=None, network=DEFAULT_NETWORK):
"""
Initialize Value class with smallest denominator as input. Such as represented in script and transactions cryptocurrency values.
:param value: Amount of Satoshi's / smallest denominator for this network
:type value: int
:param denominator: Denominator as integer or string. Such as 0.001 or m for milli, 1000 or k for kilo, etc. See NETWORK_DENOMINATORS for list of available denominator symbols.
:type denominator: int, float, str
:param network: Specify network if not supplied already in the value string
:type network: str, Network
:return Value:
"""
if not isinstance(network, Network):
network = Network(network)
return cls(value or 0, network.denominator, network)
if denominator is None:
denominator = network.denominator
else:
if isinstance(denominator, str):
dens = [den for den, symb in NETWORK_DENOMINATORS.items() if symb == denominator]
if dens:
denominator = dens[0]
value = value * (network.denominator / denominator)
return cls(value or 0, denominator, network)

def __init__(self, value, denominator=1, network=DEFAULT_NETWORK):
def __init__(self, value, denominator=None, network=DEFAULT_NETWORK):
"""
Create a new Value class. Specify value as integer, float or string. If a string is provided
the amount, denominator and currency will be extracted if provided
Expand Down Expand Up @@ -114,12 +124,13 @@ def __init__(self, value, denominator=1, network=DEFAULT_NETWORK):
dens = [den for den, symb in NETWORK_DENOMINATORS.items() if symb == denominator]
if dens:
denominator = dens[0]
self.denominator = float(denominator) if denominator else 1.0
den_arg = denominator

if isinstance(value, str):
value_items = value.split()
value = value_items[0]
cur_code = self.network.currency_code
den_input = 1
if len(value_items) > 1:
cur_code = value_items[1]
network_names = [n for n in NETWORK_DEFINITIONS if
Expand All @@ -138,10 +149,12 @@ def __init__(self, value, denominator=1, network=DEFAULT_NETWORK):
self.currency = cur_code
elif len(cur_code):
raise ValueError("Currency symbol not recognised")
self.denominator = den
den_input = den
break
self.value = float(value) * self.denominator
self.value = float(value) * den_input
self.denominator = den_input if den_arg is None else den_arg
else:
self.denominator = den_arg or 1.0
self.value = float(value) * self.denominator

def __str__(self):
Expand Down Expand Up @@ -250,14 +263,16 @@ def __le__(self, other):
return self.value <= other.value

def __eq__(self, other):
if self.network != other.network:
raise ValueError("Cannot compare values from different networks")
return self.value == other.value
if isinstance(other, Value):
if self.network != other.network:
raise ValueError("Cannot compare values from different networks")
return self.value == other.value
else:
other = Value(other)
return self.value == other.value and self.network == other.network

def __ne__(self, other):
if self.network != other.network:
raise ValueError("Cannot compare values from different networks")
return self.value != other.value
return not self.__eq__(other)

def __ge__(self, other):
if self.network != other.network:
Expand All @@ -274,37 +289,37 @@ def __add__(self, other):
if self.network != other.network:
raise ValueError("Cannot calculate with values from different networks")
other = other.value
return Value(self.value + other, self.denominator, self.network)
return Value((self.value + other) / self.denominator, self.denominator, self.network)

def __iadd__(self, other):
if isinstance(other, Value):
if self.network != other.network:
raise ValueError("Cannot calculate with values from different networks")
other = other.value
return Value(self.value + other, self.denominator, self.network)
return Value((self.value + other) / self.denominator, self.denominator, self.network)

def __isub__(self, other):
if isinstance(other, Value):
if self.network != other.network:
raise ValueError("Cannot calculate with values from different networks")
other = other.value
return Value(self.value - other, self.denominator, self.network)
return Value((self.value - other) / self.denominator, self.denominator, self.network)

def __sub__(self, other):
if isinstance(other, Value):
if self.network != other.network:
raise ValueError("Cannot calculate with values from different networks")
other = other.value
return Value(self.value - other, self.denominator, self.network)
return Value((self.value - other) / self.denominator, self.denominator, self.network)

def __mul__(self, other):
return Value(self.value * other, self.denominator, self.network)
return Value((self.value * other) / self.denominator, self.denominator, self.network)

def __truediv__(self, other):
return Value(self.value / other, self.denominator, self.network)
return Value((self.value / other) / self.denominator, self.denominator, self.network)

def __floordiv__(self, other):
return Value(((self.value / self.denominator) // other) * self.denominator, self.denominator, self.network)
return Value(((self.value / self.denominator) // other), self.denominator, self.network)

def __round__(self, n=0):
val = round(self.value / self.denominator, n) * self.denominator
Expand Down
10 changes: 5 additions & 5 deletions bitcoinlib/wallets.py
Original file line number Diff line number Diff line change
Expand Up @@ -514,7 +514,7 @@ def balance(self, as_string=False):
"""

if as_string:
return Value.from_satoshi(self._balance, self.network).str1()
return Value.from_satoshi(self._balance, network=self.network).str1()
else:
return self._balance

Expand Down Expand Up @@ -2511,7 +2511,7 @@ def balance(self, account_id=None, network=None, as_string=False):
if len(b_res):
balance = b_res[0]
if as_string:
return Value.from_satoshi(balance, network).str1()
return Value.from_satoshi(balance, network=network).str1()
else:
return float(balance)

Expand Down Expand Up @@ -3919,7 +3919,7 @@ def info(self, detail=3):
for key in self.keys(depth=d, network=nw.name, is_active=is_active):
print("%5s %-28s %-45s %-25s %25s" %
(key.id, key.path, key.address, key.name,
Value.from_satoshi(key.balance, nw).str1(currency_repr='symbol')))
Value.from_satoshi(key.balance, network=nw).str1(currency_repr='symbol')))

if detail > 2:
include_new = False
Expand All @@ -3943,13 +3943,13 @@ def info(self, detail=3):
if tx['status'] not in ['confirmed', 'unconfirmed']:
status = tx['status']
print("%64s %43s %8d %21s %s %s" % (tx['txid'], address, tx['confirmations'],
Value.from_satoshi(tx['value'], nw).str1(
Value.from_satoshi(tx['value'], network=nw).str1(
currency_repr='symbol'),
spent, status))
print("\n= Balance Totals (includes unconfirmed) =")
for na_balance in balances:
print("%-20s %-20s %20s" % (na_balance['network'], "(Account %s)" % na_balance['account_id'],
Value.from_satoshi(na_balance['balance'], na_balance['network']).str1(
Value.from_satoshi(na_balance['balance'], network=na_balance['network']).str1(
currency_repr='symbol')))
print("\n")

Expand Down
17 changes: 17 additions & 0 deletions tests/test_transactions.py
Original file line number Diff line number Diff line change
Expand Up @@ -1154,6 +1154,23 @@ def test_transaction_sign_p2pk(self):
self.assertEqual(t.signature_hash(sign_id=0).hex(),
'67b94bf5a5c17a5f6b2bedbefc51a17db669ce7ff3bbbc4943cfd876d68df986')

def test_transaction_sign_p2pk_value(self):
wif = 'tprv8ZgxMBicQKsPdx411rqb5SjGvY43Bjc2PyhU2UCVtbEwCDSyKzHhaM88XaKHe5LcyNVdwWgG9NBut4oytRLbhr7iHbJ7KxioG' \
'nQETYvZu3j'
k = HDKey(wif)
prev_txid = '9f5c85ceb8f0c6c9b4dd3ab3b4a522d6be7c90c33ee4e9097bf1f5fc8e367bcb'
output_n = 0
value = Value.from_satoshi(9000)
fee = 0.00002000

inp = Input(prev_txid, output_n, k, value=value, network='testnet', script_type='signature')
outp = Output(value - fee, k, network='testnet', script_type='p2pk')
t = Transaction([inp], [outp], network='testnet')
t.sign(k.private_byte)
self.assertTrue(t.verify())
self.assertEqual(t.signature_hash(sign_id=0).hex(),
'67b94bf5a5c17a5f6b2bedbefc51a17db669ce7ff3bbbc4943cfd876d68df986')

def test_transaction_locktime(self):
# FIXME: Add more usefull unittests for locktime
s = bytes.fromhex('76a914af8e14a2cecd715c363b3a72b55b59a31e2acac988ac')
Expand Down
32 changes: 32 additions & 0 deletions tests/test_values.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,32 @@ def test_value_class(self):
self.assertEqual(str(Value('10 satLTC')), '10 satLTC')
self.assertEqual(str(Value('10 sat', network='litecoin')), '10 satLTC')
self.assertRaisesRegex(ValueError, "Currency symbol not recognised", Value, '10 mfliepflap')
self.assertEqual(Value('10000 sat'), '10000 sat')
self.assertEqual(Value(10000, 'sat'), '10000 sat')
self.assertEqual(Value.from_satoshi(10000), '10000 sat')
self.assertEqual(Value('10000 sat', 1), '0.00010000 BTC')
self.assertEqual(Value(0.0001), '0.00010000 BTC')
self.assertEqual(Value.from_satoshi(10000, 1), '0.00010000 BTC')
self.assertEqual(Value('10000 sat', 'm'), '0.10000 mBTC')
self.assertEqual(Value(0.1, 'm'), '0.10000 mBTC')
self.assertEqual(Value.from_satoshi(10000, 'm'), '0.10000 mBTC')
self.assertNotEqual(Value.from_satoshi(10001, 'm'), '0.10000 mBTC')

def test_value_class_rounding(self):
self.assertEqual(str(Value('12.123456785')), '12.12345679 BTC')
self.assertEqual(str(Value('12.1234567849')), '12.12345678 BTC')
self.assertEqual(str(Value(5001.5, 'sat')), '5002 sat')
v1 = Value('10000.51 sat')
self.assertEqual(str(v1 + 0.002), '210001 sat')
self.assertEqual(str(v1 - 0.00005), '5001 sat')
self.assertEqual(str(v1 * 2), '20001 sat')
self.assertEqual(str(v1 / 2), '5000 sat')
self.assertEqual(str(v1 // 2), '5000 sat')
self.assertEqual(str(Value('10000.999 sat') / 2), '5000 sat')
self.assertEqual(str(Value('10001 sat') / 2), '5000 sat')
self.assertEqual(str(Value('10001.51 sat') / 2), '5001 sat')
self.assertEqual(str(Value('10001.51 sat') // 2), '5000 sat')
self.assertEqual(str(Value('103 sat') / 2), '52 sat')

def test_value_class_str(self):
self.assertEqual(Value(10).str(), '10.00000000 BTC')
Expand Down Expand Up @@ -109,6 +135,12 @@ def test_value_operators_comparison(self):
self.assertRaisesRegex(ValueError, "Cannot compare values from different networks", Value.__ge__, v1, v2)
self.assertRaisesRegex(ValueError, "Cannot compare values from different networks", Value.__ne__, v1, v2)

v3 = Value('1000 mBTC')
self.assertTrue(v3 == '1000.00000 mBTC')
self.assertTrue(v3 == '1 BTC')
self.assertTrue(v3 == '100000000 sat')
self.assertFalse(v3 == '1 dash')

def test_value_operators_arithmetic(self):
value1 = Value('3 BTC')
self.assertEqual(value1 + Value('500 mBTC'), Value('3.50000000 BTC'))
Expand Down

0 comments on commit cd3245a

Please sign in to comment.