From 3d3da78f8aab2ce7d2b304f1f503b6edddb41347 Mon Sep 17 00:00:00 2001 From: Lint Action Date: Thu, 20 Oct 2022 09:56:10 +0000 Subject: [PATCH] Fix code style issues with Black --- api/admin.py | 172 +++-- api/lightning/node.py | 205 +++--- api/logics.py | 671 +++++++++++--------- api/management/commands/clean_orders.py | 11 +- api/management/commands/follow_invoices.py | 37 +- api/management/commands/telegram_watcher.py | 42 +- api/messages.py | 91 +-- api/models.py | 474 +++++++------- api/nick_generator/dicts/en/adjectives.py | 3 +- api/nick_generator/nick_generator.py | 37 +- api/nick_generator/utils.py | 4 +- api/serializers.py | 369 +++++------ api/tasks.py | 110 ++-- api/urls.py | 27 +- api/utils.py | 88 +-- api/views.py | 429 +++++++------ chat/admin.py | 12 +- chat/consumers.py | 130 ++-- chat/models.py | 49 +- chat/routing.py | 3 +- chat/serializers.py | 19 +- chat/tasks.py | 25 +- chat/views.py | 123 ++-- control/admin.py | 4 +- control/apps.py | 4 +- control/models.py | 110 +++- control/tasks.py | 143 +++-- robosats/__init__.py | 2 +- robosats/celery/__init__.py | 10 +- robosats/routing.py | 18 +- robosats/settings.py | 72 +-- 31 files changed, 1916 insertions(+), 1578 deletions(-) diff --git a/api/admin.py b/api/admin.py index 509718ec..7f80632d 100644 --- a/api/admin.py +++ b/api/admin.py @@ -5,6 +5,7 @@ from django.contrib.auth.admin import UserAdmin from api.models import OnchainPayment, Order, LNPayment, Profile, MarketTick, Currency from api.logics import Logics from statistics import median + admin.site.unregister(Group) admin.site.unregister(User) @@ -12,10 +13,11 @@ admin.site.unregister(User) class ProfileInline(admin.StackedInline): model = Profile can_delete = False - fields = ("avatar_tag", ) + fields = ("avatar_tag",) readonly_fields = ["avatar_tag"] show_change_link = True + # extended users with avatars @admin.register(User) class EUserAdmin(AdminChangeLinksMixin, UserAdmin): @@ -30,14 +32,13 @@ class EUserAdmin(AdminChangeLinksMixin, UserAdmin): "is_staff", ) list_display_links = ("id", "username") - change_links = ( - "profile", - ) - ordering = ("-id", ) + change_links = ("profile",) + ordering = ("-id",) def avatar_tag(self, obj): return obj.profile.avatar_tag() + @admin.register(Order) class OrderAdmin(AdminChangeLinksMixin, admin.ModelAdmin): list_display = ( @@ -79,19 +80,34 @@ class OrderAdmin(AdminChangeLinksMixin, admin.ModelAdmin): "taker_bond", "trade_escrow", ) - list_filter = ("is_disputed", "is_fiat_sent", "is_swap","type", "currency", "status") - search_fields = ["id","amount","min_amount","max_amount"] + list_filter = ( + "is_disputed", + "is_fiat_sent", + "is_swap", + "type", + "currency", + "status", + ) + search_fields = ["id", "amount", "min_amount", "max_amount"] - actions = ['maker_wins', 'taker_wins', 'return_everything','compite_median_trade_time'] + actions = [ + "maker_wins", + "taker_wins", + "return_everything", + "compite_median_trade_time", + ] - @admin.action(description='Solve dispute: maker wins') + @admin.action(description="Solve dispute: maker wins") def maker_wins(self, request, queryset): - ''' + """ Solves a dispute on favor of the maker. Adds Sats to compensations (earned_rewards) of the maker profile. - ''' + """ for order in queryset: - if order.status in [Order.Status.DIS, Order.Status.WFR] and order.is_disputed: + if ( + order.status in [Order.Status.DIS, Order.Status.WFR] + and order.is_disputed + ): own_bond_sats = order.maker_bond.num_satoshis if Logics.is_buyer(order, order.maker): if order.is_swap: @@ -105,19 +121,30 @@ class OrderAdmin(AdminChangeLinksMixin, admin.ModelAdmin): order.maker.profile.earned_rewards = own_bond_sats + trade_sats order.maker.profile.save() order.save() - self.message_user(request,f"Dispute of order {order.id} solved successfully on favor of the maker", messages.SUCCESS) + self.message_user( + request, + f"Dispute of order {order.id} solved successfully on favor of the maker", + messages.SUCCESS, + ) else: - self.message_user(request,f"Order {order.id} is not in a disputed state", messages.ERROR) + self.message_user( + request, + f"Order {order.id} is not in a disputed state", + messages.ERROR, + ) - @admin.action(description='Solve dispute: taker wins') + @admin.action(description="Solve dispute: taker wins") def taker_wins(self, request, queryset): - ''' + """ Solves a dispute on favor of the taker. Adds Sats to compensations (earned_rewards) of the taker profile. - ''' + """ for order in queryset: - if order.status in [Order.Status.DIS, Order.Status.WFR] and order.is_disputed: + if ( + order.status in [Order.Status.DIS, Order.Status.WFR] + and order.is_disputed + ): own_bond_sats = order.maker_bond.num_satoshis if Logics.is_buyer(order, order.taker): if order.is_swap: @@ -131,56 +158,90 @@ class OrderAdmin(AdminChangeLinksMixin, admin.ModelAdmin): order.taker.profile.earned_rewards = own_bond_sats + trade_sats order.taker.profile.save() order.save() - self.message_user(request,f"Dispute of order {order.id} solved successfully on favor of the taker", messages.SUCCESS) + self.message_user( + request, + f"Dispute of order {order.id} solved successfully on favor of the taker", + messages.SUCCESS, + ) else: - self.message_user(request,f"Order {order.id} is not in a disputed state", messages.ERROR) + self.message_user( + request, + f"Order {order.id} is not in a disputed state", + messages.ERROR, + ) - @admin.action(description='Solve dispute: return everything') + @admin.action(description="Solve dispute: return everything") def return_everything(self, request, queryset): - ''' + """ Solves a dispute by pushing back every bond and escrow to their sender. - ''' + """ for order in queryset: - if order.status in [Order.Status.DIS, Order.Status.WFR] and order.is_disputed: - order.maker_bond.sender.profile.earned_rewards += order.maker_bond.num_satoshis + if ( + order.status in [Order.Status.DIS, Order.Status.WFR] + and order.is_disputed + ): + order.maker_bond.sender.profile.earned_rewards += ( + order.maker_bond.num_satoshis + ) order.maker_bond.sender.profile.save() - order.taker_bond.sender.profile.earned_rewards += order.taker_bond.num_satoshis + order.taker_bond.sender.profile.earned_rewards += ( + order.taker_bond.num_satoshis + ) order.taker_bond.sender.profile.save() - order.trade_escrow.sender.profile.earned_rewards += order.trade_escrow.num_satoshis + order.trade_escrow.sender.profile.earned_rewards += ( + order.trade_escrow.num_satoshis + ) order.trade_escrow.sender.profile.save() order.status = Order.Status.CCA order.save() - self.message_user(request,f"Dispute of order {order.id} solved successfully, everything returned as compensations", messages.SUCCESS) + self.message_user( + request, + f"Dispute of order {order.id} solved successfully, everything returned as compensations", + messages.SUCCESS, + ) else: - self.message_user(request,f"Order {order.id} is not in a disputed state", messages.ERROR) + self.message_user( + request, + f"Order {order.id} is not in a disputed state", + messages.ERROR, + ) - @admin.action(description='Compute median trade completion time') + @admin.action(description="Compute median trade completion time") def compite_median_trade_time(self, request, queryset): - ''' - Computes the median time from an order taken to finishing + """ + Computes the median time from an order taken to finishing successfully for the set of selected orders. - ''' + """ times = [] for order in queryset: if order.contract_finalization_time: timedelta = order.contract_finalization_time - order.last_satoshis_time times.append(timedelta.total_seconds()) - + if len(times) > 0: median_time_secs = median(times) - mins = int(median_time_secs/60) - secs = int(median_time_secs - mins*60) - self.message_user(request, f"The median time to complete the trades is {mins}m {secs}s", messages.SUCCESS) + mins = int(median_time_secs / 60) + secs = int(median_time_secs - mins * 60) + self.message_user( + request, + f"The median time to complete the trades is {mins}m {secs}s", + messages.SUCCESS, + ) else: - self.message_user(request, "There is no successfully finished orders in the selection", messages.ERROR) - + self.message_user( + request, + "There is no successfully finished orders in the selection", + messages.ERROR, + ) + def amt(self, obj): if obj.has_range and obj.amount == None: - return str(float(obj.min_amount))+"-"+ str(float(obj.max_amount)) + return str(float(obj.min_amount)) + "-" + str(float(obj.max_amount)) else: - return float(obj.amount) + return float(obj.amount) + @admin.register(LNPayment) class LNPaymentAdmin(AdminChangeLinksMixin, admin.ModelAdmin): @@ -210,8 +271,15 @@ class LNPaymentAdmin(AdminChangeLinksMixin, admin.ModelAdmin): "order_paid_LN", ) list_filter = ("type", "concept", "status") - ordering = ("-expires_at", ) - search_fields = ["payment_hash","num_satoshis","sender__username","receiver__username","description"] + ordering = ("-expires_at",) + search_fields = [ + "payment_hash", + "num_satoshis", + "sender__username", + "receiver__username", + "description", + ] + @admin.register(OnchainPayment) class OnchainPaymentAdmin(AdminChangeLinksMixin, admin.ModelAdmin): @@ -231,9 +299,10 @@ class OnchainPaymentAdmin(AdminChangeLinksMixin, admin.ModelAdmin): "balance", "order_paid_TX", ) - list_display_links = ("id","address", "concept") + list_display_links = ("id", "address", "concept") list_filter = ("concept", "status") - search_fields = ["address","num_satoshis","receiver__username","txid"] + search_fields = ["address", "num_satoshis", "receiver__username", "txid"] + @admin.register(Profile) class UserProfileAdmin(AdminChangeLinksMixin, admin.ModelAdmin): @@ -261,7 +330,7 @@ class UserProfileAdmin(AdminChangeLinksMixin, admin.ModelAdmin): list_display_links = ("avatar_tag", "id") change_links = ["user"] readonly_fields = ["avatar_tag"] - search_fields = ["user__username","id"] + search_fields = ["user__username", "id"] readonly_fields = ("public_key", "encrypted_private_key") @@ -270,13 +339,12 @@ class CurrencieAdmin(admin.ModelAdmin): list_display = ("id", "currency", "exchange_rate", "timestamp") list_display_links = ("id", "currency") readonly_fields = ("currency", "exchange_rate", "timestamp") - ordering = ("id", ) + ordering = ("id",) + @admin.register(MarketTick) class MarketTickAdmin(admin.ModelAdmin): - list_display = ("timestamp", "price", "volume", "premium", "currency", - "fee") - readonly_fields = ("timestamp", "price", "volume", "premium", "currency", - "fee") + list_display = ("timestamp", "price", "volume", "premium", "currency", "fee") + readonly_fields = ("timestamp", "price", "volume", "premium", "currency", "fee") list_filter = ["currency"] - ordering = ("-timestamp", ) + ordering = ("-timestamp",) diff --git a/api/lightning/node.py b/api/lightning/node.py index f4739ad4..1e83ee59 100644 --- a/api/lightning/node.py +++ b/api/lightning/node.py @@ -24,8 +24,9 @@ except: # Read macaroon from file or .env variable string encoded as base64 try: - MACAROON = open(os.path.join(config("LND_DIR"), config("MACAROON_path")), - "rb").read() + MACAROON = open( + os.path.join(config("LND_DIR"), config("MACAROON_path")), "rb" + ).read() except: MACAROON = b64decode(config("LND_MACAROON_BASE64")) @@ -49,13 +50,10 @@ class LNNode: payment_failure_context = { 0: "Payment isn't failed (yet)", - 1: - "There are more routes to try, but the payment timeout was exceeded.", - 2: - "All possible routes were tried and failed permanently. Or were no routes to the destination at all.", + 1: "There are more routes to try, but the payment timeout was exceeded.", + 2: "All possible routes were tried and failed permanently. Or were no routes to the destination at all.", 3: "A non-recoverable error has occured.", - 4: - "Payment details incorrect (unknown hash, invalid amt or invalid final cltv delta)", + 4: "Payment details incorrect (unknown hash, invalid amt or invalid final cltv delta)", 5: "Insufficient local balance.", } @@ -63,9 +61,9 @@ class LNNode: def decode_payreq(cls, invoice): """Decodes a lightning payment request (invoice)""" request = lnrpc.PayReqString(pay_req=invoice) - response = cls.lightningstub.DecodePayReq(request, - metadata=[("macaroon", - MACAROON.hex())]) + response = cls.lightningstub.DecodePayReq( + request, metadata=[("macaroon", MACAROON.hex())] + ) return response @classmethod @@ -73,46 +71,56 @@ class LNNode: """Returns estimated fee for onchain payouts""" # We assume segwit. Use robosats donation address as shortcut so there is no need of user inputs - request = lnrpc.EstimateFeeRequest(AddrToAmount={'bc1q3cpp7ww92n6zp04hv40kd3eyy5avgughx6xqnx':amount_sats}, - target_conf=target_conf, - min_confs=min_confs, - spend_unconfirmed=False) + request = lnrpc.EstimateFeeRequest( + AddrToAmount={"bc1q3cpp7ww92n6zp04hv40kd3eyy5avgughx6xqnx": amount_sats}, + target_conf=target_conf, + min_confs=min_confs, + spend_unconfirmed=False, + ) - response = cls.lightningstub.EstimateFee(request, - metadata=[("macaroon", - MACAROON.hex())]) + response = cls.lightningstub.EstimateFee( + request, metadata=[("macaroon", MACAROON.hex())] + ) - return {'mining_fee_sats': response.fee_sat, 'mining_fee_rate': response.sat_per_vbyte} + return { + "mining_fee_sats": response.fee_sat, + "mining_fee_rate": response.sat_per_vbyte, + } wallet_balance_cache = {} + @ring.dict(wallet_balance_cache, expire=10) # keeps in cache for 10 seconds @classmethod def wallet_balance(cls): """Returns onchain balance""" request = lnrpc.WalletBalanceRequest() - response = cls.lightningstub.WalletBalance(request, - metadata=[("macaroon", - MACAROON.hex())]) + response = cls.lightningstub.WalletBalance( + request, metadata=[("macaroon", MACAROON.hex())] + ) - return {'total_balance': response.total_balance, - 'confirmed_balance': response.confirmed_balance, - 'unconfirmed_balance': response.unconfirmed_balance} + return { + "total_balance": response.total_balance, + "confirmed_balance": response.confirmed_balance, + "unconfirmed_balance": response.unconfirmed_balance, + } channel_balance_cache = {} + @ring.dict(channel_balance_cache, expire=10) # keeps in cache for 10 seconds @classmethod def channel_balance(cls): """Returns channels balance""" request = lnrpc.ChannelBalanceRequest() - response = cls.lightningstub.ChannelBalance(request, - metadata=[("macaroon", - MACAROON.hex())]) + response = cls.lightningstub.ChannelBalance( + request, metadata=[("macaroon", MACAROON.hex())] + ) - - return {'local_balance': response.local_balance.sat, - 'remote_balance': response.remote_balance.sat, - 'unsettled_local_balance': response.unsettled_local_balance.sat, - 'unsettled_remote_balance': response.unsettled_remote_balance.sat} + return { + "local_balance": response.local_balance.sat, + "remote_balance": response.remote_balance.sat, + "unsettled_local_balance": response.unsettled_local_balance.sat, + "unsettled_remote_balance": response.unsettled_remote_balance.sat, + } @classmethod def pay_onchain(cls, onchainpayment): @@ -121,15 +129,17 @@ class LNNode: if config("DISABLE_ONCHAIN", cast=bool): return False - request = lnrpc.SendCoinsRequest(addr=onchainpayment.address, - amount=int(onchainpayment.sent_satoshis), - sat_per_vbyte=int(onchainpayment.mining_fee_rate), - label=str("Payout order #" + str(onchainpayment.order_paid_TX.id)), - spend_unconfirmed=True) + request = lnrpc.SendCoinsRequest( + addr=onchainpayment.address, + amount=int(onchainpayment.sent_satoshis), + sat_per_vbyte=int(onchainpayment.mining_fee_rate), + label=str("Payout order #" + str(onchainpayment.order_paid_TX.id)), + spend_unconfirmed=True, + ) - response = cls.lightningstub.SendCoins(request, - metadata=[("macaroon", - MACAROON.hex())]) + response = cls.lightningstub.SendCoins( + request, metadata=[("macaroon", MACAROON.hex())] + ) onchainpayment.txid = response.txid onchainpayment.save() @@ -139,28 +149,27 @@ class LNNode: @classmethod def cancel_return_hold_invoice(cls, payment_hash): """Cancels or returns a hold invoice""" - request = invoicesrpc.CancelInvoiceMsg( - payment_hash=bytes.fromhex(payment_hash)) - response = cls.invoicesstub.CancelInvoice(request, - metadata=[("macaroon", - MACAROON.hex())]) + request = invoicesrpc.CancelInvoiceMsg(payment_hash=bytes.fromhex(payment_hash)) + response = cls.invoicesstub.CancelInvoice( + request, metadata=[("macaroon", MACAROON.hex())] + ) # Fix this: tricky because canceling sucessfully an invoice has no response. TODO return str(response) == "" # True if no response, false otherwise. @classmethod def settle_hold_invoice(cls, preimage): """settles a hold invoice""" - request = invoicesrpc.SettleInvoiceMsg( - preimage=bytes.fromhex(preimage)) - response = cls.invoicesstub.SettleInvoice(request, - metadata=[("macaroon", - MACAROON.hex())]) + request = invoicesrpc.SettleInvoiceMsg(preimage=bytes.fromhex(preimage)) + response = cls.invoicesstub.SettleInvoice( + request, metadata=[("macaroon", MACAROON.hex())] + ) # Fix this: tricky because settling sucessfully an invoice has None response. TODO return str(response) == "" # True if no response, false otherwise. @classmethod - def gen_hold_invoice(cls, num_satoshis, description, invoice_expiry, - cltv_expiry_blocks): + def gen_hold_invoice( + cls, num_satoshis, description, invoice_expiry, cltv_expiry_blocks + ): """Generates hold invoice""" hold_payment = {} @@ -179,18 +188,20 @@ class LNNode: ), # actual expiry is padded by 50%, if tight, wrong client system clock will say invoice is expired. cltv_expiry=cltv_expiry_blocks, ) - response = cls.invoicesstub.AddHoldInvoice(request, - metadata=[("macaroon", - MACAROON.hex())]) + response = cls.invoicesstub.AddHoldInvoice( + request, metadata=[("macaroon", MACAROON.hex())] + ) hold_payment["invoice"] = response.payment_request payreq_decoded = cls.decode_payreq(hold_payment["invoice"]) hold_payment["preimage"] = preimage.hex() hold_payment["payment_hash"] = payreq_decoded.payment_hash hold_payment["created_at"] = timezone.make_aware( - datetime.fromtimestamp(payreq_decoded.timestamp)) + datetime.fromtimestamp(payreq_decoded.timestamp) + ) hold_payment["expires_at"] = hold_payment["created_at"] + timedelta( - seconds=payreq_decoded.expiry) + seconds=payreq_decoded.expiry + ) hold_payment["cltv_expiry"] = cltv_expiry_blocks return hold_payment @@ -201,11 +212,11 @@ class LNNode: from api.models import LNPayment request = invoicesrpc.LookupInvoiceMsg( - payment_hash=bytes.fromhex(lnpayment.payment_hash)) - response = cls.invoicesstub.LookupInvoiceV2(request, - metadata=[("macaroon", - MACAROON.hex()) - ]) + payment_hash=bytes.fromhex(lnpayment.payment_hash) + ) + response = cls.invoicesstub.LookupInvoiceV2( + request, metadata=[("macaroon", MACAROON.hex())] + ) # Will fail if 'unable to locate invoice'. Happens if invoice expiry # time has passed (but these are 15% padded at the moment). Should catch it @@ -225,11 +236,9 @@ class LNNode: @classmethod def resetmc(cls): request = routerrpc.ResetMissionControlRequest() - response = cls.routerstub.ResetMissionControl(request, - metadata=[ - ("macaroon", - MACAROON.hex()) - ]) + response = cls.routerstub.ResetMissionControl( + request, metadata=[("macaroon", MACAROON.hex())] + ) return True @classmethod @@ -258,7 +267,10 @@ class LNNode: route_hints = payreq_decoded.route_hints # Max amount RoboSats will pay for routing - max_routing_fee_sats = max(num_satoshis * float(config("PROPORTIONAL_ROUTING_FEE_LIMIT")), float(config("MIN_FLAT_ROUTING_FEE_LIMIT_REWARD"))) + max_routing_fee_sats = max( + num_satoshis * float(config("PROPORTIONAL_ROUTING_FEE_LIMIT")), + float(config("MIN_FLAT_ROUTING_FEE_LIMIT_REWARD")), + ) if route_hints: routes_cost = [] @@ -268,15 +280,17 @@ class LNNode: # ...add up the cost of every hinted hop... for hop_hint in hinted_route.hop_hints: route_cost += hop_hint.fee_base_msat / 1000 - route_cost += hop_hint.fee_proportional_millionths * num_satoshis / 1000000 + route_cost += ( + hop_hint.fee_proportional_millionths * num_satoshis / 1000000 + ) # ...and store the cost of the route to the array routes_cost.append(route_cost) # If the cheapest possible private route is more expensive than what RoboSats is willing to pay - if min(routes_cost) >= max_routing_fee_sats : + if min(routes_cost) >= max_routing_fee_sats: payout["context"] = { - "bad_invoice": "The invoice submitted only has a trick on the routing hints, you might be using an incompatible wallet (probably Muun? Use an onchain address instead!). Check the wallet compatibility guide at wallets.robosats.com" + "bad_invoice": "The invoice submitted only has a trick on the routing hints, you might be using an incompatible wallet (probably Muun? Use an onchain address instead!). Check the wallet compatibility guide at wallets.robosats.com" } return payout @@ -288,16 +302,18 @@ class LNNode: if not payreq_decoded.num_satoshis == num_satoshis: payout["context"] = { - "bad_invoice": - "The invoice provided is not for " + - "{:,}".format(num_satoshis) + " Sats" + "bad_invoice": "The invoice provided is not for " + + "{:,}".format(num_satoshis) + + " Sats" } return payout payout["created_at"] = timezone.make_aware( - datetime.fromtimestamp(payreq_decoded.timestamp)) + datetime.fromtimestamp(payreq_decoded.timestamp) + ) payout["expires_at"] = payout["created_at"] + timedelta( - seconds=payreq_decoded.expiry) + seconds=payreq_decoded.expiry + ) if payout["expires_at"] < timezone.now(): payout["context"] = { @@ -315,21 +331,24 @@ class LNNode: def pay_invoice(cls, lnpayment): """Sends sats. Used for rewards payouts""" from api.models import LNPayment - + fee_limit_sat = int( max( - lnpayment.num_satoshis * float(config("PROPORTIONAL_ROUTING_FEE_LIMIT")), + lnpayment.num_satoshis + * float(config("PROPORTIONAL_ROUTING_FEE_LIMIT")), float(config("MIN_FLAT_ROUTING_FEE_LIMIT_REWARD")), - )) # 200 ppm or 10 sats + ) + ) # 200 ppm or 10 sats timeout_seconds = int(config("REWARDS_TIMEOUT_SECONDS")) - request = routerrpc.SendPaymentRequest(payment_request=lnpayment.invoice, - fee_limit_sat=fee_limit_sat, - timeout_seconds=timeout_seconds) + request = routerrpc.SendPaymentRequest( + payment_request=lnpayment.invoice, + fee_limit_sat=fee_limit_sat, + timeout_seconds=timeout_seconds, + ) - for response in cls.routerstub.SendPaymentV2(request, - metadata=[("macaroon", - MACAROON.hex()) - ]): + for response in cls.routerstub.SendPaymentV2( + request, metadata=[("macaroon", MACAROON.hex())] + ): if response.status == 0: # Status 0 'UNKNOWN' # Not sure when this status happens @@ -354,7 +373,7 @@ class LNNode: if response.status == 2: # STATUS 'SUCCEEDED' lnpayment.status = LNPayment.Status.SUCCED - lnpayment.fee = float(response.fee_msat)/1000 + lnpayment.fee = float(response.fee_msat) / 1000 lnpayment.preimage = response.payment_preimage lnpayment.save() return True, None @@ -364,12 +383,10 @@ class LNNode: @classmethod def double_check_htlc_is_settled(cls, payment_hash): """Just as it sounds. Better safe than sorry!""" - request = invoicesrpc.LookupInvoiceMsg( - payment_hash=bytes.fromhex(payment_hash)) - response = cls.invoicesstub.LookupInvoiceV2(request, - metadata=[("macaroon", - MACAROON.hex()) - ]) + request = invoicesrpc.LookupInvoiceMsg(payment_hash=bytes.fromhex(payment_hash)) + response = cls.invoicesstub.LookupInvoiceV2( + request, metadata=[("macaroon", MACAROON.hex())] + ) return ( response.state == 1 diff --git a/api/logics.py b/api/logics.py index 3131bbcd..9191e6c8 100644 --- a/api/logics.py +++ b/api/logics.py @@ -26,13 +26,15 @@ EXP_MAKER_BOND_INVOICE = int(config("EXP_MAKER_BOND_INVOICE")) EXP_TAKER_BOND_INVOICE = int(config("EXP_TAKER_BOND_INVOICE")) BLOCK_TIME = float(config("BLOCK_TIME")) -MAX_MINING_NETWORK_SPEEDUP_EXPECTED = float(config("MAX_MINING_NETWORK_SPEEDUP_EXPECTED")) +MAX_MINING_NETWORK_SPEEDUP_EXPECTED = float( + config("MAX_MINING_NETWORK_SPEEDUP_EXPECTED") +) INVOICE_AND_ESCROW_DURATION = int(config("INVOICE_AND_ESCROW_DURATION")) FIAT_EXCHANGE_DURATION = int(config("FIAT_EXCHANGE_DURATION")) -class Logics: +class Logics: @classmethod def validate_already_maker_or_taker(cls, user): """Validates if a use is already not part of an active order""" @@ -51,39 +53,34 @@ class Logics: Order.Status.WFR, ] """Checks if the user is already partipant of an active order""" - queryset = Order.objects.filter(maker=user, - status__in=active_order_status) + queryset = Order.objects.filter(maker=user, status__in=active_order_status) if queryset.exists(): return ( False, - { - "bad_request": "You are already maker of an active order" - }, + {"bad_request": "You are already maker of an active order"}, queryset[0], ) - queryset = Order.objects.filter(taker=user, - status__in=active_order_status) + queryset = Order.objects.filter(taker=user, status__in=active_order_status) if queryset.exists(): return ( False, - { - "bad_request": "You are already taker of an active order" - }, + {"bad_request": "You are already taker of an active order"}, queryset[0], ) # Edge case when the user is in an order that is failing payment and he is the buyer - queryset = Order.objects.filter(Q(maker=user) | Q(taker=user), - status__in=[Order.Status.FAI,Order.Status.PAY]) + queryset = Order.objects.filter( + Q(maker=user) | Q(taker=user), + status__in=[Order.Status.FAI, Order.Status.PAY], + ) if queryset.exists(): order = queryset[0] if cls.is_buyer(order, user): return ( False, { - "bad_request": - "You are still pending a payment from a recent order" + "bad_request": "You are still pending a payment from a recent order" }, order, ) @@ -91,12 +88,12 @@ class Logics: return True, None, None def validate_pgp_keys(pub_key, enc_priv_key): - ''' Validates PGP valid keys. Formats them in a way understandable by the frontend ''' + """Validates PGP valid keys. Formats them in a way understandable by the frontend""" gpg = gnupg.GPG() # Standarize format with linux linebreaks '\n'. Windows users submitting their own keys have '\r\n' breaking communication. - enc_priv_key = enc_priv_key.replace('\r\n', '\n') - pub_key = pub_key.replace('\r\n', '\n') + enc_priv_key = enc_priv_key.replace("\r\n", "\n") + pub_key = pub_key.replace("\r\n", "\n") # Try to import the public key import_pub_result = gpg.import_keys(pub_key) @@ -104,16 +101,16 @@ class Logics: return ( False, { - "bad_request": - f"Your PGP public key does not seem valid.\n"+ - f"Stderr: {str(import_pub_result.stderr)}\n"+ - f"ReturnCode: {str(import_pub_result.returncode)}\n"+ - f"Summary: {str(import_pub_result.summary)}\n"+ - f"Results: {str(import_pub_result.results)}\n"+ - f"Imported: {str(import_pub_result.imported)}\n" - }, - None, - None) + "bad_request": f"Your PGP public key does not seem valid.\n" + + f"Stderr: {str(import_pub_result.stderr)}\n" + + f"ReturnCode: {str(import_pub_result.returncode)}\n" + + f"Summary: {str(import_pub_result.summary)}\n" + + f"Results: {str(import_pub_result.results)}\n" + + f"Imported: {str(import_pub_result.imported)}\n" + }, + None, + None, + ) # Exports the public key again for uniform formatting. pub_key = gpg.export_keys(import_pub_result.fingerprints[0]) @@ -123,16 +120,16 @@ class Logics: return ( False, { - "bad_request": - f"Your PGP encrypted private key does not seem valid.\n"+ - f"Stderr: {str(import_priv_result.stderr)}\n"+ - f"ReturnCode: {str(import_priv_result.returncode)}\n"+ - f"Summary: {str(import_priv_result.summary)}\n"+ - f"Results: {str(import_priv_result.results)}\n"+ - f"Sec Imported: {str(import_priv_result.sec_imported)}\n" - }, - None, - None) + "bad_request": f"Your PGP encrypted private key does not seem valid.\n" + + f"Stderr: {str(import_priv_result.stderr)}\n" + + f"ReturnCode: {str(import_priv_result.returncode)}\n" + + f"Summary: {str(import_priv_result.summary)}\n" + + f"Results: {str(import_priv_result.results)}\n" + + f"Sec Imported: {str(import_priv_result.sec_imported)}\n" + }, + None, + None, + ) return True, None, pub_key, enc_priv_key @@ -142,48 +139,50 @@ class Logics: if not order.has_range: if order.t0_satoshis > MAX_TRADE: return False, { - "bad_request": - "Your order is too big. It is worth " + - "{:,}".format(order.t0_satoshis) + - " Sats now, but the limit is " + "{:,}".format(MAX_TRADE) + - " Sats" + "bad_request": "Your order is too big. It is worth " + + "{:,}".format(order.t0_satoshis) + + " Sats now, but the limit is " + + "{:,}".format(MAX_TRADE) + + " Sats" } if order.t0_satoshis < MIN_TRADE: return False, { - "bad_request": - "Your order is too small. It is worth " + - "{:,}".format(order.t0_satoshis) + - " Sats now, but the limit is " + "{:,}".format(MIN_TRADE) + - " Sats" + "bad_request": "Your order is too small. It is worth " + + "{:,}".format(order.t0_satoshis) + + " Sats now, but the limit is " + + "{:,}".format(MIN_TRADE) + + " Sats" } elif order.has_range: - min_sats = cls.calc_sats(order.min_amount, order.currency.exchange_rate, order.premium) - max_sats = cls.calc_sats(order.max_amount, order.currency.exchange_rate, order.premium) - if min_sats > max_sats/1.5: + min_sats = cls.calc_sats( + order.min_amount, order.currency.exchange_rate, order.premium + ) + max_sats = cls.calc_sats( + order.max_amount, order.currency.exchange_rate, order.premium + ) + if min_sats > max_sats / 1.5: return False, { - "bad_request": - "Maximum range amount must be at least 50 percent higher than the minimum amount" + "bad_request": "Maximum range amount must be at least 50 percent higher than the minimum amount" } elif max_sats > MAX_TRADE: return False, { - "bad_request": - "Your order maximum amount is too big. It is worth " + - "{:,}".format(int(max_sats)) + - " Sats now, but the limit is " + "{:,}".format(MAX_TRADE) + - " Sats" + "bad_request": "Your order maximum amount is too big. It is worth " + + "{:,}".format(int(max_sats)) + + " Sats now, but the limit is " + + "{:,}".format(MAX_TRADE) + + " Sats" } elif min_sats < MIN_TRADE: return False, { - "bad_request": - "Your order minimum amount is too small. It is worth " + - "{:,}".format(int(min_sats)) + - " Sats now, but the limit is " + "{:,}".format(MIN_TRADE) + - " Sats" + "bad_request": "Your order minimum amount is too small. It is worth " + + "{:,}".format(int(min_sats)) + + " Sats now, but the limit is " + + "{:,}".format(MIN_TRADE) + + " Sats" } - elif min_sats < max_sats/8: + elif min_sats < max_sats / 8: return False, { - "bad_request": - f"Your order amount range is too large. Max amount can only be 8 times bigger than min amount" + "bad_request": f"Your order amount range is too large. Max amount can only be 8 times bigger than min amount" } return True, None @@ -191,8 +190,7 @@ class Logics: def validate_amount_within_range(order, amount): if amount > float(order.max_amount) or amount < float(order.min_amount): return False, { - "bad_request": - "The amount specified is outside the range specified by the maker" + "bad_request": "The amount specified is outside the range specified by the maker" } return True, None @@ -215,11 +213,12 @@ class Logics: } else: if order.has_range: - order.amount= amount + order.amount = amount order.taker = user order.status = Order.Status.TAK order.expires_at = timezone.now() + timedelta( - seconds=order.t_to_expire(Order.Status.TAK)) + seconds=order.t_to_expire(Order.Status.TAK) + ) order.save() return True, None @@ -227,18 +226,20 @@ class Logics: is_maker = order.maker == user is_taker = order.taker == user return (is_maker and order.type == Order.Types.BUY) or ( - is_taker and order.type == Order.Types.SELL) + is_taker and order.type == Order.Types.SELL + ) def is_seller(order, user): is_maker = order.maker == user is_taker = order.taker == user return (is_maker and order.type == Order.Types.SELL) or ( - is_taker and order.type == Order.Types.BUY) + is_taker and order.type == Order.Types.BUY + ) def calc_sats(amount, exchange_rate, premium): exchange_rate = float(exchange_rate) premium_rate = exchange_rate * (1 + float(premium) / 100) - return (float(amount) /premium_rate) * 100 * 1000 * 1000 + return (float(amount) / premium_rate) * 100 * 1000 * 1000 @classmethod def satoshis_now(cls, order): @@ -247,7 +248,9 @@ class Logics: satoshis_now = order.satoshis else: amount = order.amount if order.amount != None else order.max_amount - satoshis_now = cls.calc_sats(amount, order.currency.exchange_rate, order.premium) + satoshis_now = cls.calc_sats( + amount, order.currency.exchange_rate, order.premium + ) return int(satoshis_now) @@ -266,8 +269,8 @@ class Logics: significant_digits = 5 price = round( - price, - significant_digits - int(math.floor(math.log10(abs(price)))) - 1) + price, significant_digits - int(math.floor(math.log10(abs(price)))) - 1 + ) return price, premium @@ -308,7 +311,7 @@ class Logics: order.status = Order.Status.EXP order.expiry_reason = Order.ExpiryReasons.NTAKEN order.save() - send_message.delay(order.id,'order_expired_untaken') + send_message.delay(order.id, "order_expired_untaken") return True elif order.status == Order.Status.TAK: @@ -337,7 +340,7 @@ class Logics: cls.settle_bond(order.maker_bond) cls.return_bond(order.taker_bond) # If seller is offline the escrow LNpayment does not exist - try: + try: cls.cancel_escrow(order) except: pass @@ -352,7 +355,7 @@ class Logics: else: cls.settle_bond(order.taker_bond) # If seller is offline the escrow LNpayment does not even exist - try: + try: cls.cancel_escrow(order) except: pass @@ -362,7 +365,7 @@ class Logics: order.trade_escrow = None order.payout = None cls.publish_order(order) - send_message.delay(order.id,'order_published') + send_message.delay(order.id, "order_published") # Reward maker with part of the taker bond cls.add_slashed_rewards(taker_bond, order.maker.profile) return True @@ -393,7 +396,7 @@ class Logics: order.taker_bond = None order.trade_escrow = None cls.publish_order(order) - send_message.delay(order.id,'order_published') + send_message.delay(order.id, "order_published") # Reward maker with part of the taker bond cls.add_slashed_rewards(taker_bond, order.maker.profile) return True @@ -412,7 +415,8 @@ class Logics: if order.taker: profile = order.taker.profile profile.penalty_expiration = timezone.now() + timedelta( - seconds=PENALTY_TIMEOUT) + seconds=PENALTY_TIMEOUT + ) profile.save() # Make order public again @@ -424,9 +428,9 @@ class Logics: @classmethod def open_dispute(cls, order, user=None): - # Always settle escrow and bonds during a dispute. Disputes - # can take long to resolve, it might trigger force closure - # for unresolved HTLCs) Dispute winner will have to submit a + # Always settle escrow and bonds during a dispute. Disputes + # can take long to resolve, it might trigger force closure + # for unresolved HTLCs) Dispute winner will have to submit a # new invoice for value of escrow + bond. valid_status_open_dispute = [ @@ -435,8 +439,10 @@ class Logics: ] if order.status not in valid_status_open_dispute: - return False, {"bad_request": "You cannot open a dispute of this order at this stage"} - + return False, { + "bad_request": "You cannot open a dispute of this order at this stage" + } + if not order.trade_escrow.status == LNPayment.Status.SETLED: cls.settle_escrow(order) cls.settle_bond(order.maker_bond) @@ -445,7 +451,8 @@ class Logics: order.is_disputed = True order.status = Order.Status.DIS order.expires_at = timezone.now() + timedelta( - seconds=order.t_to_expire(Order.Status.DIS)) + seconds=order.t_to_expire(Order.Status.DIS) + ) order.save() # User could be None if a dispute is open automatically due to weird expiration. @@ -456,10 +463,11 @@ class Logics: profile.orders_disputes_started = [str(order.id)] else: profile.orders_disputes_started = list( - profile.orders_disputes_started).append(str(order.id)) + profile.orders_disputes_started + ).append(str(order.id)) profile.save() - send_message.delay(order.id,'dispute_opened') + send_message.delay(order.id, "dispute_opened") return True, None def dispute_statement(order, user, statement): @@ -467,15 +475,14 @@ class Logics: if not order.status == Order.Status.DIS: return False, { - "bad_request": - "Only orders in dispute accept dispute statements" + "bad_request": "Only orders in dispute accept dispute statements" } if len(statement) > 5000: return False, { "bad_statement": "The statement is longer than 5000 characters" } - + if len(statement) < 100: return False, { "bad_statement": "The statement is too short. Make sure to be thorough." @@ -487,72 +494,89 @@ class Logics: order.taker_statement = statement # If both statements are in, move status to wait for dispute resolution - if order.maker_statement not in [None,""] and order.taker_statement not in [None,""]: + if order.maker_statement not in [None, ""] and order.taker_statement not in [ + None, + "", + ]: order.status = Order.Status.WFR order.expires_at = timezone.now() + timedelta( - seconds=order.t_to_expire(Order.Status.WFR)) + seconds=order.t_to_expire(Order.Status.WFR) + ) order.save() return True, None def compute_swap_fee_rate(balance): - - shape = str(config('SWAP_FEE_SHAPE')) + shape = str(config("SWAP_FEE_SHAPE")) if shape == "linear": - MIN_SWAP_FEE = float(config('MIN_SWAP_FEE')) - MIN_POINT = float(config('MIN_POINT')) - MAX_SWAP_FEE = float(config('MAX_SWAP_FEE')) - MAX_POINT = float(config('MAX_POINT')) + MIN_SWAP_FEE = float(config("MIN_SWAP_FEE")) + MIN_POINT = float(config("MIN_POINT")) + MAX_SWAP_FEE = float(config("MAX_SWAP_FEE")) + MAX_POINT = float(config("MAX_POINT")) if float(balance.onchain_fraction) > MIN_POINT: swap_fee_rate = MIN_SWAP_FEE else: slope = (MAX_SWAP_FEE - MIN_SWAP_FEE) / (MAX_POINT - MIN_POINT) - swap_fee_rate = slope * (balance.onchain_fraction - MAX_POINT) + MAX_SWAP_FEE + swap_fee_rate = ( + slope * (balance.onchain_fraction - MAX_POINT) + MAX_SWAP_FEE + ) elif shape == "exponential": - MIN_SWAP_FEE = float(config('MIN_SWAP_FEE')) - MAX_SWAP_FEE = float(config('MAX_SWAP_FEE')) - SWAP_LAMBDA = float(config('SWAP_LAMBDA')) - swap_fee_rate = MIN_SWAP_FEE + (MAX_SWAP_FEE - MIN_SWAP_FEE) * math.exp(-SWAP_LAMBDA * float(balance.onchain_fraction)) + MIN_SWAP_FEE = float(config("MIN_SWAP_FEE")) + MAX_SWAP_FEE = float(config("MAX_SWAP_FEE")) + SWAP_LAMBDA = float(config("SWAP_LAMBDA")) + swap_fee_rate = MIN_SWAP_FEE + (MAX_SWAP_FEE - MIN_SWAP_FEE) * math.exp( + -SWAP_LAMBDA * float(balance.onchain_fraction) + ) return swap_fee_rate * 100 @classmethod def create_onchain_payment(cls, order, user, preliminary_amount): - ''' + """ Creates an empty OnchainPayment for order.payout_tx. It sets the fees to be applied to this order if onchain Swap is used. If the user submits a LN invoice instead. The returned OnchainPayment goes unused. - ''' + """ # Make sure no invoice payout is attached to order order.payout = None # Create onchain_payment onchain_payment = OnchainPayment.objects.create(receiver=user) - + # Compute a safer available onchain liquidity: (confirmed_utxos - reserve - pending_outgoing_txs)) # Accounts for already committed outgoing TX for previous users. confirmed = onchain_payment.balance.onchain_confirmed reserve = 0.01 * onchain_payment.balance.total # We assume a reserve of 1% - pending_txs = OnchainPayment.objects.filter(status=OnchainPayment.Status.VALID).aggregate(Sum('num_satoshis'))['num_satoshis__sum'] - + pending_txs = OnchainPayment.objects.filter( + status=OnchainPayment.Status.VALID + ).aggregate(Sum("num_satoshis"))["num_satoshis__sum"] + if pending_txs == None: pending_txs = 0 - + available_onchain = confirmed - reserve - pending_txs - if preliminary_amount > available_onchain: # Not enough onchain balance to commit for this swap. + if ( + preliminary_amount > available_onchain + ): # Not enough onchain balance to commit for this swap. return False - suggested_mining_fee_rate = LNNode.estimate_fee(amount_sats=preliminary_amount)["mining_fee_rate"] + suggested_mining_fee_rate = LNNode.estimate_fee(amount_sats=preliminary_amount)[ + "mining_fee_rate" + ] # Hardcap mining fee suggested at 50 sats/vbyte if suggested_mining_fee_rate > 50: suggested_mining_fee_rate = 50 - onchain_payment.suggested_mining_fee_rate = max(1.05, LNNode.estimate_fee(amount_sats=preliminary_amount)["mining_fee_rate"]) - onchain_payment.swap_fee_rate = cls.compute_swap_fee_rate(onchain_payment.balance) + onchain_payment.suggested_mining_fee_rate = max( + 1.05, LNNode.estimate_fee(amount_sats=preliminary_amount)["mining_fee_rate"] + ) + onchain_payment.swap_fee_rate = cls.compute_swap_fee_rate( + onchain_payment.balance + ) onchain_payment.save() order.payout_tx = onchain_payment @@ -575,18 +599,22 @@ class Logics: fee_sats = order.last_satoshis * fee_fraction - reward_tip = int(config('REWARD_TIP')) if user.profile.is_referred else 0 + reward_tip = int(config("REWARD_TIP")) if user.profile.is_referred else 0 context = {} # context necessary for the user to submit a LN invoice - context["invoice_amount"] = round(order.last_satoshis - fee_sats - reward_tip) # Trading fee to buyer is charged here. + context["invoice_amount"] = round( + order.last_satoshis - fee_sats - reward_tip + ) # Trading fee to buyer is charged here. # context necessary for the user to submit an onchain address MIN_SWAP_AMOUNT = int(config("MIN_SWAP_AMOUNT")) if context["invoice_amount"] < MIN_SWAP_AMOUNT: context["swap_allowed"] = False - context["swap_failure_reason"] = "Order amount is too small to be eligible for a swap" + context[ + "swap_failure_reason" + ] = "Order amount is too small to be eligible for a swap" return True, context if config("DISABLE_ONCHAIN", cast=bool): @@ -596,10 +624,14 @@ class Logics: if order.payout_tx == None: # Creates the OnchainPayment object and checks node balance - valid = cls.create_onchain_payment(order, user, preliminary_amount=context["invoice_amount"]) + valid = cls.create_onchain_payment( + order, user, preliminary_amount=context["invoice_amount"] + ) if not valid: context["swap_allowed"] = False - context["swap_failure_reason"] = "Not enough onchain liquidity available to offer a swap" + context[ + "swap_failure_reason" + ] = "Not enough onchain liquidity available to offer a swap" return True, context context["swap_allowed"] = True @@ -611,45 +643,45 @@ class Logics: @classmethod def escrow_amount(cls, order, user): """Computes escrow invoice amount. Uses order.last_satoshis, - that is the final trade amount set at Taker Bond time""" + that is the final trade amount set at Taker Bond time""" if user == order.maker: fee_fraction = FEE * MAKER_FEE_SPLIT elif user == order.taker: fee_fraction = FEE * (1 - MAKER_FEE_SPLIT) - fee_sats = order.last_satoshis * fee_fraction + fee_sats = order.last_satoshis * fee_fraction - reward_tip = int(config('REWARD_TIP')) if user.profile.is_referred else 0 + reward_tip = int(config("REWARD_TIP")) if user.profile.is_referred else 0 if cls.is_seller(order, user): - escrow_amount = round(order.last_satoshis + fee_sats + reward_tip) # Trading fee to seller is charged here. + escrow_amount = round( + order.last_satoshis + fee_sats + reward_tip + ) # Trading fee to seller is charged here. return True, {"escrow_amount": escrow_amount} @classmethod def update_address(cls, order, user, address, mining_fee_rate): - + # Empty address? if not address: - return False, { - "bad_address": - "You submitted an empty invoice" - } + return False, {"bad_address": "You submitted an empty invoice"} # only the buyer can post a buyer address if not cls.is_buyer(order, user): return False, { - "bad_request": - "Only the buyer of this order can provide a payout address." + "bad_request": "Only the buyer of this order can provide a payout address." } # not the right time to submit - if (not (order.taker_bond.status == order.maker_bond.status == - LNPayment.Status.LOCKED) - and not order.status == Order.Status.FAI): - return False, { - "bad_request": - "You cannot submit an adress are not locked." - } + if ( + not ( + order.taker_bond.status + == order.maker_bond.status + == LNPayment.Status.LOCKED + ) + and not order.status == Order.Status.FAI + ): + return False, {"bad_request": "You cannot submit an adress are not locked."} # not a valid address (does not accept Taproot as of now) valid, context = validate_onchain_address(address) if not valid: @@ -659,13 +691,11 @@ class Logics: # not a valid mining fee if float(mining_fee_rate) < 1: return False, { - "bad_address": - "The mining fee is too low, must be higher than 1 Sat/vbyte" + "bad_address": "The mining fee is too low, must be higher than 1 Sat/vbyte" } elif float(mining_fee_rate) > 50: return False, { - "bad_address": - "The mining fee is too high, must be less than 50 Sats/vbyte" + "bad_address": "The mining fee is too high, must be less than 50 Sats/vbyte" } order.payout_tx.mining_fee_rate = float(mining_fee_rate) # If not mining ee provider use backend's suggested fee rate @@ -676,7 +706,11 @@ class Logics: tx.address = address tx.mining_fee_sats = int(tx.mining_fee_rate * 141) tx.num_satoshis = cls.payout_amount(order, user)[1]["invoice_amount"] - tx.sent_satoshis = int(float(tx.num_satoshis) - float(tx.num_satoshis) * float(tx.swap_fee_rate)/100 - float(tx.mining_fee_sats)) + tx.sent_satoshis = int( + float(tx.num_satoshis) + - float(tx.num_satoshis) * float(tx.swap_fee_rate) / 100 + - float(tx.mining_fee_sats) + ) tx.status = OnchainPayment.Status.VALID tx.save() @@ -689,33 +723,32 @@ class Logics: @classmethod def update_invoice(cls, order, user, invoice): - + # Empty invoice? if not invoice: - return False, { - "bad_invoice": - "You submitted an empty invoice" - } + return False, {"bad_invoice": "You submitted an empty invoice"} # only the buyer can post a buyer invoice if not cls.is_buyer(order, user): return False, { - "bad_request": - "Only the buyer of this order can provide a buyer invoice." + "bad_request": "Only the buyer of this order can provide a buyer invoice." } if not order.taker_bond: return False, {"bad_request": "Wait for your order to be taken."} - if (not (order.taker_bond.status == order.maker_bond.status == - LNPayment.Status.LOCKED) - and not order.status == Order.Status.FAI): + if ( + not ( + order.taker_bond.status + == order.maker_bond.status + == LNPayment.Status.LOCKED + ) + and not order.status == Order.Status.FAI + ): return False, { - "bad_request": - "You cannot submit a invoice while bonds are not locked." + "bad_request": "You cannot submit a invoice while bonds are not locked." } if order.status == Order.Status.FAI: if order.payout.status != LNPayment.Status.EXPIRE: return False, { - "bad_request": - "You cannot submit an invoice only after expiration or 3 failed attempts" + "bad_request": "You cannot submit an invoice only after expiration or 3 failed attempts" } # cancel onchain_payout if existing @@ -731,8 +764,7 @@ class Logics: concept=LNPayment.Concepts.PAYBUYER, type=LNPayment.Types.NORM, sender=User.objects.get(username=ESCROW_USERNAME), - order_paid_LN= - order, # In case this user has other payouts, update the one related to this order. + order_paid_LN=order, # In case this user has other payouts, update the one related to this order. receiver=user, # if there is a LNPayment matching these above, it updates that one with defaults below. defaults={ @@ -754,13 +786,14 @@ class Logics: return True, None @classmethod - def move_state_updated_payout_method(cls,order): + def move_state_updated_payout_method(cls, order): # If the order status is 'Waiting for invoice'. Move forward to 'chat' if order.status == Order.Status.WFI: order.status = Order.Status.CHA order.expires_at = timezone.now() + timedelta( - seconds=order.t_to_expire(Order.Status.CHA)) - send_message.delay(order.id,'fiat_exchange_starts') + seconds=order.t_to_expire(Order.Status.CHA) + ) + send_message.delay(order.id, "fiat_exchange_starts") # If the order status is 'Waiting for both'. Move forward to 'waiting for escrow' elif order.status == Order.Status.WF2: @@ -771,20 +804,20 @@ class Logics: elif order.trade_escrow.status == LNPayment.Status.LOCKED: order.status = Order.Status.CHA order.expires_at = timezone.now() + timedelta( - seconds=order.t_to_expire(Order.Status.CHA)) - send_message.delay(order.id,'fiat_exchange_starts') + seconds=order.t_to_expire(Order.Status.CHA) + ) + send_message.delay(order.id, "fiat_exchange_starts") else: order.status = Order.Status.WFE # If the order status is 'Failed Routing'. Retry payment. elif order.status == Order.Status.FAI: - if LNNode.double_check_htlc_is_settled( - order.trade_escrow.payment_hash): + if LNNode.double_check_htlc_is_settled(order.trade_escrow.payment_hash): order.status = Order.Status.PAY order.payout.status = LNPayment.Status.FLIGHT order.payout.routing_attempts = 0 order.payout.save() - + order.save() return True @@ -814,8 +847,7 @@ class Logics: if user.profile.penalty_expiration: if user.profile.penalty_expiration > timezone.now(): - time_out = (user.profile.penalty_expiration - - timezone.now()).seconds + time_out = (user.profile.penalty_expiration - timezone.now()).seconds return True, time_out return False, None @@ -853,12 +885,14 @@ class Logics: """The order dissapears from book and goes to cancelled. If strict, maker is charged the bond to prevent DDOS on the LN node and order book. If not strict, maker is returned the bond (more user friendly).""" - elif order.status in [Order.Status.PUB, Order.Status.PAU] and order.maker == user: + elif ( + order.status in [Order.Status.PUB, Order.Status.PAU] and order.maker == user + ): # Return the maker bond (Maker gets returned the bond for cancelling public order) - if cls.return_bond(order.maker_bond): + if cls.return_bond(order.maker_bond): order.status = Order.Status.UCA order.save() - send_message.delay(order.id,'public_order_cancelled') + send_message.delay(order.id, "public_order_cancelled") return True, None # 2.b) When maker cancels after bond and before taker bond is locked @@ -866,11 +900,11 @@ class Logics: The bond maker bond is returned.""" elif order.status == Order.Status.TAK and order.maker == user: # Return the maker bond (Maker gets returned the bond for cancelling public order) - if cls.return_bond(order.maker_bond): + if cls.return_bond(order.maker_bond): cls.cancel_bond(order.taker_bond) order.status = Order.Status.UCA order.save() - send_message.delay(order.id,'public_order_cancelled') + send_message.delay(order.id, "public_order_cancelled") return True, None # 3) When taker cancels before bond @@ -889,7 +923,9 @@ class Logics: # 4.a) When maker cancel after bond (before escrow) """The order into cancelled status if maker cancels.""" - elif (order.status in [Order.Status.WF2,Order.Status.WFE] and order.maker == user): + elif ( + order.status in [Order.Status.WF2, Order.Status.WFE] and order.maker == user + ): # cancel onchain payment if existing cls.cancel_onchain_payment(order) # Settle the maker bond (Maker loses the bond for canceling an ongoing trade) @@ -905,8 +941,9 @@ class Logics: # 4.b) When taker cancel after bond (before escrow) """The order into cancelled status if mtker cancels.""" - elif (order.status in [Order.Status.WF2, Order.Status.WFE] - and order.taker == user): + elif ( + order.status in [Order.Status.WF2, Order.Status.WFE] and order.taker == user + ): # cancel onchain payment if existing cls.cancel_onchain_payment(order) # Settle the maker bond (Maker loses the bond for canceling an ongoing trade) @@ -916,7 +953,7 @@ class Logics: order.payout = None order.trade_escrow = None cls.publish_order(order) - send_message.delay(order.id,'order_published') + send_message.delay(order.id, "order_published") # Reward maker with part of the taker bond cls.add_slashed_rewards(order.taker_bond, order.maker.profile) return True, None @@ -926,9 +963,7 @@ class Logics: When a user asks for cancel, 'order.m/t/aker_asked_cancel' goes True. When the second user asks for cancel. Order is totally cancelled. Must have a small cost for both parties to prevent node DDOS.""" - elif order.status in [ - Order.Status.WFI, Order.Status.CHA - ]: + elif order.status in [Order.Status.WFI, Order.Status.CHA]: # if the maker had asked, and now the taker does: cancel order, return everything if order.maker_asked_cancel and user == order.taker: @@ -965,14 +1000,15 @@ class Logics: cls.return_escrow(order) order.status = Order.Status.CCA order.save() - send_message.delay(order.id,'collaborative_cancelled') + send_message.delay(order.id, "collaborative_cancelled") return @classmethod def publish_order(cls, order): order.status = Order.Status.PUB order.expires_at = order.created_at + timedelta( - seconds=order.t_to_expire(Order.Status.PUB)) + seconds=order.t_to_expire(Order.Status.PUB) + ) if order.has_range: order.amount = None order.last_satoshis = cls.satoshis_now(order) @@ -982,10 +1018,10 @@ class Logics: return def compute_cltv_expiry_blocks(order, invoice_concept): - ''' Computes timelock CLTV expiry of the last hop in blocks for hodl invoices + """Computes timelock CLTV expiry of the last hop in blocks for hodl invoices invoice_concepts (str): maker_bond, taker_bond, trade_escrow - ''' + """ # Every invoice_concept must be locked by at least the fiat exchange duration # Every invoice must also be locked for deposit_time (order.escrow_duration or WFE status) cltv_expiry_secs = order.t_to_expire(Order.Status.CHA) @@ -1000,7 +1036,13 @@ class Logics: safe_cltv_expiry_secs = cltv_expiry_secs * MAX_MINING_NETWORK_SPEEDUP_EXPECTED # Convert to blocks using assummed average block time (~8 mins/block) cltv_expiry_blocks = int(safe_cltv_expiry_secs / (BLOCK_TIME * 60)) - print(invoice_concept," cltv_expiry_hours:",cltv_expiry_secs/3600," cltv_expiry_blocks:",cltv_expiry_blocks) + print( + invoice_concept, + " cltv_expiry_hours:", + cltv_expiry_secs / 3600, + " cltv_expiry_blocks:", + cltv_expiry_blocks, + ) return cltv_expiry_blocks @@ -1010,7 +1052,7 @@ class Logics: return True elif LNNode.validate_hold_invoice_locked(order.maker_bond): cls.publish_order(order) - send_message.delay(order.id,'order_published') + send_message.delay(order.id, "order_published") return True return False @@ -1021,8 +1063,7 @@ class Logics: if order.expires_at < timezone.now(): cls.order_expires(order) return False, { - "bad_request": - "Invoice expired. You did not confirm publishing the order in time. Make a new order." + "bad_request": "Invoice expired. You did not confirm publishing the order in time. Make a new order." } # Return the previous invoice if there was one and is still unpaid @@ -1038,7 +1079,7 @@ class Logics: # If there was no maker_bond object yet, generates one order.last_satoshis = cls.satoshis_now(order) order.last_satoshis_time = timezone.now() - bond_satoshis = int(order.last_satoshis * order.bond_size/100) + bond_satoshis = int(order.last_satoshis * order.bond_size / 100) if user.profile.wants_stealth: description = f"This payment WILL FREEZE IN YOUR WALLET, check on the website if it was successful. It will automatically return unless you cheat or cancel unilaterally. Payment reference: {order.reference}" @@ -1051,19 +1092,17 @@ class Logics: bond_satoshis, description, invoice_expiry=order.t_to_expire(Order.Status.WFB), - cltv_expiry_blocks=cls.compute_cltv_expiry_blocks(order, "maker_bond") + cltv_expiry_blocks=cls.compute_cltv_expiry_blocks(order, "maker_bond"), ) except Exception as e: print(str(e)) if "failed to connect to all addresses" in str(e): return False, { - "bad_request": - "The Lightning Network Daemon (LND) is down. Write in the Telegram group to make sure the staff is aware." + "bad_request": "The Lightning Network Daemon (LND) is down. Write in the Telegram group to make sure the staff is aware." } elif "wallet locked" in str(e): return False, { - "bad_request": - "This is weird, RoboSats' lightning wallet is locked. Check in the Telegram group, maybe the staff has died." + "bad_request": "This is weird, RoboSats' lightning wallet is locked. Check in the Telegram group, maybe the staff has died." } order.maker_bond = LNPayment.objects.create( @@ -1100,9 +1139,10 @@ class Logics: order.taker_bond.status = LNPayment.Status.LOCKED order.taker_bond.save() - # With the bond confirmation the order is extended 'public_order_duration' hours + # With the bond confirmation the order is extended 'public_order_duration' hours order.expires_at = timezone.now() + timedelta( - seconds=order.t_to_expire(Order.Status.WF2)) + seconds=order.t_to_expire(Order.Status.WF2) + ) order.status = Order.Status.WF2 order.save() @@ -1111,13 +1151,13 @@ class Logics: order.taker.profile.total_contracts += 1 order.maker.profile.save() order.taker.profile.save() - + # Log a market tick try: MarketTick.log_a_tick(order) except: pass - send_message.delay(order.id,'order_taken_confirmed') + send_message.delay(order.id, "order_taken_confirmed") return True @classmethod @@ -1136,8 +1176,7 @@ class Logics: if order.expires_at < timezone.now(): cls.order_expires(order) return False, { - "bad_request": - "Invoice expired. You did not confirm taking the order in time." + "bad_request": "Invoice expired. You did not confirm taking the order in time." } # Do not gen if a taker invoice exist. Do not return if it is already locked. Return the old one if still waiting. @@ -1153,15 +1192,14 @@ class Logics: # If there was no taker_bond object yet, generates one order.last_satoshis = cls.satoshis_now(order) order.last_satoshis_time = timezone.now() - bond_satoshis = int(order.last_satoshis * order.bond_size/100) + bond_satoshis = int(order.last_satoshis * order.bond_size / 100) pos_text = "Buying" if cls.is_buyer(order, user) else "Selling" if user.profile.wants_stealth: description = f"This payment WILL FREEZE IN YOUR WALLET, check on the website if it was successful. It will automatically return unless you cheat or cancel unilaterally. Payment reference: {order.reference}" else: description = ( f"RoboSats - Taking 'Order {order.id}' {pos_text} BTC for {str(float(order.amount)) + Currency.currency_dict[str(order.currency.currency)]}" - + - " - Taker bond - This payment WILL FREEZE IN YOUR WALLET, check on the website if it was successful. It will automatically return unless you cheat or cancel unilaterally." + + " - Taker bond - This payment WILL FREEZE IN YOUR WALLET, check on the website if it was successful. It will automatically return unless you cheat or cancel unilaterally." ) # Gen hold Invoice @@ -1170,14 +1208,13 @@ class Logics: bond_satoshis, description, invoice_expiry=order.t_to_expire(Order.Status.TAK), - cltv_expiry_blocks=cls.compute_cltv_expiry_blocks(order, "taker_bond") + cltv_expiry_blocks=cls.compute_cltv_expiry_blocks(order, "taker_bond"), ) except Exception as e: if "status = StatusCode.UNAVAILABLE" in str(e): return False, { - "bad_request": - "The Lightning Network Daemon (LND) is down. Write in the Telegram group to make sure the staff is aware." + "bad_request": "The Lightning Network Daemon (LND) is down. Write in the Telegram group to make sure the staff is aware." } order.taker_bond = LNPayment.objects.create( @@ -1197,7 +1234,8 @@ class Logics: ) order.expires_at = timezone.now() + timedelta( - seconds=order.t_to_expire(Order.Status.TAK)) + seconds=order.t_to_expire(Order.Status.TAK) + ) order.save() return True, { "bond_invoice": hold_payment["invoice"], @@ -1213,8 +1251,9 @@ class Logics: elif order.status == Order.Status.WFE: order.status = Order.Status.CHA order.expires_at = timezone.now() + timedelta( - seconds=order.t_to_expire(Order.Status.CHA)) - send_message.delay(order.id,'fiat_exchange_starts') + seconds=order.t_to_expire(Order.Status.CHA) + ) + send_message.delay(order.id, "fiat_exchange_starts") order.save() @classmethod @@ -1234,8 +1273,7 @@ class Logics: if order.expires_at < timezone.now(): cls.order_expires(order) return False, { - "bad_request": - "Invoice expired. You did not send the escrow in time." + "bad_request": "Invoice expired. You did not send the escrow in time." } # Do not gen if an escrow invoice exist. Do not return if it is already locked. Return the old one if still waiting. @@ -1250,7 +1288,9 @@ class Logics: } # If there was no taker_bond object yet, generate one - escrow_satoshis = cls.escrow_amount(order, user)[1]["escrow_amount"] # Amount was fixed when taker bond was locked, fee applied here + escrow_satoshis = cls.escrow_amount(order, user)[1][ + "escrow_amount" + ] # Amount was fixed when taker bond was locked, fee applied here if user.profile.wants_stealth: description = f"This payment WILL FREEZE IN YOUR WALLET, check on the website if it was successful. It will automatically return unless you cheat or cancel unilaterally. Payment reference: {order.reference}" else: @@ -1262,14 +1302,15 @@ class Logics: escrow_satoshis, description, invoice_expiry=order.t_to_expire(Order.Status.WF2), - cltv_expiry_blocks=cls.compute_cltv_expiry_blocks(order, "trade_escrow") + cltv_expiry_blocks=cls.compute_cltv_expiry_blocks( + order, "trade_escrow" + ), ) except Exception as e: if "status = StatusCode.UNAVAILABLE" in str(e): return False, { - "bad_request": - "The Lightning Network Daemon (LND) is down. Write in the Telegram group to make sure the staff is aware." + "bad_request": "The Lightning Network Daemon (LND) is down. Write in the Telegram group to make sure the staff is aware." } order.trade_escrow = LNPayment.objects.create( @@ -1341,7 +1382,7 @@ class Logics: raise e def cancel_onchain_payment(order): - ''' Cancel onchain_payment if existing ''' + """Cancel onchain_payment if existing""" if order.payout_tx: order.payout_tx.status = OnchainPayment.Status.CANCE @@ -1370,7 +1411,7 @@ class Logics: @classmethod def pay_buyer(cls, order): - '''Pays buyer invoice or onchain address''' + """Pays buyer invoice or onchain address""" # Pay to buyer invoice if not order.is_swap: @@ -1379,7 +1420,7 @@ class Logics: order.payout.status = LNPayment.Status.FLIGHT order.payout.save() order.save() - send_message.delay(order.id,'trade_successful') + send_message.delay(order.id, "trade_successful") order.contract_finalization_time = timezone.now() order.save() return True @@ -1395,7 +1436,7 @@ class Logics: order.payout_tx.save() order.status = Order.Status.SUC order.save() - send_message.delay(order.id,'trade_successful') + send_message.delay(order.id, "trade_successful") order.contract_finalization_time = timezone.now() order.save() return True @@ -1407,9 +1448,7 @@ class Logics: If user is buyer: fiat_sent goes to true. If User is seller and fiat_sent is true: settle the escrow and pay buyer invoice!""" - if (order.status == Order.Status.CHA - or order.status == Order.Status.FSE - ): + if order.status == Order.Status.CHA or order.status == Order.Status.FSE: # If buyer, settle escrow and mark fiat sent if cls.is_buyer(order, user): @@ -1420,25 +1459,27 @@ class Logics: elif cls.is_seller(order, user): if not order.is_fiat_sent: return False, { - "bad_request": - "You cannot confirm to have received the fiat before it is confirmed to be sent by the buyer." + "bad_request": "You cannot confirm to have received the fiat before it is confirmed to be sent by the buyer." } # Make sure the trade escrow is at least as big as the buyer invoice - num_satoshis = order.payout_tx.num_satoshis if order.is_swap else order.payout.num_satoshis + num_satoshis = ( + order.payout_tx.num_satoshis + if order.is_swap + else order.payout.num_satoshis + ) if order.trade_escrow.num_satoshis <= num_satoshis: return False, { - "bad_request": - "Woah, something broke badly. Report in the public channels, or open a Github Issue." + "bad_request": "Woah, something broke badly. Report in the public channels, or open a Github Issue." } - + # !!! KEY LINE - SETTLES THE TRADE ESCROW !!! - if cls.settle_escrow(order): + if cls.settle_escrow(order): order.trade_escrow.status = LNPayment.Status.SETLED # Double check the escrow is settled. if LNNode.double_check_htlc_is_settled(order.trade_escrow.payment_hash): - # RETURN THE BONDS + # RETURN THE BONDS cls.return_bond(order.taker_bond) cls.return_bond(order.maker_bond) ##### !!! KEY LINE - PAYS THE BUYER INVOICE !!! @@ -1454,18 +1495,16 @@ class Logics: else: return False, { - "bad_request": - "You cannot confirm the fiat payment at this stage" + "bad_request": "You cannot confirm the fiat payment at this stage" } order.save() return True, None - def pause_unpause_public_order(order,user): + def pause_unpause_public_order(order, user): if not order.maker == user: return False, { - "bad_request": - "You cannot pause or unpause an order you did not make" + "bad_request": "You cannot pause or unpause an order you did not make" } else: if order.status == Order.Status.PUB: @@ -1474,17 +1513,16 @@ class Logics: order.status = Order.Status.PUB else: return False, { - "bad_request": - "You can only pause/unpause an order that is either public or paused" + "bad_request": "You can only pause/unpause an order that is either public or paused" } order.save() return True, None @classmethod def rate_counterparty(cls, order, user, rating): - ''' + """ Not in use - ''' + """ rating_allowed_status = [ Order.Status.PAY, @@ -1507,9 +1545,7 @@ class Logics: order.taker_rated = True order.save() else: - return False, { - "bad_request": "You cannot rate your counterparty yet." - } + return False, {"bad_request": "You cannot rate your counterparty yet."} return True, None @@ -1521,31 +1557,31 @@ class Logics: @classmethod def add_rewards(cls, order): - ''' - This function is called when a trade is finished. + """ + This function is called when a trade is finished. If participants of the order were referred, the reward is given to the referees. - ''' + """ if order.maker.profile.is_referred: profile = order.maker.profile.referred_by - profile.pending_rewards += int(config('REWARD_TIP')) + profile.pending_rewards += int(config("REWARD_TIP")) profile.save() - + if order.taker.profile.is_referred: profile = order.taker.profile.referred_by - profile.pending_rewards += int(config('REWARD_TIP')) + profile.pending_rewards += int(config("REWARD_TIP")) profile.save() return @classmethod def add_slashed_rewards(cls, bond, profile): - ''' + """ When a bond is slashed due to overtime, rewards the user that was waiting. If participants of the order were referred, the reward is given to the referees. - ''' - reward_fraction = float(config('SLASHED_BOND_REWARD_SPLIT')) - reward = int(bond.num_satoshis*reward_fraction) + """ + reward_fraction = float(config("SLASHED_BOND_REWARD_SPLIT")) + reward = int(bond.num_satoshis * reward_fraction) profile.earned_rewards += reward profile.save() @@ -1568,17 +1604,17 @@ class Logics: try: lnpayment = LNPayment.objects.create( - concept= LNPayment.Concepts.WITHREWA, - type= LNPayment.Types.NORM, - sender= User.objects.get(username=ESCROW_USERNAME), - status= LNPayment.Status.VALIDI, + concept=LNPayment.Concepts.WITHREWA, + type=LNPayment.Types.NORM, + sender=User.objects.get(username=ESCROW_USERNAME), + status=LNPayment.Status.VALIDI, receiver=user, - invoice= invoice, - num_satoshis= num_satoshis, - description= reward_payout["description"], - payment_hash= reward_payout["payment_hash"], - created_at= reward_payout["created_at"], - expires_at= reward_payout["expires_at"], + invoice=invoice, + num_satoshis=num_satoshis, + description=reward_payout["description"], + payment_hash=reward_payout["payment_hash"], + created_at=reward_payout["created_at"], + expires_at=reward_payout["expires_at"], ) # Might fail if payment_hash already exists in DB except: @@ -1589,72 +1625,103 @@ class Logics: # Pays the invoice. paid, failure_reason = LNNode.pay_invoice(lnpayment) - if paid: + if paid: user.profile.earned_rewards = 0 user.profile.claimed_rewards += num_satoshis user.profile.save() return True, None # If fails, adds the rewards again. - else: + else: user.profile.earned_rewards = num_satoshis user.profile.save() context = {} - context['bad_invoice'] = failure_reason + context["bad_invoice"] = failure_reason return False, context @classmethod def summarize_trade(cls, order, user): - ''' + """ Summarizes a finished order. Returns a dict with amounts, fees, costs, etc, for buyer and seller. - ''' - if not order.status in [Order.Status.SUC, Order.Status.PAY, Order.Status.FAI]: - return False, {'bad_summary':'Order has not finished yet'} - + """ + if not order.status in [Order.Status.SUC, Order.Status.PAY, Order.Status.FAI]: + return False, {"bad_summary": "Order has not finished yet"} + context = {} - users = {'taker': order.taker, 'maker': order.maker} + users = {"taker": order.taker, "maker": order.maker} for order_user in users: summary = {} - summary['trade_fee_percent'] = FEE * MAKER_FEE_SPLIT if order_user == 'maker' else FEE * (1 - MAKER_FEE_SPLIT) - summary['bond_size_sats'] = order.maker_bond.num_satoshis if order_user == 'maker' else order.taker_bond.num_satoshis - summary['bond_size_percent'] = order.bond_size - summary['is_buyer'] = cls.is_buyer(order, users[order_user]) + summary["trade_fee_percent"] = ( + FEE * MAKER_FEE_SPLIT + if order_user == "maker" + else FEE * (1 - MAKER_FEE_SPLIT) + ) + summary["bond_size_sats"] = ( + order.maker_bond.num_satoshis + if order_user == "maker" + else order.taker_bond.num_satoshis + ) + summary["bond_size_percent"] = order.bond_size + summary["is_buyer"] = cls.is_buyer(order, users[order_user]) - if summary['is_buyer']: - summary['sent_fiat'] = order.amount + if summary["is_buyer"]: + summary["sent_fiat"] = order.amount if order.is_swap: - summary['received_sats'] = order.payout_tx.sent_satoshis + summary["received_sats"] = order.payout_tx.sent_satoshis else: - summary['received_sats'] = order.payout.num_satoshis - summary['trade_fee_sats'] = round(order.last_satoshis - summary['received_sats']) + summary["received_sats"] = order.payout.num_satoshis + summary["trade_fee_sats"] = round( + order.last_satoshis - summary["received_sats"] + ) # Only add context for swap costs if the user is the swap recipient. Peer should not know whether it was a swap if users[order_user] == user and order.is_swap: - summary['is_swap'] = order.is_swap - summary['received_onchain_sats'] = order.payout_tx.sent_satoshis - summary['mining_fee_sats'] = order.payout_tx.mining_fee_sats - summary['swap_fee_sats'] = round(order.payout_tx.num_satoshis - order.payout_tx.mining_fee_sats - order.payout_tx.sent_satoshis) - summary['swap_fee_percent'] = order.payout_tx.swap_fee_rate - summary['trade_fee_sats'] = round(order.last_satoshis - summary['received_sats'] - summary['mining_fee_sats'] - summary['swap_fee_sats']) + summary["is_swap"] = order.is_swap + summary["received_onchain_sats"] = order.payout_tx.sent_satoshis + summary["mining_fee_sats"] = order.payout_tx.mining_fee_sats + summary["swap_fee_sats"] = round( + order.payout_tx.num_satoshis + - order.payout_tx.mining_fee_sats + - order.payout_tx.sent_satoshis + ) + summary["swap_fee_percent"] = order.payout_tx.swap_fee_rate + summary["trade_fee_sats"] = round( + order.last_satoshis + - summary["received_sats"] + - summary["mining_fee_sats"] + - summary["swap_fee_sats"] + ) else: - summary['sent_sats'] = order.trade_escrow.num_satoshis - summary['received_fiat'] = order.amount - summary['trade_fee_sats'] = round(summary['sent_sats'] - order.last_satoshis ) - context[f'{order_user}_summary']=summary + summary["sent_sats"] = order.trade_escrow.num_satoshis + summary["received_fiat"] = order.amount + summary["trade_fee_sats"] = round( + summary["sent_sats"] - order.last_satoshis + ) + context[f"{order_user}_summary"] = summary platform_summary = {} - platform_summary['contract_exchange_rate'] = float(order.amount) / (float(order.last_satoshis) / 100000000) + platform_summary["contract_exchange_rate"] = float(order.amount) / ( + float(order.last_satoshis) / 100000000 + ) if order.last_satoshis_time != None: - platform_summary['contract_timestamp'] = order.last_satoshis_time - platform_summary['contract_total_time'] = order.contract_finalization_time - order.last_satoshis_time + platform_summary["contract_timestamp"] = order.last_satoshis_time + platform_summary["contract_total_time"] = ( + order.contract_finalization_time - order.last_satoshis_time + ) if not order.is_swap: - platform_summary['routing_fee_sats'] = order.payout.fee - platform_summary['trade_revenue_sats'] = int(order.trade_escrow.num_satoshis - order.payout.num_satoshis - order.payout.fee) + platform_summary["routing_fee_sats"] = order.payout.fee + platform_summary["trade_revenue_sats"] = int( + order.trade_escrow.num_satoshis + - order.payout.num_satoshis + - order.payout.fee + ) else: - platform_summary['routing_fee_sats'] = 0 - platform_summary['trade_revenue_sats'] = int(order.trade_escrow.num_satoshis - order.payout_tx.num_satoshis) - context['platform_summary'] = platform_summary + platform_summary["routing_fee_sats"] = 0 + platform_summary["trade_revenue_sats"] = int( + order.trade_escrow.num_satoshis - order.payout_tx.num_satoshis + ) + context["platform_summary"] = platform_summary - return True, context \ No newline at end of file + return True, context diff --git a/api/management/commands/clean_orders.py b/api/management/commands/clean_orders.py index a72725f1..7fd3d2a5 100644 --- a/api/management/commands/clean_orders.py +++ b/api/management/commands/clean_orders.py @@ -36,7 +36,8 @@ class Command(BaseCommand): queryset = Order.objects.exclude(status__in=do_nothing) queryset = queryset.filter( - expires_at__lt=timezone.now()) # expires at lower than now + expires_at__lt=timezone.now() + ) # expires at lower than now debug = {} debug["num_expired_orders"] = len(queryset) @@ -45,11 +46,9 @@ class Command(BaseCommand): debug["reason_failure"] = [] for idx, order in enumerate(queryset): - context = str(order) + " was " + Order.Status( - order.status).label + context = str(order) + " was " + Order.Status(order.status).label try: - if Logics.order_expires( - order): # Order send to expire here + if Logics.order_expires(order): # Order send to expire here debug["expired_orders"].append({idx: context}) # It should not happen, but if it cannot locate the hold invoice @@ -57,7 +56,7 @@ class Command(BaseCommand): except Exception as e: debug["failed_order_expiry"].append({idx: context}) debug["reason_failure"].append({idx: str(e)}) - + if "unable to locate invoice" in str(e): self.stdout.write(str(e)) order.status = Order.Status.EXP diff --git a/api/management/commands/follow_invoices.py b/api/management/commands/follow_invoices.py index ec7887df..1e845c33 100644 --- a/api/management/commands/follow_invoices.py +++ b/api/management/commands/follow_invoices.py @@ -73,18 +73,17 @@ class Command(BaseCommand): try: # this is similar to LNNnode.validate_hold_invoice_locked request = LNNode.invoicesrpc.LookupInvoiceMsg( - payment_hash=bytes.fromhex(hold_lnpayment.payment_hash)) - response = stub.LookupInvoiceV2(request, - metadata=[("macaroon", - MACAROON.hex())]) - hold_lnpayment.status = lnd_state_to_lnpayment_status[ - response.state] + payment_hash=bytes.fromhex(hold_lnpayment.payment_hash) + ) + response = stub.LookupInvoiceV2( + request, metadata=[("macaroon", MACAROON.hex())] + ) + hold_lnpayment.status = lnd_state_to_lnpayment_status[response.state] # try saving expiry height if hasattr(response, "htlcs"): try: - hold_lnpayment.expiry_height = response.htlcs[ - 0].expiry_height + hold_lnpayment.expiry_height = response.htlcs[0].expiry_height except: pass @@ -97,8 +96,7 @@ class Command(BaseCommand): # LND restarted. if "wallet locked, unlock it" in str(e): - self.stdout.write( - str(timezone.now()) + " :: Wallet Locked") + self.stdout.write(str(timezone.now()) + " :: Wallet Locked") # Other write to logs else: self.stdout.write(str(e)) @@ -114,13 +112,15 @@ class Command(BaseCommand): # Report for debugging new_status = LNPayment.Status(hold_lnpayment.status).label - debug["invoices"].append({ - idx: { - "payment_hash": str(hold_lnpayment.payment_hash), - "old_status": old_status, - "new_status": new_status, + debug["invoices"].append( + { + idx: { + "payment_hash": str(hold_lnpayment.payment_hash), + "old_status": old_status, + "new_status": new_status, + } } - }) + ) at_least_one_changed = at_least_one_changed or changed @@ -148,7 +148,8 @@ class Command(BaseCommand): status__in=[LNPayment.Status.VALIDI, LNPayment.Status.FAILRO], in_flight=False, last_routing_time__lt=( - timezone.now() - timedelta(minutes=int(config("RETRY_TIME")))), + timezone.now() - timedelta(minutes=int(config("RETRY_TIME"))) + ), ) queryset = queryset.union(queryset_retries) @@ -167,7 +168,7 @@ class Command(BaseCommand): # It is a maker bond => Publish order. if hasattr(lnpayment, "order_made"): Logics.publish_order(lnpayment.order_made) - send_message.delay(lnpayment.order_made.id,'order_published') + send_message.delay(lnpayment.order_made.id, "order_published") return # It is a taker bond => close contract. diff --git a/api/management/commands/telegram_watcher.py b/api/management/commands/telegram_watcher.py index 5723e319..547951b8 100644 --- a/api/management/commands/telegram_watcher.py +++ b/api/management/commands/telegram_watcher.py @@ -7,50 +7,54 @@ from decouple import config import requests import time + class Command(BaseCommand): help = "Polls telegram /getUpdates method" - rest = 3 # seconds between consecutive polls + rest = 3 # seconds between consecutive polls - bot_token = config('TELEGRAM_TOKEN') - updates_url = f'https://api.telegram.org/bot{bot_token}/getUpdates' + bot_token = config("TELEGRAM_TOKEN") + updates_url = f"https://api.telegram.org/bot{bot_token}/getUpdates" session = get_session() telegram = Telegram() + def handle(self, *args, **options): """Infinite loop to check for telegram updates. If it finds a new user (/start), enables it's taker found notification and sends a 'Hey {username} {order_id}' message back""" - + offset = 0 while True: time.sleep(self.rest) - params = {'offset' : offset + 1 , 'timeout' : 5} + params = {"offset": offset + 1, "timeout": 5} response = self.session.get(self.updates_url, params=params).json() - if len(list(response['result'])) == 0: + if len(list(response["result"])) == 0: continue - for result in response['result']: + for result in response["result"]: - try: # if there is no key message, skips this result. - text = result['message']['text'] + try: # if there is no key message, skips this result. + text = result["message"]["text"] except: continue - - splitted_text = text.split(' ') - if splitted_text[0] == '/start': + + splitted_text = text.split(" ") + if splitted_text[0] == "/start": token = splitted_text[-1] - try : + try: profile = Profile.objects.get(telegram_token=token) except: - print(f'No profile with token {token}') + print(f"No profile with token {token}") continue - + attempts = 5 while attempts >= 0: try: - profile.telegram_chat_id = result['message']['from']['id'] - profile.telegram_lang_code = result['message']['from']['language_code'] + profile.telegram_chat_id = result["message"]["from"]["id"] + profile.telegram_lang_code = result["message"]["from"][ + "language_code" + ] self.telegram.welcome(profile.user) profile.telegram_enabled = True profile.save() @@ -59,6 +63,4 @@ class Command(BaseCommand): time.sleep(5) attempts = attempts - 1 - offset = response['result'][-1]['update_id'] - - + offset = response["result"][-1]["update_id"] diff --git a/api/messages.py b/api/messages.py index 3fa7b846..ce3111c4 100644 --- a/api/messages.py +++ b/api/messages.py @@ -3,37 +3,38 @@ from secrets import token_urlsafe from api.models import Order from api.utils import get_session -class Telegram(): - ''' Simple telegram messages by requesting to API''' + +class Telegram: + """Simple telegram messages by requesting to API""" session = get_session() - site = config('HOST_NAME') + site = config("HOST_NAME") def get_context(user): """returns context needed to enable TG notifications""" context = {} - if user.profile.telegram_enabled : - context['tg_enabled'] = True + if user.profile.telegram_enabled: + context["tg_enabled"] = True else: - context['tg_enabled'] = False - + context["tg_enabled"] = False + if user.profile.telegram_token == None: user.profile.telegram_token = token_urlsafe(15) user.profile.save() - context['tg_token'] = user.profile.telegram_token - context['tg_bot_name'] = config("TELEGRAM_BOT_NAME") + context["tg_token"] = user.profile.telegram_token + context["tg_bot_name"] = config("TELEGRAM_BOT_NAME") return context def send_message(self, user, text): - """ sends a message to a user with telegram notifications enabled""" + """sends a message to a user with telegram notifications enabled""" - bot_token=config('TELEGRAM_TOKEN') + bot_token = config("TELEGRAM_TOKEN") chat_id = user.profile.telegram_chat_id - message_url = f'https://api.telegram.org/bot{bot_token}/sendMessage?chat_id={chat_id}&text={text}' - + message_url = f"https://api.telegram.org/bot{bot_token}/sendMessage?chat_id={chat_id}&text={text}" + # if it fails, it should keep trying while True: try: @@ -41,13 +42,13 @@ class Telegram(): return except: pass - + def welcome(self, user): - ''' User enabled Telegram Notifications''' + """User enabled Telegram Notifications""" lang = user.profile.telegram_lang_code - if lang == 'es': - text = f'Hola {user.username}, te enviaré notificaciones sobre tus órdenes en RoboSats.' + if lang == "es": + text = f"Hola {user.username}, te enviaré notificaciones sobre tus órdenes en RoboSats." else: text = f"Hey {user.username}, I will send you notifications about your RoboSats orders." self.send_message(user, text) @@ -75,18 +76,18 @@ class Telegram(): def order_taken_confirmed(self, order): if order.maker.profile.telegram_enabled: lang = order.maker.profile.telegram_lang_code - if lang == 'es': - text = f'Hey {order.maker.username} ¡Tu orden con ID {order.id} ha sido tomada por {order.taker.username}!🥳 Visita http://{self.site}/order/{order.id} para continuar.' + if lang == "es": + text = f"Hey {order.maker.username} ¡Tu orden con ID {order.id} ha sido tomada por {order.taker.username}!🥳 Visita http://{self.site}/order/{order.id} para continuar." else: - text = f'Hey {order.maker.username}, your order was taken by {order.taker.username}!🥳 Visit http://{self.site}/order/{order.id} to proceed with the trade.' + text = f"Hey {order.maker.username}, your order was taken by {order.taker.username}!🥳 Visit http://{self.site}/order/{order.id} to proceed with the trade." self.send_message(order.maker, text) if order.taker.profile.telegram_enabled: lang = order.taker.profile.telegram_lang_code - if lang == 'es': - text = f'Hey {order.taker.username}, acabas de tomar la orden con ID {order.id}.' + if lang == "es": + text = f"Hey {order.taker.username}, acabas de tomar la orden con ID {order.id}." else: - text = f'Hey {order.taker.username}, you just took the order with ID {order.id}.' + text = f"Hey {order.taker.username}, you just took the order with ID {order.id}." self.send_message(order.taker, text) return @@ -95,20 +96,20 @@ class Telegram(): for user in [order.maker, order.taker]: if user.profile.telegram_enabled: lang = user.profile.telegram_lang_code - if lang == 'es': - text = f'Hey {user.username}, el depósito de garantía y el recibo del comprador han sido recibidos. Es hora de enviar el dinero fiat. Visita http://{self.site}/order/{order.id} para hablar con tu contraparte.' + if lang == "es": + text = f"Hey {user.username}, el depósito de garantía y el recibo del comprador han sido recibidos. Es hora de enviar el dinero fiat. Visita http://{self.site}/order/{order.id} para hablar con tu contraparte." else: - text = f'Hey {user.username}, the escrow and invoice have been submitted. The fiat exchange starts now via the platform chat. Visit http://{self.site}/order/{order.id} to talk with your counterpart.' + text = f"Hey {user.username}, the escrow and invoice have been submitted. The fiat exchange starts now via the platform chat. Visit http://{self.site}/order/{order.id} to talk with your counterpart." self.send_message(user, text) return def order_expired_untaken(self, order): if order.maker.profile.telegram_enabled: lang = order.maker.profile.telegram_lang_code - if lang == 'es': - text = f'Hey {order.maker.username}, tu orden con ID {order.id} ha expirado sin ser tomada por ningún robot. Visita http://{self.site}/order/{order.id} para renovarla.' + if lang == "es": + text = f"Hey {order.maker.username}, tu orden con ID {order.id} ha expirado sin ser tomada por ningún robot. Visita http://{self.site}/order/{order.id} para renovarla." else: - text = f'Hey {order.maker.username}, your order with ID {order.id} has expired without a taker. Visit http://{self.site}/order/{order.id} to renew it.' + text = f"Hey {order.maker.username}, your order with ID {order.id} has expired without a taker. Visit http://{self.site}/order/{order.id} to renew it." self.send_message(order.maker, text) return @@ -116,42 +117,42 @@ class Telegram(): for user in [order.maker, order.taker]: if user.profile.telegram_enabled: lang = user.profile.telegram_lang_code - if lang == 'es': - text = f'¡Tu orden con ID {order.id} ha finalizado exitosamente!⚡ Únete a nosotros en @robosats_es y ayúdanos a mejorar.' + if lang == "es": + text = f"¡Tu orden con ID {order.id} ha finalizado exitosamente!⚡ Únete a nosotros en @robosats_es y ayúdanos a mejorar." else: - text = f'Your order with ID {order.id} has finished successfully!⚡ Join us @robosats and help us improve.' + text = f"Your order with ID {order.id} has finished successfully!⚡ Join us @robosats and help us improve." self.send_message(user, text) return def public_order_cancelled(self, order): if order.maker.profile.telegram_enabled: lang = order.maker.profile.telegram_lang_code - if lang == 'es': - text = f'Hey {order.maker.username}, has cancelado tu orden pública con ID {order.id}.' + if lang == "es": + text = f"Hey {order.maker.username}, has cancelado tu orden pública con ID {order.id}." else: - text = f'Hey {order.maker.username}, you have cancelled your public order with ID {order.id}.' + text = f"Hey {order.maker.username}, you have cancelled your public order with ID {order.id}." self.send_message(order.maker, text) return - + def collaborative_cancelled(self, order): for user in [order.maker, order.taker]: if user.profile.telegram_enabled: lang = user.profile.telegram_lang_code - if lang == 'es': - text = f'Hey {user.username}, tu orden con ID {str(order.id)} fue cancelada colaborativamente.' + if lang == "es": + text = f"Hey {user.username}, tu orden con ID {str(order.id)} fue cancelada colaborativamente." else: - text = f'Hey {user.username}, your order with ID {str(order.id)} has been collaboratively cancelled.' + text = f"Hey {user.username}, your order with ID {str(order.id)} has been collaboratively cancelled." self.send_message(user, text) return - + def dispute_opened(self, order): for user in [order.maker, order.taker]: if user.profile.telegram_enabled: lang = user.profile.telegram_lang_code - if lang == 'es': - text = f'Hey {user.username}, la orden con ID {str(order.id)} ha entrado en disputa.' + if lang == "es": + text = f"Hey {user.username}, la orden con ID {str(order.id)} ha entrado en disputa." else: - text = f'Hey {user.username}, a dispute has been opened on your order with ID {str(order.id)}.' + text = f"Hey {user.username}, a dispute has been opened on your order with ID {str(order.id)}." self.send_message(user, text) return @@ -163,8 +164,8 @@ class Telegram(): if len(queryset) == 0: return order = queryset.last() - if lang == 'es': - text = f'Hey {order.maker.username}, tu orden con ID {str(order.id)} es pública en el libro de ordenes.' + if lang == "es": + text = f"Hey {order.maker.username}, tu orden con ID {str(order.id)} es pública en el libro de ordenes." else: text = f"Hey {order.maker.username}, your order with ID {str(order.id)} is public in the order book." self.send_message(order.maker, text) diff --git a/api/models.py b/api/models.py index 94996584..386c7314 100644 --- a/api/models.py +++ b/api/models.py @@ -29,12 +29,11 @@ DEFAULT_BOND_SIZE = float(config("DEFAULT_BOND_SIZE")) class Currency(models.Model): currency_dict = json.load(open("frontend/static/assets/currencies.json")) - currency_choices = [(int(val), label) - for val, label in list(currency_dict.items())] + currency_choices = [(int(val), label) for val, label in list(currency_dict.items())] - currency = models.PositiveSmallIntegerField(choices=currency_choices, - null=False, - unique=True) + currency = models.PositiveSmallIntegerField( + choices=currency_choices, null=False, unique=True + ) exchange_rate = models.DecimalField( max_digits=14, decimal_places=4, @@ -54,7 +53,6 @@ class Currency(models.Model): class LNPayment(models.Model): - class Types(models.IntegerChoices): NORM = 0, "Regular invoice" HOLD = 1, "hold invoice" @@ -80,77 +78,78 @@ class LNPayment(models.Model): class FailureReason(models.IntegerChoices): NOTYETF = 0, "Payment isn't failed (yet)" - TIMEOUT = 1, "There are more routes to try, but the payment timeout was exceeded." - NOROUTE = 2, "All possible routes were tried and failed permanently. Or there were no routes to the destination at all." + TIMEOUT = ( + 1, + "There are more routes to try, but the payment timeout was exceeded.", + ) + NOROUTE = ( + 2, + "All possible routes were tried and failed permanently. Or there were no routes to the destination at all.", + ) NONRECO = 3, "A non-recoverable error has occurred." - INCORRE = 4, "Payment details are incorrect (unknown hash, invalid amount or invalid final CLTV delta)." + INCORRE = ( + 4, + "Payment details are incorrect (unknown hash, invalid amount or invalid final CLTV delta).", + ) NOBALAN = 5, "Insufficient unlocked balance in RoboSats' node." # payment use details - type = models.PositiveSmallIntegerField(choices=Types.choices, - null=False, - default=Types.HOLD) - concept = models.PositiveSmallIntegerField(choices=Concepts.choices, - null=False, - default=Concepts.MAKEBOND) - status = models.PositiveSmallIntegerField(choices=Status.choices, - null=False, - default=Status.INVGEN) - failure_reason = models.PositiveSmallIntegerField(choices=FailureReason.choices, - null=True, - default=None) + type = models.PositiveSmallIntegerField( + choices=Types.choices, null=False, default=Types.HOLD + ) + concept = models.PositiveSmallIntegerField( + choices=Concepts.choices, null=False, default=Concepts.MAKEBOND + ) + status = models.PositiveSmallIntegerField( + choices=Status.choices, null=False, default=Status.INVGEN + ) + failure_reason = models.PositiveSmallIntegerField( + choices=FailureReason.choices, null=True, default=None + ) # payment info - payment_hash = models.CharField(max_length=100, - unique=True, - default=None, - blank=True, - primary_key=True) + payment_hash = models.CharField( + max_length=100, unique=True, default=None, blank=True, primary_key=True + ) invoice = models.CharField( - max_length=1200, unique=True, null=True, default=None, - blank=True) # Some invoices with lots of routing hints might be long - preimage = models.CharField(max_length=64, - unique=True, - null=True, - default=None, - blank=True) - description = models.CharField(max_length=500, - unique=False, - null=True, - default=None, - blank=True) - num_satoshis = models.PositiveBigIntegerField(validators=[ - MinValueValidator(100), - MaxValueValidator(1.5 * MAX_TRADE), - ]) + max_length=1200, unique=True, null=True, default=None, blank=True + ) # Some invoices with lots of routing hints might be long + preimage = models.CharField( + max_length=64, unique=True, null=True, default=None, blank=True + ) + description = models.CharField( + max_length=500, unique=False, null=True, default=None, blank=True + ) + num_satoshis = models.PositiveBigIntegerField( + validators=[ + MinValueValidator(100), + MaxValueValidator(1.5 * MAX_TRADE), + ] + ) # Fee in sats with mSats decimals fee_msat - fee = models.DecimalField(max_digits=10, decimal_places=3, default=0, null=False, blank=False) + fee = models.DecimalField( + max_digits=10, decimal_places=3, default=0, null=False, blank=False + ) created_at = models.DateTimeField() expires_at = models.DateTimeField() - cltv_expiry = models.PositiveSmallIntegerField(null=True, - default=None, - blank=True) - expiry_height = models.PositiveBigIntegerField(null=True, - default=None, - blank=True) + cltv_expiry = models.PositiveSmallIntegerField(null=True, default=None, blank=True) + expiry_height = models.PositiveBigIntegerField(null=True, default=None, blank=True) # routing routing_attempts = models.PositiveSmallIntegerField(null=False, default=0) - last_routing_time = models.DateTimeField(null=True, - default=None, - blank=True) + last_routing_time = models.DateTimeField(null=True, default=None, blank=True) in_flight = models.BooleanField(default=False, null=False, blank=False) # involved parties - sender = models.ForeignKey(User, - related_name="sender", - on_delete=models.SET_NULL, - null=True, - default=None) - receiver = models.ForeignKey(User, - related_name="receiver", - on_delete=models.SET_NULL, - null=True, - default=None) + sender = models.ForeignKey( + User, related_name="sender", on_delete=models.SET_NULL, null=True, default=None + ) + receiver = models.ForeignKey( + User, + related_name="receiver", + on_delete=models.SET_NULL, + null=True, + default=None, + ) def __str__(self): return f"LN-{str(self.payment_hash)[:8]}: {self.Concepts(self.concept).label} - {self.Status(self.status).label}" @@ -166,89 +165,89 @@ class LNPayment(models.Model): # We created a truncated property for display 'hash' return truncatechars(self.payment_hash, 10) -class OnchainPayment(models.Model): +class OnchainPayment(models.Model): class Concepts(models.IntegerChoices): PAYBUYER = 3, "Payment to buyer" class Status(models.IntegerChoices): - CREAT = 0, "Created" # User was given platform fees and suggested mining fees - VALID = 1, "Valid" # Valid onchain address submitted - MEMPO = 2, "In mempool" # Tx is sent to mempool - CONFI = 3, "Confirmed" # Tx is confirme +2 blocks - CANCE = 4, "Cancelled" # Cancelled tx + CREAT = 0, "Created" # User was given platform fees and suggested mining fees + VALID = 1, "Valid" # Valid onchain address submitted + MEMPO = 2, "In mempool" # Tx is sent to mempool + CONFI = 3, "Confirmed" # Tx is confirme +2 blocks + CANCE = 4, "Cancelled" # Cancelled tx def get_balance(): balance = BalanceLog.objects.create() return balance.time # payment use details - concept = models.PositiveSmallIntegerField(choices=Concepts.choices, - null=False, - default=Concepts.PAYBUYER) - status = models.PositiveSmallIntegerField(choices=Status.choices, - null=False, - default=Status.CREAT) + concept = models.PositiveSmallIntegerField( + choices=Concepts.choices, null=False, default=Concepts.PAYBUYER + ) + status = models.PositiveSmallIntegerField( + choices=Status.choices, null=False, default=Status.CREAT + ) # payment info - address = models.CharField(max_length=100, - unique=False, - default=None, - null=True, - blank=True) - - txid = models.CharField(max_length=64, - unique=True, - null=True, - default=None, - blank=True) + address = models.CharField( + max_length=100, unique=False, default=None, null=True, blank=True + ) - num_satoshis = models.PositiveBigIntegerField(null=True, - validators=[ - MinValueValidator(0.5 * MIN_SWAP_AMOUNT), - MaxValueValidator(1.5 * MAX_TRADE), - ]) - sent_satoshis = models.PositiveBigIntegerField(null=True, - validators=[ - MinValueValidator(0.5 * MIN_SWAP_AMOUNT), - MaxValueValidator(1.5 * MAX_TRADE), - ]) + txid = models.CharField( + max_length=64, unique=True, null=True, default=None, blank=True + ) + + num_satoshis = models.PositiveBigIntegerField( + null=True, + validators=[ + MinValueValidator(0.5 * MIN_SWAP_AMOUNT), + MaxValueValidator(1.5 * MAX_TRADE), + ], + ) + sent_satoshis = models.PositiveBigIntegerField( + null=True, + validators=[ + MinValueValidator(0.5 * MIN_SWAP_AMOUNT), + MaxValueValidator(1.5 * MAX_TRADE), + ], + ) # fee in sats/vbyte with mSats decimals fee_msat - suggested_mining_fee_rate = models.DecimalField(max_digits=6, - decimal_places=3, - default=1.05, - null=False, - blank=False) - mining_fee_rate = models.DecimalField(max_digits=6, - decimal_places=3, - default=1.05, - null=False, - blank=False) - mining_fee_sats = models.PositiveBigIntegerField(default=0, - null=False, - blank=False) + suggested_mining_fee_rate = models.DecimalField( + max_digits=6, decimal_places=3, default=1.05, null=False, blank=False + ) + mining_fee_rate = models.DecimalField( + max_digits=6, decimal_places=3, default=1.05, null=False, blank=False + ) + mining_fee_sats = models.PositiveBigIntegerField(default=0, null=False, blank=False) # platform onchain/channels balance at creation, swap fee rate as percent of total volume - balance = models.ForeignKey(BalanceLog, - related_name="balance", - on_delete=models.SET_NULL, - null=True, - default=get_balance) + balance = models.ForeignKey( + BalanceLog, + related_name="balance", + on_delete=models.SET_NULL, + null=True, + default=get_balance, + ) - swap_fee_rate = models.DecimalField(max_digits=4, - decimal_places=2, - default=float(config("MIN_SWAP_FEE"))*100, - null=False, - blank=False) + swap_fee_rate = models.DecimalField( + max_digits=4, + decimal_places=2, + default=float(config("MIN_SWAP_FEE")) * 100, + null=False, + blank=False, + ) created_at = models.DateTimeField(default=timezone.now) # involved parties - receiver = models.ForeignKey(User, - related_name="tx_receiver", - on_delete=models.SET_NULL, - null=True, - default=None) + receiver = models.ForeignKey( + User, + related_name="tx_receiver", + on_delete=models.SET_NULL, + null=True, + default=None, + ) def __str__(self): return f"TX-{str(self.id)}: {self.Concepts(self.concept).label} - {self.Status(self.status).label}" @@ -262,8 +261,8 @@ class OnchainPayment(models.Model): # Display txid as 'hash' truncated return truncatechars(self.txid, 10) -class Order(models.Model): +class Order(models.Model): class Types(models.IntegerChoices): BUY = 0, "BUY" SELL = 1, "SELL" @@ -297,30 +296,30 @@ class Order(models.Model): NESINV = 4, "Neither escrow locked or invoice submitted" # order info - reference = models.UUIDField(default = uuid.uuid4, editable = False) - status = models.PositiveSmallIntegerField(choices=Status.choices, - null=False, - default=Status.WFB) + reference = models.UUIDField(default=uuid.uuid4, editable=False) + status = models.PositiveSmallIntegerField( + choices=Status.choices, null=False, default=Status.WFB + ) created_at = models.DateTimeField(default=timezone.now) expires_at = models.DateTimeField() - expiry_reason = models.PositiveSmallIntegerField(choices=ExpiryReasons.choices, - null=True, - blank=True, - default=None) + expiry_reason = models.PositiveSmallIntegerField( + choices=ExpiryReasons.choices, null=True, blank=True, default=None + ) # order details type = models.PositiveSmallIntegerField(choices=Types.choices, null=False) - currency = models.ForeignKey(Currency, - null=True, - on_delete=models.SET_NULL) + currency = models.ForeignKey(Currency, null=True, on_delete=models.SET_NULL) amount = models.DecimalField(max_digits=18, decimal_places=8, null=True, blank=True) has_range = models.BooleanField(default=False, null=False, blank=False) - min_amount = models.DecimalField(max_digits=18, decimal_places=8, null=True, blank=True) - max_amount = models.DecimalField(max_digits=18, decimal_places=8, null=True, blank=True) - payment_method = models.CharField(max_length=70, - null=False, - default="not specified", - blank=True) + min_amount = models.DecimalField( + max_digits=18, decimal_places=8, null=True, blank=True + ) + max_amount = models.DecimalField( + max_digits=18, decimal_places=8, null=True, blank=True + ) + payment_method = models.CharField( + max_length=70, null=False, default="not specified", blank=True + ) bondless_taker = models.BooleanField(default=False, null=False, blank=False) # order pricing method. A explicit amount of sats, or a relative premium above/below market. is_explicit = models.BooleanField(default=False, null=False) @@ -330,37 +329,37 @@ class Order(models.Model): decimal_places=2, default=0, null=True, - validators=[MinValueValidator(-100), - MaxValueValidator(999)], + validators=[MinValueValidator(-100), MaxValueValidator(999)], blank=True, ) # explicit satoshis = models.PositiveBigIntegerField( null=True, - validators=[ - MinValueValidator(MIN_TRADE), - MaxValueValidator(MAX_TRADE) - ], + validators=[MinValueValidator(MIN_TRADE), MaxValueValidator(MAX_TRADE)], blank=True, ) # optionally makers can choose the public order duration length (seconds) public_duration = models.PositiveBigIntegerField( - default=60*60*int(config("DEFAULT_PUBLIC_ORDER_DURATION"))-1, + default=60 * 60 * int(config("DEFAULT_PUBLIC_ORDER_DURATION")) - 1, null=False, validators=[ - MinValueValidator(60*60*float(config("MIN_PUBLIC_ORDER_DURATION"))), # Min is 10 minutes - MaxValueValidator(60*60*float(config("MAX_PUBLIC_ORDER_DURATION"))), # Max is 24 Hours + MinValueValidator( + 60 * 60 * float(config("MIN_PUBLIC_ORDER_DURATION")) + ), # Min is 10 minutes + MaxValueValidator( + 60 * 60 * float(config("MAX_PUBLIC_ORDER_DURATION")) + ), # Max is 24 Hours ], blank=False, ) # optionally makers can choose the escrow lock / invoice submission step length (seconds) escrow_duration = models.PositiveBigIntegerField( - default=60 * int(config("INVOICE_AND_ESCROW_DURATION"))-1, + default=60 * int(config("INVOICE_AND_ESCROW_DURATION")) - 1, null=False, validators=[ - MinValueValidator(60*30), # Min is 30 minutes - MaxValueValidator(60*60*8), # Max is 8 Hours + MinValueValidator(60 * 30), # Min is 30 minutes + MaxValueValidator(60 * 60 * 8), # Max is 8 Hours ], blank=False, ) @@ -372,8 +371,8 @@ class Order(models.Model): default=DEFAULT_BOND_SIZE, null=False, validators=[ - MinValueValidator(float(config("MIN_BOND_SIZE"))), # 1 % - MaxValueValidator(float(config("MAX_BOND_SIZE"))), # 15 % + MinValueValidator(float(config("MIN_BOND_SIZE"))), # 1 % + MaxValueValidator(float(config("MAX_BOND_SIZE"))), # 15 % ], blank=False, ) @@ -381,29 +380,24 @@ class Order(models.Model): # how many sats at creation and at last check (relevant for marked to market) t0_satoshis = models.PositiveBigIntegerField( null=True, - validators=[ - MinValueValidator(MIN_TRADE), - MaxValueValidator(MAX_TRADE) - ], + validators=[MinValueValidator(MIN_TRADE), MaxValueValidator(MAX_TRADE)], blank=True, ) # sats at creation last_satoshis = models.PositiveBigIntegerField( null=True, - validators=[MinValueValidator(0), - MaxValueValidator(MAX_TRADE * 2)], + validators=[MinValueValidator(0), MaxValueValidator(MAX_TRADE * 2)], blank=True, ) # sats last time checked. Weird if 2* trade max... # timestamp of last_satoshis last_satoshis_time = models.DateTimeField(null=True, default=None, blank=True) # time the fiat exchange is confirmed and Sats released to buyer - contract_finalization_time = models.DateTimeField(null=True, default=None, blank=True) + contract_finalization_time = models.DateTimeField( + null=True, default=None, blank=True + ) # order participants maker = models.ForeignKey( - User, - related_name="maker", - on_delete=models.SET_NULL, - null=True, - default=None) # unique = True, a maker can only make one order + User, related_name="maker", on_delete=models.SET_NULL, null=True, default=None + ) # unique = True, a maker can only make one order taker = models.ForeignKey( User, related_name="taker", @@ -416,21 +410,19 @@ class Order(models.Model): taker_last_seen = models.DateTimeField(null=True, default=None, blank=True) # When collaborative cancel is needed and one partner has cancelled. - maker_asked_cancel = models.BooleanField(default=False, null=False) + maker_asked_cancel = models.BooleanField(default=False, null=False) taker_asked_cancel = models.BooleanField(default=False, null=False) is_fiat_sent = models.BooleanField(default=False, null=False) # in dispute is_disputed = models.BooleanField(default=False, null=False) - maker_statement = models.TextField(max_length=5000, - null=True, - default=None, - blank=True) - taker_statement = models.TextField(max_length=5000, - null=True, - default=None, - blank=True) + maker_statement = models.TextField( + max_length=5000, null=True, default=None, blank=True + ) + taker_statement = models.TextField( + max_length=5000, null=True, default=None, blank=True + ) # LNpayments # Order collateral @@ -487,7 +479,7 @@ class Order(models.Model): def __str__(self): if self.has_range and self.amount == None: - amt = str(float(self.min_amount))+"-"+ str(float(self.max_amount)) + amt = str(float(self.min_amount)) + "-" + str(float(self.max_amount)) else: amt = float(self.amount) return f"Order {self.id}: {self.Types(self.type).label} BTC for {amt} {self.currency}" @@ -495,27 +487,33 @@ class Order(models.Model): def t_to_expire(self, status): t_to_expire = { - 0: int(config("EXP_MAKER_BOND_INVOICE")), # 'Waiting for maker bond' - 1: self.public_duration, # 'Public' - 2: 0, # 'Deleted' - 3: int(config("EXP_TAKER_BOND_INVOICE")), # 'Waiting for taker bond' - 4: 0, # 'Cancelled' - 5: 0, # 'Expired' - 6: int(self.escrow_duration), # 'Waiting for trade collateral and buyer invoice' - 7: int(self.escrow_duration), # 'Waiting only for seller trade collateral' - 8: int(self.escrow_duration), # 'Waiting only for buyer invoice' - 9: 60 * 60 * int(config("FIAT_EXCHANGE_DURATION")), # 'Sending fiat - In chatroom' - 10: 60 * 60 * int(config("FIAT_EXCHANGE_DURATION")),# 'Fiat sent - In chatroom' - 11: 1 * 24 * 60 * 60, # 'In dispute' - 12: 0, # 'Collaboratively cancelled' - 13: 100 * 24 * 60 * 60, # 'Sending satoshis to buyer' - 14: 100 * 24 * 60 * 60, # 'Sucessful trade' - 15: 100 * 24 * 60 * 60, # 'Failed lightning network routing' - 16: 100 * 24 * 60 * 60, # 'Wait for dispute resolution' - 17: 100 * 24 * 60 * 60, # 'Maker lost dispute' - 18: 100 * 24 * 60 * 60, # 'Taker lost dispute' + 0: int(config("EXP_MAKER_BOND_INVOICE")), # 'Waiting for maker bond' + 1: self.public_duration, # 'Public' + 2: 0, # 'Deleted' + 3: int(config("EXP_TAKER_BOND_INVOICE")), # 'Waiting for taker bond' + 4: 0, # 'Cancelled' + 5: 0, # 'Expired' + 6: int( + self.escrow_duration + ), # 'Waiting for trade collateral and buyer invoice' + 7: int(self.escrow_duration), # 'Waiting only for seller trade collateral' + 8: int(self.escrow_duration), # 'Waiting only for buyer invoice' + 9: 60 + * 60 + * int(config("FIAT_EXCHANGE_DURATION")), # 'Sending fiat - In chatroom' + 10: 60 + * 60 + * int(config("FIAT_EXCHANGE_DURATION")), # 'Fiat sent - In chatroom' + 11: 1 * 24 * 60 * 60, # 'In dispute' + 12: 0, # 'Collaboratively cancelled' + 13: 100 * 24 * 60 * 60, # 'Sending satoshis to buyer' + 14: 100 * 24 * 60 * 60, # 'Sucessful trade' + 15: 100 * 24 * 60 * 60, # 'Failed lightning network routing' + 16: 100 * 24 * 60 * 60, # 'Wait for dispute resolution' + 17: 100 * 24 * 60 * 60, # 'Maker lost dispute' + 18: 100 * 24 * 60 * 60, # 'Taker lost dispute' } - + return t_to_expire[status] @@ -570,53 +568,27 @@ class Profile(models.Model): decimal_places=1, default=None, null=True, - validators=[MinValueValidator(0), - MaxValueValidator(100)], + validators=[MinValueValidator(0), MaxValueValidator(100)], blank=True, ) # Used to deep link telegram chat in case telegram notifications are enabled - telegram_token = models.CharField( - max_length=20, - null=True, - blank=True - ) - telegram_chat_id = models.BigIntegerField( - null=True, - default=None, - blank=True - ) - telegram_enabled = models.BooleanField( - default=False, - null=False - ) - telegram_lang_code = models.CharField( - max_length=10, - null=True, - blank=True - ) - telegram_welcomed = models.BooleanField( - default=False, - null=False - ) + telegram_token = models.CharField(max_length=20, null=True, blank=True) + telegram_chat_id = models.BigIntegerField(null=True, default=None, blank=True) + telegram_enabled = models.BooleanField(default=False, null=False) + telegram_lang_code = models.CharField(max_length=10, null=True, blank=True) + telegram_welcomed = models.BooleanField(default=False, null=False) # Referral program - is_referred = models.BooleanField( - default=False, - null=False - ) + is_referred = models.BooleanField(default=False, null=False) referred_by = models.ForeignKey( - 'self', + "self", related_name="referee", on_delete=models.SET_NULL, null=True, default=None, blank=True, ) - referral_code = models.CharField( - max_length=15, - null=True, - blank=True - ) + referral_code = models.CharField(max_length=15, null=True, blank=True) # Recent rewards from referred trades that will be "earned" at a later point to difficult spionage. pending_rewards = models.PositiveIntegerField(null=False, default=0) # Claimable rewards @@ -644,18 +616,13 @@ class Profile(models.Model): ) # Penalty expiration (only used then taking/cancelling repeatedly orders in the book before comitting bond) - penalty_expiration = models.DateTimeField(null=True, - default=None, - blank=True) + penalty_expiration = models.DateTimeField(null=True, default=None, blank=True) # Platform rate - platform_rating = models.PositiveIntegerField(null=True, - default=None, - blank=True) + platform_rating = models.PositiveIntegerField(null=True, default=None, blank=True) # Stealth invoices - wants_stealth = models.BooleanField(default=True, - null=False) + wants_stealth = models.BooleanField(default=True, null=False) @receiver(post_save, sender=User) def create_user_profile(sender, instance, created, **kwargs): @@ -669,8 +636,9 @@ class Profile(models.Model): @receiver(pre_delete, sender=User) def del_avatar_from_disk(sender, instance, **kwargs): try: - avatar_file = Path(settings.AVATAR_ROOT + - instance.profile.avatar.url.split("/")[-1]) + avatar_file = Path( + settings.AVATAR_ROOT + instance.profile.avatar.url.split("/")[-1] + ) avatar_file.unlink() except: pass @@ -686,8 +654,7 @@ class Profile(models.Model): # method to create a fake table field in read only mode def avatar_tag(self): - return mark_safe('' % - self.get_avatar()) + return mark_safe('' % self.get_avatar()) class MarketTick(models.Model): @@ -723,13 +690,10 @@ class MarketTick(models.Model): decimal_places=2, default=None, null=True, - validators=[MinValueValidator(-100), - MaxValueValidator(999)], + validators=[MinValueValidator(-100), MaxValueValidator(999)], blank=True, ) - currency = models.ForeignKey(Currency, - null=True, - on_delete=models.SET_NULL) + currency = models.ForeignKey(Currency, null=True, on_delete=models.SET_NULL) timestamp = models.DateTimeField(default=timezone.now) # Relevant to keep record of the historical fee, so the insight on the premium can be better analyzed @@ -737,8 +701,7 @@ class MarketTick(models.Model): max_digits=4, decimal_places=4, default=FEE, - validators=[MinValueValidator(0), - MaxValueValidator(1)], + validators=[MinValueValidator(0), MaxValueValidator(1)], ) def log_a_tick(order): @@ -755,10 +718,9 @@ class MarketTick(models.Model): market_exchange_rate = float(order.currency.exchange_rate) premium = 100 * (price / market_exchange_rate - 1) - tick = MarketTick.objects.create(price=price, - volume=volume, - premium=premium, - currency=order.currency) + tick = MarketTick.objects.create( + price=price, volume=volume, premium=premium, currency=order.currency + ) tick.save() @@ -767,4 +729,4 @@ class MarketTick(models.Model): class Meta: verbose_name = "Market tick" - verbose_name_plural = "Market ticks" \ No newline at end of file + verbose_name_plural = "Market ticks" diff --git a/api/nick_generator/dicts/en/adjectives.py b/api/nick_generator/dicts/en/adjectives.py index cf9d1757..15f8a4cd 100755 --- a/api/nick_generator/dicts/en/adjectives.py +++ b/api/nick_generator/dicts/en/adjectives.py @@ -4823,8 +4823,7 @@ adjectives = [ "Vindictive", "Chatting", "Nightmarish", - "Niggardly" - "Hated", + "Niggardly" "Hated", "Satiric", "Shattering", "Fabled", diff --git a/api/nick_generator/nick_generator.py b/api/nick_generator/nick_generator.py index 395566c6..d60e2dfe 100755 --- a/api/nick_generator/nick_generator.py +++ b/api/nick_generator/nick_generator.py @@ -2,6 +2,7 @@ from .utils import human_format import hashlib import time + """ Deterministic nick generator from SHA256 hash. @@ -15,7 +16,6 @@ is a total of to 450*4800*12500*1000 = class NickGenerator: - def __init__( self, lang="English", @@ -43,11 +43,13 @@ class NickGenerator: raise ValueError("Language not implemented.") if verbose: - print(f"{lang} SHA256 Nick Generator initialized with:" + - f"\nUp to {len(adverbs)} adverbs." + - f"\nUp to {len(adjectives)} adjectives." + - f"\nUp to {len(nouns)} nouns." + - f"\nUp to {max_num+1} numerics.\n") + print( + f"{lang} SHA256 Nick Generator initialized with:" + + f"\nUp to {len(adverbs)} adverbs." + + f"\nUp to {len(adjectives)} adjectives." + + f"\nUp to {len(nouns)} nouns." + + f"\nUp to {max_num+1} numerics.\n" + ) self.use_adv = use_adv self.use_adj = use_adj @@ -147,10 +149,7 @@ class NickGenerator: i = i + 1 return "", 0, 0, i - def compute_pool_size_loss(self, - max_length=22, - max_iter=1000000, - num_runs=5000): + def compute_pool_size_loss(self, max_length=22, max_iter=1000000, num_runs=5000): """ Computes median an average loss of nick pool diversity due to max_lenght @@ -196,13 +195,16 @@ if __name__ == "__main__": # Generates a short nick with length limit from SHA256 nick, nick_id, pool_size, iterations = GenNick.short_from_SHA256( - hash, max_length, max_iter) + hash, max_length, max_iter + ) # Output - print(f"Nick number {nick_id} has been selected among" + - f" {human_format(pool_size)} possible nicks.\n" + - f"Needed {iterations} iterations to find one " + - f"this short.\nYour nick is {nick} !\n") + print( + f"Nick number {nick_id} has been selected among" + + f" {human_format(pool_size)} possible nicks.\n" + + f"Needed {iterations} iterations to find one " + + f"this short.\nYour nick is {nick} !\n" + ) print(f"Nick lenght is {len(nick)} characters.") print(f"Nick landed at height {nick_id/(pool_size+1)} on the pool.") print(f"Took {time.time()-t0} secs.\n") @@ -216,9 +218,8 @@ if __name__ == "__main__": string = str(random.uniform(0, 1000000)) hash = hashlib.sha256(str.encode(string)).hexdigest() print( - GenNick.short_from_SHA256(hash, - max_length=max_length, - max_iter=max_iter)[0]) + GenNick.short_from_SHA256(hash, max_length=max_length, max_iter=max_iter)[0] + ) # Other analysis GenNick.compute_pool_size_loss(max_length, max_iter, 200) diff --git a/api/nick_generator/utils.py b/api/nick_generator/utils.py index 9f2c957f..0786ca93 100755 --- a/api/nick_generator/utils.py +++ b/api/nick_generator/utils.py @@ -2,9 +2,7 @@ from math import log, floor def human_format(number): - units = [ - "", " Thousand", " Million", " Billion", " Trillion", " Quatrillion" - ] + units = ["", " Thousand", " Million", " Billion", " Trillion", " Quatrillion"] k = 1000.0 magnitude = int(floor(log(number, k))) return "%.2f%s" % (number / k**magnitude, units[magnitude]) diff --git a/api/serializers.py b/api/serializers.py index eb33e179..d12c150f 100644 --- a/api/serializers.py +++ b/api/serializers.py @@ -3,17 +3,26 @@ from .models import MarketTick, Order from decouple import config RETRY_TIME = int(config("RETRY_TIME")) -MIN_PUBLIC_ORDER_DURATION_SECS=60*60*float(config("MIN_PUBLIC_ORDER_DURATION")) -MAX_PUBLIC_ORDER_DURATION_SECS=60*60*float(config("MAX_PUBLIC_ORDER_DURATION")) +MIN_PUBLIC_ORDER_DURATION_SECS = 60 * 60 * float(config("MIN_PUBLIC_ORDER_DURATION")) +MAX_PUBLIC_ORDER_DURATION_SECS = 60 * 60 * float(config("MAX_PUBLIC_ORDER_DURATION")) + class InfoSerializer(serializers.Serializer): num_public_buy_orders = serializers.IntegerField() num_public_sell_orders = serializers.IntegerField() - book_liquidity = serializers.IntegerField(help_text='Total amount of BTC in the order book') + book_liquidity = serializers.IntegerField( + help_text="Total amount of BTC in the order book" + ) active_robots_today = serializers.CharField() - last_day_nonkyc_btc_premium = serializers.FloatField(help_text='Average premium (weighted by volume) of the orders in the last 24h') - last_day_volume = serializers.FloatField(help_text='Total volume in BTC in the last 24h') - lifetime_volume = serializers.FloatField(help_text='Total volume in BTC since exchange\'s inception') + last_day_nonkyc_btc_premium = serializers.FloatField( + help_text="Average premium (weighted by volume) of the orders in the last 24h" + ) + last_day_volume = serializers.FloatField( + help_text="Total volume in BTC in the last 24h" + ) + lifetime_volume = serializers.FloatField( + help_text="Total volume in BTC since exchange's inception" + ) lnd_version = serializers.CharField() robosats_running_commit_hash = serializers.CharField() alternative_site = serializers.CharField() @@ -21,17 +30,20 @@ class InfoSerializer(serializers.Serializer): node_alias = serializers.CharField() node_id = serializers.CharField() network = serializers.CharField() - maker_fee = serializers.FloatField(help_text='Exchange\'s set maker fee') - taker_fee = serializers.FloatField(help_text='Exchange\'s set taker fee ') - bond_size = serializers.FloatField(help_text='Default bond size (percent)') - current_swap_fee_rate = serializers.FloatField(help_text='Swap fees to perform on-chain transaction (percent)') - nickname = serializers.CharField(help_text='Currenlty logged in Robot name') - referral_code = serializers.CharField(help_text='Logged in users\'s referral code') - earned_rewards = serializers.IntegerField(help_text='Logged in user\'s earned rewards in satoshis') + maker_fee = serializers.FloatField(help_text="Exchange's set maker fee") + taker_fee = serializers.FloatField(help_text="Exchange's set taker fee ") + bond_size = serializers.FloatField(help_text="Default bond size (percent)") + current_swap_fee_rate = serializers.FloatField( + help_text="Swap fees to perform on-chain transaction (percent)" + ) + nickname = serializers.CharField(help_text="Currenlty logged in Robot name") + referral_code = serializers.CharField(help_text="Logged in users's referral code") + earned_rewards = serializers.IntegerField( + help_text="Logged in user's earned rewards in satoshis" + ) class ListOrderSerializer(serializers.ModelSerializer): - class Meta: model = Order fields = ( @@ -53,51 +65,46 @@ class ListOrderSerializer(serializers.ModelSerializer): "maker", "taker", "escrow_duration", - "bond_size" + "bond_size", ) # Only used in oas_schemas class SummarySerializer(serializers.Serializer): sent_fiat = serializers.IntegerField( - required=False, - help_text="same as `amount` (only for buyer)" + required=False, help_text="same as `amount` (only for buyer)" ) received_sats = serializers.IntegerField( - required=False, - help_text="same as `trade_satoshis` (only for buyer)" + required=False, help_text="same as `trade_satoshis` (only for buyer)" ) is_swap = serializers.BooleanField( - required=False, - help_text="True if the payout was on-chain (only for buyer)" + required=False, help_text="True if the payout was on-chain (only for buyer)" ) received_onchain_sats = serializers.IntegerField( required=False, - help_text="The on-chain sats received (only for buyer and if `is_swap` is `true`)" + help_text="The on-chain sats received (only for buyer and if `is_swap` is `true`)", ) mining_fee_sats = serializers.IntegerField( required=False, - help_text="Mining fees paid in satoshis (only for buyer and if `is_swap` is `true`)" + help_text="Mining fees paid in satoshis (only for buyer and if `is_swap` is `true`)", ) swap_fee_sats = serializers.IntegerField( required=False, - help_text="Exchange swap fee in sats (i.e excluding miner fees) (only for buyer and if `is_swap` is `true`)" + help_text="Exchange swap fee in sats (i.e excluding miner fees) (only for buyer and if `is_swap` is `true`)", ) swap_fee_percent = serializers.FloatField( required=False, - help_text="same as `swap_fee_rate` (only for buyer and if `is_swap` is `true`" + help_text="same as `swap_fee_rate` (only for buyer and if `is_swap` is `true`", ) sent_sats = serializers.IntegerField( - required=False, - help_text="The total sats you sent (only for seller)" + required=False, help_text="The total sats you sent (only for seller)" ) received_fiat = serializers.IntegerField( - required=False, - help_text="same as `amount` (only for seller)" + required=False, help_text="same as `amount` (only for seller)" ) trade_fee_sats = serializers.IntegerField( required=False, - help_text="Exchange fees in sats (Does not include swap fee and miner fee)" + help_text="Exchange fees in sats (Does not include swap fee and miner fee)", ) @@ -105,45 +112,42 @@ class SummarySerializer(serializers.Serializer): class PlatformSummarySerializer(serializers.Serializer): contract_timestamp = serializers.DateTimeField( required=False, - help_text="Timestamp of when the contract was finalized (price and sats fixed)" + help_text="Timestamp of when the contract was finalized (price and sats fixed)", ) contract_total_time = serializers.FloatField( required=False, - help_text="The time taken for the contract to complete (from taker taking the order to completion of order) in seconds" + help_text="The time taken for the contract to complete (from taker taking the order to completion of order) in seconds", ) routing_fee_sats = serializers.IntegerField( required=False, - help_text="Sats payed by the exchange for routing fees. Mining fee in case of on-chain swap payout" + help_text="Sats payed by the exchange for routing fees. Mining fee in case of on-chain swap payout", ) trade_revenue_sats = serializers.IntegerField( - required=False, - help_text="The sats the exchange earned from the trade" + required=False, help_text="The sats the exchange earned from the trade" ) - + # Only used in oas_schemas class OrderDetailSerializer(serializers.ModelSerializer): total_secs_exp = serializers.IntegerField( required=False, help_text="Duration of time (in seconds) to expire, according to the current status of order." - "This is duration of time after `created_at` (in seconds) that the order will automatically expire." - "This value changes according to which stage the order is in" + "This is duration of time after `created_at` (in seconds) that the order will automatically expire." + "This value changes according to which stage the order is in", ) penalty = serializers.DateTimeField( required=False, - help_text="Time when the user penalty will expire. Penalty applies when you create orders repeatedly without commiting a bond" + help_text="Time when the user penalty will expire. Penalty applies when you create orders repeatedly without commiting a bond", ) is_maker = serializers.BooleanField( - required=False, - help_text="Whether you are the maker or not" + required=False, help_text="Whether you are the maker or not" ) is_taker = serializers.BooleanField( - required=False, - help_text="Whether you are the taker or not" + required=False, help_text="Whether you are the taker or not" ) is_participant = serializers.BooleanField( required=False, - help_text="True if you are either a taker or maker, False otherwise" + help_text="True if you are either a taker or maker, False otherwise", ) maker_status = serializers.CharField( required=False, @@ -151,193 +155,170 @@ class OrderDetailSerializer(serializers.ModelSerializer): "- **'Active'** (seen within last 2 min)\n" "- **'Seen Recently'** (seen within last 10 min)\n" "- **'Inactive'** (seen more than 10 min ago)\n\n" - "Note: When you make a request to this route, your own status get's updated and can be seen by your counterparty" + "Note: When you make a request to this route, your own status get's updated and can be seen by your counterparty", ) taker_status = serializers.BooleanField( required=False, - help_text="True if you are either a taker or maker, False otherwise" + help_text="True if you are either a taker or maker, False otherwise", ) price_now = serializers.IntegerField( required=False, - help_text="Price of the order in the order's currency at the time of request (upto 5 significant digits)" + help_text="Price of the order in the order's currency at the time of request (upto 5 significant digits)", ) premium = serializers.IntegerField( - required=False, - help_text="Premium over the CEX price at the current time" + required=False, help_text="Premium over the CEX price at the current time" ) premium_percentile = serializers.IntegerField( required=False, - help_text="(Only if `is_maker`) Premium percentile of your order compared to other public orders in the same currency currently in the order book" + help_text="(Only if `is_maker`) Premium percentile of your order compared to other public orders in the same currency currently in the order book", ) num_similar_orders = serializers.IntegerField( required=False, - help_text="(Only if `is_maker`) The number of public orders of the same currency currently in the order book" + help_text="(Only if `is_maker`) The number of public orders of the same currency currently in the order book", ) tg_enabled = serializers.BooleanField( required=False, - help_text="(Only if `is_maker`) Whether Telegram notification is enabled or not" + help_text="(Only if `is_maker`) Whether Telegram notification is enabled or not", ) tg_token = serializers.CharField( required=False, - help_text="(Only if `is_maker`) Your telegram bot token required to enable notifications." + help_text="(Only if `is_maker`) Your telegram bot token required to enable notifications.", ) tg_bot_name = serializers.CharField( required=False, - help_text="(Only if `is_maker`) The Telegram username of the bot" + help_text="(Only if `is_maker`) The Telegram username of the bot", ) is_buyer = serializers.BooleanField( required=False, - help_text="Whether you are a buyer of sats (you will be receiving sats)" + help_text="Whether you are a buyer of sats (you will be receiving sats)", ) is_seller = serializers.BooleanField( required=False, - help_text="Whether you are a seller of sats or not (you will be sending sats)" + help_text="Whether you are a seller of sats or not (you will be sending sats)", ) maker_nick = serializers.CharField( - required=False, - help_text="Nickname (Robot name) of the maker" + required=False, help_text="Nickname (Robot name) of the maker" ) taker_nick = serializers.CharField( - required=False, - help_text="Nickname (Robot name) of the taker" + required=False, help_text="Nickname (Robot name) of the taker" ) status_message = serializers.CharField( required=False, - help_text="The current status of the order corresponding to the `status`" + help_text="The current status of the order corresponding to the `status`", ) is_fiat_sent = serializers.BooleanField( - required=False, - help_text="Whether or not the fiat amount is sent by the buyer" + required=False, help_text="Whether or not the fiat amount is sent by the buyer" ) is_disputed = serializers.BooleanField( - required=False, - help_text="Whether or not the counterparty raised a dispute" - ) - ur_nick = serializers.CharField( - required=False, - help_text="Your Nickname" - ) - ur_nick = serializers.CharField( - required=False, - help_text="Your Nick" + required=False, help_text="Whether or not the counterparty raised a dispute" ) + ur_nick = serializers.CharField(required=False, help_text="Your Nickname") + ur_nick = serializers.CharField(required=False, help_text="Your Nick") maker_locked = serializers.BooleanField( - required=False, - help_text="True if maker bond is locked, False otherwise" + required=False, help_text="True if maker bond is locked, False otherwise" ) taker_locked = serializers.BooleanField( - required=False, - help_text="True if taker bond is locked, False otherwise" + required=False, help_text="True if taker bond is locked, False otherwise" ) escrow_locked = serializers.BooleanField( required=False, - help_text="True if escrow is locked, False otherwise. Escrow is the sats to be sold, held by Robosats until the trade is finised." + help_text="True if escrow is locked, False otherwise. Escrow is the sats to be sold, held by Robosats until the trade is finised.", ) trade_satoshis = serializers.IntegerField( required=False, - help_text="Seller sees the amount of sats they need to send. Buyer sees the amount of sats they will receive " + help_text="Seller sees the amount of sats they need to send. Buyer sees the amount of sats they will receive ", ) bond_invoice = serializers.CharField( - required=False, - help_text="When `status` = `0`, `3`. Bond invoice to be paid" + required=False, help_text="When `status` = `0`, `3`. Bond invoice to be paid" ) bond_satoshis = serializers.IntegerField( - required=False, - help_text="The bond amount in satoshis" + required=False, help_text="The bond amount in satoshis" ) escrow_invoice = serializers.CharField( required=False, - help_text="For the seller, the escrow invoice to be held by RoboSats" + help_text="For the seller, the escrow invoice to be held by RoboSats", ) escrow_satoshis = serializers.IntegerField( - required=False, - help_text="The escrow amount in satoshis" + required=False, help_text="The escrow amount in satoshis" ) invoice_amount = serializers.IntegerField( required=False, - help_text="The amount in sats the buyer needs to submit an invoice of to receive the trade amount" + help_text="The amount in sats the buyer needs to submit an invoice of to receive the trade amount", ) swap_allowed = serializers.BooleanField( - required=False, - help_text="Whether on-chain swap is allowed" + required=False, help_text="Whether on-chain swap is allowed" ) swap_failure_reason = serializers.CharField( - required=False, - help_text="Reason for why on-chain swap is not available" + required=False, help_text="Reason for why on-chain swap is not available" ) suggested_mining_fee_rate = serializers.IntegerField( - required=False, - help_text="fee in sats/vbyte for the on-chain swap" + required=False, help_text="fee in sats/vbyte for the on-chain swap" ) - swap_fee_rate = serializers.FloatField( + swap_fee_rate = serializers.FloatField( required=False, - help_text="in percentage, the swap fee rate the platform charges" + help_text="in percentage, the swap fee rate the platform charges", ) pending_cancel = serializers.BooleanField( required=False, - help_text="Your counterparty requested for a collaborative cancel when `status` is either `8`, `9` or `10`" + help_text="Your counterparty requested for a collaborative cancel when `status` is either `8`, `9` or `10`", ) asked_for_cancel = serializers.BooleanField( required=False, - help_text="You requested for a collaborative cancel `status` is either `8`, `9` or `10`" + help_text="You requested for a collaborative cancel `status` is either `8`, `9` or `10`", ) statement_submitted = serializers.BooleanField( required=False, - help_text="True if you have submitted a statement. Available when `status` is `11`" + help_text="True if you have submitted a statement. Available when `status` is `11`", ) retries = serializers.IntegerField( required=False, - help_text="Number of times ln node has tried to make the payment to you (only if you are the buyer)" + help_text="Number of times ln node has tried to make the payment to you (only if you are the buyer)", ) next_retry_time = serializers.DateTimeField( required=False, - help_text=f"The next time payment will be retried. Payment is retried every {RETRY_TIME} sec" + help_text=f"The next time payment will be retried. Payment is retried every {RETRY_TIME} sec", ) failure_reason = serializers.CharField( - required=False, - help_text="The reason the payout failed" + required=False, help_text="The reason the payout failed" ) invoice_expired = serializers.BooleanField( required=False, - help_text="True if the payout invoice expired. `invoice_amount` will be re-set and sent which means the user has to submit a new invoice to be payed" + help_text="True if the payout invoice expired. `invoice_amount` will be re-set and sent which means the user has to submit a new invoice to be payed", ) trade_fee_percent = serializers.IntegerField( required=False, - help_text="The fee for the trade (fees differ for maker and taker)" + help_text="The fee for the trade (fees differ for maker and taker)", ) bond_size_sats = serializers.IntegerField( - required=False, - help_text="The size of the bond in sats" + required=False, help_text="The size of the bond in sats" ) bond_size_percent = serializers.IntegerField( - required=False, - help_text="same as `bond_size`" + required=False, help_text="same as `bond_size`" ) maker_summary = SummarySerializer(required=False) taker_summary = SummarySerializer(required=False) platform_summary = PlatformSummarySerializer(required=True) expiry_message = serializers.CharField( required=False, - help_text="The reason the order expired (message associated with the `expiry_reason`)" + help_text="The reason the order expired (message associated with the `expiry_reason`)", ) num_satoshis = serializers.IntegerField( required=False, - help_text="only if status = `14` (Successful Trade) and is_buyer = `true`" + help_text="only if status = `14` (Successful Trade) and is_buyer = `true`", ) sent_satoshis = serializers.IntegerField( required=False, - help_text="only if status = `14` (Successful Trade) and is_buyer = `true`" + help_text="only if status = `14` (Successful Trade) and is_buyer = `true`", ) txid = serializers.CharField( required=False, - help_text="Transaction id of the on-chain swap payout. Only if status = `14` (Successful Trade) and is_buyer = `true`" + help_text="Transaction id of the on-chain swap payout. Only if status = `14` (Successful Trade) and is_buyer = `true`", ) network = serializers.CharField( required=False, - help_text="The network eg. 'testnet', 'mainnet'. Only if status = `14` (Successful Trade) and is_buyer = `true`" + help_text="The network eg. 'testnet', 'mainnet'. Only if status = `14` (Successful Trade) and is_buyer = `true`", ) - class Meta: model = Order fields = ( @@ -392,7 +373,7 @@ class OrderDetailSerializer(serializers.ModelSerializer): "escrow_satoshis", "invoice_amount", "swap_allowed", - 'swap_failure_reason', + "swap_failure_reason", "suggested_mining_fee_rate", "swap_fee_rate", "pending_cancel", @@ -421,9 +402,16 @@ class OrderDetailSerializer(serializers.ModelSerializer): class OrderPublicSerializer(serializers.ModelSerializer): maker_nick = serializers.CharField(required=False) - maker_status = serializers.CharField(help_text='Status of the nick - "Active" or "Inactive"', required=False) - price = serializers.FloatField(help_text="Price in order's fiat currency", required=False) - satoshis_now = serializers.IntegerField(help_text="The amount of sats to be traded at the present moment (not including the fees)", required=False) + maker_status = serializers.CharField( + help_text='Status of the nick - "Active" or "Inactive"', required=False + ) + price = serializers.FloatField( + help_text="Price in order's fiat currency", required=False + ) + satoshis_now = serializers.IntegerField( + help_text="The amount of sats to be traded at the present moment (not including the fees)", + required=False, + ) class Meta: model = Order @@ -448,7 +436,7 @@ class OrderPublicSerializer(serializers.ModelSerializer): "price", "escrow_duration", "satoshis_now", - "bond_size" + "bond_size", ) @@ -461,19 +449,19 @@ class MakeOrderSerializer(serializers.ModelSerializer): max_length=70, default="not specified", required=False, - help_text="Can be any string. The UI recognizes [these payment methods](https://github.com/Reckless-Satoshi/robosats/blob/main/frontend/src/components/payment-methods/Methods.js) and displays them with a logo." + help_text="Can be any string. The UI recognizes [these payment methods](https://github.com/Reckless-Satoshi/robosats/blob/main/frontend/src/components/payment-methods/Methods.js) and displays them with a logo.", ) is_explicit = serializers.BooleanField( default=False, - help_text='Whether the order is explicitly priced or not. If set to `true` then `satoshis` need to be specified' + help_text="Whether the order is explicitly priced or not. If set to `true` then `satoshis` need to be specified", ) has_range = serializers.BooleanField( default=False, - help_text='Whether the order specifies a range of amount or a fixed amount.\n\nIf `true`, then `min_amount` and `max_amount` fields are **required**.\n\n If `false` then `amount` is **required**', + help_text="Whether the order specifies a range of amount or a fixed amount.\n\nIf `true`, then `min_amount` and `max_amount` fields are **required**.\n\n If `false` then `amount` is **required**", ) bondless_taker = serializers.BooleanField( default=False, - help_text='Whether bondless takers are allowed for this order or not', + help_text="Whether bondless takers are allowed for this order or not", ) class Meta: @@ -495,19 +483,17 @@ class MakeOrderSerializer(serializers.ModelSerializer): "bondless_taker", ) + class UpdateOrderSerializer(serializers.Serializer): - invoice = serializers.CharField(max_length=2000, - allow_null=True, - allow_blank=True, - default=None) - address = serializers.CharField(max_length=100, - allow_null=True, - allow_blank=True, - default=None) - statement = serializers.CharField(max_length=10000, - allow_null=True, - allow_blank=True, - default=None) + invoice = serializers.CharField( + max_length=2000, allow_null=True, allow_blank=True, default=None + ) + address = serializers.CharField( + max_length=100, allow_null=True, allow_blank=True, default=None + ) + statement = serializers.CharField( + max_length=10000, allow_null=True, allow_blank=True, default=None + ) action = serializers.ChoiceField( choices=( "pause", @@ -529,64 +515,86 @@ class UpdateOrderSerializer(serializers.Serializer): allow_blank=True, default=None, ) - amount = serializers.DecimalField(max_digits=18, decimal_places=8, allow_null=True, required=False, default=None) - mining_fee_rate = serializers.DecimalField(max_digits=6, decimal_places=3, allow_null=True, required=False, default=None) + amount = serializers.DecimalField( + max_digits=18, decimal_places=8, allow_null=True, required=False, default=None + ) + mining_fee_rate = serializers.DecimalField( + max_digits=6, decimal_places=3, allow_null=True, required=False, default=None + ) + class UserGenSerializer(serializers.Serializer): # Mandatory fields token_sha256 = serializers.CharField( - min_length=64, - max_length=64, - allow_null=False, - allow_blank=False, - required=True, - help_text="SHA256 of user secret") - public_key = serializers.CharField(max_length=2000, - allow_null=False, - allow_blank=False, - required=True, - help_text="Armored ASCII PGP public key block") - encrypted_private_key = serializers.CharField(max_length=2000, - allow_null=False, - allow_blank=False, - required=True, - help_text="Armored ASCII PGP encrypted private key block") + min_length=64, + max_length=64, + allow_null=False, + allow_blank=False, + required=True, + help_text="SHA256 of user secret", + ) + public_key = serializers.CharField( + max_length=2000, + allow_null=False, + allow_blank=False, + required=True, + help_text="Armored ASCII PGP public key block", + ) + encrypted_private_key = serializers.CharField( + max_length=2000, + allow_null=False, + allow_blank=False, + required=True, + help_text="Armored ASCII PGP encrypted private key block", + ) # Optional fields - ref_code = serializers.CharField(max_length=30, - allow_null=True, - allow_blank=True, - required=False, - default=None, - help_text="Referal code") - counts = serializers.ListField(child=serializers.IntegerField(), - allow_null=True, - required=False, - default=None, - help_text="Counts of the unique characters in the token") - length = serializers.IntegerField(allow_null=True, - default=None, - required=False, - min_value=1, - help_text="Length of the token") - unique_values = serializers.IntegerField(allow_null=True, - default=None, - required=False, - min_value=1, - help_text="Number of unique values in the token") + ref_code = serializers.CharField( + max_length=30, + allow_null=True, + allow_blank=True, + required=False, + default=None, + help_text="Referal code", + ) + counts = serializers.ListField( + child=serializers.IntegerField(), + allow_null=True, + required=False, + default=None, + help_text="Counts of the unique characters in the token", + ) + length = serializers.IntegerField( + allow_null=True, + default=None, + required=False, + min_value=1, + help_text="Length of the token", + ) + unique_values = serializers.IntegerField( + allow_null=True, + default=None, + required=False, + min_value=1, + help_text="Number of unique values in the token", + ) + class ClaimRewardSerializer(serializers.Serializer): - invoice = serializers.CharField(max_length=2000, - allow_null=True, - allow_blank=True, - default=None, - help_text="A valid LN invoice with the reward amount to withdraw") + invoice = serializers.CharField( + max_length=2000, + allow_null=True, + allow_blank=True, + default=None, + help_text="A valid LN invoice with the reward amount to withdraw", + ) + class PriceSerializer(serializers.Serializer): pass -class TickSerializer(serializers.ModelSerializer): +class TickSerializer(serializers.ModelSerializer): class Meta: model = MarketTick fields = ( @@ -599,5 +607,6 @@ class TickSerializer(serializers.ModelSerializer): ) depth = 1 + class StealthSerializer(serializers.Serializer): wantsStealth = serializers.BooleanField() diff --git a/api/tasks.py b/api/tasks.py index 9767961e..8dbbcbbe 100644 --- a/api/tasks.py +++ b/api/tasks.py @@ -1,5 +1,6 @@ from celery import shared_task + @shared_task(name="users_cleansing") def users_cleansing(): """ @@ -21,7 +22,11 @@ def users_cleansing(): for user in queryset: # Try an except, due to unknown cause for users lacking profiles. try: - if user.profile.pending_rewards > 0 or user.profile.earned_rewards > 0 or user.profile.claimed_rewards > 0: + if ( + user.profile.pending_rewards > 0 + or user.profile.earned_rewards > 0 + or user.profile.claimed_rewards > 0 + ): continue if not user.profile.total_contracts == 0: continue @@ -38,6 +43,7 @@ def users_cleansing(): } return results + @shared_task(name="give_rewards") def give_rewards(): """ @@ -57,10 +63,14 @@ def give_rewards(): profile.pending_rewards = 0 profile.save() - results[profile.user.username] = {'given_reward':given_reward,'earned_rewards':profile.earned_rewards} + results[profile.user.username] = { + "given_reward": given_reward, + "earned_rewards": profile.earned_rewards, + } return results + @shared_task(name="follow_send_payment") def follow_send_payment(hash): """Sends sats to buyer, continuous update""" @@ -75,10 +85,10 @@ def follow_send_payment(hash): lnpayment = LNPayment.objects.get(payment_hash=hash) fee_limit_sat = int( max( - lnpayment.num_satoshis * - float(config("PROPORTIONAL_ROUTING_FEE_LIMIT")), + lnpayment.num_satoshis * float(config("PROPORTIONAL_ROUTING_FEE_LIMIT")), float(config("MIN_FLAT_ROUTING_FEE_LIMIT")), - )) # 1000 ppm or 10 sats + ) + ) # 1000 ppm or 10 sats timeout_seconds = int(config("PAYOUT_TIMEOUT_SECONDS")) request = LNNode.routerrpc.SendPaymentRequest( @@ -89,15 +99,13 @@ def follow_send_payment(hash): order = lnpayment.order_paid_LN try: - for response in LNNode.routerstub.SendPaymentV2(request, - metadata=[ - ("macaroon", - MACAROON.hex()) - ]): - + for response in LNNode.routerstub.SendPaymentV2( + request, metadata=[("macaroon", MACAROON.hex())] + ): + lnpayment.in_flight = True lnpayment.save() - + if response.status == 0: # Status 0 'UNKNOWN' # Not sure when this status happens lnpayment.in_flight = False @@ -125,18 +133,20 @@ def follow_send_payment(hash): order.status = Order.Status.FAI order.expires_at = timezone.now() + timedelta( - seconds=order.t_to_expire(Order.Status.FAI)) + seconds=order.t_to_expire(Order.Status.FAI) + ) order.save() context = { - "routing_failed": - LNNode.payment_failure_context[response.failure_reason], - "IN_FLIGHT":False, + "routing_failed": LNNode.payment_failure_context[ + response.failure_reason + ], + "IN_FLIGHT": False, } print(context) # If failed due to not route, reset mission control. (This won't scale well, just a temporary fix) # ResetMC deactivate temporary for tests - #if response.failure_reason==2: + # if response.failure_reason==2: # LNNode.resetmc() return False, context @@ -144,12 +154,13 @@ def follow_send_payment(hash): if response.status == 2: # Status 2 'SUCCEEDED' print("SUCCEEDED") lnpayment.status = LNPayment.Status.SUCCED - lnpayment.fee = float(response.fee_msat)/1000 + lnpayment.fee = float(response.fee_msat) / 1000 lnpayment.preimage = response.payment_preimage lnpayment.save() order.status = Order.Status.SUC order.expires_at = timezone.now() + timedelta( - seconds=order.t_to_expire(Order.Status.SUC)) + seconds=order.t_to_expire(Order.Status.SUC) + ) order.save() return True, None @@ -162,17 +173,19 @@ def follow_send_payment(hash): lnpayment.save() order.status = Order.Status.FAI order.expires_at = timezone.now() + timedelta( - seconds=order.t_to_expire(Order.Status.FAI)) + seconds=order.t_to_expire(Order.Status.FAI) + ) order.save() context = {"routing_failed": "The payout invoice has expired"} return False, context + @shared_task(name="payments_cleansing") def payments_cleansing(): """ - Deletes cancelled payments (hodl invoices never locked) that + Deletes cancelled payments (hodl invoices never locked) that belong to orders expired more than 3 days ago. - Deletes 'cancelled' or 'create' onchain_payments + Deletes 'cancelled' or 'create' onchain_payments """ from django.db.models import Q @@ -185,10 +198,11 @@ def payments_cleansing(): # Usually expiry is 1 day for every finished order. So ~4 days until # a never locked hodl invoice is removed. finished_time = timezone.now() - timedelta(days=3) - queryset = LNPayment.objects.filter(Q(status=LNPayment.Status.CANCEL), - Q(order_made__expires_at__lt=finished_time)| - Q(order_taken__expires_at__lt=finished_time)) - + queryset = LNPayment.objects.filter( + Q(status=LNPayment.Status.CANCEL), + Q(order_made__expires_at__lt=finished_time) + | Q(order_taken__expires_at__lt=finished_time), + ) # And do not have an active trade, any past contract or any reward. deleted_lnpayments = [] @@ -200,10 +214,12 @@ def payments_cleansing(): deleted_lnpayments.append(name) except: pass - + # same for onchain payments - queryset = OnchainPayment.objects.filter(Q(status__in=[OnchainPayment.Status.CANCE, OnchainPayment.Status.CREAT]), - Q(order_paid_TX__expires_at__lt=finished_time)|Q(order_paid_TX__isnull=True)) + queryset = OnchainPayment.objects.filter( + Q(status__in=[OnchainPayment.Status.CANCE, OnchainPayment.Status.CREAT]), + Q(order_paid_TX__expires_at__lt=finished_time) | Q(order_paid_TX__isnull=True), + ) # And do not have an active trade, any past contract or any reward. deleted_onchainpayments = [] @@ -224,6 +240,7 @@ def payments_cleansing(): } return results + @shared_task(name="cache_external_market_prices", ignore_result=True) def cache_market(): @@ -236,7 +253,9 @@ def cache_market(): exchange_rates = get_exchange_rates(currency_codes) results = {} - for i in range(len(Currency.currency_dict.values())): # currencies are indexed starting at 1 (USD) + for i in range( + len(Currency.currency_dict.values()) + ): # currencies are indexed starting at 1 (USD) rate = exchange_rates[i] results[i] = {currency_codes[i], rate} @@ -259,45 +278,48 @@ def cache_market(): return results + @shared_task(name="send_message", ignore_result=True) def send_message(order_id, message): from api.models import Order + order = Order.objects.get(id=order_id) if not order.maker.profile.telegram_enabled: return from api.messages import Telegram + telegram = Telegram() - if message == 'welcome': + if message == "welcome": telegram.welcome(order) - - elif message == 'order_expired_untaken': + + elif message == "order_expired_untaken": telegram.order_expired_untaken(order) - elif message == 'trade_successful': + elif message == "trade_successful": telegram.trade_successful(order) - elif message == 'public_order_cancelled': + elif message == "public_order_cancelled": telegram.public_order_cancelled(order) - elif message == 'taker_expired_b4bond': + elif message == "taker_expired_b4bond": telegram.taker_expired_b4bond(order) - elif message == 'order_published': + elif message == "order_published": telegram.order_published(order) - - elif message == 'order_taken_confirmed': + + elif message == "order_taken_confirmed": telegram.order_taken_confirmed(order) - - elif message == 'fiat_exchange_starts': + + elif message == "fiat_exchange_starts": telegram.fiat_exchange_starts(order) - elif message == 'dispute_opened': + elif message == "dispute_opened": telegram.dispute_opened(order) - elif message == 'collaborative_cancelled': + elif message == "collaborative_cancelled": telegram.collaborative_cancelled(order) - return \ No newline at end of file + return diff --git a/api/urls.py b/api/urls.py index 446d656a..d893508c 100644 --- a/api/urls.py +++ b/api/urls.py @@ -1,16 +1,27 @@ from django.urls import path -from .views import MakerView, OrderView, UserView, BookView, InfoView, RewardView, PriceView, LimitView, HistoricalView, TickView, StealthView +from .views import ( + MakerView, + OrderView, + UserView, + BookView, + InfoView, + RewardView, + PriceView, + LimitView, + HistoricalView, + TickView, + StealthView, +) from drf_spectacular.views import SpectacularAPIView, SpectacularRedocView from chat.views import ChatView urlpatterns = [ - path('schema/', SpectacularAPIView.as_view(), name='schema'), - path('', SpectacularRedocView.as_view(url_name='schema'), name='redoc'), + path("schema/", SpectacularAPIView.as_view(), name="schema"), + path("", SpectacularRedocView.as_view(url_name="schema"), name="redoc"), path("make/", MakerView.as_view()), - path("order/",OrderView.as_view({ - "get": "get", - "post": "take_update_confirm_dispute_cancel" - }), + path( + "order/", + OrderView.as_view({"get": "get", "post": "take_update_confirm_dispute_cancel"}), ), path("user/", UserView.as_view()), path("book/", BookView.as_view()), @@ -21,5 +32,5 @@ urlpatterns = [ path("historical/", HistoricalView.as_view()), path("ticks/", TickView.as_view()), path("stealth/", StealthView.as_view()), - path("chat/", ChatView.as_view({"get": "get","post":"post"})), + path("chat/", ChatView.as_view({"get": "get", "post": "post"})), ] diff --git a/api/utils.py b/api/utils.py index 117a0768..e7134303 100644 --- a/api/utils.py +++ b/api/utils.py @@ -7,17 +7,20 @@ from decouple import config from api.models import Order -logger = logging.getLogger('api.utils') +logger = logging.getLogger("api.utils") + +TOR_PROXY = config("TOR_PROXY", default="127.0.0.1:9050") +USE_TOR = config("USE_TOR", cast=bool, default=True) -TOR_PROXY = config('TOR_PROXY', default='127.0.0.1:9050') -USE_TOR = config('USE_TOR', cast=bool, default=True) def get_session(): session = requests.session() # Tor uses the 9050 port as the default socks port if USE_TOR: - session.proxies = {'http': 'socks5://' + TOR_PROXY, - 'https': 'socks5://' + TOR_PROXY} + session.proxies = { + "http": "socks5://" + TOR_PROXY, + "https": "socks5://" + TOR_PROXY, + } return session @@ -29,22 +32,19 @@ def bitcoind_rpc(method, params=None): :return: """ - BITCOIND_RPCURL = config('BITCOIND_RPCURL') - BITCOIND_RPCUSER = config('BITCOIND_RPCUSER') - BITCOIND_RPCPASSWORD = config('BITCOIND_RPCPASSWORD') + BITCOIND_RPCURL = config("BITCOIND_RPCURL") + BITCOIND_RPCUSER = config("BITCOIND_RPCUSER") + BITCOIND_RPCPASSWORD = config("BITCOIND_RPCPASSWORD") if params is None: params = [] payload = json.dumps( - { - "jsonrpc": "2.0", - "id": "robosats", - "method": method, - "params": params - } + {"jsonrpc": "2.0", "id": "robosats", "method": method, "params": params} ) - return requests.post(BITCOIND_RPCURL, auth=(BITCOIND_RPCUSER, BITCOIND_RPCPASSWORD), data=payload).json()['result'] + return requests.post( + BITCOIND_RPCURL, auth=(BITCOIND_RPCUSER, BITCOIND_RPCPASSWORD), data=payload + ).json()["result"] def validate_onchain_address(address): @@ -53,17 +53,21 @@ def validate_onchain_address(address): """ try: - validation = bitcoind_rpc('validateaddress', [address]) - if not validation['isvalid']: + validation = bitcoind_rpc("validateaddress", [address]) + if not validation["isvalid"]: return False, {"bad_address": "Invalid address"} except Exception as e: logger.error(e) - return False, {"bad_address": 'Unable to validate address, check bitcoind backend'} + return False, { + "bad_address": "Unable to validate address, check bitcoind backend" + } return True, None market_cache = {} + + @ring.dict(market_cache, expire=3) # keeps in cache for 3 seconds def get_exchange_rates(currencies): """ @@ -74,8 +78,7 @@ def get_exchange_rates(currencies): session = get_session() - APIS = config("MARKET_PRICE_APIS", - cast=lambda v: [s.strip() for s in v.split(",")]) + APIS = config("MARKET_PRICE_APIS", cast=lambda v: [s.strip() for s in v.split(",")]) api_rates = [] for api_url in APIS: @@ -86,7 +89,8 @@ def get_exchange_rates(currencies): for currency in currencies: try: # If a currency is missing place a None blockchain_rates.append( - float(blockchain_prices[currency]["last"])) + float(blockchain_prices[currency]["last"]) + ) except: blockchain_rates.append(np.nan) api_rates.append(blockchain_rates) @@ -96,8 +100,7 @@ def get_exchange_rates(currencies): yadio_rates = [] for currency in currencies: try: - yadio_rates.append(float( - yadio_prices["BTC"][currency])) + yadio_rates.append(float(yadio_prices["BTC"][currency])) except: yadio_rates.append(np.nan) api_rates.append(yadio_rates) @@ -133,6 +136,8 @@ def get_lnd_version(): robosats_commit_cache = {} + + @ring.dict(robosats_commit_cache, expire=3600) def get_robosats_commit(): @@ -140,13 +145,16 @@ def get_robosats_commit(): commit_hash = commit.read() # .git folder is included in .dockerignore. But automatic build will drop in a commit_sha.txt file on root - if commit_hash == None or commit_hash =="": + if commit_hash == None or commit_hash == "": with open("commit_sha.txt") as f: commit_hash = f.read() return commit_hash + robosats_version_cache = {} + + @ring.dict(robosats_commit_cache, expire=99999) def get_robosats_version(): @@ -156,12 +164,16 @@ def get_robosats_version(): print(version_dict) return version_dict + premium_percentile = {} + + @ring.dict(premium_percentile, expire=300) def compute_premium_percentile(order): queryset = Order.objects.filter( - currency=order.currency, status=Order.Status.PUB, type=order.type).exclude(id=order.id) + currency=order.currency, status=Order.Status.PUB, type=order.type + ).exclude(id=order.id) print(len(queryset)) if len(queryset) <= 1: @@ -171,15 +183,18 @@ def compute_premium_percentile(order): order_rate = float(order.last_satoshis) / float(amount) rates = [] for similar_order in queryset: - similar_order_amount = similar_order.amount if not similar_order.has_range else similar_order.max_amount - rates.append( - float(similar_order.last_satoshis) / float(similar_order_amount)) + similar_order_amount = ( + similar_order.amount + if not similar_order.has_range + else similar_order.max_amount + ) + rates.append(float(similar_order.last_satoshis) / float(similar_order_amount)) rates = np.array(rates) return round(np.sum(rates < order_rate) / len(rates), 2) -def weighted_median(values, sample_weight=None, quantiles= 0.5, values_sorted=False): +def weighted_median(values, sample_weight=None, quantiles=0.5, values_sorted=False): """Very close to numpy.percentile, but it supports weights. NOTE: quantiles should be in [0, 1]! :param values: numpy.array with data @@ -194,8 +209,9 @@ def weighted_median(values, sample_weight=None, quantiles= 0.5, values_sorted=Fa if sample_weight is None: sample_weight = np.ones(len(values)) sample_weight = np.array(sample_weight) - assert np.all(quantiles >= 0) and np.all(quantiles <= 1), \ - 'quantiles should be in [0, 1]' + assert np.all(quantiles >= 0) and np.all( + quantiles <= 1 + ), "quantiles should be in [0, 1]" if not values_sorted: sorter = np.argsort(values) @@ -208,6 +224,7 @@ def weighted_median(values, sample_weight=None, quantiles= 0.5, values_sorted=Fa return np.interp(quantiles, weighted_quantiles, values) + def compute_avg_premium(queryset): premiums = [] volumes = [] @@ -221,11 +238,10 @@ def compute_avg_premium(queryset): total_volume = sum(volumes) # weighted_median_premium is the weighted median of the premiums by volume - if len(premiums) > 0 and len(volumes)>0: - weighted_median_premium = weighted_median(values=premiums, - sample_weight=volumes, - quantiles=0.5, - values_sorted=False) + if len(premiums) > 0 and len(volumes) > 0: + weighted_median_premium = weighted_median( + values=premiums, sample_weight=volumes, quantiles=0.5, values_sorted=False + ) else: weighted_median_premium = 0.0 return weighted_median_premium, total_volume diff --git a/api/views.py b/api/views.py index 2b782032..398be825 100644 --- a/api/views.py +++ b/api/views.py @@ -7,16 +7,45 @@ from rest_framework.response import Response from django.contrib.auth import authenticate, login, logout from django.contrib.auth.models import User -from api.oas_schemas import BookViewSchema, HistoricalViewSchema, InfoViewSchema, LimitViewSchema, MakerViewSchema, OrderViewSchema, PriceViewSchema, RewardViewSchema, StealthViewSchema, TickViewSchema, UserViewSchema +from api.oas_schemas import ( + BookViewSchema, + HistoricalViewSchema, + InfoViewSchema, + LimitViewSchema, + MakerViewSchema, + OrderViewSchema, + PriceViewSchema, + RewardViewSchema, + StealthViewSchema, + TickViewSchema, + UserViewSchema, +) from chat.views import ChatView -from api.serializers import InfoSerializer, ListOrderSerializer, MakeOrderSerializer, OrderPublicSerializer, UpdateOrderSerializer, ClaimRewardSerializer, PriceSerializer, UserGenSerializer, TickSerializer, StealthSerializer +from api.serializers import ( + InfoSerializer, + ListOrderSerializer, + MakeOrderSerializer, + OrderPublicSerializer, + UpdateOrderSerializer, + ClaimRewardSerializer, + PriceSerializer, + UserGenSerializer, + TickSerializer, + StealthSerializer, +) from api.models import LNPayment, MarketTick, OnchainPayment, Order, Currency, Profile from control.models import AccountingDay, BalanceLog from api.logics import Logics from api.messages import Telegram from secrets import token_urlsafe -from api.utils import get_lnd_version, get_robosats_commit, get_robosats_version, compute_premium_percentile, compute_avg_premium +from api.utils import ( + get_lnd_version, + get_robosats_commit, + get_robosats_version, + compute_premium_percentile, + compute_avg_premium, +) from .nick_generator.nick_generator import NickGenerator from robohash import Robohash @@ -31,7 +60,7 @@ from decouple import config EXP_MAKER_BOND_INVOICE = int(config("EXP_MAKER_BOND_INVOICE")) RETRY_TIME = int(config("RETRY_TIME")) -PUBLIC_DURATION = 60*60*int(config("DEFAULT_PUBLIC_ORDER_DURATION"))-1 +PUBLIC_DURATION = 60 * 60 * int(config("DEFAULT_PUBLIC_ORDER_DURATION")) - 1 ESCROW_DURATION = 60 * int(config("INVOICE_AND_ESCROW_DURATION")) BOND_SIZE = int(config("DEFAULT_BOND_SIZE")) @@ -50,10 +79,7 @@ class MakerView(CreateAPIView): if not request.user.is_authenticated: return Response( - { - "bad_request": - "Woops! It seems you do not have a robot avatar" - }, + {"bad_request": "Woops! It seems you do not have a robot avatar"}, status.HTTP_400_BAD_REQUEST, ) @@ -61,11 +87,12 @@ class MakerView(CreateAPIView): return Response(status=status.HTTP_400_BAD_REQUEST) # In case it gets overwhelming. Limit the number of public orders. - if Order.objects.filter(status=Order.Status.PUB).count() >= int(config("MAX_PUBLIC_ORDERS")): + if Order.objects.filter(status=Order.Status.PUB).count() >= int( + config("MAX_PUBLIC_ORDERS") + ): return Response( { - "bad_request": - "Woah! RoboSats' book is at full capacity! Try again later" + "bad_request": "Woah! RoboSats' book is at full capacity! Try again later" }, status.HTTP_400_BAD_REQUEST, ) @@ -90,11 +117,16 @@ class MakerView(CreateAPIView): bondless_taker = serializer.data.get("bondless_taker") # Optional params - if public_duration == None: public_duration = PUBLIC_DURATION - if escrow_duration == None: escrow_duration = ESCROW_DURATION - if bond_size == None: bond_size = BOND_SIZE - if bondless_taker == None: bondless_taker = False - if has_range == None: has_range = False + if public_duration == None: + public_duration = PUBLIC_DURATION + if escrow_duration == None: + escrow_duration = ESCROW_DURATION + if bond_size == None: + bond_size = BOND_SIZE + if bondless_taker == None: + bondless_taker = False + if has_range == None: + has_range = False # TODO add a check - if `is_explicit` is true then `satoshis` need to be specified @@ -109,21 +141,16 @@ class MakerView(CreateAPIView): if has_range and (min_amount == None or max_amount == None): return Response( { - "bad_request": - "You must specify min_amount and max_amount for a range order" + "bad_request": "You must specify min_amount and max_amount for a range order" }, status.HTTP_400_BAD_REQUEST, ) elif not has_range and amount == None: return Response( - { - "bad_request": - "You must specify an order amount" - }, + {"bad_request": "You must specify an order amount"}, status.HTTP_400_BAD_REQUEST, ) - # Creates a new order order = Order( type=type, @@ -136,8 +163,7 @@ class MakerView(CreateAPIView): premium=premium, satoshis=satoshis, is_explicit=is_explicit, - expires_at=timezone.now() + timedelta( - seconds=EXP_MAKER_BOND_INVOICE), + expires_at=timezone.now() + timedelta(seconds=EXP_MAKER_BOND_INVOICE), maker=request.user, public_duration=public_duration, escrow_duration=escrow_duration, @@ -152,8 +178,7 @@ class MakerView(CreateAPIView): return Response(context, status.HTTP_400_BAD_REQUEST) order.save() - return Response(ListOrderSerializer(order).data, - status=status.HTTP_201_CREATED) + return Response(ListOrderSerializer(order).data, status=status.HTTP_201_CREATED) class OrderView(viewsets.ViewSet): @@ -170,8 +195,7 @@ class OrderView(viewsets.ViewSet): if not request.user.is_authenticated: return Response( { - "bad_request": - "You must have a robot avatar to see the order details" + "bad_request": "You must have a robot avatar to see the order details" }, status=status.HTTP_400_BAD_REQUEST, ) @@ -186,8 +210,9 @@ class OrderView(viewsets.ViewSet): # check if exactly one order is found in the db if len(order) != 1: - return Response({"bad_request": "Invalid Order Id"}, - status.HTTP_404_NOT_FOUND) + return Response( + {"bad_request": "Invalid Order Id"}, status.HTTP_404_NOT_FOUND + ) # This is our order. order = order[0] @@ -200,10 +225,7 @@ class OrderView(viewsets.ViewSet): ) if order.status == Order.Status.CCA: return Response( - { - "bad_request": - "This order has been cancelled collaborativelly" - }, + {"bad_request": "This order has been cancelled collaborativelly"}, status.HTTP_400_BAD_REQUEST, ) @@ -239,11 +261,9 @@ class OrderView(viewsets.ViewSet): # Add activity status of participants based on last_seen if order.taker_last_seen != None: - data["taker_status"] = Logics.user_activity_status( - order.taker_last_seen) + data["taker_status"] = Logics.user_activity_status(order.taker_last_seen) if order.maker_last_seen != None: - data["maker_status"] = Logics.user_activity_status( - order.maker_last_seen) + data["maker_status"] = Logics.user_activity_status(order.maker_last_seen) # 3.b) Non participants can view details (but only if PUB) if not data["is_participant"] and order.status == Order.Status.PUB: @@ -253,16 +273,21 @@ class OrderView(viewsets.ViewSet): if order.status >= Order.Status.PUB and order.status < Order.Status.WF2: data["price_now"], data["premium_now"] = Logics.price_and_premium_now(order) - # 4. a) If maker and Public/Paused, add premium percentile + # 4. a) If maker and Public/Paused, add premium percentile # num similar orders, and maker information to enable telegram notifications. - if data["is_maker"] and order.status in [Order.Status.PUB, Order.Status.PAU]: + if data["is_maker"] and order.status in [ + Order.Status.PUB, + Order.Status.PAU, + ]: data["premium_percentile"] = compute_premium_percentile(order) data["num_similar_orders"] = len( - Order.objects.filter(currency=order.currency, - status=Order.Status.PUB)) + Order.objects.filter( + currency=order.currency, status=Order.Status.PUB + ) + ) # Adds/generate telegram token and whether it is enabled # Deprecated - data = {**data,**Telegram.get_context(request.user)} + data = {**data, **Telegram.get_context(request.user)} # For participants add positions, nicks and status as a message and hold invoices status data["is_buyer"] = Logics.is_buyer(order, request.user) @@ -294,16 +319,21 @@ class OrderView(viewsets.ViewSet): # If both bonds are locked, participants can see the final trade amount in sats. if order.taker_bond: - if (order.maker_bond.status == order.taker_bond.status == - LNPayment.Status.LOCKED): + if ( + order.maker_bond.status + == order.taker_bond.status + == LNPayment.Status.LOCKED + ): # Seller sees the amount he sends if data["is_seller"]: - data["trade_satoshis"] = Logics.escrow_amount( - order, request.user)[1]["escrow_amount"] + data["trade_satoshis"] = Logics.escrow_amount(order, request.user)[ + 1 + ]["escrow_amount"] # Buyer sees the amount he receives elif data["is_buyer"]: - data["trade_satoshis"] = Logics.payout_amount( - order, request.user)[1]["invoice_amount"] + data["trade_satoshis"] = Logics.payout_amount(order, request.user)[ + 1 + ]["invoice_amount"] # 5) If status is 'waiting for maker bond' and user is MAKER, reply with a MAKER hold invoice. if order.status == Order.Status.WFB and data["is_maker"]: @@ -322,25 +352,32 @@ class OrderView(viewsets.ViewSet): return Response(context, status.HTTP_400_BAD_REQUEST) # 7 a. ) If seller and status is 'WF2' or 'WFE' - elif data["is_seller"] and (order.status == Order.Status.WF2 - or order.status == Order.Status.WFE): + elif data["is_seller"] and ( + order.status == Order.Status.WF2 or order.status == Order.Status.WFE + ): # If the two bonds are locked, reply with an ESCROW hold invoice. - if (order.maker_bond.status == order.taker_bond.status == - LNPayment.Status.LOCKED): - valid, context = Logics.gen_escrow_hold_invoice( - order, request.user) + if ( + order.maker_bond.status + == order.taker_bond.status + == LNPayment.Status.LOCKED + ): + valid, context = Logics.gen_escrow_hold_invoice(order, request.user) if valid: data = {**data, **context} else: return Response(context, status.HTTP_400_BAD_REQUEST) # 7.b) If user is Buyer and status is 'WF2' or 'WFI' - elif data["is_buyer"] and (order.status == Order.Status.WF2 - or order.status == Order.Status.WFI): + elif data["is_buyer"] and ( + order.status == Order.Status.WF2 or order.status == Order.Status.WFI + ): # If the two bonds are locked, reply with an AMOUNT and onchain swap cost so he can send the buyer invoice/address. - if (order.maker_bond.status == order.taker_bond.status == - LNPayment.Status.LOCKED): + if ( + order.maker_bond.status + == order.taker_bond.status + == LNPayment.Status.LOCKED + ): valid, context = Logics.payout_amount(order, request.user) if valid: data = {**data, **context} @@ -348,23 +385,27 @@ class OrderView(viewsets.ViewSet): return Response(context, status.HTTP_400_BAD_REQUEST) # 8) If status is 'CHA' or 'FSE' and all HTLCS are in LOCKED - elif order.status in [ - Order.Status.WFI, Order.Status.CHA, Order.Status.FSE - ]: + elif order.status in [Order.Status.WFI, Order.Status.CHA, Order.Status.FSE]: # If all bonds are locked. - if (order.maker_bond.status == order.taker_bond.status == - order.trade_escrow.status == LNPayment.Status.LOCKED): + if ( + order.maker_bond.status + == order.taker_bond.status + == order.trade_escrow.status + == LNPayment.Status.LOCKED + ): # add whether a collaborative cancel is pending or has been asked if (data["is_maker"] and order.taker_asked_cancel) or ( - data["is_taker"] and order.maker_asked_cancel): + data["is_taker"] and order.maker_asked_cancel + ): data["pending_cancel"] = True elif (data["is_maker"] and order.maker_asked_cancel) or ( - data["is_taker"] and order.taker_asked_cancel): + data["is_taker"] and order.taker_asked_cancel + ): data["asked_for_cancel"] = True else: data["asked_for_cancel"] = False - - offset = request.GET.get('offset', None) + + offset = request.GET.get("offset", None) if offset: data["chat"] = ChatView.get(None, request).data @@ -373,35 +414,47 @@ class OrderView(viewsets.ViewSet): # add whether the dispute statement has been received if data["is_maker"]: - data["statement_submitted"] = (order.maker_statement != None - and order.maker_statement != "") + data["statement_submitted"] = ( + order.maker_statement != None and order.maker_statement != "" + ) elif data["is_taker"]: - data["statement_submitted"] = (order.taker_statement != None - and order.taker_statement != "") + data["statement_submitted"] = ( + order.taker_statement != None and order.taker_statement != "" + ) # 9) If status is 'Failed routing', reply with retry amounts, time of next retry and ask for invoice at third. - elif (order.status == Order.Status.FAI - and order.payout.receiver == request.user - ): # might not be the buyer if after a dispute where winner wins + elif ( + order.status == Order.Status.FAI and order.payout.receiver == request.user + ): # might not be the buyer if after a dispute where winner wins data["retries"] = order.payout.routing_attempts data["next_retry_time"] = order.payout.last_routing_time + timedelta( - minutes=RETRY_TIME) + minutes=RETRY_TIME + ) if order.payout.failure_reason: - data["failure_reason"] = LNPayment.FailureReason(order.payout.failure_reason).label + data["failure_reason"] = LNPayment.FailureReason( + order.payout.failure_reason + ).label if order.payout.status == LNPayment.Status.EXPIRE: data["invoice_expired"] = True # Add invoice amount once again if invoice was expired. - data["invoice_amount"] = Logics.payout_amount(order,request.user)[1]["invoice_amount"] + data["invoice_amount"] = Logics.payout_amount(order, request.user)[1][ + "invoice_amount" + ] # 10) If status is 'Expired', "Sending", "Finished" or "failed routing", add info for renewal: - elif order.status in [Order.Status.EXP, Order.Status.SUC, Order.Status.PAY, Order.Status.FAI]: + elif order.status in [ + Order.Status.EXP, + Order.Status.SUC, + Order.Status.PAY, + Order.Status.FAI, + ]: data["public_duration"] = order.public_duration data["bond_size"] = order.bond_size data["bondless_taker"] = order.bondless_taker # Adds trade summary - if order.status in [Order.Status.SUC, Order.Status.PAY, Order.Status.FAI]: + if order.status in [Order.Status.SUC, Order.Status.PAY, Order.Status.FAI]: valid, context = Logics.summarize_trade(order, request.user) if valid: data = {**data, **context} @@ -414,16 +467,17 @@ class OrderView(viewsets.ViewSet): # If status is 'Succes' add final stats and txid if it is a swap if order.status == Order.Status.SUC: # If buyer and is a swap, add TXID - if Logics.is_buyer(order,request.user): + if Logics.is_buyer(order, request.user): if order.is_swap: data["num_satoshis"] = order.payout_tx.num_satoshis data["sent_satoshis"] = order.payout_tx.sent_satoshis - if order.payout_tx.status in [OnchainPayment.Status.MEMPO, OnchainPayment.Status.CONFI]: + if order.payout_tx.status in [ + OnchainPayment.Status.MEMPO, + OnchainPayment.Status.CONFI, + ]: data["txid"] = order.payout_tx.txid data["network"] = str(config("NETWORK")) - - return Response(data, status.HTTP_200_OK) @extend_schema(**OrderViewSchema.take_update_confirm_dispute_cancel) @@ -452,8 +506,7 @@ class OrderView(viewsets.ViewSet): # 1) If action is take, it is a taker request! if action == "take": if order.status == Order.Status.PUB: - valid, context, _ = Logics.validate_already_maker_or_taker( - request.user) + valid, context, _ = Logics.validate_already_maker_or_taker(request.user) if not valid: return Response(context, status=status.HTTP_409_CONFLICT) @@ -487,15 +540,15 @@ class OrderView(viewsets.ViewSet): # 2) If action is 'update invoice' elif action == "update_invoice": - valid, context = Logics.update_invoice(order, request.user, - invoice) + valid, context = Logics.update_invoice(order, request.user, invoice) if not valid: return Response(context, status.HTTP_400_BAD_REQUEST) - + # 2.b) If action is 'update address' elif action == "update_address": - valid, context = Logics.update_address(order, request.user, - address, mining_fee_rate) + valid, context = Logics.update_address( + order, request.user, address, mining_fee_rate + ) if not valid: return Response(context, status.HTTP_400_BAD_REQUEST) @@ -518,15 +571,13 @@ class OrderView(viewsets.ViewSet): return Response(context, status.HTTP_400_BAD_REQUEST) elif action == "submit_statement": - valid, context = Logics.dispute_statement(order, request.user, - statement) + valid, context = Logics.dispute_statement(order, request.user, statement) if not valid: return Response(context, status.HTTP_400_BAD_REQUEST) # 6) If action is rate elif action == "rate_user" and rating: - valid, context = Logics.rate_counterparty(order, request.user, - rating) + valid, context = Logics.rate_counterparty(order, request.user, rating) if not valid: return Response(context, status.HTTP_400_BAD_REQUEST) @@ -546,10 +597,8 @@ class OrderView(viewsets.ViewSet): else: return Response( { - "bad_request": - "The Robotic Satoshis working in the warehouse did not understand you. " - + - "Please, fill a Bug Issue in Github https://github.com/reckless-satoshi/robosats/issues" + "bad_request": "The Robotic Satoshis working in the warehouse did not understand you. " + + "Please, fill a Bug Issue in Github https://github.com/reckless-satoshi/robosats/issues" }, status.HTTP_501_NOT_IMPLEMENTED, ) @@ -558,15 +607,12 @@ class OrderView(viewsets.ViewSet): class UserView(APIView): - NickGen = NickGenerator(lang="English", - use_adv=False, - use_adj=True, - use_noun=True, - max_num=999) + NickGen = NickGenerator( + lang="English", use_adv=False, use_adj=True, use_noun=True, max_num=999 + ) serializer_class = UserGenSerializer - def post(self, request, format=None): """ Get a new user derived from a high entropy token @@ -581,7 +627,7 @@ class UserView(APIView): context = {} serializer = self.serializer_class(data=request.data) - # Return bad request if serializer is not valid + # Return bad request if serializer is not valid if not serializer.is_valid(): context = {"bad_request": "Invalid serializer"} return Response(context, status=status.HTTP_400_BAD_REQUEST) @@ -590,12 +636,15 @@ class UserView(APIView): if request.user.is_authenticated: context = {"nickname": request.user.username} not_participant, _, order = Logics.validate_already_maker_or_taker( - request.user) + request.user + ) # Does not allow this 'mistake' if an active order if not not_participant: context["active_order_id"] = order.id - context["bad_request"] = f"You are already logged in as {request.user} and have an active order" + context[ + "bad_request" + ] = f"You are already logged in as {request.user} and have an active order" return Response(context, status.HTTP_400_BAD_REQUEST) # The new way. The token is never sent. Only its SHA256 @@ -603,16 +652,21 @@ class UserView(APIView): public_key = serializer.data.get("public_key") encrypted_private_key = serializer.data.get("encrypted_private_key") ref_code = serializer.data.get("ref_code") - + if not public_key or not encrypted_private_key: context["bad_request"] = "Must provide valid 'pub' and 'enc_priv' PGP keys" return Response(context, status.HTTP_400_BAD_REQUEST) - valid, bad_keys_context, public_key, encrypted_private_key = Logics.validate_pgp_keys(public_key, encrypted_private_key) + ( + valid, + bad_keys_context, + public_key, + encrypted_private_key, + ) = Logics.validate_pgp_keys(public_key, encrypted_private_key) if not valid: return Response(bad_keys_context, status.HTTP_400_BAD_REQUEST) - # Now the server only receives a hash of the token. So server trusts the client + # Now the server only receives a hash of the token. So server trusts the client # with computing length, counts and unique_values to confirm the high entropy of the token # In any case, it is up to the client if they want to create a bad high entropy token. @@ -640,7 +694,7 @@ class UserView(APIView): pass # Hash the token_sha256, only 1 iteration. (this is the second SHA256 of the user token, aka RoboSats ID) - hash = hashlib.sha256(token_sha256.encode('utf-8')).hexdigest() + hash = hashlib.sha256(token_sha256.encode("utf-8")).hexdigest() # Generate nickname deterministically nickname = self.NickGen.short_from_SHA256(hash, max_length=18)[0] @@ -658,17 +712,17 @@ class UserView(APIView): # Create new credentials and login if nickname is new if len(User.objects.filter(username=nickname)) == 0: - User.objects.create_user(username=nickname, - password=token_sha256, - is_staff=False) + User.objects.create_user( + username=nickname, password=token_sha256, is_staff=False + ) user = authenticate(request, username=nickname, password=token_sha256) login(request, user) - context['referral_code'] = token_urlsafe(8) - user.profile.referral_code = context['referral_code'] + context["referral_code"] = token_urlsafe(8) + user.profile.referral_code = context["referral_code"] user.profile.avatar = "static/assets/avatars/" + nickname + ".png" - - # Noticed some PGP keys replaced at re-login. Should not happen. + + # Noticed some PGP keys replaced at re-login. Should not happen. # Let's implement this sanity check "If profile has not keys..." if not user.profile.public_key: user.profile.public_key = public_key @@ -700,17 +754,21 @@ class UserView(APIView): context["wants_stealth"] = user.profile.wants_stealth # Adds/generate telegram token and whether it is enabled - context = {**context,**Telegram.get_context(user)} + context = {**context, **Telegram.get_context(user)} # return active order or last made order if any - has_no_active_order, _, order = Logics.validate_already_maker_or_taker(request.user) + has_no_active_order, _, order = Logics.validate_already_maker_or_taker( + request.user + ) if not has_no_active_order: context["active_order_id"] = order.id else: - last_order = Order.objects.filter(Q(maker=request.user) | Q(taker=request.user)).last() + last_order = Order.objects.filter( + Q(maker=request.user) | Q(taker=request.user) + ).last() if last_order: context["last_order_id"] = last_order.id - + # Sends the welcome back message, only if created +3 mins ago if request.user.date_joined < (timezone.now() - timedelta(minutes=3)): context["found"] = "We found your Robot avatar. Welcome back!" @@ -737,8 +795,7 @@ class UserView(APIView): if not not_participant: return Response( { - "bad_request": - "Maybe a mistake? User cannot be deleted while he is part of an order" + "bad_request": "Maybe a mistake? User cannot be deleted while he is part of an order" }, status.HTTP_400_BAD_REQUEST, ) @@ -746,8 +803,7 @@ class UserView(APIView): if user.profile.total_contracts > 0: return Response( { - "bad_request": - "Maybe a mistake? User cannot be deleted as it has completed trades" + "bad_request": "Maybe a mistake? User cannot be deleted as it has completed trades" }, status.HTTP_400_BAD_REQUEST, ) @@ -775,12 +831,11 @@ class BookView(ListAPIView): if int(currency) == 0 and int(type) != 2: queryset = Order.objects.filter(type=type, status=Order.Status.PUB) elif int(type) == 2 and int(currency) != 0: - queryset = Order.objects.filter(currency=currency, - status=Order.Status.PUB) + queryset = Order.objects.filter(currency=currency, status=Order.Status.PUB) elif not (int(currency) == 0 and int(type) == 2): - queryset = Order.objects.filter(currency=currency, - type=type, - status=Order.Status.PUB) + queryset = Order.objects.filter( + currency=currency, type=type, status=Order.Status.PUB + ) if len(queryset) == 0: return Response( @@ -795,11 +850,12 @@ class BookView(ListAPIView): data["satoshis_now"] = Logics.satoshis_now(order) # Compute current premium for those orders that are explicitly priced. - data["price"], data["premium"] = Logics.price_and_premium_now( - order) - data["maker_status"] = Logics.user_activity_status( - order.maker_last_seen) - for key in ("status","taker"): # Non participants should not see the status or who is the taker + data["price"], data["premium"] = Logics.price_and_premium_now(order) + data["maker_status"] = Logics.user_activity_status(order.maker_last_seen) + for key in ( + "status", + "taker", + ): # Non participants should not see the status or who is the taker del data[key] book_data.append(data) @@ -816,18 +872,23 @@ class InfoView(ListAPIView): context = {} context["num_public_buy_orders"] = len( - Order.objects.filter(type=Order.Types.BUY, - status=Order.Status.PUB)) + Order.objects.filter(type=Order.Types.BUY, status=Order.Status.PUB) + ) context["num_public_sell_orders"] = len( - Order.objects.filter(type=Order.Types.SELL, - status=Order.Status.PUB)) - context["book_liquidity"] = Order.objects.filter(status=Order.Status.PUB).aggregate(Sum('last_satoshis'))['last_satoshis__sum'] - context["book_liquidity"] = 0 if context["book_liquidity"] == None else context["book_liquidity"] + Order.objects.filter(type=Order.Types.SELL, status=Order.Status.PUB) + ) + context["book_liquidity"] = Order.objects.filter( + status=Order.Status.PUB + ).aggregate(Sum("last_satoshis"))["last_satoshis__sum"] + context["book_liquidity"] = ( + 0 if context["book_liquidity"] == None else context["book_liquidity"] + ) # Number of active users (logged in in last 30 minutes) today = datetime.today() context["active_robots_today"] = len( - User.objects.filter(last_login__day=today.day)) + User.objects.filter(last_login__day=today.day) + ) # Compute average premium and volume of today last_day = timezone.now() - timedelta(days=1) @@ -860,11 +921,15 @@ class InfoView(ListAPIView): context["node_alias"] = config("NODE_ALIAS") context["node_id"] = config("NODE_ID") context["network"] = config("NETWORK") - context["maker_fee"] = float(config("FEE"))*float(config("MAKER_FEE_SPLIT")) - context["taker_fee"] = float(config("FEE"))*(1 - float(config("MAKER_FEE_SPLIT"))) + context["maker_fee"] = float(config("FEE")) * float(config("MAKER_FEE_SPLIT")) + context["taker_fee"] = float(config("FEE")) * ( + 1 - float(config("MAKER_FEE_SPLIT")) + ) context["bond_size"] = float(config("DEFAULT_BOND_SIZE")) - context["current_swap_fee_rate"] = Logics.compute_swap_fee_rate(BalanceLog.objects.latest('time')) + context["current_swap_fee_rate"] = Logics.compute_swap_fee_rate( + BalanceLog.objects.latest("time") + ) if request.user.is_authenticated: context["nickname"] = request.user.username @@ -872,13 +937,16 @@ class InfoView(ListAPIView): context["earned_rewards"] = request.user.profile.earned_rewards context["wants_stealth"] = request.user.profile.wants_stealth # Adds/generate telegram token and whether it is enabled - context = {**context,**Telegram.get_context(request.user)} + context = {**context, **Telegram.get_context(request.user)} has_no_active_order, _, order = Logics.validate_already_maker_or_taker( - request.user) + request.user + ) if not has_no_active_order: context["active_order_id"] = order.id else: - last_order = Order.objects.filter(Q(maker=request.user) | Q(taker=request.user)).last() + last_order = Order.objects.filter( + Q(maker=request.user) | Q(taker=request.user) + ).last() if last_order: context["last_order_id"] = last_order.id @@ -894,10 +962,7 @@ class RewardView(CreateAPIView): if not request.user.is_authenticated: return Response( - { - "bad_request": - "Woops! It seems you do not have a robot avatar" - }, + {"bad_request": "Woops! It seems you do not have a robot avatar"}, status.HTTP_400_BAD_REQUEST, ) @@ -909,7 +974,7 @@ class RewardView(CreateAPIView): valid, context = Logics.withdraw_rewards(request.user, invoice) if not valid: - context['successful_withdrawal'] = False + context["successful_withdrawal"] = False return Response(context, status.HTTP_400_BAD_REQUEST) return Response({"successful_withdrawal": True}, status.HTTP_200_OK) @@ -923,17 +988,19 @@ class PriceView(ListAPIView): def get(self, request): payload = {} - queryset = Currency.objects.all().order_by('currency') + queryset = Currency.objects.all().order_by("currency") for currency in queryset: code = Currency.currency_dict[str(currency.currency)] try: - last_tick = MarketTick.objects.filter(currency=currency).latest('timestamp') + last_tick = MarketTick.objects.filter(currency=currency).latest( + "timestamp" + ) payload[code] = { - 'price': last_tick.price, - 'volume': last_tick.volume, - 'premium': last_tick.premium, - 'timestamp': last_tick.timestamp, + "price": last_tick.price, + "volume": last_tick.volume, + "premium": last_tick.premium, + "timestamp": last_tick.timestamp, } except: payload[code] = None @@ -948,48 +1015,48 @@ class TickView(ListAPIView): @extend_schema(**TickViewSchema.get) def get(self, request): - data = self.serializer_class(self.queryset.all(), many=True, read_only=True).data + data = self.serializer_class( + self.queryset.all(), many=True, read_only=True + ).data return Response(data, status=status.HTTP_200_OK) class LimitView(ListAPIView): - @extend_schema(**LimitViewSchema.get) def get(self, request): - + # Trade limits as BTC - min_trade = float(config('MIN_TRADE')) / 100000000 - max_trade = float(config('MAX_TRADE')) / 100000000 - max_bondless_trade = float(config('MAX_TRADE_BONDLESS_TAKER')) / 100000000 + min_trade = float(config("MIN_TRADE")) / 100000000 + max_trade = float(config("MAX_TRADE")) / 100000000 + max_bondless_trade = float(config("MAX_TRADE_BONDLESS_TAKER")) / 100000000 payload = {} - queryset = Currency.objects.all().order_by('currency') + queryset = Currency.objects.all().order_by("currency") for currency in queryset: code = Currency.currency_dict[str(currency.currency)] exchange_rate = float(currency.exchange_rate) payload[currency.currency] = { - 'code': code, - 'price': exchange_rate, - 'min_amount': min_trade * exchange_rate, - 'max_amount': max_trade * exchange_rate, - 'max_bondless_amount': max_bondless_trade * exchange_rate, + "code": code, + "price": exchange_rate, + "min_amount": min_trade * exchange_rate, + "max_amount": max_trade * exchange_rate, + "max_bondless_amount": max_bondless_trade * exchange_rate, } return Response(payload, status.HTTP_200_OK) class HistoricalView(ListAPIView): - @extend_schema(**HistoricalViewSchema.get) def get(self, request): payload = {} - queryset = AccountingDay.objects.all().order_by('day') + queryset = AccountingDay.objects.all().order_by("day") for accounting_day in queryset: payload[str(accounting_day.day)] = { - 'volume': accounting_day.contracted, - 'num_contracts': accounting_day.num_contracts, + "volume": accounting_day.contracted, + "num_contracts": accounting_day.num_contracts, } return Response(payload, status.HTTP_200_OK) @@ -998,16 +1065,14 @@ class HistoricalView(ListAPIView): class StealthView(UpdateAPIView): serializer_class = StealthSerializer + @extend_schema(**StealthViewSchema.put) def put(self, request): serializer = self.serializer_class(data=request.data) if not request.user.is_authenticated: return Response( - { - "bad_request": - "Woops! It seems you do not have a robot avatar" - }, + {"bad_request": "Woops! It seems you do not have a robot avatar"}, status.HTTP_400_BAD_REQUEST, ) diff --git a/chat/admin.py b/chat/admin.py index ec78db0f..96c01a28 100644 --- a/chat/admin.py +++ b/chat/admin.py @@ -1,6 +1,7 @@ from django.contrib import admin from django_admin_relation_links import AdminChangeLinksMixin from chat.models import ChatRoom, Message + # Register your models here. @@ -17,9 +18,10 @@ class ChatRoomAdmin(AdminChangeLinksMixin, admin.ModelAdmin): "taker_connect_date", "room_group_name", ) - change_links = ["order","maker","taker"] + change_links = ["order", "maker", "taker"] search_fields = ["id"] + @admin.register(Message) class MessageAdmin(AdminChangeLinksMixin, admin.ModelAdmin): list_display = ( @@ -31,7 +33,7 @@ class MessageAdmin(AdminChangeLinksMixin, admin.ModelAdmin): "receiver_link", "created_at", ) - change_links = ["chatroom","order","sender","receiver"] - search_fields = ["id","index"] - ordering = ["-chatroom_id","-index"] - list_filter = ("chatroom",) \ No newline at end of file + change_links = ["chatroom", "order", "sender", "receiver"] + search_fields = ["id", "index"] + ordering = ["-chatroom_id", "-index"] + list_filter = ("chatroom",) diff --git a/chat/consumers.py b/chat/consumers.py index 66c238a7..07b127ce 100644 --- a/chat/consumers.py +++ b/chat/consumers.py @@ -6,8 +6,8 @@ from asgiref.sync import async_to_sync import json -class ChatRoomConsumer(AsyncWebsocketConsumer): +class ChatRoomConsumer(AsyncWebsocketConsumer): @database_sync_to_async def allow_in_chatroom(self): order = Order.objects.get(id=self.order_id) @@ -23,37 +23,37 @@ class ChatRoomConsumer(AsyncWebsocketConsumer): @database_sync_to_async def save_connect_user(self): - '''Creates or updates the ChatRoom object''' + """Creates or updates the ChatRoom object""" order = Order.objects.get(id=self.order_id) if order.maker == self.user: ChatRoom.objects.update_or_create( - id=self.order_id, - order=order, + id=self.order_id, + order=order, room_group_name=self.room_group_name, defaults={ "maker": self.user, "maker_connected": True, - } - ) + }, + ) elif order.taker == self.user: ChatRoom.objects.update_or_create( - id=self.order_id, - order=order, + id=self.order_id, + order=order, room_group_name=self.room_group_name, defaults={ "taker": self.user, "taker_connected": True, - } - ) + }, + ) return None @database_sync_to_async def save_new_PGP_message(self, PGP_message): - '''Creates a Message object''' + """Creates a Message object""" order = Order.objects.get(id=self.order_id) chatroom = ChatRoom.objects.get(order=order) @@ -71,39 +71,33 @@ class ChatRoomConsumer(AsyncWebsocketConsumer): receiver = order.taker msg_obj = Message.objects.create( - order=order, - chatroom=chatroom, - index=index, - sender=sender, - receiver=receiver, - PGP_message=PGP_message, - ) + order=order, + chatroom=chatroom, + index=index, + sender=sender, + receiver=receiver, + PGP_message=PGP_message, + ) return msg_obj @database_sync_to_async def save_disconnect_user(self): - '''Creates or updates the ChatRoom object''' - + """Creates or updates the ChatRoom object""" + order = Order.objects.get(id=self.order_id) if order.maker == self.user: ChatRoom.objects.update_or_create( - id=self.order_id, - defaults={ - "maker_connected": False - } - ) + id=self.order_id, defaults={"maker_connected": False} + ) elif order.taker == self.user: ChatRoom.objects.update_or_create( - id=self.order_id, - defaults={ - "taker_connected": False - } - ) + id=self.order_id, defaults={"taker_connected": False} + ) return None @database_sync_to_async def is_peer_connected(self): - '''Returns whether the consumer's peer is connected''' + """Returns whether the consumer's peer is connected""" chatroom = ChatRoom.objects.get(id=self.order_id) @@ -115,7 +109,7 @@ class ChatRoomConsumer(AsyncWebsocketConsumer): @database_sync_to_async def get_peer_PGP_public_key(self): - '''Returns peer PGP public key''' + """Returns peer PGP public key""" order = Order.objects.get(id=self.order_id) @@ -127,19 +121,21 @@ class ChatRoomConsumer(AsyncWebsocketConsumer): @database_sync_to_async def get_all_PGP_messages(self): - '''Returns all PGP messages''' + """Returns all PGP messages""" order = Order.objects.get(id=self.order_id) messages = Message.objects.filter(order=order) msgs = [] for message in messages: - msgs.append({ - "index": message.index, - "time": str(message.created_at), - "message": message.PGP_message, - "nick": str(message.sender), - }) + msgs.append( + { + "index": message.index, + "time": str(message.created_at), + "message": message.PGP_message, + "nick": str(message.sender), + } + ) return msgs @@ -153,8 +149,7 @@ class ChatRoomConsumer(AsyncWebsocketConsumer): if allowed: await self.save_connect_user() - await self.channel_layer.group_add(self.room_group_name, - self.channel_name) + await self.channel_layer.group_add(self.room_group_name, self.channel_name) await self.accept() @@ -173,13 +168,12 @@ class ChatRoomConsumer(AsyncWebsocketConsumer): async def disconnect(self, close_code): await self.save_disconnect_user() - await self.channel_layer.group_discard(self.room_group_name, - self.channel_name) + await self.channel_layer.group_discard(self.room_group_name, self.channel_name) await self.channel_layer.group_send( self.room_group_name, { "type": "chatroom_message", - "message": 'peer-disconnected', + "message": "peer-disconnected", "nick": self.scope["user"].username, "peer_connected": False, }, @@ -189,9 +183,9 @@ class ChatRoomConsumer(AsyncWebsocketConsumer): text_data_json = json.loads(text_data) message = text_data_json["message"] peer_connected = await self.is_peer_connected() - + # Encrypted messages are stored. They are served later when a user reconnects. - if message[0:27] == '-----BEGIN PGP MESSAGE-----': + if message[0:27] == "-----BEGIN PGP MESSAGE-----": # save to database msg_obj = await self.save_new_PGP_message(message) @@ -210,9 +204,9 @@ class ChatRoomConsumer(AsyncWebsocketConsumer): "peer_connected": peer_connected, }, ) - + # Encrypted messages are served when the user requests them - elif message[0:23] == '-----SERVE HISTORY-----': + elif message[0:23] == "-----SERVE HISTORY-----": # If there is any stored message, serve them. msgs = await self.get_all_PGP_messages() peer_connected = await self.is_peer_connected() @@ -221,10 +215,10 @@ class ChatRoomConsumer(AsyncWebsocketConsumer): self.room_group_name, { "type": "PGP_message", - "index": msg['index'], - "time": msg['time'], - "message": msg['message'], - "nick": msg['nick'], + "index": msg["index"], + "time": msg["time"], + "message": msg["message"], + "nick": msg["nick"], "peer_connected": peer_connected, }, ) @@ -245,11 +239,15 @@ class ChatRoomConsumer(AsyncWebsocketConsumer): nick = event["nick"] peer_connected = event["peer_connected"] - await self.send(text_data=json.dumps({ - "message": message, - "user_nick": nick, - "peer_connected": peer_connected, - })) + await self.send( + text_data=json.dumps( + { + "message": message, + "user_nick": nick, + "peer_connected": peer_connected, + } + ) + ) async def PGP_message(self, event): message = event["message"] @@ -258,10 +256,14 @@ class ChatRoomConsumer(AsyncWebsocketConsumer): peer_connected = event["peer_connected"] time = event["time"] - await self.send(text_data=json.dumps({ - "index": index, - "message": message, - "user_nick": nick, - "peer_connected": peer_connected, - "time":time, - })) + await self.send( + text_data=json.dumps( + { + "index": index, + "message": message, + "user_nick": nick, + "peer_connected": peer_connected, + "time": time, + } + ) + ) diff --git a/chat/models.py b/chat/models.py index 5e637ba9..55079f02 100644 --- a/chat/models.py +++ b/chat/models.py @@ -3,24 +3,29 @@ from api.models import User, Order from django.utils import timezone import uuid -class ChatRoom(models.Model): - ''' - Simple ChatRoom model. Needed to facilitate communication: Is my counterpart in the room? - ''' - id = models.PositiveBigIntegerField(primary_key=True, null=False,default=None, blank=True) +class ChatRoom(models.Model): + """ + Simple ChatRoom model. Needed to facilitate communication: Is my counterpart in the room? + """ + + id = models.PositiveBigIntegerField( + primary_key=True, null=False, default=None, blank=True + ) order = models.ForeignKey( Order, related_name="chatroom", on_delete=models.SET_NULL, null=True, - default=None) + default=None, + ) maker = models.ForeignKey( User, related_name="chat_maker", on_delete=models.SET_NULL, null=True, - default=None) + default=None, + ) taker = models.ForeignKey( User, related_name="chat_taker", @@ -30,8 +35,8 @@ class ChatRoom(models.Model): blank=True, ) - maker_connected = models.BooleanField(default=False, null=False) - taker_connected = models.BooleanField(default=False, null=False) + maker_connected = models.BooleanField(default=False, null=False) + taker_connected = models.BooleanField(default=False, null=False) maker_connect_date = models.DateTimeField(auto_now_add=True) taker_connect_date = models.DateTimeField(auto_now_add=True) @@ -46,41 +51,39 @@ class ChatRoom(models.Model): def __str__(self): return f"Chat:{str(self.id)}" + class Message(models.Model): class Meta: - get_latest_by = 'index' + get_latest_by = "index" # id = models.PositiveBigIntegerField(primary_key=True, default=uuid.uuid4, editable=False) order = models.ForeignKey( - Order, - related_name="message", - on_delete=models.CASCADE, - null=True, - default=None) + Order, related_name="message", on_delete=models.CASCADE, null=True, default=None + ) chatroom = models.ForeignKey( ChatRoom, related_name="chatroom", on_delete=models.CASCADE, null=True, - default=None) - index = models.PositiveIntegerField(null=False,default=None, blank=True) + default=None, + ) + index = models.PositiveIntegerField(null=False, default=None, blank=True) sender = models.ForeignKey( User, related_name="message_sender", on_delete=models.SET_NULL, null=True, - default=None) + default=None, + ) receiver = models.ForeignKey( User, related_name="message_receiver", on_delete=models.SET_NULL, null=True, - default=None) + default=None, + ) - PGP_message = models.TextField(max_length=5000, - null=True, - default=None, - blank=True) + PGP_message = models.TextField(max_length=5000, null=True, default=None, blank=True) created_at = models.DateTimeField(default=timezone.now) diff --git a/chat/routing.py b/chat/routing.py index 0b05a8cd..43ffec36 100644 --- a/chat/routing.py +++ b/chat/routing.py @@ -2,6 +2,5 @@ from django.urls import re_path from . import consumers websocket_urlpatterns = [ - re_path(r"ws/chat/(?P\w+)/$", - consumers.ChatRoomConsumer.as_asgi()), + re_path(r"ws/chat/(?P\w+)/$", consumers.ChatRoomConsumer.as_asgi()), ] diff --git a/chat/serializers.py b/chat/serializers.py index 22faac71..7e39142b 100644 --- a/chat/serializers.py +++ b/chat/serializers.py @@ -1,8 +1,8 @@ from rest_framework import serializers from chat.models import Message -class ChatSerializer(serializers.ModelSerializer): +class ChatSerializer(serializers.ModelSerializer): class Meta: model = Message fields = ( @@ -13,14 +13,17 @@ class ChatSerializer(serializers.ModelSerializer): ) depth = 0 + class PostMessageSerializer(serializers.ModelSerializer): class Meta: model = Message - fields = ("PGP_message","order","offset") + fields = ("PGP_message", "order", "offset") depth = 0 - - offset = serializers.IntegerField(allow_null=True, - default=None, - required=False, - min_value=0, - help_text="Offset for message index to get as response") \ No newline at end of file + + offset = serializers.IntegerField( + allow_null=True, + default=None, + required=False, + min_value=0, + help_text="Offset for message index to get as response", + ) diff --git a/chat/tasks.py b/chat/tasks.py index 61c30d77..60f9bf23 100644 --- a/chat/tasks.py +++ b/chat/tasks.py @@ -1,9 +1,10 @@ from celery import shared_task + @shared_task(name="chatrooms_cleansing") def chatrooms_cleansing(): """ - Deletes chatrooms and encrypted messages of orders + Deletes chatrooms and encrypted messages of orders that have completely finished more than 3 days ago. """ @@ -12,24 +13,28 @@ def chatrooms_cleansing(): from datetime import timedelta from django.utils import timezone - finished_states = [Order.Status.SUC, - Order.Status.TLD, - Order.Status.MLD, - Order.Status.CCA, - Order.Status.UCA] + finished_states = [ + Order.Status.SUC, + Order.Status.TLD, + Order.Status.MLD, + Order.Status.CCA, + Order.Status.UCA, + ] # Orders that have expired more than 3 days ago - # Usually expiry takes place 1 day after a finished order. So, ~4 days + # Usually expiry takes place 1 day after a finished order. So, ~4 days # until encrypted messages are deleted. finished_time = timezone.now() - timedelta(days=3) - queryset = Order.objects.filter(status__in=finished_states, expires_at__lt=finished_time) + queryset = Order.objects.filter( + status__in=finished_states, expires_at__lt=finished_time + ) # And do not have an active trade, any past contract or any reward. deleted_chatrooms = [] for order in queryset: # Try an except. In case some chatroom is already missing. try: - chatroom = ChatRoom.objects.get(id = order.id) + chatroom = ChatRoom.objects.get(id=order.id) deleted_chatrooms.append(str(chatroom)) chatroom.delete() except: @@ -39,4 +44,4 @@ def chatrooms_cleansing(): "num_deleted": len(deleted_chatrooms), "deleted_chatrooms": deleted_chatrooms, } - return results \ No newline at end of file + return results diff --git a/chat/views.py b/chat/views.py index af7ef7ea..e5d471d7 100644 --- a/chat/views.py +++ b/chat/views.py @@ -10,11 +10,14 @@ from django.utils import timezone from asgiref.sync import async_to_sync from channels.layers import get_channel_layer + class ChatView(viewsets.ViewSet): serializer_class = PostMessageSerializer - lookup_url_kwarg = ["order_id","offset"] + lookup_url_kwarg = ["order_id", "offset"] - queryset = Message.objects.filter(order__status__in=[Order.Status.CHA, Order.Status.FSE]) + queryset = Message.objects.filter( + order__status__in=[Order.Status.CHA, Order.Status.FSE] + ) def get(self, request, format=None): """ @@ -26,63 +29,57 @@ class ChatView(viewsets.ViewSet): if order_id is None: return Response( - { - "bad_request": - "Order ID does not exist" - }, + {"bad_request": "Order ID does not exist"}, status.HTTP_400_BAD_REQUEST, - ) + ) order = Order.objects.get(id=order_id) if not (request.user == order.maker or request.user == order.taker): return Response( - { - "bad_request": - "You are not participant in this order" - }, + {"bad_request": "You are not participant in this order"}, status.HTTP_400_BAD_REQUEST, ) if not order.status in [Order.Status.CHA, Order.Status.FSE]: return Response( - { - "bad_request": - "Order is not in chat status" - }, + {"bad_request": "Order is not in chat status"}, status.HTTP_400_BAD_REQUEST, ) queryset = Message.objects.filter(order=order, index__gt=offset) chatroom = ChatRoom.objects.get(order=order) - - # Poor idea: is_peer_connected() mockup. Update connection status based on last time a GET request was sent + + # Poor idea: is_peer_connected() mockup. Update connection status based on last time a GET request was sent if chatroom.maker == request.user: - chatroom.taker_connected = order.taker_last_seen > (timezone.now() - timedelta(minutes=1)) + chatroom.taker_connected = order.taker_last_seen > ( + timezone.now() - timedelta(minutes=1) + ) chatroom.maker_connected = True chatroom.save() peer_connected = chatroom.taker_connected elif chatroom.taker == request.user: - chatroom.maker_connected = order.maker_last_seen > (timezone.now() - timedelta(minutes=1)) + chatroom.maker_connected = order.maker_last_seen > ( + timezone.now() - timedelta(minutes=1) + ) chatroom.taker_connected = True chatroom.save() peer_connected = chatroom.maker_connected - - + messages = [] for message in queryset: d = ChatSerializer(message).data print(d) # Re-serialize so the response is identical to the consumer message data = { - 'index':d['index'], - 'time':d['created_at'], - 'message':d['PGP_message'], - 'nick': User.objects.get(id=d['sender']).username - } + "index": d["index"], + "time": d["created_at"], + "message": d["PGP_message"], + "nick": User.objects.get(id=d["sender"]).username, + } messages.append(data) - response = {'peer_connected': peer_connected, 'messages':messages} + response = {"peer_connected": peer_connected, "messages": messages} return Response(response, status.HTTP_200_OK) @@ -92,7 +89,7 @@ class ChatView(viewsets.ViewSet): """ serializer = self.serializer_class(data=request.data) - # Return bad request if serializer is not valid + # Return bad request if serializer is not valid if not serializer.is_valid(): context = {"bad_request": "Invalid serializer"} return Response(context, status=status.HTTP_400_BAD_REQUEST) @@ -102,30 +99,21 @@ class ChatView(viewsets.ViewSet): if order_id is None: return Response( - { - "bad_request": - "Order ID does not exist" - }, + {"bad_request": "Order ID does not exist"}, status.HTTP_400_BAD_REQUEST, - ) - + ) + order = Order.objects.get(id=order_id) if not (request.user == order.maker or request.user == order.taker): return Response( - { - "bad_request": - "You are not participant in this order" - }, + {"bad_request": "You are not participant in this order"}, status.HTTP_400_BAD_REQUEST, ) if not order.status in [Order.Status.CHA, Order.Status.FSE]: return Response( - { - "bad_request": - "Order is not in chat status" - }, + {"bad_request": "Order is not in chat status"}, status.HTTP_400_BAD_REQUEST, ) @@ -137,26 +125,26 @@ class ChatView(viewsets.ViewSet): receiver = order.maker chatroom, _ = ChatRoom.objects.get_or_create( - id=order_id, - order=order, - room_group_name=f"chat_order_{order_id}", - defaults={ - "maker": order.maker, - "maker_connected": order.maker == request.user, - "taker": order.taker, - "taker_connected": order.taker == request.user, - } - ) + id=order_id, + order=order, + room_group_name=f"chat_order_{order_id}", + defaults={ + "maker": order.maker, + "maker_connected": order.maker == request.user, + "taker": order.taker, + "taker_connected": order.taker == request.user, + }, + ) last_index = Message.objects.filter(order=order, chatroom=chatroom).count() new_message = Message.objects.create( - index=last_index+1, + index=last_index + 1, PGP_message=serializer.data.get("PGP_message"), order=order, chatroom=chatroom, sender=sender, receiver=receiver, - ) + ) # Send websocket message if chatroom.maker == request.user: @@ -168,13 +156,13 @@ class ChatView(viewsets.ViewSet): async_to_sync(channel_layer.group_send)( f"chat_order_{order_id}", { - "type": "PGP_message", - "index": new_message.index, - "message": new_message.PGP_message, - "time": str(new_message.created_at), - "nick": new_message.sender.username, - "peer_connected": peer_connected, - } + "type": "PGP_message", + "index": new_message.index, + "message": new_message.PGP_message, + "time": str(new_message.created_at), + "nick": new_message.sender.username, + "peer_connected": peer_connected, + }, ) # if offset is given, reply with messages @@ -187,16 +175,15 @@ class ChatView(viewsets.ViewSet): print(d) # Re-serialize so the response is identical to the consumer message data = { - 'index':d['index'], - 'time':d['created_at'], - 'message':d['PGP_message'], - 'nick': User.objects.get(id=d['sender']).username - } + "index": d["index"], + "time": d["created_at"], + "message": d["PGP_message"], + "nick": User.objects.get(id=d["sender"]).username, + } messages.append(data) - response = {'peer_connected': peer_connected, 'messages':messages} + response = {"peer_connected": peer_connected, "messages": messages} else: response = {} return Response(response, status.HTTP_200_OK) - \ No newline at end of file diff --git a/control/admin.py b/control/admin.py index 2740c408..3d1aea5a 100755 --- a/control/admin.py +++ b/control/admin.py @@ -4,6 +4,7 @@ from import_export.admin import ImportExportModelAdmin # Register your models here. + @admin.register(AccountingDay) class AccountingDayAdmin(ImportExportModelAdmin): @@ -29,6 +30,7 @@ class AccountingDayAdmin(ImportExportModelAdmin): change_links = ["day"] search_fields = ["day"] + @admin.register(BalanceLog) class BalanceLogAdmin(ImportExportModelAdmin): @@ -57,4 +59,4 @@ class BalanceLogAdmin(ImportExportModelAdmin): "ln_remote_unsettled", ] change_links = ["time"] - search_fields = ["time"] \ No newline at end of file + search_fields = ["time"] diff --git a/control/apps.py b/control/apps.py index 14765b63..d010bd75 100755 --- a/control/apps.py +++ b/control/apps.py @@ -2,5 +2,5 @@ from django.apps import AppConfig class ControlConfig(AppConfig): - default_auto_field = 'django.db.models.BigAutoField' - name = 'control' + default_auto_field = "django.db.models.BigAutoField" + name = "control" diff --git a/control/models.py b/control/models.py index cc3af699..b13aaee9 100755 --- a/control/models.py +++ b/control/models.py @@ -3,79 +3,129 @@ from django.utils import timezone from api.lightning.node import LNNode + class AccountingDay(models.Model): day = models.DateTimeField(primary_key=True, auto_now=False, auto_now_add=False) # Every field is denominated in Sats with (3 decimals for millisats) # Total volume contracted - contracted = models.DecimalField(max_digits=15, decimal_places=3, default=0, null=False, blank=False) + contracted = models.DecimalField( + max_digits=15, decimal_places=3, default=0, null=False, blank=False + ) # Number of contracts num_contracts = models.BigIntegerField(default=0, null=False, blank=False) # Net volume of trading invoices settled (excludes disputes) - net_settled = models.DecimalField(max_digits=15, decimal_places=3, default=0, null=False, blank=False) + net_settled = models.DecimalField( + max_digits=15, decimal_places=3, default=0, null=False, blank=False + ) # Net volume of trading invoices paid (excludes rewards and disputes) - net_paid = models.DecimalField(max_digits=15, decimal_places=3, default=0, null=False, blank=False) + net_paid = models.DecimalField( + max_digits=15, decimal_places=3, default=0, null=False, blank=False + ) # Sum of net settled and net paid - net_balance = models.DecimalField(max_digits=15, decimal_places=3, default=0, null=False, blank=False) + net_balance = models.DecimalField( + max_digits=15, decimal_places=3, default=0, null=False, blank=False + ) # Total volume of invoices settled - inflow = models.DecimalField(max_digits=15, decimal_places=3, default=0, null=False, blank=False) + inflow = models.DecimalField( + max_digits=15, decimal_places=3, default=0, null=False, blank=False + ) # Total volume of invoices paid - outflow = models.DecimalField(max_digits=15, decimal_places=3, default=0, null=False, blank=False) + outflow = models.DecimalField( + max_digits=15, decimal_places=3, default=0, null=False, blank=False + ) # Total cost in routing fees - routing_fees = models.DecimalField(max_digits=15, decimal_places=3, default=0, null=False, blank=False) + routing_fees = models.DecimalField( + max_digits=15, decimal_places=3, default=0, null=False, blank=False + ) # Total cost in minig fees - mining_fees = models.DecimalField(max_digits=15, decimal_places=3, default=0, null=False, blank=False) + mining_fees = models.DecimalField( + max_digits=15, decimal_places=3, default=0, null=False, blank=False + ) # Total inflows minus outflows and routing fees - cashflow = models.DecimalField(max_digits=15, decimal_places=3, default=0, null=False, blank=False) + cashflow = models.DecimalField( + max_digits=15, decimal_places=3, default=0, null=False, blank=False + ) # Balance on earned rewards (referral rewards, slashed bonds and solved disputes) - outstanding_earned_rewards = models.DecimalField(max_digits=15, decimal_places=3, default=0, null=False, blank=False) + outstanding_earned_rewards = models.DecimalField( + max_digits=15, decimal_places=3, default=0, null=False, blank=False + ) # Balance on pending disputes (not resolved yet) - outstanding_pending_disputes = models.DecimalField(max_digits=15, decimal_places=3, default=0, null=False, blank=False) + outstanding_pending_disputes = models.DecimalField( + max_digits=15, decimal_places=3, default=0, null=False, blank=False + ) # Rewards claimed lifetime - lifetime_rewards_claimed = models.DecimalField(max_digits=15, decimal_places=3, default=0, null=False, blank=False) + lifetime_rewards_claimed = models.DecimalField( + max_digits=15, decimal_places=3, default=0, null=False, blank=False + ) # Balance change from last day on earned rewards (referral rewards, slashed bonds and solved disputes) - earned_rewards = models.DecimalField(max_digits=15, decimal_places=3, default=0, null=False, blank=False) + earned_rewards = models.DecimalField( + max_digits=15, decimal_places=3, default=0, null=False, blank=False + ) # Balance change on pending disputes (not resolved yet) - disputes = models.DecimalField(max_digits=15, decimal_places=3, default=0, null=False, blank=False) + disputes = models.DecimalField( + max_digits=15, decimal_places=3, default=0, null=False, blank=False + ) # Rewards claimed on day - rewards_claimed = models.DecimalField(max_digits=15, decimal_places=3, default=0, null=False, blank=False) + rewards_claimed = models.DecimalField( + max_digits=15, decimal_places=3, default=0, null=False, blank=False + ) + class BalanceLog(models.Model): - def get_total(): - return LNNode.wallet_balance()['total_balance'] + LNNode.channel_balance()['local_balance'] + return ( + LNNode.wallet_balance()["total_balance"] + + LNNode.channel_balance()["local_balance"] + ) + def get_frac(): - return LNNode.wallet_balance()['total_balance'] / (LNNode.wallet_balance()['total_balance'] + LNNode.channel_balance()['local_balance']) - def get_oc_total(): - return LNNode.wallet_balance()['total_balance'] - def get_oc_conf(): - return LNNode.wallet_balance()['confirmed_balance'] + return LNNode.wallet_balance()["total_balance"] / ( + LNNode.wallet_balance()["total_balance"] + + LNNode.channel_balance()["local_balance"] + ) + + def get_oc_total(): + return LNNode.wallet_balance()["total_balance"] + + def get_oc_conf(): + return LNNode.wallet_balance()["confirmed_balance"] + def get_oc_unconf(): - return LNNode.wallet_balance()['unconfirmed_balance'] + return LNNode.wallet_balance()["unconfirmed_balance"] + def get_ln_local(): - return LNNode.channel_balance()['local_balance'] + return LNNode.channel_balance()["local_balance"] + def get_ln_remote(): - return LNNode.channel_balance()['remote_balance'] + return LNNode.channel_balance()["remote_balance"] + def get_ln_local_unsettled(): - return LNNode.channel_balance()['unsettled_local_balance'] + return LNNode.channel_balance()["unsettled_local_balance"] + def get_ln_remote_unsettled(): - return LNNode.channel_balance()['unsettled_remote_balance'] + return LNNode.channel_balance()["unsettled_remote_balance"] time = models.DateTimeField(primary_key=True, default=timezone.now) # Every field is denominated in Sats total = models.PositiveBigIntegerField(default=get_total) - onchain_fraction = models.DecimalField(max_digits=6, decimal_places=5, default=get_frac) + onchain_fraction = models.DecimalField( + max_digits=6, decimal_places=5, default=get_frac + ) onchain_total = models.PositiveBigIntegerField(default=get_oc_total) onchain_confirmed = models.PositiveBigIntegerField(default=get_oc_conf) onchain_unconfirmed = models.PositiveBigIntegerField(default=get_oc_unconf) ln_local = models.PositiveBigIntegerField(default=get_ln_local) ln_remote = models.PositiveBigIntegerField(default=get_ln_remote) ln_local_unsettled = models.PositiveBigIntegerField(default=get_ln_local_unsettled) - ln_remote_unsettled = models.PositiveBigIntegerField(default=get_ln_remote_unsettled) + ln_remote_unsettled = models.PositiveBigIntegerField( + default=get_ln_remote_unsettled + ) def __str__(self): return f"Balance at {self.time.strftime('%d/%m/%Y %H:%M:%S')}" + class Dispute(models.Model): - pass \ No newline at end of file + pass diff --git a/control/tasks.py b/control/tasks.py index 019f61b6..b7250a81 100644 --- a/control/tasks.py +++ b/control/tasks.py @@ -1,10 +1,11 @@ from celery import shared_task + @shared_task(name="do_accounting") def do_accounting(): - ''' + """ Does all accounting from the beginning of time - ''' + """ from api.models import Order, LNPayment, OnchainPayment, Profile, MarketTick from control.models import AccountingDay @@ -18,61 +19,84 @@ def do_accounting(): today = timezone.now().date() try: - last_accounted_day = AccountingDay.objects.latest('day').day.date() - accounted_yesterday = AccountingDay.objects.latest('day') + last_accounted_day = AccountingDay.objects.latest("day").day.date() + accounted_yesterday = AccountingDay.objects.latest("day") except: last_accounted_day = None accounted_yesterday = None if last_accounted_day == today: - return {'message':'no days to account for'} + return {"message": "no days to account for"} elif last_accounted_day != None: initial_day = last_accounted_day + timedelta(days=1) elif last_accounted_day == None: - initial_day = all_payments.earliest('created_at').created_at.date() + initial_day = all_payments.earliest("created_at").created_at.date() - day = initial_day result = {} while day <= today: - day_payments = all_payments.filter(created_at__gte=day,created_at__lte=day+timedelta(days=1)) - day_onchain_payments = OnchainPayment.objects.filter(created_at__gte=day,created_at__lte=day+timedelta(days=1)) - day_ticks = all_ticks.filter(timestamp__gte=day,timestamp__lte=day+timedelta(days=1)) + day_payments = all_payments.filter( + created_at__gte=day, created_at__lte=day + timedelta(days=1) + ) + day_onchain_payments = OnchainPayment.objects.filter( + created_at__gte=day, created_at__lte=day + timedelta(days=1) + ) + day_ticks = all_ticks.filter( + timestamp__gte=day, timestamp__lte=day + timedelta(days=1) + ) # Coarse accounting based on LNpayment and OnchainPayment objects - contracted = day_ticks.aggregate(Sum('volume'))['volume__sum'] + contracted = day_ticks.aggregate(Sum("volume"))["volume__sum"] num_contracts = day_ticks.count() - inflow = day_payments.filter(type=LNPayment.Types.HOLD,status=LNPayment.Status.SETLED).aggregate(Sum('num_satoshis'))['num_satoshis__sum'] - onchain_outflow = day_onchain_payments.filter(status__in=[OnchainPayment.Status.MEMPO,OnchainPayment.Status.CONFI]).aggregate(Sum('sent_satoshis'))['sent_satoshis__sum'] + inflow = day_payments.filter( + type=LNPayment.Types.HOLD, status=LNPayment.Status.SETLED + ).aggregate(Sum("num_satoshis"))["num_satoshis__sum"] + onchain_outflow = day_onchain_payments.filter( + status__in=[OnchainPayment.Status.MEMPO, OnchainPayment.Status.CONFI] + ).aggregate(Sum("sent_satoshis"))["sent_satoshis__sum"] onchain_outflow = 0 if onchain_outflow == None else int(onchain_outflow) - offchain_outflow = day_payments.filter(type=LNPayment.Types.NORM,status=LNPayment.Status.SUCCED).aggregate(Sum('num_satoshis'))['num_satoshis__sum'] + offchain_outflow = day_payments.filter( + type=LNPayment.Types.NORM, status=LNPayment.Status.SUCCED + ).aggregate(Sum("num_satoshis"))["num_satoshis__sum"] offchain_outflow = 0 if offchain_outflow == None else int(offchain_outflow) - routing_fees = day_payments.filter(type=LNPayment.Types.NORM,status=LNPayment.Status.SUCCED).aggregate(Sum('fee'))['fee__sum'] - mining_fees = day_onchain_payments.filter(status__in=[OnchainPayment.Status.MEMPO,OnchainPayment.Status.CONFI]).aggregate(Sum('mining_fee_sats'))['mining_fee_sats__sum'] - rewards_claimed = day_payments.filter(type=LNPayment.Types.NORM,concept=LNPayment.Concepts.WITHREWA,status=LNPayment.Status.SUCCED).aggregate(Sum('num_satoshis'))['num_satoshis__sum'] + routing_fees = day_payments.filter( + type=LNPayment.Types.NORM, status=LNPayment.Status.SUCCED + ).aggregate(Sum("fee"))["fee__sum"] + mining_fees = day_onchain_payments.filter( + status__in=[OnchainPayment.Status.MEMPO, OnchainPayment.Status.CONFI] + ).aggregate(Sum("mining_fee_sats"))["mining_fee_sats__sum"] + rewards_claimed = day_payments.filter( + type=LNPayment.Types.NORM, + concept=LNPayment.Concepts.WITHREWA, + status=LNPayment.Status.SUCCED, + ).aggregate(Sum("num_satoshis"))["num_satoshis__sum"] contracted = 0 if contracted == None else contracted inflow = 0 if inflow == None else inflow outflow = offchain_outflow + onchain_outflow - routing_fees = 0 if routing_fees == None else routing_fees + routing_fees = 0 if routing_fees == None else routing_fees rewards_claimed = 0 if rewards_claimed == None else rewards_claimed mining_fees = 0 if mining_fees == None else mining_fees accounted_day = AccountingDay.objects.create( - day = day, - contracted = contracted, - num_contracts = num_contracts, - inflow = inflow, - outflow = outflow, - routing_fees = routing_fees, - mining_fees = mining_fees, - cashflow = inflow - outflow - routing_fees, - rewards_claimed = rewards_claimed, - ) - + day=day, + contracted=contracted, + num_contracts=num_contracts, + inflow=inflow, + outflow=outflow, + routing_fees=routing_fees, + mining_fees=mining_fees, + cashflow=inflow - outflow - routing_fees, + rewards_claimed=rewards_claimed, + ) + # Fine Net Daily accounting based on orders # Only account for orders where everything worked out right - payouts = day_payments.filter(type=LNPayment.Types.NORM,concept=LNPayment.Concepts.PAYBUYER, status=LNPayment.Status.SUCCED) + payouts = day_payments.filter( + type=LNPayment.Types.NORM, + concept=LNPayment.Concepts.PAYBUYER, + status=LNPayment.Status.SUCCED, + ) escrows_settled = 0 payouts_paid = 0 costs = 0 @@ -80,31 +104,42 @@ def do_accounting(): escrows_settled += int(payout.order_paid_LN.trade_escrow.num_satoshis) payouts_paid += int(payout.num_satoshis) costs += int(payout.fee) - + # Same for orders that use onchain payments. - payouts_tx = day_onchain_payments.filter(status__in=[OnchainPayment.Status.MEMPO,OnchainPayment.Status.CONFI]) + payouts_tx = day_onchain_payments.filter( + status__in=[OnchainPayment.Status.MEMPO, OnchainPayment.Status.CONFI] + ) for payout_tx in payouts_tx: escrows_settled += int(payout_tx.order_paid_TX.trade_escrow.num_satoshis) payouts_paid += int(payout_tx.sent_satoshis) costs += int(payout_tx.mining_fee_sats) - # account for those orders where bonds were lost # + Settled bonds / bond_split - bonds_settled = day_payments.filter(type=LNPayment.Types.HOLD,concept__in=[LNPayment.Concepts.TAKEBOND,LNPayment.Concepts.MAKEBOND], status=LNPayment.Status.SETLED) + bonds_settled = day_payments.filter( + type=LNPayment.Types.HOLD, + concept__in=[LNPayment.Concepts.TAKEBOND, LNPayment.Concepts.MAKEBOND], + status=LNPayment.Status.SETLED, + ) if len(bonds_settled) > 0: - collected_slashed_bonds = (bonds_settled.aggregate(Sum('num_satoshis'))['num_satoshis__sum'])* float(config('SLASHED_BOND_REWARD_SPLIT')) + collected_slashed_bonds = ( + bonds_settled.aggregate(Sum("num_satoshis"))["num_satoshis__sum"] + ) * float(config("SLASHED_BOND_REWARD_SPLIT")) else: collected_slashed_bonds = 0 - + accounted_day.net_settled = escrows_settled + collected_slashed_bonds accounted_day.net_paid = payouts_paid + costs - accounted_day.net_balance = float(accounted_day.net_settled) - float(accounted_day.net_paid) + accounted_day.net_balance = float(accounted_day.net_settled) - float( + accounted_day.net_paid + ) # Differential accounting based on change of outstanding states and disputes unreslved if day == today: - pending_disputes = Order.objects.filter(status__in=[Order.Status.DIS,Order.Status.WFR]) + pending_disputes = Order.objects.filter( + status__in=[Order.Status.DIS, Order.Status.WFR] + ) if len(pending_disputes) > 0: outstanding_pending_disputes = 0 for order in pending_disputes: @@ -112,28 +147,44 @@ def do_accounting(): else: outstanding_pending_disputes = 0 - accounted_day.outstanding_earned_rewards = Profile.objects.all().aggregate(Sum('earned_rewards'))['earned_rewards__sum'] + accounted_day.outstanding_earned_rewards = Profile.objects.all().aggregate( + Sum("earned_rewards") + )["earned_rewards__sum"] accounted_day.outstanding_pending_disputes = outstanding_pending_disputes - accounted_day.lifetime_rewards_claimed = Profile.objects.all().aggregate(Sum('claimed_rewards'))['claimed_rewards__sum'] + accounted_day.lifetime_rewards_claimed = Profile.objects.all().aggregate( + Sum("claimed_rewards") + )["claimed_rewards__sum"] if accounted_yesterday != None: - accounted_day.earned_rewards = accounted_day.outstanding_earned_rewards - accounted_yesterday.outstanding_earned_rewards - accounted_day.disputes = outstanding_pending_disputes - accounted_yesterday.outstanding_earned_rewards + accounted_day.earned_rewards = ( + accounted_day.outstanding_earned_rewards + - accounted_yesterday.outstanding_earned_rewards + ) + accounted_day.disputes = ( + outstanding_pending_disputes + - accounted_yesterday.outstanding_earned_rewards + ) # Close the loop accounted_day.save() accounted_yesterday = accounted_day - result[str(day)]={'contracted':contracted,'inflow':inflow,'outflow':outflow} + result[str(day)] = { + "contracted": contracted, + "inflow": inflow, + "outflow": outflow, + } day = day + timedelta(days=1) return result + @shared_task(name="compute_node_balance", ignore_result=True) def compute_node_balance(): - ''' + """ Queries LND for channel and wallet balance - ''' + """ from control.models import BalanceLog + BalanceLog.objects.create() - + return diff --git a/robosats/__init__.py b/robosats/__init__.py index ce285f3c..0165ba0d 100644 --- a/robosats/__init__.py +++ b/robosats/__init__.py @@ -4,4 +4,4 @@ from __future__ import absolute_import, unicode_literals # Django starts so that shared_task will use this app. from .celery import app as celery_app -__all__ = ("celery_app", ) +__all__ = ("celery_app",) diff --git a/robosats/celery/__init__.py b/robosats/celery/__init__.py index 8185d708..7d83369c 100644 --- a/robosats/celery/__init__.py +++ b/robosats/celery/__init__.py @@ -35,11 +35,11 @@ app.conf.beat_schedule = { "task": "users_cleansing", "schedule": crontab(hour=0, minute=0), }, - "chatrooms-cleansing": { # Cleans 3+ days old encrypted messages and chatrooms at midnight + "chatrooms-cleansing": { # Cleans 3+ days old encrypted messages and chatrooms at midnight "task": "chatrooms_cleansing", "schedule": crontab(hour=0, minute=0), }, - "lnpayments-cleansing": { # Cleans 3+ days old unlocked hodl invoices + "lnpayments-cleansing": { # Cleans 3+ days old unlocked hodl invoices "task": "payments_cleansing", "schedule": crontab(hour=0, minute=0), }, @@ -55,10 +55,10 @@ app.conf.beat_schedule = { "task": "cache_external_market_prices", "schedule": timedelta(seconds=60), }, - "compute-node-balance": { # Logs LND channel and wallet balance - "task":"compute_node_balance", + "compute-node-balance": { # Logs LND channel and wallet balance + "task": "compute_node_balance", "schedule": timedelta(minutes=60), - } + }, } app.conf.timezone = "UTC" diff --git a/robosats/routing.py b/robosats/routing.py index 36d2ca11..2aa404e4 100644 --- a/robosats/routing.py +++ b/robosats/routing.py @@ -2,11 +2,13 @@ from channels.auth import AuthMiddlewareStack from channels.routing import ProtocolTypeRouter, URLRouter import chat.routing -application = ProtocolTypeRouter({ - "websocket": - AuthMiddlewareStack( - URLRouter( - chat.routing.websocket_urlpatterns, - # TODO add api.routing.websocket_urlpatterns when Order page works with websocket - )), -}) +application = ProtocolTypeRouter( + { + "websocket": AuthMiddlewareStack( + URLRouter( + chat.routing.websocket_urlpatterns, + # TODO add api.routing.websocket_urlpatterns when Order page works with websocket + ) + ), + } +) diff --git a/robosats/settings.py b/robosats/settings.py index d8ecf741..ed08a977 100644 --- a/robosats/settings.py +++ b/robosats/settings.py @@ -53,21 +53,21 @@ SESSION_COOKIE_HTTPONLY = False # Logging settings if os.environ.get("LOG_TO_CONSOLE"): LOGGING = { - 'version': 1, - 'disable_existing_loggers': False, - 'handlers': { - 'console': { - 'class': 'logging.StreamHandler', + "version": 1, + "disable_existing_loggers": False, + "handlers": { + "console": { + "class": "logging.StreamHandler", }, }, - 'root': { - 'handlers': ['console'], - 'level': 'WARNING', + "root": { + "handlers": ["console"], + "level": "WARNING", }, - 'loggers': { - 'api.utils': { - 'handlers': ['console'], - 'level': 'WARNING', + "loggers": { + "api.utils": { + "handlers": ["console"], + "level": "WARNING", }, }, } @@ -95,12 +95,12 @@ INSTALLED_APPS = [ ] REST_FRAMEWORK = { - 'DEFAULT_SCHEMA_CLASS': 'drf_spectacular.openapi.AutoSchema', + "DEFAULT_SCHEMA_CLASS": "drf_spectacular.openapi.AutoSchema", } SPECTACULAR_SETTINGS = { - 'TITLE': 'RoboSats REST API v0', - 'DESCRIPTION': textwrap.dedent( + "TITLE": "RoboSats REST API v0", + "DESCRIPTION": textwrap.dedent( """ REST API Documentation for [RoboSats](https://learn.robosats.com) - A Simple and Private LN P2P Exchange @@ -114,21 +114,21 @@ SPECTACULAR_SETTINGS = { """ ), - 'VERSION': '0.1.0', - 'SERVE_INCLUDE_SCHEMA': False, - 'SWAGGER_UI_DIST': 'SIDECAR', # shorthand to use the sidecar instead - 'SWAGGER_UI_FAVICON_HREF': 'SIDECAR', - 'REDOC_UI_SETTINGS': { - 'expandResponses': '200,201', + "VERSION": "0.1.0", + "SERVE_INCLUDE_SCHEMA": False, + "SWAGGER_UI_DIST": "SIDECAR", # shorthand to use the sidecar instead + "SWAGGER_UI_FAVICON_HREF": "SIDECAR", + "REDOC_UI_SETTINGS": { + "expandResponses": "200,201", }, - 'EXTENSIONS_INFO': { - 'x-logo': { - 'url': 'https://raw.githubusercontent.com/Reckless-Satoshi/robosats/main/frontend/static/assets/images/robosats-0.1.1-banner.png', - 'backgroundColor': '#FFFFFF', - 'altText': 'RoboSats logo' + "EXTENSIONS_INFO": { + "x-logo": { + "url": "https://raw.githubusercontent.com/Reckless-Satoshi/robosats/main/frontend/static/assets/images/robosats-0.1.1-banner.png", + "backgroundColor": "#FFFFFF", + "altText": "RoboSats logo", } }, - 'REDOC_DIST': 'SIDECAR', + "REDOC_DIST": "SIDECAR", } from .celery.conf import * @@ -173,7 +173,7 @@ DATABASES = { "NAME": config("POSTGRES_DB"), "USER": config("POSTGRES_USER"), "PASSWORD": config("POSTGRES_PASSWORD"), - 'HOST': config("POSTGRES_HOST"), + "HOST": config("POSTGRES_HOST"), "PORT": config("POSTGRES_PORT"), } } @@ -183,20 +183,16 @@ DATABASES = { AUTH_PASSWORD_VALIDATORS = [ { - "NAME": - "django.contrib.auth.password_validation.UserAttributeSimilarityValidator", + "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator", }, { - "NAME": - "django.contrib.auth.password_validation.MinimumLengthValidator", + "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator", }, { - "NAME": - "django.contrib.auth.password_validation.CommonPasswordValidator", + "NAME": "django.contrib.auth.password_validation.CommonPasswordValidator", }, { - "NAME": - "django.contrib.auth.password_validation.NumericPasswordValidator", + "NAME": "django.contrib.auth.password_validation.NumericPasswordValidator", }, ] @@ -230,9 +226,7 @@ CACHES = { "default": { "BACKEND": "django_redis.cache.RedisCache", "LOCATION": config("REDIS_URL"), - "OPTIONS": { - "CLIENT_CLASS": "django_redis.client.DefaultClient" - }, + "OPTIONS": {"CLIENT_CLASS": "django_redis.client.DefaultClient"}, } }