Esempio n. 1
0
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
def test_get_bond_values() -> None:
    load_test_config()
    # 1 BTC
    amount = pow(10, 8)
    months = 1
    interest = jm_single().config.getfloat("POLICY", "interest_rate")
    exponent = jm_single().config.getfloat("POLICY", "bond_value_exponent")
    parameters, results = get_bond_values(amount, months)
    assert parameters["amount"] == amount
    assert parameters["current_time"] == parameters["confirm_time"]
    assert parameters["interest"] == interest
    assert parameters["exponent"] == exponent
    assert len(results) == months
    locktime = datetime.fromtimestamp(results[0]["locktime"])
    assert locktime.month == get_next_locktime(datetime.now()).month
    value = FidelityBondMixin.calculate_timelocked_fidelity_bond_value(
        parameters["amount"],
        parameters["confirm_time"],
        results[0]["locktime"],
        parameters["current_time"],
        parameters["interest"],
    )
    assert results[0]["value"] == value

    months = 12
    interest = 0.02
    exponent = 2
    confirm_time = datetime(2021, 12, 1).timestamp()
    parameters, results = get_bond_values(amount, months, confirm_time,
                                          interest, exponent)
    assert parameters["amount"] == amount
    assert parameters["current_time"] != parameters["confirm_time"]
    assert parameters["confirm_time"] == confirm_time
    assert parameters["interest"] == interest
    assert parameters["exponent"] == exponent
    assert len(results) == months
    current_time = datetime.now()
    # get_bond_values(), at the end, reset the exponent to the config one.
    # So we have to set the exponent here, otherwise the bond value calculation
    # won't match and the assert would fail.
    old_exponent = jm_single().config.get("POLICY", "bond_value_exponent")
    jm_single().config.set("POLICY", "bond_value_exponent", str(exponent))
    for result in results:
        locktime = datetime.fromtimestamp(result["locktime"])
        assert locktime.month == get_next_locktime(current_time).month
        current_time = locktime
        value = FidelityBondMixin.calculate_timelocked_fidelity_bond_value(
            parameters["amount"],
            parameters["confirm_time"],
            result["locktime"],
            parameters["current_time"],
            parameters["interest"],
        )
        assert result["value"] == value
    jm_single().config.set("POLICY", "bond_value_exponent", old_exponent)
def get_fidelity_bond_data(taker):
    with taker.dblock:
        fbonds = taker.db.execute("SELECT * FROM fidelitybonds;").fetchall()

    blocks = jm_single().bc_interface.get_current_block_height()
    mediantime = jm_single().bc_interface.get_best_block_median_time()
    interest_rate = get_interest_rate()

    bond_utxo_set = set()
    fidelity_bond_data = []
    bond_outpoint_conf_times = []
    fidelity_bond_values = []
    for fb in fbonds:
        try:
            parsed_bond = FidelityBondProof.parse_and_verify_proof_msg(
                fb["counterparty"], fb["takernick"], fb["proof"])
        except ValueError:
            continue
        bond_utxo_data = FidelityBondMixin.get_validated_timelocked_fidelity_bond_utxo(
            parsed_bond.utxo, parsed_bond.utxo_pub, parsed_bond.locktime,
            parsed_bond.cert_expiry, blocks)
        if bond_utxo_data == None:
            continue
        #check for duplicated utxos i.e. two or more makers using the same UTXO
        # which is obviously not allowed, a fidelity bond must only be usable by one maker nick
        utxo_str = parsed_bond.utxo[0] + b":" + str(
            parsed_bond.utxo[1]).encode("ascii")
        if utxo_str in bond_utxo_set:
            continue
        bond_utxo_set.add(utxo_str)

        fidelity_bond_data.append((parsed_bond, bond_utxo_data))
        conf_time = jm_single().bc_interface.get_block_time(
            jm_single().bc_interface.get_block_hash(
                blocks - bond_utxo_data["confirms"] + 1))
        bond_outpoint_conf_times.append(conf_time)

        bond_value = FidelityBondMixin.calculate_timelocked_fidelity_bond_value(
            bond_utxo_data["value"], conf_time, parsed_bond.locktime,
            mediantime, interest_rate)
        fidelity_bond_values.append(bond_value)
    return (fidelity_bond_data, fidelity_bond_values, bond_outpoint_conf_times)
def get_bond_values(amount: int,
                    months: int,
                    confirm_time: Optional[float] = None,
                    interest: Optional[float] = None,
                    exponent: Optional[float] = None,
                    orderbook: Optional[Mapping[str, Any]] = None) -> Tuple[Dict[str, Any], List[Dict[str, Any]]]:
    """
    Conveniently generate values [and statistics] for multiple possible fidelity bonds.

    Args:
        amount: Fidelity bond UTXO amount in satoshi
        months: For how many months to calculate the results
        confirm_time: Fidelity bond UTXO confirmation time as timestamp, if None, current time is used.
                      I.e., like if the fidelity bond UTXO with given amount has just confirmed on the blockchain.
        interest: Interest rate, if None, value is taken from config
        exponent: Exponent, if None, value is taken from config
        orderbook: Orderbook data, if given, additional statistics are included in the results.
    Returns:
        A tuple with 2 elements.
         First is a dictionary with all the parameters used to perform fidelity bond calculations.
         Second is a list of dictionaries, one for each month, with the results.
    """
    current_time = datetime.now().timestamp()
    if confirm_time is None:
        confirm_time = current_time
    if interest is None:
        interest = get_interest_rate()
    if exponent is None:
        exponent = jm_single().config.getfloat("POLICY", "bond_value_exponent")
        use_config_exp = True
    else:
        old_exponent = jm_single().config.get("POLICY", "bond_value_exponent")
        jm_single().config.set("POLICY", "bond_value_exponent", str(exponent))
        use_config_exp = False
    if orderbook:
        bond_values = [fb["bond_value"] for fb in orderbook["fidelitybonds"]]
        bonds_sum = sum(bond_values)
        percentiles = quantiles(bond_values, n=100, method="inclusive")

    parameters = {
        "amount": amount,
        "confirm_time": confirm_time,
        "current_time": current_time,
        "interest": interest,
        "exponent": exponent,
    }
    locktime = get_next_locktime(datetime.fromtimestamp(current_time))
    results = []
    for _ in range(months):
        fb_value = FidelityBondMixin.calculate_timelocked_fidelity_bond_value(
            amount,
            confirm_time,
            locktime.timestamp(),
            current_time,
            interest,
        )
        result = {"locktime": locktime.timestamp(),
                  "value": fb_value}
        if orderbook:
            result["weight"] = fb_value / (bonds_sum + fb_value)
            result["percentile"] = 100 - bisect_left(percentiles, fb_value)
        results.append(result)
        locktime = get_next_locktime(locktime)
    if not use_config_exp:
        # We don't want the modified exponent value to persist in memory, so we reset to whatever it was before
        jm_single().config.set("POLICY", "bond_value_exponent", old_exponent)
    return parameters, results
    def create_orderbook_table(self, btc_unit, rel_unit):
        result = ''
        try:
            self.taker.dblock.acquire(True)
            rows = self.taker.db.execute('SELECT * FROM orderbook;').fetchall()
        finally:
            self.taker.dblock.release()
        if not rows:
            return 0, result
        rows = [o for o in rows if o["ordertype"] in filtered_offername_list]

        if jm_single().bc_interface == None:
            for row in rows:
                row["bondvalue"] = "No data"
        else:
            blocks = jm_single().bc_interface.get_current_block_height()
            mediantime = jm_single().bc_interface.get_best_block_median_time()
            interest_rate = get_interest_rate()
            for row in rows:
                with self.taker.dblock:
                    fbond_data = self.taker.db.execute(
                        "SELECT * FROM fidelitybonds WHERE counterparty=?;",
                        (row["counterparty"], )).fetchall()
                if len(fbond_data) == 0:
                    row["bondvalue"] = "0"
                    continue
                else:
                    try:
                        parsed_bond = FidelityBondProof.parse_and_verify_proof_msg(
                            fbond_data[0]["counterparty"],
                            fbond_data[0]["takernick"], fbond_data[0]["proof"])
                    except ValueError:
                        row["bondvalue"] = "0"
                        continue
                    utxo_data = FidelityBondMixin.get_validated_timelocked_fidelity_bond_utxo(
                        parsed_bond.utxo, parsed_bond.utxo_pub,
                        parsed_bond.locktime, parsed_bond.cert_expiry, blocks)
                    if utxo_data == None:
                        row["bondvalue"] = "0"
                        continue
                    bond_value = FidelityBondMixin.calculate_timelocked_fidelity_bond_value(
                        utxo_data["value"],
                        jm_single().bc_interface.get_block_time(
                            jm_single().bc_interface.get_block_hash(
                                blocks - utxo_data["confirms"] + 1)),
                        parsed_bond.locktime, mediantime, interest_rate)
                    row["bondvalue"] = satoshi_to_unit_power(
                        bond_value, 2 * unit_to_power[btc_unit])

        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), ('bondvalue',
                                                             do_nothing))

        # somewhat complex sorting to sort by cjfee but with swabsoffers on top

        def orderby_cmp(x, y):
            if x['ordertype'] == y['ordertype']:
                return cmp(Decimal(x['cjfee']), Decimal(y['cjfee']))
            return cmp(offername_list.index(x['ordertype']),
                       offername_list.index(y['ordertype']))

        for o in sorted(rows, key=cmp_to_key(orderby_cmp)):
            result += ' <tr>\n'
            for key, displayer in order_keys_display:
                result += '  <td>' + displayer(o[key], o, btc_unit,
                                               rel_unit) + '</td>\n'
            result += ' </tr>\n'
        return len(rows), result