diff --git a/jmclient/jmclient/wallet.py b/jmclient/jmclient/wallet.py index 9f21e35..657c8a9 100644 --- a/jmclient/jmclient/wallet.py +++ b/jmclient/jmclient/wallet.py @@ -17,6 +17,7 @@ from hashlib import sha256 from itertools import chain from decimal import Decimal from numbers import Integral +from math import exp from .configure import jm_single @@ -2380,6 +2381,25 @@ class FidelityBondMixin(object): self._storage.data[self._BURNER_OUTPUT_STORAGE_KEY][path][2] = \ merkle_branch + + @classmethod + def calculate_timelocked_fidelity_bond_value(cls, utxo_value, confirmation_time, locktime, + current_time, interest_rate): + """ + utxo_value is in satoshi + interest rate is per year + all times are seconds + """ + YEAR = 60 * 60 * 24 * 365.2425 #gregorian calender year length + + r = interest_rate + T = (locktime - confirmation_time) / YEAR + L = locktime / YEAR + t = current_time / YEAR + + a = max(0, min(1, exp(r*T) - 1) - min(1, exp(r*max(0, t-L)) - 1)) + return utxo_value*utxo_value*a*a + class BIP49Wallet(BIP32PurposedWallet): _PURPOSE = 2**31 + 49 _ENGINE = ENGINES[TYPE_P2SH_P2WPKH] diff --git a/jmclient/test/test_wallet.py b/jmclient/test/test_wallet.py index 2fedf1b..6b83ee9 100644 --- a/jmclient/test/test_wallet.py +++ b/jmclient/test/test_wallet.py @@ -821,6 +821,67 @@ def test_watchonly_wallet(setup_wallet): assert script == watchonly_script assert burn_pubkey == watchonly_burn_pubkey +def test_calculate_timelocked_fidelity_bond_value(setup_wallet): + EPSILON = 0.000001 + YEAR = 60*60*24*356.25 + + #the function should be flat anywhere before the locktime ends + values = [FidelityBondMixin.calculate_timelocked_fidelity_bond_value( + utxo_value=100000000, + confirmation_time=0, + locktime=6*YEAR, + current_time=y*YEAR, + interest_rate=0.01 + ) + for y in range(4) + ] + value_diff = [values[i] - values[i+1] for i in range(len(values)-1)] + for vd in value_diff: + assert abs(vd) < EPSILON + + #after locktime, the value should go down + values = [FidelityBondMixin.calculate_timelocked_fidelity_bond_value( + utxo_value=100000000, + confirmation_time=0, + locktime=6*YEAR, + current_time=(6+y)*YEAR, + interest_rate=0.01 + ) + for y in range(5) + ] + value_diff = [values[i+1] - values[i] for i in range(len(values)-1)] + for vrd in value_diff: + assert vrd < 0 + + #value of a bond goes up as the locktime goes up + values = [FidelityBondMixin.calculate_timelocked_fidelity_bond_value( + utxo_value=100000000, + confirmation_time=0, + locktime=y*YEAR, + current_time=0, + interest_rate=0.01 + ) + for y in range(5) + ] + value_ratio = [values[i] / values[i+1] for i in range(len(values)-1)] + value_ratio_diff = [value_ratio[i] - value_ratio[i+1] for i in range(len(value_ratio)-1)] + for vrd in value_ratio_diff: + assert vrd < 0 + + #value of a bond locked into the far future is constant, clamped at the value of burned coins + values = [FidelityBondMixin.calculate_timelocked_fidelity_bond_value( + utxo_value=100000000, + confirmation_time=0, + locktime=(200+y)*YEAR, + current_time=0, + interest_rate=0.01 + ) + for y in range(5) + ] + value_diff = [values[i] - values[i+1] for i in range(len(values)-1)] + for vd in value_diff: + assert abs(vd) < EPSILON + @pytest.mark.parametrize('password, wallet_cls', [ ["hunter2", SegwitLegacyWallet], ["hunter2", SegwitWallet],