diff --git a/api/admin.py b/api/admin.py
index 5f99723f..2e71e0fa 100644
--- a/api/admin.py
+++ b/api/admin.py
@@ -7,55 +7,127 @@ from .models import Order, LNPayment, Profile, MarketTick, Currency
admin.site.unregister(Group)
admin.site.unregister(User)
+
class ProfileInline(admin.StackedInline):
model = Profile
- can_delete = False
- fields = ('avatar_tag',)
- readonly_fields = ['avatar_tag']
-
+ can_delete = False
+ fields = ("avatar_tag", )
+ readonly_fields = ["avatar_tag"]
+
+
# extended users with avatars
@admin.register(User)
class EUserAdmin(UserAdmin):
inlines = [ProfileInline]
- list_display = ('avatar_tag','id','username','last_login','date_joined','is_staff')
- list_display_links = ('id','username')
- ordering = ('-id',)
+ list_display = (
+ "avatar_tag",
+ "id",
+ "username",
+ "last_login",
+ "date_joined",
+ "is_staff",
+ )
+ list_display_links = ("id", "username")
+ ordering = ("-id", )
+
def avatar_tag(self, obj):
return obj.profile.avatar_tag()
@admin.register(Order)
class OrderAdmin(AdminChangeLinksMixin, admin.ModelAdmin):
- list_display = ('id','type','maker_link','taker_link','status','amount','currency_link','t0_satoshis','is_disputed','is_fiat_sent','created_at','expires_at', 'payout_link','maker_bond_link','taker_bond_link','trade_escrow_link')
- list_display_links = ('id','type')
- change_links = ('maker','taker','currency','payout','maker_bond','taker_bond','trade_escrow')
- list_filter = ('is_disputed','is_fiat_sent','type','currency','status')
+ list_display = (
+ "id",
+ "type",
+ "maker_link",
+ "taker_link",
+ "status",
+ "amount",
+ "currency_link",
+ "t0_satoshis",
+ "is_disputed",
+ "is_fiat_sent",
+ "created_at",
+ "expires_at",
+ "payout_link",
+ "maker_bond_link",
+ "taker_bond_link",
+ "trade_escrow_link",
+ )
+ list_display_links = ("id", "type")
+ change_links = (
+ "maker",
+ "taker",
+ "currency",
+ "payout",
+ "maker_bond",
+ "taker_bond",
+ "trade_escrow",
+ )
+ list_filter = ("is_disputed", "is_fiat_sent", "type", "currency", "status")
+
@admin.register(LNPayment)
class LNPaymentAdmin(AdminChangeLinksMixin, admin.ModelAdmin):
- list_display = ('hash','concept','status','num_satoshis','type','expires_at','expiry_height','sender_link','receiver_link','order_made_link','order_taken_link','order_escrow_link','order_paid_link')
- list_display_links = ('hash','concept')
- change_links = ('sender','receiver','order_made','order_taken','order_escrow','order_paid')
- list_filter = ('type','concept','status')
- ordering = ('-expires_at',)
+ list_display = (
+ "hash",
+ "concept",
+ "status",
+ "num_satoshis",
+ "type",
+ "expires_at",
+ "expiry_height",
+ "sender_link",
+ "receiver_link",
+ "order_made_link",
+ "order_taken_link",
+ "order_escrow_link",
+ "order_paid_link",
+ )
+ list_display_links = ("hash", "concept")
+ change_links = (
+ "sender",
+ "receiver",
+ "order_made",
+ "order_taken",
+ "order_escrow",
+ "order_paid",
+ )
+ list_filter = ("type", "concept", "status")
+ ordering = ("-expires_at", )
+
@admin.register(Profile)
class UserProfileAdmin(AdminChangeLinksMixin, admin.ModelAdmin):
- list_display = ('avatar_tag','id','user_link','total_contracts','platform_rating','total_ratings','avg_rating','num_disputes','lost_disputes')
- list_display_links = ('avatar_tag','id')
- change_links =['user']
- readonly_fields = ['avatar_tag']
+ list_display = (
+ "avatar_tag",
+ "id",
+ "user_link",
+ "total_contracts",
+ "platform_rating",
+ "total_ratings",
+ "avg_rating",
+ "num_disputes",
+ "lost_disputes",
+ )
+ list_display_links = ("avatar_tag", "id")
+ change_links = ["user"]
+ readonly_fields = ["avatar_tag"]
+
@admin.register(Currency)
class CurrencieAdmin(admin.ModelAdmin):
- list_display = ('id','currency','exchange_rate','timestamp')
- list_display_links = ('id','currency')
- readonly_fields = ('currency','exchange_rate','timestamp')
- ordering = ('id',)
+ list_display = ("id", "currency", "exchange_rate", "timestamp")
+ list_display_links = ("id", "currency")
+ readonly_fields = ("currency", "exchange_rate", "timestamp")
+ 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_filter = ['currency']
- ordering = ('-timestamp',)
\ No newline at end of file
+ list_display = ("timestamp", "price", "volume", "premium", "currency",
+ "fee")
+ readonly_fields = ("timestamp", "price", "volume", "premium", "currency",
+ "fee")
+ list_filter = ["currency"]
+ ordering = ("-timestamp", )
diff --git a/api/apps.py b/api/apps.py
index 66656fd2..878e7d54 100644
--- a/api/apps.py
+++ b/api/apps.py
@@ -2,5 +2,5 @@ from django.apps import AppConfig
class ApiConfig(AppConfig):
- default_auto_field = 'django.db.models.BigAutoField'
- name = 'api'
+ default_auto_field = "django.db.models.BigAutoField"
+ name = "api"
diff --git a/api/lightning/node.py b/api/lightning/node.py
index 884628ae..c6214eb2 100644
--- a/api/lightning/node.py
+++ b/api/lightning/node.py
@@ -10,27 +10,30 @@ from datetime import timedelta, datetime
from django.utils import timezone
from api.models import LNPayment
+
#######
# Should work with LND (c-lightning in the future if there are features that deserve the work)
#######
# Read tls.cert from file or .env variable string encoded as base64
try:
- CERT = open(os.path.join(config('LND_DIR'),'tls.cert'), 'rb').read()
+ CERT = open(os.path.join(config("LND_DIR"), "tls.cert"), "rb").read()
except:
- CERT = b64decode(config('LND_CERT_BASE64'))
+ CERT = b64decode(config("LND_CERT_BASE64"))
# 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'))
+ MACAROON = b64decode(config("LND_MACAROON_BASE64"))
-LND_GRPC_HOST = config('LND_GRPC_HOST')
+LND_GRPC_HOST = config("LND_GRPC_HOST")
-class LNNode():
- os.environ["GRPC_SSL_CIPHER_SUITES"] = 'HIGH+ECDSA'
+class LNNode:
+
+ os.environ["GRPC_SSL_CIPHER_SUITES"] = "HIGH+ECDSA"
creds = grpc.ssl_channel_credentials(CERT)
channel = grpc.secure_channel(LND_GRPC_HOST, creds)
@@ -44,89 +47,112 @@ class LNNode():
routerrpc = routerrpc
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.",
- 3: "A non-recoverable error has occured.",
- 4: "Payment details incorrect (unknown hash, invalid amt or invalid final cltv delta)",
- 5: "Insufficient local balance."}
+ 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.",
+ 3: "A non-recoverable error has occured.",
+ 4:
+ "Payment details incorrect (unknown hash, invalid amt or invalid final cltv delta)",
+ 5: "Insufficient local balance.",
+ }
@classmethod
def decode_payreq(cls, invoice):
- '''Decodes a lightning payment request (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
- 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())])
+ 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())])
# Fix this: tricky because canceling sucessfully an invoice has no response. TODO
- return str(response) == "" # True if no response, false otherwise.
+ 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())])
+ """settles a hold invoice"""
+ 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.
+ return str(response) == "" # True if no response, false otherwise.
@classmethod
- def gen_hold_invoice(cls, num_satoshis, description, invoice_expiry, cltv_expiry_secs):
- '''Generates hold invoice'''
+ def gen_hold_invoice(cls, num_satoshis, description, invoice_expiry,
+ cltv_expiry_secs):
+ """Generates hold invoice"""
hold_payment = {}
# The preimage is a random hash of 256 bits entropy
- preimage = hashlib.sha256(secrets.token_bytes(nbytes=32)).digest()
+ preimage = hashlib.sha256(secrets.token_bytes(nbytes=32)).digest()
# Its hash is used to generate the hold invoice
r_hash = hashlib.sha256(preimage).digest()
-
- # timelock expiry for the last hop, computed based on a 10 minutes block with 30% padding (~7 min block)
- cltv_expiry_blocks = int(cltv_expiry_secs / (7*60))
- request = invoicesrpc.AddHoldInvoiceRequest(
- memo=description,
- value=num_satoshis,
- hash=r_hash,
- expiry=int(invoice_expiry*1.5), # 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())])
- 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))
- hold_payment['expires_at'] = hold_payment['created_at'] + timedelta(seconds=payreq_decoded.expiry)
- hold_payment['cltv_expiry'] = cltv_expiry_blocks
+ # timelock expiry for the last hop, computed based on a 10 minutes block with 30% padding (~7 min block)
+ cltv_expiry_blocks = int(cltv_expiry_secs / (7 * 60))
+ request = invoicesrpc.AddHoldInvoiceRequest(
+ memo=description,
+ value=num_satoshis,
+ hash=r_hash,
+ expiry=int(
+ invoice_expiry * 1.5
+ ), # 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())])
+
+ 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))
+ hold_payment["expires_at"] = hold_payment["created_at"] + timedelta(
+ seconds=payreq_decoded.expiry)
+ hold_payment["cltv_expiry"] = cltv_expiry_blocks
return hold_payment
@classmethod
def validate_hold_invoice_locked(cls, lnpayment):
- '''Checks if hold invoice is locked'''
- request = invoicesrpc.LookupInvoiceMsg(payment_hash=bytes.fromhex(lnpayment.payment_hash))
- response = cls.invoicesstub.LookupInvoiceV2(request, metadata=[('macaroon', MACAROON.hex())])
- print('status here')
+ """Checks if hold invoice is locked"""
+ request = invoicesrpc.LookupInvoiceMsg(
+ payment_hash=bytes.fromhex(lnpayment.payment_hash))
+ response = cls.invoicesstub.LookupInvoiceV2(request,
+ metadata=[("macaroon",
+ MACAROON.hex())
+ ])
+ print("status here")
print(response.state)
# TODO ERROR HANDLING
- # Will fail if 'unable to locate invoice'. Happens if invoice expiry
+ # 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
# and report back that the invoice has expired (better robustness)
- if response.state == 0: # OPEN
- print('STATUS: OPEN')
+ if response.state == 0: # OPEN
+ print("STATUS: OPEN")
pass
- if response.state == 1: # SETTLED
+ if response.state == 1: # SETTLED
pass
- if response.state == 2: # CANCELLED
+ if response.state == 2: # CANCELLED
pass
- if response.state == 3: # ACCEPTED (LOCKED)
- print('STATUS: ACCEPTED')
+ if response.state == 3: # ACCEPTED (LOCKED)
+ print("STATUS: ACCEPTED")
lnpayment.expiry_height = response.htlcs[0].expiry_height
lnpayment.status = LNPayment.Status.LOCKED
lnpayment.save()
@@ -135,85 +161,104 @@ 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
def validate_ln_invoice(cls, invoice, num_satoshis):
- '''Checks if the submited LN invoice comforms to expectations'''
+ """Checks if the submited LN invoice comforms to expectations"""
payout = {
- 'valid': False,
- 'context': None,
- 'description': None,
- 'payment_hash': None,
- 'created_at': None,
- 'expires_at': None,
- }
+ "valid": False,
+ "context": None,
+ "description": None,
+ "payment_hash": None,
+ "created_at": None,
+ "expires_at": None,
+ }
try:
payreq_decoded = cls.decode_payreq(invoice)
print(payreq_decoded)
except:
- payout['context'] = {'bad_invoice':'Does not look like a valid lightning invoice'}
+ payout["context"] = {
+ "bad_invoice": "Does not look like a valid lightning invoice"
+ }
return payout
if payreq_decoded.num_satoshis == 0:
- payout['context'] = {'bad_invoice':'The invoice provided has no explicit amount'}
+ payout["context"] = {
+ "bad_invoice": "The invoice provided has no explicit amount"
+ }
return payout
if not payreq_decoded.num_satoshis == num_satoshis:
- payout['context'] = {'bad_invoice':'The invoice provided is not for '+'{:,}'.format(num_satoshis)+ ' Sats'}
+ payout["context"] = {
+ "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))
- payout['expires_at'] = payout['created_at'] + timedelta(seconds=payreq_decoded.expiry)
+ payout["created_at"] = timezone.make_aware(
+ datetime.fromtimestamp(payreq_decoded.timestamp))
+ payout["expires_at"] = payout["created_at"] + timedelta(
+ seconds=payreq_decoded.expiry)
- if payout['expires_at'] < timezone.now():
- payout['context'] = {'bad_invoice':f'The invoice provided has already expired'}
+ if payout["expires_at"] < timezone.now():
+ payout["context"] = {
+ "bad_invoice": f"The invoice provided has already expired"
+ }
return payout
- payout['valid'] = True
- payout['description'] = payreq_decoded.description
- payout['payment_hash'] = payreq_decoded.payment_hash
+ payout["valid"] = True
+ payout["description"] = payreq_decoded.description
+ payout["payment_hash"] = payreq_decoded.payment_hash
-
return payout
@classmethod
def pay_invoice(cls, invoice, num_satoshis):
- '''Sends sats to buyer'''
+ """Sends sats to buyer"""
- fee_limit_sat = int(max(num_satoshis * float(config('PROPORTIONAL_ROUTING_FEE_LIMIT')), float(config('MIN_FLAT_ROUTING_FEE_LIMIT')))) # 200 ppm or 10 sats
- request = routerrpc.SendPaymentRequest(
- payment_request=invoice,
- fee_limit_sat=fee_limit_sat,
- timeout_seconds=60)
+ fee_limit_sat = int(
+ max(
+ num_satoshis * float(config("PROPORTIONAL_ROUTING_FEE_LIMIT")),
+ float(config("MIN_FLAT_ROUTING_FEE_LIMIT")),
+ )) # 200 ppm or 10 sats
+ request = routerrpc.SendPaymentRequest(payment_request=invoice,
+ fee_limit_sat=fee_limit_sat,
+ timeout_seconds=60)
- for response in cls.routerstub.SendPaymentV2(request, metadata=[('macaroon', MACAROON.hex())]):
+ for response in cls.routerstub.SendPaymentV2(request,
+ metadata=[("macaroon",
+ MACAROON.hex())
+ ]):
print(response)
print(response.status)
# TODO ERROR HANDLING
- if response.status == 0 : # Status 0 'UNKNOWN'
- pass
- if response.status == 1 : # Status 1 'IN_FLIGHT'
- return True, 'In flight'
- if response.status == 3 : # 4 'FAILED' ??
- '''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.
- 3 A non-recoverable error has occured.
- 4 Payment details incorrect (unknown hash, invalid amt or invalid final cltv delta)
- 5 Insufficient local balance.
- '''
+ if response.status == 0: # Status 0 'UNKNOWN'
+ pass
+ if response.status == 1: # Status 1 'IN_FLIGHT'
+ return True, "In flight"
+ if response.status == 3: # 4 'FAILED' ??
+ """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.
+ 3 A non-recoverable error has occured.
+ 4 Payment details incorrect (unknown hash, invalid amt or invalid final cltv delta)
+ 5 Insufficient local balance.
+ """
context = cls.payment_failure_context[response.failure_reason]
return False, context
- if response.status == 2 : # STATUS 'SUCCEEDED'
+ if response.status == 2: # STATUS 'SUCCEEDED'
return True, None
-
# How to catch the errors like:"grpc_message":"invoice is already paid","grpc_status":6}
# These are not in the response only printed to commandline
@@ -221,15 +266,14 @@ 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())])
-
- return response.state == 1 # LND states: 0 OPEN, 1 SETTLED, 3 ACCEPTED, GRPC_ERROR status 5 when cancelled/returned
-
-
-
-
-
-
+ """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())
+ ])
+ return (
+ response.state == 1
+ ) # LND states: 0 OPEN, 1 SETTLED, 3 ACCEPTED, GRPC_ERROR status 5 when cancelled/returned
diff --git a/api/logics.py b/api/logics.py
index f5d2eb13..8553b4fd 100644
--- a/api/logics.py
+++ b/api/logics.py
@@ -11,129 +11,189 @@ from api.tasks import follow_send_payment
import math
import ast
-FEE = float(config('FEE'))
-BOND_SIZE = float(config('BOND_SIZE'))
-ESCROW_USERNAME = config('ESCROW_USERNAME')
-PENALTY_TIMEOUT = int(config('PENALTY_TIMEOUT'))
+FEE = float(config("FEE"))
+BOND_SIZE = float(config("BOND_SIZE"))
+ESCROW_USERNAME = config("ESCROW_USERNAME")
+PENALTY_TIMEOUT = int(config("PENALTY_TIMEOUT"))
-MIN_TRADE = int(config('MIN_TRADE'))
-MAX_TRADE = int(config('MAX_TRADE'))
+MIN_TRADE = int(config("MIN_TRADE"))
+MAX_TRADE = int(config("MAX_TRADE"))
-EXP_MAKER_BOND_INVOICE = int(config('EXP_MAKER_BOND_INVOICE'))
-EXP_TAKER_BOND_INVOICE = int(config('EXP_TAKER_BOND_INVOICE'))
+EXP_MAKER_BOND_INVOICE = int(config("EXP_MAKER_BOND_INVOICE"))
+EXP_TAKER_BOND_INVOICE = int(config("EXP_TAKER_BOND_INVOICE"))
-BOND_EXPIRY = int(config('BOND_EXPIRY'))
-ESCROW_EXPIRY = int(config('ESCROW_EXPIRY'))
+BOND_EXPIRY = int(config("BOND_EXPIRY"))
+ESCROW_EXPIRY = int(config("ESCROW_EXPIRY"))
-PUBLIC_ORDER_DURATION = int(config('PUBLIC_ORDER_DURATION'))
-INVOICE_AND_ESCROW_DURATION = int(config('INVOICE_AND_ESCROW_DURATION'))
-FIAT_EXCHANGE_DURATION = int(config('FIAT_EXCHANGE_DURATION'))
+PUBLIC_ORDER_DURATION = int(config("PUBLIC_ORDER_DURATION"))
+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'''
+ """Validates if a use is already not part of an active order"""
- active_order_status = [Order.Status.WFB, Order.Status.PUB, Order.Status.TAK,
- Order.Status.WF2, Order.Status.WFE, Order.Status.WFI,
- Order.Status.CHA, Order.Status.FSE, Order.Status.DIS,
- 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)
+ active_order_status = [
+ Order.Status.WFB,
+ Order.Status.PUB,
+ Order.Status.TAK,
+ Order.Status.WF2,
+ Order.Status.WFE,
+ Order.Status.WFI,
+ Order.Status.CHA,
+ Order.Status.FSE,
+ Order.Status.DIS,
+ 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)
if queryset.exists():
- return False, {'bad_request':'You are already maker of an active order'}, queryset[0]
+ return (
+ False,
+ {
+ "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'}, queryset[0]
-
+ return (
+ False,
+ {
+ "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=Order.Status.FAI)
+ queryset = Order.objects.filter(Q(maker=user) | Q(taker=user),
+ status=Order.Status.FAI)
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'}, order
+ return (
+ False,
+ {
+ "bad_request":
+ "You are still pending a payment from a recent order"
+ },
+ order,
+ )
return True, None, None
def validate_order_size(order):
- '''Validates if order is withing limits in satoshis at t0'''
+ """Validates if order is withing limits in satoshis at t0"""
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'}
+ 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"
+ }
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'}
+ 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"
+ }
return True, None
def user_activity_status(last_seen):
if last_seen > (timezone.now() - timedelta(minutes=2)):
- return 'Active'
+ return "Active"
elif last_seen > (timezone.now() - timedelta(minutes=10)):
- return 'Seen recently'
+ return "Seen recently"
else:
- return 'Inactive'
+ return "Inactive"
- @classmethod
+ @classmethod
def take(cls, order, user):
is_penalized, time_out = cls.is_penalized(user)
if is_penalized:
- return False, {'bad_request',f'You need to wait {time_out} seconds to take an order'}
+ return False, {
+ "bad_request",
+ f"You need to wait {time_out} seconds to take an order",
+ }
else:
order.taker = user
order.status = Order.Status.TAK
- order.expires_at = timezone.now() + timedelta(seconds=Order.t_to_expire[Order.Status.TAK])
+ order.expires_at = timezone.now() + timedelta(
+ seconds=Order.t_to_expire[Order.Status.TAK])
order.save()
return True, None
def is_buyer(order, user):
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)
+ return (is_maker and order.type == Order.Types.BUY) or (
+ 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)
-
+ return (is_maker and order.type == Order.Types.SELL) or (
+ is_taker and order.type == Order.Types.BUY)
+
def satoshis_now(order):
- ''' checks trade amount in sats '''
+ """checks trade amount in sats"""
if order.is_explicit:
satoshis_now = order.satoshis
else:
exchange_rate = float(order.currency.exchange_rate)
- premium_rate = exchange_rate * (1+float(order.premium)/100)
- satoshis_now = (float(order.amount) / premium_rate) * 100*1000*1000
+ premium_rate = exchange_rate * (1 + float(order.premium) / 100)
+ satoshis_now = (float(order.amount) /
+ premium_rate) * 100 * 1000 * 1000
return int(satoshis_now)
def price_and_premium_now(order):
- ''' computes order price and premium with current rates '''
+ """computes order price and premium with current rates"""
exchange_rate = float(order.currency.exchange_rate)
if not order.is_explicit:
premium = order.premium
- price = exchange_rate * (1+float(premium)/100)
+ price = exchange_rate * (1 + float(premium) / 100)
else:
- order_rate = float(order.amount) / (float(order.satoshis) / 100000000)
+ order_rate = float(
+ order.amount) / (float(order.satoshis) / 100000000)
premium = order_rate / exchange_rate - 1
- premium = int(premium*10000)/100 # 2 decimals left
+ premium = int(premium * 10000) / 100 # 2 decimals left
price = order_rate
significant_digits = 5
- price = round(price, significant_digits - int(math.floor(math.log10(abs(price)))) - 1)
-
+ price = round(
+ price,
+ significant_digits - int(math.floor(math.log10(abs(price)))) - 1)
+
return price, premium
@classmethod
def order_expires(cls, order):
- ''' General cases when time runs out.'''
+ """General cases when time runs out."""
# Do not change order status if an order in any with
# any of these status is sent to expire here
- does_not_expire = [Order.Status.DEL, Order.Status.UCA,
- Order.Status.EXP, Order.Status.TLD,
- Order.Status.DIS, Order.Status.CCA,
- Order.Status.PAY, Order.Status.SUC,
- Order.Status.FAI, Order.Status.MLD]
+ does_not_expire = [
+ Order.Status.DEL,
+ Order.Status.UCA,
+ Order.Status.EXP,
+ Order.Status.TLD,
+ Order.Status.DIS,
+ Order.Status.CCA,
+ Order.Status.PAY,
+ Order.Status.SUC,
+ Order.Status.FAI,
+ Order.Status.MLD,
+ ]
if order.status in does_not_expire:
return False
@@ -143,7 +203,7 @@ class Logics():
cls.cancel_bond(order.maker_bond)
order.save()
return True
-
+
elif order.status == Order.Status.PUB:
cls.return_bond(order.maker_bond)
order.status = Order.Status.EXP
@@ -156,10 +216,10 @@ class Logics():
return True
elif order.status == Order.Status.WF2:
- '''Weird case where an order expires and both participants
+ """Weird case where an order expires and both participants
did not proceed with the contract. Likely the site was
down or there was a bug. Still bonds must be charged
- to avoid service DDOS. '''
+ to avoid service DDOS."""
cls.settle_bond(order.maker_bond)
cls.settle_bond(order.taker_bond)
@@ -212,21 +272,22 @@ class Logics():
order.trade_escrow = None
cls.publish_order(order)
return True
-
+
elif order.status in [Order.Status.CHA, Order.Status.FSE]:
# Another weird case. The time to confirm 'fiat sent or received' expired. Yet no dispute
- # was opened. Hint: a seller-scammer could persuade a buyer to not click "fiat
+ # was opened. Hint: a seller-scammer could persuade a buyer to not click "fiat
# sent", we assume this is a dispute case by default.
cls.open_dispute(order)
return True
@classmethod
def kick_taker(cls, order):
- ''' The taker did not lock the taker_bond. Now he has to go'''
+ """The taker did not lock the taker_bond. Now he has to go"""
# Add a time out to the taker
if order.taker:
profile = order.taker.profile
- profile.penalty_expiration = timezone.now() + timedelta(seconds=PENALTY_TIMEOUT)
+ profile.penalty_expiration = timezone.now() + timedelta(
+ seconds=PENALTY_TIMEOUT)
profile.save()
# Make order public again
@@ -242,11 +303,12 @@ class Logics():
# Dispute winner will have to submit a new invoice.
if not order.trade_escrow.status == LNPayment.Status.SETLED:
- cls.settle_escrow(order)
-
+ cls.settle_escrow(order)
+
order.is_disputed = True
order.status = Order.Status.DIS
- order.expires_at = timezone.now() + timedelta(seconds=Order.t_to_expire[Order.Status.DIS])
+ order.expires_at = timezone.now() + timedelta(
+ seconds=Order.t_to_expire[Order.Status.DIS])
order.save()
# User could be None if a dispute is open automatically due to weird expiration.
@@ -256,82 +318,101 @@ class Logics():
if profile.orders_disputes_started == None:
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 = list(
+ profile.orders_disputes_started).append(str(order.id))
profile.save()
return True, None
def dispute_statement(order, user, statement):
- ''' Updates the dispute statements'''
+ """Updates the dispute statements"""
if not order.status == Order.Status.DIS:
- return False, {'bad_request':'Only orders in dispute accept a dispute statements'}
+ return False, {
+ "bad_request":
+ "Only orders in dispute accept a dispute statements"
+ }
if len(statement) > 5000:
- return False, {'bad_statement':'The statement is longer than 5000 characters'}
+ return False, {
+ "bad_statement": "The statement is longer than 5000 characters"
+ }
if order.maker == user:
order.maker_statement = statement
else:
order.taker_statement = statement
-
+
# If both statements are in, move status to wait for dispute resolution
if order.maker_statement != None and order.taker_statement != None:
order.status = Order.Status.WFR
- order.expires_at = timezone.now() + timedelta(seconds=Order.t_to_expire[Order.Status.WFR])
+ order.expires_at = timezone.now() + timedelta(
+ seconds=Order.t_to_expire[Order.Status.WFR])
order.save()
return True, None
@classmethod
def payout_amount(cls, order, user):
- ''' Computes buyer invoice amount. Uses order.last_satoshis,
- that is the final trade amount set at Taker Bond time'''
+ """Computes buyer invoice amount. Uses order.last_satoshis,
+ that is the final trade amount set at Taker Bond time"""
if cls.is_buyer(order, user):
- invoice_amount = int(order.last_satoshis * (1-FEE)) # Trading FEE is charged here.
+ invoice_amount = int(order.last_satoshis *
+ (1 - FEE)) # Trading FEE is charged here.
- return True, {'invoice_amount': invoice_amount}
+ return True, {"invoice_amount": invoice_amount}
@classmethod
def update_invoice(cls, order, user, 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.'}
+ return False, {
+ "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:
- return False, {'bad_request':'You cannot submit a invoice while bonds are not locked.'}
+ 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):
+ return False, {
+ "bad_request":
+ "You cannot submit a invoice while bonds are not locked."
+ }
- num_satoshis = cls.payout_amount(order, user)[1]['invoice_amount']
+ num_satoshis = cls.payout_amount(order, user)[1]["invoice_amount"]
payout = LNNode.validate_ln_invoice(invoice, num_satoshis)
- if not payout['valid']:
- return False, payout['context']
+ if not payout["valid"]:
+ return False, payout["context"]
order.payout, _ = LNPayment.objects.update_or_create(
- concept = LNPayment.Concepts.PAYBUYER,
- type = LNPayment.Types.NORM,
- sender = User.objects.get(username=ESCROW_USERNAME),
- order_paid = order, # In case this user has other payouts, update the one related to this order.
- receiver= user,
+ concept=LNPayment.Concepts.PAYBUYER,
+ type=LNPayment.Types.NORM,
+ sender=User.objects.get(username=ESCROW_USERNAME),
+ order_paid=
+ 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={
- 'invoice' : invoice,
- 'status' : LNPayment.Status.VALIDI,
- 'num_satoshis' : num_satoshis,
- 'description' : payout['description'],
- 'payment_hash' : payout['payment_hash'],
- 'created_at' : payout['created_at'],
- 'expires_at' : payout['expires_at']}
- )
+ "invoice": invoice,
+ "status": LNPayment.Status.VALIDI,
+ "num_satoshis": num_satoshis,
+ "description": payout["description"],
+ "payment_hash": payout["payment_hash"],
+ "created_at": payout["created_at"],
+ "expires_at": payout["expires_at"],
+ },
+ )
# If the order status is 'Waiting for invoice'. Move forward to 'chat'
- if order.status == Order.Status.WFI:
+ 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])
+ order.expires_at = timezone.now() + timedelta(
+ seconds=Order.t_to_expire[Order.Status.CHA])
# If the order status is 'Waiting for both'. Move forward to 'waiting for escrow'
if order.status == Order.Status.WF2:
@@ -341,13 +422,15 @@ class Logics():
# If the escrow is locked move to Chat.
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])
+ order.expires_at = timezone.now() + timedelta(
+ seconds=Order.t_to_expire[Order.Status.CHA])
else:
order.status = Order.Status.WFE
-
+
# If the order status is 'Failed Routing'. Retry payment.
if 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
@@ -358,7 +441,7 @@ class Logics():
return True, None
def add_profile_rating(profile, rating):
- ''' adds a new rating to a user profile'''
+ """adds a new rating to a user profile"""
# TODO Unsafe, does not update ratings, it adds more ratings everytime a new rating is clicked.
profile.total_ratings += 1
@@ -371,84 +454,98 @@ class Logics():
latest_ratings = ast.literal_eval(latest_ratings)
latest_ratings.append(rating)
profile.latest_ratings = latest_ratings
- profile.avg_rating = sum(list(map(int, latest_ratings))) / len(latest_ratings) # Just an average, but it is a list of strings. Has to be converted to int.
+ profile.avg_rating = sum(list(map(int, latest_ratings))) / len(
+ latest_ratings
+ ) # Just an average, but it is a list of strings. Has to be converted to int.
profile.save()
def is_penalized(user):
- ''' Checks if a user that is not participant of orders
- has a limit on taking or making a order'''
-
+ """Checks if a user that is not participant of orders
+ has a limit on taking or making a order"""
+
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
-
@classmethod
def cancel_order(cls, order, user, state=None):
# Do not change order status if an is in order
# any of these status
- do_not_cancel = [Order.Status.DEL, Order.Status.UCA,
- Order.Status.EXP, Order.Status.TLD,
- Order.Status.DIS, Order.Status.CCA,
- Order.Status.PAY, Order.Status.SUC,
- Order.Status.FAI, Order.Status.MLD]
+ do_not_cancel = [
+ Order.Status.DEL,
+ Order.Status.UCA,
+ Order.Status.EXP,
+ Order.Status.TLD,
+ Order.Status.DIS,
+ Order.Status.CCA,
+ Order.Status.PAY,
+ Order.Status.SUC,
+ Order.Status.FAI,
+ Order.Status.MLD,
+ ]
if order.status in do_not_cancel:
- return False, {'bad_request':'You cannot cancel this order'}
+ return False, {"bad_request": "You cannot cancel this order"}
# 1) When maker cancels before bond
- '''The order never shows up on the book and order
- status becomes "cancelled" '''
+ """The order never shows up on the book and order
+ status becomes "cancelled" """
if order.status == Order.Status.WFB and order.maker == user:
cls.cancel_bond(order.maker_bond)
order.status = Order.Status.UCA
order.save()
return True, None
- # 2) When maker cancels after bond
- '''The order dissapears from book and goes to cancelled. If strict, maker is charged the bond
+ # 2) When maker cancels after bond
+ """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).'''
+ the bond (more user friendly)."""
elif order.status == Order.Status.PUB and order.maker == user:
- #Settle the maker bond (Maker loses the bond for cancelling public order)
- if cls.return_bond(order.maker_bond): # strict: cls.settle_bond(order.maker_bond):
+ # Settle the maker bond (Maker loses the bond for cancelling public order)
+ if cls.return_bond(order.maker_bond
+ ): # strict: cls.settle_bond(order.maker_bond):
order.status = Order.Status.UCA
order.save()
return True, None
- # 3) When taker cancels before bond
- ''' The order goes back to the book as public.
- LNPayment "order.taker_bond" is deleted() '''
+ # 3) When taker cancels before bond
+ """ The order goes back to the book as public.
+ LNPayment "order.taker_bond" is deleted() """
elif order.status == Order.Status.TAK and order.taker == user:
# adds a timeout penalty
cls.cancel_bond(order.taker_bond)
cls.kick_taker(order)
return True, None
- # 4) When taker or maker cancel after bond (before escrow)
- '''The order goes into cancelled status if maker cancels.
+ # 4) When taker or maker cancel after bond (before escrow)
+ """The order goes into cancelled status if maker cancels.
The order goes into the public book if taker cancels.
- In both cases there is a small fee.'''
+ In both cases there is a small fee."""
- # 4.a) When maker cancel after bond (before escrow)
- '''The order into cancelled status if maker cancels.'''
- elif order.status in [Order.Status.PUB, Order.Status.TAK, Order.Status.WF2, Order.Status.WFE] and order.maker == user:
- #Settle the maker bond (Maker loses the bond for canceling an ongoing trade)
+ # 4.a) When maker cancel after bond (before escrow)
+ """The order into cancelled status if maker cancels."""
+ elif (order.status in [
+ Order.Status.PUB, Order.Status.TAK, Order.Status.WF2,
+ Order.Status.WFE
+ ] and order.maker == user):
+ # Settle the maker bond (Maker loses the bond for canceling an ongoing trade)
valid = cls.settle_bond(order.maker_bond)
- cls.return_bond(order.taker_bond) # returns taker bond
+ cls.return_bond(order.taker_bond) # returns taker bond
if valid:
order.status = Order.Status.UCA
order.save()
return True, None
- # 4.b) When taker 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.taker == user:
+ # 4.b) When taker 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.taker == user):
# Settle the maker bond (Maker loses the bond for canceling an ongoing trade)
valid = cls.settle_bond(order.taker_bond)
if valid:
@@ -456,18 +553,20 @@ class Logics():
cls.publish_order(order)
return True, None
- # 5) When trade collateral has been posted (after escrow)
- '''Always goes to CCA status. Collaboration is needed.
+ # 5) When trade collateral has been posted (after escrow)
+ """Always goes to CCA status. Collaboration is needed.
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, Order.Status.FSE]:
-
+ Must have a small cost for both parties to prevent node DDOS."""
+ elif order.status in [
+ Order.Status.WFI, Order.Status.CHA, Order.Status.FSE
+ ]:
+
# if the maker had asked, and now the taker does: cancel order, return everything
if order.maker_asked_cancel and user == order.taker:
cls.collaborative_cancel(order)
return True, None
-
+
# if the taker had asked, and now the maker does: cancel order, return everything
elif order.taker_asked_cancel and user == order.maker:
cls.collaborative_cancel(order)
@@ -478,15 +577,14 @@ class Logics():
order.taker_asked_cancel = True
order.save()
return True, None
-
+
elif user == order.maker:
order.maker_asked_cancel = True
order.save()
return True, None
-
else:
- return False, {'bad_request':'You cannot cancel this order'}
+ return False, {"bad_request": "You cannot cancel this order"}
@classmethod
def collaborative_cancel(cls, order):
@@ -499,7 +597,8 @@ class Logics():
def publish_order(order):
order.status = Order.Status.PUB
- order.expires_at = order.created_at + timedelta(seconds=Order.t_to_expire[Order.Status.PUB])
+ order.expires_at = order.created_at + timedelta(
+ seconds=Order.t_to_expire[Order.Status.PUB])
order.save()
return
@@ -518,14 +617,20 @@ class Logics():
# Do not gen and cancel if order is older than expiry time
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.'}
+ return False, {
+ "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
if order.maker_bond:
if cls.is_maker_bond_locked(order):
return False, None
elif order.maker_bond.status == LNPayment.Status.INVGEN:
- return True, {'bond_invoice':order.maker_bond.invoice,'bond_satoshis':order.maker_bond.num_satoshis}
+ return True, {
+ "bond_invoice": order.maker_bond.invoice,
+ "bond_satoshis": order.maker_bond.num_satoshis,
+ }
# If there was no maker_bond object yet, generates one
order.last_satoshis = cls.satoshis_now(order)
@@ -535,60 +640,73 @@ class Logics():
# Gen hold Invoice
try:
- hold_payment = LNNode.gen_hold_invoice(bond_satoshis,
- description,
- invoice_expiry=Order.t_to_expire[Order.Status.WFB],
- cltv_expiry_secs=BOND_EXPIRY*3600)
+ hold_payment = LNNode.gen_hold_invoice(
+ bond_satoshis,
+ description,
+ invoice_expiry=Order.t_to_expire[Order.Status.WFB],
+ cltv_expiry_secs=BOND_EXPIRY * 3600,
+ )
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.'}
- if '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."}
-
+ 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."
+ }
+ if "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."
+ }
+
order.maker_bond = LNPayment.objects.create(
- concept = LNPayment.Concepts.MAKEBOND,
- type = LNPayment.Types.HOLD,
- sender = user,
- receiver = User.objects.get(username=ESCROW_USERNAME),
- invoice = hold_payment['invoice'],
- preimage = hold_payment['preimage'],
- status = LNPayment.Status.INVGEN,
- num_satoshis = bond_satoshis,
- description = description,
- payment_hash = hold_payment['payment_hash'],
- created_at = hold_payment['created_at'],
- expires_at = hold_payment['expires_at'],
- cltv_expiry = hold_payment['cltv_expiry'])
+ concept=LNPayment.Concepts.MAKEBOND,
+ type=LNPayment.Types.HOLD,
+ sender=user,
+ receiver=User.objects.get(username=ESCROW_USERNAME),
+ invoice=hold_payment["invoice"],
+ preimage=hold_payment["preimage"],
+ status=LNPayment.Status.INVGEN,
+ num_satoshis=bond_satoshis,
+ description=description,
+ payment_hash=hold_payment["payment_hash"],
+ created_at=hold_payment["created_at"],
+ expires_at=hold_payment["expires_at"],
+ cltv_expiry=hold_payment["cltv_expiry"],
+ )
order.save()
- return True, {'bond_invoice':hold_payment['invoice'], 'bond_satoshis':bond_satoshis}
+ return True, {
+ "bond_invoice": hold_payment["invoice"],
+ "bond_satoshis": bond_satoshis,
+ }
@classmethod
def finalize_contract(cls, order):
- ''' When the taker locks the taker_bond
- the contract is final '''
+ """When the taker locks the taker_bond
+ the contract is final"""
- # THE TRADE AMOUNT IS FINAL WITH THE CONFIRMATION OF THE TAKER BOND!
- # (This is the last update to "last_satoshis", it becomes the escrow amount next)
- order.last_satoshis = cls.satoshis_now(order)
- order.taker_bond.status = LNPayment.Status.LOCKED
- order.taker_bond.save()
+ # THE TRADE AMOUNT IS FINAL WITH THE CONFIRMATION OF THE TAKER BOND!
+ # (This is the last update to "last_satoshis", it becomes the escrow amount next)
+ order.last_satoshis = cls.satoshis_now(order)
+ order.taker_bond.status = LNPayment.Status.LOCKED
+ order.taker_bond.save()
- # Both users profiles are added one more contract // Unsafe can add more than once.
- order.maker.profile.total_contracts += 1
- order.taker.profile.total_contracts += 1
- order.maker.profile.save()
- order.taker.profile.save()
+ # Both users profiles are added one more contract // Unsafe can add more than once.
+ order.maker.profile.total_contracts += 1
+ order.taker.profile.total_contracts += 1
+ order.maker.profile.save()
+ order.taker.profile.save()
- # Log a market tick
- MarketTick.log_a_tick(order)
+ # Log a market tick
+ MarketTick.log_a_tick(order)
- # 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])
- order.status = Order.Status.WF2
- order.save()
- return True
+ # 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])
+ order.status = Order.Status.WF2
+ order.save()
+ return True
@classmethod
def is_taker_bond_locked(cls, order):
@@ -605,61 +723,81 @@ class Logics():
# Do not gen and kick out the taker if order is older than expiry time
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.'}
+ return False, {
+ "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.
if order.taker_bond:
if cls.is_taker_bond_locked(order):
return False, None
elif order.taker_bond.status == LNPayment.Status.INVGEN:
- return True, {'bond_invoice':order.taker_bond.invoice,'bond_satoshis':order.taker_bond.num_satoshis}
+ return True, {
+ "bond_invoice": order.taker_bond.invoice,
+ "bond_satoshis": order.taker_bond.num_satoshis,
+ }
# If there was no taker_bond object yet, generates one
order.last_satoshis = cls.satoshis_now(order)
bond_satoshis = int(order.last_satoshis * BOND_SIZE)
- pos_text = 'Buying' if cls.is_buyer(order, user) else 'Selling'
- 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.")
+ pos_text = "Buying" if cls.is_buyer(order, user) else "Selling"
+ 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."
+ )
# Gen hold Invoice
try:
- hold_payment = LNNode.gen_hold_invoice(bond_satoshis,
- description,
- invoice_expiry=Order.t_to_expire[Order.Status.TAK],
- cltv_expiry_secs=BOND_EXPIRY*3600)
-
- 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.'}
-
- order.taker_bond = LNPayment.objects.create(
- concept = LNPayment.Concepts.TAKEBOND,
- type = LNPayment.Types.HOLD,
- sender = user,
- receiver = User.objects.get(username=ESCROW_USERNAME),
- invoice = hold_payment['invoice'],
- preimage = hold_payment['preimage'],
- status = LNPayment.Status.INVGEN,
- num_satoshis = bond_satoshis,
- description = description,
- payment_hash = hold_payment['payment_hash'],
- created_at = hold_payment['created_at'],
- expires_at = hold_payment['expires_at'],
- cltv_expiry = hold_payment['cltv_expiry'])
+ hold_payment = LNNode.gen_hold_invoice(
+ bond_satoshis,
+ description,
+ invoice_expiry=Order.t_to_expire[Order.Status.TAK],
+ cltv_expiry_secs=BOND_EXPIRY * 3600,
+ )
- order.expires_at = timezone.now() + timedelta(seconds=Order.t_to_expire[Order.Status.TAK])
+ 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."
+ }
+
+ order.taker_bond = LNPayment.objects.create(
+ concept=LNPayment.Concepts.TAKEBOND,
+ type=LNPayment.Types.HOLD,
+ sender=user,
+ receiver=User.objects.get(username=ESCROW_USERNAME),
+ invoice=hold_payment["invoice"],
+ preimage=hold_payment["preimage"],
+ status=LNPayment.Status.INVGEN,
+ num_satoshis=bond_satoshis,
+ description=description,
+ payment_hash=hold_payment["payment_hash"],
+ created_at=hold_payment["created_at"],
+ expires_at=hold_payment["expires_at"],
+ cltv_expiry=hold_payment["cltv_expiry"],
+ )
+
+ order.expires_at = timezone.now() + timedelta(
+ seconds=Order.t_to_expire[Order.Status.TAK])
order.save()
- return True, {'bond_invoice': hold_payment['invoice'], 'bond_satoshis': bond_satoshis}
+ return True, {
+ "bond_invoice": hold_payment["invoice"],
+ "bond_satoshis": bond_satoshis,
+ }
def trade_escrow_received(order):
- ''' Moves the order forward'''
+ """Moves the order forward"""
# If status is 'Waiting for both' move to Waiting for invoice
if order.status == Order.Status.WF2:
order.status = Order.Status.WFI
# If status is 'Waiting for invoice' move to Chat
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])
+ order.expires_at = timezone.now() + timedelta(
+ seconds=Order.t_to_expire[Order.Status.CHA])
order.save()
@classmethod
@@ -677,7 +815,10 @@ class Logics():
# Do not generate if escrow deposit time has expired
if order.expires_at < timezone.now():
cls.order_expires(order)
- return False, {'bad_request':'Invoice expired. You did not send the escrow in time.'}
+ return False, {
+ "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.
if order.trade_escrow:
@@ -685,44 +826,56 @@ class Logics():
if cls.is_trade_escrow_locked(order):
return False, None
elif order.trade_escrow.status == LNPayment.Status.INVGEN:
- return True, {'escrow_invoice':order.trade_escrow.invoice, 'escrow_satoshis':order.trade_escrow.num_satoshis}
+ return True, {
+ "escrow_invoice": order.trade_escrow.invoice,
+ "escrow_satoshis": order.trade_escrow.num_satoshis,
+ }
# If there was no taker_bond object yet, generate one
- escrow_satoshis = order.last_satoshis # Amount was fixed when taker bond was locked
+ escrow_satoshis = (order.last_satoshis
+ ) # Amount was fixed when taker bond was locked
description = f"RoboSats - Escrow amount for '{str(order)}' - It WILL FREEZE IN YOUR WALLET. It will be released to the buyer once you confirm you received the fiat. It will automatically return if buyer does not confirm the payment."
# Gen hold Invoice
try:
- hold_payment = LNNode.gen_hold_invoice(escrow_satoshis,
- description,
- invoice_expiry=Order.t_to_expire[Order.Status.WF2],
- cltv_expiry_secs=ESCROW_EXPIRY*3600)
-
+ hold_payment = LNNode.gen_hold_invoice(
+ escrow_satoshis,
+ description,
+ invoice_expiry=Order.t_to_expire[Order.Status.WF2],
+ cltv_expiry_secs=ESCROW_EXPIRY * 3600,
+ )
+
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.'}
-
+ 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."
+ }
order.trade_escrow = LNPayment.objects.create(
- concept = LNPayment.Concepts.TRESCROW,
- type = LNPayment.Types.HOLD,
- sender = user,
- receiver = User.objects.get(username=ESCROW_USERNAME),
- invoice = hold_payment['invoice'],
- preimage = hold_payment['preimage'],
- status = LNPayment.Status.INVGEN,
- num_satoshis = escrow_satoshis,
- description = description,
- payment_hash = hold_payment['payment_hash'],
- created_at = hold_payment['created_at'],
- expires_at = hold_payment['expires_at'],
- cltv_expiry = hold_payment['cltv_expiry'])
+ concept=LNPayment.Concepts.TRESCROW,
+ type=LNPayment.Types.HOLD,
+ sender=user,
+ receiver=User.objects.get(username=ESCROW_USERNAME),
+ invoice=hold_payment["invoice"],
+ preimage=hold_payment["preimage"],
+ status=LNPayment.Status.INVGEN,
+ num_satoshis=escrow_satoshis,
+ description=description,
+ payment_hash=hold_payment["payment_hash"],
+ created_at=hold_payment["created_at"],
+ expires_at=hold_payment["expires_at"],
+ cltv_expiry=hold_payment["cltv_expiry"],
+ )
order.save()
- return True, {'escrow_invoice':hold_payment['invoice'],'escrow_satoshis': escrow_satoshis}
-
+ return True, {
+ "escrow_invoice": hold_payment["invoice"],
+ "escrow_satoshis": escrow_satoshis,
+ }
+
def settle_escrow(order):
- ''' Settles the trade escrow hold invoice'''
+ """Settles the trade escrow hold invoice"""
# TODO ERROR HANDLING
if LNNode.settle_hold_invoice(order.trade_escrow.preimage):
order.trade_escrow.status = LNPayment.Status.SETLED
@@ -730,7 +883,7 @@ class Logics():
return True
def settle_bond(bond):
- ''' Settles the bond hold invoice'''
+ """Settles the bond hold invoice"""
# TODO ERROR HANDLING
if LNNode.settle_hold_invoice(bond.preimage):
bond.status = LNPayment.Status.SETLED
@@ -738,14 +891,14 @@ class Logics():
return True
def return_escrow(order):
- '''returns the trade escrow'''
+ """returns the trade escrow"""
if LNNode.cancel_return_hold_invoice(order.trade_escrow.payment_hash):
order.trade_escrow.status = LNPayment.Status.RETNED
order.trade_escrow.save()
return True
def cancel_escrow(order):
- '''returns the trade escrow'''
+ """returns the trade escrow"""
# Same as return escrow, but used when the invoice was never LOCKED
if LNNode.cancel_return_hold_invoice(order.trade_escrow.payment_hash):
order.trade_escrow.status = LNPayment.Status.CANCEL
@@ -753,7 +906,7 @@ class Logics():
return True
def return_bond(bond):
- '''returns a bond'''
+ """returns a bond"""
if bond == None:
return
try:
@@ -762,7 +915,7 @@ class Logics():
bond.save()
return True
except Exception as e:
- if 'invoice already settled' in str(e):
+ if "invoice already settled" in str(e):
bond.status = LNPayment.Status.SETLED
bond.save()
return True
@@ -770,7 +923,7 @@ class Logics():
raise e
def cancel_bond(bond):
- '''cancel a bond'''
+ """cancel a bond"""
# Same as return bond, but used when the invoice was never LOCKED
if bond == None:
return True
@@ -780,7 +933,7 @@ class Logics():
bond.save()
return True
except Exception as e:
- if 'invoice already settled' in str(e):
+ if "invoice already settled" in str(e):
bond.status = LNPayment.Status.SETLED
bond.save()
return True
@@ -789,12 +942,14 @@ class Logics():
@classmethod
def confirm_fiat(cls, order, user):
- ''' If Order is in the CHAT states:
+ """If Order is in the CHAT states:
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 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
+ ): # TODO Alternatively, if all collateral is locked? test out
- if order.status == Order.Status.CHA or order.status == Order.Status.FSE: # TODO Alternatively, if all collateral is locked? test out
-
# If buyer, settle escrow and mark fiat sent
if cls.is_buyer(order, user):
order.status = Order.Status.FSE
@@ -803,17 +958,26 @@ class Logics():
# If seller and fiat was sent, SETTLE ESCROW AND PAY BUYER INVOICE
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.'}
-
- # Make sure the trade escrow is at least as big as the buyer invoice
- if order.trade_escrow.num_satoshis <= order.payout.num_satoshis:
- return False, {'bad_request':'Woah, something broke badly. Report in the public channels, or open a Github Issue.'}
+ return False, {
+ "bad_request":
+ "You cannot confirm to have received the fiat before it is confirmed to be sent by the buyer."
+ }
- if cls.settle_escrow(order): ##### !!! KEY LINE - SETTLES THE TRADE ESCROW !!!
+ # Make sure the trade escrow is at least as big as the buyer invoice
+ if order.trade_escrow.num_satoshis <= order.payout.num_satoshis:
+ return False, {
+ "bad_request":
+ "Woah, something broke badly. Report in the public channels, or open a Github Issue."
+ }
+
+ if cls.settle_escrow(
+ order
+ ): ##### !!! KEY LINE - SETTLES THE TRADE ESCROW !!!
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):
+ if LNNode.double_check_htlc_is_settled(
+ order.trade_escrow.payment_hash):
# RETURN THE BONDS // Probably best also do it even if payment failed
cls.return_bond(order.taker_bond)
cls.return_bond(order.maker_bond)
@@ -832,15 +996,24 @@ class Logics():
# # error handling here
# return False, context
else:
- return False, {'bad_request':'You cannot confirm the fiat payment at this stage'}
+ return False, {
+ "bad_request":
+ "You cannot confirm the fiat payment at this stage"
+ }
order.save()
return True, None
@classmethod
def rate_counterparty(cls, order, user, rating):
-
- rating_allowed_status = [Order.Status.PAY, Order.Status.SUC, Order.Status.FAI, Order.Status.MLD, Order.Status.TLD]
+
+ rating_allowed_status = [
+ Order.Status.PAY,
+ Order.Status.SUC,
+ Order.Status.FAI,
+ Order.Status.MLD,
+ Order.Status.TLD,
+ ]
# If the trade is finished
if order.status in rating_allowed_status:
@@ -855,7 +1028,9 @@ 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
diff --git a/api/management/commands/clean_orders.py b/api/management/commands/clean_orders.py
index 9c814b3b..8c19067c 100644
--- a/api/management/commands/clean_orders.py
+++ b/api/management/commands/clean_orders.py
@@ -5,61 +5,72 @@ from api.models import Order
from api.logics import Logics
from django.utils import timezone
+
class Command(BaseCommand):
- help = 'Follows all active hold invoices'
+ help = "Follows all active hold invoices"
# def add_arguments(self, parser):
# parser.add_argument('debug', nargs='+', type=boolean)
def clean_orders(self, *args, **options):
- ''' Continuously checks order expiration times for 1 hour. If order
- has expires, it calls the logics module for expiration handling.'''
+ """Continuously checks order expiration times for 1 hour. If order
+ has expires, it calls the logics module for expiration handling."""
# TODO handle 'database is locked'
-
- do_nothing = [Order.Status.DEL, Order.Status.UCA,
- Order.Status.EXP, Order.Status.FSE,
- Order.Status.DIS, Order.Status.CCA,
- Order.Status.PAY, Order.Status.SUC,
- Order.Status.FAI, Order.Status.MLD,
- Order.Status.TLD, Order.Status.WFR]
+
+ do_nothing = [
+ Order.Status.DEL,
+ Order.Status.UCA,
+ Order.Status.EXP,
+ Order.Status.FSE,
+ Order.Status.DIS,
+ Order.Status.CCA,
+ Order.Status.PAY,
+ Order.Status.SUC,
+ Order.Status.FAI,
+ Order.Status.MLD,
+ Order.Status.TLD,
+ Order.Status.WFR,
+ ]
while True:
time.sleep(5)
queryset = Order.objects.exclude(status__in=do_nothing)
- queryset = queryset.filter(expires_at__lt=timezone.now()) # expires at lower than now
+ queryset = queryset.filter(
+ expires_at__lt=timezone.now()) # expires at lower than now
debug = {}
- debug['num_expired_orders'] = len(queryset)
- debug['expired_orders'] = []
+ debug["num_expired_orders"] = len(queryset)
+ debug["expired_orders"] = []
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
- debug['expired_orders'].append({idx:context})
-
- # It should not happen, but if it cannot locate the hold invoice
+ 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
# it probably was cancelled by another thread, make it expire anyway.
except Exception as e:
- if 'unable to locate invoice' in str(e):
+ if "unable to locate invoice" in str(e):
self.stdout.write(str(e))
order.status = Order.Status.EXP
order.save()
- debug['expired_orders'].append({idx:context})
-
+ debug["expired_orders"].append({idx: context})
- if debug['num_expired_orders'] > 0:
+ if debug["num_expired_orders"] > 0:
self.stdout.write(str(timezone.now()))
self.stdout.write(str(debug))
-
+
def handle(self, *args, **options):
- ''' Never mind database locked error, keep going, print them out'''
+ """Never mind database locked error, keep going, print them out"""
try:
self.clean_orders()
except Exception as e:
- if 'database is locked' in str(e):
- self.stdout.write('database is locked')
-
+ if "database is locked" in str(e):
+ self.stdout.write("database is locked")
+
self.stdout.write(str(e))
diff --git a/api/management/commands/follow_invoices.py b/api/management/commands/follow_invoices.py
index 4f660c1c..302bc745 100644
--- a/api/management/commands/follow_invoices.py
+++ b/api/management/commands/follow_invoices.py
@@ -11,17 +11,18 @@ from decouple import config
from base64 import b64decode
import time
-MACAROON = b64decode(config('LND_MACAROON_BASE64'))
+MACAROON = b64decode(config("LND_MACAROON_BASE64"))
+
class Command(BaseCommand):
- help = 'Follows all active hold invoices'
- rest = 5 # seconds between consecutive checks for invoice updates
+ help = "Follows all active hold invoices"
+ rest = 5 # seconds between consecutive checks for invoice updates
def handle(self, *args, **options):
- ''' Infinite loop to check invoices and retry payments.
- ever mind database locked error, keep going, print out'''
-
+ """Infinite loop to check invoices and retry payments.
+ ever mind database locked error, keep going, print out"""
+
while True:
time.sleep(self.rest)
@@ -35,67 +36,76 @@ class Command(BaseCommand):
self.stdout.write(str(e))
def follow_hold_invoices(self):
- ''' Follows and updates LNpayment objects
+ """Follows and updates LNpayment objects
until settled or canceled
-
+
Background: SubscribeInvoices stub iterator would be great to use here.
However, it only sends updates when the invoice is OPEN (new) or SETTLED.
We are very interested on the other two states (CANCELLED and ACCEPTED).
Therefore, this thread (follow_invoices) will iterate over all LNpayment
- objects and do InvoiceLookupV2 every X seconds to update their state 'live'
- '''
-
+ objects and do InvoiceLookupV2 every X seconds to update their state 'live'
+ """
+
lnd_state_to_lnpayment_status = {
- 0: LNPayment.Status.INVGEN, # OPEN
- 1: LNPayment.Status.SETLED, # SETTLED
- 2: LNPayment.Status.CANCEL, # CANCELLED
- 3: LNPayment.Status.LOCKED # ACCEPTED
- }
+ 0: LNPayment.Status.INVGEN, # OPEN
+ 1: LNPayment.Status.SETLED, # SETTLED
+ 2: LNPayment.Status.CANCEL, # CANCELLED
+ 3: LNPayment.Status.LOCKED, # ACCEPTED
+ }
stub = LNNode.invoicesstub
# time it for debugging
t0 = time.time()
- queryset = LNPayment.objects.filter(type=LNPayment.Types.HOLD, status__in=[LNPayment.Status.INVGEN, LNPayment.Status.LOCKED])
+ queryset = LNPayment.objects.filter(
+ type=LNPayment.Types.HOLD,
+ status__in=[LNPayment.Status.INVGEN, LNPayment.Status.LOCKED],
+ )
debug = {}
- debug['num_active_invoices'] = len(queryset)
- debug['invoices'] = []
+ debug["num_active_invoices"] = len(queryset)
+ debug["invoices"] = []
at_least_one_changed = False
for idx, hold_lnpayment in enumerate(queryset):
old_status = LNPayment.Status(hold_lnpayment.status).label
- try:
+ 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]
+ 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]
# try saving expiry height
- if hasattr(response, 'htlcs' ):
+ 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
except Exception as e:
# If it fails at finding the invoice: it has been canceled.
# In RoboSats DB we make a distinction between cancelled and returned (LND does not)
- if 'unable to locate invoice' in str(e):
+ if "unable to locate invoice" in str(e):
self.stdout.write(str(e))
hold_lnpayment.status = LNPayment.Status.CANCEL
-
+
# LND restarted.
- if 'wallet locked, unlock it' in str(e):
- self.stdout.write(str(timezone.now())+' :: Wallet Locked')
+ if "wallet locked, unlock it" in str(e):
+ self.stdout.write(
+ str(timezone.now()) + " :: Wallet Locked")
# Other write to logs
else:
self.stdout.write(str(e))
-
+
new_status = LNPayment.Status(hold_lnpayment.status).label
# Only save the hold_payments that change (otherwise this function does not scale)
- changed = not old_status==new_status
+ changed = not old_status == new_status
if changed:
# self.handle_status_change(hold_lnpayment, old_status)
self.update_order_status(hold_lnpayment)
@@ -103,39 +113,48 @@ 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
-
- debug['time']=time.time()-t0
+
+ debug["time"] = time.time() - t0
if at_least_one_changed:
self.stdout.write(str(timezone.now()))
self.stdout.write(str(debug))
def send_payments(self):
- '''
+ """
Checks for invoices that are due to pay; i.e., INFLIGHT status and 0 routing_attempts.
Checks if any payment is due for retry, and tries to pay it.
- '''
+ """
- queryset = LNPayment.objects.filter(type=LNPayment.Types.NORM,
- status=LNPayment.Status.FLIGHT,
- routing_attempts=0)
+ queryset = LNPayment.objects.filter(
+ type=LNPayment.Types.NORM,
+ status=LNPayment.Status.FLIGHT,
+ routing_attempts=0,
+ )
+
+ queryset_retries = LNPayment.objects.filter(
+ type=LNPayment.Types.NORM,
+ status__in=[LNPayment.Status.VALIDI, LNPayment.Status.FAILRO],
+ routing_attempts__lt=5,
+ last_routing_time__lt=(
+ timezone.now() - timedelta(minutes=int(config("RETRY_TIME")))),
+ )
- queryset_retries = LNPayment.objects.filter(type=LNPayment.Types.NORM,
- status__in=[LNPayment.Status.VALIDI, LNPayment.Status.FAILRO],
- routing_attempts__lt=5,
- last_routing_time__lt=(timezone.now()-timedelta(minutes=int(config('RETRY_TIME')))))
-
queryset = queryset.union(queryset_retries)
-
+
for lnpayment in queryset:
- success, _ = follow_send_payment(lnpayment) # Do follow_send_payment.delay() for further concurrency.
+ success, _ = follow_send_payment(
+ lnpayment
+ ) # Do follow_send_payment.delay() for further concurrency.
# If failed, reset mision control. (This won't scale well, just a temporary fix)
if not success:
@@ -148,26 +167,26 @@ class Command(BaseCommand):
lnpayment.save()
def update_order_status(self, lnpayment):
- ''' Background process following LND hold invoices
+ """Background process following LND hold invoices
can catch LNpayments changing status. If they do,
- the order status might have to change too.'''
+ the order status might have to change too."""
# If the LNPayment goes to LOCKED (ACCEPTED)
if lnpayment.status == LNPayment.Status.LOCKED:
try:
# It is a maker bond => Publish order.
- if hasattr(lnpayment, 'order_made' ):
+ if hasattr(lnpayment, "order_made"):
Logics.publish_order(lnpayment.order_made)
return
# It is a taker bond => close contract.
- elif hasattr(lnpayment, 'order_taken' ):
+ elif hasattr(lnpayment, "order_taken"):
if lnpayment.order_taken.status == Order.Status.TAK:
Logics.finalize_contract(lnpayment.order_taken)
return
# It is a trade escrow => move foward order status.
- elif hasattr(lnpayment, 'order_escrow' ):
+ elif hasattr(lnpayment, "order_escrow"):
Logics.trade_escrow_received(lnpayment.order_escrow)
return
@@ -177,20 +196,20 @@ class Command(BaseCommand):
# If the LNPayment goes to CANCEL from INVGEN, the invoice had expired
# If it goes to CANCEL from LOCKED the bond was relased. Order had expired in both cases.
# Testing needed for end of time trades!
- if lnpayment.status == LNPayment.Status.CANCEL :
- if hasattr(lnpayment, 'order_made' ):
- Logics.order_expires(lnpayment.order_made)
- return
+ if lnpayment.status == LNPayment.Status.CANCEL:
+ if hasattr(lnpayment, "order_made"):
+ Logics.order_expires(lnpayment.order_made)
+ return
- elif hasattr(lnpayment, 'order_taken' ):
- Logics.order_expires(lnpayment.order_taken)
- return
+ elif hasattr(lnpayment, "order_taken"):
+ Logics.order_expires(lnpayment.order_taken)
+ return
- elif hasattr(lnpayment, 'order_escrow' ):
- Logics.order_expires(lnpayment.order_escrow)
- return
+ elif hasattr(lnpayment, "order_escrow"):
+ Logics.order_expires(lnpayment.order_escrow)
+ return
# TODO If a lnpayment goes from LOCKED to INVGEN. Totally weird
# halt the order
if lnpayment.status == LNPayment.Status.INVGEN:
- pass
\ No newline at end of file
+ pass
diff --git a/api/models.py b/api/models.py
index 13799007..7cb50230 100644
--- a/api/models.py
+++ b/api/models.py
@@ -1,6 +1,10 @@
from django.db import models
from django.contrib.auth.models import User
-from django.core.validators import MaxValueValidator, MinValueValidator, validate_comma_separated_integer_list
+from django.core.validators import (
+ MaxValueValidator,
+ MinValueValidator,
+ validate_comma_separated_integer_list,
+)
from django.db.models.signals import post_save, pre_delete
from django.template.defaultfilters import truncatechars
from django.dispatch import receiver
@@ -12,19 +16,28 @@ from decouple import config
from pathlib import Path
import json
-MIN_TRADE = int(config('MIN_TRADE'))
-MAX_TRADE = int(config('MAX_TRADE'))
-FEE = float(config('FEE'))
-BOND_SIZE = float(config('BOND_SIZE'))
+MIN_TRADE = int(config("MIN_TRADE"))
+MAX_TRADE = int(config("MAX_TRADE"))
+FEE = float(config("FEE"))
+BOND_SIZE = float(config("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_dict = json.load(open("frontend/static/assets/currencies.json"))
+ currency_choices = [(int(val), label)
+ for val, label in list(currency_dict.items())]
- currency = models.PositiveSmallIntegerField(choices=currency_choices, null=False, unique=True)
- exchange_rate = models.DecimalField(max_digits=14, decimal_places=4, default=None, null=True, validators=[MinValueValidator(0)])
+ currency = models.PositiveSmallIntegerField(choices=currency_choices,
+ null=False,
+ unique=True)
+ exchange_rate = models.DecimalField(
+ max_digits=14,
+ decimal_places=4,
+ default=None,
+ null=True,
+ validators=[MinValueValidator(0)],
+ )
timestamp = models.DateTimeField(auto_now_add=True)
def __str__(self):
@@ -32,63 +45,101 @@ class Currency(models.Model):
return self.currency_dict[str(self.currency)]
class Meta:
- verbose_name = 'Cached market currency'
- verbose_name_plural = 'Currencies'
+ verbose_name = "Cached market currency"
+ verbose_name_plural = "Currencies"
+
class LNPayment(models.Model):
class Types(models.IntegerChoices):
- NORM = 0, 'Regular invoice'
- HOLD = 1, 'hold invoice'
+ NORM = 0, "Regular invoice"
+ HOLD = 1, "hold invoice"
class Concepts(models.IntegerChoices):
- MAKEBOND = 0, 'Maker bond'
- TAKEBOND = 1, 'Taker bond'
- TRESCROW = 2, 'Trade escrow'
- PAYBUYER = 3, 'Payment to buyer'
+ MAKEBOND = 0, "Maker bond"
+ TAKEBOND = 1, "Taker bond"
+ TRESCROW = 2, "Trade escrow"
+ PAYBUYER = 3, "Payment to buyer"
class Status(models.IntegerChoices):
- INVGEN = 0, 'Generated'
- LOCKED = 1, 'Locked'
- SETLED = 2, 'Settled'
- RETNED = 3, 'Returned'
- CANCEL = 4, 'Cancelled'
- EXPIRE = 5, 'Expired'
- VALIDI = 6, 'Valid'
- FLIGHT = 7, 'In flight'
- SUCCED = 8, 'Succeeded'
- FAILRO = 9, 'Routing failed'
+ INVGEN = 0, "Generated"
+ LOCKED = 1, "Locked"
+ SETLED = 2, "Settled"
+ RETNED = 3, "Returned"
+ CANCEL = 4, "Cancelled"
+ EXPIRE = 5, "Expired"
+ VALIDI = 6, "Valid"
+ FLIGHT = 7, "In flight"
+ SUCCED = 8, "Succeeded"
+ FAILRO = 9, "Routing failed"
# 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)
-
+ 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)
+
# payment info
- 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(MIN_TRADE*BOND_SIZE), MaxValueValidator(MAX_TRADE*(1+BOND_SIZE+FEE))])
+ 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(MIN_TRADE * BOND_SIZE),
+ MaxValueValidator(MAX_TRADE * (1 + BOND_SIZE + FEE)),
+ ])
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)
# involved parties
- sender = models.ForeignKey(User, related_name='sender', on_delete=models.CASCADE, null=True, default=None)
- receiver = models.ForeignKey(User, related_name='receiver', on_delete=models.CASCADE, null=True, default=None)
+ sender = models.ForeignKey(User,
+ related_name="sender",
+ on_delete=models.CASCADE,
+ null=True,
+ default=None)
+ receiver = models.ForeignKey(User,
+ related_name="receiver",
+ on_delete=models.CASCADE,
+ 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}')
+ return f"LN-{str(self.payment_hash)[:8]}: {self.Concepts(self.concept).label} - {self.Status(self.status).label}"
class Meta:
- verbose_name = 'Lightning payment'
- verbose_name_plural = 'Lightning payments'
+ verbose_name = "Lightning payment"
+ verbose_name_plural = "Lightning payments"
@property
def hash(self):
@@ -97,75 +148,162 @@ class LNPayment(models.Model):
# We created a truncated property for display 'hash'
return truncatechars(self.payment_hash, 10)
+
class Order(models.Model):
-
+
class Types(models.IntegerChoices):
- BUY = 0, 'BUY'
- SELL = 1, 'SELL'
+ BUY = 0, "BUY"
+ SELL = 1, "SELL"
class Status(models.IntegerChoices):
- WFB = 0, 'Waiting for maker bond'
- PUB = 1, 'Public'
- DEL = 2, 'Deleted'
- TAK = 3, 'Waiting for taker bond'
- UCA = 4, 'Cancelled'
- EXP = 5, 'Expired'
- WF2 = 6, 'Waiting for trade collateral and buyer invoice'
- WFE = 7, 'Waiting only for seller trade collateral'
- WFI = 8, 'Waiting only for buyer invoice'
- CHA = 9, 'Sending fiat - In chatroom'
- FSE = 10, 'Fiat sent - In chatroom'
- DIS = 11, 'In dispute'
- CCA = 12, 'Collaboratively cancelled'
- PAY = 13, 'Sending satoshis to buyer'
- SUC = 14, 'Sucessful trade'
- FAI = 15, 'Failed lightning network routing'
- WFR = 16, 'Wait for dispute resolution'
- MLD = 17, 'Maker lost dispute'
- TLD = 18, 'Taker lost dispute'
+ WFB = 0, "Waiting for maker bond"
+ PUB = 1, "Public"
+ DEL = 2, "Deleted"
+ TAK = 3, "Waiting for taker bond"
+ UCA = 4, "Cancelled"
+ EXP = 5, "Expired"
+ WF2 = 6, "Waiting for trade collateral and buyer invoice"
+ WFE = 7, "Waiting only for seller trade collateral"
+ WFI = 8, "Waiting only for buyer invoice"
+ CHA = 9, "Sending fiat - In chatroom"
+ FSE = 10, "Fiat sent - In chatroom"
+ DIS = 11, "In dispute"
+ CCA = 12, "Collaboratively cancelled"
+ PAY = 13, "Sending satoshis to buyer"
+ SUC = 14, "Sucessful trade"
+ FAI = 15, "Failed lightning network routing"
+ WFR = 16, "Wait for dispute resolution"
+ MLD = 17, "Maker lost dispute"
+ TLD = 18, "Taker lost dispute"
# order info
- status = models.PositiveSmallIntegerField(choices=Status.choices, null=False, default=Status.WFB)
+ status = models.PositiveSmallIntegerField(choices=Status.choices,
+ null=False,
+ default=Status.WFB)
created_at = models.DateTimeField(auto_now_add=True)
expires_at = models.DateTimeField()
# order details
type = models.PositiveSmallIntegerField(choices=Types.choices, null=False)
- currency = models.ForeignKey(Currency, null=True, on_delete=models.SET_NULL)
- amount = models.DecimalField(max_digits=16, decimal_places=8, validators=[MinValueValidator(0.00000001)])
- payment_method = models.CharField(max_length=35, null=False, default="not specified", blank=True)
+ currency = models.ForeignKey(Currency,
+ null=True,
+ on_delete=models.SET_NULL)
+ amount = models.DecimalField(max_digits=16,
+ decimal_places=8,
+ validators=[MinValueValidator(0.00000001)])
+ payment_method = models.CharField(max_length=35,
+ null=False,
+ default="not specified",
+ blank=True)
# order pricing method. A explicit amount of sats, or a relative premium above/below market.
is_explicit = models.BooleanField(default=False, null=False)
# marked to market
- premium = models.DecimalField(max_digits=5, decimal_places=2, default=0, null=True, validators=[MinValueValidator(-100), MaxValueValidator(999)], blank=True)
+ premium = models.DecimalField(
+ max_digits=5,
+ decimal_places=2,
+ default=0,
+ null=True,
+ validators=[MinValueValidator(-100),
+ MaxValueValidator(999)],
+ blank=True,
+ )
# explicit
- satoshis = models.PositiveBigIntegerField(null=True, validators=[MinValueValidator(MIN_TRADE), MaxValueValidator(MAX_TRADE)], blank=True)
+ satoshis = models.PositiveBigIntegerField(
+ null=True,
+ validators=[
+ MinValueValidator(MIN_TRADE),
+ MaxValueValidator(MAX_TRADE)
+ ],
+ blank=True,
+ )
# 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)], blank=True) # sats at creation
- last_satoshis = models.PositiveBigIntegerField(null=True, validators=[MinValueValidator(0), MaxValueValidator(MAX_TRADE*2)], blank=True) # sats last time checked. Weird if 2* trade max...
-
+ t0_satoshis = models.PositiveBigIntegerField(
+ null=True,
+ 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)],
+ blank=True,
+ ) # sats last time checked. Weird if 2* trade max...
+
# 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
- taker = models.ForeignKey(User, related_name='taker', on_delete=models.SET_NULL, null=True, default=None, blank=True) # unique = True, a taker can only take one order
- maker_last_seen = models.DateTimeField(null=True,default=None, blank=True)
- taker_last_seen = models.DateTimeField(null=True,default=None, blank=True)
- maker_asked_cancel = models.BooleanField(default=False, null=False) # When collaborative cancel is needed and one partner has cancelled.
- taker_asked_cancel = models.BooleanField(default=False, null=False) # When collaborative cancel is needed and one partner has cancelled.
+ 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
+ taker = models.ForeignKey(
+ User,
+ related_name="taker",
+ on_delete=models.SET_NULL,
+ null=True,
+ default=None,
+ blank=True,
+ ) # unique = True, a taker can only take one order
+ maker_last_seen = models.DateTimeField(null=True, default=None, blank=True)
+ taker_last_seen = models.DateTimeField(null=True, default=None, blank=True)
+ maker_asked_cancel = models.BooleanField(
+ default=False, null=False
+ ) # When collaborative cancel is needed and one partner has cancelled.
+ taker_asked_cancel = models.BooleanField(
+ default=False, null=False
+ ) # When collaborative cancel is needed and one partner has cancelled.
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
- maker_bond = models.OneToOneField(LNPayment, related_name='order_made', on_delete=models.SET_NULL, null=True, default=None, blank=True)
- taker_bond = models.OneToOneField(LNPayment, related_name='order_taken', on_delete=models.SET_NULL, null=True, default=None, blank=True)
- trade_escrow = models.OneToOneField(LNPayment, related_name='order_escrow', on_delete=models.SET_NULL, null=True, default=None, blank=True)
+ maker_bond = models.OneToOneField(
+ LNPayment,
+ related_name="order_made",
+ on_delete=models.SET_NULL,
+ null=True,
+ default=None,
+ blank=True,
+ )
+ taker_bond = models.OneToOneField(
+ LNPayment,
+ related_name="order_taken",
+ on_delete=models.SET_NULL,
+ null=True,
+ default=None,
+ blank=True,
+ )
+ trade_escrow = models.OneToOneField(
+ LNPayment,
+ related_name="order_escrow",
+ on_delete=models.SET_NULL,
+ null=True,
+ default=None,
+ blank=True,
+ )
# buyer payment LN invoice
- payout = models.OneToOneField(LNPayment, related_name='order_paid', on_delete=models.SET_NULL, null=True, default=None, blank=True)
+ payout = models.OneToOneField(
+ LNPayment,
+ related_name="order_paid",
+ on_delete=models.SET_NULL,
+ null=True,
+ default=None,
+ blank=True,
+ )
# ratings
maker_rated = models.BooleanField(default=False, null=False)
@@ -174,34 +312,44 @@ class Order(models.Model):
taker_platform_rated = models.BooleanField(default=False, null=False)
t_to_expire = {
- 0 : int(config('EXP_MAKER_BOND_INVOICE')) , # 'Waiting for maker bond'
- 1 : 60*60*int(config('PUBLIC_ORDER_DURATION')), # 'Public'
- 2 : 0, # 'Deleted'
- 3 : int(config('EXP_TAKER_BOND_INVOICE')), # 'Waiting for taker bond'
- 4 : 0, # 'Cancelled'
- 5 : 0, # 'Expired'
- 6 : 60*int(config('INVOICE_AND_ESCROW_DURATION')), # 'Waiting for trade collateral and buyer invoice'
- 7 : 60*int(config('INVOICE_AND_ESCROW_DURATION')), # 'Waiting only for seller trade collateral'
- 8 : 60*int(config('INVOICE_AND_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 : 24*60*60, # 'Sending satoshis to buyer'
- 14 : 24*60*60, # 'Sucessful trade'
- 15 : 24*60*60, # 'Failed lightning network routing'
- 16 : 10*24*60*60, # 'Wait for dispute resolution'
- 17 : 24*60*60, # 'Maker lost dispute'
- 18 : 24*60*60, # 'Taker lost dispute'
- }
+ 0: int(config("EXP_MAKER_BOND_INVOICE")), # 'Waiting for maker bond'
+ 1: 60 * 60 * int(config("PUBLIC_ORDER_DURATION")), # 'Public'
+ 2: 0, # 'Deleted'
+ 3: int(config("EXP_TAKER_BOND_INVOICE")), # 'Waiting for taker bond'
+ 4: 0, # 'Cancelled'
+ 5: 0, # 'Expired'
+ 6: 60 * int(config("INVOICE_AND_ESCROW_DURATION")
+ ), # 'Waiting for trade collateral and buyer invoice'
+ 7: 60 * int(config("INVOICE_AND_ESCROW_DURATION")
+ ), # 'Waiting only for seller trade collateral'
+ 8: 60 * int(config("INVOICE_AND_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: 24 * 60 * 60, # 'Sending satoshis to buyer'
+ 14: 24 * 60 * 60, # 'Sucessful trade'
+ 15: 24 * 60 * 60, # 'Failed lightning network routing'
+ 16: 10 * 24 * 60 * 60, # 'Wait for dispute resolution'
+ 17: 24 * 60 * 60, # 'Maker lost dispute'
+ 18: 24 * 60 * 60, # 'Taker lost dispute'
+ }
def __str__(self):
- return (f'Order {self.id}: {self.Types(self.type).label} BTC for {float(self.amount)} {self.currency}')
-
+ return f"Order {self.id}: {self.Types(self.type).label} BTC for {float(self.amount)} {self.currency}"
+
@receiver(pre_delete, sender=Order)
def delete_lnpayment_at_order_deletion(sender, instance, **kwargs):
- to_delete = (instance.maker_bond, instance.payout, instance.taker_bond, instance.trade_escrow)
+ to_delete = (
+ instance.maker_bond,
+ instance.payout,
+ instance.taker_bond,
+ instance.trade_escrow,
+ )
for lnpayment in to_delete:
try:
@@ -209,31 +357,60 @@ def delete_lnpayment_at_order_deletion(sender, instance, **kwargs):
except:
pass
+
class Profile(models.Model):
- user = models.OneToOneField(User,on_delete=models.CASCADE)
+ user = models.OneToOneField(User, on_delete=models.CASCADE)
# Total trades
- total_contracts = models.PositiveIntegerField(null=False, default=0)
+ total_contracts = models.PositiveIntegerField(null=False, default=0)
# Ratings stored as a comma separated integer list
- total_ratings = models.PositiveIntegerField(null=False, default=0)
- latest_ratings = models.CharField(max_length=999, null=True, default=None, validators=[validate_comma_separated_integer_list], blank=True) # Will only store latest ratings
- avg_rating = models.DecimalField(max_digits=4, decimal_places=1, default=None, null=True, validators=[MinValueValidator(0), MaxValueValidator(100)], blank=True)
+ total_ratings = models.PositiveIntegerField(null=False, default=0)
+ latest_ratings = models.CharField(
+ max_length=999,
+ null=True,
+ default=None,
+ validators=[validate_comma_separated_integer_list],
+ blank=True,
+ ) # Will only store latest ratings
+ avg_rating = models.DecimalField(
+ max_digits=4,
+ decimal_places=1,
+ default=None,
+ null=True,
+ validators=[MinValueValidator(0),
+ MaxValueValidator(100)],
+ blank=True,
+ )
# Disputes
num_disputes = models.PositiveIntegerField(null=False, default=0)
lost_disputes = models.PositiveIntegerField(null=False, default=0)
num_disputes_started = models.PositiveIntegerField(null=False, default=0)
- orders_disputes_started = models.CharField(max_length=999, null=True, default=None, validators=[validate_comma_separated_integer_list], blank=True) # Will only store ID of orders
+ orders_disputes_started = models.CharField(
+ max_length=999,
+ null=True,
+ default=None,
+ validators=[validate_comma_separated_integer_list],
+ blank=True,
+ ) # Will only store ID of orders
# RoboHash
- avatar = models.ImageField(default=("static/assets/avatars/"+"unknown_avatar.png"), verbose_name='Avatar', blank=True)
+ avatar = models.ImageField(
+ default=("static/assets/avatars/" + "unknown_avatar.png"),
+ verbose_name="Avatar",
+ blank=True,
+ )
# 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)
@receiver(post_save, sender=User)
def create_user_profile(sender, instance, created, **kwargs):
@@ -247,50 +424,82 @@ 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
def __str__(self):
return self.user.username
-
+
# to display avatars in admin panel
def get_avatar(self):
if not self.avatar:
- return settings.STATIC_ROOT + 'unknown_avatar.png'
+ return settings.STATIC_ROOT + "unknown_avatar.png"
return self.avatar.url
# 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):
- '''
- Records tick by tick Non-KYC Bitcoin price.
+ """
+ Records tick by tick Non-KYC Bitcoin price.
Data to be aggregated and offered via public API.
It is checked against current CEX price for useful
insight on the historical premium of Non-KYC BTC
- Price is set when taker bond is locked. Both
- maker and taker are commited with bonds (contract
+ Price is set when taker bond is locked. Both
+ maker and taker are commited with bonds (contract
is finished and cancellation has a cost)
- '''
+ """
+
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
- price = models.DecimalField(max_digits=10, decimal_places=2, default=None, null=True, validators=[MinValueValidator(0)])
- volume = models.DecimalField(max_digits=8, decimal_places=8, default=None, null=True, validators=[MinValueValidator(0)])
- premium = models.DecimalField(max_digits=5, decimal_places=2, default=None, null=True, validators=[MinValueValidator(-100), MaxValueValidator(999)], blank=True)
- currency = models.ForeignKey(Currency, null=True, on_delete=models.SET_NULL)
+ price = models.DecimalField(
+ max_digits=10,
+ decimal_places=2,
+ default=None,
+ null=True,
+ validators=[MinValueValidator(0)],
+ )
+ volume = models.DecimalField(
+ max_digits=8,
+ decimal_places=8,
+ default=None,
+ null=True,
+ validators=[MinValueValidator(0)],
+ )
+ premium = models.DecimalField(
+ max_digits=5,
+ decimal_places=2,
+ default=None,
+ null=True,
+ validators=[MinValueValidator(-100),
+ MaxValueValidator(999)],
+ blank=True,
+ )
+ currency = models.ForeignKey(Currency,
+ null=True,
+ on_delete=models.SET_NULL)
timestamp = models.DateTimeField(auto_now_add=True)
-
+
# Relevant to keep record of the historical fee, so the insight on the premium can be better analyzed
- fee = models.DecimalField(max_digits=4, decimal_places=4, default=FEE, validators=[MinValueValidator(0), MaxValueValidator(1)])
+ fee = models.DecimalField(
+ max_digits=4,
+ decimal_places=4,
+ default=FEE,
+ validators=[MinValueValidator(0),
+ MaxValueValidator(1)],
+ )
def log_a_tick(order):
- '''
+ """
Creates a new tick
- '''
+ """
if not order.taker_bond:
return None
@@ -301,18 +510,16 @@ 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()
def __str__(self):
- return f'Tick: {str(self.id)[:8]}'
+ return f"Tick: {str(self.id)[:8]}"
class Meta:
- verbose_name = 'Market tick'
- verbose_name_plural = 'Market ticks'
-
+ verbose_name = "Market tick"
+ verbose_name_plural = "Market ticks"
diff --git a/api/nick_generator/dicts/en/adjectives.py b/api/nick_generator/dicts/en/adjectives.py
index 4b9e2216..cf9d1757 100755
--- a/api/nick_generator/dicts/en/adjectives.py
+++ b/api/nick_generator/dicts/en/adjectives.py
@@ -4832,4 +4832,4 @@ adjectives = [
"Bodacious",
"Unpersuasive",
"Simplistic",
-]
\ No newline at end of file
+]
diff --git a/api/nick_generator/nick_generator.py b/api/nick_generator/nick_generator.py
index 93371619..395566c6 100755
--- a/api/nick_generator/nick_generator.py
+++ b/api/nick_generator/nick_generator.py
@@ -2,7 +2,6 @@ from .utils import human_format
import hashlib
import time
-
"""
Deterministic nick generator from SHA256 hash.
@@ -14,7 +13,9 @@ is a total of to 450*4800*12500*1000 =
28 Trillion deterministic nicks
"""
+
class NickGenerator:
+
def __init__(
self,
lang="English",
@@ -42,13 +43,11 @@ 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
@@ -78,7 +77,7 @@ class NickGenerator:
pool_size = self.max_num * num_nouns * num_adj * num_adv
# Min-Max scale the hash relative to the pool size
- max_int_hash = 2 ** 256
+ max_int_hash = 2**256
int_hash = int(hash, 16)
nick_id = int((int_hash / max_int_hash) * pool_size)
@@ -148,7 +147,10 @@ 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
@@ -184,7 +186,7 @@ if __name__ == "__main__":
t0 = time.time()
# Hardcoded example text and hashing
- nick_lang = 'English' #Spanish
+ nick_lang = "English" # Spanish
hash = hashlib.sha256(b"No one expected such cool nick!!").hexdigest()
max_length = 22
max_iter = 100000000
@@ -194,16 +196,13 @@ 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")
@@ -217,8 +216,9 @@ 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 fd8954fc..9f2c957f 100755
--- a/api/nick_generator/utils.py
+++ b/api/nick_generator/utils.py
@@ -1,7 +1,10 @@
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])
+ return "%.2f%s" % (number / k**magnitude, units[magnitude])
diff --git a/api/serializers.py b/api/serializers.py
index aa569c26..18b13319 100644
--- a/api/serializers.py
+++ b/api/serializers.py
@@ -1,18 +1,68 @@
from rest_framework import serializers
from .models import Order
+
class ListOrderSerializer(serializers.ModelSerializer):
+
class Meta:
model = Order
- fields = ('id','status','created_at','expires_at','type','currency','amount','payment_method','is_explicit','premium','satoshis','maker','taker')
+ fields = (
+ "id",
+ "status",
+ "created_at",
+ "expires_at",
+ "type",
+ "currency",
+ "amount",
+ "payment_method",
+ "is_explicit",
+ "premium",
+ "satoshis",
+ "maker",
+ "taker",
+ )
+
class MakeOrderSerializer(serializers.ModelSerializer):
+
class Meta:
model = Order
- fields = ('type','currency','amount','payment_method','is_explicit','premium','satoshis')
+ fields = (
+ "type",
+ "currency",
+ "amount",
+ "payment_method",
+ "is_explicit",
+ "premium",
+ "satoshis",
+ )
+
class UpdateOrderSerializer(serializers.Serializer):
- invoice = serializers.CharField(max_length=2000, 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=('take','update_invoice','submit_statement','dispute','cancel','confirm','rate_user','rate_platform'), allow_null=False)
- rating = serializers.ChoiceField(choices=('1','2','3','4','5'), allow_null=True, allow_blank=True, default=None)
\ No newline at end of file
+ invoice = serializers.CharField(max_length=2000,
+ 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=(
+ "take",
+ "update_invoice",
+ "submit_statement",
+ "dispute",
+ "cancel",
+ "confirm",
+ "rate_user",
+ "rate_platform",
+ ),
+ allow_null=False,
+ )
+ rating = serializers.ChoiceField(
+ choices=("1", "2", "3", "4", "5"),
+ allow_null=True,
+ allow_blank=True,
+ default=None,
+ )
diff --git a/api/tasks.py b/api/tasks.py
index c20268d8..038e7190 100644
--- a/api/tasks.py
+++ b/api/tasks.py
@@ -1,10 +1,11 @@
from celery import shared_task
+
@shared_task(name="users_cleansing")
def users_cleansing():
- '''
+ """
Deletes users never used 12 hours after creation
- '''
+ """
from django.contrib.auth.models import User
from django.db.models import Q
from .logics import Logics
@@ -14,8 +15,8 @@ def users_cleansing():
# Users who's last login has not been in the last 6 hours
active_time_range = (timezone.now() - timedelta(hours=6), timezone.now())
queryset = User.objects.filter(~Q(last_login__range=active_time_range))
- queryset = queryset.filter(is_staff=False) # Do not delete staff users
-
+ queryset = queryset.filter(is_staff=False) # Do not delete staff users
+
# And do not have an active trade or any past contract.
deleted_users = []
for user in queryset:
@@ -27,14 +28,15 @@ def users_cleansing():
user.delete()
results = {
- 'num_deleted': len(deleted_users),
- 'deleted_users': deleted_users,
+ "num_deleted": len(deleted_users),
+ "deleted_users": deleted_users,
}
return results
-@shared_task(name='follow_send_payment')
+
+@shared_task(name="follow_send_payment")
def follow_send_payment(lnpayment):
- '''Sends sats to buyer, continuous update'''
+ """Sends sats to buyer, continuous update"""
from decouple import config
from base64 import b64decode
@@ -44,60 +46,77 @@ def follow_send_payment(lnpayment):
from api.lightning.node import LNNode, MACAROON
from api.models import LNPayment, Order
- fee_limit_sat = int(max(lnpayment.num_satoshis * float(config('PROPORTIONAL_ROUTING_FEE_LIMIT')), float(config('MIN_FLAT_ROUTING_FEE_LIMIT')))) # 200 ppm or 10 sats
+ fee_limit_sat = int(
+ max(
+ lnpayment.num_satoshis *
+ float(config("PROPORTIONAL_ROUTING_FEE_LIMIT")),
+ float(config("MIN_FLAT_ROUTING_FEE_LIMIT")),
+ )) # 200 ppm or 10 sats
request = LNNode.routerrpc.SendPaymentRequest(
payment_request=lnpayment.invoice,
fee_limit_sat=fee_limit_sat,
- timeout_seconds=60) # time out payment in 60 seconds
+ timeout_seconds=60,
+ ) # time out payment in 60 seconds
order = lnpayment.order_paid
try:
- for response in LNNode.routerstub.SendPaymentV2(request, metadata=[('macaroon', MACAROON.hex())]):
- if response.status == 0 : # Status 0 'UNKNOWN'
+ for response in LNNode.routerstub.SendPaymentV2(request,
+ metadata=[
+ ("macaroon",
+ MACAROON.hex())
+ ]):
+ if response.status == 0: # Status 0 'UNKNOWN'
# Not sure when this status happens
- pass
+ pass
- if response.status == 1 : # Status 1 'IN_FLIGHT'
- print('IN_FLIGHT')
+ if response.status == 1: # Status 1 'IN_FLIGHT'
+ print("IN_FLIGHT")
lnpayment.status = LNPayment.Status.FLIGHT
lnpayment.save()
order.status = Order.Status.PAY
order.save()
- if response.status == 3 : # Status 3 'FAILED'
- print('FAILED')
+ if response.status == 3: # Status 3 'FAILED'
+ print("FAILED")
lnpayment.status = LNPayment.Status.FAILRO
lnpayment.last_routing_time = timezone.now()
lnpayment.routing_attempts += 1
lnpayment.save()
order.status = Order.Status.FAI
- order.expires_at = timezone.now() + timedelta(seconds=Order.t_to_expire[Order.Status.FAI])
+ order.expires_at = timezone.now() + timedelta(
+ seconds=Order.t_to_expire[Order.Status.FAI])
order.save()
- context = {'routing_failed': LNNode.payment_failure_context[response.failure_reason]}
+ context = {
+ "routing_failed":
+ LNNode.payment_failure_context[response.failure_reason]
+ }
print(context)
return False, context
- if response.status == 2 : # Status 2 'SUCCEEDED'
- print('SUCCEEDED')
+ if response.status == 2: # Status 2 'SUCCEEDED'
+ print("SUCCEEDED")
lnpayment.status = LNPayment.Status.SUCCED
lnpayment.save()
order.status = Order.Status.SUC
- order.expires_at = timezone.now() + timedelta(seconds=Order.t_to_expire[Order.Status.SUC])
+ order.expires_at = timezone.now() + timedelta(
+ seconds=Order.t_to_expire[Order.Status.SUC])
order.save()
return True, None
except Exception as e:
if "invoice expired" in str(e):
- print('INVOICE EXPIRED')
+ print("INVOICE EXPIRED")
lnpayment.status = LNPayment.Status.EXPIRE
lnpayment.last_routing_time = timezone.now()
lnpayment.save()
order.status = Order.Status.FAI
- order.expires_at = timezone.now() + timedelta(seconds=Order.t_to_expire[Order.Status.FAI])
+ order.expires_at = timezone.now() + timedelta(
+ seconds=Order.t_to_expire[Order.Status.FAI])
order.save()
- context = {'routing_failed':'The payout invoice has expired'}
+ context = {"routing_failed": "The payout invoice has expired"}
return False, context
+
@shared_task(name="cache_external_market_prices", ignore_result=True)
def cache_market():
@@ -110,23 +129,26 @@ def cache_market():
exchange_rates = get_exchange_rates(currency_codes)
results = {}
- for i in range(len(Currency.currency_dict.values())): # currecies are indexed starting at 1 (USD)
+ for i in range(len(Currency.currency_dict.values())
+ ): # currecies are indexed starting at 1 (USD)
- rate = exchange_rates[i]
+ rate = exchange_rates[i]
results[i] = {currency_codes[i], rate}
# Do not update if no new rate was found
- if str(rate) == 'nan': continue
+ if str(rate) == "nan":
+ continue
# Create / Update database cached prices
currency_key = list(Currency.currency_dict.keys())[i]
Currency.objects.update_or_create(
- id = int(currency_key),
- currency = int(currency_key),
+ id=int(currency_key),
+ currency=int(currency_key),
# if there is a Cached market prices matching that id, it updates it with defaults below
- defaults = {
- 'exchange_rate': float(rate),
- 'timestamp': timezone.now(),
- })
+ defaults={
+ "exchange_rate": float(rate),
+ "timestamp": timezone.now(),
+ },
+ )
- return results
\ No newline at end of file
+ return results
diff --git a/api/urls.py b/api/urls.py
index 563fb14c..ada3031e 100644
--- a/api/urls.py
+++ b/api/urls.py
@@ -2,10 +2,16 @@ from django.urls import path
from .views import MakerView, OrderView, UserView, BookView, InfoView
urlpatterns = [
- path('make/', MakerView.as_view()),
- path('order/', OrderView.as_view({'get':'get','post':'take_update_confirm_dispute_cancel'})),
- path('user/', UserView.as_view()),
- path('book/', BookView.as_view()),
+ path("make/", MakerView.as_view()),
+ path(
+ "order/",
+ OrderView.as_view({
+ "get": "get",
+ "post": "take_update_confirm_dispute_cancel"
+ }),
+ ),
+ path("user/", UserView.as_view()),
+ path("book/", BookView.as_view()),
# path('robot/') # Profile Info
- path('info/', InfoView.as_view()),
- ]
\ No newline at end of file
+ path("info/", InfoView.as_view()),
+]
diff --git a/api/utils.py b/api/utils.py
index 9173456e..03c8a983 100644
--- a/api/utils.py
+++ b/api/utils.py
@@ -1,4 +1,3 @@
-
import requests, ring, os
from decouple import config
import numpy as np
@@ -7,35 +6,39 @@ from api.models import Order
market_cache = {}
-@ring.dict(market_cache, expire=3) #keeps in cache for 3 seconds
+
+@ring.dict(market_cache, expire=3) # keeps in cache for 3 seconds
def get_exchange_rates(currencies):
- '''
+ """
Params: list of currency codes.
Checks for exchange rates in several public APIs.
Returns the median price list.
- '''
+ """
- 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:
- try: # If one API is unavailable pass
- if 'blockchain.info' in api_url:
+ try: # If one API is unavailable pass
+ if "blockchain.info" in api_url:
blockchain_prices = requests.get(api_url).json()
blockchain_rates = []
for currency in currencies:
- try: # If a currency is missing place a None
- blockchain_rates.append(float(blockchain_prices[currency]['last']))
+ try: # If a currency is missing place a None
+ blockchain_rates.append(
+ float(blockchain_prices[currency]["last"]))
except:
blockchain_rates.append(np.nan)
api_rates.append(blockchain_rates)
- elif 'yadio.io' in api_url:
+ elif "yadio.io" in api_url:
yadio_prices = requests.get(api_url).json()
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)
@@ -43,32 +46,36 @@ def get_exchange_rates(currencies):
pass
if len(api_rates) == 0:
- return None # Wops there is not API available!
+ return None # Wops there is not API available!
exchange_rates = np.array(api_rates)
median_rates = np.nanmedian(exchange_rates, axis=0)
return median_rates.tolist()
+
def get_lnd_version():
# If dockerized, return LND_VERSION envvar used for docker image.
# Otherwise it would require LND's version.grpc libraries...
try:
- lnd_version = config('LND_VERSION')
+ lnd_version = config("LND_VERSION")
return lnd_version
except:
pass
# If not dockerized and LND is local, read from CLI
try:
- stream = os.popen('lnd --version')
+ stream = os.popen("lnd --version")
lnd_version = stream.read()[:-1]
return lnd_version
except:
- return ''
+ return ""
+
robosats_commit_cache = {}
+
+
@ring.dict(robosats_commit_cache, expire=3600)
def get_commit_robosats():
@@ -77,11 +84,15 @@ def get_commit_robosats():
return commit_hash
+
premium_percentile = {}
+
+
@ring.dict(premium_percentile, expire=300)
def compute_premium_percentile(order):
- queryset = Order.objects.filter(currency=order.currency, status=Order.Status.PUB).exclude(id=order.id)
+ queryset = Order.objects.filter(
+ currency=order.currency, status=Order.Status.PUB).exclude(id=order.id)
print(len(queryset))
if len(queryset) <= 1:
@@ -90,8 +101,8 @@ def compute_premium_percentile(order):
order_rate = float(order.last_satoshis) / float(order.amount)
rates = []
for similar_order in queryset:
- rates.append(float(similar_order.last_satoshis) / float(similar_order.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)
-
+ return round(np.sum(rates < order_rate) / len(rates), 2)
diff --git a/api/views.py b/api/views.py
index 083d94c1..3b054dee 100644
--- a/api/views.py
+++ b/api/views.py
@@ -26,37 +26,46 @@ from django.utils import timezone
from django.conf import settings
from decouple import config
-EXP_MAKER_BOND_INVOICE = int(config('EXP_MAKER_BOND_INVOICE'))
-FEE = float(config('FEE'))
-RETRY_TIME = int(config('RETRY_TIME'))
-
+EXP_MAKER_BOND_INVOICE = int(config("EXP_MAKER_BOND_INVOICE"))
+FEE = float(config("FEE"))
+RETRY_TIME = int(config("RETRY_TIME"))
avatar_path = Path(settings.AVATAR_ROOT)
avatar_path.mkdir(parents=True, exist_ok=True)
# Create your views here.
-class MakerView(CreateAPIView):
- serializer_class = MakeOrderSerializer
- def post(self,request):
+class MakerView(CreateAPIView):
+ serializer_class = MakeOrderSerializer
+
+ def post(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'}, status.HTTP_400_BAD_REQUEST)
+ return Response(
+ {
+ "bad_request":
+ "Woops! It seems you do not have a robot avatar"
+ },
+ status.HTTP_400_BAD_REQUEST,
+ )
- if not serializer.is_valid(): return Response(status=status.HTTP_400_BAD_REQUEST)
+ if not serializer.is_valid():
+ return Response(status=status.HTTP_400_BAD_REQUEST)
- type = serializer.data.get('type')
- currency = serializer.data.get('currency')
- amount = serializer.data.get('amount')
- payment_method = serializer.data.get('payment_method')
- premium = serializer.data.get('premium')
- satoshis = serializer.data.get('satoshis')
- is_explicit = serializer.data.get('is_explicit')
+ type = serializer.data.get("type")
+ currency = serializer.data.get("currency")
+ amount = serializer.data.get("amount")
+ payment_method = serializer.data.get("payment_method")
+ premium = serializer.data.get("premium")
+ satoshis = serializer.data.get("satoshis")
+ is_explicit = serializer.data.get("is_explicit")
- valid, context, _ = Logics.validate_already_maker_or_taker(request.user)
- if not valid: return Response(context, status.HTTP_409_CONFLICT)
+ valid, context, _ = Logics.validate_already_maker_or_taker(
+ request.user)
+ if not valid:
+ return Response(context, status.HTTP_409_CONFLICT)
# Creates a new order
order = Order(
@@ -67,67 +76,93 @@ class MakerView(CreateAPIView):
premium=premium,
satoshis=satoshis,
is_explicit=is_explicit,
- expires_at=timezone.now()+timedelta(seconds=EXP_MAKER_BOND_INVOICE), # TODO Move to class method
- maker=request.user)
-
+ expires_at=timezone.now() + timedelta(
+ seconds=EXP_MAKER_BOND_INVOICE), # TODO Move to class method
+ maker=request.user,
+ )
+
# TODO move to Order class method when new instance is created!
order.last_satoshis = order.t0_satoshis = Logics.satoshis_now(order)
valid, context = Logics.validate_order_size(order)
- if not valid: return Response(context, status.HTTP_400_BAD_REQUEST)
-
+ if not valid:
+ 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):
serializer_class = UpdateOrderSerializer
- lookup_url_kwarg = 'order_id'
+ lookup_url_kwarg = "order_id"
def get(self, request, format=None):
- '''
+ """
Full trade pipeline takes place while looking/refreshing the order page.
- '''
+ """
order_id = request.GET.get(self.lookup_url_kwarg)
if not request.user.is_authenticated:
- return Response({'bad_request':'You must have a robot avatar to see the order details'}, status=status.HTTP_400_BAD_REQUEST)
+ return Response(
+ {
+ "bad_request":
+ "You must have a robot avatar to see the order details"
+ },
+ status=status.HTTP_400_BAD_REQUEST,
+ )
if order_id == None:
- return Response({'bad_request':'Order ID parameter not found in request'}, status=status.HTTP_400_BAD_REQUEST)
-
+ return Response(
+ {"bad_request": "Order ID parameter not found in request"},
+ status=status.HTTP_400_BAD_REQUEST,
+ )
+
order = Order.objects.filter(id=order_id)
# 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)
-
+ if len(order) != 1:
+ return Response({"bad_request": "Invalid Order Id"},
+ status.HTTP_404_NOT_FOUND)
+
# This is our order.
order = order[0]
# 2) If order has been cancelled
if order.status == Order.Status.UCA:
- return Response({'bad_request':'This order has been cancelled by the maker'},status.HTTP_400_BAD_REQUEST)
+ return Response(
+ {"bad_request": "This order has been cancelled by the maker"},
+ status.HTTP_400_BAD_REQUEST,
+ )
if order.status == Order.Status.CCA:
- return Response({'bad_request':'This order has been cancelled collaborativelly'},status.HTTP_400_BAD_REQUEST)
+ return Response(
+ {
+ "bad_request":
+ "This order has been cancelled collaborativelly"
+ },
+ status.HTTP_400_BAD_REQUEST,
+ )
data = ListOrderSerializer(order).data
- data['total_secs_exp'] = Order.t_to_expire[order.status]
+ data["total_secs_exp"] = Order.t_to_expire[order.status]
# if user is under a limit (penalty), inform him.
is_penalized, time_out = Logics.is_penalized(request.user)
if is_penalized:
- data['penalty'] = request.user.profile.penalty_expiration
+ data["penalty"] = request.user.profile.penalty_expiration
# Add booleans if user is maker, taker, partipant, buyer or seller
- data['is_maker'] = order.maker == request.user
- data['is_taker'] = order.taker == request.user
- data['is_participant'] = data['is_maker'] or data['is_taker']
-
+ data["is_maker"] = order.maker == request.user
+ data["is_taker"] = order.taker == request.user
+ data["is_participant"] = data["is_maker"] or data["is_taker"]
+
# 3.a) If not a participant and order is not public, forbid.
- if not data['is_participant'] and order.status != Order.Status.PUB:
- return Response({'bad_request':'You are not allowed to see this order'},status.HTTP_403_FORBIDDEN)
-
+ if not data["is_participant"] and order.status != Order.Status.PUB:
+ return Response(
+ {"bad_request": "You are not allowed to see this order"},
+ status.HTTP_403_FORBIDDEN,
+ )
+
# WRITE Update last_seen for maker and taker.
# Note down that the taker/maker was here recently, so counterpart knows if the user is paying attention.
if order.maker == request.user:
@@ -139,93 +174,108 @@ 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 If order is between public and WF2
if order.status >= Order.Status.PUB and order.status < Order.Status.WF2:
- data['price_now'], data['premium_now'] = Logics.price_and_premium_now(order)
+ data["price_now"], data[
+ "premium_now"] = Logics.price_and_premium_now(order)
+
+ # 3. c) If maker and Public, add num robots in book, premium percentile and num similar orders.
+ if data["is_maker"] and order.status == Order.Status.PUB:
+ data["premium_percentile"] = compute_premium_percentile(order)
+ data["num_similar_orders"] = len(
+ Order.objects.filter(currency=order.currency,
+ status=Order.Status.PUB))
- # 3. c) If maker and Public, add num robots in book, premium percentile and num similar orders.
- if data['is_maker'] and order.status == Order.Status.PUB:
- data['premium_percentile'] = compute_premium_percentile(order)
- data['num_similar_orders'] = len(Order.objects.filter(currency=order.currency, status=Order.Status.PUB))
-
# 4) Non participants can view details (but only if PUB)
- elif not data['is_participant'] and order.status != Order.Status.PUB:
- return Response(data, status=status.HTTP_200_OK)
+ elif not data["is_participant"] and order.status != Order.Status.PUB:
+ return Response(data, status=status.HTTP_200_OK)
# For participants add positions, nicks and status as a message and hold invoices status
- data['is_buyer'] = Logics.is_buyer(order,request.user)
- data['is_seller'] = Logics.is_seller(order,request.user)
- data['maker_nick'] = str(order.maker)
- data['taker_nick'] = str(order.taker)
- data['status_message'] = Order.Status(order.status).label
- data['is_fiat_sent'] = order.is_fiat_sent
- data['is_disputed'] = order.is_disputed
- data['ur_nick'] = request.user.username
+ data["is_buyer"] = Logics.is_buyer(order, request.user)
+ data["is_seller"] = Logics.is_seller(order, request.user)
+ data["maker_nick"] = str(order.maker)
+ data["taker_nick"] = str(order.taker)
+ data["status_message"] = Order.Status(order.status).label
+ data["is_fiat_sent"] = order.is_fiat_sent
+ data["is_disputed"] = order.is_disputed
+ data["ur_nick"] = request.user.username
# Add whether hold invoices are LOCKED (ACCEPTED)
# Is there a maker bond? If so, True if locked, False otherwise
if order.maker_bond:
- data['maker_locked'] = order.maker_bond.status == LNPayment.Status.LOCKED
+ data[
+ "maker_locked"] = order.maker_bond.status == LNPayment.Status.LOCKED
else:
- data['maker_locked'] = False
+ data["maker_locked"] = False
# Is there a taker bond? If so, True if locked, False otherwise
if order.taker_bond:
- data['taker_locked'] = order.taker_bond.status == LNPayment.Status.LOCKED
+ data[
+ "taker_locked"] = order.taker_bond.status == LNPayment.Status.LOCKED
else:
- data['taker_locked'] = False
+ data["taker_locked"] = False
# Is there an escrow? If so, True if locked, False otherwise
if order.trade_escrow:
- data['escrow_locked'] = order.trade_escrow.status == LNPayment.Status.LOCKED
+ data[
+ "escrow_locked"] = order.trade_escrow.status == LNPayment.Status.LOCKED
else:
- data['escrow_locked'] = False
+ data["escrow_locked"] = False
# 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'] = order.last_satoshis
+ if data["is_seller"]:
+ data["trade_satoshis"] = order.last_satoshis
# Buyer sees the amount he receives
- elif data['is_buyer']:
- data['trade_satoshis'] = Logics.payout_amount(order, request.user)[1]['invoice_amount']
+ elif data["is_buyer"]:
+ 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']:
+ if order.status == Order.Status.WFB and data["is_maker"]:
valid, context = Logics.gen_maker_hold_invoice(order, request.user)
if valid:
data = {**data, **context}
else:
return Response(context, status.HTTP_400_BAD_REQUEST)
-
+
# 6) If status is 'waiting for taker bond' and user is TAKER, reply with a TAKER hold invoice.
- elif order.status == Order.Status.TAK and data['is_taker']:
+ elif order.status == Order.Status.TAK and data["is_taker"]:
valid, context = Logics.gen_taker_hold_invoice(order, request.user)
if valid:
data = {**data, **context}
else:
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):
+
+ # 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):
# 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):
+ # 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):
# If the two bonds are locked, reply with an AMOUNT so he can send the buyer invoice.
- 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}
@@ -233,148 +283,185 @@ 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['pending_cancel'] = True
- elif (data['is_maker'] and order.maker_asked_cancel) or (data['is_taker'] and order.taker_asked_cancel):
- data['asked_for_cancel'] = True
+ if (data["is_maker"] and order.taker_asked_cancel) or (
+ 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["asked_for_cancel"] = True
else:
- data['asked_for_cancel'] = False
+ data["asked_for_cancel"] = False
# 9) If status is 'DIS' and all HTLCS are in LOCKED
elif order.status == Order.Status.DIS:
# add whether the dispute statement has been received
- if data['is_maker']:
- data['statement_submitted'] = (order.maker_statement != None and order.maker_statement != "")
- elif data['is_taker']:
- data['statement_submitted'] = (order.taker_statement != None and order.maker_statement != "")
+ if data["is_maker"]:
+ data["statement_submitted"] = (order.maker_statement != None
+ and order.maker_statement != "")
+ elif data["is_taker"]:
+ data["statement_submitted"] = (order.taker_statement != None
+ and order.maker_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
- data['retries'] = order.payout.routing_attempts
- data['next_retry_time'] = order.payout.last_routing_time + timedelta(minutes=RETRY_TIME)
+ 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)
if order.payout.status == LNPayment.Status.EXPIRE:
- data['invoice_expired'] = True
+ data["invoice_expired"] = True
# Add invoice amount once again if invoice was expired.
- data['invoice_amount'] = int(order.last_satoshis * (1-FEE))
-
+ data["invoice_amount"] = int(order.last_satoshis * (1 - FEE))
+
return Response(data, status.HTTP_200_OK)
def take_update_confirm_dispute_cancel(self, request, format=None):
- '''
+ """
Here takes place all of the updates to the order object.
That is: take, confim, cancel, dispute, update_invoice or rate.
- '''
+ """
order_id = request.GET.get(self.lookup_url_kwarg)
serializer = UpdateOrderSerializer(data=request.data)
- if not serializer.is_valid(): return Response(status=status.HTTP_400_BAD_REQUEST)
-
+ if not serializer.is_valid():
+ return Response(status=status.HTTP_400_BAD_REQUEST)
+
order = Order.objects.get(id=order_id)
- # action is either 1)'take', 2)'confirm', 3)'cancel', 4)'dispute' , 5)'update_invoice'
+ # action is either 1)'take', 2)'confirm', 3)'cancel', 4)'dispute' , 5)'update_invoice'
# 6)'submit_statement' (in dispute), 7)'rate_user' , 'rate_platform'
- action = serializer.data.get('action')
- invoice = serializer.data.get('invoice')
- statement = serializer.data.get('statement')
- rating = serializer.data.get('rating')
-
+ action = serializer.data.get("action")
+ invoice = serializer.data.get("invoice")
+ statement = serializer.data.get("statement")
+ rating = serializer.data.get("rating")
# 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)
- if not valid: return Response(context, status=status.HTTP_409_CONFLICT)
+ if action == "take":
+ if order.status == Order.Status.PUB:
+ valid, context, _ = Logics.validate_already_maker_or_taker(
+ request.user)
+ if not valid:
+ return Response(context, status=status.HTTP_409_CONFLICT)
valid, context = Logics.take(order, request.user)
- if not valid: return Response(context, status=status.HTTP_403_FORBIDDEN)
+ if not valid:
+ return Response(context, status=status.HTTP_403_FORBIDDEN)
return self.get(request)
- else: Response({'bad_request':'This order is not public anymore.'}, status.HTTP_400_BAD_REQUEST)
+ else:
+ Response(
+ {"bad_request": "This order is not public anymore."},
+ status.HTTP_400_BAD_REQUEST,
+ )
# Any other action is only allowed if the user is a participant
if not (order.maker == request.user or order.taker == request.user):
- return Response({'bad_request':'You are not a participant in this order'}, status.HTTP_403_FORBIDDEN)
+ return Response(
+ {"bad_request": "You are not a participant in this order"},
+ status.HTTP_403_FORBIDDEN,
+ )
# 2) If action is 'update invoice'
- if action == 'update_invoice' and invoice:
- valid, context = Logics.update_invoice(order,request.user,invoice)
- if not valid: return Response(context, status.HTTP_400_BAD_REQUEST)
-
+ if action == "update_invoice" and invoice:
+ valid, context = Logics.update_invoice(order, request.user,
+ invoice)
+ if not valid:
+ return Response(context, status.HTTP_400_BAD_REQUEST)
+
# 3) If action is cancel
- elif action == 'cancel':
- valid, context = Logics.cancel_order(order,request.user)
- if not valid: return Response(context, status.HTTP_400_BAD_REQUEST)
+ elif action == "cancel":
+ valid, context = Logics.cancel_order(order, request.user)
+ if not valid:
+ return Response(context, status.HTTP_400_BAD_REQUEST)
# 4) If action is confirm
- elif action == 'confirm':
- valid, context = Logics.confirm_fiat(order,request.user)
- if not valid: return Response(context, status.HTTP_400_BAD_REQUEST)
+ elif action == "confirm":
+ valid, context = Logics.confirm_fiat(order, request.user)
+ if not valid:
+ return Response(context, status.HTTP_400_BAD_REQUEST)
# 5) If action is dispute
- elif action == 'dispute':
- valid, context = Logics.open_dispute(order,request.user)
- if not valid: return Response(context, status.HTTP_400_BAD_REQUEST)
+ elif action == "dispute":
+ valid, context = Logics.open_dispute(order, request.user)
+ if not valid:
+ return Response(context, status.HTTP_400_BAD_REQUEST)
- elif action == 'submit_statement':
- valid, context = Logics.dispute_statement(order,request.user, statement)
- if not valid: return Response(context, status.HTTP_400_BAD_REQUEST)
+ elif action == "submit_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)
- if not valid: return Response(context, status.HTTP_400_BAD_REQUEST)
+ elif action == "rate_user" and rating:
+ valid, context = Logics.rate_counterparty(order, request.user,
+ rating)
+ if not valid:
+ return Response(context, status.HTTP_400_BAD_REQUEST)
# 6) If action is rate_platform
- elif action == 'rate_platform' and rating:
+ elif action == "rate_platform" and rating:
valid, context = Logics.rate_platform(request.user, rating)
- if not valid: return Response(context, status.HTTP_400_BAD_REQUEST)
+ if not valid:
+ return Response(context, status.HTTP_400_BAD_REQUEST)
# If nothing of the above... something else is going on. Probably not allowed!
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'},
- status.HTTP_501_NOT_IMPLEMENTED)
+ {
+ "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,
+ )
return self.get(request)
+
class UserView(APIView):
- lookup_url_kwarg = 'token'
- NickGen = NickGenerator(
- lang='English',
- use_adv=False,
- use_adj=True,
- use_noun=True,
- max_num=999)
+ lookup_url_kwarg = "token"
+ NickGen = NickGenerator(lang="English",
+ use_adv=False,
+ use_adj=True,
+ use_noun=True,
+ max_num=999)
# Probably should be turned into a post method
- def get(self,request, format=None):
- '''
+ def get(self, request, format=None):
+ """
Get a new user derived from a high entropy token
-
+
- Request has a high-entropy token,
- Generates new nickname and avatar.
- Creates login credentials (new User object)
Response with Avatar and Nickname.
- '''
+ """
# If an existing user opens the main page by mistake, we do not want it to create a new nickname/profile for him
if request.user.is_authenticated:
- context = {'nickname': request.user.username}
- not_participant, _, _ = Logics.validate_already_maker_or_taker(request.user)
+ context = {"nickname": request.user.username}
+ not_participant, _, _ = Logics.validate_already_maker_or_taker(
+ request.user)
# Does not allow this 'mistake' if an active order
if not not_participant:
- 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)
-
+
# Does not allow this 'mistake' if the last login was sometime ago (5 minutes)
# if request.user.last_login < timezone.now() - timedelta(minutes=5):
# context['bad_request'] = f'You are already logged in as {request.user}'
@@ -387,35 +474,40 @@ class UserView(APIView):
shannon_entropy = entropy(counts, base=62)
bits_entropy = log2(len(value)**len(token))
# Payload
- context = {'token_shannon_entropy': shannon_entropy, 'token_bits_entropy': bits_entropy}
+ context = {
+ "token_shannon_entropy": shannon_entropy,
+ "token_bits_entropy": bits_entropy,
+ }
# Deny user gen if entropy below 128 bits or 0.7 shannon heterogeneity
if bits_entropy < 128 or shannon_entropy < 0.7:
- context['bad_request'] = 'The token does not have enough entropy'
+ context["bad_request"] = "The token does not have enough entropy"
return Response(context, status=status.HTTP_400_BAD_REQUEST)
# Hash the token, only 1 iteration.
- hash = hashlib.sha256(str.encode(token)).hexdigest()
+ hash = hashlib.sha256(str.encode(token)).hexdigest()
# Generate nickname deterministically
- nickname = self.NickGen.short_from_SHA256(hash, max_length=18)[0]
- context['nickname'] = nickname
+ nickname = self.NickGen.short_from_SHA256(hash, max_length=18)[0]
+ context["nickname"] = nickname
# Generate avatar
rh = Robohash(hash)
- rh.assemble(roboset='set1', bgset='any')# for backgrounds ON
+ rh.assemble(roboset="set1", bgset="any") # for backgrounds ON
# Does not replace image if existing (avoid re-avatar in case of nick collusion)
- image_path = avatar_path.joinpath(nickname+".png")
+ image_path = avatar_path.joinpath(nickname + ".png")
if not image_path.exists():
with open(image_path, "wb") as f:
rh.img.save(f, format="png")
# 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, is_staff=False)
+ User.objects.create_user(username=nickname,
+ password=token,
+ is_staff=False)
user = authenticate(request, username=nickname, password=token)
- user.profile.avatar = "static/assets/avatars/" + nickname + '.png'
+ user.profile.avatar = "static/assets/avatars/" + nickname + ".png"
login(request, user)
return Response(context, status=status.HTTP_201_CREATED)
@@ -424,17 +516,19 @@ class UserView(APIView):
if user is not None:
login(request, user)
# 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!'
+ if request.user.date_joined < (timezone.now() -
+ timedelta(minutes=3)):
+ context[
+ "found"] = "We found your Robot avatar. Welcome back!"
return Response(context, status=status.HTTP_202_ACCEPTED)
else:
# It is unlikely, but maybe the nickname is taken (1 in 20 Billion change)
- context['found'] = 'Bad luck, this nickname is taken'
- context['bad_request'] = 'Enter a different token'
+ context["found"] = "Bad luck, this nickname is taken"
+ context["bad_request"] = "Enter a different token"
return Response(context, status.HTTP_403_FORBIDDEN)
- def delete(self,request):
- ''' Pressing "give me another" deletes the logged in user '''
+ def delete(self, request):
+ """Pressing "give me another" deletes the logged in user"""
user = request.user
if not user.is_authenticated:
return Response(status.HTTP_403_FORBIDDEN)
@@ -446,62 +540,95 @@ class UserView(APIView):
# Check if it is not a maker or taker!
not_participant, _, _ = Logics.validate_already_maker_or_taker(user)
if not not_participant:
- return Response({'bad_request':'Maybe a mistake? User cannot be deleted while he is part of an order'}, status.HTTP_400_BAD_REQUEST)
+ return Response(
+ {
+ "bad_request":
+ "Maybe a mistake? User cannot be deleted while he is part of an order"
+ },
+ status.HTTP_400_BAD_REQUEST,
+ )
# Check if has already a profile with
if user.profile.total_contracts > 0:
- return Response({'bad_request':'Maybe a mistake? User cannot be deleted as it has completed trades'}, status.HTTP_400_BAD_REQUEST)
+ return Response(
+ {
+ "bad_request":
+ "Maybe a mistake? User cannot be deleted as it has completed trades"
+ },
+ status.HTTP_400_BAD_REQUEST,
+ )
logout(request)
user.delete()
- return Response({'user_deleted':'User deleted permanently'}, status.HTTP_301_MOVED_PERMANENTLY)
+ return Response(
+ {"user_deleted": "User deleted permanently"},
+ status.HTTP_301_MOVED_PERMANENTLY,
+ )
+
class BookView(ListAPIView):
serializer_class = ListOrderSerializer
- queryset = Order.objects.filter(status=Order.Status.PUB)
+ queryset = Order.objects.filter(status=Order.Status.PUB)
+
+ def get(self, request, format=None):
+ currency = request.GET.get("currency")
+ type = request.GET.get("type")
- def get(self,request, format=None):
- currency = request.GET.get('currency')
- type = request.GET.get('type')
-
queryset = Order.objects.filter(status=Order.Status.PUB)
# Currency 0 and type 2 are special cases treated as "ANY". (These are not really possible choices)
if int(currency) == 0 and int(type) != 2:
- queryset = Order.objects.filter(type=type, status=Order.Status.PUB)
+ 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({'not_found':'No orders found, be the first to make one'}, status=status.HTTP_404_NOT_FOUND)
+ if len(queryset) == 0:
+ return Response(
+ {"not_found": "No orders found, be the first to make one"},
+ status=status.HTTP_404_NOT_FOUND,
+ )
book_data = []
for order in queryset:
data = ListOrderSerializer(order).data
- data['maker_nick'] = str(order.maker)
-
+ data["maker_nick"] = str(order.maker)
+
# 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)
-
+
return Response(book_data, status=status.HTTP_200_OK)
+
class InfoView(ListAPIView):
def get(self, request):
context = {}
- context['num_public_buy_orders'] = len(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["num_public_buy_orders"] = len(
+ 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))
+
# 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))
+ context["active_robots_today"] = len(
+ User.objects.filter(last_login__day=today.day))
# Compute average premium and volume of today
queryset = MarketTick.objects.filter(timestamp__day=today.day)
@@ -509,9 +636,9 @@ class InfoView(ListAPIView):
weighted_premiums = []
volumes = []
for tick in queryset:
- weighted_premiums.append(tick.premium*tick.volume)
+ weighted_premiums.append(tick.premium * tick.volume)
volumes.append(tick.volume)
-
+
total_volume = sum(volumes)
# Avg_premium is the weighted average of the premiums by volume
avg_premium = sum(weighted_premiums) / total_volume
@@ -524,28 +651,27 @@ class InfoView(ListAPIView):
volume_settled = []
for tick in queryset:
volume_settled.append(tick.volume)
- lifetime_volume_settled = int(sum(volume_settled)*100000000)
+ lifetime_volume_settled = int(sum(volume_settled) * 100000000)
else:
lifetime_volume_settled = 0
- context['today_avg_nonkyc_btc_premium'] = round(avg_premium,2)
- context['today_total_volume'] = total_volume
- context['lifetime_satoshis_settled'] = lifetime_volume_settled
- context['lnd_version'] = get_lnd_version()
- context['robosats_running_commit_hash'] = get_commit_robosats()
- context['alternative_site'] = config('ALTERNATIVE_SITE')
- context['alternative_name'] = config('ALTERNATIVE_NAME')
- context['node_alias'] = config('NODE_ALIAS')
- context['node_id'] = config('NODE_ID')
- context['network'] = config('NETWORK')
- context['fee'] = FEE
- context['bond_size'] = float(config('BOND_SIZE'))
+ context["today_avg_nonkyc_btc_premium"] = round(avg_premium, 2)
+ context["today_total_volume"] = total_volume
+ context["lifetime_satoshis_settled"] = lifetime_volume_settled
+ context["lnd_version"] = get_lnd_version()
+ context["robosats_running_commit_hash"] = get_commit_robosats()
+ context["alternative_site"] = config("ALTERNATIVE_SITE")
+ context["alternative_name"] = config("ALTERNATIVE_NAME")
+ context["node_alias"] = config("NODE_ALIAS")
+ context["node_id"] = config("NODE_ID")
+ context["network"] = config("NETWORK")
+ context["fee"] = FEE
+ context["bond_size"] = float(config("BOND_SIZE"))
if request.user.is_authenticated:
- context['nickname'] = request.user.username
- has_no_active_order, _, order = Logics.validate_already_maker_or_taker(request.user)
+ context["nickname"] = request.user.username
+ 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
+ context["active_order_id"] = order.id
return Response(context, status.HTTP_200_OK)
-
-
diff --git a/chat/apps.py b/chat/apps.py
index 2fe899ad..5f75238d 100644
--- a/chat/apps.py
+++ b/chat/apps.py
@@ -2,5 +2,5 @@ from django.apps import AppConfig
class ChatConfig(AppConfig):
- default_auto_field = 'django.db.models.BigAutoField'
- name = 'chat'
+ default_auto_field = "django.db.models.BigAutoField"
+ name = "chat"
diff --git a/chat/consumers.py b/chat/consumers.py
index cb3da612..671d1c74 100644
--- a/chat/consumers.py
+++ b/chat/consumers.py
@@ -4,12 +4,12 @@ from api.models import Order
import json
+
class ChatRoomConsumer(AsyncWebsocketConsumer):
-
async def connect(self):
- self.order_id = self.scope['url_route']['kwargs']['order_id']
- self.room_group_name = f'chat_order_{self.order_id}'
+ self.order_id = self.scope["url_route"]["kwargs"]["order_id"]
+ self.room_group_name = f"chat_order_{self.order_id}"
self.user = self.scope["user"]
self.user_nick = str(self.user)
@@ -21,49 +21,45 @@ class ChatRoomConsumer(AsyncWebsocketConsumer):
# if not (Logics.is_buyer(order[0], self.user) or Logics.is_seller(order[0], self.user)):
# print ("Outta this chat")
# return False
-
- 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()
async def disconnect(self, close_code):
- 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)
async def receive(self, text_data):
text_data_json = json.loads(text_data)
- message = text_data_json['message']
- nick = text_data_json['nick']
+ message = text_data_json["message"]
+ nick = text_data_json["nick"]
await self.channel_layer.group_send(
self.room_group_name,
{
- 'type': 'chatroom_message',
- 'message': message,
- 'nick': nick,
- }
+ "type": "chatroom_message",
+ "message": message,
+ "nick": nick,
+ },
)
async def chatroom_message(self, event):
- message = event['message']
- nick = event['nick']
+ message = event["message"]
+ nick = event["nick"]
# Insert a white space in words longer than 22 characters.
# Helps when messages overflow in a single line.
- words = message.split(' ')
- fix_message = ''
+ words = message.split(" ")
+ fix_message = ""
for word in words:
- word = ' '.join(word[i:i+22] for i in range(0, len(word), 22))
- fix_message = fix_message +' '+ word
+ word = " ".join(word[i:i + 22] for i in range(0, len(word), 22))
+ fix_message = fix_message + " " + word
await self.send(text_data=json.dumps({
- 'message': fix_message,
- 'user_nick': nick,
+ "message": fix_message,
+ "user_nick": nick,
}))
- pass
\ No newline at end of file
+ pass
diff --git a/chat/routing.py b/chat/routing.py
index a00dddf7..0b05a8cd 100644
--- a/chat/routing.py
+++ b/chat/routing.py
@@ -2,5 +2,6 @@ from django.urls import re_path
from . import consumers
websocket_urlpatterns = [
- re_path(r'ws/chat/(?P\w+)/$', consumers.ChatRoomConsumer.as_asgi()),
-]
\ No newline at end of file
+ re_path(r"ws/chat/(?P\w+)/$",
+ consumers.ChatRoomConsumer.as_asgi()),
+]
diff --git a/chat/urls.py b/chat/urls.py
index f0107bfe..9206e478 100644
--- a/chat/urls.py
+++ b/chat/urls.py
@@ -5,4 +5,4 @@ from . import views
# urlpatterns = [
# path('', views.index, name='index'),
# path('/', views.room, name='order_chat'),
-# ]
\ No newline at end of file
+# ]
diff --git a/chat/views.py b/chat/views.py
index 1198a822..920bd54f 100644
--- a/chat/views.py
+++ b/chat/views.py
@@ -1,7 +1,6 @@
from django.shortcuts import render
-
# def room(request, order_id):
# return render(request, 'chatroom.html', {
# 'order_id': order_id
-# })
\ No newline at end of file
+# })
diff --git a/frontend/apps.py b/frontend/apps.py
index 04f7b898..c626efa1 100644
--- a/frontend/apps.py
+++ b/frontend/apps.py
@@ -2,5 +2,5 @@ from django.apps import AppConfig
class FrontendConfig(AppConfig):
- default_auto_field = 'django.db.models.BigAutoField'
- name = 'frontend'
+ default_auto_field = "django.db.models.BigAutoField"
+ name = "frontend"
diff --git a/frontend/urls.py b/frontend/urls.py
index 4ea62613..763cf34b 100644
--- a/frontend/urls.py
+++ b/frontend/urls.py
@@ -2,11 +2,11 @@ from django.urls import path
from .views import index
urlpatterns = [
- path('', index),
- path('info/', index),
- path('login/', index),
- path('make/', index),
- path('book/', index),
- path('order/', index),
- path('wait/', index),
-]
\ No newline at end of file
+ path("", index),
+ path("info/", index),
+ path("login/", index),
+ path("make/", index),
+ path("book/", index),
+ path("order/", index),
+ path("wait/", index),
+]
diff --git a/frontend/views.py b/frontend/views.py
index 6b59e3cd..b7e06527 100644
--- a/frontend/views.py
+++ b/frontend/views.py
@@ -1,7 +1,9 @@
from django.shortcuts import render
from decouple import config
+
# Create your views here.
+
def index(request, *args, **kwargs):
- context={'ONION_LOCATION': config('ONION_LOCATION')}
- return render(request, 'frontend/index.html', context=context)
\ No newline at end of file
+ context = {"ONION_LOCATION": config("ONION_LOCATION")}
+ return render(request, "frontend/index.html", context=context)
diff --git a/manage.py b/manage.py
index 047fb189..3f91428b 100755
--- a/manage.py
+++ b/manage.py
@@ -6,7 +6,7 @@ import sys
def main():
"""Run administrative tasks."""
- os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'robosats.settings')
+ os.environ.setdefault("DJANGO_SETTINGS_MODULE", "robosats.settings")
try:
from django.core.management import execute_from_command_line
except ImportError as exc:
@@ -18,5 +18,5 @@ def main():
execute_from_command_line(sys.argv)
-if __name__ == '__main__':
+if __name__ == "__main__":
main()
diff --git a/robosats/__init__.py b/robosats/__init__.py
index d128d39c..ce285f3c 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',)
\ No newline at end of file
+__all__ = ("celery_app", )
diff --git a/robosats/asgi.py b/robosats/asgi.py
index 85ecfc5f..f305266e 100644
--- a/robosats/asgi.py
+++ b/robosats/asgi.py
@@ -11,7 +11,7 @@ import os
import django
from channels.routing import get_default_application
-os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'tabulator.settings')
+os.environ.setdefault("DJANGO_SETTINGS_MODULE", "tabulator.settings")
django.setup()
diff --git a/robosats/celery/__init__.py b/robosats/celery/__init__.py
index 0d1999d2..e3e730ec 100644
--- a/robosats/celery/__init__.py
+++ b/robosats/celery/__init__.py
@@ -7,18 +7,18 @@ from celery.schedules import crontab
from datetime import timedelta
# You can use rabbitmq instead here.
-BASE_REDIS_URL = os.environ.get('REDIS_URL', 'redis://localhost:6379')
+BASE_REDIS_URL = os.environ.get("REDIS_URL", "redis://localhost:6379")
# set the default Django settings module for the 'celery' program.
-os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'robosats.settings')
+os.environ.setdefault("DJANGO_SETTINGS_MODULE", "robosats.settings")
-app = Celery('robosats')
+app = Celery("robosats")
# Using a string here means the worker doesn't have to serialize
# the configuration object to child processes.
# - namespace='CELERY' means all celery-related configuration keys
# should have a `CELERY_` prefix.
-app.config_from_object('django.conf:settings', namespace='CELERY')
+app.config_from_object("django.conf:settings", namespace="CELERY")
# Load task modules from all registered Django app configs.
app.autodiscover_tasks()
@@ -26,19 +26,19 @@ app.autodiscover_tasks()
app.conf.broker_url = BASE_REDIS_URL
# this allows schedule items in the Django admin.
-app.conf.beat_scheduler = 'django_celery_beat.schedulers:DatabaseScheduler'
+app.conf.beat_scheduler = "django_celery_beat.schedulers:DatabaseScheduler"
# Configure the periodic tasks
app.conf.beat_schedule = {
- 'users-cleansing': { # Cleans abandoned users every 6 hours
- 'task': 'users_cleansing',
- 'schedule': timedelta(hours=6),
+ "users-cleansing": { # Cleans abandoned users every 6 hours
+ "task": "users_cleansing",
+ "schedule": timedelta(hours=6),
},
- 'cache-market-prices': { # Cache market prices every minutes for now.
- 'task': 'cache_external_market_prices',
- 'schedule': timedelta(seconds=60),
+ "cache-market-prices": { # Cache market prices every minutes for now.
+ "task": "cache_external_market_prices",
+ "schedule": timedelta(seconds=60),
},
}
-app.conf.timezone = 'UTC'
\ No newline at end of file
+app.conf.timezone = "UTC"
diff --git a/robosats/celery/conf.py b/robosats/celery/conf.py
index 6b1aa603..7bccd42b 100644
--- a/robosats/celery/conf.py
+++ b/robosats/celery/conf.py
@@ -1,2 +1,2 @@
# This sets the django-celery-results backend
-CELERY_RESULT_BACKEND = 'django-db'
\ No newline at end of file
+CELERY_RESULT_BACKEND = "django-db"
diff --git a/robosats/routing.py b/robosats/routing.py
index c842b36c..36d2ca11 100644
--- a/robosats/routing.py
+++ b/robosats/routing.py
@@ -2,12 +2,11 @@ from channels.auth import AuthMiddlewareStack
from channels.routing import ProtocolTypeRouter, URLRouter
import chat.routing
-
application = ProtocolTypeRouter({
- 'websocket': AuthMiddlewareStack(
+ "websocket":
+ AuthMiddlewareStack(
URLRouter(
chat.routing.websocket_urlpatterns,
# TODO add api.routing.websocket_urlpatterns when Order page works with websocket
- )
- ),
-})
\ No newline at end of file
+ )),
+})
diff --git a/robosats/settings.py b/robosats/settings.py
index a80875a9..57ff0eae 100644
--- a/robosats/settings.py
+++ b/robosats/settings.py
@@ -17,133 +17,138 @@ from decouple import config
# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent
-STATIC_URL = '/static/'
+STATIC_URL = "/static/"
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/4.0/howto/deployment/checklist/
# SECURITY WARNING: keep the secret key used in production secret!
-SECRET_KEY = config('SECRET_KEY')
+SECRET_KEY = config("SECRET_KEY")
DEBUG = False
-STATIC_URL = 'static/'
-STATIC_ROOT ='/usr/src/static/'
+STATIC_URL = "static/"
+STATIC_ROOT = "/usr/src/static/"
# SECURITY WARNING: don't run with debug turned on in production!
-if os.environ.get('DEVELOPMENT'):
+if os.environ.get("DEVELOPMENT"):
DEBUG = True
- STATIC_ROOT = 'frontend/static/'
-
-AVATAR_ROOT = STATIC_ROOT + 'assets/avatars/'
+ STATIC_ROOT = "frontend/static/"
-ALLOWED_HOSTS = [config('HOST_NAME'),config('HOST_NAME2'),config('LOCAL_ALIAS'),'127.0.0.1']
+AVATAR_ROOT = STATIC_ROOT + "assets/avatars/"
+
+ALLOWED_HOSTS = [
+ config("HOST_NAME"),
+ config("HOST_NAME2"),
+ config("LOCAL_ALIAS"),
+ "127.0.0.1",
+]
# Application definition
INSTALLED_APPS = [
- 'django.contrib.admin',
- 'django.contrib.auth',
- 'django.contrib.contenttypes',
- 'django.contrib.sessions',
- 'django.contrib.messages',
- 'django.contrib.staticfiles',
- 'rest_framework',
- 'channels',
- 'django_celery_beat',
- 'django_celery_results',
- 'api',
- 'chat',
- 'frontend.apps.FrontendConfig',
+ "django.contrib.admin",
+ "django.contrib.auth",
+ "django.contrib.contenttypes",
+ "django.contrib.sessions",
+ "django.contrib.messages",
+ "django.contrib.staticfiles",
+ "rest_framework",
+ "channels",
+ "django_celery_beat",
+ "django_celery_results",
+ "api",
+ "chat",
+ "frontend.apps.FrontendConfig",
]
from .celery.conf import *
MIDDLEWARE = [
- 'django.middleware.security.SecurityMiddleware',
- 'django.contrib.sessions.middleware.SessionMiddleware',
- 'django.middleware.common.CommonMiddleware',
- 'django.middleware.csrf.CsrfViewMiddleware',
- 'django.contrib.auth.middleware.AuthenticationMiddleware',
- 'django.contrib.messages.middleware.MessageMiddleware',
- 'django.middleware.clickjacking.XFrameOptionsMiddleware',
+ "django.middleware.security.SecurityMiddleware",
+ "django.contrib.sessions.middleware.SessionMiddleware",
+ "django.middleware.common.CommonMiddleware",
+ "django.middleware.csrf.CsrfViewMiddleware",
+ "django.contrib.auth.middleware.AuthenticationMiddleware",
+ "django.contrib.messages.middleware.MessageMiddleware",
+ "django.middleware.clickjacking.XFrameOptionsMiddleware",
]
-ROOT_URLCONF = 'robosats.urls'
+ROOT_URLCONF = "robosats.urls"
TEMPLATES = [
{
- 'BACKEND': 'django.template.backends.django.DjangoTemplates',
- 'DIRS': [],
- 'APP_DIRS': True,
- 'OPTIONS': {
- 'context_processors': [
- 'django.template.context_processors.debug',
- 'django.template.context_processors.request',
- 'django.contrib.auth.context_processors.auth',
- 'django.contrib.messages.context_processors.messages',
+ "BACKEND": "django.template.backends.django.DjangoTemplates",
+ "DIRS": [],
+ "APP_DIRS": True,
+ "OPTIONS": {
+ "context_processors": [
+ "django.template.context_processors.debug",
+ "django.template.context_processors.request",
+ "django.contrib.auth.context_processors.auth",
+ "django.contrib.messages.context_processors.messages",
],
},
},
]
-WSGI_APPLICATION = 'robosats.wsgi.application'
-
+WSGI_APPLICATION = "robosats.wsgi.application"
# Database
# https://docs.djangoproject.com/en/4.0/ref/settings/#databases
-
DATABASES = {
- 'default': {
- 'ENGINE': 'django.db.backends.sqlite3',
- 'NAME': '/usr/src/database/db.sqlite3',
- 'OPTIONS': {
- 'timeout': 20, # in seconds
- }
- }
+ "default": {
+ "ENGINE": "django.db.backends.sqlite3",
+ "NAME": "/usr/src/database/db.sqlite3",
+ "OPTIONS": {
+ "timeout": 20, # in seconds
+ },
}
+}
# Password validation
# https://docs.djangoproject.com/en/4.0/ref/settings/#auth-password-validators
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",
},
]
-
# Internationalization
# https://docs.djangoproject.com/en/4.0/topics/i18n/
-LANGUAGE_CODE = 'en-us'
+LANGUAGE_CODE = "en-us"
-TIME_ZONE = 'UTC'
+TIME_ZONE = "UTC"
USE_I18N = True
USE_TZ = True
-
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/4.0/howto/static-files/
-STATIC_URL = 'static/'
+STATIC_URL = "static/"
ASGI_APPLICATION = "robosats.routing.application"
CHANNEL_LAYERS = {
- 'default': {
- 'BACKEND': 'channels_redis.core.RedisChannelLayer',
- 'CONFIG': {
- "hosts": [config('REDIS_URL')],
+ "default": {
+ "BACKEND": "channels_redis.core.RedisChannelLayer",
+ "CONFIG": {
+ "hosts": [config("REDIS_URL")],
},
},
}
@@ -151,14 +156,14 @@ CHANNEL_LAYERS = {
CACHES = {
"default": {
"BACKEND": "django_redis.cache.RedisCache",
- "LOCATION": config('REDIS_URL'),
+ "LOCATION": config("REDIS_URL"),
"OPTIONS": {
"CLIENT_CLASS": "django_redis.client.DefaultClient"
- }
+ },
}
}
# Default primary key field type
# https://docs.djangoproject.com/en/4.0/ref/settings/#default-auto-field
-DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
+DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
diff --git a/robosats/urls.py b/robosats/urls.py
index 2e44786c..9001ba28 100644
--- a/robosats/urls.py
+++ b/robosats/urls.py
@@ -17,8 +17,8 @@ from django.contrib import admin
from django.urls import path, include
urlpatterns = [
- path('admin/', admin.site.urls),
- path('api/', include('api.urls')),
+ path("admin/", admin.site.urls),
+ path("api/", include("api.urls")),
# path('chat/', include('chat.urls')),
- path('', include('frontend.urls')),
- ]
+ path("", include("frontend.urls")),
+]
diff --git a/robosats/wsgi.py b/robosats/wsgi.py
index b37f599a..bc5c27f3 100644
--- a/robosats/wsgi.py
+++ b/robosats/wsgi.py
@@ -11,6 +11,6 @@ import os
from django.core.wsgi import get_wsgi_application
-os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'robosats.settings')
+os.environ.setdefault("DJANGO_SETTINGS_MODULE", "robosats.settings")
application = get_wsgi_application()