mirror of
https://github.com/RoboSats/robosats.git
synced 2024-12-24 12:52:41 +03:00
Re-format all python code
This commit is contained in:
parent
f8f306101e
commit
fc4ccd5281
128
api/admin.py
128
api/admin.py
@ -7,55 +7,127 @@ from .models import Order, LNPayment, Profile, MarketTick, Currency
|
||||
admin.site.unregister(Group)
|
||||
admin.site.unregister(User)
|
||||
|
||||
|
||||
class ProfileInline(admin.StackedInline):
|
||||
model = Profile
|
||||
can_delete = False
|
||||
fields = ('avatar_tag',)
|
||||
readonly_fields = ['avatar_tag']
|
||||
|
||||
can_delete = False
|
||||
fields = ("avatar_tag", )
|
||||
readonly_fields = ["avatar_tag"]
|
||||
|
||||
|
||||
# extended users with avatars
|
||||
@admin.register(User)
|
||||
class EUserAdmin(UserAdmin):
|
||||
inlines = [ProfileInline]
|
||||
list_display = ('avatar_tag','id','username','last_login','date_joined','is_staff')
|
||||
list_display_links = ('id','username')
|
||||
ordering = ('-id',)
|
||||
list_display = (
|
||||
"avatar_tag",
|
||||
"id",
|
||||
"username",
|
||||
"last_login",
|
||||
"date_joined",
|
||||
"is_staff",
|
||||
)
|
||||
list_display_links = ("id", "username")
|
||||
ordering = ("-id", )
|
||||
|
||||
def avatar_tag(self, obj):
|
||||
return obj.profile.avatar_tag()
|
||||
|
||||
|
||||
@admin.register(Order)
|
||||
class OrderAdmin(AdminChangeLinksMixin, admin.ModelAdmin):
|
||||
list_display = ('id','type','maker_link','taker_link','status','amount','currency_link','t0_satoshis','is_disputed','is_fiat_sent','created_at','expires_at', 'payout_link','maker_bond_link','taker_bond_link','trade_escrow_link')
|
||||
list_display_links = ('id','type')
|
||||
change_links = ('maker','taker','currency','payout','maker_bond','taker_bond','trade_escrow')
|
||||
list_filter = ('is_disputed','is_fiat_sent','type','currency','status')
|
||||
list_display = (
|
||||
"id",
|
||||
"type",
|
||||
"maker_link",
|
||||
"taker_link",
|
||||
"status",
|
||||
"amount",
|
||||
"currency_link",
|
||||
"t0_satoshis",
|
||||
"is_disputed",
|
||||
"is_fiat_sent",
|
||||
"created_at",
|
||||
"expires_at",
|
||||
"payout_link",
|
||||
"maker_bond_link",
|
||||
"taker_bond_link",
|
||||
"trade_escrow_link",
|
||||
)
|
||||
list_display_links = ("id", "type")
|
||||
change_links = (
|
||||
"maker",
|
||||
"taker",
|
||||
"currency",
|
||||
"payout",
|
||||
"maker_bond",
|
||||
"taker_bond",
|
||||
"trade_escrow",
|
||||
)
|
||||
list_filter = ("is_disputed", "is_fiat_sent", "type", "currency", "status")
|
||||
|
||||
|
||||
@admin.register(LNPayment)
|
||||
class LNPaymentAdmin(AdminChangeLinksMixin, admin.ModelAdmin):
|
||||
list_display = ('hash','concept','status','num_satoshis','type','expires_at','expiry_height','sender_link','receiver_link','order_made_link','order_taken_link','order_escrow_link','order_paid_link')
|
||||
list_display_links = ('hash','concept')
|
||||
change_links = ('sender','receiver','order_made','order_taken','order_escrow','order_paid')
|
||||
list_filter = ('type','concept','status')
|
||||
ordering = ('-expires_at',)
|
||||
list_display = (
|
||||
"hash",
|
||||
"concept",
|
||||
"status",
|
||||
"num_satoshis",
|
||||
"type",
|
||||
"expires_at",
|
||||
"expiry_height",
|
||||
"sender_link",
|
||||
"receiver_link",
|
||||
"order_made_link",
|
||||
"order_taken_link",
|
||||
"order_escrow_link",
|
||||
"order_paid_link",
|
||||
)
|
||||
list_display_links = ("hash", "concept")
|
||||
change_links = (
|
||||
"sender",
|
||||
"receiver",
|
||||
"order_made",
|
||||
"order_taken",
|
||||
"order_escrow",
|
||||
"order_paid",
|
||||
)
|
||||
list_filter = ("type", "concept", "status")
|
||||
ordering = ("-expires_at", )
|
||||
|
||||
|
||||
@admin.register(Profile)
|
||||
class UserProfileAdmin(AdminChangeLinksMixin, admin.ModelAdmin):
|
||||
list_display = ('avatar_tag','id','user_link','total_contracts','platform_rating','total_ratings','avg_rating','num_disputes','lost_disputes')
|
||||
list_display_links = ('avatar_tag','id')
|
||||
change_links =['user']
|
||||
readonly_fields = ['avatar_tag']
|
||||
list_display = (
|
||||
"avatar_tag",
|
||||
"id",
|
||||
"user_link",
|
||||
"total_contracts",
|
||||
"platform_rating",
|
||||
"total_ratings",
|
||||
"avg_rating",
|
||||
"num_disputes",
|
||||
"lost_disputes",
|
||||
)
|
||||
list_display_links = ("avatar_tag", "id")
|
||||
change_links = ["user"]
|
||||
readonly_fields = ["avatar_tag"]
|
||||
|
||||
|
||||
@admin.register(Currency)
|
||||
class CurrencieAdmin(admin.ModelAdmin):
|
||||
list_display = ('id','currency','exchange_rate','timestamp')
|
||||
list_display_links = ('id','currency')
|
||||
readonly_fields = ('currency','exchange_rate','timestamp')
|
||||
ordering = ('id',)
|
||||
list_display = ("id", "currency", "exchange_rate", "timestamp")
|
||||
list_display_links = ("id", "currency")
|
||||
readonly_fields = ("currency", "exchange_rate", "timestamp")
|
||||
ordering = ("id", )
|
||||
|
||||
|
||||
@admin.register(MarketTick)
|
||||
class MarketTickAdmin(admin.ModelAdmin):
|
||||
list_display = ('timestamp','price','volume','premium','currency','fee')
|
||||
readonly_fields = ('timestamp','price','volume','premium','currency','fee')
|
||||
list_filter = ['currency']
|
||||
ordering = ('-timestamp',)
|
||||
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)
|
||||
@ -44,89 +47,112 @@ class LNNode():
|
||||
routerrpc = routerrpc
|
||||
|
||||
payment_failure_context = {
|
||||
0: "Payment isn't failed (yet)",
|
||||
1: "There are more routes to try, but the payment timeout was exceeded.",
|
||||
2: "All possible routes were tried and failed permanently. Or were no routes to the destination at all.",
|
||||
3: "A non-recoverable error has occured.",
|
||||
4: "Payment details incorrect (unknown hash, invalid amt or invalid final cltv delta)",
|
||||
5: "Insufficient local balance."}
|
||||
0: "Payment isn't failed (yet)",
|
||||
1:
|
||||
"There are more routes to try, but the payment timeout was exceeded.",
|
||||
2:
|
||||
"All possible routes were tried and failed permanently. Or were no routes to the destination at all.",
|
||||
3: "A non-recoverable error has occured.",
|
||||
4:
|
||||
"Payment details incorrect (unknown hash, invalid amt or invalid final cltv delta)",
|
||||
5: "Insufficient local balance.",
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def decode_payreq(cls, invoice):
|
||||
'''Decodes a lightning payment request (invoice)'''
|
||||
"""Decodes a lightning payment request (invoice)"""
|
||||
request = lnrpc.PayReqString(pay_req=invoice)
|
||||
response = cls.lightningstub.DecodePayReq(request, metadata=[('macaroon', MACAROON.hex())])
|
||||
response = cls.lightningstub.DecodePayReq(request,
|
||||
metadata=[("macaroon",
|
||||
MACAROON.hex())])
|
||||
return response
|
||||
|
||||
@classmethod
|
||||
def cancel_return_hold_invoice(cls, payment_hash):
|
||||
'''Cancels or returns a hold invoice'''
|
||||
request = invoicesrpc.CancelInvoiceMsg(payment_hash=bytes.fromhex(payment_hash))
|
||||
response = cls.invoicesstub.CancelInvoice(request, metadata=[('macaroon', MACAROON.hex())])
|
||||
def cancel_return_hold_invoice(cls, payment_hash):
|
||||
"""Cancels or returns a hold invoice"""
|
||||
request = invoicesrpc.CancelInvoiceMsg(
|
||||
payment_hash=bytes.fromhex(payment_hash))
|
||||
response = cls.invoicesstub.CancelInvoice(request,
|
||||
metadata=[("macaroon",
|
||||
MACAROON.hex())])
|
||||
# Fix this: tricky because canceling sucessfully an invoice has no response. TODO
|
||||
return str(response) == "" # True if no response, false otherwise.
|
||||
return str(response) == "" # True if no response, false otherwise.
|
||||
|
||||
@classmethod
|
||||
def settle_hold_invoice(cls, preimage):
|
||||
'''settles a hold invoice'''
|
||||
request = invoicesrpc.SettleInvoiceMsg(preimage=bytes.fromhex(preimage))
|
||||
response = cls.invoicesstub.SettleInvoice(request, metadata=[('macaroon', MACAROON.hex())])
|
||||
"""settles a hold invoice"""
|
||||
request = invoicesrpc.SettleInvoiceMsg(
|
||||
preimage=bytes.fromhex(preimage))
|
||||
response = cls.invoicesstub.SettleInvoice(request,
|
||||
metadata=[("macaroon",
|
||||
MACAROON.hex())])
|
||||
# Fix this: tricky because settling sucessfully an invoice has None response. TODO
|
||||
return str(response)=="" # True if no response, false otherwise.
|
||||
return str(response) == "" # True if no response, false otherwise.
|
||||
|
||||
@classmethod
|
||||
def gen_hold_invoice(cls, num_satoshis, description, invoice_expiry, cltv_expiry_secs):
|
||||
'''Generates hold invoice'''
|
||||
def gen_hold_invoice(cls, num_satoshis, description, invoice_expiry,
|
||||
cltv_expiry_secs):
|
||||
"""Generates hold invoice"""
|
||||
|
||||
hold_payment = {}
|
||||
# The preimage is a random hash of 256 bits entropy
|
||||
preimage = hashlib.sha256(secrets.token_bytes(nbytes=32)).digest()
|
||||
preimage = hashlib.sha256(secrets.token_bytes(nbytes=32)).digest()
|
||||
|
||||
# Its hash is used to generate the hold invoice
|
||||
r_hash = hashlib.sha256(preimage).digest()
|
||||
|
||||
# timelock expiry for the last hop, computed based on a 10 minutes block with 30% padding (~7 min block)
|
||||
cltv_expiry_blocks = int(cltv_expiry_secs / (7*60))
|
||||
request = invoicesrpc.AddHoldInvoiceRequest(
|
||||
memo=description,
|
||||
value=num_satoshis,
|
||||
hash=r_hash,
|
||||
expiry=int(invoice_expiry*1.5), # actual expiry is padded by 50%, if tight, wrong client system clock will say invoice is expired.
|
||||
cltv_expiry=cltv_expiry_blocks,
|
||||
)
|
||||
response = cls.invoicesstub.AddHoldInvoice(request, metadata=[('macaroon', MACAROON.hex())])
|
||||
|
||||
hold_payment['invoice'] = response.payment_request
|
||||
payreq_decoded = cls.decode_payreq(hold_payment['invoice'])
|
||||
hold_payment['preimage'] = preimage.hex()
|
||||
hold_payment['payment_hash'] = payreq_decoded.payment_hash
|
||||
hold_payment['created_at'] = timezone.make_aware(datetime.fromtimestamp(payreq_decoded.timestamp))
|
||||
hold_payment['expires_at'] = hold_payment['created_at'] + timedelta(seconds=payreq_decoded.expiry)
|
||||
hold_payment['cltv_expiry'] = cltv_expiry_blocks
|
||||
# timelock expiry for the last hop, computed based on a 10 minutes block with 30% padding (~7 min block)
|
||||
cltv_expiry_blocks = int(cltv_expiry_secs / (7 * 60))
|
||||
request = invoicesrpc.AddHoldInvoiceRequest(
|
||||
memo=description,
|
||||
value=num_satoshis,
|
||||
hash=r_hash,
|
||||
expiry=int(
|
||||
invoice_expiry * 1.5
|
||||
), # actual expiry is padded by 50%, if tight, wrong client system clock will say invoice is expired.
|
||||
cltv_expiry=cltv_expiry_blocks,
|
||||
)
|
||||
response = cls.invoicesstub.AddHoldInvoice(request,
|
||||
metadata=[("macaroon",
|
||||
MACAROON.hex())])
|
||||
|
||||
hold_payment["invoice"] = response.payment_request
|
||||
payreq_decoded = cls.decode_payreq(hold_payment["invoice"])
|
||||
hold_payment["preimage"] = preimage.hex()
|
||||
hold_payment["payment_hash"] = payreq_decoded.payment_hash
|
||||
hold_payment["created_at"] = timezone.make_aware(
|
||||
datetime.fromtimestamp(payreq_decoded.timestamp))
|
||||
hold_payment["expires_at"] = hold_payment["created_at"] + timedelta(
|
||||
seconds=payreq_decoded.expiry)
|
||||
hold_payment["cltv_expiry"] = cltv_expiry_blocks
|
||||
|
||||
return hold_payment
|
||||
|
||||
@classmethod
|
||||
def validate_hold_invoice_locked(cls, lnpayment):
|
||||
'''Checks if hold invoice is locked'''
|
||||
request = invoicesrpc.LookupInvoiceMsg(payment_hash=bytes.fromhex(lnpayment.payment_hash))
|
||||
response = cls.invoicesstub.LookupInvoiceV2(request, metadata=[('macaroon', MACAROON.hex())])
|
||||
print('status here')
|
||||
"""Checks if hold invoice is locked"""
|
||||
request = invoicesrpc.LookupInvoiceMsg(
|
||||
payment_hash=bytes.fromhex(lnpayment.payment_hash))
|
||||
response = cls.invoicesstub.LookupInvoiceV2(request,
|
||||
metadata=[("macaroon",
|
||||
MACAROON.hex())
|
||||
])
|
||||
print("status here")
|
||||
print(response.state)
|
||||
|
||||
# TODO ERROR HANDLING
|
||||
# Will fail if 'unable to locate invoice'. Happens if invoice expiry
|
||||
# Will fail if 'unable to locate invoice'. Happens if invoice expiry
|
||||
# time has passed (but these are 15% padded at the moment). Should catch it
|
||||
# and report back that the invoice has expired (better robustness)
|
||||
if response.state == 0: # OPEN
|
||||
print('STATUS: OPEN')
|
||||
if response.state == 0: # OPEN
|
||||
print("STATUS: OPEN")
|
||||
pass
|
||||
if response.state == 1: # SETTLED
|
||||
if response.state == 1: # SETTLED
|
||||
pass
|
||||
if response.state == 2: # CANCELLED
|
||||
if response.state == 2: # CANCELLED
|
||||
pass
|
||||
if response.state == 3: # ACCEPTED (LOCKED)
|
||||
print('STATUS: ACCEPTED')
|
||||
if response.state == 3: # ACCEPTED (LOCKED)
|
||||
print("STATUS: ACCEPTED")
|
||||
lnpayment.expiry_height = response.htlcs[0].expiry_height
|
||||
lnpayment.status = LNPayment.Status.LOCKED
|
||||
lnpayment.save()
|
||||
@ -135,85 +161,104 @@ class LNNode():
|
||||
@classmethod
|
||||
def resetmc(cls):
|
||||
request = routerrpc.ResetMissionControlRequest()
|
||||
response = cls.routerstub.ResetMissionControl(request, metadata=[('macaroon', MACAROON.hex())])
|
||||
response = cls.routerstub.ResetMissionControl(request,
|
||||
metadata=[
|
||||
("macaroon",
|
||||
MACAROON.hex())
|
||||
])
|
||||
return True
|
||||
|
||||
|
||||
@classmethod
|
||||
def validate_ln_invoice(cls, invoice, num_satoshis):
|
||||
'''Checks if the submited LN invoice comforms to expectations'''
|
||||
"""Checks if the submited LN invoice comforms to expectations"""
|
||||
|
||||
payout = {
|
||||
'valid': False,
|
||||
'context': None,
|
||||
'description': None,
|
||||
'payment_hash': None,
|
||||
'created_at': None,
|
||||
'expires_at': None,
|
||||
}
|
||||
"valid": False,
|
||||
"context": None,
|
||||
"description": None,
|
||||
"payment_hash": None,
|
||||
"created_at": None,
|
||||
"expires_at": None,
|
||||
}
|
||||
|
||||
try:
|
||||
payreq_decoded = cls.decode_payreq(invoice)
|
||||
print(payreq_decoded)
|
||||
except:
|
||||
payout['context'] = {'bad_invoice':'Does not look like a valid lightning invoice'}
|
||||
payout["context"] = {
|
||||
"bad_invoice": "Does not look like a valid lightning invoice"
|
||||
}
|
||||
return payout
|
||||
|
||||
if payreq_decoded.num_satoshis == 0:
|
||||
payout['context'] = {'bad_invoice':'The invoice provided has no explicit amount'}
|
||||
payout["context"] = {
|
||||
"bad_invoice": "The invoice provided has no explicit amount"
|
||||
}
|
||||
return payout
|
||||
|
||||
if not payreq_decoded.num_satoshis == num_satoshis:
|
||||
payout['context'] = {'bad_invoice':'The invoice provided is not for '+'{:,}'.format(num_satoshis)+ ' Sats'}
|
||||
payout["context"] = {
|
||||
"bad_invoice":
|
||||
"The invoice provided is not for " +
|
||||
"{:,}".format(num_satoshis) + " Sats"
|
||||
}
|
||||
return payout
|
||||
|
||||
payout['created_at'] = timezone.make_aware(datetime.fromtimestamp(payreq_decoded.timestamp))
|
||||
payout['expires_at'] = payout['created_at'] + timedelta(seconds=payreq_decoded.expiry)
|
||||
payout["created_at"] = timezone.make_aware(
|
||||
datetime.fromtimestamp(payreq_decoded.timestamp))
|
||||
payout["expires_at"] = payout["created_at"] + timedelta(
|
||||
seconds=payreq_decoded.expiry)
|
||||
|
||||
if payout['expires_at'] < timezone.now():
|
||||
payout['context'] = {'bad_invoice':f'The invoice provided has already expired'}
|
||||
if payout["expires_at"] < timezone.now():
|
||||
payout["context"] = {
|
||||
"bad_invoice": f"The invoice provided has already expired"
|
||||
}
|
||||
return payout
|
||||
|
||||
payout['valid'] = True
|
||||
payout['description'] = payreq_decoded.description
|
||||
payout['payment_hash'] = payreq_decoded.payment_hash
|
||||
payout["valid"] = True
|
||||
payout["description"] = payreq_decoded.description
|
||||
payout["payment_hash"] = payreq_decoded.payment_hash
|
||||
|
||||
|
||||
return payout
|
||||
|
||||
@classmethod
|
||||
def pay_invoice(cls, invoice, num_satoshis):
|
||||
'''Sends sats to buyer'''
|
||||
"""Sends sats to buyer"""
|
||||
|
||||
fee_limit_sat = int(max(num_satoshis * float(config('PROPORTIONAL_ROUTING_FEE_LIMIT')), float(config('MIN_FLAT_ROUTING_FEE_LIMIT')))) # 200 ppm or 10 sats
|
||||
request = routerrpc.SendPaymentRequest(
|
||||
payment_request=invoice,
|
||||
fee_limit_sat=fee_limit_sat,
|
||||
timeout_seconds=60)
|
||||
fee_limit_sat = int(
|
||||
max(
|
||||
num_satoshis * float(config("PROPORTIONAL_ROUTING_FEE_LIMIT")),
|
||||
float(config("MIN_FLAT_ROUTING_FEE_LIMIT")),
|
||||
)) # 200 ppm or 10 sats
|
||||
request = routerrpc.SendPaymentRequest(payment_request=invoice,
|
||||
fee_limit_sat=fee_limit_sat,
|
||||
timeout_seconds=60)
|
||||
|
||||
for response in cls.routerstub.SendPaymentV2(request, metadata=[('macaroon', MACAROON.hex())]):
|
||||
for response in cls.routerstub.SendPaymentV2(request,
|
||||
metadata=[("macaroon",
|
||||
MACAROON.hex())
|
||||
]):
|
||||
print(response)
|
||||
print(response.status)
|
||||
|
||||
# TODO ERROR HANDLING
|
||||
if response.status == 0 : # Status 0 'UNKNOWN'
|
||||
pass
|
||||
if response.status == 1 : # Status 1 'IN_FLIGHT'
|
||||
return True, 'In flight'
|
||||
if response.status == 3 : # 4 'FAILED' ??
|
||||
'''0 Payment isn't failed (yet).
|
||||
1 There are more routes to try, but the payment timeout was exceeded.
|
||||
2 All possible routes were tried and failed permanently. Or were no routes to the destination at all.
|
||||
3 A non-recoverable error has occured.
|
||||
4 Payment details incorrect (unknown hash, invalid amt or invalid final cltv delta)
|
||||
5 Insufficient local balance.
|
||||
'''
|
||||
if response.status == 0: # Status 0 'UNKNOWN'
|
||||
pass
|
||||
if response.status == 1: # Status 1 'IN_FLIGHT'
|
||||
return True, "In flight"
|
||||
if response.status == 3: # 4 'FAILED' ??
|
||||
"""0 Payment isn't failed (yet).
|
||||
1 There are more routes to try, but the payment timeout was exceeded.
|
||||
2 All possible routes were tried and failed permanently. Or were no routes to the destination at all.
|
||||
3 A non-recoverable error has occured.
|
||||
4 Payment details incorrect (unknown hash, invalid amt or invalid final cltv delta)
|
||||
5 Insufficient local balance.
|
||||
"""
|
||||
context = cls.payment_failure_context[response.failure_reason]
|
||||
return False, context
|
||||
if response.status == 2 : # STATUS 'SUCCEEDED'
|
||||
if response.status == 2: # STATUS 'SUCCEEDED'
|
||||
return True, None
|
||||
|
||||
|
||||
# How to catch the errors like:"grpc_message":"invoice is already paid","grpc_status":6}
|
||||
# These are not in the response only printed to commandline
|
||||
|
||||
@ -221,15 +266,14 @@ class LNNode():
|
||||
|
||||
@classmethod
|
||||
def double_check_htlc_is_settled(cls, payment_hash):
|
||||
''' Just as it sounds. Better safe than sorry!'''
|
||||
request = invoicesrpc.LookupInvoiceMsg(payment_hash=bytes.fromhex(payment_hash))
|
||||
response = cls.invoicesstub.LookupInvoiceV2(request, metadata=[('macaroon', MACAROON.hex())])
|
||||
|
||||
return response.state == 1 # LND states: 0 OPEN, 1 SETTLED, 3 ACCEPTED, GRPC_ERROR status 5 when cancelled/returned
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
"""Just as it sounds. Better safe than sorry!"""
|
||||
request = invoicesrpc.LookupInvoiceMsg(
|
||||
payment_hash=bytes.fromhex(payment_hash))
|
||||
response = cls.invoicesstub.LookupInvoiceV2(request,
|
||||
metadata=[("macaroon",
|
||||
MACAROON.hex())
|
||||
])
|
||||
|
||||
return (
|
||||
response.state == 1
|
||||
) # LND states: 0 OPEN, 1 SETTLED, 3 ACCEPTED, GRPC_ERROR status 5 when cancelled/returned
|
||||
|
715
api/logics.py
715
api/logics.py
File diff suppressed because it is too large
Load Diff
@ -5,61 +5,72 @@ from api.models import Order
|
||||
from api.logics import Logics
|
||||
from django.utils import timezone
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = 'Follows all active hold invoices'
|
||||
help = "Follows all active hold invoices"
|
||||
|
||||
# def add_arguments(self, parser):
|
||||
# parser.add_argument('debug', nargs='+', type=boolean)
|
||||
|
||||
def clean_orders(self, *args, **options):
|
||||
''' Continuously checks order expiration times for 1 hour. If order
|
||||
has expires, it calls the logics module for expiration handling.'''
|
||||
"""Continuously checks order expiration times for 1 hour. If order
|
||||
has expires, it calls the logics module for expiration handling."""
|
||||
|
||||
# TODO handle 'database is locked'
|
||||
|
||||
do_nothing = [Order.Status.DEL, Order.Status.UCA,
|
||||
Order.Status.EXP, Order.Status.FSE,
|
||||
Order.Status.DIS, Order.Status.CCA,
|
||||
Order.Status.PAY, Order.Status.SUC,
|
||||
Order.Status.FAI, Order.Status.MLD,
|
||||
Order.Status.TLD, Order.Status.WFR]
|
||||
|
||||
do_nothing = [
|
||||
Order.Status.DEL,
|
||||
Order.Status.UCA,
|
||||
Order.Status.EXP,
|
||||
Order.Status.FSE,
|
||||
Order.Status.DIS,
|
||||
Order.Status.CCA,
|
||||
Order.Status.PAY,
|
||||
Order.Status.SUC,
|
||||
Order.Status.FAI,
|
||||
Order.Status.MLD,
|
||||
Order.Status.TLD,
|
||||
Order.Status.WFR,
|
||||
]
|
||||
|
||||
while True:
|
||||
time.sleep(5)
|
||||
|
||||
queryset = Order.objects.exclude(status__in=do_nothing)
|
||||
queryset = queryset.filter(expires_at__lt=timezone.now()) # expires at lower than now
|
||||
queryset = queryset.filter(
|
||||
expires_at__lt=timezone.now()) # expires at lower than now
|
||||
|
||||
debug = {}
|
||||
debug['num_expired_orders'] = len(queryset)
|
||||
debug['expired_orders'] = []
|
||||
debug["num_expired_orders"] = len(queryset)
|
||||
debug["expired_orders"] = []
|
||||
|
||||
for idx, order in enumerate(queryset):
|
||||
context = str(order)+ " was "+ Order.Status(order.status).label
|
||||
context = str(order) + " was " + Order.Status(
|
||||
order.status).label
|
||||
try:
|
||||
if Logics.order_expires(order): # Order send to expire here
|
||||
debug['expired_orders'].append({idx:context})
|
||||
|
||||
# It should not happen, but if it cannot locate the hold invoice
|
||||
if Logics.order_expires(
|
||||
order): # Order send to expire here
|
||||
debug["expired_orders"].append({idx: context})
|
||||
|
||||
# It should not happen, but if it cannot locate the hold invoice
|
||||
# it probably was cancelled by another thread, make it expire anyway.
|
||||
except Exception as e:
|
||||
if 'unable to locate invoice' in str(e):
|
||||
if "unable to locate invoice" in str(e):
|
||||
self.stdout.write(str(e))
|
||||
order.status = Order.Status.EXP
|
||||
order.save()
|
||||
debug['expired_orders'].append({idx:context})
|
||||
|
||||
debug["expired_orders"].append({idx: context})
|
||||
|
||||
if debug['num_expired_orders'] > 0:
|
||||
if debug["num_expired_orders"] > 0:
|
||||
self.stdout.write(str(timezone.now()))
|
||||
self.stdout.write(str(debug))
|
||||
|
||||
|
||||
def handle(self, *args, **options):
|
||||
''' Never mind database locked error, keep going, print them out'''
|
||||
"""Never mind database locked error, keep going, print them out"""
|
||||
try:
|
||||
self.clean_orders()
|
||||
except Exception as e:
|
||||
if 'database is locked' in str(e):
|
||||
self.stdout.write('database is locked')
|
||||
|
||||
if "database is locked" in str(e):
|
||||
self.stdout.write("database is locked")
|
||||
|
||||
self.stdout.write(str(e))
|
||||
|
@ -11,17 +11,18 @@ from decouple import config
|
||||
from base64 import b64decode
|
||||
import time
|
||||
|
||||
MACAROON = b64decode(config('LND_MACAROON_BASE64'))
|
||||
MACAROON = b64decode(config("LND_MACAROON_BASE64"))
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
|
||||
help = 'Follows all active hold invoices'
|
||||
rest = 5 # seconds between consecutive checks for invoice updates
|
||||
help = "Follows all active hold invoices"
|
||||
rest = 5 # seconds between consecutive checks for invoice updates
|
||||
|
||||
def handle(self, *args, **options):
|
||||
''' Infinite loop to check invoices and retry payments.
|
||||
ever mind database locked error, keep going, print out'''
|
||||
|
||||
"""Infinite loop to check invoices and retry payments.
|
||||
ever mind database locked error, keep going, print out"""
|
||||
|
||||
while True:
|
||||
time.sleep(self.rest)
|
||||
|
||||
@ -35,67 +36,76 @@ class Command(BaseCommand):
|
||||
self.stdout.write(str(e))
|
||||
|
||||
def follow_hold_invoices(self):
|
||||
''' Follows and updates LNpayment objects
|
||||
"""Follows and updates LNpayment objects
|
||||
until settled or canceled
|
||||
|
||||
|
||||
Background: SubscribeInvoices stub iterator would be great to use here.
|
||||
However, it only sends updates when the invoice is OPEN (new) or SETTLED.
|
||||
We are very interested on the other two states (CANCELLED and ACCEPTED).
|
||||
Therefore, this thread (follow_invoices) will iterate over all LNpayment
|
||||
objects and do InvoiceLookupV2 every X seconds to update their state 'live'
|
||||
'''
|
||||
|
||||
objects and do InvoiceLookupV2 every X seconds to update their state 'live'
|
||||
"""
|
||||
|
||||
lnd_state_to_lnpayment_status = {
|
||||
0: LNPayment.Status.INVGEN, # OPEN
|
||||
1: LNPayment.Status.SETLED, # SETTLED
|
||||
2: LNPayment.Status.CANCEL, # CANCELLED
|
||||
3: LNPayment.Status.LOCKED # ACCEPTED
|
||||
}
|
||||
0: LNPayment.Status.INVGEN, # OPEN
|
||||
1: LNPayment.Status.SETLED, # SETTLED
|
||||
2: LNPayment.Status.CANCEL, # CANCELLED
|
||||
3: LNPayment.Status.LOCKED, # ACCEPTED
|
||||
}
|
||||
|
||||
stub = LNNode.invoicesstub
|
||||
|
||||
# time it for debugging
|
||||
t0 = time.time()
|
||||
queryset = LNPayment.objects.filter(type=LNPayment.Types.HOLD, status__in=[LNPayment.Status.INVGEN, LNPayment.Status.LOCKED])
|
||||
queryset = LNPayment.objects.filter(
|
||||
type=LNPayment.Types.HOLD,
|
||||
status__in=[LNPayment.Status.INVGEN, LNPayment.Status.LOCKED],
|
||||
)
|
||||
|
||||
debug = {}
|
||||
debug['num_active_invoices'] = len(queryset)
|
||||
debug['invoices'] = []
|
||||
debug["num_active_invoices"] = len(queryset)
|
||||
debug["invoices"] = []
|
||||
at_least_one_changed = False
|
||||
|
||||
for idx, hold_lnpayment in enumerate(queryset):
|
||||
old_status = LNPayment.Status(hold_lnpayment.status).label
|
||||
try:
|
||||
try:
|
||||
# this is similar to LNNnode.validate_hold_invoice_locked
|
||||
request = LNNode.invoicesrpc.LookupInvoiceMsg(payment_hash=bytes.fromhex(hold_lnpayment.payment_hash))
|
||||
response = stub.LookupInvoiceV2(request, metadata=[('macaroon', MACAROON.hex())])
|
||||
hold_lnpayment.status = lnd_state_to_lnpayment_status[response.state]
|
||||
request = LNNode.invoicesrpc.LookupInvoiceMsg(
|
||||
payment_hash=bytes.fromhex(hold_lnpayment.payment_hash))
|
||||
response = stub.LookupInvoiceV2(request,
|
||||
metadata=[("macaroon",
|
||||
MACAROON.hex())])
|
||||
hold_lnpayment.status = lnd_state_to_lnpayment_status[
|
||||
response.state]
|
||||
|
||||
# try saving expiry height
|
||||
if hasattr(response, 'htlcs' ):
|
||||
if hasattr(response, "htlcs"):
|
||||
try:
|
||||
hold_lnpayment.expiry_height = response.htlcs[0].expiry_height
|
||||
hold_lnpayment.expiry_height = response.htlcs[
|
||||
0].expiry_height
|
||||
except:
|
||||
pass
|
||||
|
||||
except Exception as e:
|
||||
# If it fails at finding the invoice: it has been canceled.
|
||||
# In RoboSats DB we make a distinction between cancelled and returned (LND does not)
|
||||
if 'unable to locate invoice' in str(e):
|
||||
if "unable to locate invoice" in str(e):
|
||||
self.stdout.write(str(e))
|
||||
hold_lnpayment.status = LNPayment.Status.CANCEL
|
||||
|
||||
|
||||
# LND restarted.
|
||||
if 'wallet locked, unlock it' in str(e):
|
||||
self.stdout.write(str(timezone.now())+' :: Wallet Locked')
|
||||
if "wallet locked, unlock it" in str(e):
|
||||
self.stdout.write(
|
||||
str(timezone.now()) + " :: Wallet Locked")
|
||||
# Other write to logs
|
||||
else:
|
||||
self.stdout.write(str(e))
|
||||
|
||||
|
||||
new_status = LNPayment.Status(hold_lnpayment.status).label
|
||||
|
||||
# Only save the hold_payments that change (otherwise this function does not scale)
|
||||
changed = not old_status==new_status
|
||||
changed = not old_status == new_status
|
||||
if changed:
|
||||
# self.handle_status_change(hold_lnpayment, old_status)
|
||||
self.update_order_status(hold_lnpayment)
|
||||
@ -103,39 +113,48 @@ class Command(BaseCommand):
|
||||
|
||||
# Report for debugging
|
||||
new_status = LNPayment.Status(hold_lnpayment.status).label
|
||||
debug['invoices'].append({idx:{
|
||||
'payment_hash': str(hold_lnpayment.payment_hash),
|
||||
'old_status': old_status,
|
||||
'new_status': new_status,
|
||||
}})
|
||||
debug["invoices"].append({
|
||||
idx: {
|
||||
"payment_hash": str(hold_lnpayment.payment_hash),
|
||||
"old_status": old_status,
|
||||
"new_status": new_status,
|
||||
}
|
||||
})
|
||||
|
||||
at_least_one_changed = at_least_one_changed or changed
|
||||
|
||||
debug['time']=time.time()-t0
|
||||
|
||||
debug["time"] = time.time() - t0
|
||||
|
||||
if at_least_one_changed:
|
||||
self.stdout.write(str(timezone.now()))
|
||||
self.stdout.write(str(debug))
|
||||
|
||||
def send_payments(self):
|
||||
'''
|
||||
"""
|
||||
Checks for invoices that are due to pay; i.e., INFLIGHT status and 0 routing_attempts.
|
||||
Checks if any payment is due for retry, and tries to pay it.
|
||||
'''
|
||||
"""
|
||||
|
||||
queryset = LNPayment.objects.filter(type=LNPayment.Types.NORM,
|
||||
status=LNPayment.Status.FLIGHT,
|
||||
routing_attempts=0)
|
||||
queryset = LNPayment.objects.filter(
|
||||
type=LNPayment.Types.NORM,
|
||||
status=LNPayment.Status.FLIGHT,
|
||||
routing_attempts=0,
|
||||
)
|
||||
|
||||
queryset_retries = LNPayment.objects.filter(
|
||||
type=LNPayment.Types.NORM,
|
||||
status__in=[LNPayment.Status.VALIDI, LNPayment.Status.FAILRO],
|
||||
routing_attempts__lt=5,
|
||||
last_routing_time__lt=(
|
||||
timezone.now() - timedelta(minutes=int(config("RETRY_TIME")))),
|
||||
)
|
||||
|
||||
queryset_retries = LNPayment.objects.filter(type=LNPayment.Types.NORM,
|
||||
status__in=[LNPayment.Status.VALIDI, LNPayment.Status.FAILRO],
|
||||
routing_attempts__lt=5,
|
||||
last_routing_time__lt=(timezone.now()-timedelta(minutes=int(config('RETRY_TIME')))))
|
||||
|
||||
queryset = queryset.union(queryset_retries)
|
||||
|
||||
|
||||
for lnpayment in queryset:
|
||||
success, _ = follow_send_payment(lnpayment) # Do follow_send_payment.delay() for further concurrency.
|
||||
success, _ = follow_send_payment(
|
||||
lnpayment
|
||||
) # Do follow_send_payment.delay() for further concurrency.
|
||||
|
||||
# If failed, reset mision control. (This won't scale well, just a temporary fix)
|
||||
if not success:
|
||||
@ -148,26 +167,26 @@ class Command(BaseCommand):
|
||||
lnpayment.save()
|
||||
|
||||
def update_order_status(self, lnpayment):
|
||||
''' Background process following LND hold invoices
|
||||
"""Background process following LND hold invoices
|
||||
can catch LNpayments changing status. If they do,
|
||||
the order status might have to change too.'''
|
||||
the order status might have to change too."""
|
||||
|
||||
# If the LNPayment goes to LOCKED (ACCEPTED)
|
||||
if lnpayment.status == LNPayment.Status.LOCKED:
|
||||
try:
|
||||
# It is a maker bond => Publish order.
|
||||
if hasattr(lnpayment, 'order_made' ):
|
||||
if hasattr(lnpayment, "order_made"):
|
||||
Logics.publish_order(lnpayment.order_made)
|
||||
return
|
||||
|
||||
# It is a taker bond => close contract.
|
||||
elif hasattr(lnpayment, 'order_taken' ):
|
||||
elif hasattr(lnpayment, "order_taken"):
|
||||
if lnpayment.order_taken.status == Order.Status.TAK:
|
||||
Logics.finalize_contract(lnpayment.order_taken)
|
||||
return
|
||||
|
||||
# It is a trade escrow => move foward order status.
|
||||
elif hasattr(lnpayment, 'order_escrow' ):
|
||||
elif hasattr(lnpayment, "order_escrow"):
|
||||
Logics.trade_escrow_received(lnpayment.order_escrow)
|
||||
return
|
||||
|
||||
@ -177,20 +196,20 @@ class Command(BaseCommand):
|
||||
# If the LNPayment goes to CANCEL from INVGEN, the invoice had expired
|
||||
# If it goes to CANCEL from LOCKED the bond was relased. Order had expired in both cases.
|
||||
# Testing needed for end of time trades!
|
||||
if lnpayment.status == LNPayment.Status.CANCEL :
|
||||
if hasattr(lnpayment, 'order_made' ):
|
||||
Logics.order_expires(lnpayment.order_made)
|
||||
return
|
||||
if lnpayment.status == LNPayment.Status.CANCEL:
|
||||
if hasattr(lnpayment, "order_made"):
|
||||
Logics.order_expires(lnpayment.order_made)
|
||||
return
|
||||
|
||||
elif hasattr(lnpayment, 'order_taken' ):
|
||||
Logics.order_expires(lnpayment.order_taken)
|
||||
return
|
||||
elif hasattr(lnpayment, "order_taken"):
|
||||
Logics.order_expires(lnpayment.order_taken)
|
||||
return
|
||||
|
||||
elif hasattr(lnpayment, 'order_escrow' ):
|
||||
Logics.order_expires(lnpayment.order_escrow)
|
||||
return
|
||||
elif hasattr(lnpayment, "order_escrow"):
|
||||
Logics.order_expires(lnpayment.order_escrow)
|
||||
return
|
||||
|
||||
# TODO If a lnpayment goes from LOCKED to INVGEN. Totally weird
|
||||
# halt the order
|
||||
if lnpayment.status == LNPayment.Status.INVGEN:
|
||||
pass
|
||||
pass
|
||||
|
499
api/models.py
499
api/models.py
@ -1,6 +1,10 @@
|
||||
from django.db import models
|
||||
from django.contrib.auth.models import User
|
||||
from django.core.validators import MaxValueValidator, MinValueValidator, validate_comma_separated_integer_list
|
||||
from django.core.validators import (
|
||||
MaxValueValidator,
|
||||
MinValueValidator,
|
||||
validate_comma_separated_integer_list,
|
||||
)
|
||||
from django.db.models.signals import post_save, pre_delete
|
||||
from django.template.defaultfilters import truncatechars
|
||||
from django.dispatch import receiver
|
||||
@ -12,19 +16,28 @@ from decouple import config
|
||||
from pathlib import Path
|
||||
import json
|
||||
|
||||
MIN_TRADE = int(config('MIN_TRADE'))
|
||||
MAX_TRADE = int(config('MAX_TRADE'))
|
||||
FEE = float(config('FEE'))
|
||||
BOND_SIZE = float(config('BOND_SIZE'))
|
||||
MIN_TRADE = int(config("MIN_TRADE"))
|
||||
MAX_TRADE = int(config("MAX_TRADE"))
|
||||
FEE = float(config("FEE"))
|
||||
BOND_SIZE = float(config("BOND_SIZE"))
|
||||
|
||||
|
||||
class Currency(models.Model):
|
||||
|
||||
currency_dict = json.load(open('frontend/static/assets/currencies.json'))
|
||||
currency_choices = [(int(val), label) for val, label in list(currency_dict.items())]
|
||||
currency_dict = json.load(open("frontend/static/assets/currencies.json"))
|
||||
currency_choices = [(int(val), label)
|
||||
for val, label in list(currency_dict.items())]
|
||||
|
||||
currency = models.PositiveSmallIntegerField(choices=currency_choices, null=False, unique=True)
|
||||
exchange_rate = models.DecimalField(max_digits=14, decimal_places=4, default=None, null=True, validators=[MinValueValidator(0)])
|
||||
currency = models.PositiveSmallIntegerField(choices=currency_choices,
|
||||
null=False,
|
||||
unique=True)
|
||||
exchange_rate = models.DecimalField(
|
||||
max_digits=14,
|
||||
decimal_places=4,
|
||||
default=None,
|
||||
null=True,
|
||||
validators=[MinValueValidator(0)],
|
||||
)
|
||||
timestamp = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
def __str__(self):
|
||||
@ -32,63 +45,101 @@ class Currency(models.Model):
|
||||
return self.currency_dict[str(self.currency)]
|
||||
|
||||
class Meta:
|
||||
verbose_name = 'Cached market currency'
|
||||
verbose_name_plural = 'Currencies'
|
||||
verbose_name = "Cached market currency"
|
||||
verbose_name_plural = "Currencies"
|
||||
|
||||
|
||||
class LNPayment(models.Model):
|
||||
|
||||
class Types(models.IntegerChoices):
|
||||
NORM = 0, 'Regular invoice'
|
||||
HOLD = 1, 'hold invoice'
|
||||
NORM = 0, "Regular invoice"
|
||||
HOLD = 1, "hold invoice"
|
||||
|
||||
class Concepts(models.IntegerChoices):
|
||||
MAKEBOND = 0, 'Maker bond'
|
||||
TAKEBOND = 1, 'Taker bond'
|
||||
TRESCROW = 2, 'Trade escrow'
|
||||
PAYBUYER = 3, 'Payment to buyer'
|
||||
MAKEBOND = 0, "Maker bond"
|
||||
TAKEBOND = 1, "Taker bond"
|
||||
TRESCROW = 2, "Trade escrow"
|
||||
PAYBUYER = 3, "Payment to buyer"
|
||||
|
||||
class Status(models.IntegerChoices):
|
||||
INVGEN = 0, 'Generated'
|
||||
LOCKED = 1, 'Locked'
|
||||
SETLED = 2, 'Settled'
|
||||
RETNED = 3, 'Returned'
|
||||
CANCEL = 4, 'Cancelled'
|
||||
EXPIRE = 5, 'Expired'
|
||||
VALIDI = 6, 'Valid'
|
||||
FLIGHT = 7, 'In flight'
|
||||
SUCCED = 8, 'Succeeded'
|
||||
FAILRO = 9, 'Routing failed'
|
||||
INVGEN = 0, "Generated"
|
||||
LOCKED = 1, "Locked"
|
||||
SETLED = 2, "Settled"
|
||||
RETNED = 3, "Returned"
|
||||
CANCEL = 4, "Cancelled"
|
||||
EXPIRE = 5, "Expired"
|
||||
VALIDI = 6, "Valid"
|
||||
FLIGHT = 7, "In flight"
|
||||
SUCCED = 8, "Succeeded"
|
||||
FAILRO = 9, "Routing failed"
|
||||
|
||||
# payment use details
|
||||
type = models.PositiveSmallIntegerField(choices=Types.choices, null=False, default=Types.HOLD)
|
||||
concept = models.PositiveSmallIntegerField(choices=Concepts.choices, null=False, default=Concepts.MAKEBOND)
|
||||
status = models.PositiveSmallIntegerField(choices=Status.choices, null=False, default=Status.INVGEN)
|
||||
|
||||
type = models.PositiveSmallIntegerField(choices=Types.choices,
|
||||
null=False,
|
||||
default=Types.HOLD)
|
||||
concept = models.PositiveSmallIntegerField(choices=Concepts.choices,
|
||||
null=False,
|
||||
default=Concepts.MAKEBOND)
|
||||
status = models.PositiveSmallIntegerField(choices=Status.choices,
|
||||
null=False,
|
||||
default=Status.INVGEN)
|
||||
|
||||
# payment info
|
||||
payment_hash = models.CharField(max_length=100, unique=True, default=None, blank=True, primary_key=True)
|
||||
invoice = models.CharField(max_length=1200, unique=True, null=True, default=None, blank=True) # Some invoices with lots of routing hints might be long
|
||||
preimage = models.CharField(max_length=64, unique=True, null=True, default=None, blank=True)
|
||||
description = models.CharField(max_length=500, unique=False, null=True, default=None, blank=True)
|
||||
num_satoshis = models.PositiveBigIntegerField(validators=[MinValueValidator(MIN_TRADE*BOND_SIZE), MaxValueValidator(MAX_TRADE*(1+BOND_SIZE+FEE))])
|
||||
payment_hash = models.CharField(max_length=100,
|
||||
unique=True,
|
||||
default=None,
|
||||
blank=True,
|
||||
primary_key=True)
|
||||
invoice = models.CharField(
|
||||
max_length=1200, unique=True, null=True, default=None,
|
||||
blank=True) # Some invoices with lots of routing hints might be long
|
||||
preimage = models.CharField(max_length=64,
|
||||
unique=True,
|
||||
null=True,
|
||||
default=None,
|
||||
blank=True)
|
||||
description = models.CharField(max_length=500,
|
||||
unique=False,
|
||||
null=True,
|
||||
default=None,
|
||||
blank=True)
|
||||
num_satoshis = models.PositiveBigIntegerField(validators=[
|
||||
MinValueValidator(MIN_TRADE * BOND_SIZE),
|
||||
MaxValueValidator(MAX_TRADE * (1 + BOND_SIZE + FEE)),
|
||||
])
|
||||
created_at = models.DateTimeField()
|
||||
expires_at = models.DateTimeField()
|
||||
cltv_expiry = models.PositiveSmallIntegerField(null=True, default=None, blank=True)
|
||||
expiry_height = models.PositiveBigIntegerField(null=True, default=None, blank=True)
|
||||
|
||||
cltv_expiry = models.PositiveSmallIntegerField(null=True,
|
||||
default=None,
|
||||
blank=True)
|
||||
expiry_height = models.PositiveBigIntegerField(null=True,
|
||||
default=None,
|
||||
blank=True)
|
||||
|
||||
# routing
|
||||
routing_attempts = models.PositiveSmallIntegerField(null=False, default=0)
|
||||
last_routing_time = models.DateTimeField(null=True, default=None, blank=True)
|
||||
last_routing_time = models.DateTimeField(null=True,
|
||||
default=None,
|
||||
blank=True)
|
||||
|
||||
# involved parties
|
||||
sender = models.ForeignKey(User, related_name='sender', on_delete=models.CASCADE, null=True, default=None)
|
||||
receiver = models.ForeignKey(User, related_name='receiver', on_delete=models.CASCADE, null=True, default=None)
|
||||
sender = models.ForeignKey(User,
|
||||
related_name="sender",
|
||||
on_delete=models.CASCADE,
|
||||
null=True,
|
||||
default=None)
|
||||
receiver = models.ForeignKey(User,
|
||||
related_name="receiver",
|
||||
on_delete=models.CASCADE,
|
||||
null=True,
|
||||
default=None)
|
||||
|
||||
def __str__(self):
|
||||
return (f'LN-{str(self.payment_hash)[:8]}: {self.Concepts(self.concept).label} - {self.Status(self.status).label}')
|
||||
return f"LN-{str(self.payment_hash)[:8]}: {self.Concepts(self.concept).label} - {self.Status(self.status).label}"
|
||||
|
||||
class Meta:
|
||||
verbose_name = 'Lightning payment'
|
||||
verbose_name_plural = 'Lightning payments'
|
||||
verbose_name = "Lightning payment"
|
||||
verbose_name_plural = "Lightning payments"
|
||||
|
||||
@property
|
||||
def hash(self):
|
||||
@ -97,75 +148,162 @@ class LNPayment(models.Model):
|
||||
# We created a truncated property for display 'hash'
|
||||
return truncatechars(self.payment_hash, 10)
|
||||
|
||||
|
||||
class Order(models.Model):
|
||||
|
||||
|
||||
class Types(models.IntegerChoices):
|
||||
BUY = 0, 'BUY'
|
||||
SELL = 1, 'SELL'
|
||||
BUY = 0, "BUY"
|
||||
SELL = 1, "SELL"
|
||||
|
||||
class Status(models.IntegerChoices):
|
||||
WFB = 0, 'Waiting for maker bond'
|
||||
PUB = 1, 'Public'
|
||||
DEL = 2, 'Deleted'
|
||||
TAK = 3, 'Waiting for taker bond'
|
||||
UCA = 4, 'Cancelled'
|
||||
EXP = 5, 'Expired'
|
||||
WF2 = 6, 'Waiting for trade collateral and buyer invoice'
|
||||
WFE = 7, 'Waiting only for seller trade collateral'
|
||||
WFI = 8, 'Waiting only for buyer invoice'
|
||||
CHA = 9, 'Sending fiat - In chatroom'
|
||||
FSE = 10, 'Fiat sent - In chatroom'
|
||||
DIS = 11, 'In dispute'
|
||||
CCA = 12, 'Collaboratively cancelled'
|
||||
PAY = 13, 'Sending satoshis to buyer'
|
||||
SUC = 14, 'Sucessful trade'
|
||||
FAI = 15, 'Failed lightning network routing'
|
||||
WFR = 16, 'Wait for dispute resolution'
|
||||
MLD = 17, 'Maker lost dispute'
|
||||
TLD = 18, 'Taker lost dispute'
|
||||
WFB = 0, "Waiting for maker bond"
|
||||
PUB = 1, "Public"
|
||||
DEL = 2, "Deleted"
|
||||
TAK = 3, "Waiting for taker bond"
|
||||
UCA = 4, "Cancelled"
|
||||
EXP = 5, "Expired"
|
||||
WF2 = 6, "Waiting for trade collateral and buyer invoice"
|
||||
WFE = 7, "Waiting only for seller trade collateral"
|
||||
WFI = 8, "Waiting only for buyer invoice"
|
||||
CHA = 9, "Sending fiat - In chatroom"
|
||||
FSE = 10, "Fiat sent - In chatroom"
|
||||
DIS = 11, "In dispute"
|
||||
CCA = 12, "Collaboratively cancelled"
|
||||
PAY = 13, "Sending satoshis to buyer"
|
||||
SUC = 14, "Sucessful trade"
|
||||
FAI = 15, "Failed lightning network routing"
|
||||
WFR = 16, "Wait for dispute resolution"
|
||||
MLD = 17, "Maker lost dispute"
|
||||
TLD = 18, "Taker lost dispute"
|
||||
|
||||
# order info
|
||||
status = models.PositiveSmallIntegerField(choices=Status.choices, null=False, default=Status.WFB)
|
||||
status = models.PositiveSmallIntegerField(choices=Status.choices,
|
||||
null=False,
|
||||
default=Status.WFB)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
expires_at = models.DateTimeField()
|
||||
|
||||
# order details
|
||||
type = models.PositiveSmallIntegerField(choices=Types.choices, null=False)
|
||||
currency = models.ForeignKey(Currency, null=True, on_delete=models.SET_NULL)
|
||||
amount = models.DecimalField(max_digits=16, decimal_places=8, validators=[MinValueValidator(0.00000001)])
|
||||
payment_method = models.CharField(max_length=35, null=False, default="not specified", blank=True)
|
||||
currency = models.ForeignKey(Currency,
|
||||
null=True,
|
||||
on_delete=models.SET_NULL)
|
||||
amount = models.DecimalField(max_digits=16,
|
||||
decimal_places=8,
|
||||
validators=[MinValueValidator(0.00000001)])
|
||||
payment_method = models.CharField(max_length=35,
|
||||
null=False,
|
||||
default="not specified",
|
||||
blank=True)
|
||||
|
||||
# order pricing method. A explicit amount of sats, or a relative premium above/below market.
|
||||
is_explicit = models.BooleanField(default=False, null=False)
|
||||
# marked to market
|
||||
premium = models.DecimalField(max_digits=5, decimal_places=2, default=0, null=True, validators=[MinValueValidator(-100), MaxValueValidator(999)], blank=True)
|
||||
premium = models.DecimalField(
|
||||
max_digits=5,
|
||||
decimal_places=2,
|
||||
default=0,
|
||||
null=True,
|
||||
validators=[MinValueValidator(-100),
|
||||
MaxValueValidator(999)],
|
||||
blank=True,
|
||||
)
|
||||
# explicit
|
||||
satoshis = models.PositiveBigIntegerField(null=True, validators=[MinValueValidator(MIN_TRADE), MaxValueValidator(MAX_TRADE)], blank=True)
|
||||
satoshis = models.PositiveBigIntegerField(
|
||||
null=True,
|
||||
validators=[
|
||||
MinValueValidator(MIN_TRADE),
|
||||
MaxValueValidator(MAX_TRADE)
|
||||
],
|
||||
blank=True,
|
||||
)
|
||||
# how many sats at creation and at last check (relevant for marked to market)
|
||||
t0_satoshis = models.PositiveBigIntegerField(null=True, validators=[MinValueValidator(MIN_TRADE), MaxValueValidator(MAX_TRADE)], blank=True) # sats at creation
|
||||
last_satoshis = models.PositiveBigIntegerField(null=True, validators=[MinValueValidator(0), MaxValueValidator(MAX_TRADE*2)], blank=True) # sats last time checked. Weird if 2* trade max...
|
||||
|
||||
t0_satoshis = models.PositiveBigIntegerField(
|
||||
null=True,
|
||||
validators=[
|
||||
MinValueValidator(MIN_TRADE),
|
||||
MaxValueValidator(MAX_TRADE)
|
||||
],
|
||||
blank=True,
|
||||
) # sats at creation
|
||||
last_satoshis = models.PositiveBigIntegerField(
|
||||
null=True,
|
||||
validators=[MinValueValidator(0),
|
||||
MaxValueValidator(MAX_TRADE * 2)],
|
||||
blank=True,
|
||||
) # sats last time checked. Weird if 2* trade max...
|
||||
|
||||
# order participants
|
||||
maker = models.ForeignKey(User, related_name='maker', on_delete=models.SET_NULL, null=True, default=None) # unique = True, a maker can only make one order
|
||||
taker = models.ForeignKey(User, related_name='taker', on_delete=models.SET_NULL, null=True, default=None, blank=True) # unique = True, a taker can only take one order
|
||||
maker_last_seen = models.DateTimeField(null=True,default=None, blank=True)
|
||||
taker_last_seen = models.DateTimeField(null=True,default=None, blank=True)
|
||||
maker_asked_cancel = models.BooleanField(default=False, null=False) # When collaborative cancel is needed and one partner has cancelled.
|
||||
taker_asked_cancel = models.BooleanField(default=False, null=False) # When collaborative cancel is needed and one partner has cancelled.
|
||||
maker = models.ForeignKey(
|
||||
User,
|
||||
related_name="maker",
|
||||
on_delete=models.SET_NULL,
|
||||
null=True,
|
||||
default=None) # unique = True, a maker can only make one order
|
||||
taker = models.ForeignKey(
|
||||
User,
|
||||
related_name="taker",
|
||||
on_delete=models.SET_NULL,
|
||||
null=True,
|
||||
default=None,
|
||||
blank=True,
|
||||
) # unique = True, a taker can only take one order
|
||||
maker_last_seen = models.DateTimeField(null=True, default=None, blank=True)
|
||||
taker_last_seen = models.DateTimeField(null=True, default=None, blank=True)
|
||||
maker_asked_cancel = models.BooleanField(
|
||||
default=False, null=False
|
||||
) # When collaborative cancel is needed and one partner has cancelled.
|
||||
taker_asked_cancel = models.BooleanField(
|
||||
default=False, null=False
|
||||
) # When collaborative cancel is needed and one partner has cancelled.
|
||||
is_fiat_sent = models.BooleanField(default=False, null=False)
|
||||
|
||||
# in dispute
|
||||
is_disputed = models.BooleanField(default=False, null=False)
|
||||
maker_statement = models.TextField(max_length=5000, null=True, default=None, blank=True)
|
||||
taker_statement = models.TextField(max_length=5000, null=True, default=None, blank=True)
|
||||
maker_statement = models.TextField(max_length=5000,
|
||||
null=True,
|
||||
default=None,
|
||||
blank=True)
|
||||
taker_statement = models.TextField(max_length=5000,
|
||||
null=True,
|
||||
default=None,
|
||||
blank=True)
|
||||
|
||||
# LNpayments
|
||||
# Order collateral
|
||||
maker_bond = models.OneToOneField(LNPayment, related_name='order_made', on_delete=models.SET_NULL, null=True, default=None, blank=True)
|
||||
taker_bond = models.OneToOneField(LNPayment, related_name='order_taken', on_delete=models.SET_NULL, null=True, default=None, blank=True)
|
||||
trade_escrow = models.OneToOneField(LNPayment, related_name='order_escrow', on_delete=models.SET_NULL, null=True, default=None, blank=True)
|
||||
maker_bond = models.OneToOneField(
|
||||
LNPayment,
|
||||
related_name="order_made",
|
||||
on_delete=models.SET_NULL,
|
||||
null=True,
|
||||
default=None,
|
||||
blank=True,
|
||||
)
|
||||
taker_bond = models.OneToOneField(
|
||||
LNPayment,
|
||||
related_name="order_taken",
|
||||
on_delete=models.SET_NULL,
|
||||
null=True,
|
||||
default=None,
|
||||
blank=True,
|
||||
)
|
||||
trade_escrow = models.OneToOneField(
|
||||
LNPayment,
|
||||
related_name="order_escrow",
|
||||
on_delete=models.SET_NULL,
|
||||
null=True,
|
||||
default=None,
|
||||
blank=True,
|
||||
)
|
||||
# buyer payment LN invoice
|
||||
payout = models.OneToOneField(LNPayment, related_name='order_paid', on_delete=models.SET_NULL, null=True, default=None, blank=True)
|
||||
payout = models.OneToOneField(
|
||||
LNPayment,
|
||||
related_name="order_paid",
|
||||
on_delete=models.SET_NULL,
|
||||
null=True,
|
||||
default=None,
|
||||
blank=True,
|
||||
)
|
||||
|
||||
# ratings
|
||||
maker_rated = models.BooleanField(default=False, null=False)
|
||||
@ -174,34 +312,44 @@ class Order(models.Model):
|
||||
taker_platform_rated = models.BooleanField(default=False, null=False)
|
||||
|
||||
t_to_expire = {
|
||||
0 : int(config('EXP_MAKER_BOND_INVOICE')) , # 'Waiting for maker bond'
|
||||
1 : 60*60*int(config('PUBLIC_ORDER_DURATION')), # 'Public'
|
||||
2 : 0, # 'Deleted'
|
||||
3 : int(config('EXP_TAKER_BOND_INVOICE')), # 'Waiting for taker bond'
|
||||
4 : 0, # 'Cancelled'
|
||||
5 : 0, # 'Expired'
|
||||
6 : 60*int(config('INVOICE_AND_ESCROW_DURATION')), # 'Waiting for trade collateral and buyer invoice'
|
||||
7 : 60*int(config('INVOICE_AND_ESCROW_DURATION')), # 'Waiting only for seller trade collateral'
|
||||
8 : 60*int(config('INVOICE_AND_ESCROW_DURATION')), # 'Waiting only for buyer invoice'
|
||||
9 : 60*60*int(config('FIAT_EXCHANGE_DURATION')), # 'Sending fiat - In chatroom'
|
||||
10 : 60*60*int(config('FIAT_EXCHANGE_DURATION')), # 'Fiat sent - In chatroom'
|
||||
11 : 1*24*60*60, # 'In dispute'
|
||||
12 : 0, # 'Collaboratively cancelled'
|
||||
13 : 24*60*60, # 'Sending satoshis to buyer'
|
||||
14 : 24*60*60, # 'Sucessful trade'
|
||||
15 : 24*60*60, # 'Failed lightning network routing'
|
||||
16 : 10*24*60*60, # 'Wait for dispute resolution'
|
||||
17 : 24*60*60, # 'Maker lost dispute'
|
||||
18 : 24*60*60, # 'Taker lost dispute'
|
||||
}
|
||||
0: int(config("EXP_MAKER_BOND_INVOICE")), # 'Waiting for maker bond'
|
||||
1: 60 * 60 * int(config("PUBLIC_ORDER_DURATION")), # 'Public'
|
||||
2: 0, # 'Deleted'
|
||||
3: int(config("EXP_TAKER_BOND_INVOICE")), # 'Waiting for taker bond'
|
||||
4: 0, # 'Cancelled'
|
||||
5: 0, # 'Expired'
|
||||
6: 60 * int(config("INVOICE_AND_ESCROW_DURATION")
|
||||
), # 'Waiting for trade collateral and buyer invoice'
|
||||
7: 60 * int(config("INVOICE_AND_ESCROW_DURATION")
|
||||
), # 'Waiting only for seller trade collateral'
|
||||
8: 60 * int(config("INVOICE_AND_ESCROW_DURATION")
|
||||
), # 'Waiting only for buyer invoice'
|
||||
9: 60 * 60 *
|
||||
int(config("FIAT_EXCHANGE_DURATION")), # 'Sending fiat - In chatroom'
|
||||
10: 60 * 60 *
|
||||
int(config("FIAT_EXCHANGE_DURATION")), # 'Fiat sent - In chatroom'
|
||||
11: 1 * 24 * 60 * 60, # 'In dispute'
|
||||
12: 0, # 'Collaboratively cancelled'
|
||||
13: 24 * 60 * 60, # 'Sending satoshis to buyer'
|
||||
14: 24 * 60 * 60, # 'Sucessful trade'
|
||||
15: 24 * 60 * 60, # 'Failed lightning network routing'
|
||||
16: 10 * 24 * 60 * 60, # 'Wait for dispute resolution'
|
||||
17: 24 * 60 * 60, # 'Maker lost dispute'
|
||||
18: 24 * 60 * 60, # 'Taker lost dispute'
|
||||
}
|
||||
|
||||
def __str__(self):
|
||||
return (f'Order {self.id}: {self.Types(self.type).label} BTC for {float(self.amount)} {self.currency}')
|
||||
|
||||
return f"Order {self.id}: {self.Types(self.type).label} BTC for {float(self.amount)} {self.currency}"
|
||||
|
||||
|
||||
@receiver(pre_delete, sender=Order)
|
||||
def delete_lnpayment_at_order_deletion(sender, instance, **kwargs):
|
||||
to_delete = (instance.maker_bond, instance.payout, instance.taker_bond, instance.trade_escrow)
|
||||
to_delete = (
|
||||
instance.maker_bond,
|
||||
instance.payout,
|
||||
instance.taker_bond,
|
||||
instance.trade_escrow,
|
||||
)
|
||||
|
||||
for lnpayment in to_delete:
|
||||
try:
|
||||
@ -209,31 +357,60 @@ def delete_lnpayment_at_order_deletion(sender, instance, **kwargs):
|
||||
except:
|
||||
pass
|
||||
|
||||
|
||||
class Profile(models.Model):
|
||||
user = models.OneToOneField(User,on_delete=models.CASCADE)
|
||||
user = models.OneToOneField(User, on_delete=models.CASCADE)
|
||||
|
||||
# Total trades
|
||||
total_contracts = models.PositiveIntegerField(null=False, default=0)
|
||||
total_contracts = models.PositiveIntegerField(null=False, default=0)
|
||||
|
||||
# Ratings stored as a comma separated integer list
|
||||
total_ratings = models.PositiveIntegerField(null=False, default=0)
|
||||
latest_ratings = models.CharField(max_length=999, null=True, default=None, validators=[validate_comma_separated_integer_list], blank=True) # Will only store latest ratings
|
||||
avg_rating = models.DecimalField(max_digits=4, decimal_places=1, default=None, null=True, validators=[MinValueValidator(0), MaxValueValidator(100)], blank=True)
|
||||
total_ratings = models.PositiveIntegerField(null=False, default=0)
|
||||
latest_ratings = models.CharField(
|
||||
max_length=999,
|
||||
null=True,
|
||||
default=None,
|
||||
validators=[validate_comma_separated_integer_list],
|
||||
blank=True,
|
||||
) # Will only store latest ratings
|
||||
avg_rating = models.DecimalField(
|
||||
max_digits=4,
|
||||
decimal_places=1,
|
||||
default=None,
|
||||
null=True,
|
||||
validators=[MinValueValidator(0),
|
||||
MaxValueValidator(100)],
|
||||
blank=True,
|
||||
)
|
||||
|
||||
# Disputes
|
||||
num_disputes = models.PositiveIntegerField(null=False, default=0)
|
||||
lost_disputes = models.PositiveIntegerField(null=False, default=0)
|
||||
num_disputes_started = models.PositiveIntegerField(null=False, default=0)
|
||||
orders_disputes_started = models.CharField(max_length=999, null=True, default=None, validators=[validate_comma_separated_integer_list], blank=True) # Will only store ID of orders
|
||||
orders_disputes_started = models.CharField(
|
||||
max_length=999,
|
||||
null=True,
|
||||
default=None,
|
||||
validators=[validate_comma_separated_integer_list],
|
||||
blank=True,
|
||||
) # Will only store ID of orders
|
||||
|
||||
# RoboHash
|
||||
avatar = models.ImageField(default=("static/assets/avatars/"+"unknown_avatar.png"), verbose_name='Avatar', blank=True)
|
||||
avatar = models.ImageField(
|
||||
default=("static/assets/avatars/" + "unknown_avatar.png"),
|
||||
verbose_name="Avatar",
|
||||
blank=True,
|
||||
)
|
||||
|
||||
# Penalty expiration (only used then taking/cancelling repeatedly orders in the book before comitting bond)
|
||||
penalty_expiration = models.DateTimeField(null=True,default=None, blank=True)
|
||||
penalty_expiration = models.DateTimeField(null=True,
|
||||
default=None,
|
||||
blank=True)
|
||||
|
||||
# Platform rate
|
||||
platform_rating = models.PositiveIntegerField(null=True, default=None, blank=True)
|
||||
platform_rating = models.PositiveIntegerField(null=True,
|
||||
default=None,
|
||||
blank=True)
|
||||
|
||||
@receiver(post_save, sender=User)
|
||||
def create_user_profile(sender, instance, created, **kwargs):
|
||||
@ -247,50 +424,82 @@ class Profile(models.Model):
|
||||
@receiver(pre_delete, sender=User)
|
||||
def del_avatar_from_disk(sender, instance, **kwargs):
|
||||
try:
|
||||
avatar_file=Path(settings.AVATAR_ROOT + instance.profile.avatar.url.split('/')[-1])
|
||||
avatar_file = Path(settings.AVATAR_ROOT +
|
||||
instance.profile.avatar.url.split("/")[-1])
|
||||
avatar_file.unlink()
|
||||
except:
|
||||
pass
|
||||
|
||||
def __str__(self):
|
||||
return self.user.username
|
||||
|
||||
|
||||
# to display avatars in admin panel
|
||||
def get_avatar(self):
|
||||
if not self.avatar:
|
||||
return settings.STATIC_ROOT + 'unknown_avatar.png'
|
||||
return settings.STATIC_ROOT + "unknown_avatar.png"
|
||||
return self.avatar.url
|
||||
|
||||
# method to create a fake table field in read only mode
|
||||
def avatar_tag(self):
|
||||
return mark_safe('<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.
|
||||
"""
|
||||
Records tick by tick Non-KYC Bitcoin price.
|
||||
Data to be aggregated and offered via public API.
|
||||
|
||||
It is checked against current CEX price for useful
|
||||
insight on the historical premium of Non-KYC BTC
|
||||
|
||||
Price is set when taker bond is locked. Both
|
||||
maker and taker are commited with bonds (contract
|
||||
Price is set when taker bond is locked. Both
|
||||
maker and taker are commited with bonds (contract
|
||||
is finished and cancellation has a cost)
|
||||
'''
|
||||
"""
|
||||
|
||||
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
|
||||
price = models.DecimalField(max_digits=10, decimal_places=2, default=None, null=True, validators=[MinValueValidator(0)])
|
||||
volume = models.DecimalField(max_digits=8, decimal_places=8, default=None, null=True, validators=[MinValueValidator(0)])
|
||||
premium = models.DecimalField(max_digits=5, decimal_places=2, default=None, null=True, validators=[MinValueValidator(-100), MaxValueValidator(999)], blank=True)
|
||||
currency = models.ForeignKey(Currency, null=True, on_delete=models.SET_NULL)
|
||||
price = models.DecimalField(
|
||||
max_digits=10,
|
||||
decimal_places=2,
|
||||
default=None,
|
||||
null=True,
|
||||
validators=[MinValueValidator(0)],
|
||||
)
|
||||
volume = models.DecimalField(
|
||||
max_digits=8,
|
||||
decimal_places=8,
|
||||
default=None,
|
||||
null=True,
|
||||
validators=[MinValueValidator(0)],
|
||||
)
|
||||
premium = models.DecimalField(
|
||||
max_digits=5,
|
||||
decimal_places=2,
|
||||
default=None,
|
||||
null=True,
|
||||
validators=[MinValueValidator(-100),
|
||||
MaxValueValidator(999)],
|
||||
blank=True,
|
||||
)
|
||||
currency = models.ForeignKey(Currency,
|
||||
null=True,
|
||||
on_delete=models.SET_NULL)
|
||||
timestamp = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
|
||||
# Relevant to keep record of the historical fee, so the insight on the premium can be better analyzed
|
||||
fee = models.DecimalField(max_digits=4, decimal_places=4, default=FEE, validators=[MinValueValidator(0), MaxValueValidator(1)])
|
||||
fee = models.DecimalField(
|
||||
max_digits=4,
|
||||
decimal_places=4,
|
||||
default=FEE,
|
||||
validators=[MinValueValidator(0),
|
||||
MaxValueValidator(1)],
|
||||
)
|
||||
|
||||
def log_a_tick(order):
|
||||
'''
|
||||
"""
|
||||
Creates a new tick
|
||||
'''
|
||||
"""
|
||||
|
||||
if not order.taker_bond:
|
||||
return None
|
||||
@ -301,18 +510,16 @@ class MarketTick(models.Model):
|
||||
market_exchange_rate = float(order.currency.exchange_rate)
|
||||
premium = 100 * (price / market_exchange_rate - 1)
|
||||
|
||||
tick = MarketTick.objects.create(
|
||||
price=price,
|
||||
volume=volume,
|
||||
premium=premium,
|
||||
currency=order.currency)
|
||||
tick = MarketTick.objects.create(price=price,
|
||||
volume=volume,
|
||||
premium=premium,
|
||||
currency=order.currency)
|
||||
|
||||
tick.save()
|
||||
|
||||
def __str__(self):
|
||||
return f'Tick: {str(self.id)[:8]}'
|
||||
return f"Tick: {str(self.id)[:8]}"
|
||||
|
||||
class Meta:
|
||||
verbose_name = 'Market tick'
|
||||
verbose_name_plural = 'Market ticks'
|
||||
|
||||
verbose_name = "Market tick"
|
||||
verbose_name_plural = "Market ticks"
|
||||
|
@ -4832,4 +4832,4 @@ adjectives = [
|
||||
"Bodacious",
|
||||
"Unpersuasive",
|
||||
"Simplistic",
|
||||
]
|
||||
]
|
||||
|
@ -2,7 +2,6 @@ from .utils import human_format
|
||||
|
||||
import hashlib
|
||||
import time
|
||||
|
||||
"""
|
||||
Deterministic nick generator from SHA256 hash.
|
||||
|
||||
@ -14,7 +13,9 @@ is a total of to 450*4800*12500*1000 =
|
||||
28 Trillion deterministic nicks
|
||||
"""
|
||||
|
||||
|
||||
class NickGenerator:
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
lang="English",
|
||||
@ -42,13 +43,11 @@ class NickGenerator:
|
||||
raise ValueError("Language not implemented.")
|
||||
|
||||
if verbose:
|
||||
print(
|
||||
f"{lang} SHA256 Nick Generator initialized with:"
|
||||
+ f"\nUp to {len(adverbs)} adverbs."
|
||||
+ f"\nUp to {len(adjectives)} adjectives."
|
||||
+ f"\nUp to {len(nouns)} nouns."
|
||||
+ f"\nUp to {max_num+1} numerics.\n"
|
||||
)
|
||||
print(f"{lang} SHA256 Nick Generator initialized with:" +
|
||||
f"\nUp to {len(adverbs)} adverbs." +
|
||||
f"\nUp to {len(adjectives)} adjectives." +
|
||||
f"\nUp to {len(nouns)} nouns." +
|
||||
f"\nUp to {max_num+1} numerics.\n")
|
||||
|
||||
self.use_adv = use_adv
|
||||
self.use_adj = use_adj
|
||||
@ -78,7 +77,7 @@ class NickGenerator:
|
||||
pool_size = self.max_num * num_nouns * num_adj * num_adv
|
||||
|
||||
# Min-Max scale the hash relative to the pool size
|
||||
max_int_hash = 2 ** 256
|
||||
max_int_hash = 2**256
|
||||
int_hash = int(hash, 16)
|
||||
nick_id = int((int_hash / max_int_hash) * pool_size)
|
||||
|
||||
@ -148,7 +147,10 @@ class NickGenerator:
|
||||
i = i + 1
|
||||
return "", 0, 0, i
|
||||
|
||||
def compute_pool_size_loss(self, max_length=22, max_iter=1000000, num_runs=5000):
|
||||
def compute_pool_size_loss(self,
|
||||
max_length=22,
|
||||
max_iter=1000000,
|
||||
num_runs=5000):
|
||||
"""
|
||||
Computes median an average loss of
|
||||
nick pool diversity due to max_lenght
|
||||
@ -184,7 +186,7 @@ if __name__ == "__main__":
|
||||
t0 = time.time()
|
||||
|
||||
# Hardcoded example text and hashing
|
||||
nick_lang = 'English' #Spanish
|
||||
nick_lang = "English" # Spanish
|
||||
hash = hashlib.sha256(b"No one expected such cool nick!!").hexdigest()
|
||||
max_length = 22
|
||||
max_iter = 100000000
|
||||
@ -194,16 +196,13 @@ if __name__ == "__main__":
|
||||
|
||||
# Generates a short nick with length limit from SHA256
|
||||
nick, nick_id, pool_size, iterations = GenNick.short_from_SHA256(
|
||||
hash, max_length, max_iter
|
||||
)
|
||||
hash, max_length, max_iter)
|
||||
|
||||
# Output
|
||||
print(
|
||||
f"Nick number {nick_id} has been selected among"
|
||||
+ f" {human_format(pool_size)} possible nicks.\n"
|
||||
+ f"Needed {iterations} iterations to find one "
|
||||
+ f"this short.\nYour nick is {nick} !\n"
|
||||
)
|
||||
print(f"Nick number {nick_id} has been selected among" +
|
||||
f" {human_format(pool_size)} possible nicks.\n" +
|
||||
f"Needed {iterations} iterations to find one " +
|
||||
f"this short.\nYour nick is {nick} !\n")
|
||||
print(f"Nick lenght is {len(nick)} characters.")
|
||||
print(f"Nick landed at height {nick_id/(pool_size+1)} on the pool.")
|
||||
print(f"Took {time.time()-t0} secs.\n")
|
||||
@ -217,8 +216,9 @@ if __name__ == "__main__":
|
||||
string = str(random.uniform(0, 1000000))
|
||||
hash = hashlib.sha256(str.encode(string)).hexdigest()
|
||||
print(
|
||||
GenNick.short_from_SHA256(hash, max_length=max_length, max_iter=max_iter)[0]
|
||||
)
|
||||
GenNick.short_from_SHA256(hash,
|
||||
max_length=max_length,
|
||||
max_iter=max_iter)[0])
|
||||
|
||||
# Other analysis
|
||||
GenNick.compute_pool_size_loss(max_length, max_iter, 200)
|
||||
|
@ -1,7 +1,10 @@
|
||||
from math import log, floor
|
||||
|
||||
|
||||
def human_format(number):
|
||||
units = ["", " Thousand", " Million", " Billion", " Trillion", " Quatrillion"]
|
||||
units = [
|
||||
"", " Thousand", " Million", " Billion", " Trillion", " Quatrillion"
|
||||
]
|
||||
k = 1000.0
|
||||
magnitude = int(floor(log(number, k)))
|
||||
return "%.2f%s" % (number / k ** magnitude, units[magnitude])
|
||||
return "%.2f%s" % (number / k**magnitude, units[magnitude])
|
||||
|
@ -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,
|
||||
)
|
||||
|
92
api/tasks.py
92
api/tasks.py
@ -1,10 +1,11 @@
|
||||
from celery import shared_task
|
||||
|
||||
|
||||
@shared_task(name="users_cleansing")
|
||||
def users_cleansing():
|
||||
'''
|
||||
"""
|
||||
Deletes users never used 12 hours after creation
|
||||
'''
|
||||
"""
|
||||
from django.contrib.auth.models import User
|
||||
from django.db.models import Q
|
||||
from .logics import Logics
|
||||
@ -14,8 +15,8 @@ def users_cleansing():
|
||||
# Users who's last login has not been in the last 6 hours
|
||||
active_time_range = (timezone.now() - timedelta(hours=6), timezone.now())
|
||||
queryset = User.objects.filter(~Q(last_login__range=active_time_range))
|
||||
queryset = queryset.filter(is_staff=False) # Do not delete staff users
|
||||
|
||||
queryset = queryset.filter(is_staff=False) # Do not delete staff users
|
||||
|
||||
# And do not have an active trade or any past contract.
|
||||
deleted_users = []
|
||||
for user in queryset:
|
||||
@ -27,14 +28,15 @@ def users_cleansing():
|
||||
user.delete()
|
||||
|
||||
results = {
|
||||
'num_deleted': len(deleted_users),
|
||||
'deleted_users': deleted_users,
|
||||
"num_deleted": len(deleted_users),
|
||||
"deleted_users": deleted_users,
|
||||
}
|
||||
return results
|
||||
|
||||
@shared_task(name='follow_send_payment')
|
||||
|
||||
@shared_task(name="follow_send_payment")
|
||||
def follow_send_payment(lnpayment):
|
||||
'''Sends sats to buyer, continuous update'''
|
||||
"""Sends sats to buyer, continuous update"""
|
||||
|
||||
from decouple import config
|
||||
from base64 import b64decode
|
||||
@ -44,60 +46,77 @@ def follow_send_payment(lnpayment):
|
||||
from api.lightning.node import LNNode, MACAROON
|
||||
from api.models import LNPayment, Order
|
||||
|
||||
fee_limit_sat = int(max(lnpayment.num_satoshis * float(config('PROPORTIONAL_ROUTING_FEE_LIMIT')), float(config('MIN_FLAT_ROUTING_FEE_LIMIT')))) # 200 ppm or 10 sats
|
||||
fee_limit_sat = int(
|
||||
max(
|
||||
lnpayment.num_satoshis *
|
||||
float(config("PROPORTIONAL_ROUTING_FEE_LIMIT")),
|
||||
float(config("MIN_FLAT_ROUTING_FEE_LIMIT")),
|
||||
)) # 200 ppm or 10 sats
|
||||
request = LNNode.routerrpc.SendPaymentRequest(
|
||||
payment_request=lnpayment.invoice,
|
||||
fee_limit_sat=fee_limit_sat,
|
||||
timeout_seconds=60) # time out payment in 60 seconds
|
||||
timeout_seconds=60,
|
||||
) # time out payment in 60 seconds
|
||||
|
||||
order = lnpayment.order_paid
|
||||
try:
|
||||
for response in LNNode.routerstub.SendPaymentV2(request, metadata=[('macaroon', MACAROON.hex())]):
|
||||
if response.status == 0 : # Status 0 'UNKNOWN'
|
||||
for response in LNNode.routerstub.SendPaymentV2(request,
|
||||
metadata=[
|
||||
("macaroon",
|
||||
MACAROON.hex())
|
||||
]):
|
||||
if response.status == 0: # Status 0 'UNKNOWN'
|
||||
# Not sure when this status happens
|
||||
pass
|
||||
pass
|
||||
|
||||
if response.status == 1 : # Status 1 'IN_FLIGHT'
|
||||
print('IN_FLIGHT')
|
||||
if response.status == 1: # Status 1 'IN_FLIGHT'
|
||||
print("IN_FLIGHT")
|
||||
lnpayment.status = LNPayment.Status.FLIGHT
|
||||
lnpayment.save()
|
||||
order.status = Order.Status.PAY
|
||||
order.save()
|
||||
|
||||
if response.status == 3 : # Status 3 'FAILED'
|
||||
print('FAILED')
|
||||
if response.status == 3: # Status 3 'FAILED'
|
||||
print("FAILED")
|
||||
lnpayment.status = LNPayment.Status.FAILRO
|
||||
lnpayment.last_routing_time = timezone.now()
|
||||
lnpayment.routing_attempts += 1
|
||||
lnpayment.save()
|
||||
order.status = Order.Status.FAI
|
||||
order.expires_at = timezone.now() + timedelta(seconds=Order.t_to_expire[Order.Status.FAI])
|
||||
order.expires_at = timezone.now() + timedelta(
|
||||
seconds=Order.t_to_expire[Order.Status.FAI])
|
||||
order.save()
|
||||
context = {'routing_failed': LNNode.payment_failure_context[response.failure_reason]}
|
||||
context = {
|
||||
"routing_failed":
|
||||
LNNode.payment_failure_context[response.failure_reason]
|
||||
}
|
||||
print(context)
|
||||
return False, context
|
||||
|
||||
if response.status == 2 : # Status 2 'SUCCEEDED'
|
||||
print('SUCCEEDED')
|
||||
if response.status == 2: # Status 2 'SUCCEEDED'
|
||||
print("SUCCEEDED")
|
||||
lnpayment.status = LNPayment.Status.SUCCED
|
||||
lnpayment.save()
|
||||
order.status = Order.Status.SUC
|
||||
order.expires_at = timezone.now() + timedelta(seconds=Order.t_to_expire[Order.Status.SUC])
|
||||
order.expires_at = timezone.now() + timedelta(
|
||||
seconds=Order.t_to_expire[Order.Status.SUC])
|
||||
order.save()
|
||||
return True, None
|
||||
|
||||
except Exception as e:
|
||||
if "invoice expired" in str(e):
|
||||
print('INVOICE EXPIRED')
|
||||
print("INVOICE EXPIRED")
|
||||
lnpayment.status = LNPayment.Status.EXPIRE
|
||||
lnpayment.last_routing_time = timezone.now()
|
||||
lnpayment.save()
|
||||
order.status = Order.Status.FAI
|
||||
order.expires_at = timezone.now() + timedelta(seconds=Order.t_to_expire[Order.Status.FAI])
|
||||
order.expires_at = timezone.now() + timedelta(
|
||||
seconds=Order.t_to_expire[Order.Status.FAI])
|
||||
order.save()
|
||||
context = {'routing_failed':'The payout invoice has expired'}
|
||||
context = {"routing_failed": "The payout invoice has expired"}
|
||||
return False, context
|
||||
|
||||
|
||||
@shared_task(name="cache_external_market_prices", ignore_result=True)
|
||||
def cache_market():
|
||||
|
||||
@ -110,23 +129,26 @@ def cache_market():
|
||||
exchange_rates = get_exchange_rates(currency_codes)
|
||||
|
||||
results = {}
|
||||
for i in range(len(Currency.currency_dict.values())): # currecies are indexed starting at 1 (USD)
|
||||
for i in range(len(Currency.currency_dict.values())
|
||||
): # currecies are indexed starting at 1 (USD)
|
||||
|
||||
rate = exchange_rates[i]
|
||||
rate = exchange_rates[i]
|
||||
results[i] = {currency_codes[i], rate}
|
||||
|
||||
# Do not update if no new rate was found
|
||||
if str(rate) == 'nan': continue
|
||||
if str(rate) == "nan":
|
||||
continue
|
||||
|
||||
# Create / Update database cached prices
|
||||
currency_key = list(Currency.currency_dict.keys())[i]
|
||||
Currency.objects.update_or_create(
|
||||
id = int(currency_key),
|
||||
currency = int(currency_key),
|
||||
id=int(currency_key),
|
||||
currency=int(currency_key),
|
||||
# if there is a Cached market prices matching that id, it updates it with defaults below
|
||||
defaults = {
|
||||
'exchange_rate': float(rate),
|
||||
'timestamp': timezone.now(),
|
||||
})
|
||||
defaults={
|
||||
"exchange_rate": float(rate),
|
||||
"timestamp": timezone.now(),
|
||||
},
|
||||
)
|
||||
|
||||
return results
|
||||
return results
|
||||
|
18
api/urls.py
18
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()),
|
||||
]
|
||||
|
51
api/utils.py
51
api/utils.py
@ -1,4 +1,3 @@
|
||||
|
||||
import requests, ring, os
|
||||
from decouple import config
|
||||
import numpy as np
|
||||
@ -7,35 +6,39 @@ from api.models import Order
|
||||
|
||||
market_cache = {}
|
||||
|
||||
@ring.dict(market_cache, expire=3) #keeps in cache for 3 seconds
|
||||
|
||||
@ring.dict(market_cache, expire=3) # keeps in cache for 3 seconds
|
||||
def get_exchange_rates(currencies):
|
||||
'''
|
||||
"""
|
||||
Params: list of currency codes.
|
||||
Checks for exchange rates in several public APIs.
|
||||
Returns the median price list.
|
||||
'''
|
||||
"""
|
||||
|
||||
APIS = config('MARKET_PRICE_APIS', cast=lambda v: [s.strip() for s in v.split(',')])
|
||||
APIS = config("MARKET_PRICE_APIS",
|
||||
cast=lambda v: [s.strip() for s in v.split(",")])
|
||||
|
||||
api_rates = []
|
||||
for api_url in APIS:
|
||||
try: # If one API is unavailable pass
|
||||
if 'blockchain.info' in api_url:
|
||||
try: # If one API is unavailable pass
|
||||
if "blockchain.info" in api_url:
|
||||
blockchain_prices = requests.get(api_url).json()
|
||||
blockchain_rates = []
|
||||
for currency in currencies:
|
||||
try: # If a currency is missing place a None
|
||||
blockchain_rates.append(float(blockchain_prices[currency]['last']))
|
||||
try: # If a currency is missing place a None
|
||||
blockchain_rates.append(
|
||||
float(blockchain_prices[currency]["last"]))
|
||||
except:
|
||||
blockchain_rates.append(np.nan)
|
||||
api_rates.append(blockchain_rates)
|
||||
|
||||
elif 'yadio.io' in api_url:
|
||||
elif "yadio.io" in api_url:
|
||||
yadio_prices = requests.get(api_url).json()
|
||||
yadio_rates = []
|
||||
for currency in currencies:
|
||||
try:
|
||||
yadio_rates.append(float(yadio_prices['BTC'][currency]))
|
||||
yadio_rates.append(float(
|
||||
yadio_prices["BTC"][currency]))
|
||||
except:
|
||||
yadio_rates.append(np.nan)
|
||||
api_rates.append(yadio_rates)
|
||||
@ -43,32 +46,36 @@ def get_exchange_rates(currencies):
|
||||
pass
|
||||
|
||||
if len(api_rates) == 0:
|
||||
return None # Wops there is not API available!
|
||||
return None # Wops there is not API available!
|
||||
|
||||
exchange_rates = np.array(api_rates)
|
||||
median_rates = np.nanmedian(exchange_rates, axis=0)
|
||||
|
||||
return median_rates.tolist()
|
||||
|
||||
|
||||
def get_lnd_version():
|
||||
|
||||
# If dockerized, return LND_VERSION envvar used for docker image.
|
||||
# Otherwise it would require LND's version.grpc libraries...
|
||||
try:
|
||||
lnd_version = config('LND_VERSION')
|
||||
lnd_version = config("LND_VERSION")
|
||||
return lnd_version
|
||||
except:
|
||||
pass
|
||||
|
||||
# If not dockerized and LND is local, read from CLI
|
||||
try:
|
||||
stream = os.popen('lnd --version')
|
||||
stream = os.popen("lnd --version")
|
||||
lnd_version = stream.read()[:-1]
|
||||
return lnd_version
|
||||
except:
|
||||
return ''
|
||||
return ""
|
||||
|
||||
|
||||
robosats_commit_cache = {}
|
||||
|
||||
|
||||
@ring.dict(robosats_commit_cache, expire=3600)
|
||||
def get_commit_robosats():
|
||||
|
||||
@ -77,11 +84,15 @@ def get_commit_robosats():
|
||||
|
||||
return commit_hash
|
||||
|
||||
|
||||
premium_percentile = {}
|
||||
|
||||
|
||||
@ring.dict(premium_percentile, expire=300)
|
||||
def compute_premium_percentile(order):
|
||||
|
||||
queryset = Order.objects.filter(currency=order.currency, status=Order.Status.PUB).exclude(id=order.id)
|
||||
queryset = Order.objects.filter(
|
||||
currency=order.currency, status=Order.Status.PUB).exclude(id=order.id)
|
||||
|
||||
print(len(queryset))
|
||||
if len(queryset) <= 1:
|
||||
@ -90,8 +101,8 @@ def compute_premium_percentile(order):
|
||||
order_rate = float(order.last_satoshis) / float(order.amount)
|
||||
rates = []
|
||||
for similar_order in queryset:
|
||||
rates.append(float(similar_order.last_satoshis) / float(similar_order.amount))
|
||||
|
||||
rates.append(
|
||||
float(similar_order.last_satoshis) / float(similar_order.amount))
|
||||
|
||||
rates = np.array(rates)
|
||||
return round(np.sum(rates < order_rate) / len(rates),2)
|
||||
|
||||
return round(np.sum(rates < order_rate) / len(rates), 2)
|
||||
|
560
api/views.py
560
api/views.py
@ -26,37 +26,46 @@ from django.utils import timezone
|
||||
from django.conf import settings
|
||||
from decouple import config
|
||||
|
||||
EXP_MAKER_BOND_INVOICE = int(config('EXP_MAKER_BOND_INVOICE'))
|
||||
FEE = float(config('FEE'))
|
||||
RETRY_TIME = int(config('RETRY_TIME'))
|
||||
|
||||
EXP_MAKER_BOND_INVOICE = int(config("EXP_MAKER_BOND_INVOICE"))
|
||||
FEE = float(config("FEE"))
|
||||
RETRY_TIME = int(config("RETRY_TIME"))
|
||||
|
||||
avatar_path = Path(settings.AVATAR_ROOT)
|
||||
avatar_path.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Create your views here.
|
||||
|
||||
class MakerView(CreateAPIView):
|
||||
serializer_class = MakeOrderSerializer
|
||||
|
||||
def post(self,request):
|
||||
class MakerView(CreateAPIView):
|
||||
serializer_class = MakeOrderSerializer
|
||||
|
||||
def post(self, request):
|
||||
serializer = self.serializer_class(data=request.data)
|
||||
|
||||
if not request.user.is_authenticated:
|
||||
return Response({'bad_request':'Woops! It seems you do not have a robot avatar'}, status.HTTP_400_BAD_REQUEST)
|
||||
return Response(
|
||||
{
|
||||
"bad_request":
|
||||
"Woops! It seems you do not have a robot avatar"
|
||||
},
|
||||
status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
if not serializer.is_valid(): return Response(status=status.HTTP_400_BAD_REQUEST)
|
||||
if not serializer.is_valid():
|
||||
return Response(status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
type = serializer.data.get('type')
|
||||
currency = serializer.data.get('currency')
|
||||
amount = serializer.data.get('amount')
|
||||
payment_method = serializer.data.get('payment_method')
|
||||
premium = serializer.data.get('premium')
|
||||
satoshis = serializer.data.get('satoshis')
|
||||
is_explicit = serializer.data.get('is_explicit')
|
||||
type = serializer.data.get("type")
|
||||
currency = serializer.data.get("currency")
|
||||
amount = serializer.data.get("amount")
|
||||
payment_method = serializer.data.get("payment_method")
|
||||
premium = serializer.data.get("premium")
|
||||
satoshis = serializer.data.get("satoshis")
|
||||
is_explicit = serializer.data.get("is_explicit")
|
||||
|
||||
valid, context, _ = Logics.validate_already_maker_or_taker(request.user)
|
||||
if not valid: return Response(context, status.HTTP_409_CONFLICT)
|
||||
valid, context, _ = Logics.validate_already_maker_or_taker(
|
||||
request.user)
|
||||
if not valid:
|
||||
return Response(context, status.HTTP_409_CONFLICT)
|
||||
|
||||
# Creates a new order
|
||||
order = Order(
|
||||
@ -67,67 +76,93 @@ class MakerView(CreateAPIView):
|
||||
premium=premium,
|
||||
satoshis=satoshis,
|
||||
is_explicit=is_explicit,
|
||||
expires_at=timezone.now()+timedelta(seconds=EXP_MAKER_BOND_INVOICE), # TODO Move to class method
|
||||
maker=request.user)
|
||||
|
||||
expires_at=timezone.now() + timedelta(
|
||||
seconds=EXP_MAKER_BOND_INVOICE), # TODO Move to class method
|
||||
maker=request.user,
|
||||
)
|
||||
|
||||
# TODO move to Order class method when new instance is created!
|
||||
order.last_satoshis = order.t0_satoshis = Logics.satoshis_now(order)
|
||||
|
||||
valid, context = Logics.validate_order_size(order)
|
||||
if not valid: return Response(context, status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
if not valid:
|
||||
return Response(context, status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
order.save()
|
||||
return Response(ListOrderSerializer(order).data, status=status.HTTP_201_CREATED)
|
||||
return Response(ListOrderSerializer(order).data,
|
||||
status=status.HTTP_201_CREATED)
|
||||
|
||||
|
||||
class OrderView(viewsets.ViewSet):
|
||||
serializer_class = UpdateOrderSerializer
|
||||
lookup_url_kwarg = 'order_id'
|
||||
lookup_url_kwarg = "order_id"
|
||||
|
||||
def get(self, request, format=None):
|
||||
'''
|
||||
"""
|
||||
Full trade pipeline takes place while looking/refreshing the order page.
|
||||
'''
|
||||
"""
|
||||
order_id = request.GET.get(self.lookup_url_kwarg)
|
||||
|
||||
if not request.user.is_authenticated:
|
||||
return Response({'bad_request':'You must have a robot avatar to see the order details'}, status=status.HTTP_400_BAD_REQUEST)
|
||||
return Response(
|
||||
{
|
||||
"bad_request":
|
||||
"You must have a robot avatar to see the order details"
|
||||
},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
if order_id == None:
|
||||
return Response({'bad_request':'Order ID parameter not found in request'}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
return Response(
|
||||
{"bad_request": "Order ID parameter not found in request"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
order = Order.objects.filter(id=order_id)
|
||||
|
||||
# check if exactly one order is found in the db
|
||||
if len(order) != 1 :
|
||||
return Response({'bad_request':'Invalid Order Id'}, status.HTTP_404_NOT_FOUND)
|
||||
|
||||
if len(order) != 1:
|
||||
return Response({"bad_request": "Invalid Order Id"},
|
||||
status.HTTP_404_NOT_FOUND)
|
||||
|
||||
# This is our order.
|
||||
order = order[0]
|
||||
|
||||
# 2) If order has been cancelled
|
||||
if order.status == Order.Status.UCA:
|
||||
return Response({'bad_request':'This order has been cancelled by the maker'},status.HTTP_400_BAD_REQUEST)
|
||||
return Response(
|
||||
{"bad_request": "This order has been cancelled by the maker"},
|
||||
status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
if order.status == Order.Status.CCA:
|
||||
return Response({'bad_request':'This order has been cancelled collaborativelly'},status.HTTP_400_BAD_REQUEST)
|
||||
return Response(
|
||||
{
|
||||
"bad_request":
|
||||
"This order has been cancelled collaborativelly"
|
||||
},
|
||||
status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
data = ListOrderSerializer(order).data
|
||||
data['total_secs_exp'] = Order.t_to_expire[order.status]
|
||||
data["total_secs_exp"] = Order.t_to_expire[order.status]
|
||||
|
||||
# if user is under a limit (penalty), inform him.
|
||||
is_penalized, time_out = Logics.is_penalized(request.user)
|
||||
if is_penalized:
|
||||
data['penalty'] = request.user.profile.penalty_expiration
|
||||
data["penalty"] = request.user.profile.penalty_expiration
|
||||
|
||||
# Add booleans if user is maker, taker, partipant, buyer or seller
|
||||
data['is_maker'] = order.maker == request.user
|
||||
data['is_taker'] = order.taker == request.user
|
||||
data['is_participant'] = data['is_maker'] or data['is_taker']
|
||||
|
||||
data["is_maker"] = order.maker == request.user
|
||||
data["is_taker"] = order.taker == request.user
|
||||
data["is_participant"] = data["is_maker"] or data["is_taker"]
|
||||
|
||||
# 3.a) If not a participant and order is not public, forbid.
|
||||
if not data['is_participant'] and order.status != Order.Status.PUB:
|
||||
return Response({'bad_request':'You are not allowed to see this order'},status.HTTP_403_FORBIDDEN)
|
||||
|
||||
if not data["is_participant"] and order.status != Order.Status.PUB:
|
||||
return Response(
|
||||
{"bad_request": "You are not allowed to see this order"},
|
||||
status.HTTP_403_FORBIDDEN,
|
||||
)
|
||||
|
||||
# WRITE Update last_seen for maker and taker.
|
||||
# Note down that the taker/maker was here recently, so counterpart knows if the user is paying attention.
|
||||
if order.maker == request.user:
|
||||
@ -139,93 +174,108 @@ class OrderView(viewsets.ViewSet):
|
||||
|
||||
# Add activity status of participants based on last_seen
|
||||
if order.taker_last_seen != None:
|
||||
data['taker_status'] = Logics.user_activity_status(order.taker_last_seen)
|
||||
data["taker_status"] = Logics.user_activity_status(
|
||||
order.taker_last_seen)
|
||||
if order.maker_last_seen != None:
|
||||
data['maker_status'] = Logics.user_activity_status(order.maker_last_seen)
|
||||
data["maker_status"] = Logics.user_activity_status(
|
||||
order.maker_last_seen)
|
||||
|
||||
# 3.b If order is between public and WF2
|
||||
if order.status >= Order.Status.PUB and order.status < Order.Status.WF2:
|
||||
data['price_now'], data['premium_now'] = Logics.price_and_premium_now(order)
|
||||
data["price_now"], data[
|
||||
"premium_now"] = Logics.price_and_premium_now(order)
|
||||
|
||||
# 3. c) If maker and Public, add num robots in book, premium percentile and num similar orders.
|
||||
if data["is_maker"] and order.status == Order.Status.PUB:
|
||||
data["premium_percentile"] = compute_premium_percentile(order)
|
||||
data["num_similar_orders"] = len(
|
||||
Order.objects.filter(currency=order.currency,
|
||||
status=Order.Status.PUB))
|
||||
|
||||
# 3. c) If maker and Public, add num robots in book, premium percentile and num similar orders.
|
||||
if data['is_maker'] and order.status == Order.Status.PUB:
|
||||
data['premium_percentile'] = compute_premium_percentile(order)
|
||||
data['num_similar_orders'] = len(Order.objects.filter(currency=order.currency, status=Order.Status.PUB))
|
||||
|
||||
# 4) Non participants can view details (but only if PUB)
|
||||
elif not data['is_participant'] and order.status != Order.Status.PUB:
|
||||
return Response(data, status=status.HTTP_200_OK)
|
||||
elif not data["is_participant"] and order.status != Order.Status.PUB:
|
||||
return Response(data, status=status.HTTP_200_OK)
|
||||
|
||||
# For participants add positions, nicks and status as a message and hold invoices status
|
||||
data['is_buyer'] = Logics.is_buyer(order,request.user)
|
||||
data['is_seller'] = Logics.is_seller(order,request.user)
|
||||
data['maker_nick'] = str(order.maker)
|
||||
data['taker_nick'] = str(order.taker)
|
||||
data['status_message'] = Order.Status(order.status).label
|
||||
data['is_fiat_sent'] = order.is_fiat_sent
|
||||
data['is_disputed'] = order.is_disputed
|
||||
data['ur_nick'] = request.user.username
|
||||
data["is_buyer"] = Logics.is_buyer(order, request.user)
|
||||
data["is_seller"] = Logics.is_seller(order, request.user)
|
||||
data["maker_nick"] = str(order.maker)
|
||||
data["taker_nick"] = str(order.taker)
|
||||
data["status_message"] = Order.Status(order.status).label
|
||||
data["is_fiat_sent"] = order.is_fiat_sent
|
||||
data["is_disputed"] = order.is_disputed
|
||||
data["ur_nick"] = request.user.username
|
||||
|
||||
# Add whether hold invoices are LOCKED (ACCEPTED)
|
||||
# Is there a maker bond? If so, True if locked, False otherwise
|
||||
if order.maker_bond:
|
||||
data['maker_locked'] = order.maker_bond.status == LNPayment.Status.LOCKED
|
||||
data[
|
||||
"maker_locked"] = order.maker_bond.status == LNPayment.Status.LOCKED
|
||||
else:
|
||||
data['maker_locked'] = False
|
||||
data["maker_locked"] = False
|
||||
|
||||
# Is there a taker bond? If so, True if locked, False otherwise
|
||||
if order.taker_bond:
|
||||
data['taker_locked'] = order.taker_bond.status == LNPayment.Status.LOCKED
|
||||
data[
|
||||
"taker_locked"] = order.taker_bond.status == LNPayment.Status.LOCKED
|
||||
else:
|
||||
data['taker_locked'] = False
|
||||
data["taker_locked"] = False
|
||||
|
||||
# Is there an escrow? If so, True if locked, False otherwise
|
||||
if order.trade_escrow:
|
||||
data['escrow_locked'] = order.trade_escrow.status == LNPayment.Status.LOCKED
|
||||
data[
|
||||
"escrow_locked"] = order.trade_escrow.status == LNPayment.Status.LOCKED
|
||||
else:
|
||||
data['escrow_locked'] = False
|
||||
data["escrow_locked"] = False
|
||||
|
||||
# If both bonds are locked, participants can see the final trade amount in sats.
|
||||
if order.taker_bond:
|
||||
if order.maker_bond.status == order.taker_bond.status == LNPayment.Status.LOCKED:
|
||||
if (order.maker_bond.status == order.taker_bond.status ==
|
||||
LNPayment.Status.LOCKED):
|
||||
# Seller sees the amount he sends
|
||||
if data['is_seller']:
|
||||
data['trade_satoshis'] = order.last_satoshis
|
||||
if data["is_seller"]:
|
||||
data["trade_satoshis"] = order.last_satoshis
|
||||
# Buyer sees the amount he receives
|
||||
elif data['is_buyer']:
|
||||
data['trade_satoshis'] = Logics.payout_amount(order, request.user)[1]['invoice_amount']
|
||||
elif data["is_buyer"]:
|
||||
data["trade_satoshis"] = Logics.payout_amount(
|
||||
order, request.user)[1]["invoice_amount"]
|
||||
|
||||
# 5) If status is 'waiting for maker bond' and user is MAKER, reply with a MAKER hold invoice.
|
||||
if order.status == Order.Status.WFB and data['is_maker']:
|
||||
if order.status == Order.Status.WFB and data["is_maker"]:
|
||||
valid, context = Logics.gen_maker_hold_invoice(order, request.user)
|
||||
if valid:
|
||||
data = {**data, **context}
|
||||
else:
|
||||
return Response(context, status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
|
||||
# 6) If status is 'waiting for taker bond' and user is TAKER, reply with a TAKER hold invoice.
|
||||
elif order.status == Order.Status.TAK and data['is_taker']:
|
||||
elif order.status == Order.Status.TAK and data["is_taker"]:
|
||||
valid, context = Logics.gen_taker_hold_invoice(order, request.user)
|
||||
if valid:
|
||||
data = {**data, **context}
|
||||
else:
|
||||
return Response(context, status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
# 7 a. ) If seller and status is 'WF2' or 'WFE'
|
||||
elif data['is_seller'] and (order.status == Order.Status.WF2 or order.status == Order.Status.WFE):
|
||||
|
||||
# 7 a. ) If seller and status is 'WF2' or 'WFE'
|
||||
elif data["is_seller"] and (order.status == Order.Status.WF2
|
||||
or order.status == Order.Status.WFE):
|
||||
# If the two bonds are locked, reply with an ESCROW hold invoice.
|
||||
if order.maker_bond.status == order.taker_bond.status == LNPayment.Status.LOCKED:
|
||||
valid, context = Logics.gen_escrow_hold_invoice(order, request.user)
|
||||
if (order.maker_bond.status == order.taker_bond.status ==
|
||||
LNPayment.Status.LOCKED):
|
||||
valid, context = Logics.gen_escrow_hold_invoice(
|
||||
order, request.user)
|
||||
if valid:
|
||||
data = {**data, **context}
|
||||
else:
|
||||
return Response(context, status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
# 7.b) If user is Buyer and status is 'WF2' or 'WFI'
|
||||
elif data['is_buyer'] and (order.status == Order.Status.WF2 or order.status == Order.Status.WFI):
|
||||
# 7.b) If user is Buyer and status is 'WF2' or 'WFI'
|
||||
elif data["is_buyer"] and (order.status == Order.Status.WF2
|
||||
or order.status == Order.Status.WFI):
|
||||
|
||||
# If the two bonds are locked, reply with an AMOUNT so he can send the buyer invoice.
|
||||
if order.maker_bond.status == order.taker_bond.status == LNPayment.Status.LOCKED:
|
||||
if (order.maker_bond.status == order.taker_bond.status ==
|
||||
LNPayment.Status.LOCKED):
|
||||
valid, context = Logics.payout_amount(order, request.user)
|
||||
if valid:
|
||||
data = {**data, **context}
|
||||
@ -233,148 +283,185 @@ class OrderView(viewsets.ViewSet):
|
||||
return Response(context, status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
# 8) If status is 'CHA' or 'FSE' and all HTLCS are in LOCKED
|
||||
elif order.status in [Order.Status.WFI, Order.Status.CHA, Order.Status.FSE]:
|
||||
elif order.status in [
|
||||
Order.Status.WFI, Order.Status.CHA, Order.Status.FSE
|
||||
]:
|
||||
# If all bonds are locked.
|
||||
if order.maker_bond.status == order.taker_bond.status == order.trade_escrow.status == LNPayment.Status.LOCKED:
|
||||
if (order.maker_bond.status == order.taker_bond.status ==
|
||||
order.trade_escrow.status == LNPayment.Status.LOCKED):
|
||||
# add whether a collaborative cancel is pending or has been asked
|
||||
if (data['is_maker'] and order.taker_asked_cancel) or (data['is_taker'] and order.maker_asked_cancel):
|
||||
data['pending_cancel'] = True
|
||||
elif (data['is_maker'] and order.maker_asked_cancel) or (data['is_taker'] and order.taker_asked_cancel):
|
||||
data['asked_for_cancel'] = True
|
||||
if (data["is_maker"] and order.taker_asked_cancel) or (
|
||||
data["is_taker"] and order.maker_asked_cancel):
|
||||
data["pending_cancel"] = True
|
||||
elif (data["is_maker"] and order.maker_asked_cancel) or (
|
||||
data["is_taker"] and order.taker_asked_cancel):
|
||||
data["asked_for_cancel"] = True
|
||||
else:
|
||||
data['asked_for_cancel'] = False
|
||||
data["asked_for_cancel"] = False
|
||||
|
||||
# 9) If status is 'DIS' and all HTLCS are in LOCKED
|
||||
elif order.status == Order.Status.DIS:
|
||||
|
||||
# add whether the dispute statement has been received
|
||||
if data['is_maker']:
|
||||
data['statement_submitted'] = (order.maker_statement != None and order.maker_statement != "")
|
||||
elif data['is_taker']:
|
||||
data['statement_submitted'] = (order.taker_statement != None and order.maker_statement != "")
|
||||
if data["is_maker"]:
|
||||
data["statement_submitted"] = (order.maker_statement != None
|
||||
and order.maker_statement != "")
|
||||
elif data["is_taker"]:
|
||||
data["statement_submitted"] = (order.taker_statement != None
|
||||
and order.maker_statement != "")
|
||||
|
||||
# 9) If status is 'Failed routing', reply with retry amounts, time of next retry and ask for invoice at third.
|
||||
elif order.status == Order.Status.FAI and order.payout.receiver == request.user: # might not be the buyer if after a dispute where winner wins
|
||||
data['retries'] = order.payout.routing_attempts
|
||||
data['next_retry_time'] = order.payout.last_routing_time + timedelta(minutes=RETRY_TIME)
|
||||
elif (order.status == Order.Status.FAI
|
||||
and order.payout.receiver == request.user
|
||||
): # might not be the buyer if after a dispute where winner wins
|
||||
data["retries"] = order.payout.routing_attempts
|
||||
data[
|
||||
"next_retry_time"] = order.payout.last_routing_time + timedelta(
|
||||
minutes=RETRY_TIME)
|
||||
|
||||
if order.payout.status == LNPayment.Status.EXPIRE:
|
||||
data['invoice_expired'] = True
|
||||
data["invoice_expired"] = True
|
||||
# Add invoice amount once again if invoice was expired.
|
||||
data['invoice_amount'] = int(order.last_satoshis * (1-FEE))
|
||||
|
||||
data["invoice_amount"] = int(order.last_satoshis * (1 - FEE))
|
||||
|
||||
return Response(data, status.HTTP_200_OK)
|
||||
|
||||
def take_update_confirm_dispute_cancel(self, request, format=None):
|
||||
'''
|
||||
"""
|
||||
Here takes place all of the updates to the order object.
|
||||
That is: take, confim, cancel, dispute, update_invoice or rate.
|
||||
'''
|
||||
"""
|
||||
order_id = request.GET.get(self.lookup_url_kwarg)
|
||||
|
||||
serializer = UpdateOrderSerializer(data=request.data)
|
||||
if not serializer.is_valid(): return Response(status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
if not serializer.is_valid():
|
||||
return Response(status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
order = Order.objects.get(id=order_id)
|
||||
|
||||
# action is either 1)'take', 2)'confirm', 3)'cancel', 4)'dispute' , 5)'update_invoice'
|
||||
# action is either 1)'take', 2)'confirm', 3)'cancel', 4)'dispute' , 5)'update_invoice'
|
||||
# 6)'submit_statement' (in dispute), 7)'rate_user' , 'rate_platform'
|
||||
action = serializer.data.get('action')
|
||||
invoice = serializer.data.get('invoice')
|
||||
statement = serializer.data.get('statement')
|
||||
rating = serializer.data.get('rating')
|
||||
|
||||
action = serializer.data.get("action")
|
||||
invoice = serializer.data.get("invoice")
|
||||
statement = serializer.data.get("statement")
|
||||
rating = serializer.data.get("rating")
|
||||
|
||||
# 1) If action is take, it is a taker request!
|
||||
if action == 'take':
|
||||
if order.status == Order.Status.PUB:
|
||||
valid, context, _ = Logics.validate_already_maker_or_taker(request.user)
|
||||
if not valid: return Response(context, status=status.HTTP_409_CONFLICT)
|
||||
if action == "take":
|
||||
if order.status == Order.Status.PUB:
|
||||
valid, context, _ = Logics.validate_already_maker_or_taker(
|
||||
request.user)
|
||||
if not valid:
|
||||
return Response(context, status=status.HTTP_409_CONFLICT)
|
||||
valid, context = Logics.take(order, request.user)
|
||||
if not valid: return Response(context, status=status.HTTP_403_FORBIDDEN)
|
||||
if not valid:
|
||||
return Response(context, status=status.HTTP_403_FORBIDDEN)
|
||||
|
||||
return self.get(request)
|
||||
|
||||
else: Response({'bad_request':'This order is not public anymore.'}, status.HTTP_400_BAD_REQUEST)
|
||||
else:
|
||||
Response(
|
||||
{"bad_request": "This order is not public anymore."},
|
||||
status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
# Any other action is only allowed if the user is a participant
|
||||
if not (order.maker == request.user or order.taker == request.user):
|
||||
return Response({'bad_request':'You are not a participant in this order'}, status.HTTP_403_FORBIDDEN)
|
||||
return Response(
|
||||
{"bad_request": "You are not a participant in this order"},
|
||||
status.HTTP_403_FORBIDDEN,
|
||||
)
|
||||
|
||||
# 2) If action is 'update invoice'
|
||||
if action == 'update_invoice' and invoice:
|
||||
valid, context = Logics.update_invoice(order,request.user,invoice)
|
||||
if not valid: return Response(context, status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
if action == "update_invoice" and invoice:
|
||||
valid, context = Logics.update_invoice(order, request.user,
|
||||
invoice)
|
||||
if not valid:
|
||||
return Response(context, status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
# 3) If action is cancel
|
||||
elif action == 'cancel':
|
||||
valid, context = Logics.cancel_order(order,request.user)
|
||||
if not valid: return Response(context, status.HTTP_400_BAD_REQUEST)
|
||||
elif action == "cancel":
|
||||
valid, context = Logics.cancel_order(order, request.user)
|
||||
if not valid:
|
||||
return Response(context, status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
# 4) If action is confirm
|
||||
elif action == 'confirm':
|
||||
valid, context = Logics.confirm_fiat(order,request.user)
|
||||
if not valid: return Response(context, status.HTTP_400_BAD_REQUEST)
|
||||
elif action == "confirm":
|
||||
valid, context = Logics.confirm_fiat(order, request.user)
|
||||
if not valid:
|
||||
return Response(context, status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
# 5) If action is dispute
|
||||
elif action == 'dispute':
|
||||
valid, context = Logics.open_dispute(order,request.user)
|
||||
if not valid: return Response(context, status.HTTP_400_BAD_REQUEST)
|
||||
elif action == "dispute":
|
||||
valid, context = Logics.open_dispute(order, request.user)
|
||||
if not valid:
|
||||
return Response(context, status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
elif action == 'submit_statement':
|
||||
valid, context = Logics.dispute_statement(order,request.user, statement)
|
||||
if not valid: return Response(context, status.HTTP_400_BAD_REQUEST)
|
||||
elif action == "submit_statement":
|
||||
valid, context = Logics.dispute_statement(order, request.user,
|
||||
statement)
|
||||
if not valid:
|
||||
return Response(context, status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
# 6) If action is rate
|
||||
elif action == 'rate_user' and rating:
|
||||
valid, context = Logics.rate_counterparty(order,request.user, rating)
|
||||
if not valid: return Response(context, status.HTTP_400_BAD_REQUEST)
|
||||
elif action == "rate_user" and rating:
|
||||
valid, context = Logics.rate_counterparty(order, request.user,
|
||||
rating)
|
||||
if not valid:
|
||||
return Response(context, status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
# 6) If action is rate_platform
|
||||
elif action == 'rate_platform' and rating:
|
||||
elif action == "rate_platform" and rating:
|
||||
valid, context = Logics.rate_platform(request.user, rating)
|
||||
if not valid: return Response(context, status.HTTP_400_BAD_REQUEST)
|
||||
if not valid:
|
||||
return Response(context, status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
# If nothing of the above... something else is going on. Probably not allowed!
|
||||
else:
|
||||
return Response(
|
||||
{'bad_request':
|
||||
'The Robotic Satoshis working in the warehouse did not understand you. ' +
|
||||
'Please, fill a Bug Issue in Github https://github.com/reckless-satoshi/robosats/issues'},
|
||||
status.HTTP_501_NOT_IMPLEMENTED)
|
||||
{
|
||||
"bad_request":
|
||||
"The Robotic Satoshis working in the warehouse did not understand you. "
|
||||
+
|
||||
"Please, fill a Bug Issue in Github https://github.com/reckless-satoshi/robosats/issues"
|
||||
},
|
||||
status.HTTP_501_NOT_IMPLEMENTED,
|
||||
)
|
||||
|
||||
return self.get(request)
|
||||
|
||||
|
||||
class UserView(APIView):
|
||||
lookup_url_kwarg = 'token'
|
||||
NickGen = NickGenerator(
|
||||
lang='English',
|
||||
use_adv=False,
|
||||
use_adj=True,
|
||||
use_noun=True,
|
||||
max_num=999)
|
||||
lookup_url_kwarg = "token"
|
||||
NickGen = NickGenerator(lang="English",
|
||||
use_adv=False,
|
||||
use_adj=True,
|
||||
use_noun=True,
|
||||
max_num=999)
|
||||
|
||||
# Probably should be turned into a post method
|
||||
def get(self,request, format=None):
|
||||
'''
|
||||
def get(self, request, format=None):
|
||||
"""
|
||||
Get a new user derived from a high entropy token
|
||||
|
||||
|
||||
- Request has a high-entropy token,
|
||||
- Generates new nickname and avatar.
|
||||
- Creates login credentials (new User object)
|
||||
Response with Avatar and Nickname.
|
||||
'''
|
||||
"""
|
||||
|
||||
# If an existing user opens the main page by mistake, we do not want it to create a new nickname/profile for him
|
||||
if request.user.is_authenticated:
|
||||
context = {'nickname': request.user.username}
|
||||
not_participant, _, _ = Logics.validate_already_maker_or_taker(request.user)
|
||||
context = {"nickname": request.user.username}
|
||||
not_participant, _, _ = Logics.validate_already_maker_or_taker(
|
||||
request.user)
|
||||
|
||||
# Does not allow this 'mistake' if an active order
|
||||
if not not_participant:
|
||||
context['bad_request'] = f'You are already logged in as {request.user} and have an active order'
|
||||
context[
|
||||
"bad_request"] = f"You are already logged in as {request.user} and have an active order"
|
||||
return Response(context, status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
|
||||
# Does not allow this 'mistake' if the last login was sometime ago (5 minutes)
|
||||
# if request.user.last_login < timezone.now() - timedelta(minutes=5):
|
||||
# context['bad_request'] = f'You are already logged in as {request.user}'
|
||||
@ -387,35 +474,40 @@ class UserView(APIView):
|
||||
shannon_entropy = entropy(counts, base=62)
|
||||
bits_entropy = log2(len(value)**len(token))
|
||||
# Payload
|
||||
context = {'token_shannon_entropy': shannon_entropy, 'token_bits_entropy': bits_entropy}
|
||||
context = {
|
||||
"token_shannon_entropy": shannon_entropy,
|
||||
"token_bits_entropy": bits_entropy,
|
||||
}
|
||||
|
||||
# Deny user gen if entropy below 128 bits or 0.7 shannon heterogeneity
|
||||
if bits_entropy < 128 or shannon_entropy < 0.7:
|
||||
context['bad_request'] = 'The token does not have enough entropy'
|
||||
context["bad_request"] = "The token does not have enough entropy"
|
||||
return Response(context, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
# Hash the token, only 1 iteration.
|
||||
hash = hashlib.sha256(str.encode(token)).hexdigest()
|
||||
hash = hashlib.sha256(str.encode(token)).hexdigest()
|
||||
|
||||
# Generate nickname deterministically
|
||||
nickname = self.NickGen.short_from_SHA256(hash, max_length=18)[0]
|
||||
context['nickname'] = nickname
|
||||
nickname = self.NickGen.short_from_SHA256(hash, max_length=18)[0]
|
||||
context["nickname"] = nickname
|
||||
|
||||
# Generate avatar
|
||||
rh = Robohash(hash)
|
||||
rh.assemble(roboset='set1', bgset='any')# for backgrounds ON
|
||||
rh.assemble(roboset="set1", bgset="any") # for backgrounds ON
|
||||
|
||||
# Does not replace image if existing (avoid re-avatar in case of nick collusion)
|
||||
image_path = avatar_path.joinpath(nickname+".png")
|
||||
image_path = avatar_path.joinpath(nickname + ".png")
|
||||
if not image_path.exists():
|
||||
with open(image_path, "wb") as f:
|
||||
rh.img.save(f, format="png")
|
||||
|
||||
# Create new credentials and login if nickname is new
|
||||
if len(User.objects.filter(username=nickname)) == 0:
|
||||
User.objects.create_user(username=nickname, password=token, is_staff=False)
|
||||
User.objects.create_user(username=nickname,
|
||||
password=token,
|
||||
is_staff=False)
|
||||
user = authenticate(request, username=nickname, password=token)
|
||||
user.profile.avatar = "static/assets/avatars/" + nickname + '.png'
|
||||
user.profile.avatar = "static/assets/avatars/" + nickname + ".png"
|
||||
login(request, user)
|
||||
return Response(context, status=status.HTTP_201_CREATED)
|
||||
|
||||
@ -424,17 +516,19 @@ class UserView(APIView):
|
||||
if user is not None:
|
||||
login(request, user)
|
||||
# Sends the welcome back message, only if created +3 mins ago
|
||||
if request.user.date_joined < (timezone.now()-timedelta(minutes=3)):
|
||||
context['found'] = 'We found your Robot avatar. Welcome back!'
|
||||
if request.user.date_joined < (timezone.now() -
|
||||
timedelta(minutes=3)):
|
||||
context[
|
||||
"found"] = "We found your Robot avatar. Welcome back!"
|
||||
return Response(context, status=status.HTTP_202_ACCEPTED)
|
||||
else:
|
||||
# It is unlikely, but maybe the nickname is taken (1 in 20 Billion change)
|
||||
context['found'] = 'Bad luck, this nickname is taken'
|
||||
context['bad_request'] = 'Enter a different token'
|
||||
context["found"] = "Bad luck, this nickname is taken"
|
||||
context["bad_request"] = "Enter a different token"
|
||||
return Response(context, status.HTTP_403_FORBIDDEN)
|
||||
|
||||
def delete(self,request):
|
||||
''' Pressing "give me another" deletes the logged in user '''
|
||||
def delete(self, request):
|
||||
"""Pressing "give me another" deletes the logged in user"""
|
||||
user = request.user
|
||||
if not user.is_authenticated:
|
||||
return Response(status.HTTP_403_FORBIDDEN)
|
||||
@ -446,62 +540,95 @@ class UserView(APIView):
|
||||
# Check if it is not a maker or taker!
|
||||
not_participant, _, _ = Logics.validate_already_maker_or_taker(user)
|
||||
if not not_participant:
|
||||
return Response({'bad_request':'Maybe a mistake? User cannot be deleted while he is part of an order'}, status.HTTP_400_BAD_REQUEST)
|
||||
return Response(
|
||||
{
|
||||
"bad_request":
|
||||
"Maybe a mistake? User cannot be deleted while he is part of an order"
|
||||
},
|
||||
status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
# Check if has already a profile with
|
||||
if user.profile.total_contracts > 0:
|
||||
return Response({'bad_request':'Maybe a mistake? User cannot be deleted as it has completed trades'}, status.HTTP_400_BAD_REQUEST)
|
||||
return Response(
|
||||
{
|
||||
"bad_request":
|
||||
"Maybe a mistake? User cannot be deleted as it has completed trades"
|
||||
},
|
||||
status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
logout(request)
|
||||
user.delete()
|
||||
return Response({'user_deleted':'User deleted permanently'}, status.HTTP_301_MOVED_PERMANENTLY)
|
||||
return Response(
|
||||
{"user_deleted": "User deleted permanently"},
|
||||
status.HTTP_301_MOVED_PERMANENTLY,
|
||||
)
|
||||
|
||||
|
||||
class BookView(ListAPIView):
|
||||
serializer_class = ListOrderSerializer
|
||||
queryset = Order.objects.filter(status=Order.Status.PUB)
|
||||
queryset = Order.objects.filter(status=Order.Status.PUB)
|
||||
|
||||
def get(self, request, format=None):
|
||||
currency = request.GET.get("currency")
|
||||
type = request.GET.get("type")
|
||||
|
||||
def get(self,request, format=None):
|
||||
currency = request.GET.get('currency')
|
||||
type = request.GET.get('type')
|
||||
|
||||
queryset = Order.objects.filter(status=Order.Status.PUB)
|
||||
|
||||
# Currency 0 and type 2 are special cases treated as "ANY". (These are not really possible choices)
|
||||
if int(currency) == 0 and int(type) != 2:
|
||||
queryset = Order.objects.filter(type=type, status=Order.Status.PUB)
|
||||
queryset = Order.objects.filter(type=type, status=Order.Status.PUB)
|
||||
elif int(type) == 2 and int(currency) != 0:
|
||||
queryset = Order.objects.filter(currency=currency, status=Order.Status.PUB)
|
||||
queryset = Order.objects.filter(currency=currency,
|
||||
status=Order.Status.PUB)
|
||||
elif not (int(currency) == 0 and int(type) == 2):
|
||||
queryset = Order.objects.filter(currency=currency, type=type, status=Order.Status.PUB)
|
||||
queryset = Order.objects.filter(currency=currency,
|
||||
type=type,
|
||||
status=Order.Status.PUB)
|
||||
|
||||
if len(queryset)== 0:
|
||||
return Response({'not_found':'No orders found, be the first to make one'}, status=status.HTTP_404_NOT_FOUND)
|
||||
if len(queryset) == 0:
|
||||
return Response(
|
||||
{"not_found": "No orders found, be the first to make one"},
|
||||
status=status.HTTP_404_NOT_FOUND,
|
||||
)
|
||||
|
||||
book_data = []
|
||||
for order in queryset:
|
||||
data = ListOrderSerializer(order).data
|
||||
data['maker_nick'] = str(order.maker)
|
||||
|
||||
data["maker_nick"] = str(order.maker)
|
||||
|
||||
# Compute current premium for those orders that are explicitly priced.
|
||||
data['price'], data['premium'] = Logics.price_and_premium_now(order)
|
||||
data['maker_status'] = Logics.user_activity_status(order.maker_last_seen)
|
||||
for key in ('status','taker'): # Non participants should not see the status or who is the taker
|
||||
data["price"], data["premium"] = Logics.price_and_premium_now(
|
||||
order)
|
||||
data["maker_status"] = Logics.user_activity_status(
|
||||
order.maker_last_seen)
|
||||
for key in (
|
||||
"status",
|
||||
"taker",
|
||||
): # Non participants should not see the status or who is the taker
|
||||
del data[key]
|
||||
|
||||
|
||||
book_data.append(data)
|
||||
|
||||
|
||||
return Response(book_data, status=status.HTTP_200_OK)
|
||||
|
||||
|
||||
class InfoView(ListAPIView):
|
||||
|
||||
def get(self, request):
|
||||
context = {}
|
||||
|
||||
context['num_public_buy_orders'] = len(Order.objects.filter(type=Order.Types.BUY, status=Order.Status.PUB))
|
||||
context['num_public_sell_orders'] = len(Order.objects.filter(type=Order.Types.SELL, status=Order.Status.PUB))
|
||||
|
||||
context["num_public_buy_orders"] = len(
|
||||
Order.objects.filter(type=Order.Types.BUY,
|
||||
status=Order.Status.PUB))
|
||||
context["num_public_sell_orders"] = len(
|
||||
Order.objects.filter(type=Order.Types.SELL,
|
||||
status=Order.Status.PUB))
|
||||
|
||||
# Number of active users (logged in in last 30 minutes)
|
||||
today = datetime.today()
|
||||
context['active_robots_today'] = len(User.objects.filter(last_login__day=today.day))
|
||||
context["active_robots_today"] = len(
|
||||
User.objects.filter(last_login__day=today.day))
|
||||
|
||||
# Compute average premium and volume of today
|
||||
queryset = MarketTick.objects.filter(timestamp__day=today.day)
|
||||
@ -509,9 +636,9 @@ class InfoView(ListAPIView):
|
||||
weighted_premiums = []
|
||||
volumes = []
|
||||
for tick in queryset:
|
||||
weighted_premiums.append(tick.premium*tick.volume)
|
||||
weighted_premiums.append(tick.premium * tick.volume)
|
||||
volumes.append(tick.volume)
|
||||
|
||||
|
||||
total_volume = sum(volumes)
|
||||
# Avg_premium is the weighted average of the premiums by volume
|
||||
avg_premium = sum(weighted_premiums) / total_volume
|
||||
@ -524,28 +651,27 @@ class InfoView(ListAPIView):
|
||||
volume_settled = []
|
||||
for tick in queryset:
|
||||
volume_settled.append(tick.volume)
|
||||
lifetime_volume_settled = int(sum(volume_settled)*100000000)
|
||||
lifetime_volume_settled = int(sum(volume_settled) * 100000000)
|
||||
else:
|
||||
lifetime_volume_settled = 0
|
||||
|
||||
context['today_avg_nonkyc_btc_premium'] = round(avg_premium,2)
|
||||
context['today_total_volume'] = total_volume
|
||||
context['lifetime_satoshis_settled'] = lifetime_volume_settled
|
||||
context['lnd_version'] = get_lnd_version()
|
||||
context['robosats_running_commit_hash'] = get_commit_robosats()
|
||||
context['alternative_site'] = config('ALTERNATIVE_SITE')
|
||||
context['alternative_name'] = config('ALTERNATIVE_NAME')
|
||||
context['node_alias'] = config('NODE_ALIAS')
|
||||
context['node_id'] = config('NODE_ID')
|
||||
context['network'] = config('NETWORK')
|
||||
context['fee'] = FEE
|
||||
context['bond_size'] = float(config('BOND_SIZE'))
|
||||
context["today_avg_nonkyc_btc_premium"] = round(avg_premium, 2)
|
||||
context["today_total_volume"] = total_volume
|
||||
context["lifetime_satoshis_settled"] = lifetime_volume_settled
|
||||
context["lnd_version"] = get_lnd_version()
|
||||
context["robosats_running_commit_hash"] = get_commit_robosats()
|
||||
context["alternative_site"] = config("ALTERNATIVE_SITE")
|
||||
context["alternative_name"] = config("ALTERNATIVE_NAME")
|
||||
context["node_alias"] = config("NODE_ALIAS")
|
||||
context["node_id"] = config("NODE_ID")
|
||||
context["network"] = config("NETWORK")
|
||||
context["fee"] = FEE
|
||||
context["bond_size"] = float(config("BOND_SIZE"))
|
||||
if request.user.is_authenticated:
|
||||
context['nickname'] = request.user.username
|
||||
has_no_active_order, _, order = Logics.validate_already_maker_or_taker(request.user)
|
||||
context["nickname"] = request.user.username
|
||||
has_no_active_order, _, order = Logics.validate_already_maker_or_taker(
|
||||
request.user)
|
||||
if not has_no_active_order:
|
||||
context['active_order_id'] = order.id
|
||||
context["active_order_id"] = order.id
|
||||
|
||||
return Response(context, status.HTTP_200_OK)
|
||||
|
||||
|
||||
|
@ -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)
|
||||
|
||||
@ -21,49 +21,45 @@ class ChatRoomConsumer(AsyncWebsocketConsumer):
|
||||
# if not (Logics.is_buyer(order[0], self.user) or Logics.is_seller(order[0], self.user)):
|
||||
# print ("Outta this chat")
|
||||
# return False
|
||||
|
||||
await self.channel_layer.group_add(
|
||||
self.room_group_name,
|
||||
self.channel_name
|
||||
)
|
||||
|
||||
await self.channel_layer.group_add(self.room_group_name,
|
||||
self.channel_name)
|
||||
|
||||
await self.accept()
|
||||
|
||||
async def disconnect(self, close_code):
|
||||
await self.channel_layer.group_discard(
|
||||
self.room_group_name,
|
||||
self.channel_name
|
||||
)
|
||||
await self.channel_layer.group_discard(self.room_group_name,
|
||||
self.channel_name)
|
||||
|
||||
async def receive(self, text_data):
|
||||
text_data_json = json.loads(text_data)
|
||||
message = text_data_json['message']
|
||||
nick = text_data_json['nick']
|
||||
message = text_data_json["message"]
|
||||
nick = text_data_json["nick"]
|
||||
|
||||
await self.channel_layer.group_send(
|
||||
self.room_group_name,
|
||||
{
|
||||
'type': 'chatroom_message',
|
||||
'message': message,
|
||||
'nick': nick,
|
||||
}
|
||||
"type": "chatroom_message",
|
||||
"message": message,
|
||||
"nick": nick,
|
||||
},
|
||||
)
|
||||
|
||||
async def chatroom_message(self, event):
|
||||
message = event['message']
|
||||
nick = event['nick']
|
||||
message = event["message"]
|
||||
nick = event["nick"]
|
||||
|
||||
# Insert a white space in words longer than 22 characters.
|
||||
# Helps when messages overflow in a single line.
|
||||
words = message.split(' ')
|
||||
fix_message = ''
|
||||
words = message.split(" ")
|
||||
fix_message = ""
|
||||
for word in words:
|
||||
word = ' '.join(word[i:i+22] for i in range(0, len(word), 22))
|
||||
fix_message = fix_message +' '+ word
|
||||
word = " ".join(word[i:i + 22] for i in range(0, len(word), 22))
|
||||
fix_message = fix_message + " " + word
|
||||
|
||||
await self.send(text_data=json.dumps({
|
||||
'message': fix_message,
|
||||
'user_nick': nick,
|
||||
"message": fix_message,
|
||||
"user_nick": nick,
|
||||
}))
|
||||
|
||||
pass
|
||||
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()),
|
||||
]
|
||||
|
@ -5,4 +5,4 @@ from . import views
|
||||
# urlpatterns = [
|
||||
# path('', views.index, name='index'),
|
||||
# path('<str:order_id>/', views.room, name='order_chat'),
|
||||
# ]
|
||||
# ]
|
||||
|
@ -1,7 +1,6 @@
|
||||
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,133 +17,138 @@ from decouple import config
|
||||
# Build paths inside the project like this: BASE_DIR / 'subdir'.
|
||||
BASE_DIR = Path(__file__).resolve().parent.parent
|
||||
|
||||
STATIC_URL = '/static/'
|
||||
STATIC_URL = "/static/"
|
||||
|
||||
# Quick-start development settings - unsuitable for production
|
||||
# See https://docs.djangoproject.com/en/4.0/howto/deployment/checklist/
|
||||
|
||||
# SECURITY WARNING: keep the secret key used in production secret!
|
||||
SECRET_KEY = config('SECRET_KEY')
|
||||
SECRET_KEY = config("SECRET_KEY")
|
||||
|
||||
DEBUG = False
|
||||
STATIC_URL = 'static/'
|
||||
STATIC_ROOT ='/usr/src/static/'
|
||||
STATIC_URL = "static/"
|
||||
STATIC_ROOT = "/usr/src/static/"
|
||||
|
||||
# SECURITY WARNING: don't run with debug turned on in production!
|
||||
if os.environ.get('DEVELOPMENT'):
|
||||
if os.environ.get("DEVELOPMENT"):
|
||||
DEBUG = True
|
||||
STATIC_ROOT = 'frontend/static/'
|
||||
|
||||
AVATAR_ROOT = STATIC_ROOT + 'assets/avatars/'
|
||||
STATIC_ROOT = "frontend/static/"
|
||||
|
||||
ALLOWED_HOSTS = [config('HOST_NAME'),config('HOST_NAME2'),config('LOCAL_ALIAS'),'127.0.0.1']
|
||||
AVATAR_ROOT = STATIC_ROOT + "assets/avatars/"
|
||||
|
||||
ALLOWED_HOSTS = [
|
||||
config("HOST_NAME"),
|
||||
config("HOST_NAME2"),
|
||||
config("LOCAL_ALIAS"),
|
||||
"127.0.0.1",
|
||||
]
|
||||
|
||||
# Application definition
|
||||
|
||||
INSTALLED_APPS = [
|
||||
'django.contrib.admin',
|
||||
'django.contrib.auth',
|
||||
'django.contrib.contenttypes',
|
||||
'django.contrib.sessions',
|
||||
'django.contrib.messages',
|
||||
'django.contrib.staticfiles',
|
||||
'rest_framework',
|
||||
'channels',
|
||||
'django_celery_beat',
|
||||
'django_celery_results',
|
||||
'api',
|
||||
'chat',
|
||||
'frontend.apps.FrontendConfig',
|
||||
"django.contrib.admin",
|
||||
"django.contrib.auth",
|
||||
"django.contrib.contenttypes",
|
||||
"django.contrib.sessions",
|
||||
"django.contrib.messages",
|
||||
"django.contrib.staticfiles",
|
||||
"rest_framework",
|
||||
"channels",
|
||||
"django_celery_beat",
|
||||
"django_celery_results",
|
||||
"api",
|
||||
"chat",
|
||||
"frontend.apps.FrontendConfig",
|
||||
]
|
||||
from .celery.conf import *
|
||||
|
||||
MIDDLEWARE = [
|
||||
'django.middleware.security.SecurityMiddleware',
|
||||
'django.contrib.sessions.middleware.SessionMiddleware',
|
||||
'django.middleware.common.CommonMiddleware',
|
||||
'django.middleware.csrf.CsrfViewMiddleware',
|
||||
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
||||
'django.contrib.messages.middleware.MessageMiddleware',
|
||||
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
||||
"django.middleware.security.SecurityMiddleware",
|
||||
"django.contrib.sessions.middleware.SessionMiddleware",
|
||||
"django.middleware.common.CommonMiddleware",
|
||||
"django.middleware.csrf.CsrfViewMiddleware",
|
||||
"django.contrib.auth.middleware.AuthenticationMiddleware",
|
||||
"django.contrib.messages.middleware.MessageMiddleware",
|
||||
"django.middleware.clickjacking.XFrameOptionsMiddleware",
|
||||
]
|
||||
|
||||
ROOT_URLCONF = 'robosats.urls'
|
||||
ROOT_URLCONF = "robosats.urls"
|
||||
|
||||
TEMPLATES = [
|
||||
{
|
||||
'BACKEND': 'django.template.backends.django.DjangoTemplates',
|
||||
'DIRS': [],
|
||||
'APP_DIRS': True,
|
||||
'OPTIONS': {
|
||||
'context_processors': [
|
||||
'django.template.context_processors.debug',
|
||||
'django.template.context_processors.request',
|
||||
'django.contrib.auth.context_processors.auth',
|
||||
'django.contrib.messages.context_processors.messages',
|
||||
"BACKEND": "django.template.backends.django.DjangoTemplates",
|
||||
"DIRS": [],
|
||||
"APP_DIRS": True,
|
||||
"OPTIONS": {
|
||||
"context_processors": [
|
||||
"django.template.context_processors.debug",
|
||||
"django.template.context_processors.request",
|
||||
"django.contrib.auth.context_processors.auth",
|
||||
"django.contrib.messages.context_processors.messages",
|
||||
],
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
WSGI_APPLICATION = 'robosats.wsgi.application'
|
||||
|
||||
WSGI_APPLICATION = "robosats.wsgi.application"
|
||||
|
||||
# Database
|
||||
# https://docs.djangoproject.com/en/4.0/ref/settings/#databases
|
||||
|
||||
|
||||
DATABASES = {
|
||||
'default': {
|
||||
'ENGINE': 'django.db.backends.sqlite3',
|
||||
'NAME': '/usr/src/database/db.sqlite3',
|
||||
'OPTIONS': {
|
||||
'timeout': 20, # in seconds
|
||||
}
|
||||
}
|
||||
"default": {
|
||||
"ENGINE": "django.db.backends.sqlite3",
|
||||
"NAME": "/usr/src/database/db.sqlite3",
|
||||
"OPTIONS": {
|
||||
"timeout": 20, # in seconds
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
# Password validation
|
||||
# https://docs.djangoproject.com/en/4.0/ref/settings/#auth-password-validators
|
||||
|
||||
AUTH_PASSWORD_VALIDATORS = [
|
||||
{
|
||||
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
|
||||
"NAME":
|
||||
"django.contrib.auth.password_validation.UserAttributeSimilarityValidator",
|
||||
},
|
||||
{
|
||||
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
|
||||
"NAME":
|
||||
"django.contrib.auth.password_validation.MinimumLengthValidator",
|
||||
},
|
||||
{
|
||||
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
|
||||
"NAME":
|
||||
"django.contrib.auth.password_validation.CommonPasswordValidator",
|
||||
},
|
||||
{
|
||||
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
|
||||
"NAME":
|
||||
"django.contrib.auth.password_validation.NumericPasswordValidator",
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
# Internationalization
|
||||
# https://docs.djangoproject.com/en/4.0/topics/i18n/
|
||||
|
||||
LANGUAGE_CODE = 'en-us'
|
||||
LANGUAGE_CODE = "en-us"
|
||||
|
||||
TIME_ZONE = 'UTC'
|
||||
TIME_ZONE = "UTC"
|
||||
|
||||
USE_I18N = True
|
||||
|
||||
USE_TZ = True
|
||||
|
||||
|
||||
# Static files (CSS, JavaScript, Images)
|
||||
# https://docs.djangoproject.com/en/4.0/howto/static-files/
|
||||
|
||||
STATIC_URL = 'static/'
|
||||
STATIC_URL = "static/"
|
||||
ASGI_APPLICATION = "robosats.routing.application"
|
||||
|
||||
CHANNEL_LAYERS = {
|
||||
'default': {
|
||||
'BACKEND': 'channels_redis.core.RedisChannelLayer',
|
||||
'CONFIG': {
|
||||
"hosts": [config('REDIS_URL')],
|
||||
"default": {
|
||||
"BACKEND": "channels_redis.core.RedisChannelLayer",
|
||||
"CONFIG": {
|
||||
"hosts": [config("REDIS_URL")],
|
||||
},
|
||||
},
|
||||
}
|
||||
@ -151,14 +156,14 @@ CHANNEL_LAYERS = {
|
||||
CACHES = {
|
||||
"default": {
|
||||
"BACKEND": "django_redis.cache.RedisCache",
|
||||
"LOCATION": config('REDIS_URL'),
|
||||
"LOCATION": config("REDIS_URL"),
|
||||
"OPTIONS": {
|
||||
"CLIENT_CLASS": "django_redis.client.DefaultClient"
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
# Default primary key field type
|
||||
# https://docs.djangoproject.com/en/4.0/ref/settings/#default-auto-field
|
||||
|
||||
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
|
||||
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
|
||||
|
@ -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