Re-format all python code

This commit is contained in:
Reckless_Satoshi 2022-02-17 11:50:10 -08:00
parent f8f306101e
commit fc4ccd5281
No known key found for this signature in database
GPG Key ID: 9C4585B561315571
32 changed files with 1842 additions and 1094 deletions

View File

@ -7,55 +7,127 @@ from .models import Order, LNPayment, Profile, MarketTick, Currency
admin.site.unregister(Group)
admin.site.unregister(User)
class ProfileInline(admin.StackedInline):
model = Profile
can_delete = False
fields = ('avatar_tag',)
readonly_fields = ['avatar_tag']
can_delete = False
fields = ("avatar_tag", )
readonly_fields = ["avatar_tag"]
# extended users with avatars
@admin.register(User)
class EUserAdmin(UserAdmin):
inlines = [ProfileInline]
list_display = ('avatar_tag','id','username','last_login','date_joined','is_staff')
list_display_links = ('id','username')
ordering = ('-id',)
list_display = (
"avatar_tag",
"id",
"username",
"last_login",
"date_joined",
"is_staff",
)
list_display_links = ("id", "username")
ordering = ("-id", )
def avatar_tag(self, obj):
return obj.profile.avatar_tag()
@admin.register(Order)
class OrderAdmin(AdminChangeLinksMixin, admin.ModelAdmin):
list_display = ('id','type','maker_link','taker_link','status','amount','currency_link','t0_satoshis','is_disputed','is_fiat_sent','created_at','expires_at', 'payout_link','maker_bond_link','taker_bond_link','trade_escrow_link')
list_display_links = ('id','type')
change_links = ('maker','taker','currency','payout','maker_bond','taker_bond','trade_escrow')
list_filter = ('is_disputed','is_fiat_sent','type','currency','status')
list_display = (
"id",
"type",
"maker_link",
"taker_link",
"status",
"amount",
"currency_link",
"t0_satoshis",
"is_disputed",
"is_fiat_sent",
"created_at",
"expires_at",
"payout_link",
"maker_bond_link",
"taker_bond_link",
"trade_escrow_link",
)
list_display_links = ("id", "type")
change_links = (
"maker",
"taker",
"currency",
"payout",
"maker_bond",
"taker_bond",
"trade_escrow",
)
list_filter = ("is_disputed", "is_fiat_sent", "type", "currency", "status")
@admin.register(LNPayment)
class LNPaymentAdmin(AdminChangeLinksMixin, admin.ModelAdmin):
list_display = ('hash','concept','status','num_satoshis','type','expires_at','expiry_height','sender_link','receiver_link','order_made_link','order_taken_link','order_escrow_link','order_paid_link')
list_display_links = ('hash','concept')
change_links = ('sender','receiver','order_made','order_taken','order_escrow','order_paid')
list_filter = ('type','concept','status')
ordering = ('-expires_at',)
list_display = (
"hash",
"concept",
"status",
"num_satoshis",
"type",
"expires_at",
"expiry_height",
"sender_link",
"receiver_link",
"order_made_link",
"order_taken_link",
"order_escrow_link",
"order_paid_link",
)
list_display_links = ("hash", "concept")
change_links = (
"sender",
"receiver",
"order_made",
"order_taken",
"order_escrow",
"order_paid",
)
list_filter = ("type", "concept", "status")
ordering = ("-expires_at", )
@admin.register(Profile)
class UserProfileAdmin(AdminChangeLinksMixin, admin.ModelAdmin):
list_display = ('avatar_tag','id','user_link','total_contracts','platform_rating','total_ratings','avg_rating','num_disputes','lost_disputes')
list_display_links = ('avatar_tag','id')
change_links =['user']
readonly_fields = ['avatar_tag']
list_display = (
"avatar_tag",
"id",
"user_link",
"total_contracts",
"platform_rating",
"total_ratings",
"avg_rating",
"num_disputes",
"lost_disputes",
)
list_display_links = ("avatar_tag", "id")
change_links = ["user"]
readonly_fields = ["avatar_tag"]
@admin.register(Currency)
class CurrencieAdmin(admin.ModelAdmin):
list_display = ('id','currency','exchange_rate','timestamp')
list_display_links = ('id','currency')
readonly_fields = ('currency','exchange_rate','timestamp')
ordering = ('id',)
list_display = ("id", "currency", "exchange_rate", "timestamp")
list_display_links = ("id", "currency")
readonly_fields = ("currency", "exchange_rate", "timestamp")
ordering = ("id", )
@admin.register(MarketTick)
class MarketTickAdmin(admin.ModelAdmin):
list_display = ('timestamp','price','volume','premium','currency','fee')
readonly_fields = ('timestamp','price','volume','premium','currency','fee')
list_filter = ['currency']
ordering = ('-timestamp',)
list_display = ("timestamp", "price", "volume", "premium", "currency",
"fee")
readonly_fields = ("timestamp", "price", "volume", "premium", "currency",
"fee")
list_filter = ["currency"]
ordering = ("-timestamp", )

View File

@ -2,5 +2,5 @@ from django.apps import AppConfig
class ApiConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'api'
default_auto_field = "django.db.models.BigAutoField"
name = "api"

View File

@ -10,27 +10,30 @@ from datetime import timedelta, datetime
from django.utils import timezone
from api.models import LNPayment
#######
# Should work with LND (c-lightning in the future if there are features that deserve the work)
#######
# Read tls.cert from file or .env variable string encoded as base64
try:
CERT = open(os.path.join(config('LND_DIR'),'tls.cert'), 'rb').read()
CERT = open(os.path.join(config("LND_DIR"), "tls.cert"), "rb").read()
except:
CERT = b64decode(config('LND_CERT_BASE64'))
CERT = b64decode(config("LND_CERT_BASE64"))
# Read macaroon from file or .env variable string encoded as base64
try:
MACAROON = open(os.path.join(config('LND_DIR'), config('MACAROON_path')), 'rb').read()
MACAROON = open(os.path.join(config("LND_DIR"), config("MACAROON_path")),
"rb").read()
except:
MACAROON = b64decode(config('LND_MACAROON_BASE64'))
MACAROON = b64decode(config("LND_MACAROON_BASE64"))
LND_GRPC_HOST = config('LND_GRPC_HOST')
LND_GRPC_HOST = config("LND_GRPC_HOST")
class LNNode():
os.environ["GRPC_SSL_CIPHER_SUITES"] = 'HIGH+ECDSA'
class LNNode:
os.environ["GRPC_SSL_CIPHER_SUITES"] = "HIGH+ECDSA"
creds = grpc.ssl_channel_credentials(CERT)
channel = grpc.secure_channel(LND_GRPC_HOST, creds)
@ -44,89 +47,112 @@ class LNNode():
routerrpc = routerrpc
payment_failure_context = {
0: "Payment isn't failed (yet)",
1: "There are more routes to try, but the payment timeout was exceeded.",
2: "All possible routes were tried and failed permanently. Or were no routes to the destination at all.",
3: "A non-recoverable error has occured.",
4: "Payment details incorrect (unknown hash, invalid amt or invalid final cltv delta)",
5: "Insufficient local balance."}
0: "Payment isn't failed (yet)",
1:
"There are more routes to try, but the payment timeout was exceeded.",
2:
"All possible routes were tried and failed permanently. Or were no routes to the destination at all.",
3: "A non-recoverable error has occured.",
4:
"Payment details incorrect (unknown hash, invalid amt or invalid final cltv delta)",
5: "Insufficient local balance.",
}
@classmethod
def decode_payreq(cls, invoice):
'''Decodes a lightning payment request (invoice)'''
"""Decodes a lightning payment request (invoice)"""
request = lnrpc.PayReqString(pay_req=invoice)
response = cls.lightningstub.DecodePayReq(request, metadata=[('macaroon', MACAROON.hex())])
response = cls.lightningstub.DecodePayReq(request,
metadata=[("macaroon",
MACAROON.hex())])
return response
@classmethod
def cancel_return_hold_invoice(cls, payment_hash):
'''Cancels or returns a hold invoice'''
request = invoicesrpc.CancelInvoiceMsg(payment_hash=bytes.fromhex(payment_hash))
response = cls.invoicesstub.CancelInvoice(request, metadata=[('macaroon', MACAROON.hex())])
def cancel_return_hold_invoice(cls, payment_hash):
"""Cancels or returns a hold invoice"""
request = invoicesrpc.CancelInvoiceMsg(
payment_hash=bytes.fromhex(payment_hash))
response = cls.invoicesstub.CancelInvoice(request,
metadata=[("macaroon",
MACAROON.hex())])
# Fix this: tricky because canceling sucessfully an invoice has no response. TODO
return str(response) == "" # True if no response, false otherwise.
return str(response) == "" # True if no response, false otherwise.
@classmethod
def settle_hold_invoice(cls, preimage):
'''settles a hold invoice'''
request = invoicesrpc.SettleInvoiceMsg(preimage=bytes.fromhex(preimage))
response = cls.invoicesstub.SettleInvoice(request, metadata=[('macaroon', MACAROON.hex())])
"""settles a hold invoice"""
request = invoicesrpc.SettleInvoiceMsg(
preimage=bytes.fromhex(preimage))
response = cls.invoicesstub.SettleInvoice(request,
metadata=[("macaroon",
MACAROON.hex())])
# Fix this: tricky because settling sucessfully an invoice has None response. TODO
return str(response)=="" # True if no response, false otherwise.
return str(response) == "" # True if no response, false otherwise.
@classmethod
def gen_hold_invoice(cls, num_satoshis, description, invoice_expiry, cltv_expiry_secs):
'''Generates hold invoice'''
def gen_hold_invoice(cls, num_satoshis, description, invoice_expiry,
cltv_expiry_secs):
"""Generates hold invoice"""
hold_payment = {}
# The preimage is a random hash of 256 bits entropy
preimage = hashlib.sha256(secrets.token_bytes(nbytes=32)).digest()
preimage = hashlib.sha256(secrets.token_bytes(nbytes=32)).digest()
# Its hash is used to generate the hold invoice
r_hash = hashlib.sha256(preimage).digest()
# timelock expiry for the last hop, computed based on a 10 minutes block with 30% padding (~7 min block)
cltv_expiry_blocks = int(cltv_expiry_secs / (7*60))
request = invoicesrpc.AddHoldInvoiceRequest(
memo=description,
value=num_satoshis,
hash=r_hash,
expiry=int(invoice_expiry*1.5), # actual expiry is padded by 50%, if tight, wrong client system clock will say invoice is expired.
cltv_expiry=cltv_expiry_blocks,
)
response = cls.invoicesstub.AddHoldInvoice(request, metadata=[('macaroon', MACAROON.hex())])
hold_payment['invoice'] = response.payment_request
payreq_decoded = cls.decode_payreq(hold_payment['invoice'])
hold_payment['preimage'] = preimage.hex()
hold_payment['payment_hash'] = payreq_decoded.payment_hash
hold_payment['created_at'] = timezone.make_aware(datetime.fromtimestamp(payreq_decoded.timestamp))
hold_payment['expires_at'] = hold_payment['created_at'] + timedelta(seconds=payreq_decoded.expiry)
hold_payment['cltv_expiry'] = cltv_expiry_blocks
# timelock expiry for the last hop, computed based on a 10 minutes block with 30% padding (~7 min block)
cltv_expiry_blocks = int(cltv_expiry_secs / (7 * 60))
request = invoicesrpc.AddHoldInvoiceRequest(
memo=description,
value=num_satoshis,
hash=r_hash,
expiry=int(
invoice_expiry * 1.5
), # actual expiry is padded by 50%, if tight, wrong client system clock will say invoice is expired.
cltv_expiry=cltv_expiry_blocks,
)
response = cls.invoicesstub.AddHoldInvoice(request,
metadata=[("macaroon",
MACAROON.hex())])
hold_payment["invoice"] = response.payment_request
payreq_decoded = cls.decode_payreq(hold_payment["invoice"])
hold_payment["preimage"] = preimage.hex()
hold_payment["payment_hash"] = payreq_decoded.payment_hash
hold_payment["created_at"] = timezone.make_aware(
datetime.fromtimestamp(payreq_decoded.timestamp))
hold_payment["expires_at"] = hold_payment["created_at"] + timedelta(
seconds=payreq_decoded.expiry)
hold_payment["cltv_expiry"] = cltv_expiry_blocks
return hold_payment
@classmethod
def validate_hold_invoice_locked(cls, lnpayment):
'''Checks if hold invoice is locked'''
request = invoicesrpc.LookupInvoiceMsg(payment_hash=bytes.fromhex(lnpayment.payment_hash))
response = cls.invoicesstub.LookupInvoiceV2(request, metadata=[('macaroon', MACAROON.hex())])
print('status here')
"""Checks if hold invoice is locked"""
request = invoicesrpc.LookupInvoiceMsg(
payment_hash=bytes.fromhex(lnpayment.payment_hash))
response = cls.invoicesstub.LookupInvoiceV2(request,
metadata=[("macaroon",
MACAROON.hex())
])
print("status here")
print(response.state)
# TODO ERROR HANDLING
# Will fail if 'unable to locate invoice'. Happens if invoice expiry
# Will fail if 'unable to locate invoice'. Happens if invoice expiry
# time has passed (but these are 15% padded at the moment). Should catch it
# and report back that the invoice has expired (better robustness)
if response.state == 0: # OPEN
print('STATUS: OPEN')
if response.state == 0: # OPEN
print("STATUS: OPEN")
pass
if response.state == 1: # SETTLED
if response.state == 1: # SETTLED
pass
if response.state == 2: # CANCELLED
if response.state == 2: # CANCELLED
pass
if response.state == 3: # ACCEPTED (LOCKED)
print('STATUS: ACCEPTED')
if response.state == 3: # ACCEPTED (LOCKED)
print("STATUS: ACCEPTED")
lnpayment.expiry_height = response.htlcs[0].expiry_height
lnpayment.status = LNPayment.Status.LOCKED
lnpayment.save()
@ -135,85 +161,104 @@ class LNNode():
@classmethod
def resetmc(cls):
request = routerrpc.ResetMissionControlRequest()
response = cls.routerstub.ResetMissionControl(request, metadata=[('macaroon', MACAROON.hex())])
response = cls.routerstub.ResetMissionControl(request,
metadata=[
("macaroon",
MACAROON.hex())
])
return True
@classmethod
def validate_ln_invoice(cls, invoice, num_satoshis):
'''Checks if the submited LN invoice comforms to expectations'''
"""Checks if the submited LN invoice comforms to expectations"""
payout = {
'valid': False,
'context': None,
'description': None,
'payment_hash': None,
'created_at': None,
'expires_at': None,
}
"valid": False,
"context": None,
"description": None,
"payment_hash": None,
"created_at": None,
"expires_at": None,
}
try:
payreq_decoded = cls.decode_payreq(invoice)
print(payreq_decoded)
except:
payout['context'] = {'bad_invoice':'Does not look like a valid lightning invoice'}
payout["context"] = {
"bad_invoice": "Does not look like a valid lightning invoice"
}
return payout
if payreq_decoded.num_satoshis == 0:
payout['context'] = {'bad_invoice':'The invoice provided has no explicit amount'}
payout["context"] = {
"bad_invoice": "The invoice provided has no explicit amount"
}
return payout
if not payreq_decoded.num_satoshis == num_satoshis:
payout['context'] = {'bad_invoice':'The invoice provided is not for '+'{:,}'.format(num_satoshis)+ ' Sats'}
payout["context"] = {
"bad_invoice":
"The invoice provided is not for " +
"{:,}".format(num_satoshis) + " Sats"
}
return payout
payout['created_at'] = timezone.make_aware(datetime.fromtimestamp(payreq_decoded.timestamp))
payout['expires_at'] = payout['created_at'] + timedelta(seconds=payreq_decoded.expiry)
payout["created_at"] = timezone.make_aware(
datetime.fromtimestamp(payreq_decoded.timestamp))
payout["expires_at"] = payout["created_at"] + timedelta(
seconds=payreq_decoded.expiry)
if payout['expires_at'] < timezone.now():
payout['context'] = {'bad_invoice':f'The invoice provided has already expired'}
if payout["expires_at"] < timezone.now():
payout["context"] = {
"bad_invoice": f"The invoice provided has already expired"
}
return payout
payout['valid'] = True
payout['description'] = payreq_decoded.description
payout['payment_hash'] = payreq_decoded.payment_hash
payout["valid"] = True
payout["description"] = payreq_decoded.description
payout["payment_hash"] = payreq_decoded.payment_hash
return payout
@classmethod
def pay_invoice(cls, invoice, num_satoshis):
'''Sends sats to buyer'''
"""Sends sats to buyer"""
fee_limit_sat = int(max(num_satoshis * float(config('PROPORTIONAL_ROUTING_FEE_LIMIT')), float(config('MIN_FLAT_ROUTING_FEE_LIMIT')))) # 200 ppm or 10 sats
request = routerrpc.SendPaymentRequest(
payment_request=invoice,
fee_limit_sat=fee_limit_sat,
timeout_seconds=60)
fee_limit_sat = int(
max(
num_satoshis * float(config("PROPORTIONAL_ROUTING_FEE_LIMIT")),
float(config("MIN_FLAT_ROUTING_FEE_LIMIT")),
)) # 200 ppm or 10 sats
request = routerrpc.SendPaymentRequest(payment_request=invoice,
fee_limit_sat=fee_limit_sat,
timeout_seconds=60)
for response in cls.routerstub.SendPaymentV2(request, metadata=[('macaroon', MACAROON.hex())]):
for response in cls.routerstub.SendPaymentV2(request,
metadata=[("macaroon",
MACAROON.hex())
]):
print(response)
print(response.status)
# TODO ERROR HANDLING
if response.status == 0 : # Status 0 'UNKNOWN'
pass
if response.status == 1 : # Status 1 'IN_FLIGHT'
return True, 'In flight'
if response.status == 3 : # 4 'FAILED' ??
'''0 Payment isn't failed (yet).
1 There are more routes to try, but the payment timeout was exceeded.
2 All possible routes were tried and failed permanently. Or were no routes to the destination at all.
3 A non-recoverable error has occured.
4 Payment details incorrect (unknown hash, invalid amt or invalid final cltv delta)
5 Insufficient local balance.
'''
if response.status == 0: # Status 0 'UNKNOWN'
pass
if response.status == 1: # Status 1 'IN_FLIGHT'
return True, "In flight"
if response.status == 3: # 4 'FAILED' ??
"""0 Payment isn't failed (yet).
1 There are more routes to try, but the payment timeout was exceeded.
2 All possible routes were tried and failed permanently. Or were no routes to the destination at all.
3 A non-recoverable error has occured.
4 Payment details incorrect (unknown hash, invalid amt or invalid final cltv delta)
5 Insufficient local balance.
"""
context = cls.payment_failure_context[response.failure_reason]
return False, context
if response.status == 2 : # STATUS 'SUCCEEDED'
if response.status == 2: # STATUS 'SUCCEEDED'
return True, None
# How to catch the errors like:"grpc_message":"invoice is already paid","grpc_status":6}
# These are not in the response only printed to commandline
@ -221,15 +266,14 @@ class LNNode():
@classmethod
def double_check_htlc_is_settled(cls, payment_hash):
''' Just as it sounds. Better safe than sorry!'''
request = invoicesrpc.LookupInvoiceMsg(payment_hash=bytes.fromhex(payment_hash))
response = cls.invoicesstub.LookupInvoiceV2(request, metadata=[('macaroon', MACAROON.hex())])
return response.state == 1 # LND states: 0 OPEN, 1 SETTLED, 3 ACCEPTED, GRPC_ERROR status 5 when cancelled/returned
"""Just as it sounds. Better safe than sorry!"""
request = invoicesrpc.LookupInvoiceMsg(
payment_hash=bytes.fromhex(payment_hash))
response = cls.invoicesstub.LookupInvoiceV2(request,
metadata=[("macaroon",
MACAROON.hex())
])
return (
response.state == 1
) # LND states: 0 OPEN, 1 SETTLED, 3 ACCEPTED, GRPC_ERROR status 5 when cancelled/returned

File diff suppressed because it is too large Load Diff

View File

@ -5,61 +5,72 @@ from api.models import Order
from api.logics import Logics
from django.utils import timezone
class Command(BaseCommand):
help = 'Follows all active hold invoices'
help = "Follows all active hold invoices"
# def add_arguments(self, parser):
# parser.add_argument('debug', nargs='+', type=boolean)
def clean_orders(self, *args, **options):
''' Continuously checks order expiration times for 1 hour. If order
has expires, it calls the logics module for expiration handling.'''
"""Continuously checks order expiration times for 1 hour. If order
has expires, it calls the logics module for expiration handling."""
# TODO handle 'database is locked'
do_nothing = [Order.Status.DEL, Order.Status.UCA,
Order.Status.EXP, Order.Status.FSE,
Order.Status.DIS, Order.Status.CCA,
Order.Status.PAY, Order.Status.SUC,
Order.Status.FAI, Order.Status.MLD,
Order.Status.TLD, Order.Status.WFR]
do_nothing = [
Order.Status.DEL,
Order.Status.UCA,
Order.Status.EXP,
Order.Status.FSE,
Order.Status.DIS,
Order.Status.CCA,
Order.Status.PAY,
Order.Status.SUC,
Order.Status.FAI,
Order.Status.MLD,
Order.Status.TLD,
Order.Status.WFR,
]
while True:
time.sleep(5)
queryset = Order.objects.exclude(status__in=do_nothing)
queryset = queryset.filter(expires_at__lt=timezone.now()) # expires at lower than now
queryset = queryset.filter(
expires_at__lt=timezone.now()) # expires at lower than now
debug = {}
debug['num_expired_orders'] = len(queryset)
debug['expired_orders'] = []
debug["num_expired_orders"] = len(queryset)
debug["expired_orders"] = []
for idx, order in enumerate(queryset):
context = str(order)+ " was "+ Order.Status(order.status).label
context = str(order) + " was " + Order.Status(
order.status).label
try:
if Logics.order_expires(order): # Order send to expire here
debug['expired_orders'].append({idx:context})
# It should not happen, but if it cannot locate the hold invoice
if Logics.order_expires(
order): # Order send to expire here
debug["expired_orders"].append({idx: context})
# It should not happen, but if it cannot locate the hold invoice
# it probably was cancelled by another thread, make it expire anyway.
except Exception as e:
if 'unable to locate invoice' in str(e):
if "unable to locate invoice" in str(e):
self.stdout.write(str(e))
order.status = Order.Status.EXP
order.save()
debug['expired_orders'].append({idx:context})
debug["expired_orders"].append({idx: context})
if debug['num_expired_orders'] > 0:
if debug["num_expired_orders"] > 0:
self.stdout.write(str(timezone.now()))
self.stdout.write(str(debug))
def handle(self, *args, **options):
''' Never mind database locked error, keep going, print them out'''
"""Never mind database locked error, keep going, print them out"""
try:
self.clean_orders()
except Exception as e:
if 'database is locked' in str(e):
self.stdout.write('database is locked')
if "database is locked" in str(e):
self.stdout.write("database is locked")
self.stdout.write(str(e))

View File

@ -11,17 +11,18 @@ from decouple import config
from base64 import b64decode
import time
MACAROON = b64decode(config('LND_MACAROON_BASE64'))
MACAROON = b64decode(config("LND_MACAROON_BASE64"))
class Command(BaseCommand):
help = 'Follows all active hold invoices'
rest = 5 # seconds between consecutive checks for invoice updates
help = "Follows all active hold invoices"
rest = 5 # seconds between consecutive checks for invoice updates
def handle(self, *args, **options):
''' Infinite loop to check invoices and retry payments.
ever mind database locked error, keep going, print out'''
"""Infinite loop to check invoices and retry payments.
ever mind database locked error, keep going, print out"""
while True:
time.sleep(self.rest)
@ -35,67 +36,76 @@ class Command(BaseCommand):
self.stdout.write(str(e))
def follow_hold_invoices(self):
''' Follows and updates LNpayment objects
"""Follows and updates LNpayment objects
until settled or canceled
Background: SubscribeInvoices stub iterator would be great to use here.
However, it only sends updates when the invoice is OPEN (new) or SETTLED.
We are very interested on the other two states (CANCELLED and ACCEPTED).
Therefore, this thread (follow_invoices) will iterate over all LNpayment
objects and do InvoiceLookupV2 every X seconds to update their state 'live'
'''
objects and do InvoiceLookupV2 every X seconds to update their state 'live'
"""
lnd_state_to_lnpayment_status = {
0: LNPayment.Status.INVGEN, # OPEN
1: LNPayment.Status.SETLED, # SETTLED
2: LNPayment.Status.CANCEL, # CANCELLED
3: LNPayment.Status.LOCKED # ACCEPTED
}
0: LNPayment.Status.INVGEN, # OPEN
1: LNPayment.Status.SETLED, # SETTLED
2: LNPayment.Status.CANCEL, # CANCELLED
3: LNPayment.Status.LOCKED, # ACCEPTED
}
stub = LNNode.invoicesstub
# time it for debugging
t0 = time.time()
queryset = LNPayment.objects.filter(type=LNPayment.Types.HOLD, status__in=[LNPayment.Status.INVGEN, LNPayment.Status.LOCKED])
queryset = LNPayment.objects.filter(
type=LNPayment.Types.HOLD,
status__in=[LNPayment.Status.INVGEN, LNPayment.Status.LOCKED],
)
debug = {}
debug['num_active_invoices'] = len(queryset)
debug['invoices'] = []
debug["num_active_invoices"] = len(queryset)
debug["invoices"] = []
at_least_one_changed = False
for idx, hold_lnpayment in enumerate(queryset):
old_status = LNPayment.Status(hold_lnpayment.status).label
try:
try:
# this is similar to LNNnode.validate_hold_invoice_locked
request = LNNode.invoicesrpc.LookupInvoiceMsg(payment_hash=bytes.fromhex(hold_lnpayment.payment_hash))
response = stub.LookupInvoiceV2(request, metadata=[('macaroon', MACAROON.hex())])
hold_lnpayment.status = lnd_state_to_lnpayment_status[response.state]
request = LNNode.invoicesrpc.LookupInvoiceMsg(
payment_hash=bytes.fromhex(hold_lnpayment.payment_hash))
response = stub.LookupInvoiceV2(request,
metadata=[("macaroon",
MACAROON.hex())])
hold_lnpayment.status = lnd_state_to_lnpayment_status[
response.state]
# try saving expiry height
if hasattr(response, 'htlcs' ):
if hasattr(response, "htlcs"):
try:
hold_lnpayment.expiry_height = response.htlcs[0].expiry_height
hold_lnpayment.expiry_height = response.htlcs[
0].expiry_height
except:
pass
except Exception as e:
# If it fails at finding the invoice: it has been canceled.
# In RoboSats DB we make a distinction between cancelled and returned (LND does not)
if 'unable to locate invoice' in str(e):
if "unable to locate invoice" in str(e):
self.stdout.write(str(e))
hold_lnpayment.status = LNPayment.Status.CANCEL
# LND restarted.
if 'wallet locked, unlock it' in str(e):
self.stdout.write(str(timezone.now())+' :: Wallet Locked')
if "wallet locked, unlock it" in str(e):
self.stdout.write(
str(timezone.now()) + " :: Wallet Locked")
# Other write to logs
else:
self.stdout.write(str(e))
new_status = LNPayment.Status(hold_lnpayment.status).label
# Only save the hold_payments that change (otherwise this function does not scale)
changed = not old_status==new_status
changed = not old_status == new_status
if changed:
# self.handle_status_change(hold_lnpayment, old_status)
self.update_order_status(hold_lnpayment)
@ -103,39 +113,48 @@ class Command(BaseCommand):
# Report for debugging
new_status = LNPayment.Status(hold_lnpayment.status).label
debug['invoices'].append({idx:{
'payment_hash': str(hold_lnpayment.payment_hash),
'old_status': old_status,
'new_status': new_status,
}})
debug["invoices"].append({
idx: {
"payment_hash": str(hold_lnpayment.payment_hash),
"old_status": old_status,
"new_status": new_status,
}
})
at_least_one_changed = at_least_one_changed or changed
debug['time']=time.time()-t0
debug["time"] = time.time() - t0
if at_least_one_changed:
self.stdout.write(str(timezone.now()))
self.stdout.write(str(debug))
def send_payments(self):
'''
"""
Checks for invoices that are due to pay; i.e., INFLIGHT status and 0 routing_attempts.
Checks if any payment is due for retry, and tries to pay it.
'''
"""
queryset = LNPayment.objects.filter(type=LNPayment.Types.NORM,
status=LNPayment.Status.FLIGHT,
routing_attempts=0)
queryset = LNPayment.objects.filter(
type=LNPayment.Types.NORM,
status=LNPayment.Status.FLIGHT,
routing_attempts=0,
)
queryset_retries = LNPayment.objects.filter(
type=LNPayment.Types.NORM,
status__in=[LNPayment.Status.VALIDI, LNPayment.Status.FAILRO],
routing_attempts__lt=5,
last_routing_time__lt=(
timezone.now() - timedelta(minutes=int(config("RETRY_TIME")))),
)
queryset_retries = LNPayment.objects.filter(type=LNPayment.Types.NORM,
status__in=[LNPayment.Status.VALIDI, LNPayment.Status.FAILRO],
routing_attempts__lt=5,
last_routing_time__lt=(timezone.now()-timedelta(minutes=int(config('RETRY_TIME')))))
queryset = queryset.union(queryset_retries)
for lnpayment in queryset:
success, _ = follow_send_payment(lnpayment) # Do follow_send_payment.delay() for further concurrency.
success, _ = follow_send_payment(
lnpayment
) # Do follow_send_payment.delay() for further concurrency.
# If failed, reset mision control. (This won't scale well, just a temporary fix)
if not success:
@ -148,26 +167,26 @@ class Command(BaseCommand):
lnpayment.save()
def update_order_status(self, lnpayment):
''' Background process following LND hold invoices
"""Background process following LND hold invoices
can catch LNpayments changing status. If they do,
the order status might have to change too.'''
the order status might have to change too."""
# If the LNPayment goes to LOCKED (ACCEPTED)
if lnpayment.status == LNPayment.Status.LOCKED:
try:
# It is a maker bond => Publish order.
if hasattr(lnpayment, 'order_made' ):
if hasattr(lnpayment, "order_made"):
Logics.publish_order(lnpayment.order_made)
return
# It is a taker bond => close contract.
elif hasattr(lnpayment, 'order_taken' ):
elif hasattr(lnpayment, "order_taken"):
if lnpayment.order_taken.status == Order.Status.TAK:
Logics.finalize_contract(lnpayment.order_taken)
return
# It is a trade escrow => move foward order status.
elif hasattr(lnpayment, 'order_escrow' ):
elif hasattr(lnpayment, "order_escrow"):
Logics.trade_escrow_received(lnpayment.order_escrow)
return
@ -177,20 +196,20 @@ class Command(BaseCommand):
# If the LNPayment goes to CANCEL from INVGEN, the invoice had expired
# If it goes to CANCEL from LOCKED the bond was relased. Order had expired in both cases.
# Testing needed for end of time trades!
if lnpayment.status == LNPayment.Status.CANCEL :
if hasattr(lnpayment, 'order_made' ):
Logics.order_expires(lnpayment.order_made)
return
if lnpayment.status == LNPayment.Status.CANCEL:
if hasattr(lnpayment, "order_made"):
Logics.order_expires(lnpayment.order_made)
return
elif hasattr(lnpayment, 'order_taken' ):
Logics.order_expires(lnpayment.order_taken)
return
elif hasattr(lnpayment, "order_taken"):
Logics.order_expires(lnpayment.order_taken)
return
elif hasattr(lnpayment, 'order_escrow' ):
Logics.order_expires(lnpayment.order_escrow)
return
elif hasattr(lnpayment, "order_escrow"):
Logics.order_expires(lnpayment.order_escrow)
return
# TODO If a lnpayment goes from LOCKED to INVGEN. Totally weird
# halt the order
if lnpayment.status == LNPayment.Status.INVGEN:
pass
pass

View File

@ -1,6 +1,10 @@
from django.db import models
from django.contrib.auth.models import User
from django.core.validators import MaxValueValidator, MinValueValidator, validate_comma_separated_integer_list
from django.core.validators import (
MaxValueValidator,
MinValueValidator,
validate_comma_separated_integer_list,
)
from django.db.models.signals import post_save, pre_delete
from django.template.defaultfilters import truncatechars
from django.dispatch import receiver
@ -12,19 +16,28 @@ from decouple import config
from pathlib import Path
import json
MIN_TRADE = int(config('MIN_TRADE'))
MAX_TRADE = int(config('MAX_TRADE'))
FEE = float(config('FEE'))
BOND_SIZE = float(config('BOND_SIZE'))
MIN_TRADE = int(config("MIN_TRADE"))
MAX_TRADE = int(config("MAX_TRADE"))
FEE = float(config("FEE"))
BOND_SIZE = float(config("BOND_SIZE"))
class Currency(models.Model):
currency_dict = json.load(open('frontend/static/assets/currencies.json'))
currency_choices = [(int(val), label) for val, label in list(currency_dict.items())]
currency_dict = json.load(open("frontend/static/assets/currencies.json"))
currency_choices = [(int(val), label)
for val, label in list(currency_dict.items())]
currency = models.PositiveSmallIntegerField(choices=currency_choices, null=False, unique=True)
exchange_rate = models.DecimalField(max_digits=14, decimal_places=4, default=None, null=True, validators=[MinValueValidator(0)])
currency = models.PositiveSmallIntegerField(choices=currency_choices,
null=False,
unique=True)
exchange_rate = models.DecimalField(
max_digits=14,
decimal_places=4,
default=None,
null=True,
validators=[MinValueValidator(0)],
)
timestamp = models.DateTimeField(auto_now_add=True)
def __str__(self):
@ -32,63 +45,101 @@ class Currency(models.Model):
return self.currency_dict[str(self.currency)]
class Meta:
verbose_name = 'Cached market currency'
verbose_name_plural = 'Currencies'
verbose_name = "Cached market currency"
verbose_name_plural = "Currencies"
class LNPayment(models.Model):
class Types(models.IntegerChoices):
NORM = 0, 'Regular invoice'
HOLD = 1, 'hold invoice'
NORM = 0, "Regular invoice"
HOLD = 1, "hold invoice"
class Concepts(models.IntegerChoices):
MAKEBOND = 0, 'Maker bond'
TAKEBOND = 1, 'Taker bond'
TRESCROW = 2, 'Trade escrow'
PAYBUYER = 3, 'Payment to buyer'
MAKEBOND = 0, "Maker bond"
TAKEBOND = 1, "Taker bond"
TRESCROW = 2, "Trade escrow"
PAYBUYER = 3, "Payment to buyer"
class Status(models.IntegerChoices):
INVGEN = 0, 'Generated'
LOCKED = 1, 'Locked'
SETLED = 2, 'Settled'
RETNED = 3, 'Returned'
CANCEL = 4, 'Cancelled'
EXPIRE = 5, 'Expired'
VALIDI = 6, 'Valid'
FLIGHT = 7, 'In flight'
SUCCED = 8, 'Succeeded'
FAILRO = 9, 'Routing failed'
INVGEN = 0, "Generated"
LOCKED = 1, "Locked"
SETLED = 2, "Settled"
RETNED = 3, "Returned"
CANCEL = 4, "Cancelled"
EXPIRE = 5, "Expired"
VALIDI = 6, "Valid"
FLIGHT = 7, "In flight"
SUCCED = 8, "Succeeded"
FAILRO = 9, "Routing failed"
# payment use details
type = models.PositiveSmallIntegerField(choices=Types.choices, null=False, default=Types.HOLD)
concept = models.PositiveSmallIntegerField(choices=Concepts.choices, null=False, default=Concepts.MAKEBOND)
status = models.PositiveSmallIntegerField(choices=Status.choices, null=False, default=Status.INVGEN)
type = models.PositiveSmallIntegerField(choices=Types.choices,
null=False,
default=Types.HOLD)
concept = models.PositiveSmallIntegerField(choices=Concepts.choices,
null=False,
default=Concepts.MAKEBOND)
status = models.PositiveSmallIntegerField(choices=Status.choices,
null=False,
default=Status.INVGEN)
# payment info
payment_hash = models.CharField(max_length=100, unique=True, default=None, blank=True, primary_key=True)
invoice = models.CharField(max_length=1200, unique=True, null=True, default=None, blank=True) # Some invoices with lots of routing hints might be long
preimage = models.CharField(max_length=64, unique=True, null=True, default=None, blank=True)
description = models.CharField(max_length=500, unique=False, null=True, default=None, blank=True)
num_satoshis = models.PositiveBigIntegerField(validators=[MinValueValidator(MIN_TRADE*BOND_SIZE), MaxValueValidator(MAX_TRADE*(1+BOND_SIZE+FEE))])
payment_hash = models.CharField(max_length=100,
unique=True,
default=None,
blank=True,
primary_key=True)
invoice = models.CharField(
max_length=1200, unique=True, null=True, default=None,
blank=True) # Some invoices with lots of routing hints might be long
preimage = models.CharField(max_length=64,
unique=True,
null=True,
default=None,
blank=True)
description = models.CharField(max_length=500,
unique=False,
null=True,
default=None,
blank=True)
num_satoshis = models.PositiveBigIntegerField(validators=[
MinValueValidator(MIN_TRADE * BOND_SIZE),
MaxValueValidator(MAX_TRADE * (1 + BOND_SIZE + FEE)),
])
created_at = models.DateTimeField()
expires_at = models.DateTimeField()
cltv_expiry = models.PositiveSmallIntegerField(null=True, default=None, blank=True)
expiry_height = models.PositiveBigIntegerField(null=True, default=None, blank=True)
cltv_expiry = models.PositiveSmallIntegerField(null=True,
default=None,
blank=True)
expiry_height = models.PositiveBigIntegerField(null=True,
default=None,
blank=True)
# routing
routing_attempts = models.PositiveSmallIntegerField(null=False, default=0)
last_routing_time = models.DateTimeField(null=True, default=None, blank=True)
last_routing_time = models.DateTimeField(null=True,
default=None,
blank=True)
# involved parties
sender = models.ForeignKey(User, related_name='sender', on_delete=models.CASCADE, null=True, default=None)
receiver = models.ForeignKey(User, related_name='receiver', on_delete=models.CASCADE, null=True, default=None)
sender = models.ForeignKey(User,
related_name="sender",
on_delete=models.CASCADE,
null=True,
default=None)
receiver = models.ForeignKey(User,
related_name="receiver",
on_delete=models.CASCADE,
null=True,
default=None)
def __str__(self):
return (f'LN-{str(self.payment_hash)[:8]}: {self.Concepts(self.concept).label} - {self.Status(self.status).label}')
return f"LN-{str(self.payment_hash)[:8]}: {self.Concepts(self.concept).label} - {self.Status(self.status).label}"
class Meta:
verbose_name = 'Lightning payment'
verbose_name_plural = 'Lightning payments'
verbose_name = "Lightning payment"
verbose_name_plural = "Lightning payments"
@property
def hash(self):
@ -97,75 +148,162 @@ class LNPayment(models.Model):
# We created a truncated property for display 'hash'
return truncatechars(self.payment_hash, 10)
class Order(models.Model):
class Types(models.IntegerChoices):
BUY = 0, 'BUY'
SELL = 1, 'SELL'
BUY = 0, "BUY"
SELL = 1, "SELL"
class Status(models.IntegerChoices):
WFB = 0, 'Waiting for maker bond'
PUB = 1, 'Public'
DEL = 2, 'Deleted'
TAK = 3, 'Waiting for taker bond'
UCA = 4, 'Cancelled'
EXP = 5, 'Expired'
WF2 = 6, 'Waiting for trade collateral and buyer invoice'
WFE = 7, 'Waiting only for seller trade collateral'
WFI = 8, 'Waiting only for buyer invoice'
CHA = 9, 'Sending fiat - In chatroom'
FSE = 10, 'Fiat sent - In chatroom'
DIS = 11, 'In dispute'
CCA = 12, 'Collaboratively cancelled'
PAY = 13, 'Sending satoshis to buyer'
SUC = 14, 'Sucessful trade'
FAI = 15, 'Failed lightning network routing'
WFR = 16, 'Wait for dispute resolution'
MLD = 17, 'Maker lost dispute'
TLD = 18, 'Taker lost dispute'
WFB = 0, "Waiting for maker bond"
PUB = 1, "Public"
DEL = 2, "Deleted"
TAK = 3, "Waiting for taker bond"
UCA = 4, "Cancelled"
EXP = 5, "Expired"
WF2 = 6, "Waiting for trade collateral and buyer invoice"
WFE = 7, "Waiting only for seller trade collateral"
WFI = 8, "Waiting only for buyer invoice"
CHA = 9, "Sending fiat - In chatroom"
FSE = 10, "Fiat sent - In chatroom"
DIS = 11, "In dispute"
CCA = 12, "Collaboratively cancelled"
PAY = 13, "Sending satoshis to buyer"
SUC = 14, "Sucessful trade"
FAI = 15, "Failed lightning network routing"
WFR = 16, "Wait for dispute resolution"
MLD = 17, "Maker lost dispute"
TLD = 18, "Taker lost dispute"
# order info
status = models.PositiveSmallIntegerField(choices=Status.choices, null=False, default=Status.WFB)
status = models.PositiveSmallIntegerField(choices=Status.choices,
null=False,
default=Status.WFB)
created_at = models.DateTimeField(auto_now_add=True)
expires_at = models.DateTimeField()
# order details
type = models.PositiveSmallIntegerField(choices=Types.choices, null=False)
currency = models.ForeignKey(Currency, null=True, on_delete=models.SET_NULL)
amount = models.DecimalField(max_digits=16, decimal_places=8, validators=[MinValueValidator(0.00000001)])
payment_method = models.CharField(max_length=35, null=False, default="not specified", blank=True)
currency = models.ForeignKey(Currency,
null=True,
on_delete=models.SET_NULL)
amount = models.DecimalField(max_digits=16,
decimal_places=8,
validators=[MinValueValidator(0.00000001)])
payment_method = models.CharField(max_length=35,
null=False,
default="not specified",
blank=True)
# order pricing method. A explicit amount of sats, or a relative premium above/below market.
is_explicit = models.BooleanField(default=False, null=False)
# marked to market
premium = models.DecimalField(max_digits=5, decimal_places=2, default=0, null=True, validators=[MinValueValidator(-100), MaxValueValidator(999)], blank=True)
premium = models.DecimalField(
max_digits=5,
decimal_places=2,
default=0,
null=True,
validators=[MinValueValidator(-100),
MaxValueValidator(999)],
blank=True,
)
# explicit
satoshis = models.PositiveBigIntegerField(null=True, validators=[MinValueValidator(MIN_TRADE), MaxValueValidator(MAX_TRADE)], blank=True)
satoshis = models.PositiveBigIntegerField(
null=True,
validators=[
MinValueValidator(MIN_TRADE),
MaxValueValidator(MAX_TRADE)
],
blank=True,
)
# how many sats at creation and at last check (relevant for marked to market)
t0_satoshis = models.PositiveBigIntegerField(null=True, validators=[MinValueValidator(MIN_TRADE), MaxValueValidator(MAX_TRADE)], blank=True) # sats at creation
last_satoshis = models.PositiveBigIntegerField(null=True, validators=[MinValueValidator(0), MaxValueValidator(MAX_TRADE*2)], blank=True) # sats last time checked. Weird if 2* trade max...
t0_satoshis = models.PositiveBigIntegerField(
null=True,
validators=[
MinValueValidator(MIN_TRADE),
MaxValueValidator(MAX_TRADE)
],
blank=True,
) # sats at creation
last_satoshis = models.PositiveBigIntegerField(
null=True,
validators=[MinValueValidator(0),
MaxValueValidator(MAX_TRADE * 2)],
blank=True,
) # sats last time checked. Weird if 2* trade max...
# order participants
maker = models.ForeignKey(User, related_name='maker', on_delete=models.SET_NULL, null=True, default=None) # unique = True, a maker can only make one order
taker = models.ForeignKey(User, related_name='taker', on_delete=models.SET_NULL, null=True, default=None, blank=True) # unique = True, a taker can only take one order
maker_last_seen = models.DateTimeField(null=True,default=None, blank=True)
taker_last_seen = models.DateTimeField(null=True,default=None, blank=True)
maker_asked_cancel = models.BooleanField(default=False, null=False) # When collaborative cancel is needed and one partner has cancelled.
taker_asked_cancel = models.BooleanField(default=False, null=False) # When collaborative cancel is needed and one partner has cancelled.
maker = models.ForeignKey(
User,
related_name="maker",
on_delete=models.SET_NULL,
null=True,
default=None) # unique = True, a maker can only make one order
taker = models.ForeignKey(
User,
related_name="taker",
on_delete=models.SET_NULL,
null=True,
default=None,
blank=True,
) # unique = True, a taker can only take one order
maker_last_seen = models.DateTimeField(null=True, default=None, blank=True)
taker_last_seen = models.DateTimeField(null=True, default=None, blank=True)
maker_asked_cancel = models.BooleanField(
default=False, null=False
) # When collaborative cancel is needed and one partner has cancelled.
taker_asked_cancel = models.BooleanField(
default=False, null=False
) # When collaborative cancel is needed and one partner has cancelled.
is_fiat_sent = models.BooleanField(default=False, null=False)
# in dispute
is_disputed = models.BooleanField(default=False, null=False)
maker_statement = models.TextField(max_length=5000, null=True, default=None, blank=True)
taker_statement = models.TextField(max_length=5000, null=True, default=None, blank=True)
maker_statement = models.TextField(max_length=5000,
null=True,
default=None,
blank=True)
taker_statement = models.TextField(max_length=5000,
null=True,
default=None,
blank=True)
# LNpayments
# Order collateral
maker_bond = models.OneToOneField(LNPayment, related_name='order_made', on_delete=models.SET_NULL, null=True, default=None, blank=True)
taker_bond = models.OneToOneField(LNPayment, related_name='order_taken', on_delete=models.SET_NULL, null=True, default=None, blank=True)
trade_escrow = models.OneToOneField(LNPayment, related_name='order_escrow', on_delete=models.SET_NULL, null=True, default=None, blank=True)
maker_bond = models.OneToOneField(
LNPayment,
related_name="order_made",
on_delete=models.SET_NULL,
null=True,
default=None,
blank=True,
)
taker_bond = models.OneToOneField(
LNPayment,
related_name="order_taken",
on_delete=models.SET_NULL,
null=True,
default=None,
blank=True,
)
trade_escrow = models.OneToOneField(
LNPayment,
related_name="order_escrow",
on_delete=models.SET_NULL,
null=True,
default=None,
blank=True,
)
# buyer payment LN invoice
payout = models.OneToOneField(LNPayment, related_name='order_paid', on_delete=models.SET_NULL, null=True, default=None, blank=True)
payout = models.OneToOneField(
LNPayment,
related_name="order_paid",
on_delete=models.SET_NULL,
null=True,
default=None,
blank=True,
)
# ratings
maker_rated = models.BooleanField(default=False, null=False)
@ -174,34 +312,44 @@ class Order(models.Model):
taker_platform_rated = models.BooleanField(default=False, null=False)
t_to_expire = {
0 : int(config('EXP_MAKER_BOND_INVOICE')) , # 'Waiting for maker bond'
1 : 60*60*int(config('PUBLIC_ORDER_DURATION')), # 'Public'
2 : 0, # 'Deleted'
3 : int(config('EXP_TAKER_BOND_INVOICE')), # 'Waiting for taker bond'
4 : 0, # 'Cancelled'
5 : 0, # 'Expired'
6 : 60*int(config('INVOICE_AND_ESCROW_DURATION')), # 'Waiting for trade collateral and buyer invoice'
7 : 60*int(config('INVOICE_AND_ESCROW_DURATION')), # 'Waiting only for seller trade collateral'
8 : 60*int(config('INVOICE_AND_ESCROW_DURATION')), # 'Waiting only for buyer invoice'
9 : 60*60*int(config('FIAT_EXCHANGE_DURATION')), # 'Sending fiat - In chatroom'
10 : 60*60*int(config('FIAT_EXCHANGE_DURATION')), # 'Fiat sent - In chatroom'
11 : 1*24*60*60, # 'In dispute'
12 : 0, # 'Collaboratively cancelled'
13 : 24*60*60, # 'Sending satoshis to buyer'
14 : 24*60*60, # 'Sucessful trade'
15 : 24*60*60, # 'Failed lightning network routing'
16 : 10*24*60*60, # 'Wait for dispute resolution'
17 : 24*60*60, # 'Maker lost dispute'
18 : 24*60*60, # 'Taker lost dispute'
}
0: int(config("EXP_MAKER_BOND_INVOICE")), # 'Waiting for maker bond'
1: 60 * 60 * int(config("PUBLIC_ORDER_DURATION")), # 'Public'
2: 0, # 'Deleted'
3: int(config("EXP_TAKER_BOND_INVOICE")), # 'Waiting for taker bond'
4: 0, # 'Cancelled'
5: 0, # 'Expired'
6: 60 * int(config("INVOICE_AND_ESCROW_DURATION")
), # 'Waiting for trade collateral and buyer invoice'
7: 60 * int(config("INVOICE_AND_ESCROW_DURATION")
), # 'Waiting only for seller trade collateral'
8: 60 * int(config("INVOICE_AND_ESCROW_DURATION")
), # 'Waiting only for buyer invoice'
9: 60 * 60 *
int(config("FIAT_EXCHANGE_DURATION")), # 'Sending fiat - In chatroom'
10: 60 * 60 *
int(config("FIAT_EXCHANGE_DURATION")), # 'Fiat sent - In chatroom'
11: 1 * 24 * 60 * 60, # 'In dispute'
12: 0, # 'Collaboratively cancelled'
13: 24 * 60 * 60, # 'Sending satoshis to buyer'
14: 24 * 60 * 60, # 'Sucessful trade'
15: 24 * 60 * 60, # 'Failed lightning network routing'
16: 10 * 24 * 60 * 60, # 'Wait for dispute resolution'
17: 24 * 60 * 60, # 'Maker lost dispute'
18: 24 * 60 * 60, # 'Taker lost dispute'
}
def __str__(self):
return (f'Order {self.id}: {self.Types(self.type).label} BTC for {float(self.amount)} {self.currency}')
return f"Order {self.id}: {self.Types(self.type).label} BTC for {float(self.amount)} {self.currency}"
@receiver(pre_delete, sender=Order)
def delete_lnpayment_at_order_deletion(sender, instance, **kwargs):
to_delete = (instance.maker_bond, instance.payout, instance.taker_bond, instance.trade_escrow)
to_delete = (
instance.maker_bond,
instance.payout,
instance.taker_bond,
instance.trade_escrow,
)
for lnpayment in to_delete:
try:
@ -209,31 +357,60 @@ def delete_lnpayment_at_order_deletion(sender, instance, **kwargs):
except:
pass
class Profile(models.Model):
user = models.OneToOneField(User,on_delete=models.CASCADE)
user = models.OneToOneField(User, on_delete=models.CASCADE)
# Total trades
total_contracts = models.PositiveIntegerField(null=False, default=0)
total_contracts = models.PositiveIntegerField(null=False, default=0)
# Ratings stored as a comma separated integer list
total_ratings = models.PositiveIntegerField(null=False, default=0)
latest_ratings = models.CharField(max_length=999, null=True, default=None, validators=[validate_comma_separated_integer_list], blank=True) # Will only store latest ratings
avg_rating = models.DecimalField(max_digits=4, decimal_places=1, default=None, null=True, validators=[MinValueValidator(0), MaxValueValidator(100)], blank=True)
total_ratings = models.PositiveIntegerField(null=False, default=0)
latest_ratings = models.CharField(
max_length=999,
null=True,
default=None,
validators=[validate_comma_separated_integer_list],
blank=True,
) # Will only store latest ratings
avg_rating = models.DecimalField(
max_digits=4,
decimal_places=1,
default=None,
null=True,
validators=[MinValueValidator(0),
MaxValueValidator(100)],
blank=True,
)
# Disputes
num_disputes = models.PositiveIntegerField(null=False, default=0)
lost_disputes = models.PositiveIntegerField(null=False, default=0)
num_disputes_started = models.PositiveIntegerField(null=False, default=0)
orders_disputes_started = models.CharField(max_length=999, null=True, default=None, validators=[validate_comma_separated_integer_list], blank=True) # Will only store ID of orders
orders_disputes_started = models.CharField(
max_length=999,
null=True,
default=None,
validators=[validate_comma_separated_integer_list],
blank=True,
) # Will only store ID of orders
# RoboHash
avatar = models.ImageField(default=("static/assets/avatars/"+"unknown_avatar.png"), verbose_name='Avatar', blank=True)
avatar = models.ImageField(
default=("static/assets/avatars/" + "unknown_avatar.png"),
verbose_name="Avatar",
blank=True,
)
# Penalty expiration (only used then taking/cancelling repeatedly orders in the book before comitting bond)
penalty_expiration = models.DateTimeField(null=True,default=None, blank=True)
penalty_expiration = models.DateTimeField(null=True,
default=None,
blank=True)
# Platform rate
platform_rating = models.PositiveIntegerField(null=True, default=None, blank=True)
platform_rating = models.PositiveIntegerField(null=True,
default=None,
blank=True)
@receiver(post_save, sender=User)
def create_user_profile(sender, instance, created, **kwargs):
@ -247,50 +424,82 @@ class Profile(models.Model):
@receiver(pre_delete, sender=User)
def del_avatar_from_disk(sender, instance, **kwargs):
try:
avatar_file=Path(settings.AVATAR_ROOT + instance.profile.avatar.url.split('/')[-1])
avatar_file = Path(settings.AVATAR_ROOT +
instance.profile.avatar.url.split("/")[-1])
avatar_file.unlink()
except:
pass
def __str__(self):
return self.user.username
# to display avatars in admin panel
def get_avatar(self):
if not self.avatar:
return settings.STATIC_ROOT + 'unknown_avatar.png'
return settings.STATIC_ROOT + "unknown_avatar.png"
return self.avatar.url
# method to create a fake table field in read only mode
def avatar_tag(self):
return mark_safe('<img src="%s" width="50" height="50" />' % self.get_avatar())
return mark_safe('<img src="%s" width="50" height="50" />' %
self.get_avatar())
class MarketTick(models.Model):
'''
Records tick by tick Non-KYC Bitcoin price.
"""
Records tick by tick Non-KYC Bitcoin price.
Data to be aggregated and offered via public API.
It is checked against current CEX price for useful
insight on the historical premium of Non-KYC BTC
Price is set when taker bond is locked. Both
maker and taker are commited with bonds (contract
Price is set when taker bond is locked. Both
maker and taker are commited with bonds (contract
is finished and cancellation has a cost)
'''
"""
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
price = models.DecimalField(max_digits=10, decimal_places=2, default=None, null=True, validators=[MinValueValidator(0)])
volume = models.DecimalField(max_digits=8, decimal_places=8, default=None, null=True, validators=[MinValueValidator(0)])
premium = models.DecimalField(max_digits=5, decimal_places=2, default=None, null=True, validators=[MinValueValidator(-100), MaxValueValidator(999)], blank=True)
currency = models.ForeignKey(Currency, null=True, on_delete=models.SET_NULL)
price = models.DecimalField(
max_digits=10,
decimal_places=2,
default=None,
null=True,
validators=[MinValueValidator(0)],
)
volume = models.DecimalField(
max_digits=8,
decimal_places=8,
default=None,
null=True,
validators=[MinValueValidator(0)],
)
premium = models.DecimalField(
max_digits=5,
decimal_places=2,
default=None,
null=True,
validators=[MinValueValidator(-100),
MaxValueValidator(999)],
blank=True,
)
currency = models.ForeignKey(Currency,
null=True,
on_delete=models.SET_NULL)
timestamp = models.DateTimeField(auto_now_add=True)
# Relevant to keep record of the historical fee, so the insight on the premium can be better analyzed
fee = models.DecimalField(max_digits=4, decimal_places=4, default=FEE, validators=[MinValueValidator(0), MaxValueValidator(1)])
fee = models.DecimalField(
max_digits=4,
decimal_places=4,
default=FEE,
validators=[MinValueValidator(0),
MaxValueValidator(1)],
)
def log_a_tick(order):
'''
"""
Creates a new tick
'''
"""
if not order.taker_bond:
return None
@ -301,18 +510,16 @@ class MarketTick(models.Model):
market_exchange_rate = float(order.currency.exchange_rate)
premium = 100 * (price / market_exchange_rate - 1)
tick = MarketTick.objects.create(
price=price,
volume=volume,
premium=premium,
currency=order.currency)
tick = MarketTick.objects.create(price=price,
volume=volume,
premium=premium,
currency=order.currency)
tick.save()
def __str__(self):
return f'Tick: {str(self.id)[:8]}'
return f"Tick: {str(self.id)[:8]}"
class Meta:
verbose_name = 'Market tick'
verbose_name_plural = 'Market ticks'
verbose_name = "Market tick"
verbose_name_plural = "Market ticks"

View File

@ -4832,4 +4832,4 @@ adjectives = [
"Bodacious",
"Unpersuasive",
"Simplistic",
]
]

View File

@ -2,7 +2,6 @@ from .utils import human_format
import hashlib
import time
"""
Deterministic nick generator from SHA256 hash.
@ -14,7 +13,9 @@ is a total of to 450*4800*12500*1000 =
28 Trillion deterministic nicks
"""
class NickGenerator:
def __init__(
self,
lang="English",
@ -42,13 +43,11 @@ class NickGenerator:
raise ValueError("Language not implemented.")
if verbose:
print(
f"{lang} SHA256 Nick Generator initialized with:"
+ f"\nUp to {len(adverbs)} adverbs."
+ f"\nUp to {len(adjectives)} adjectives."
+ f"\nUp to {len(nouns)} nouns."
+ f"\nUp to {max_num+1} numerics.\n"
)
print(f"{lang} SHA256 Nick Generator initialized with:" +
f"\nUp to {len(adverbs)} adverbs." +
f"\nUp to {len(adjectives)} adjectives." +
f"\nUp to {len(nouns)} nouns." +
f"\nUp to {max_num+1} numerics.\n")
self.use_adv = use_adv
self.use_adj = use_adj
@ -78,7 +77,7 @@ class NickGenerator:
pool_size = self.max_num * num_nouns * num_adj * num_adv
# Min-Max scale the hash relative to the pool size
max_int_hash = 2 ** 256
max_int_hash = 2**256
int_hash = int(hash, 16)
nick_id = int((int_hash / max_int_hash) * pool_size)
@ -148,7 +147,10 @@ class NickGenerator:
i = i + 1
return "", 0, 0, i
def compute_pool_size_loss(self, max_length=22, max_iter=1000000, num_runs=5000):
def compute_pool_size_loss(self,
max_length=22,
max_iter=1000000,
num_runs=5000):
"""
Computes median an average loss of
nick pool diversity due to max_lenght
@ -184,7 +186,7 @@ if __name__ == "__main__":
t0 = time.time()
# Hardcoded example text and hashing
nick_lang = 'English' #Spanish
nick_lang = "English" # Spanish
hash = hashlib.sha256(b"No one expected such cool nick!!").hexdigest()
max_length = 22
max_iter = 100000000
@ -194,16 +196,13 @@ if __name__ == "__main__":
# Generates a short nick with length limit from SHA256
nick, nick_id, pool_size, iterations = GenNick.short_from_SHA256(
hash, max_length, max_iter
)
hash, max_length, max_iter)
# Output
print(
f"Nick number {nick_id} has been selected among"
+ f" {human_format(pool_size)} possible nicks.\n"
+ f"Needed {iterations} iterations to find one "
+ f"this short.\nYour nick is {nick} !\n"
)
print(f"Nick number {nick_id} has been selected among" +
f" {human_format(pool_size)} possible nicks.\n" +
f"Needed {iterations} iterations to find one " +
f"this short.\nYour nick is {nick} !\n")
print(f"Nick lenght is {len(nick)} characters.")
print(f"Nick landed at height {nick_id/(pool_size+1)} on the pool.")
print(f"Took {time.time()-t0} secs.\n")
@ -217,8 +216,9 @@ if __name__ == "__main__":
string = str(random.uniform(0, 1000000))
hash = hashlib.sha256(str.encode(string)).hexdigest()
print(
GenNick.short_from_SHA256(hash, max_length=max_length, max_iter=max_iter)[0]
)
GenNick.short_from_SHA256(hash,
max_length=max_length,
max_iter=max_iter)[0])
# Other analysis
GenNick.compute_pool_size_loss(max_length, max_iter, 200)

View File

@ -1,7 +1,10 @@
from math import log, floor
def human_format(number):
units = ["", " Thousand", " Million", " Billion", " Trillion", " Quatrillion"]
units = [
"", " Thousand", " Million", " Billion", " Trillion", " Quatrillion"
]
k = 1000.0
magnitude = int(floor(log(number, k)))
return "%.2f%s" % (number / k ** magnitude, units[magnitude])
return "%.2f%s" % (number / k**magnitude, units[magnitude])

View File

@ -1,18 +1,68 @@
from rest_framework import serializers
from .models import Order
class ListOrderSerializer(serializers.ModelSerializer):
class Meta:
model = Order
fields = ('id','status','created_at','expires_at','type','currency','amount','payment_method','is_explicit','premium','satoshis','maker','taker')
fields = (
"id",
"status",
"created_at",
"expires_at",
"type",
"currency",
"amount",
"payment_method",
"is_explicit",
"premium",
"satoshis",
"maker",
"taker",
)
class MakeOrderSerializer(serializers.ModelSerializer):
class Meta:
model = Order
fields = ('type','currency','amount','payment_method','is_explicit','premium','satoshis')
fields = (
"type",
"currency",
"amount",
"payment_method",
"is_explicit",
"premium",
"satoshis",
)
class UpdateOrderSerializer(serializers.Serializer):
invoice = serializers.CharField(max_length=2000, allow_null=True, allow_blank=True, default=None)
statement = serializers.CharField(max_length=10000, allow_null=True, allow_blank=True, default=None)
action = serializers.ChoiceField(choices=('take','update_invoice','submit_statement','dispute','cancel','confirm','rate_user','rate_platform'), allow_null=False)
rating = serializers.ChoiceField(choices=('1','2','3','4','5'), allow_null=True, allow_blank=True, default=None)
invoice = serializers.CharField(max_length=2000,
allow_null=True,
allow_blank=True,
default=None)
statement = serializers.CharField(max_length=10000,
allow_null=True,
allow_blank=True,
default=None)
action = serializers.ChoiceField(
choices=(
"take",
"update_invoice",
"submit_statement",
"dispute",
"cancel",
"confirm",
"rate_user",
"rate_platform",
),
allow_null=False,
)
rating = serializers.ChoiceField(
choices=("1", "2", "3", "4", "5"),
allow_null=True,
allow_blank=True,
default=None,
)

View File

@ -1,10 +1,11 @@
from celery import shared_task
@shared_task(name="users_cleansing")
def users_cleansing():
'''
"""
Deletes users never used 12 hours after creation
'''
"""
from django.contrib.auth.models import User
from django.db.models import Q
from .logics import Logics
@ -14,8 +15,8 @@ def users_cleansing():
# Users who's last login has not been in the last 6 hours
active_time_range = (timezone.now() - timedelta(hours=6), timezone.now())
queryset = User.objects.filter(~Q(last_login__range=active_time_range))
queryset = queryset.filter(is_staff=False) # Do not delete staff users
queryset = queryset.filter(is_staff=False) # Do not delete staff users
# And do not have an active trade or any past contract.
deleted_users = []
for user in queryset:
@ -27,14 +28,15 @@ def users_cleansing():
user.delete()
results = {
'num_deleted': len(deleted_users),
'deleted_users': deleted_users,
"num_deleted": len(deleted_users),
"deleted_users": deleted_users,
}
return results
@shared_task(name='follow_send_payment')
@shared_task(name="follow_send_payment")
def follow_send_payment(lnpayment):
'''Sends sats to buyer, continuous update'''
"""Sends sats to buyer, continuous update"""
from decouple import config
from base64 import b64decode
@ -44,60 +46,77 @@ def follow_send_payment(lnpayment):
from api.lightning.node import LNNode, MACAROON
from api.models import LNPayment, Order
fee_limit_sat = int(max(lnpayment.num_satoshis * float(config('PROPORTIONAL_ROUTING_FEE_LIMIT')), float(config('MIN_FLAT_ROUTING_FEE_LIMIT')))) # 200 ppm or 10 sats
fee_limit_sat = int(
max(
lnpayment.num_satoshis *
float(config("PROPORTIONAL_ROUTING_FEE_LIMIT")),
float(config("MIN_FLAT_ROUTING_FEE_LIMIT")),
)) # 200 ppm or 10 sats
request = LNNode.routerrpc.SendPaymentRequest(
payment_request=lnpayment.invoice,
fee_limit_sat=fee_limit_sat,
timeout_seconds=60) # time out payment in 60 seconds
timeout_seconds=60,
) # time out payment in 60 seconds
order = lnpayment.order_paid
try:
for response in LNNode.routerstub.SendPaymentV2(request, metadata=[('macaroon', MACAROON.hex())]):
if response.status == 0 : # Status 0 'UNKNOWN'
for response in LNNode.routerstub.SendPaymentV2(request,
metadata=[
("macaroon",
MACAROON.hex())
]):
if response.status == 0: # Status 0 'UNKNOWN'
# Not sure when this status happens
pass
pass
if response.status == 1 : # Status 1 'IN_FLIGHT'
print('IN_FLIGHT')
if response.status == 1: # Status 1 'IN_FLIGHT'
print("IN_FLIGHT")
lnpayment.status = LNPayment.Status.FLIGHT
lnpayment.save()
order.status = Order.Status.PAY
order.save()
if response.status == 3 : # Status 3 'FAILED'
print('FAILED')
if response.status == 3: # Status 3 'FAILED'
print("FAILED")
lnpayment.status = LNPayment.Status.FAILRO
lnpayment.last_routing_time = timezone.now()
lnpayment.routing_attempts += 1
lnpayment.save()
order.status = Order.Status.FAI
order.expires_at = timezone.now() + timedelta(seconds=Order.t_to_expire[Order.Status.FAI])
order.expires_at = timezone.now() + timedelta(
seconds=Order.t_to_expire[Order.Status.FAI])
order.save()
context = {'routing_failed': LNNode.payment_failure_context[response.failure_reason]}
context = {
"routing_failed":
LNNode.payment_failure_context[response.failure_reason]
}
print(context)
return False, context
if response.status == 2 : # Status 2 'SUCCEEDED'
print('SUCCEEDED')
if response.status == 2: # Status 2 'SUCCEEDED'
print("SUCCEEDED")
lnpayment.status = LNPayment.Status.SUCCED
lnpayment.save()
order.status = Order.Status.SUC
order.expires_at = timezone.now() + timedelta(seconds=Order.t_to_expire[Order.Status.SUC])
order.expires_at = timezone.now() + timedelta(
seconds=Order.t_to_expire[Order.Status.SUC])
order.save()
return True, None
except Exception as e:
if "invoice expired" in str(e):
print('INVOICE EXPIRED')
print("INVOICE EXPIRED")
lnpayment.status = LNPayment.Status.EXPIRE
lnpayment.last_routing_time = timezone.now()
lnpayment.save()
order.status = Order.Status.FAI
order.expires_at = timezone.now() + timedelta(seconds=Order.t_to_expire[Order.Status.FAI])
order.expires_at = timezone.now() + timedelta(
seconds=Order.t_to_expire[Order.Status.FAI])
order.save()
context = {'routing_failed':'The payout invoice has expired'}
context = {"routing_failed": "The payout invoice has expired"}
return False, context
@shared_task(name="cache_external_market_prices", ignore_result=True)
def cache_market():
@ -110,23 +129,26 @@ def cache_market():
exchange_rates = get_exchange_rates(currency_codes)
results = {}
for i in range(len(Currency.currency_dict.values())): # currecies are indexed starting at 1 (USD)
for i in range(len(Currency.currency_dict.values())
): # currecies are indexed starting at 1 (USD)
rate = exchange_rates[i]
rate = exchange_rates[i]
results[i] = {currency_codes[i], rate}
# Do not update if no new rate was found
if str(rate) == 'nan': continue
if str(rate) == "nan":
continue
# Create / Update database cached prices
currency_key = list(Currency.currency_dict.keys())[i]
Currency.objects.update_or_create(
id = int(currency_key),
currency = int(currency_key),
id=int(currency_key),
currency=int(currency_key),
# if there is a Cached market prices matching that id, it updates it with defaults below
defaults = {
'exchange_rate': float(rate),
'timestamp': timezone.now(),
})
defaults={
"exchange_rate": float(rate),
"timestamp": timezone.now(),
},
)
return results
return results

View File

@ -2,10 +2,16 @@ from django.urls import path
from .views import MakerView, OrderView, UserView, BookView, InfoView
urlpatterns = [
path('make/', MakerView.as_view()),
path('order/', OrderView.as_view({'get':'get','post':'take_update_confirm_dispute_cancel'})),
path('user/', UserView.as_view()),
path('book/', BookView.as_view()),
path("make/", MakerView.as_view()),
path(
"order/",
OrderView.as_view({
"get": "get",
"post": "take_update_confirm_dispute_cancel"
}),
),
path("user/", UserView.as_view()),
path("book/", BookView.as_view()),
# path('robot/') # Profile Info
path('info/', InfoView.as_view()),
]
path("info/", InfoView.as_view()),
]

View File

@ -1,4 +1,3 @@
import requests, ring, os
from decouple import config
import numpy as np
@ -7,35 +6,39 @@ from api.models import Order
market_cache = {}
@ring.dict(market_cache, expire=3) #keeps in cache for 3 seconds
@ring.dict(market_cache, expire=3) # keeps in cache for 3 seconds
def get_exchange_rates(currencies):
'''
"""
Params: list of currency codes.
Checks for exchange rates in several public APIs.
Returns the median price list.
'''
"""
APIS = config('MARKET_PRICE_APIS', cast=lambda v: [s.strip() for s in v.split(',')])
APIS = config("MARKET_PRICE_APIS",
cast=lambda v: [s.strip() for s in v.split(",")])
api_rates = []
for api_url in APIS:
try: # If one API is unavailable pass
if 'blockchain.info' in api_url:
try: # If one API is unavailable pass
if "blockchain.info" in api_url:
blockchain_prices = requests.get(api_url).json()
blockchain_rates = []
for currency in currencies:
try: # If a currency is missing place a None
blockchain_rates.append(float(blockchain_prices[currency]['last']))
try: # If a currency is missing place a None
blockchain_rates.append(
float(blockchain_prices[currency]["last"]))
except:
blockchain_rates.append(np.nan)
api_rates.append(blockchain_rates)
elif 'yadio.io' in api_url:
elif "yadio.io" in api_url:
yadio_prices = requests.get(api_url).json()
yadio_rates = []
for currency in currencies:
try:
yadio_rates.append(float(yadio_prices['BTC'][currency]))
yadio_rates.append(float(
yadio_prices["BTC"][currency]))
except:
yadio_rates.append(np.nan)
api_rates.append(yadio_rates)
@ -43,32 +46,36 @@ def get_exchange_rates(currencies):
pass
if len(api_rates) == 0:
return None # Wops there is not API available!
return None # Wops there is not API available!
exchange_rates = np.array(api_rates)
median_rates = np.nanmedian(exchange_rates, axis=0)
return median_rates.tolist()
def get_lnd_version():
# If dockerized, return LND_VERSION envvar used for docker image.
# Otherwise it would require LND's version.grpc libraries...
try:
lnd_version = config('LND_VERSION')
lnd_version = config("LND_VERSION")
return lnd_version
except:
pass
# If not dockerized and LND is local, read from CLI
try:
stream = os.popen('lnd --version')
stream = os.popen("lnd --version")
lnd_version = stream.read()[:-1]
return lnd_version
except:
return ''
return ""
robosats_commit_cache = {}
@ring.dict(robosats_commit_cache, expire=3600)
def get_commit_robosats():
@ -77,11 +84,15 @@ def get_commit_robosats():
return commit_hash
premium_percentile = {}
@ring.dict(premium_percentile, expire=300)
def compute_premium_percentile(order):
queryset = Order.objects.filter(currency=order.currency, status=Order.Status.PUB).exclude(id=order.id)
queryset = Order.objects.filter(
currency=order.currency, status=Order.Status.PUB).exclude(id=order.id)
print(len(queryset))
if len(queryset) <= 1:
@ -90,8 +101,8 @@ def compute_premium_percentile(order):
order_rate = float(order.last_satoshis) / float(order.amount)
rates = []
for similar_order in queryset:
rates.append(float(similar_order.last_satoshis) / float(similar_order.amount))
rates.append(
float(similar_order.last_satoshis) / float(similar_order.amount))
rates = np.array(rates)
return round(np.sum(rates < order_rate) / len(rates),2)
return round(np.sum(rates < order_rate) / len(rates), 2)

View File

@ -26,37 +26,46 @@ from django.utils import timezone
from django.conf import settings
from decouple import config
EXP_MAKER_BOND_INVOICE = int(config('EXP_MAKER_BOND_INVOICE'))
FEE = float(config('FEE'))
RETRY_TIME = int(config('RETRY_TIME'))
EXP_MAKER_BOND_INVOICE = int(config("EXP_MAKER_BOND_INVOICE"))
FEE = float(config("FEE"))
RETRY_TIME = int(config("RETRY_TIME"))
avatar_path = Path(settings.AVATAR_ROOT)
avatar_path.mkdir(parents=True, exist_ok=True)
# Create your views here.
class MakerView(CreateAPIView):
serializer_class = MakeOrderSerializer
def post(self,request):
class MakerView(CreateAPIView):
serializer_class = MakeOrderSerializer
def post(self, request):
serializer = self.serializer_class(data=request.data)
if not request.user.is_authenticated:
return Response({'bad_request':'Woops! It seems you do not have a robot avatar'}, status.HTTP_400_BAD_REQUEST)
return Response(
{
"bad_request":
"Woops! It seems you do not have a robot avatar"
},
status.HTTP_400_BAD_REQUEST,
)
if not serializer.is_valid(): return Response(status=status.HTTP_400_BAD_REQUEST)
if not serializer.is_valid():
return Response(status=status.HTTP_400_BAD_REQUEST)
type = serializer.data.get('type')
currency = serializer.data.get('currency')
amount = serializer.data.get('amount')
payment_method = serializer.data.get('payment_method')
premium = serializer.data.get('premium')
satoshis = serializer.data.get('satoshis')
is_explicit = serializer.data.get('is_explicit')
type = serializer.data.get("type")
currency = serializer.data.get("currency")
amount = serializer.data.get("amount")
payment_method = serializer.data.get("payment_method")
premium = serializer.data.get("premium")
satoshis = serializer.data.get("satoshis")
is_explicit = serializer.data.get("is_explicit")
valid, context, _ = Logics.validate_already_maker_or_taker(request.user)
if not valid: return Response(context, status.HTTP_409_CONFLICT)
valid, context, _ = Logics.validate_already_maker_or_taker(
request.user)
if not valid:
return Response(context, status.HTTP_409_CONFLICT)
# Creates a new order
order = Order(
@ -67,67 +76,93 @@ class MakerView(CreateAPIView):
premium=premium,
satoshis=satoshis,
is_explicit=is_explicit,
expires_at=timezone.now()+timedelta(seconds=EXP_MAKER_BOND_INVOICE), # TODO Move to class method
maker=request.user)
expires_at=timezone.now() + timedelta(
seconds=EXP_MAKER_BOND_INVOICE), # TODO Move to class method
maker=request.user,
)
# TODO move to Order class method when new instance is created!
order.last_satoshis = order.t0_satoshis = Logics.satoshis_now(order)
valid, context = Logics.validate_order_size(order)
if not valid: return Response(context, status.HTTP_400_BAD_REQUEST)
if not valid:
return Response(context, status.HTTP_400_BAD_REQUEST)
order.save()
return Response(ListOrderSerializer(order).data, status=status.HTTP_201_CREATED)
return Response(ListOrderSerializer(order).data,
status=status.HTTP_201_CREATED)
class OrderView(viewsets.ViewSet):
serializer_class = UpdateOrderSerializer
lookup_url_kwarg = 'order_id'
lookup_url_kwarg = "order_id"
def get(self, request, format=None):
'''
"""
Full trade pipeline takes place while looking/refreshing the order page.
'''
"""
order_id = request.GET.get(self.lookup_url_kwarg)
if not request.user.is_authenticated:
return Response({'bad_request':'You must have a robot avatar to see the order details'}, status=status.HTTP_400_BAD_REQUEST)
return Response(
{
"bad_request":
"You must have a robot avatar to see the order details"
},
status=status.HTTP_400_BAD_REQUEST,
)
if order_id == None:
return Response({'bad_request':'Order ID parameter not found in request'}, status=status.HTTP_400_BAD_REQUEST)
return Response(
{"bad_request": "Order ID parameter not found in request"},
status=status.HTTP_400_BAD_REQUEST,
)
order = Order.objects.filter(id=order_id)
# check if exactly one order is found in the db
if len(order) != 1 :
return Response({'bad_request':'Invalid Order Id'}, status.HTTP_404_NOT_FOUND)
if len(order) != 1:
return Response({"bad_request": "Invalid Order Id"},
status.HTTP_404_NOT_FOUND)
# This is our order.
order = order[0]
# 2) If order has been cancelled
if order.status == Order.Status.UCA:
return Response({'bad_request':'This order has been cancelled by the maker'},status.HTTP_400_BAD_REQUEST)
return Response(
{"bad_request": "This order has been cancelled by the maker"},
status.HTTP_400_BAD_REQUEST,
)
if order.status == Order.Status.CCA:
return Response({'bad_request':'This order has been cancelled collaborativelly'},status.HTTP_400_BAD_REQUEST)
return Response(
{
"bad_request":
"This order has been cancelled collaborativelly"
},
status.HTTP_400_BAD_REQUEST,
)
data = ListOrderSerializer(order).data
data['total_secs_exp'] = Order.t_to_expire[order.status]
data["total_secs_exp"] = Order.t_to_expire[order.status]
# if user is under a limit (penalty), inform him.
is_penalized, time_out = Logics.is_penalized(request.user)
if is_penalized:
data['penalty'] = request.user.profile.penalty_expiration
data["penalty"] = request.user.profile.penalty_expiration
# Add booleans if user is maker, taker, partipant, buyer or seller
data['is_maker'] = order.maker == request.user
data['is_taker'] = order.taker == request.user
data['is_participant'] = data['is_maker'] or data['is_taker']
data["is_maker"] = order.maker == request.user
data["is_taker"] = order.taker == request.user
data["is_participant"] = data["is_maker"] or data["is_taker"]
# 3.a) If not a participant and order is not public, forbid.
if not data['is_participant'] and order.status != Order.Status.PUB:
return Response({'bad_request':'You are not allowed to see this order'},status.HTTP_403_FORBIDDEN)
if not data["is_participant"] and order.status != Order.Status.PUB:
return Response(
{"bad_request": "You are not allowed to see this order"},
status.HTTP_403_FORBIDDEN,
)
# WRITE Update last_seen for maker and taker.
# Note down that the taker/maker was here recently, so counterpart knows if the user is paying attention.
if order.maker == request.user:
@ -139,93 +174,108 @@ class OrderView(viewsets.ViewSet):
# Add activity status of participants based on last_seen
if order.taker_last_seen != None:
data['taker_status'] = Logics.user_activity_status(order.taker_last_seen)
data["taker_status"] = Logics.user_activity_status(
order.taker_last_seen)
if order.maker_last_seen != None:
data['maker_status'] = Logics.user_activity_status(order.maker_last_seen)
data["maker_status"] = Logics.user_activity_status(
order.maker_last_seen)
# 3.b If order is between public and WF2
if order.status >= Order.Status.PUB and order.status < Order.Status.WF2:
data['price_now'], data['premium_now'] = Logics.price_and_premium_now(order)
data["price_now"], data[
"premium_now"] = Logics.price_and_premium_now(order)
# 3. c) If maker and Public, add num robots in book, premium percentile and num similar orders.
if data["is_maker"] and order.status == Order.Status.PUB:
data["premium_percentile"] = compute_premium_percentile(order)
data["num_similar_orders"] = len(
Order.objects.filter(currency=order.currency,
status=Order.Status.PUB))
# 3. c) If maker and Public, add num robots in book, premium percentile and num similar orders.
if data['is_maker'] and order.status == Order.Status.PUB:
data['premium_percentile'] = compute_premium_percentile(order)
data['num_similar_orders'] = len(Order.objects.filter(currency=order.currency, status=Order.Status.PUB))
# 4) Non participants can view details (but only if PUB)
elif not data['is_participant'] and order.status != Order.Status.PUB:
return Response(data, status=status.HTTP_200_OK)
elif not data["is_participant"] and order.status != Order.Status.PUB:
return Response(data, status=status.HTTP_200_OK)
# For participants add positions, nicks and status as a message and hold invoices status
data['is_buyer'] = Logics.is_buyer(order,request.user)
data['is_seller'] = Logics.is_seller(order,request.user)
data['maker_nick'] = str(order.maker)
data['taker_nick'] = str(order.taker)
data['status_message'] = Order.Status(order.status).label
data['is_fiat_sent'] = order.is_fiat_sent
data['is_disputed'] = order.is_disputed
data['ur_nick'] = request.user.username
data["is_buyer"] = Logics.is_buyer(order, request.user)
data["is_seller"] = Logics.is_seller(order, request.user)
data["maker_nick"] = str(order.maker)
data["taker_nick"] = str(order.taker)
data["status_message"] = Order.Status(order.status).label
data["is_fiat_sent"] = order.is_fiat_sent
data["is_disputed"] = order.is_disputed
data["ur_nick"] = request.user.username
# Add whether hold invoices are LOCKED (ACCEPTED)
# Is there a maker bond? If so, True if locked, False otherwise
if order.maker_bond:
data['maker_locked'] = order.maker_bond.status == LNPayment.Status.LOCKED
data[
"maker_locked"] = order.maker_bond.status == LNPayment.Status.LOCKED
else:
data['maker_locked'] = False
data["maker_locked"] = False
# Is there a taker bond? If so, True if locked, False otherwise
if order.taker_bond:
data['taker_locked'] = order.taker_bond.status == LNPayment.Status.LOCKED
data[
"taker_locked"] = order.taker_bond.status == LNPayment.Status.LOCKED
else:
data['taker_locked'] = False
data["taker_locked"] = False
# Is there an escrow? If so, True if locked, False otherwise
if order.trade_escrow:
data['escrow_locked'] = order.trade_escrow.status == LNPayment.Status.LOCKED
data[
"escrow_locked"] = order.trade_escrow.status == LNPayment.Status.LOCKED
else:
data['escrow_locked'] = False
data["escrow_locked"] = False
# If both bonds are locked, participants can see the final trade amount in sats.
if order.taker_bond:
if order.maker_bond.status == order.taker_bond.status == LNPayment.Status.LOCKED:
if (order.maker_bond.status == order.taker_bond.status ==
LNPayment.Status.LOCKED):
# Seller sees the amount he sends
if data['is_seller']:
data['trade_satoshis'] = order.last_satoshis
if data["is_seller"]:
data["trade_satoshis"] = order.last_satoshis
# Buyer sees the amount he receives
elif data['is_buyer']:
data['trade_satoshis'] = Logics.payout_amount(order, request.user)[1]['invoice_amount']
elif data["is_buyer"]:
data["trade_satoshis"] = Logics.payout_amount(
order, request.user)[1]["invoice_amount"]
# 5) If status is 'waiting for maker bond' and user is MAKER, reply with a MAKER hold invoice.
if order.status == Order.Status.WFB and data['is_maker']:
if order.status == Order.Status.WFB and data["is_maker"]:
valid, context = Logics.gen_maker_hold_invoice(order, request.user)
if valid:
data = {**data, **context}
else:
return Response(context, status.HTTP_400_BAD_REQUEST)
# 6) If status is 'waiting for taker bond' and user is TAKER, reply with a TAKER hold invoice.
elif order.status == Order.Status.TAK and data['is_taker']:
elif order.status == Order.Status.TAK and data["is_taker"]:
valid, context = Logics.gen_taker_hold_invoice(order, request.user)
if valid:
data = {**data, **context}
else:
return Response(context, status.HTTP_400_BAD_REQUEST)
# 7 a. ) If seller and status is 'WF2' or 'WFE'
elif data['is_seller'] and (order.status == Order.Status.WF2 or order.status == Order.Status.WFE):
# 7 a. ) If seller and status is 'WF2' or 'WFE'
elif data["is_seller"] and (order.status == Order.Status.WF2
or order.status == Order.Status.WFE):
# If the two bonds are locked, reply with an ESCROW hold invoice.
if order.maker_bond.status == order.taker_bond.status == LNPayment.Status.LOCKED:
valid, context = Logics.gen_escrow_hold_invoice(order, request.user)
if (order.maker_bond.status == order.taker_bond.status ==
LNPayment.Status.LOCKED):
valid, context = Logics.gen_escrow_hold_invoice(
order, request.user)
if valid:
data = {**data, **context}
else:
return Response(context, status.HTTP_400_BAD_REQUEST)
# 7.b) If user is Buyer and status is 'WF2' or 'WFI'
elif data['is_buyer'] and (order.status == Order.Status.WF2 or order.status == Order.Status.WFI):
# 7.b) If user is Buyer and status is 'WF2' or 'WFI'
elif data["is_buyer"] and (order.status == Order.Status.WF2
or order.status == Order.Status.WFI):
# If the two bonds are locked, reply with an AMOUNT so he can send the buyer invoice.
if order.maker_bond.status == order.taker_bond.status == LNPayment.Status.LOCKED:
if (order.maker_bond.status == order.taker_bond.status ==
LNPayment.Status.LOCKED):
valid, context = Logics.payout_amount(order, request.user)
if valid:
data = {**data, **context}
@ -233,148 +283,185 @@ class OrderView(viewsets.ViewSet):
return Response(context, status.HTTP_400_BAD_REQUEST)
# 8) If status is 'CHA' or 'FSE' and all HTLCS are in LOCKED
elif order.status in [Order.Status.WFI, Order.Status.CHA, Order.Status.FSE]:
elif order.status in [
Order.Status.WFI, Order.Status.CHA, Order.Status.FSE
]:
# If all bonds are locked.
if order.maker_bond.status == order.taker_bond.status == order.trade_escrow.status == LNPayment.Status.LOCKED:
if (order.maker_bond.status == order.taker_bond.status ==
order.trade_escrow.status == LNPayment.Status.LOCKED):
# add whether a collaborative cancel is pending or has been asked
if (data['is_maker'] and order.taker_asked_cancel) or (data['is_taker'] and order.maker_asked_cancel):
data['pending_cancel'] = True
elif (data['is_maker'] and order.maker_asked_cancel) or (data['is_taker'] and order.taker_asked_cancel):
data['asked_for_cancel'] = True
if (data["is_maker"] and order.taker_asked_cancel) or (
data["is_taker"] and order.maker_asked_cancel):
data["pending_cancel"] = True
elif (data["is_maker"] and order.maker_asked_cancel) or (
data["is_taker"] and order.taker_asked_cancel):
data["asked_for_cancel"] = True
else:
data['asked_for_cancel'] = False
data["asked_for_cancel"] = False
# 9) If status is 'DIS' and all HTLCS are in LOCKED
elif order.status == Order.Status.DIS:
# add whether the dispute statement has been received
if data['is_maker']:
data['statement_submitted'] = (order.maker_statement != None and order.maker_statement != "")
elif data['is_taker']:
data['statement_submitted'] = (order.taker_statement != None and order.maker_statement != "")
if data["is_maker"]:
data["statement_submitted"] = (order.maker_statement != None
and order.maker_statement != "")
elif data["is_taker"]:
data["statement_submitted"] = (order.taker_statement != None
and order.maker_statement != "")
# 9) If status is 'Failed routing', reply with retry amounts, time of next retry and ask for invoice at third.
elif order.status == Order.Status.FAI and order.payout.receiver == request.user: # might not be the buyer if after a dispute where winner wins
data['retries'] = order.payout.routing_attempts
data['next_retry_time'] = order.payout.last_routing_time + timedelta(minutes=RETRY_TIME)
elif (order.status == Order.Status.FAI
and order.payout.receiver == request.user
): # might not be the buyer if after a dispute where winner wins
data["retries"] = order.payout.routing_attempts
data[
"next_retry_time"] = order.payout.last_routing_time + timedelta(
minutes=RETRY_TIME)
if order.payout.status == LNPayment.Status.EXPIRE:
data['invoice_expired'] = True
data["invoice_expired"] = True
# Add invoice amount once again if invoice was expired.
data['invoice_amount'] = int(order.last_satoshis * (1-FEE))
data["invoice_amount"] = int(order.last_satoshis * (1 - FEE))
return Response(data, status.HTTP_200_OK)
def take_update_confirm_dispute_cancel(self, request, format=None):
'''
"""
Here takes place all of the updates to the order object.
That is: take, confim, cancel, dispute, update_invoice or rate.
'''
"""
order_id = request.GET.get(self.lookup_url_kwarg)
serializer = UpdateOrderSerializer(data=request.data)
if not serializer.is_valid(): return Response(status=status.HTTP_400_BAD_REQUEST)
if not serializer.is_valid():
return Response(status=status.HTTP_400_BAD_REQUEST)
order = Order.objects.get(id=order_id)
# action is either 1)'take', 2)'confirm', 3)'cancel', 4)'dispute' , 5)'update_invoice'
# action is either 1)'take', 2)'confirm', 3)'cancel', 4)'dispute' , 5)'update_invoice'
# 6)'submit_statement' (in dispute), 7)'rate_user' , 'rate_platform'
action = serializer.data.get('action')
invoice = serializer.data.get('invoice')
statement = serializer.data.get('statement')
rating = serializer.data.get('rating')
action = serializer.data.get("action")
invoice = serializer.data.get("invoice")
statement = serializer.data.get("statement")
rating = serializer.data.get("rating")
# 1) If action is take, it is a taker request!
if action == 'take':
if order.status == Order.Status.PUB:
valid, context, _ = Logics.validate_already_maker_or_taker(request.user)
if not valid: return Response(context, status=status.HTTP_409_CONFLICT)
if action == "take":
if order.status == Order.Status.PUB:
valid, context, _ = Logics.validate_already_maker_or_taker(
request.user)
if not valid:
return Response(context, status=status.HTTP_409_CONFLICT)
valid, context = Logics.take(order, request.user)
if not valid: return Response(context, status=status.HTTP_403_FORBIDDEN)
if not valid:
return Response(context, status=status.HTTP_403_FORBIDDEN)
return self.get(request)
else: Response({'bad_request':'This order is not public anymore.'}, status.HTTP_400_BAD_REQUEST)
else:
Response(
{"bad_request": "This order is not public anymore."},
status.HTTP_400_BAD_REQUEST,
)
# Any other action is only allowed if the user is a participant
if not (order.maker == request.user or order.taker == request.user):
return Response({'bad_request':'You are not a participant in this order'}, status.HTTP_403_FORBIDDEN)
return Response(
{"bad_request": "You are not a participant in this order"},
status.HTTP_403_FORBIDDEN,
)
# 2) If action is 'update invoice'
if action == 'update_invoice' and invoice:
valid, context = Logics.update_invoice(order,request.user,invoice)
if not valid: return Response(context, status.HTTP_400_BAD_REQUEST)
if action == "update_invoice" and invoice:
valid, context = Logics.update_invoice(order, request.user,
invoice)
if not valid:
return Response(context, status.HTTP_400_BAD_REQUEST)
# 3) If action is cancel
elif action == 'cancel':
valid, context = Logics.cancel_order(order,request.user)
if not valid: return Response(context, status.HTTP_400_BAD_REQUEST)
elif action == "cancel":
valid, context = Logics.cancel_order(order, request.user)
if not valid:
return Response(context, status.HTTP_400_BAD_REQUEST)
# 4) If action is confirm
elif action == 'confirm':
valid, context = Logics.confirm_fiat(order,request.user)
if not valid: return Response(context, status.HTTP_400_BAD_REQUEST)
elif action == "confirm":
valid, context = Logics.confirm_fiat(order, request.user)
if not valid:
return Response(context, status.HTTP_400_BAD_REQUEST)
# 5) If action is dispute
elif action == 'dispute':
valid, context = Logics.open_dispute(order,request.user)
if not valid: return Response(context, status.HTTP_400_BAD_REQUEST)
elif action == "dispute":
valid, context = Logics.open_dispute(order, request.user)
if not valid:
return Response(context, status.HTTP_400_BAD_REQUEST)
elif action == 'submit_statement':
valid, context = Logics.dispute_statement(order,request.user, statement)
if not valid: return Response(context, status.HTTP_400_BAD_REQUEST)
elif action == "submit_statement":
valid, context = Logics.dispute_statement(order, request.user,
statement)
if not valid:
return Response(context, status.HTTP_400_BAD_REQUEST)
# 6) If action is rate
elif action == 'rate_user' and rating:
valid, context = Logics.rate_counterparty(order,request.user, rating)
if not valid: return Response(context, status.HTTP_400_BAD_REQUEST)
elif action == "rate_user" and rating:
valid, context = Logics.rate_counterparty(order, request.user,
rating)
if not valid:
return Response(context, status.HTTP_400_BAD_REQUEST)
# 6) If action is rate_platform
elif action == 'rate_platform' and rating:
elif action == "rate_platform" and rating:
valid, context = Logics.rate_platform(request.user, rating)
if not valid: return Response(context, status.HTTP_400_BAD_REQUEST)
if not valid:
return Response(context, status.HTTP_400_BAD_REQUEST)
# If nothing of the above... something else is going on. Probably not allowed!
else:
return Response(
{'bad_request':
'The Robotic Satoshis working in the warehouse did not understand you. ' +
'Please, fill a Bug Issue in Github https://github.com/reckless-satoshi/robosats/issues'},
status.HTTP_501_NOT_IMPLEMENTED)
{
"bad_request":
"The Robotic Satoshis working in the warehouse did not understand you. "
+
"Please, fill a Bug Issue in Github https://github.com/reckless-satoshi/robosats/issues"
},
status.HTTP_501_NOT_IMPLEMENTED,
)
return self.get(request)
class UserView(APIView):
lookup_url_kwarg = 'token'
NickGen = NickGenerator(
lang='English',
use_adv=False,
use_adj=True,
use_noun=True,
max_num=999)
lookup_url_kwarg = "token"
NickGen = NickGenerator(lang="English",
use_adv=False,
use_adj=True,
use_noun=True,
max_num=999)
# Probably should be turned into a post method
def get(self,request, format=None):
'''
def get(self, request, format=None):
"""
Get a new user derived from a high entropy token
- Request has a high-entropy token,
- Generates new nickname and avatar.
- Creates login credentials (new User object)
Response with Avatar and Nickname.
'''
"""
# If an existing user opens the main page by mistake, we do not want it to create a new nickname/profile for him
if request.user.is_authenticated:
context = {'nickname': request.user.username}
not_participant, _, _ = Logics.validate_already_maker_or_taker(request.user)
context = {"nickname": request.user.username}
not_participant, _, _ = Logics.validate_already_maker_or_taker(
request.user)
# Does not allow this 'mistake' if an active order
if not not_participant:
context['bad_request'] = f'You are already logged in as {request.user} and have an active order'
context[
"bad_request"] = f"You are already logged in as {request.user} and have an active order"
return Response(context, status.HTTP_400_BAD_REQUEST)
# Does not allow this 'mistake' if the last login was sometime ago (5 minutes)
# if request.user.last_login < timezone.now() - timedelta(minutes=5):
# context['bad_request'] = f'You are already logged in as {request.user}'
@ -387,35 +474,40 @@ class UserView(APIView):
shannon_entropy = entropy(counts, base=62)
bits_entropy = log2(len(value)**len(token))
# Payload
context = {'token_shannon_entropy': shannon_entropy, 'token_bits_entropy': bits_entropy}
context = {
"token_shannon_entropy": shannon_entropy,
"token_bits_entropy": bits_entropy,
}
# Deny user gen if entropy below 128 bits or 0.7 shannon heterogeneity
if bits_entropy < 128 or shannon_entropy < 0.7:
context['bad_request'] = 'The token does not have enough entropy'
context["bad_request"] = "The token does not have enough entropy"
return Response(context, status=status.HTTP_400_BAD_REQUEST)
# Hash the token, only 1 iteration.
hash = hashlib.sha256(str.encode(token)).hexdigest()
hash = hashlib.sha256(str.encode(token)).hexdigest()
# Generate nickname deterministically
nickname = self.NickGen.short_from_SHA256(hash, max_length=18)[0]
context['nickname'] = nickname
nickname = self.NickGen.short_from_SHA256(hash, max_length=18)[0]
context["nickname"] = nickname
# Generate avatar
rh = Robohash(hash)
rh.assemble(roboset='set1', bgset='any')# for backgrounds ON
rh.assemble(roboset="set1", bgset="any") # for backgrounds ON
# Does not replace image if existing (avoid re-avatar in case of nick collusion)
image_path = avatar_path.joinpath(nickname+".png")
image_path = avatar_path.joinpath(nickname + ".png")
if not image_path.exists():
with open(image_path, "wb") as f:
rh.img.save(f, format="png")
# Create new credentials and login if nickname is new
if len(User.objects.filter(username=nickname)) == 0:
User.objects.create_user(username=nickname, password=token, is_staff=False)
User.objects.create_user(username=nickname,
password=token,
is_staff=False)
user = authenticate(request, username=nickname, password=token)
user.profile.avatar = "static/assets/avatars/" + nickname + '.png'
user.profile.avatar = "static/assets/avatars/" + nickname + ".png"
login(request, user)
return Response(context, status=status.HTTP_201_CREATED)
@ -424,17 +516,19 @@ class UserView(APIView):
if user is not None:
login(request, user)
# Sends the welcome back message, only if created +3 mins ago
if request.user.date_joined < (timezone.now()-timedelta(minutes=3)):
context['found'] = 'We found your Robot avatar. Welcome back!'
if request.user.date_joined < (timezone.now() -
timedelta(minutes=3)):
context[
"found"] = "We found your Robot avatar. Welcome back!"
return Response(context, status=status.HTTP_202_ACCEPTED)
else:
# It is unlikely, but maybe the nickname is taken (1 in 20 Billion change)
context['found'] = 'Bad luck, this nickname is taken'
context['bad_request'] = 'Enter a different token'
context["found"] = "Bad luck, this nickname is taken"
context["bad_request"] = "Enter a different token"
return Response(context, status.HTTP_403_FORBIDDEN)
def delete(self,request):
''' Pressing "give me another" deletes the logged in user '''
def delete(self, request):
"""Pressing "give me another" deletes the logged in user"""
user = request.user
if not user.is_authenticated:
return Response(status.HTTP_403_FORBIDDEN)
@ -446,62 +540,95 @@ class UserView(APIView):
# Check if it is not a maker or taker!
not_participant, _, _ = Logics.validate_already_maker_or_taker(user)
if not not_participant:
return Response({'bad_request':'Maybe a mistake? User cannot be deleted while he is part of an order'}, status.HTTP_400_BAD_REQUEST)
return Response(
{
"bad_request":
"Maybe a mistake? User cannot be deleted while he is part of an order"
},
status.HTTP_400_BAD_REQUEST,
)
# Check if has already a profile with
if user.profile.total_contracts > 0:
return Response({'bad_request':'Maybe a mistake? User cannot be deleted as it has completed trades'}, status.HTTP_400_BAD_REQUEST)
return Response(
{
"bad_request":
"Maybe a mistake? User cannot be deleted as it has completed trades"
},
status.HTTP_400_BAD_REQUEST,
)
logout(request)
user.delete()
return Response({'user_deleted':'User deleted permanently'}, status.HTTP_301_MOVED_PERMANENTLY)
return Response(
{"user_deleted": "User deleted permanently"},
status.HTTP_301_MOVED_PERMANENTLY,
)
class BookView(ListAPIView):
serializer_class = ListOrderSerializer
queryset = Order.objects.filter(status=Order.Status.PUB)
queryset = Order.objects.filter(status=Order.Status.PUB)
def get(self, request, format=None):
currency = request.GET.get("currency")
type = request.GET.get("type")
def get(self,request, format=None):
currency = request.GET.get('currency')
type = request.GET.get('type')
queryset = Order.objects.filter(status=Order.Status.PUB)
# Currency 0 and type 2 are special cases treated as "ANY". (These are not really possible choices)
if int(currency) == 0 and int(type) != 2:
queryset = Order.objects.filter(type=type, status=Order.Status.PUB)
queryset = Order.objects.filter(type=type, status=Order.Status.PUB)
elif int(type) == 2 and int(currency) != 0:
queryset = Order.objects.filter(currency=currency, status=Order.Status.PUB)
queryset = Order.objects.filter(currency=currency,
status=Order.Status.PUB)
elif not (int(currency) == 0 and int(type) == 2):
queryset = Order.objects.filter(currency=currency, type=type, status=Order.Status.PUB)
queryset = Order.objects.filter(currency=currency,
type=type,
status=Order.Status.PUB)
if len(queryset)== 0:
return Response({'not_found':'No orders found, be the first to make one'}, status=status.HTTP_404_NOT_FOUND)
if len(queryset) == 0:
return Response(
{"not_found": "No orders found, be the first to make one"},
status=status.HTTP_404_NOT_FOUND,
)
book_data = []
for order in queryset:
data = ListOrderSerializer(order).data
data['maker_nick'] = str(order.maker)
data["maker_nick"] = str(order.maker)
# Compute current premium for those orders that are explicitly priced.
data['price'], data['premium'] = Logics.price_and_premium_now(order)
data['maker_status'] = Logics.user_activity_status(order.maker_last_seen)
for key in ('status','taker'): # Non participants should not see the status or who is the taker
data["price"], data["premium"] = Logics.price_and_premium_now(
order)
data["maker_status"] = Logics.user_activity_status(
order.maker_last_seen)
for key in (
"status",
"taker",
): # Non participants should not see the status or who is the taker
del data[key]
book_data.append(data)
return Response(book_data, status=status.HTTP_200_OK)
class InfoView(ListAPIView):
def get(self, request):
context = {}
context['num_public_buy_orders'] = len(Order.objects.filter(type=Order.Types.BUY, status=Order.Status.PUB))
context['num_public_sell_orders'] = len(Order.objects.filter(type=Order.Types.SELL, status=Order.Status.PUB))
context["num_public_buy_orders"] = len(
Order.objects.filter(type=Order.Types.BUY,
status=Order.Status.PUB))
context["num_public_sell_orders"] = len(
Order.objects.filter(type=Order.Types.SELL,
status=Order.Status.PUB))
# Number of active users (logged in in last 30 minutes)
today = datetime.today()
context['active_robots_today'] = len(User.objects.filter(last_login__day=today.day))
context["active_robots_today"] = len(
User.objects.filter(last_login__day=today.day))
# Compute average premium and volume of today
queryset = MarketTick.objects.filter(timestamp__day=today.day)
@ -509,9 +636,9 @@ class InfoView(ListAPIView):
weighted_premiums = []
volumes = []
for tick in queryset:
weighted_premiums.append(tick.premium*tick.volume)
weighted_premiums.append(tick.premium * tick.volume)
volumes.append(tick.volume)
total_volume = sum(volumes)
# Avg_premium is the weighted average of the premiums by volume
avg_premium = sum(weighted_premiums) / total_volume
@ -524,28 +651,27 @@ class InfoView(ListAPIView):
volume_settled = []
for tick in queryset:
volume_settled.append(tick.volume)
lifetime_volume_settled = int(sum(volume_settled)*100000000)
lifetime_volume_settled = int(sum(volume_settled) * 100000000)
else:
lifetime_volume_settled = 0
context['today_avg_nonkyc_btc_premium'] = round(avg_premium,2)
context['today_total_volume'] = total_volume
context['lifetime_satoshis_settled'] = lifetime_volume_settled
context['lnd_version'] = get_lnd_version()
context['robosats_running_commit_hash'] = get_commit_robosats()
context['alternative_site'] = config('ALTERNATIVE_SITE')
context['alternative_name'] = config('ALTERNATIVE_NAME')
context['node_alias'] = config('NODE_ALIAS')
context['node_id'] = config('NODE_ID')
context['network'] = config('NETWORK')
context['fee'] = FEE
context['bond_size'] = float(config('BOND_SIZE'))
context["today_avg_nonkyc_btc_premium"] = round(avg_premium, 2)
context["today_total_volume"] = total_volume
context["lifetime_satoshis_settled"] = lifetime_volume_settled
context["lnd_version"] = get_lnd_version()
context["robosats_running_commit_hash"] = get_commit_robosats()
context["alternative_site"] = config("ALTERNATIVE_SITE")
context["alternative_name"] = config("ALTERNATIVE_NAME")
context["node_alias"] = config("NODE_ALIAS")
context["node_id"] = config("NODE_ID")
context["network"] = config("NETWORK")
context["fee"] = FEE
context["bond_size"] = float(config("BOND_SIZE"))
if request.user.is_authenticated:
context['nickname'] = request.user.username
has_no_active_order, _, order = Logics.validate_already_maker_or_taker(request.user)
context["nickname"] = request.user.username
has_no_active_order, _, order = Logics.validate_already_maker_or_taker(
request.user)
if not has_no_active_order:
context['active_order_id'] = order.id
context["active_order_id"] = order.id
return Response(context, status.HTTP_200_OK)

View File

@ -2,5 +2,5 @@ from django.apps import AppConfig
class ChatConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'chat'
default_auto_field = "django.db.models.BigAutoField"
name = "chat"

View File

@ -4,12 +4,12 @@ from api.models import Order
import json
class ChatRoomConsumer(AsyncWebsocketConsumer):
async def connect(self):
self.order_id = self.scope['url_route']['kwargs']['order_id']
self.room_group_name = f'chat_order_{self.order_id}'
self.order_id = self.scope["url_route"]["kwargs"]["order_id"]
self.room_group_name = f"chat_order_{self.order_id}"
self.user = self.scope["user"]
self.user_nick = str(self.user)
@ -21,49 +21,45 @@ class ChatRoomConsumer(AsyncWebsocketConsumer):
# if not (Logics.is_buyer(order[0], self.user) or Logics.is_seller(order[0], self.user)):
# print ("Outta this chat")
# return False
await self.channel_layer.group_add(
self.room_group_name,
self.channel_name
)
await self.channel_layer.group_add(self.room_group_name,
self.channel_name)
await self.accept()
async def disconnect(self, close_code):
await self.channel_layer.group_discard(
self.room_group_name,
self.channel_name
)
await self.channel_layer.group_discard(self.room_group_name,
self.channel_name)
async def receive(self, text_data):
text_data_json = json.loads(text_data)
message = text_data_json['message']
nick = text_data_json['nick']
message = text_data_json["message"]
nick = text_data_json["nick"]
await self.channel_layer.group_send(
self.room_group_name,
{
'type': 'chatroom_message',
'message': message,
'nick': nick,
}
"type": "chatroom_message",
"message": message,
"nick": nick,
},
)
async def chatroom_message(self, event):
message = event['message']
nick = event['nick']
message = event["message"]
nick = event["nick"]
# Insert a white space in words longer than 22 characters.
# Helps when messages overflow in a single line.
words = message.split(' ')
fix_message = ''
words = message.split(" ")
fix_message = ""
for word in words:
word = ' '.join(word[i:i+22] for i in range(0, len(word), 22))
fix_message = fix_message +' '+ word
word = " ".join(word[i:i + 22] for i in range(0, len(word), 22))
fix_message = fix_message + " " + word
await self.send(text_data=json.dumps({
'message': fix_message,
'user_nick': nick,
"message": fix_message,
"user_nick": nick,
}))
pass
pass

View File

@ -2,5 +2,6 @@ from django.urls import re_path
from . import consumers
websocket_urlpatterns = [
re_path(r'ws/chat/(?P<order_id>\w+)/$', consumers.ChatRoomConsumer.as_asgi()),
]
re_path(r"ws/chat/(?P<order_id>\w+)/$",
consumers.ChatRoomConsumer.as_asgi()),
]

View File

@ -5,4 +5,4 @@ from . import views
# urlpatterns = [
# path('', views.index, name='index'),
# path('<str:order_id>/', views.room, name='order_chat'),
# ]
# ]

View File

@ -1,7 +1,6 @@
from django.shortcuts import render
# def room(request, order_id):
# return render(request, 'chatroom.html', {
# 'order_id': order_id
# })
# })

View File

@ -2,5 +2,5 @@ from django.apps import AppConfig
class FrontendConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'frontend'
default_auto_field = "django.db.models.BigAutoField"
name = "frontend"

View File

@ -2,11 +2,11 @@ from django.urls import path
from .views import index
urlpatterns = [
path('', index),
path('info/', index),
path('login/', index),
path('make/', index),
path('book/', index),
path('order/<int:orderId>', index),
path('wait/', index),
]
path("", index),
path("info/", index),
path("login/", index),
path("make/", index),
path("book/", index),
path("order/<int:orderId>", index),
path("wait/", index),
]

View File

@ -1,7 +1,9 @@
from django.shortcuts import render
from decouple import config
# Create your views here.
def index(request, *args, **kwargs):
context={'ONION_LOCATION': config('ONION_LOCATION')}
return render(request, 'frontend/index.html', context=context)
context = {"ONION_LOCATION": config("ONION_LOCATION")}
return render(request, "frontend/index.html", context=context)

View File

@ -6,7 +6,7 @@ import sys
def main():
"""Run administrative tasks."""
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'robosats.settings')
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "robosats.settings")
try:
from django.core.management import execute_from_command_line
except ImportError as exc:
@ -18,5 +18,5 @@ def main():
execute_from_command_line(sys.argv)
if __name__ == '__main__':
if __name__ == "__main__":
main()

View File

@ -4,4 +4,4 @@ from __future__ import absolute_import, unicode_literals
# Django starts so that shared_task will use this app.
from .celery import app as celery_app
__all__ = ('celery_app',)
__all__ = ("celery_app", )

View File

@ -11,7 +11,7 @@ import os
import django
from channels.routing import get_default_application
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'tabulator.settings')
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "tabulator.settings")
django.setup()

View File

@ -7,18 +7,18 @@ from celery.schedules import crontab
from datetime import timedelta
# You can use rabbitmq instead here.
BASE_REDIS_URL = os.environ.get('REDIS_URL', 'redis://localhost:6379')
BASE_REDIS_URL = os.environ.get("REDIS_URL", "redis://localhost:6379")
# set the default Django settings module for the 'celery' program.
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'robosats.settings')
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "robosats.settings")
app = Celery('robosats')
app = Celery("robosats")
# Using a string here means the worker doesn't have to serialize
# the configuration object to child processes.
# - namespace='CELERY' means all celery-related configuration keys
# should have a `CELERY_` prefix.
app.config_from_object('django.conf:settings', namespace='CELERY')
app.config_from_object("django.conf:settings", namespace="CELERY")
# Load task modules from all registered Django app configs.
app.autodiscover_tasks()
@ -26,19 +26,19 @@ app.autodiscover_tasks()
app.conf.broker_url = BASE_REDIS_URL
# this allows schedule items in the Django admin.
app.conf.beat_scheduler = 'django_celery_beat.schedulers:DatabaseScheduler'
app.conf.beat_scheduler = "django_celery_beat.schedulers:DatabaseScheduler"
# Configure the periodic tasks
app.conf.beat_schedule = {
'users-cleansing': { # Cleans abandoned users every 6 hours
'task': 'users_cleansing',
'schedule': timedelta(hours=6),
"users-cleansing": { # Cleans abandoned users every 6 hours
"task": "users_cleansing",
"schedule": timedelta(hours=6),
},
'cache-market-prices': { # Cache market prices every minutes for now.
'task': 'cache_external_market_prices',
'schedule': timedelta(seconds=60),
"cache-market-prices": { # Cache market prices every minutes for now.
"task": "cache_external_market_prices",
"schedule": timedelta(seconds=60),
},
}
app.conf.timezone = 'UTC'
app.conf.timezone = "UTC"

View File

@ -1,2 +1,2 @@
# This sets the django-celery-results backend
CELERY_RESULT_BACKEND = 'django-db'
CELERY_RESULT_BACKEND = "django-db"

View File

@ -2,12 +2,11 @@ from channels.auth import AuthMiddlewareStack
from channels.routing import ProtocolTypeRouter, URLRouter
import chat.routing
application = ProtocolTypeRouter({
'websocket': AuthMiddlewareStack(
"websocket":
AuthMiddlewareStack(
URLRouter(
chat.routing.websocket_urlpatterns,
# TODO add api.routing.websocket_urlpatterns when Order page works with websocket
)
),
})
)),
})

View File

@ -17,133 +17,138 @@ from decouple import config
# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent
STATIC_URL = '/static/'
STATIC_URL = "/static/"
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/4.0/howto/deployment/checklist/
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = config('SECRET_KEY')
SECRET_KEY = config("SECRET_KEY")
DEBUG = False
STATIC_URL = 'static/'
STATIC_ROOT ='/usr/src/static/'
STATIC_URL = "static/"
STATIC_ROOT = "/usr/src/static/"
# SECURITY WARNING: don't run with debug turned on in production!
if os.environ.get('DEVELOPMENT'):
if os.environ.get("DEVELOPMENT"):
DEBUG = True
STATIC_ROOT = 'frontend/static/'
AVATAR_ROOT = STATIC_ROOT + 'assets/avatars/'
STATIC_ROOT = "frontend/static/"
ALLOWED_HOSTS = [config('HOST_NAME'),config('HOST_NAME2'),config('LOCAL_ALIAS'),'127.0.0.1']
AVATAR_ROOT = STATIC_ROOT + "assets/avatars/"
ALLOWED_HOSTS = [
config("HOST_NAME"),
config("HOST_NAME2"),
config("LOCAL_ALIAS"),
"127.0.0.1",
]
# Application definition
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'rest_framework',
'channels',
'django_celery_beat',
'django_celery_results',
'api',
'chat',
'frontend.apps.FrontendConfig',
"django.contrib.admin",
"django.contrib.auth",
"django.contrib.contenttypes",
"django.contrib.sessions",
"django.contrib.messages",
"django.contrib.staticfiles",
"rest_framework",
"channels",
"django_celery_beat",
"django_celery_results",
"api",
"chat",
"frontend.apps.FrontendConfig",
]
from .celery.conf import *
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
"django.middleware.security.SecurityMiddleware",
"django.contrib.sessions.middleware.SessionMiddleware",
"django.middleware.common.CommonMiddleware",
"django.middleware.csrf.CsrfViewMiddleware",
"django.contrib.auth.middleware.AuthenticationMiddleware",
"django.contrib.messages.middleware.MessageMiddleware",
"django.middleware.clickjacking.XFrameOptionsMiddleware",
]
ROOT_URLCONF = 'robosats.urls'
ROOT_URLCONF = "robosats.urls"
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
"BACKEND": "django.template.backends.django.DjangoTemplates",
"DIRS": [],
"APP_DIRS": True,
"OPTIONS": {
"context_processors": [
"django.template.context_processors.debug",
"django.template.context_processors.request",
"django.contrib.auth.context_processors.auth",
"django.contrib.messages.context_processors.messages",
],
},
},
]
WSGI_APPLICATION = 'robosats.wsgi.application'
WSGI_APPLICATION = "robosats.wsgi.application"
# Database
# https://docs.djangoproject.com/en/4.0/ref/settings/#databases
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': '/usr/src/database/db.sqlite3',
'OPTIONS': {
'timeout': 20, # in seconds
}
}
"default": {
"ENGINE": "django.db.backends.sqlite3",
"NAME": "/usr/src/database/db.sqlite3",
"OPTIONS": {
"timeout": 20, # in seconds
},
}
}
# Password validation
# https://docs.djangoproject.com/en/4.0/ref/settings/#auth-password-validators
AUTH_PASSWORD_VALIDATORS = [
{
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
"NAME":
"django.contrib.auth.password_validation.UserAttributeSimilarityValidator",
},
{
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
"NAME":
"django.contrib.auth.password_validation.MinimumLengthValidator",
},
{
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
"NAME":
"django.contrib.auth.password_validation.CommonPasswordValidator",
},
{
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
"NAME":
"django.contrib.auth.password_validation.NumericPasswordValidator",
},
]
# Internationalization
# https://docs.djangoproject.com/en/4.0/topics/i18n/
LANGUAGE_CODE = 'en-us'
LANGUAGE_CODE = "en-us"
TIME_ZONE = 'UTC'
TIME_ZONE = "UTC"
USE_I18N = True
USE_TZ = True
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/4.0/howto/static-files/
STATIC_URL = 'static/'
STATIC_URL = "static/"
ASGI_APPLICATION = "robosats.routing.application"
CHANNEL_LAYERS = {
'default': {
'BACKEND': 'channels_redis.core.RedisChannelLayer',
'CONFIG': {
"hosts": [config('REDIS_URL')],
"default": {
"BACKEND": "channels_redis.core.RedisChannelLayer",
"CONFIG": {
"hosts": [config("REDIS_URL")],
},
},
}
@ -151,14 +156,14 @@ CHANNEL_LAYERS = {
CACHES = {
"default": {
"BACKEND": "django_redis.cache.RedisCache",
"LOCATION": config('REDIS_URL'),
"LOCATION": config("REDIS_URL"),
"OPTIONS": {
"CLIENT_CLASS": "django_redis.client.DefaultClient"
}
},
}
}
# Default primary key field type
# https://docs.djangoproject.com/en/4.0/ref/settings/#default-auto-field
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"

View File

@ -17,8 +17,8 @@ from django.contrib import admin
from django.urls import path, include
urlpatterns = [
path('admin/', admin.site.urls),
path('api/', include('api.urls')),
path("admin/", admin.site.urls),
path("api/", include("api.urls")),
# path('chat/', include('chat.urls')),
path('', include('frontend.urls')),
]
path("", include("frontend.urls")),
]

View File

@ -11,6 +11,6 @@ import os
from django.core.wsgi import get_wsgi_application
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'robosats.settings')
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "robosats.settings")
application = get_wsgi_application()