diff --git a/api/logics.py b/api/logics.py index 30629347..51ec07af 100644 --- a/api/logics.py +++ b/api/logics.py @@ -9,7 +9,7 @@ from django.utils import timezone from api.lightning.node import LNNode from api.models import Currency, LNPayment, MarketTick, OnchainPayment, Order from api.tasks import send_devfund_donation, send_notification -from api.utils import validate_onchain_address +from api.utils import get_minning_fee, validate_onchain_address from chat.models import Message FEE = float(config("FEE")) @@ -640,10 +640,7 @@ class Logics: ): # Not enough onchain balance to commit for this swap. return False - suggested_mining_fee_rate = LNNode.estimate_fee( - amount_sats=preliminary_amount, - target_conf=config("SUGGESTED_TARGET_CONF", cast=int, default=2), - )["mining_fee_rate"] + suggested_mining_fee_rate = get_minning_fee("suggested", preliminary_amount) # Hardcap mining fee suggested at 1000 sats/vbyte if suggested_mining_fee_rate > 1000: @@ -795,10 +792,7 @@ class Logics: num_satoshis = cls.payout_amount(order, user)[1]["invoice_amount"] if mining_fee_rate: # not a valid mining fee - min_mining_fee_rate = LNNode.estimate_fee( - amount_sats=num_satoshis, - target_conf=config("MINIMUM_TARGET_CONF", cast=int, default=24), - )["mining_fee_rate"] + min_mining_fee_rate = get_minning_fee("minimum", num_satoshis) min_mining_fee_rate = max(2, min_mining_fee_rate) diff --git a/api/tasks.py b/api/tasks.py index fa3cdb9f..aa8afe18 100644 --- a/api/tasks.py +++ b/api/tasks.py @@ -98,15 +98,9 @@ def send_devfund_donation(order_id, proceeds, reason): from api.lightning.node import LNNode from api.models import LNPayment, Order + from api.utils import get_devfund_pubkey - if config("NETWORK", cast=str) == "testnet": - target_pubkey = ( - "03ecb271b3e2e36f2b91c92c65bab665e5165f8cdfdada1b5f46cfdd3248c87fd6" - ) - else: - target_pubkey = ( - "02187352cc4b1856b9604e0a79e1bc9b301be7e0c14acbbb8c29f7051d507127d7" - ) + target_pubkey = get_devfund_pubkey(config("NETWORK", cast=str)) order = Order.objects.get(id=order_id) coordinator_alias = config("COORDINATOR_ALIAS", cast=str, default="NoAlias") diff --git a/api/utils.py b/api/utils.py index 4348892b..45b62f1a 100644 --- a/api/utils.py +++ b/api/utils.py @@ -70,6 +70,96 @@ def validate_onchain_address(address): return True, None +mining_fee = {} + + +@ring.dict(mining_fee, expire=60) # keeps in cache for 60 seconds +def get_minning_fee(priority: str, preliminary_amount: int) -> int: + """ + priority: (str) 'suggested' | 'minimum' + Fetches suggested and minimum mining fee rates from mempool.space + uses LND/CLN fee estimator as fallback. + + mempool.space response object: + { + fastestFee: 1, + halfHourFee: 1, + hourFee: 1, + economyFee: 1, + minimumFee: 1 + } + Where 'suggested' is 'fastestFee' and 'minimum' is 'economyFee' + """ + + from api.lightning.node import LNNode + + session = get_session() + mempool_url = ( + "http://mempoolhqx4isw62xs7abwphsq7ldayuidyx2v2oethdhhj6mlo2r6ad.onion" + if USE_TOR + else "https://mempool.space" + ) + api_path = "/api/v1/fees/recommended" + + try: + response = session.get(mempool_url + api_path) + response.raise_for_status() # Raises stored HTTPError, if one occurred + data = response.json() + if priority == "suggested": + value = data.get("fastestFee") + elif priority == "minimum": + value = data.get("economyFee") + else: + raise Exception( + "an error occurred", + "unexpected value for mining fee priority", + priority, + ) + + except Exception: + # Fetch mining fee from LND/CLN instance + if priority == "suggested": + target_conf = config("SUGGESTED_TARGET_CONF", cast=int, default=2) + if priority == "minimum": + target_conf = config("MINIMUM_TARGET_CONF", cast=int, default=24) + + value = LNNode.estimate_fee( + amount_sats=preliminary_amount, + target_conf=target_conf, + )["mining_fee_rate"] + + return value + + +devfund_pubkey = {} + + +@ring.dict(devfund_pubkey, expire=3600) # keeps in cache for 3600 seconds +def get_devfund_pubkey(network: str) -> str: + """ + network: (str) "mainnet" | "testnet"; + Fetches devfund pubkey from `main` branch in the repository + fallback to hardcoded pubkey + """ + + session = get_session() + url = "https://raw.githubusercontent.com/RoboSats/robosats/main/devfund_pubkey.json" + # hardcoded fallback + mainnet = "02187352cc4b1856b9604e0a79e1bc9b301be7e0c14acbbb8c29f7051d507127d7" + testnet = "03ecb271b3e2e36f2b91c92c65bab665e5165f8cdfdada1b5f46cfdd3248c87fd6" + + try: + response = session.get(url) + response.raise_for_status() # Raises stored HTTPError, if one occurred + value = response.json().get(network) + if len(value) == 66: + return value + except Exception as e: + print(e) + + return mainnet if network == "mainnet" else testnet + + market_cache = {} diff --git a/devfund_pubkey.json b/devfund_pubkey.json new file mode 100644 index 00000000..92d8c667 --- /dev/null +++ b/devfund_pubkey.json @@ -0,0 +1,4 @@ +{ + "mainnet": "02187352cc4b1856b9604e0a79e1bc9b301be7e0c14acbbb8c29f7051d507127d7", + "testnet": "03ecb271b3e2e36f2b91c92c65bab665e5165f8cdfdada1b5f46cfdd3248c87fd6" +}