diff --git a/scripts/obwatch/ob-watcher.py b/scripts/obwatch/ob-watcher.py index 72a46738d..60da71d5a 100755 --- a/scripts/obwatch/ob-watcher.py +++ b/scripts/obwatch/ob-watcher.py @@ -115,6 +115,7 @@ def create_offerbook_table_heading(btc_unit, rel_unit): col.format('txfee', 'Miner Fee Contribution / ' + btc_unit), col.format('minsize', 'Minimum Size / ' + btc_unit), col.format('maxsize', 'Maximum Size / ' + btc_unit), + col.format('minimum_tx_fee_rate', 'Minimum feerate (sat/vB)'), col.format('bondvalue', 'Bond value / ' + btc_unit + '' + bond_exponent + '') ]) + ' ' return tableheading diff --git a/scripts/yg-privacyenhanced.py b/scripts/yg-privacyenhanced.py index 4c4e8404b..4cebd6602 100755 --- a/scripts/yg-privacyenhanced.py +++ b/scripts/yg-privacyenhanced.py @@ -90,7 +90,9 @@ def create_my_orders(self): 'minsize': randomize_minsize, 'maxsize': randomize_maxsize, 'txfee': randomize_txfee, - 'cjfee': str(randomize_cjfee)} + 'cjfee': str(randomize_cjfee), + # TODO: add some randomization factor here? + 'minimum_tx_fee_rate': self.minimum_tx_fee_rate} # sanity check assert order['minsize'] >= jm_single().DUST_THRESHOLD diff --git a/src/jmclient/blockchaininterface.py b/src/jmclient/blockchaininterface.py index 663dce800..0eec3d9db 100644 --- a/src/jmclient/blockchaininterface.py +++ b/src/jmclient/blockchaininterface.py @@ -42,9 +42,9 @@ def pushtx(self, txbin: bytes) -> bool: @abstractmethod def query_utxo_set(self, - txouts: Union[Tuple[bytes, int], List[Tuple[bytes, int]]], + txouts: Union[Tuple[bytes, int], Iterable[Tuple[bytes, int]]], includeconfs: bool = False, - include_mempool: bool = True) -> List[Optional[dict]]: + include_mempool: bool = True) -> List[dict]: """If txout is either (a) a single utxo in (txidbin, n) form, or a list of the same, returns, as a list for each txout item, the result of gettxout from the bitcoind rpc for those utxos; @@ -239,7 +239,7 @@ def _fee_per_kb_has_been_manually_set(self, tx_fees: int) -> bool: """ return tx_fees > 1000 - def estimate_fee_per_kb(self, tx_fees: int) -> int: + def estimate_fee_per_kb(self, tx_fees: int, randomize: bool = True) -> int: """ The argument tx_fees may be either a number of blocks target, for estimation of feerate by Core, or a number of satoshis per kilo-vbyte (see `_fee_per_kb_has_been_manually_set` for @@ -255,7 +255,11 @@ def estimate_fee_per_kb(self, tx_fees: int) -> int: # default to use if fees cannot be estimated fallback_fee = 10000 - tx_fees_factor = abs(jm_single().config.getfloat('POLICY', 'tx_fees_factor')) + if randomize: + tx_fees_factor = abs(jm_single().config.getfloat( + 'POLICY', 'tx_fees_factor')) + else: + tx_fees_factor = 0 mempoolminfee_in_sat = self._get_mempool_min_fee() # in case of error @@ -543,11 +547,12 @@ def pushtx(self, txbin: bytes) -> bool: return True def query_utxo_set(self, - txouts: Union[Tuple[bytes, int], List[Tuple[bytes, int]]], + txouts: Union[Tuple[bytes, int], Iterable[Tuple[bytes, int]]], includeconfs: bool = False, - include_mempool: bool = True) -> List[Optional[dict]]: - if not isinstance(txouts, list): + include_mempool: bool = True) -> List[dict]: + if isinstance(txouts, tuple): txouts = [txouts] + assert isinstance(txouts, Iterable) result = [] for txo in txouts: txo_hex = bintohex(txo[0]) @@ -802,9 +807,9 @@ def __init__(self, jsonRpc: JsonRpc, wallet_name: str) -> None: self.shutdown_signal = False self.destn_addr = self._rpc("getnewaddress", []) - def estimate_fee_per_kb(self, tx_fees: int) -> int: + def estimate_fee_per_kb(self, tx_fees: int, randomize: bool = True) -> int: if not self.absurd_fees: - return super().estimate_fee_per_kb(tx_fees) + return super().estimate_fee_per_kb(tx_fees, randomize) else: return jm_single().config.getint("POLICY", "absurd_fee_per_kb") + 100 diff --git a/src/jmclient/configure.py b/src/jmclient/configure.py index 14482ff8d..c39889559 100644 --- a/src/jmclient/configure.py +++ b/src/jmclient/configure.py @@ -519,6 +519,11 @@ def jm_single() -> AttributeDict: # [fraction, 0-1] / variance around all offer sizes. Ex: 500k minsize, 0.1 var = 450k-550k size_factor = 0.1 +# +minimum_tx_fee_rate = 1000 + +gaplimit = 6 + [SNICKER] # Any other value than 'true' will be treated as False, # and no SNICKER actions will be enabled in that case: diff --git a/src/jmclient/maker.py b/src/jmclient/maker.py index 43c0ddc06..a6586a3a7 100644 --- a/src/jmclient/maker.py +++ b/src/jmclient/maker.py @@ -15,7 +15,7 @@ jlog = get_log() class Maker(object): - def __init__(self, wallet_service): + def __init__(self, wallet_service: WalletService): self.active_orders = {} assert isinstance(wallet_service, WalletService) self.wallet_service = wallet_service @@ -28,6 +28,8 @@ def __init__(self, wallet_service): # not-enough-coins: self.sync_wait_loop.start(2.0, now=False) self.aborted = False + self.minimum_tx_fee_rate = jm_single().config.getint( + "YIELDGENERATOR", "minimum_tx_fee_rate") def try_to_create_my_orders(self): """Because wallet syncing is not synchronous(!), @@ -187,6 +189,16 @@ def verify_unsigned_tx(self, tx, offerinfo): """ tx_utxo_set = set((x.prevout.hash[::-1], x.prevout.n) for x in tx.vin) + if self.minimum_tx_fee_rate > 1000: + tx_inp_data = jm_single().bc_interface.query_utxo_set(tx_utxo_set) + total_input_value_sum = 0 + for utxo in tx_inp_data: + total_input_value_sum = total_input_value_sum + utxo["value"] + tx_fee = total_input_value_sum - btc.tx_total_outputs_value(tx) + tx_fee_rate = tx_fee / btc.tx_vsize(tx) * 1000 + if tx_fee_rate < self.minimum_tx_fee_rate: + return (False, "tx feerate below configured minimum tx feerate") + utxos = offerinfo["utxos"] cjaddr = offerinfo["cjaddr"] cjaddr_script = btc.CCoinAddress(cjaddr).to_scriptPubKey() diff --git a/src/jmclient/support.py b/src/jmclient/support.py index e8641a095..d7bb4b1cc 100644 --- a/src/jmclient/support.py +++ b/src/jmclient/support.py @@ -1,13 +1,15 @@ -from functools import reduce import random -from jmbase.support import get_log from decimal import Decimal -from .configure import get_bondless_makers_allowance - +from functools import reduce from math import exp +from typing import Callable, List, Optional, Tuple + +from jmbase.support import get_log +from .configure import get_bondless_makers_allowance, jm_single + ORDER_KEYS = ['counterparty', 'oid', 'ordertype', 'minsize', 'maxsize', 'txfee', - 'cjfee'] + 'cjfee', 'minimum_tx_fee_rate'] log = get_log() @@ -45,7 +47,7 @@ def rand_exp_array(lamda, n): return [random.expovariate(1.0 / lamda) for _ in range(n)] -def rand_weighted_choice(n, p_arr): +def rand_weighted_choice(n: int, p_arr: list) -> int: """ Choose a value in 0..n-1 with the choice weighted by the probabilities @@ -177,7 +179,7 @@ def calc_cj_fee(ordertype, cjfee, cj_amount): return real_cjfee -def weighted_order_choose(orders, n): +def weighted_order_choose(orders: List[dict], n: int) -> dict: """ Algorithm for choosing the weighting function it is an exponential @@ -208,18 +210,18 @@ def weighted_order_choose(orders, n): return orders[chosen_order_index] -def random_under_max_order_choose(orders, n): +def random_under_max_order_choose(orders: List[dict], n: int) -> dict: # orders are already pre-filtered for max_cj_fee return random.choice(orders) -def cheapest_order_choose(orders, n): +def cheapest_order_choose(orders: List[dict], n: int) -> dict: """ Return the cheapest order from the orders. """ return orders[0] -def fidelity_bond_weighted_order_choose(orders, n): +def fidelity_bond_weighted_order_choose(orders: List[dict], n: int) -> dict: """ choose orders based on fidelity bond for improved sybil resistance @@ -247,19 +249,31 @@ def check_max_fee(fee): return check_max_fee -def choose_orders(offers, cj_amount, n, chooseOrdersBy, ignored_makers=None, - pick=False, allowed_types=["sw0reloffer", "sw0absoffer"], - max_cj_fee=(1, float('inf'))): +def choose_orders(offers: List[dict], + cj_amount: int, + num_counterparties: int, + chooseOrdersBy: Callable[[List[dict], int], dict], + ignored_makers: Optional[List[str]] = None, + pick: bool = False, + allowed_types: List[str] = ["sw0reloffer", "sw0absoffer"], + max_cj_fee: Tuple = (1, float('inf'))) -> Tuple[Optional[dict], int]: is_within_max_limits = _get_is_within_max_limits( max_cj_fee[0], max_cj_fee[1], cj_amount) if ignored_makers is None: ignored_makers = [] + fee_per_kb = jm_single().bc_interface.estimate_fee_per_kb( + jm_single().config.getint("POLICY", "tx_fees"), randomize=False) #Filter ignored makers and inappropriate amounts orders = [o for o in offers if o['counterparty'] not in ignored_makers] orders = [o for o in orders if o['minsize'] < cj_amount] orders = [o for o in orders if o['maxsize'] > cj_amount] #Filter those not using wished-for offertypes orders = [o for o in orders if o["ordertype"] in allowed_types] + # provide a message to the user to let them know that there are offers not + # available due to too-low feerate: + orders_fee_too_low = [o for o in orders if o["minimum_tx_fee_rate"] > fee_per_kb] + #Filter those not accepting our tx feerate + orders = [o for o in orders if o["minimum_tx_fee_rate"] <= fee_per_kb] orders_fees = [] for o in orders: @@ -268,10 +282,18 @@ def choose_orders(offers, cj_amount, n, chooseOrdersBy, ignored_makers=None, orders_fees.append((o, fee)) counterparties = set(o['counterparty'] for o, f in orders_fees) - if n > len(counterparties): + if num_counterparties > len(counterparties): log.warn(('ERROR not enough liquidity in the orderbook n=%d ' 'suitable-counterparties=%d amount=%d totalorders=%d') % - (n, len(counterparties), cj_amount, len(orders_fees))) + (num_counterparties, len(counterparties), cj_amount, + len(orders_fees))) + if len(orders_fee_too_low) > 0: + log.warn(f"Note that {len(orders_fee_too_low)} counterparty " + "offer(s) were rejected because they demanded too " + "high a transaction fee in range from " + "{min(orders_fee_too_low, key=lambda f: 'minimum_tx_fee_rate')} to " + "{max(orders_fee_too_low, key=lambda f: 'minimum_tx_fee_rate')}.") + log.debug(str(orders_fee_too_low)) # TODO handle not enough liquidity better, maybe an Exception return None, 0 """ @@ -294,8 +316,9 @@ def choose_orders(offers, cj_amount, n, chooseOrdersBy, ignored_makers=None, ])) total_cj_fee = 0 chosen_orders = [] - for i in range(n): - chosen_order, chosen_fee = chooseOrdersBy(orders_fees, n) + for i in range(num_counterparties): + chosen_order, chosen_fee = chooseOrdersBy(orders_fees, + num_counterparties) # remove all orders from that same counterparty # only needed if offers are manually picked orders_fees = [o @@ -308,14 +331,14 @@ def choose_orders(offers, cj_amount, n, chooseOrdersBy, ignored_makers=None, return result, total_cj_fee -def choose_sweep_orders(offers, - total_input_value, - total_txfee, - n, - chooseOrdersBy, - ignored_makers=None, - allowed_types=['sw0reloffer', 'sw0absoffer'], - max_cj_fee=(1, float('inf'))): +def choose_sweep_orders(offers: List[dict], + total_input_value: int, + total_txfee: int, + num_counterparties: int, + chooseOrdersBy: Callable[[List[dict], int], dict], + ignored_makers: Optional[List[str]] = None, + allowed_types: List[str] = ['sw0reloffer', 'sw0absoffer'], + max_cj_fee: Tuple = (1, float('inf'))) -> Tuple[Optional[dict], int, int]: """ choose an order given that we want to be left with no change i.e. sweep an entire group of utxos @@ -332,7 +355,7 @@ def choose_sweep_orders(offers, if ignored_makers is None: ignored_makers = [] - def calc_zero_change_cj_amount(ordercombo): + def calc_zero_change_cj_amount(ordercombo: List[dict]) -> Tuple[int, int]: sumabsfee = 0 sumrelfee = Decimal('0') sumtxfee_contribution = 0 @@ -352,14 +375,18 @@ def calc_zero_change_cj_amount(ordercombo): cjamount = int(cjamount.quantize(Decimal(1))) return cjamount, int(sumabsfee + sumrelfee * cjamount) + fee_per_kb = jm_single().bc_interface.estimate_fee_per_kb( + jm_single().config.getint("POLICY", "tx_fees"), randomize=False) log.debug('choosing sweep orders for total_input_value = ' + str( - total_input_value) + ' n=' + str(n)) + total_input_value) + ' n=' + str(num_counterparties)) offers = [o for o in offers if o["ordertype"] in allowed_types] #Filter ignored makers and inappropriate amounts offers = [o for o in offers if o['counterparty'] not in ignored_makers] offers = [o for o in offers if o['minsize'] < total_input_value] # while we do not know the exact cj value yet, we can approximate a ceiling: offers = [o for o in offers if o['maxsize'] > (total_input_value - total_txfee)] + #Filter those not accepting our tx feerate + offers = [o for o in offers if o["minimum_tx_fee_rate"] <= fee_per_kb] log.debug('orderlist = \n' + '\n'.join([str(o) for o in offers])) orders_fees = [(o, calc_cj_fee(o['ordertype'], o['cjfee'], @@ -376,13 +403,14 @@ def calc_zero_change_cj_amount(ordercombo): if is_within_max_limits(v[1])).values(), key=feekey) chosen_orders = [] - while len(chosen_orders) < n: - for i in range(n - len(chosen_orders)): - if len(orders_fees) < n - len(chosen_orders): + while len(chosen_orders) < num_counterparties: + for i in range(num_counterparties - len(chosen_orders)): + if len(orders_fees) < num_counterparties - len(chosen_orders): log.debug('ERROR not enough liquidity in the orderbook') # TODO handle not enough liquidity better, maybe an Exception return None, 0, 0 - chosen_order, chosen_fee = chooseOrdersBy(orders_fees, n) + chosen_order, chosen_fee = chooseOrdersBy(orders_fees, + num_counterparties) log.debug('chosen = ' + str(chosen_order)) # remove all orders from that same counterparty orders_fees = [ diff --git a/src/jmclient/yieldgenerator.py b/src/jmclient/yieldgenerator.py index 4f3c6e594..a05920368 100644 --- a/src/jmclient/yieldgenerator.py +++ b/src/jmclient/yieldgenerator.py @@ -89,8 +89,6 @@ def __init__(self, wallet_service, offerconfig): self.size_factor = offerconfig super().__init__(wallet_service) - - def create_my_orders(self): mix_balance = self.get_available_mixdepths() if len([b for m, b in mix_balance.items() if b > 0]) == 0: @@ -113,7 +111,8 @@ def create_my_orders(self): 'maxsize': mix_balance[max_mix] - max( jm_single().DUST_THRESHOLD, self.txfee_contribution), 'txfee': self.txfee_contribution, - 'cjfee': f} + 'cjfee': f, + 'minimum_tx_fee_rate': self.minimum_tx_fee_rate} # sanity check assert order['minsize'] >= 0 diff --git a/src/jmdaemon/message_channel.py b/src/jmdaemon/message_channel.py index 3734ccea9..a35546471 100644 --- a/src/jmdaemon/message_channel.py +++ b/src/jmdaemon/message_channel.py @@ -293,7 +293,8 @@ def announce_orders(self, orderlist, nick, fidelity_bond_proof_msg, new_mc): privmsg, on a specific mc. Fidelity bonds can only be announced over privmsg, nick must be nonNone """ - order_keys = ['oid', 'minsize', 'maxsize', 'txfee', 'cjfee'] + order_keys = ['oid', 'minsize', 'maxsize', 'txfee', 'cjfee', + 'minimum_tx_fee_rate'] orderlines = [] for order in orderlist: orderlines.append(COMMAND_PREFIX + order['ordertype'] + \ @@ -545,7 +546,7 @@ def on_nick_change_trigger(self, new_nick): self.on_nick_change(new_nick) def on_order_seen_trigger(self, mc, counterparty, oid, ordertype, minsize, - maxsize, txfee, cjfee): + maxsize, txfee, cjfee, minimum_tx_fee_rate): """This is the entry point into private messaging. Hence, it fixes for the rest of the conversation, which message channel the bots are going to communicate over @@ -564,7 +565,7 @@ def on_order_seen_trigger(self, mc, counterparty, oid, ordertype, minsize, self.active_channels[counterparty] = mc if self.on_order_seen: self.on_order_seen(counterparty, oid, ordertype, minsize, maxsize, - txfee, cjfee) + txfee, cjfee, minimum_tx_fee_rate) # orderbook watcher commands def register_orderbookwatch_callbacks(self, @@ -785,9 +786,11 @@ def check_for_orders(self, nick, _chunks): maxsize = _chunks[3] txfee = _chunks[4] cjfee = _chunks[5] + minimum_tx_fee_rate = _chunks[6] if len(_chunks) > 6 else 0 if self.on_order_seen: self.on_order_seen(self, counterparty, oid, ordertype, - minsize, maxsize, txfee, cjfee) + minsize, maxsize, txfee, cjfee, + minimum_tx_fee_rate) except IndexError as e: log.debug(e) log.debug('index error parsing chunks, possibly malformed ' diff --git a/src/jmdaemon/orderbookwatch.py b/src/jmdaemon/orderbookwatch.py index 796675945..2cc5ffe1d 100644 --- a/src/jmdaemon/orderbookwatch.py +++ b/src/jmdaemon/orderbookwatch.py @@ -40,7 +40,8 @@ def set_msgchan(self, msgchan): self.dblock.acquire(True) self.db.execute("CREATE TABLE orderbook(counterparty TEXT, " "oid INTEGER, ordertype TEXT, minsize INTEGER, " - "maxsize INTEGER, txfee INTEGER, cjfee TEXT);") + "maxsize INTEGER, txfee INTEGER, cjfee TEXT, " + "minimum_tx_fee_rate INTEGER);") self.db.execute("CREATE TABLE fidelitybonds(counterparty TEXT, " "takernick TEXT, proof TEXT);"); finally: @@ -65,8 +66,10 @@ def on_set_topic(newtopic): print('=' * 60) joinmarket_alert[0] = alert - def on_order_seen(self, counterparty, oid, ordertype, minsize, maxsize, - txfee, cjfee): + def on_order_seen(self, counterparty: str, oid: int, ordertype: str, + minsize: int, maxsize: int, + txfee: int, cjfee: Integral, + minimum_tx_fee_rate: int) -> None: try: self.dblock.acquire(True) if int(oid) < 0 or int(oid) > sys.maxsize: @@ -112,9 +115,10 @@ def on_order_seen(self, counterparty, oid, ordertype, minsize, maxsize, " for an absoffer from " + counterparty) return self.db.execute( - 'INSERT INTO orderbook VALUES(?, ?, ?, ?, ?, ?, ?);', + 'INSERT INTO orderbook VALUES(?, ?, ?, ?, ?, ?, ?, ?);', (counterparty, oid, ordertype, minsize, maxsize, txfee, - str(Decimal(cjfee)))) # any parseable Decimal is a valid cjfee + str(Decimal(cjfee)), # any parseable Decimal is a valid cjfee + minimum_tx_fee_rate)) except InvalidOperation: log.debug("Got invalid cjfee: " + str(cjfee) + " from " + counterparty) except Exception as e: diff --git a/src/jmdaemon/protocol.py b/src/jmdaemon/protocol.py index eefe72d62..e78129012 100644 --- a/src/jmdaemon/protocol.py +++ b/src/jmdaemon/protocol.py @@ -5,25 +5,39 @@ nickname = None separator = " " -offertypes = {"reloffer": [(int, "oid"), (int, "minsize"), (int, "maxsize"), - (int, "txfee"), (float, "cjfee")], - "absoffer": [(int, "oid"), (int, "minsize"), (int, "maxsize"), - (int, "txfee"), (int, "cjfee")], - "swreloffer": [(int, "oid"), (int, "minsize"), (int, "maxsize"), - (int, "txfee"), (float, "cjfee")], - "swabsoffer": [(int, "oid"), (int, "minsize"), (int, "maxsize"), - (int, "txfee"), (int, "cjfee")], - "sw0reloffer": [(int, "oid"), (int, "minsize"), (int, "maxsize"), - (int, "txfee"), (float, "cjfee")], - "sw0absoffer": [(int, "oid"), (int, "minsize"), (int, "maxsize"), - (int, "txfee"), (int, "cjfee")]} +offertypes = { + "reloffer": [ + (int, "oid"), (int, "minsize"), (int, "maxsize"), + (int, "txfee"), (float, "cjfee"), (int, "minimum_tx_fee_rate") + ], + "absoffer": [ + (int, "oid"), (int, "minsize"), (int, "maxsize"), + (int, "txfee"), (int, "cjfee"), (int, "minimum_tx_fee_rate") + ], + "swreloffer": [ + (int, "oid"), (int, "minsize"), (int, "maxsize"), + (int, "txfee"), (float, "cjfee"), (int, "minimum_tx_fee_rate") + ], + "swabsoffer": [ + (int, "oid"), (int, "minsize"), (int, "maxsize"), + (int, "txfee"), (int, "cjfee"), (int, "minimum_tx_fee_rate") + ], + "sw0reloffer": [ + (int, "oid"), (int, "minsize"), (int, "maxsize"), + (int, "txfee"), (float, "cjfee"), (int, "minimum_tx_fee_rate") + ], + "sw0absoffer": [ + (int, "oid"), (int, "minsize"), (int, "maxsize"), + (int, "txfee"), (int, "cjfee"), (int, "minimum_tx_fee_rate") + ] +} offername_list = list(offertypes.keys()) fidelity_bond_cmd_list = ["tbond"] ORDER_KEYS = ['counterparty', 'oid', 'ordertype', 'minsize', 'maxsize', 'txfee', - 'cjfee'] + 'cjfee', 'minimum_tx_fee_rate'] COMMAND_PREFIX = '!' JOINMARKET_NICK_HEADER = 'J' diff --git a/test/jmclient/commontest.py b/test/jmclient/commontest.py index c383e7535..b1c1afaa5 100644 --- a/test/jmclient/commontest.py +++ b/test/jmclient/commontest.py @@ -170,7 +170,7 @@ def query_utxo_set(self, result.append(result_dict) return result - def estimate_fee_per_kb(self, tx_fees: int) -> int: + def estimate_fee_per_kb(self, tx_fees: int, randomize: bool = True) -> int: return 30000 diff --git a/test/jmclient/taker_test_data.py b/test/jmclient/taker_test_data.py index ec65a7754..7a9b700a5 100644 --- a/test/jmclient/taker_test_data.py +++ b/test/jmclient/taker_test_data.py @@ -1,16 +1,24 @@ #orderbook -t_orderbook = [{u'counterparty': u'J6FA1Gj7Ln4vSGne', u'ordertype': u'sw0reloffer', u'oid': 0, - u'minsize': 7500000, u'txfee': 1000, u'maxsize': 599972700, u'cjfee': u'0.0002'}, - {u'counterparty': u'J6CFffuuewjG44UJ', u'ordertype': u'sw0reloffer', u'oid': 0, - u'minsize': 7500000, u'txfee': 1000, u'maxsize': 599972700, u'cjfee': u'0.0002'}, - {u'counterparty': u'J65z23xdjxJjC7er', u'ordertype': u'sw0reloffer', u'oid': 0, - u'minsize': 7500000, u'txfee': 1000, u'maxsize': 599972700, u'cjfee': u'0.0002'}, - {u'counterparty': u'J64Ghp5PXCdY9H3t', u'ordertype': u'sw0reloffer', u'oid': 0, - u'minsize': 7500000, u'txfee': 1000, u'maxsize': 599972700, u'cjfee': u'0.0002'}, - {u'counterparty': u'J659UPUSLLjHJpaB', u'ordertype': u'sw0reloffer', u'oid': 0, - u'minsize': 7500000, u'txfee': 1000, u'maxsize': 599972700, u'cjfee': u'0.0002'}, - {u'counterparty': u'J6cBx1FwUVh9zzoO', u'ordertype': u'sw0reloffer', u'oid': 0, - u'minsize': 7500000, u'txfee': 1000, u'maxsize': 599972700, u'cjfee': u'0.0002'}] +t_orderbook = [ + {u'counterparty': u'J6FA1Gj7Ln4vSGne', u'ordertype': u'sw0reloffer', + u'oid': 0, u'minsize': 7500000, u'txfee': 1000, u'maxsize': 599972700, + u'cjfee': u'0.0002', u'minimum_tx_fee_rate': 0}, + {u'counterparty': u'J6CFffuuewjG44UJ', u'ordertype': u'sw0reloffer', + u'oid': 0, u'minsize': 7500000, u'txfee': 1000, u'maxsize': 599972700, + u'cjfee': u'0.0002', u'minimum_tx_fee_rate': 0}, + {u'counterparty': u'J65z23xdjxJjC7er', u'ordertype': u'sw0reloffer', + u'oid': 0, u'minsize': 7500000, u'txfee': 1000, u'maxsize': 599972700, + u'cjfee': u'0.0002', u'minimum_tx_fee_rate': 0}, + {u'counterparty': u'J64Ghp5PXCdY9H3t', u'ordertype': u'sw0reloffer', + u'oid': 0, u'minsize': 7500000, u'txfee': 1000, u'maxsize': 599972700, + u'cjfee': u'0.0002', u'minimum_tx_fee_rate': 0}, + {u'counterparty': u'J659UPUSLLjHJpaB', u'ordertype': u'sw0reloffer', + u'oid': 0, u'minsize': 7500000, u'txfee': 1000, u'maxsize': 599972700, + u'cjfee': u'0.0002', u'minimum_tx_fee_rate': 0}, + {u'counterparty': u'J6cBx1FwUVh9zzoO', u'ordertype': u'sw0reloffer', + u'oid': 0, u'minsize': 7500000, u'txfee': 1000, u'maxsize': 599972700, + u'cjfee': u'0.0002', u'minimum_tx_fee_rate': 0} +] t_dest_addr = "mvw1NazKDRbeNufFANqpYNAANafsMC2zVU" diff --git a/test/jmclient/test_support.py b/test/jmclient/test_support.py index dcb10758a..aa696d665 100644 --- a/test/jmclient/test_support.py +++ b/test/jmclient/test_support.py @@ -104,7 +104,8 @@ def test_choose_orders(): orderbook.append({u'counterparty': u'fake', u'ordertype': u'sw0absoffer', u'oid': 0, u'minsize': 7500000, u'txfee': 1000, - u'maxsize': 599972700, u'cjfee': 9000}) + u'maxsize': 599972700, u'cjfee': 9000, + u'minimum_tx_fee_rate': 0}) result, cjamount, total_fee = choose_sweep_orders(orderbook, 50000000, 30000, 7, cheapest_order_choose, diff --git a/test/jmclient/test_yieldgenerator.py b/test/jmclient/test_yieldgenerator.py index 54d3f9ccb..fb354536a 100644 --- a/test/jmclient/test_yieldgenerator.py +++ b/test/jmclient/test_yieldgenerator.py @@ -81,6 +81,7 @@ def test_abs_fee(self): self.assertEqual(yg.create_my_orders(), [ {'oid': 0, 'ordertype': 'swabsoffer', + 'minimum_tx_fee_rate': 1000, 'minsize': 100000, 'maxsize': 1999000, 'txfee': 1000, @@ -94,6 +95,7 @@ def test_rel_fee(self): self.assertEqual(yg.create_my_orders(), [ {'oid': 0, 'ordertype': 'sw0reloffer', + 'minimum_tx_fee_rate': 1000, 'minsize': 15000, 'maxsize': 1999000, 'txfee': 1000, @@ -107,6 +109,7 @@ def test_dust_threshold(self): self.assertEqual(yg.create_my_orders(), [ {'oid': 0, 'ordertype': 'swabsoffer', + 'minimum_tx_fee_rate': 1000, 'minsize': 100000, 'maxsize': 1999000, 'txfee': 10, diff --git a/test/jmdaemon/msgdata.py b/test/jmdaemon/msgdata.py index 0b3dc67b4..9db1db2a0 100644 --- a/test/jmdaemon/msgdata.py +++ b/test/jmdaemon/msgdata.py @@ -1,16 +1,36 @@ #orderbook -t_orderbook = [{u'counterparty': u'J5FA1Gj7Ln4vSGne', u'ordertype': u'reloffer', u'oid': 0, - u'minsize': 7500000, u'txfee': 1000, u'maxsize': 599972700, u'cjfee': u'0.0002'}, - {u'counterparty': u'J5CFffuuewjG44UJ', u'ordertype': u'reloffer', u'oid': 0, - u'minsize': 7500000, u'txfee': 1000, u'maxsize': 599972700, u'cjfee': u'0.0002'}, - {u'counterparty': u'J55z23xdjxJjC7er', u'ordertype': u'reloffer', u'oid': 0, - u'minsize': 7500000, u'txfee': 1000, u'maxsize': 599972700, u'cjfee': u'0.0002'}, - {u'counterparty': u'J54Ghp5PXCdY9H3t', u'ordertype': u'reloffer', u'oid': 0, - u'minsize': 7500000, u'txfee': 1000, u'maxsize': 599972700, u'cjfee': u'0.0002'}, - {u'counterparty': u'J559UPUSLLjHJpaB', u'ordertype': u'reloffer', u'oid': 0, - u'minsize': 7500000, u'txfee': 1000, u'maxsize': 599972700, u'cjfee': u'0.0002'}, - {u'counterparty': u'J5cBx1FwUVh9zzoO', u'ordertype': u'reloffer', u'oid': 0, - u'minsize': 7500000, u'txfee': 1000, u'maxsize': 599972700, u'cjfee': u'0.0002'}] +t_orderbook = [ + { + u'counterparty': u'J5FA1Gj7Ln4vSGne', u'ordertype': u'reloffer', + u'oid': 0, u'minsize': 7500000, u'txfee': 1000, + u'maxsize': 599972700, u'cjfee': u'0.0002', u'minimum_tx_fee_rate': 0 + }, + { + u'counterparty': u'J5CFffuuewjG44UJ', u'ordertype': u'reloffer', + u'oid': 0, u'minsize': 7500000, u'txfee': 1000, + u'maxsize': 599972700, u'cjfee': u'0.0002', u'minimum_tx_fee_rate': 0 + }, + { + u'counterparty': u'J55z23xdjxJjC7er', u'ordertype': u'reloffer', + u'oid': 0, u'minsize': 7500000, u'txfee': 1000, + u'maxsize': 599972700, u'cjfee': u'0.0002', u'minimum_tx_fee_rate': 0 + }, + { + u'counterparty': u'J54Ghp5PXCdY9H3t', u'ordertype': u'reloffer', + u'oid': 0, u'minsize': 7500000, u'txfee': 1000, + u'maxsize': 599972700, u'cjfee': u'0.0002', u'minimum_tx_fee_rate': 0 + }, + { + u'counterparty': u'J559UPUSLLjHJpaB', u'ordertype': u'reloffer', + u'oid': 0, u'minsize': 7500000, u'txfee': 1000, + u'maxsize': 599972700, u'cjfee': u'0.0002', u'minimum_tx_fee_rate': 0 + }, + { + u'counterparty': u'J5cBx1FwUVh9zzoO', u'ordertype': u'reloffer', + u'oid': 0, u'minsize': 7500000, u'txfee': 1000, + u'maxsize': 599972700, u'cjfee': u'0.0002', u'minimum_tx_fee_rate': 0 + } +] t_dest_addr = "mvw1NazKDRbeNufFANqpYNAANafsMC2zVU" diff --git a/test/jmdaemon/test_daemon_protocol.py b/test/jmdaemon/test_daemon_protocol.py index 925b6241d..a56741215 100644 --- a/test/jmdaemon/test_daemon_protocol.py +++ b/test/jmdaemon/test_daemon_protocol.py @@ -210,10 +210,10 @@ def __init__(self, factory): @JMRequestOffers.responder def on_JM_REQUEST_OFFERS(self): for o in t_orderbook: - #counterparty, oid, ordertype, minsize, maxsize,txfee, cjfee): self.on_order_seen(o["counterparty"], o["oid"], o["ordertype"], o["minsize"], o["maxsize"], - o["txfee"], o["cjfee"]) + o["txfee"], o["cjfee"], + o["minimum_tx_fee_rate"]) return super().on_JM_REQUEST_OFFERS() @JMInit.responder @@ -228,7 +228,8 @@ def on_JM_INIT(self, bcsource, network, chan_configs, minmakers, #note it must happen before callign set_msgchan for OrderbookWatch self.mcc.on_order_seen = None for c in [o['counterparty'] for o in t_orderbook]: - self.mcc.on_order_seen_trigger(mcs[0], c, "a", "b", "c", "d", "e", "f") + self.mcc.on_order_seen_trigger(mcs[0], c, "a", "b", "c", "d", + "e", "f", "g") OrderbookWatch.set_msgchan(self, self.mcc) #register taker-specific msgchan callbacks here self.mcc.register_taker_callbacks(self.on_error, self.on_pubkey, diff --git a/test/jmdaemon/test_orderbookwatch.py b/test/jmdaemon/test_orderbookwatch.py index 97e30f28b..d2840c0ae 100644 --- a/test/jmdaemon/test_orderbookwatch.py +++ b/test/jmdaemon/test_orderbookwatch.py @@ -57,36 +57,36 @@ def test_ob(badtopic): ob.on_set_topic(deprecated) @pytest.mark.parametrize( - "counterparty, oid, ordertype, minsize, maxsize, txfee, cjfee, expected", + "counterparty, oid, ordertype, minsize, maxsize, txfee, cjfee, minimum_tx_fee_rate, expected", [ #good absoffer - ("test", "0", "absoffer", "3000", "4000", "2", "300", True), + ("test", "0", "absoffer", "3000", "4000", "2", "300", "0", True), #good reloffer - ("test", "0", "reloffer", "3000", "4000", "2", "0.3", True), + ("test", "0", "reloffer", "3000", "4000", "2", "0.3", "0", True), #dusty minsize OK - ("test", "0", "reloffer", "1000", "4000", "2", "0.3", True), + ("test", "0", "reloffer", "1000", "4000", "2", "0.3", "0", True), #invalid oid - ("test", "-2", "reloffer", "3000", "4000", "2", "0.3", False), + ("test", "-2", "reloffer", "3000", "4000", "2", "0.3", "0", False), #invalid minsize - ("test", "2", "reloffer", "-3000", "4000", "2", "0.3", False), + ("test", "2", "reloffer", "-3000", "4000", "2", "0.3", "0", False), #invalid maxsize - ("test", "2", "reloffer", "3000", "2200000000000000", "2", "0.3", False), + ("test", "2", "reloffer", "3000", "2200000000000000", "2", "0.3", "0", False), #invalid txfee - ("test", "2", "reloffer", "3000", "4000", "-1", "0.3", False), + ("test", "2", "reloffer", "3000", "4000", "-1", "0.3", "0", False), #min bigger than max - ("test", "2", "reloffer", "4000", "3000", "2", "0.3", False), + ("test", "2", "reloffer", "4000", "3000", "2", "0.3", "0", False), #non-integer absoffer - ("test", "2", "absoffer", "3000", "4000", "2", "0.3", False), + ("test", "2", "absoffer", "3000", "4000", "2", "0.3", "0", False), #invalid syntax for cjfee - ("test", "2", "reloffer", "3000", "4000", "2", "0.-1", False), + ("test", "2", "reloffer", "3000", "4000", "2", "0.-1", "0", False), #invalid type for oid - ("test", "xxx", "reloffer", "3000", "4000", "2", "0.3", False), + ("test", "xxx", "reloffer", "3000", "4000", "2", "0.3", "0", False), ]) def test_order_seen_cancel(counterparty, oid, ordertype, minsize, maxsize, txfee, - cjfee, expected): + cjfee, minimum_tx_fee_rate, expected): ob = get_ob() ob.on_order_seen(counterparty, oid, ordertype, minsize, maxsize, - txfee, cjfee) + txfee, cjfee, minimum_tx_fee_rate) if expected: #offer should now be in the orderbook rows = ob.db.execute('SELECT * FROM orderbook;').fetchall() @@ -100,21 +100,48 @@ def test_order_seen_cancel(counterparty, oid, ordertype, minsize, maxsize, txfee def test_disconnect_leave(): ob = get_ob() - t_orderbook = [{u'counterparty': u'J5FA1Gj7Ln4vSGne', u'ordertype': u'reloffer', u'oid': 0, - u'minsize': 7500000, u'txfee': 1000, u'maxsize': 599972700, u'cjfee': u'0.0002'}, - {u'counterparty': u'J5CFffuuewjG44UJ', u'ordertype': u'reloffer', u'oid': 0, - u'minsize': 7500000, u'txfee': 1000, u'maxsize': 599972700, u'cjfee': u'0.0002'}, - {u'counterparty': u'J55z23xdjxJjC7er', u'ordertype': u'reloffer', u'oid': 0, - u'minsize': 7500000, u'txfee': 1000, u'maxsize': 599972700, u'cjfee': u'0.0002'}, - {u'counterparty': u'J54Ghp5PXCdY9H3t', u'ordertype': u'reloffer', u'oid': 0, - u'minsize': 7500000, u'txfee': 1000, u'maxsize': 599972700, u'cjfee': u'0.0002'}, - {u'counterparty': u'J559UPUSLLjHJpaB', u'ordertype': u'reloffer', u'oid': 0, - u'minsize': 7500000, u'txfee': 1000, u'maxsize': 599972700, u'cjfee': u'0.0002'}, - {u'counterparty': u'J5cBx1FwUVh9zzoO', u'ordertype': u'reloffer', u'oid': 0, - u'minsize': 7500000, u'txfee': 1000, u'maxsize': 599972700, u'cjfee': u'0.0002'}] + t_orderbook = [ + { + u'counterparty': u'J5FA1Gj7Ln4vSGne', u'ordertype': u'reloffer', + u'oid': 0, u'minsize': 7500000, u'txfee': 1000, + u'maxsize': 599972700, u'cjfee': u'0.0002', + u'minimum_tx_fee_rate': 0 + }, + { + u'counterparty': u'J5CFffuuewjG44UJ', u'ordertype': u'reloffer', + u'oid': 0, u'minsize': 7500000, u'txfee': 1000, + u'maxsize': 599972700, u'cjfee': u'0.0002', + u'minimum_tx_fee_rate': 0 + }, + { + u'counterparty': u'J55z23xdjxJjC7er', u'ordertype': u'reloffer', + u'oid': 0, u'minsize': 7500000, u'txfee': 1000, + u'maxsize': 599972700, u'cjfee': u'0.0002', + u'minimum_tx_fee_rate': 0 + }, + { + u'counterparty': u'J54Ghp5PXCdY9H3t', u'ordertype': u'reloffer', + u'oid': 0, u'minsize': 7500000, u'txfee': 1000, + u'maxsize': 599972700, u'cjfee': u'0.0002', + u'minimum_tx_fee_rate': 0 + }, + { + u'counterparty': u'J559UPUSLLjHJpaB', u'ordertype': u'reloffer', + u'oid': 0, u'minsize': 7500000, u'txfee': 1000, + u'maxsize': 599972700, u'cjfee': u'0.0002', + u'minimum_tx_fee_rate': 0 + }, + { + u'counterparty': u'J5cBx1FwUVh9zzoO', u'ordertype': u'reloffer', + u'oid': 0, u'minsize': 7500000, u'txfee': 1000, + u'maxsize': 599972700, u'cjfee': u'0.0002', + u'minimum_tx_fee_rate': 0 + } + ] for o in t_orderbook: ob.on_order_seen(o['counterparty'], o['oid'], o['ordertype'], - o['minsize'], o['maxsize'], o['txfee'], o['cjfee']) + o['minsize'], o['maxsize'], o['txfee'], + o['cjfee'], o['minimum_tx_fee_rate']) rows = ob.db.execute('SELECT * FROM orderbook;').fetchall() orderbook = [dict([(k, o[k]) for k in ORDER_KEYS]) for o in rows] assert len(orderbook) == 6