mirror of
https://github.com/RoboSats/robosats.git
synced 2025-01-18 04:01:34 +00:00
Re-format all python code
This commit is contained in:
parent
f8f306101e
commit
fc4ccd5281
124
api/admin.py
124
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']
|
||||
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',)
|
||||
list_display = ("timestamp", "price", "volume", "premium", "currency",
|
||||
"fee")
|
||||
readonly_fields = ("timestamp", "price", "volume", "premium", "currency",
|
||||
"fee")
|
||||
list_filter = ["currency"]
|
||||
ordering = ("-timestamp", )
|
||||
|
@ -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"
|
||||
|
@ -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)
|
||||
@ -45,38 +48,51 @@ class LNNode():
|
||||
|
||||
payment_failure_context = {
|
||||
0: "Payment isn't failed (yet)",
|
||||
1: "There are more routes to try, but the payment timeout was exceeded.",
|
||||
2: "All possible routes were tried and failed permanently. Or were no routes to the destination at all.",
|
||||
1:
|
||||
"There are more routes to try, but the payment timeout was exceeded.",
|
||||
2:
|
||||
"All possible routes were tried and failed permanently. Or were no routes to the destination at all.",
|
||||
3: "A non-recoverable error has occured.",
|
||||
4: "Payment details incorrect (unknown hash, invalid amt or invalid final cltv delta)",
|
||||
5: "Insufficient local balance."}
|
||||
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())])
|
||||
"""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.
|
||||
|
||||
@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.
|
||||
|
||||
@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
|
||||
@ -91,27 +107,37 @@ class LNNode():
|
||||
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.
|
||||
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())])
|
||||
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
|
||||
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
|
||||
@ -119,14 +145,14 @@ class LNNode():
|
||||
# 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')
|
||||
print("STATUS: OPEN")
|
||||
pass
|
||||
if response.state == 1: # SETTLED
|
||||
pass
|
||||
if response.state == 2: # CANCELLED
|
||||
pass
|
||||
if response.state == 3: # ACCEPTED (LOCKED)
|
||||
print('STATUS: ACCEPTED')
|
||||
print("STATUS: ACCEPTED")
|
||||
lnpayment.expiry_height = response.htlcs[0].expiry_height
|
||||
lnpayment.status = LNPayment.Status.LOCKED
|
||||
lnpayment.save()
|
||||
@ -135,63 +161,83 @@ 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 = 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)
|
||||
|
||||
@ -199,21 +245,20 @@ class LNNode():
|
||||
if response.status == 0: # Status 0 'UNKNOWN'
|
||||
pass
|
||||
if response.status == 1: # Status 1 'IN_FLIGHT'
|
||||
return True, 'In flight'
|
||||
return True, "In flight"
|
||||
if response.status == 3: # 4 'FAILED' ??
|
||||
'''0 Payment isn't failed (yet).
|
||||
"""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'
|
||||
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
|
||||
|
535
api/logics.py
535
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
|
||||
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
|
||||
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)
|
||||
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
|
||||
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
|
||||
@ -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)
|
||||
@ -222,11 +282,12 @@ class Logics():
|
||||
|
||||
@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
|
||||
@ -246,7 +307,8 @@ class Logics():
|
||||
|
||||
order.is_disputed = True
|
||||
order.status = Order.Status.DIS
|
||||
order.expires_at = timezone.now() + timedelta(seconds=Order.t_to_expire[Order.Status.DIS])
|
||||
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,19 +318,25 @@ 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
|
||||
@ -278,20 +346,22 @@ class Logics():
|
||||
# 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):
|
||||
@ -299,39 +369,50 @@ class Logics():
|
||||
# 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.
|
||||
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:
|
||||
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,39 +454,48 @@ 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
|
||||
@ -411,19 +503,20 @@ class Logics():
|
||||
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
|
||||
"""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):
|
||||
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() '''
|
||||
""" 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)
|
||||
@ -431,13 +524,16 @@ class Logics():
|
||||
return True, None
|
||||
|
||||
# 4) When taker or maker cancel after bond (before escrow)
|
||||
'''The order goes into cancelled status if maker cancels.
|
||||
"""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:
|
||||
"""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
|
||||
@ -447,8 +543,9 @@ class Logics():
|
||||
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:
|
||||
"""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:
|
||||
@ -457,11 +554,13 @@ class Logics():
|
||||
return True, None
|
||||
|
||||
# 5) When trade collateral has been posted (after escrow)
|
||||
'''Always goes to CCA status. Collaboration is needed.
|
||||
"""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:
|
||||
@ -484,9 +583,8 @@ class Logics():
|
||||
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,39 +640,51 @@ class Logics():
|
||||
|
||||
# Gen hold Invoice
|
||||
try:
|
||||
hold_payment = LNNode.gen_hold_invoice(bond_satoshis,
|
||||
hold_payment = LNNode.gen_hold_invoice(
|
||||
bond_satoshis,
|
||||
description,
|
||||
invoice_expiry=Order.t_to_expire[Order.Status.WFB],
|
||||
cltv_expiry_secs=BOND_EXPIRY*3600)
|
||||
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'],
|
||||
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'])
|
||||
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)
|
||||
@ -585,7 +702,8 @@ class Logics():
|
||||
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.expires_at = timezone.now() + timedelta(
|
||||
seconds=Order.t_to_expire[Order.Status.WF2])
|
||||
order.status = Order.Status.WF2
|
||||
order.save()
|
||||
return True
|
||||
@ -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,
|
||||
hold_payment = LNNode.gen_hold_invoice(
|
||||
bond_satoshis,
|
||||
description,
|
||||
invoice_expiry=Order.t_to_expire[Order.Status.TAK],
|
||||
cltv_expiry_secs=BOND_EXPIRY*3600)
|
||||
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.'}
|
||||
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'],
|
||||
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'])
|
||||
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.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,
|
||||
hold_payment = LNNode.gen_hold_invoice(
|
||||
escrow_satoshis,
|
||||
description,
|
||||
invoice_expiry=Order.t_to_expire[Order.Status.WF2],
|
||||
cltv_expiry_secs=ESCROW_EXPIRY*3600)
|
||||
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'],
|
||||
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'])
|
||||
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,11 +942,13 @@ 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):
|
||||
@ -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.'}
|
||||
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":
|
||||
"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 !!!
|
||||
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,7 +996,10 @@ 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
|
||||
@ -840,7 +1007,13 @@ class Logics():
|
||||
@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
|
||||
|
||||
|
@ -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})
|
||||
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))
|
||||
|
@ -11,16 +11,17 @@ 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'
|
||||
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,7 +36,7 @@ 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.
|
||||
@ -43,51 +44,60 @@ class Command(BaseCommand):
|
||||
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'
|
||||
'''
|
||||
"""
|
||||
|
||||
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
|
||||
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:
|
||||
# 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))
|
||||
@ -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,
|
||||
queryset = LNPayment.objects.filter(
|
||||
type=LNPayment.Types.NORM,
|
||||
status=LNPayment.Status.FLIGHT,
|
||||
routing_attempts=0)
|
||||
routing_attempts=0,
|
||||
)
|
||||
|
||||
queryset_retries = LNPayment.objects.filter(type=LNPayment.Types.NORM,
|
||||
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')))))
|
||||
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
|
||||
|
||||
@ -178,15 +197,15 @@ class Command(BaseCommand):
|
||||
# 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' ):
|
||||
if hasattr(lnpayment, "order_made"):
|
||||
Logics.order_expires(lnpayment.order_made)
|
||||
return
|
||||
|
||||
elif hasattr(lnpayment, 'order_taken' ):
|
||||
elif hasattr(lnpayment, "order_taken"):
|
||||
Logics.order_expires(lnpayment.order_taken)
|
||||
return
|
||||
|
||||
elif hasattr(lnpayment, 'order_escrow' ):
|
||||
elif hasattr(lnpayment, "order_escrow"):
|
||||
Logics.order_expires(lnpayment.order_escrow)
|
||||
return
|
||||
|
||||
|
439
api/models.py
439
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 = 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_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,17 +312,22 @@ 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'
|
||||
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'
|
||||
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'
|
||||
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'
|
||||
@ -196,12 +339,17 @@ class Order(models.Model):
|
||||
}
|
||||
|
||||
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,6 +357,7 @@ def delete_lnpayment_at_order_deletion(sender, instance, **kwargs):
|
||||
except:
|
||||
pass
|
||||
|
||||
|
||||
class Profile(models.Model):
|
||||
user = models.OneToOneField(User, on_delete=models.CASCADE)
|
||||
|
||||
@ -217,23 +366,51 @@ class Profile(models.Model):
|
||||
|
||||
# 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)
|
||||
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,7 +424,8 @@ 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
|
||||
@ -258,15 +436,17 @@ class Profile(models.Model):
|
||||
# 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('<img src="%s" width="50" height="50" />' % self.get_avatar())
|
||||
return mark_safe('<img src="%s" width="50" height="50" />' %
|
||||
self.get_avatar())
|
||||
|
||||
|
||||
class MarketTick(models.Model):
|
||||
'''
|
||||
"""
|
||||
Records tick by tick Non-KYC Bitcoin price.
|
||||
Data to be aggregated and offered via public API.
|
||||
|
||||
@ -276,21 +456,50 @@ class MarketTick(models.Model):
|
||||
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,8 +510,7 @@ 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,
|
||||
tick = MarketTick.objects.create(price=price,
|
||||
volume=volume,
|
||||
premium=premium,
|
||||
currency=order.currency)
|
||||
@ -310,9 +518,8 @@ class MarketTick(models.Model):
|
||||
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"
|
||||
|
@ -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
|
||||
@ -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)
|
||||
|
@ -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])
|
||||
|
@ -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)
|
||||
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,
|
||||
)
|
||||
|
68
api/tasks.py
68
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
|
||||
@ -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())]):
|
||||
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
|
||||
|
||||
if response.status == 1: # Status 1 'IN_FLIGHT'
|
||||
print('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')
|
||||
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')
|
||||
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,13 +129,15 @@ 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]
|
||||
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]
|
||||
@ -125,8 +146,9 @@ def cache_market():
|
||||
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(),
|
||||
})
|
||||
"exchange_rate": float(rate),
|
||||
"timestamp": timezone.now(),
|
||||
},
|
||||
)
|
||||
|
||||
return results
|
16
api/urls.py
16
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()),
|
||||
path("info/", InfoView.as_view()),
|
||||
]
|
39
api/utils.py
39
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
|
||||
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:
|
||||
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']))
|
||||
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)
|
||||
@ -50,25 +53,29 @@ def get_exchange_rates(currencies):
|
||||
|
||||
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)
|
||||
|
||||
|
464
api/views.py
464
api/views.py
@ -26,16 +26,16 @@ 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
|
||||
|
||||
@ -43,20 +43,29 @@ class MakerView(CreateAPIView):
|
||||
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,66 +76,92 @@ 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)
|
||||
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.
|
||||
@ -139,64 +174,74 @@ 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))
|
||||
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:
|
||||
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}
|
||||
@ -204,7 +249,7 @@ class OrderView(viewsets.ViewSet):
|
||||
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}
|
||||
@ -212,20 +257,25 @@ class OrderView(viewsets.ViewSet):
|
||||
return Response(context, status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
# 7 a. ) If seller and status is 'WF2' or 'WFE'
|
||||
elif data['is_seller'] and (order.status == Order.Status.WF2 or order.status == Order.Status.WFE):
|
||||
elif data["is_seller"] and (order.status == Order.Status.WF2
|
||||
or order.status == Order.Status.WFE):
|
||||
# If the two bonds are locked, reply with an ESCROW hold invoice.
|
||||
if order.maker_bond.status == order.taker_bond.status == LNPayment.Status.LOCKED:
|
||||
valid, context = Logics.gen_escrow_hold_invoice(order, request.user)
|
||||
if (order.maker_bond.status == order.taker_bond.status ==
|
||||
LNPayment.Status.LOCKED):
|
||||
valid, context = Logics.gen_escrow_hold_invoice(
|
||||
order, request.user)
|
||||
if valid:
|
||||
data = {**data, **context}
|
||||
else:
|
||||
return Response(context, status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
# 7.b) If user is Buyer and status is 'WF2' or 'WFI'
|
||||
elif data['is_buyer'] and (order.status == Order.Status.WF2 or order.status == Order.Status.WFI):
|
||||
elif data["is_buyer"] and (order.status == Order.Status.WF2
|
||||
or order.status == Order.Status.WFI):
|
||||
|
||||
# If the two bonds are locked, reply with an AMOUNT 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,122 +283,157 @@ 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'
|
||||
# 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 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.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':
|
||||
elif action == "cancel":
|
||||
valid, context = Logics.cancel_order(order, request.user)
|
||||
if not valid: return Response(context, status.HTTP_400_BAD_REQUEST)
|
||||
if not valid:
|
||||
return Response(context, status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
# 4) If action is confirm
|
||||
elif action == 'confirm':
|
||||
elif action == "confirm":
|
||||
valid, context = Logics.confirm_fiat(order, request.user)
|
||||
if not valid: return Response(context, status.HTTP_400_BAD_REQUEST)
|
||||
if not valid:
|
||||
return Response(context, status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
# 5) If action is dispute
|
||||
elif action == 'dispute':
|
||||
elif action == "dispute":
|
||||
valid, context = Logics.open_dispute(order, request.user)
|
||||
if not valid: return Response(context, status.HTTP_400_BAD_REQUEST)
|
||||
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',
|
||||
lookup_url_kwarg = "token"
|
||||
NickGen = NickGenerator(lang="English",
|
||||
use_adv=False,
|
||||
use_adj=True,
|
||||
use_noun=True,
|
||||
@ -356,23 +441,25 @@ class UserView(APIView):
|
||||
|
||||
# Probably should be turned into a post method
|
||||
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)
|
||||
@ -387,11 +474,14 @@ 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.
|
||||
@ -399,11 +489,11 @@ class UserView(APIView):
|
||||
|
||||
# Generate nickname deterministically
|
||||
nickname = self.NickGen.short_from_SHA256(hash, max_length=18)[0]
|
||||
context['nickname'] = nickname
|
||||
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")
|
||||
@ -413,9 +503,11 @@ class UserView(APIView):
|
||||
|
||||
# Create new credentials and login if nickname is new
|
||||
if len(User.objects.filter(username=nickname)) == 0:
|
||||
User.objects.create_user(username=nickname, password=token, 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 '''
|
||||
"""Pressing "give me another" deletes the logged in user"""
|
||||
user = request.user
|
||||
if not user.is_authenticated:
|
||||
return Response(status.HTTP_403_FORBIDDEN)
|
||||
@ -446,22 +540,38 @@ 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)
|
||||
|
||||
def get(self, request, format=None):
|
||||
currency = request.GET.get('currency')
|
||||
type = request.GET.get('type')
|
||||
currency = request.GET.get("currency")
|
||||
type = request.GET.get("type")
|
||||
|
||||
queryset = Order.objects.filter(status=Order.Status.PUB)
|
||||
|
||||
@ -469,39 +579,56 @@ class BookView(ListAPIView):
|
||||
if int(currency) == 0 and int(type) != 2:
|
||||
queryset = Order.objects.filter(type=type, status=Order.Status.PUB)
|
||||
elif int(type) == 2 and int(currency) != 0:
|
||||
queryset = Order.objects.filter(currency=currency, status=Order.Status.PUB)
|
||||
queryset = Order.objects.filter(currency=currency,
|
||||
status=Order.Status.PUB)
|
||||
elif not (int(currency) == 0 and int(type) == 2):
|
||||
queryset = Order.objects.filter(currency=currency, type=type, status=Order.Status.PUB)
|
||||
queryset = Order.objects.filter(currency=currency,
|
||||
type=type,
|
||||
status=Order.Status.PUB)
|
||||
|
||||
if len(queryset) == 0:
|
||||
return Response({'not_found':'No orders found, be the first to make one'}, status=status.HTTP_404_NOT_FOUND)
|
||||
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)
|
||||
@ -528,24 +655,23 @@ class InfoView(ListAPIView):
|
||||
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)
|
||||
|
||||
|
||||
|
@ -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"
|
||||
|
@ -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)
|
||||
|
||||
@ -22,48 +22,44 @@ class ChatRoomConsumer(AsyncWebsocketConsumer):
|
||||
# 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
|
@ -2,5 +2,6 @@ from django.urls import re_path
|
||||
from . import consumers
|
||||
|
||||
websocket_urlpatterns = [
|
||||
re_path(r'ws/chat/(?P<order_id>\w+)/$', consumers.ChatRoomConsumer.as_asgi()),
|
||||
re_path(r"ws/chat/(?P<order_id>\w+)/$",
|
||||
consumers.ChatRoomConsumer.as_asgi()),
|
||||
]
|
@ -1,6 +1,5 @@
|
||||
from django.shortcuts import render
|
||||
|
||||
|
||||
# def room(request, order_id):
|
||||
# return render(request, 'chatroom.html', {
|
||||
# 'order_id': order_id
|
||||
|
@ -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"
|
||||
|
@ -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/<int:orderId>', index),
|
||||
path('wait/', index),
|
||||
path("", index),
|
||||
path("info/", index),
|
||||
path("login/", index),
|
||||
path("make/", index),
|
||||
path("book/", index),
|
||||
path("order/<int:orderId>", index),
|
||||
path("wait/", index),
|
||||
]
|
@ -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)
|
||||
context = {"ONION_LOCATION": config("ONION_LOCATION")}
|
||||
return render(request, "frontend/index.html", context=context)
|
||||
|
@ -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()
|
||||
|
@ -4,4 +4,4 @@ from __future__ import absolute_import, unicode_literals
|
||||
# Django starts so that shared_task will use this app.
|
||||
from .celery import app as celery_app
|
||||
|
||||
__all__ = ('celery_app',)
|
||||
__all__ = ("celery_app", )
|
||||
|
@ -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()
|
||||
|
||||
|
@ -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'
|
||||
app.conf.timezone = "UTC"
|
||||
|
@ -1,2 +1,2 @@
|
||||
# This sets the django-celery-results backend
|
||||
CELERY_RESULT_BACKEND = 'django-db'
|
||||
CELERY_RESULT_BACKEND = "django-db"
|
||||
|
@ -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
|
||||
)
|
||||
),
|
||||
)),
|
||||
})
|
@ -17,88 +17,91 @@ 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/'
|
||||
STATIC_ROOT = "frontend/static/"
|
||||
|
||||
AVATAR_ROOT = STATIC_ROOT + 'assets/avatars/'
|
||||
AVATAR_ROOT = STATIC_ROOT + "assets/avatars/"
|
||||
|
||||
ALLOWED_HOSTS = [config('HOST_NAME'),config('HOST_NAME2'),config('LOCAL_ALIAS'),'127.0.0.1']
|
||||
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
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@ -107,43 +110,45 @@ DATABASES = {
|
||||
|
||||
AUTH_PASSWORD_VALIDATORS = [
|
||||
{
|
||||
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
|
||||
"NAME":
|
||||
"django.contrib.auth.password_validation.UserAttributeSimilarityValidator",
|
||||
},
|
||||
{
|
||||
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
|
||||
"NAME":
|
||||
"django.contrib.auth.password_validation.MinimumLengthValidator",
|
||||
},
|
||||
{
|
||||
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
|
||||
"NAME":
|
||||
"django.contrib.auth.password_validation.CommonPasswordValidator",
|
||||
},
|
||||
{
|
||||
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
|
||||
"NAME":
|
||||
"django.contrib.auth.password_validation.NumericPasswordValidator",
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
# 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"
|
||||
|
@ -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")),
|
||||
]
|
||||
|
@ -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()
|
||||
|
Loading…
Reference in New Issue
Block a user