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