From 4516f1974a865a1b3ea85572f2076a25a08b3b2f Mon Sep 17 00:00:00 2001 From: fieryfrank Date: Fri, 7 Jan 2022 18:48:23 -0500 Subject: [PATCH 1/3] backend and BookPage.js use currencies.json --- api/currencies.json | 6 ++++++ api/models.py | 11 +++++------ api/urls.py | 3 ++- api/views.py | 9 ++++++++- frontend/src/components/BookPage.js | 20 ++++++++++++++++---- frontend/urls.py | 3 ++- 6 files changed, 39 insertions(+), 13 deletions(-) create mode 100644 api/currencies.json diff --git a/api/currencies.json b/api/currencies.json new file mode 100644 index 00000000..bf3a4382 --- /dev/null +++ b/api/currencies.json @@ -0,0 +1,6 @@ +{ + "1":"USD", + "2":"EUR", + "3":"ETH", + "4":"ABC" +} diff --git a/api/models.py b/api/models.py index 452e57e3..6fa81646 100644 --- a/api/models.py +++ b/api/models.py @@ -7,6 +7,7 @@ from django.dispatch import receiver from django.utils.html import mark_safe from pathlib import Path +import json ############################# # TODO @@ -65,11 +66,6 @@ class Order(models.Model): BUY = 0, 'BUY' SELL = 1, 'SELL' - class Currencies(models.IntegerChoices): - USD = 1, 'USD' - EUR = 2, 'EUR' - ETH = 3, 'ETH' - class Status(models.IntegerChoices): WFB = 0, 'Waiting for bond' PUB = 1, 'Published in order book' @@ -92,6 +88,9 @@ class Order(models.Model): TLD = 18, 'Taker lost dispute' EXP = 19, 'Expired' + currency_dict = json.load(open('./api/currencies.json')) + currency_choices = [(int(val), label) for val, label in list(currency_dict.items())] + print(currency_choices) # order info status = models.PositiveSmallIntegerField(choices=Status.choices, null=False, default=Status.WFB) created_at = models.DateTimeField(auto_now_add=True) @@ -99,7 +98,7 @@ class Order(models.Model): # order details type = models.PositiveSmallIntegerField(choices=Types.choices, null=False) - currency = models.PositiveSmallIntegerField(choices=Currencies.choices, null=False) + currency = models.PositiveSmallIntegerField(choices=currency_choices, null=False) amount = models.DecimalField(max_digits=9, decimal_places=4, validators=[MinValueValidator(0.00001)]) payment_method = models.CharField(max_length=30, null=False, default="not specified", blank=True) diff --git a/api/urls.py b/api/urls.py index eae708dd..a3b73309 100644 --- a/api/urls.py +++ b/api/urls.py @@ -1,9 +1,10 @@ from django.urls import path -from .views import OrderMakerView, OrderView, UserView, BookView +from .views import OrderMakerView, OrderView, UserView, BookView, get_currencies_json urlpatterns = [ path('make/', OrderMakerView.as_view()), path('order/', OrderView.as_view({'get':'get','post':'take_or_update'})), path('usergen/', UserView.as_view()), path('book/', BookView.as_view()), + path('currencies/', get_currencies_json), ] \ No newline at end of file diff --git a/api/views.py b/api/views.py index 04fc6d42..470d02e7 100644 --- a/api/views.py +++ b/api/views.py @@ -21,6 +21,9 @@ from pathlib import Path from datetime import timedelta from django.utils import timezone +import json +from django.http import HttpResponse + # .env expiration_time = 8 @@ -263,6 +266,7 @@ class BookView(ListAPIView): def get(self,request, format=None): currency = request.GET.get('currency') + print("currency:", currency) type = request.GET.get('type') queryset = Order.objects.filter(currency=currency, type=type, status=int(Order.Status.PUB)) if len(queryset)== 0: @@ -281,7 +285,10 @@ class BookView(ListAPIView): book_data.append(data) return Response(book_data, status=status.HTTP_200_OK) - + +def get_currencies_json(request): + currency_dict = json.load(open('./api/currencies.json')) + return HttpResponse(json.dumps(currency_dict),content_type="application/json") diff --git a/frontend/src/components/BookPage.js b/frontend/src/components/BookPage.js index 93cb63d8..1f43c68a 100644 --- a/frontend/src/components/BookPage.js +++ b/frontend/src/components/BookPage.js @@ -8,8 +8,10 @@ export default class BookPage extends Component { orders: new Array(), currency: 1, type: 1, + currencies_dict: {"1":"USD"} }; this.getOrderDetails() + this.getCurrencyDict() this.state.currencyCode = this.getCurrencyCode(this.state.currency) } @@ -24,6 +26,15 @@ export default class BookPage extends Component { not_found: data.not_found, })); } + getCurrencyDict() { + fetch('/api/currencies') + .then((response) => response.json()) + .then((data) => + this.setState({ + currencies_dict: data + })); + + } handleCardClick=(e)=>{ console.log(e) @@ -48,7 +59,7 @@ export default class BookPage extends Component { // Gets currency code (3 letters) from numeric (e.g., 1 -> USD) // Improve this function so currencies are read from json getCurrencyCode(val){ - return (val == 1 ) ? "USD": ((val == 2 ) ? "EUR":"ETH") + return this.state.currencies_dict[val.toString()] } // pretty numbers @@ -156,9 +167,10 @@ export default class BookPage extends Component { }} onChange={this.handleCurrencyChange} > - USD - EUR - ETH + { + Object.entries(this.state.currencies_dict) + .map( ([key, value]) => {value} ) + } diff --git a/frontend/urls.py b/frontend/urls.py index 74fb0594..d7b68630 100644 --- a/frontend/urls.py +++ b/frontend/urls.py @@ -8,5 +8,6 @@ urlpatterns = [ path('make/', index), path('book/', index), path('order/', index), - path('wait/', index), + path('wait/', index), + path('currencies/',index) ] \ No newline at end of file From 215af668a2b037407344e119b7be6537528c6406 Mon Sep 17 00:00:00 2001 From: LowEntropyFace Date: Sat, 8 Jan 2022 06:51:55 -0500 Subject: [PATCH 2/3] merge htlc-model --- api/admin.py | 4 +- api/lightning.py | 24 +- api/logics.py | 294 +++++++++++++++++++++++++ api/models.py | 106 +++++---- api/serializers.py | 8 +- api/urls.py | 2 +- api/views.py | 274 ++++++++++++++--------- frontend/src/components/BookPage.js | 35 ++- frontend/src/components/MakerPage.js | 31 ++- frontend/src/components/OrderPage.js | 7 +- frontend/src/components/UserGenPage.js | 2 +- setup.md | 5 +- 12 files changed, 601 insertions(+), 191 deletions(-) create mode 100644 api/logics.py diff --git a/api/admin.py b/api/admin.py index 1d0bd415..b45ca85b 100644 --- a/api/admin.py +++ b/api/admin.py @@ -24,13 +24,13 @@ class EUserAdmin(UserAdmin): @admin.register(Order) class OrderAdmin(AdminChangeLinksMixin, admin.ModelAdmin): - list_display = ('id','type','maker_link','taker_link','status','amount','currency','created_at','expires_at', 'buyer_invoice_link','maker_bond_link','taker_bond_link','trade_escrow_link') + list_display = ('id','type','maker_link','taker_link','status','amount','currency','t0_satoshis','created_at','expires_at', 'buyer_invoice_link','maker_bond_link','taker_bond_link','trade_escrow_link') list_display_links = ('id','type') change_links = ('maker','taker','buyer_invoice','maker_bond','taker_invoice','taker_bond','trade_escrow') @admin.register(LNPayment) class LNPaymentAdmin(AdminChangeLinksMixin, admin.ModelAdmin): - list_display = ('id','concept','status','amount','type','invoice','secret','expires_at','sender_link','receiver_link') + list_display = ('id','concept','status','num_satoshis','type','invoice','expires_at','sender_link','receiver_link') list_display_links = ('id','concept') change_links = ('sender','receiver') diff --git a/api/lightning.py b/api/lightning.py index 50d60529..cf66a12f 100644 --- a/api/lightning.py +++ b/api/lightning.py @@ -1,3 +1,6 @@ +from datetime import timedelta +from django.utils import timezone + import random import string @@ -10,17 +13,28 @@ class LNNode(): Place holder functions to interact with Lightning Node ''' - def gen_hodl_invoice(): + def gen_hodl_invoice(num_satoshis, description, expiry): '''Generates hodl invoice to publish an order''' - return ''.join(random.choices(string.ascii_uppercase + string.digits, k=80)) + # TODO + invoice = ''.join(random.choices(string.ascii_uppercase + string.digits, k=80)) #FIX + payment_hash = ''.join(random.choices(string.ascii_uppercase + string.digits, k=40)) #FIX + expires_at = timezone.now() + timedelta(hours=8) ##FIX + + return invoice, payment_hash, expires_at def validate_hodl_invoice_locked(): '''Generates hodl invoice to publish an order''' return True - def validate_ln_invoice(invoice): - '''Checks if a LN invoice is valid''' - return True + def validate_ln_invoice(invoice): # num_satoshis + '''Checks if the submited LN invoice is as expected''' + valid = True + num_satoshis = 50000 # TODO decrypt and confirm sats are as expected + description = 'Placeholder desc' # TODO decrypt from LN invoice + payment_hash = '567126' # TODO decrypt + expires_at = timezone.now() # TODO decrypt + + return valid, num_satoshis, description, payment_hash, expires_at def pay_buyer_invoice(invoice): '''Sends sats to buyer''' diff --git a/api/logics.py b/api/logics.py new file mode 100644 index 00000000..4324b2b8 --- /dev/null +++ b/api/logics.py @@ -0,0 +1,294 @@ +from datetime import timedelta +from django.utils import timezone +import requests +from .lightning import LNNode + +from .models import Order, LNPayment, User +from decouple import config + +FEE = float(config('FEE')) +BOND_SIZE = float(config('BOND_SIZE')) +MARKET_PRICE_API = config('MARKET_PRICE_API') +ESCROW_USERNAME = config('ESCROW_USERNAME') + +MIN_TRADE = int(config('MIN_TRADE')) +MAX_TRADE = int(config('MAX_TRADE')) + +EXP_MAKER_BOND_INVOICE = int(config('EXP_MAKER_BOND_INVOICE')) +EXP_TAKER_BOND_INVOICE = int(config('EXP_TAKER_BOND_INVOICE')) +EXP_TRADE_ESCR_INVOICE = int(config('EXP_TRADE_ESCR_INVOICE')) + +BOND_EXPIRY = int(config('BOND_EXPIRY')) +ESCROW_EXPIRY = int(config('ESCROW_EXPIRY')) + +class Logics(): + + # escrow_user = User.objects.get(username=ESCROW_USERNAME) + + def validate_already_maker_or_taker(user): + '''Checks if the user is already partipant of an order''' + queryset = Order.objects.filter(maker=user) + if queryset.exists(): + return False, {'bad_request':'You are already maker of an order'} + queryset = Order.objects.filter(taker=user) + if queryset.exists(): + return False, {'bad_request':'You are already taker of an order'} + return True, None + + def validate_order_size(order): + '''Checks if order is withing limits at t0''' + if order.t0_satoshis > MAX_TRADE: + return False, {'bad_request': f'Your order is too big. It is worth {order.t0_satoshis} now. But maximum is {MAX_TRADE}'} + if order.t0_satoshis < MIN_TRADE: + return False, {'bad_request': f'Your order is too small. It is worth {order.t0_satoshis} now. But minimum is {MIN_TRADE}'} + return True, None + + def take(order, user): + order.taker = user + order.status = Order.Status.TAK + order.save() + + def is_buyer(order, user): + is_maker = order.maker == user + is_taker = order.taker == user + return (is_maker and order.type == Order.Types.BUY) or (is_taker and order.type == Order.Types.SELL) + + def is_seller(order, user): + is_maker = order.maker == user + is_taker = order.taker == user + return (is_maker and order.type == Order.Types.SELL) or (is_taker and order.type == Order.Types.BUY) + + def satoshis_now(order): + ''' checks trade amount in sats ''' + if order.is_explicit: + satoshis_now = order.satoshis + else: + # TODO Add fallback Public APIs and error handling + # Think about polling price data in a different way (e.g. store locally every t seconds) + market_prices = requests.get(MARKET_PRICE_API).json() + exchange_rate = float(market_prices[Order.currency_dict[str(order.currency)]]['last']) + satoshis_now = ((float(order.amount) * 1+float(order.premium)) / exchange_rate) * 100*1000*1000 + + return int(satoshis_now) + + def order_expires(order): + order.status = Order.Status.EXP + order.maker = None + order.taker = None + order.save() + + @classmethod + def buyer_invoice_amount(cls, order, user): + ''' Computes buyer invoice amount. Uses order.last_satoshis, + that is the final trade amount set at Taker Bond time''' + + if cls.is_buyer(order, user): + invoice_amount = int(order.last_satoshis * (1-FEE)) # Trading FEE is charged here. + + return True, {'invoice_amount': invoice_amount} + + @classmethod + def update_invoice(cls, order, user, invoice): + is_valid_invoice, num_satoshis, description, payment_hash, expires_at = LNNode.validate_ln_invoice(invoice) + # only user is the buyer and a valid LN invoice + if not (cls.is_buyer(order, user) or is_valid_invoice): + return False, {'bad_request':'Invalid Lightning Network Invoice. It starts by LNTB...'} + + order.buyer_invoice, _ = LNPayment.objects.update_or_create( + concept = LNPayment.Concepts.PAYBUYER, + type = LNPayment.Types.NORM, + sender = User.objects.get(username=ESCROW_USERNAME), + receiver= user, + # if there is a LNPayment matching these above, it updates that one with defaults below. + defaults={ + 'invoice' : invoice, + 'status' : LNPayment.Status.VALIDI, + 'num_satoshis' : num_satoshis, + 'description' : description, + 'payment_hash' : payment_hash, + 'expires_at' : expires_at} + ) + + # If the order status is 'Waiting for invoice'. Move forward to 'waiting for invoice' + if order.status == Order.Status.WFE: order.status = Order.Status.CHA + + # If the order status is 'Waiting for both'. Move forward to 'waiting for escrow' or to 'chat' + if order.status == Order.Status.WF2: + print(order.trade_escrow) + if order.trade_escrow: + if order.trade_escrow.status == LNPayment.Status.LOCKED: + order.status = Order.Status.CHA + else: + order.status = Order.Status.WFE + + # If the order status was Payment Failed. Move forward to invoice Updated. + if order.status == Order.Status.FAI: + order.status = Order.Status.UPI + + order.save() + return True, None + + @classmethod + def rate_counterparty(cls, order, user, rating): + # if maker, rates taker + if order.maker == user: + order.taker.profile.total_ratings = order.taker.profile.total_ratings + 1 + last_ratings = list(order.taker.profile.last_ratings).append(rating) + order.taker.profile.total_ratings = sum(last_ratings) / len(last_ratings) + # if taker, rates maker + if order.taker == user: + order.maker.profile.total_ratings = order.maker.profile.total_ratings + 1 + last_ratings = list(order.maker.profile.last_ratings).append(rating) + order.maker.profile.total_ratings = sum(last_ratings) / len(last_ratings) + + order.save() + return True, None + + @classmethod + def cancel_order(cls, order, user, state): + + # 1) When maker cancels before bond + '''The order never shows up on the book and order + status becomes "cancelled". That's it.''' + if order.status == Order.Status.WFB and order.maker == user: + order.maker = None + order.status = Order.Status.UCA + order.save() + return True, None + + # 2) When maker cancels after bond + '''The order dissapears from book and goes to cancelled. + Maker is charged a small amount of sats, to prevent DDOS + on the LN node and order book''' + + # 3) When taker cancels before bond + ''' The order goes back to the book as public. + LNPayment "order.taker_bond" is deleted() ''' + + # 4) When taker or maker cancel after bond (before escrow) + '''The order goes into cancelled status if maker cancels. + The order goes into the public book if taker cancels. + In both cases there is a small fee.''' + + # 5) When trade collateral has been posted (after escrow) + '''Always goes to cancelled status. Collaboration is needed. + When a user asks for cancel, 'order.is_pending_cancel' goes True. + When the second user asks for cancel. Order is totally cancelled. + Has a small cost for both parties to prevent node DDOS.''' + + else: + return False, {'bad_request':'You cannot cancel this order'} + + @classmethod + def gen_maker_hodl_invoice(cls, order, user): + + # Do not gen and cancel if order is more than 5 minutes old + if order.expires_at < timezone.now(): + cls.order_expires(order) + return False, {'bad_request':'Invoice expired. You did not confirm publishing the order in time. Make a new order.'} + + # Return the previous invoice if there was one and is still unpaid + if order.maker_bond: + if order.maker_bond.status == LNPayment.Status.INVGEN: + return True, {'bond_invoice':order.maker_bond.invoice,'bond_satoshis':order.maker_bond.num_satoshis} + else: + return False, None + + order.last_satoshis = cls.satoshis_now(order) + bond_satoshis = int(order.last_satoshis * BOND_SIZE) + description = f'RoboSats - Publishing {str(order)} - This bond will return to you if you do not cheat.' + + # Gen HODL Invoice + invoice, payment_hash, expires_at = LNNode.gen_hodl_invoice(bond_satoshis, description, BOND_EXPIRY*3600) + + order.maker_bond = LNPayment.objects.create( + concept = LNPayment.Concepts.MAKEBOND, + type = LNPayment.Types.HODL, + sender = user, + receiver = User.objects.get(username=ESCROW_USERNAME), + invoice = invoice, + status = LNPayment.Status.INVGEN, + num_satoshis = bond_satoshis, + description = description, + payment_hash = payment_hash, + expires_at = expires_at) + + order.save() + return True, {'bond_invoice':invoice,'bond_satoshis':bond_satoshis} + + @classmethod + def gen_taker_hodl_invoice(cls, order, user): + + # Do not gen and cancel if a taker invoice is there and older than X minutes and unpaid still + if order.taker_bond: + # Check if status is INVGEN and still not expired + if order.taker_bond.status == LNPayment.Status.INVGEN: + if order.taker_bond.created_at > (timezone.now()+timedelta(minutes=EXP_TAKER_BOND_INVOICE)): + cls.cancel_order(order, user, 3) # State 3, cancel order before taker bond + return False, {'bad_request':'Invoice expired. You did not confirm taking the order in time.'} + # Return the previous invoice there was with INVGEN status + else: + return True, {'bond_invoice':order.taker_bond.invoice,'bond_satoshis':order.taker_bond.num_satoshis} + # Invoice exists, but was already locked or settled + else: + return False, None + + order.last_satoshis = cls.satoshis_now(order) # LOCKS THE AMOUNT OF SATOSHIS FOR THE TRADE + bond_satoshis = int(order.last_satoshis * BOND_SIZE) + description = f'RoboSats - Taking {str(order)} - This bond will return to you if you do not cheat.' + + # Gen HODL Invoice + invoice, payment_hash, expires_at = LNNode.gen_hodl_invoice(bond_satoshis, description, BOND_EXPIRY*3600) + + order.taker_bond = LNPayment.objects.create( + concept = LNPayment.Concepts.TAKEBOND, + type = LNPayment.Types.HODL, + sender = user, + receiver = User.objects.get(username=ESCROW_USERNAME), + invoice = invoice, + status = LNPayment.Status.INVGEN, + num_satoshis = bond_satoshis, + description = description, + payment_hash = payment_hash, + expires_at = expires_at) + + order.save() + return True, {'bond_invoice':invoice,'bond_satoshis': bond_satoshis} + + @classmethod + def gen_escrow_hodl_invoice(cls, order, user): + + # Do not generate and cancel if an invoice is there and older than X minutes and unpaid still + if order.trade_escrow: + # Check if status is INVGEN and still not expired + if order.taker_bond.status == LNPayment.Status.INVGEN: + if order.taker_bond.created_at > (timezone.now()+timedelta(minutes=EXP_TRADE_ESCR_INVOICE)): # Expired + cls.cancel_order(order, user, 4) # State 4, cancel order before trade escrow locked + return False, {'bad_request':'Invoice expired. You did not lock the trade escrow in time.'} + # Return the previous invoice there was with INVGEN status + else: + return True, {'escrow_invoice':order.trade_escrow.invoice,'escrow_satoshis':order.trade_escrow.num_satoshis} + # Invoice exists, but was already locked or settled + else: + return False, None # Does not return any context of a healthy locked escrow + + escrow_satoshis = order.last_satoshis # Trade sats amount was fixed at the time of taker bond generation (order.last_satoshis) + description = f'RoboSats - Escrow amount for {str(order)} - This escrow will be released to the buyer once you confirm you received the fiat.' + + # Gen HODL Invoice + invoice, payment_hash, expires_at = LNNode.gen_hodl_invoice(escrow_satoshis, description, ESCROW_EXPIRY*3600) + + order.taker_bond = LNPayment.objects.create( + concept = LNPayment.Concepts.TRESCROW, + type = LNPayment.Types.HODL, + sender = user, + receiver = User.objects.get(username=ESCROW_USERNAME), + invoice = invoice, + status = LNPayment.Status.INVGEN, + num_satoshis = escrow_satoshis, + description = description, + payment_hash = payment_hash, + expires_at = expires_at) + + order.save() + return True, {'escrow_invoice':invoice,'escrow_satoshis': escrow_satoshis} \ No newline at end of file diff --git a/api/models.py b/api/models.py index 6fa81646..8dbfe09a 100644 --- a/api/models.py +++ b/api/models.py @@ -3,9 +3,9 @@ from django.contrib.auth.models import User from django.core.validators import MaxValueValidator, MinValueValidator, validate_comma_separated_integer_list from django.db.models.signals import post_save, pre_delete from django.dispatch import receiver - from django.utils.html import mark_safe +from decouple import config from pathlib import Path import json @@ -13,10 +13,11 @@ import json # TODO # Load hparams from .env file -MIN_TRADE = 10*1000 #In sats -MAX_TRADE = 500*1000 -FEE = 0.002 # Trade fee in % -BOND_SIZE = 0.01 # Bond in % +MIN_TRADE = int(config('MIN_TRADE')) +MAX_TRADE = int(config('MAX_TRADE')) +FEE = float(config('FEE')) +BOND_SIZE = float(config('BOND_SIZE')) + class LNPayment(models.Model): @@ -27,38 +28,39 @@ class LNPayment(models.Model): class Concepts(models.IntegerChoices): MAKEBOND = 0, 'Maker bond' - TAKEBOND = 1, 'Taker-buyer bond' + TAKEBOND = 1, 'Taker bond' TRESCROW = 2, 'Trade escrow' PAYBUYER = 3, 'Payment to buyer' class Status(models.IntegerChoices): - INVGEN = 0, 'Hodl invoice was generated' - LOCKED = 1, 'Hodl invoice has HTLCs locked' - CHRGED = 2, 'Hodl invoice was charged' - RETNED = 3, 'Hodl invoice was returned' - MISSNG = 4, 'Buyer invoice is missing' - IVALID = 5, 'Buyer invoice is valid' - INPAID = 6, 'Buyer invoice was paid' - INFAIL = 7, 'Buyer invoice routing failed' + INVGEN = 0, 'Generated' + LOCKED = 1, 'Locked' + SETLED = 2, 'Settled' + RETNED = 3, 'Returned' + MISSNG = 4, 'Missing' + VALIDI = 5, 'Valid' + INFAIL = 6, 'Failed routing' - # payment use case + # payment use details type = models.PositiveSmallIntegerField(choices=Types.choices, null=False, default=Types.HODL) concept = models.PositiveSmallIntegerField(choices=Concepts.choices, null=False, default=Concepts.MAKEBOND) status = models.PositiveSmallIntegerField(choices=Status.choices, null=False, default=Status.INVGEN) + routing_retries = models.PositiveSmallIntegerField(null=False, default=0) - # payment details + # payment info invoice = models.CharField(max_length=300, unique=False, null=True, default=None, blank=True) - secret = models.CharField(max_length=300, unique=False, null=True, default=None, blank=True) + payment_hash = models.CharField(max_length=300, unique=False, null=True, default=None, blank=True) + description = models.CharField(max_length=300, unique=False, null=True, default=None, blank=True) + created_at = models.DateTimeField(auto_now_add=True) expires_at = models.DateTimeField() - amount = models.PositiveBigIntegerField(validators=[MinValueValidator(MIN_TRADE*BOND_SIZE), MaxValueValidator(MAX_TRADE*(1+BOND_SIZE+FEE))]) + num_satoshis = models.PositiveBigIntegerField(validators=[MinValueValidator(MIN_TRADE*BOND_SIZE), MaxValueValidator(MAX_TRADE*(1+BOND_SIZE+FEE))]) - # payment relationals + # 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) def __str__(self): - # Make relational back to ORDER - return (f'HTLC {self.id}: {self.Concepts(self.concept).label}') + return (f'HTLC {self.id}: {self.Concepts(self.concept).label} - {self.Status(self.status).label}') class Order(models.Model): @@ -67,26 +69,25 @@ class Order(models.Model): SELL = 1, 'SELL' class Status(models.IntegerChoices): - WFB = 0, 'Waiting for bond' - PUB = 1, 'Published in order book' - DEL = 2, 'Deleted from order book' - TAK = 3, 'Taken' - UCA = 4, 'Unilaterally cancelled' - RET = 5, 'Returned to order book' # Probably same as 1 in most cases. - WF2 = 6, 'Waiting for trade collateral and buyer invoice' - WTC = 7, 'Waiting only for trade collateral' - WBI = 8, 'Waiting only for buyer invoice' - EXF = 9, 'Exchanging fiat / In chat' - CCA = 10, 'Collaboratively cancelled' - FSE = 11, 'Fiat sent' - FCO = 12, 'Fiat confirmed' - SUC = 13, 'Sucessfully settled' - FAI = 14, 'Failed lightning network routing' - UPI = 15, 'Updated invoice' - DIS = 16, 'In dispute' - MLD = 17, 'Maker lost dispute' - TLD = 18, 'Taker lost dispute' - EXP = 19, 'Expired' + WFB = 0, 'Waiting for maker bond' + PUB = 1, 'Public' + DEL = 2, 'Deleted' + TAK = 3, 'Waiting for taker bond' + UCA = 4, 'Cancelled' + WF2 = 5, 'Waiting for trade collateral and buyer invoice' + WFE = 6, 'Waiting only for seller trade collateral' + WFI = 7, 'Waiting only for buyer invoice' + CHA = 8, 'Sending fiat - In chatroom' + CCA = 9, 'Collaboratively cancelled' + FSE = 10, 'Fiat sent - In chatroom' + FCO = 11, 'Fiat confirmed' + SUC = 12, 'Sucessfully settled' + FAI = 13, 'Failed lightning network routing' + UPI = 14, 'Updated invoice' + DIS = 15, 'In dispute' + MLD = 16, 'Maker lost dispute' + TLD = 17, 'Taker lost dispute' + EXP = 18, 'Expired' currency_dict = json.load(open('./api/currencies.json')) currency_choices = [(int(val), label) for val, label in list(currency_dict.items())] @@ -104,16 +105,19 @@ class Order(models.Model): # 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 marked + # marked to market premium = models.DecimalField(max_digits=5, decimal_places=2, default=0, null=True, validators=[MinValueValidator(-100), MaxValueValidator(999)], blank=True) - t0_market_satoshis = models.PositiveBigIntegerField(null=True, validators=[MinValueValidator(MIN_TRADE), MaxValueValidator(MAX_TRADE)], blank=True) # explicit 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... # order participants maker = models.ForeignKey(User, related_name='maker', on_delete=models.CASCADE, 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 - + is_pending_cancel = models.BooleanField(default=False, null=False) # When collaborative cancel is needed and one partner has cancelled. + # order collateral maker_bond = models.ForeignKey(LNPayment, related_name='maker_bond', on_delete=models.SET_NULL, null=True, default=None, blank=True) taker_bond = models.ForeignKey(LNPayment, related_name='taker_bond', on_delete=models.SET_NULL, null=True, default=None, blank=True) @@ -122,6 +126,19 @@ class Order(models.Model): # buyer payment LN invoice buyer_invoice = models.ForeignKey(LNPayment, related_name='buyer_invoice', on_delete=models.SET_NULL, null=True, default=None, blank=True) + def __str__(self): + # Make relational back to ORDER + return (f'Order {self.id}: {self.Types(self.type).label} BTC for {self.amount} {self.Currencies(self.currency).label}') + +@receiver(pre_delete, sender=Order) +def delelete_HTLCs_at_order_deletion(sender, instance, **kwargs): + to_delete = (instance.maker_bond, instance.buyer_invoice, instance.taker_bond, instance.trade_escrow) + + for htlc in to_delete: + try: + htlc.delete() + except: + pass class Profile(models.Model): @@ -165,3 +182,4 @@ class Profile(models.Model): # method to create a fake table field in read only mode def avatar_tag(self): return mark_safe('' % self.get_avatar()) + diff --git a/api/serializers.py b/api/serializers.py index c88b14b8..a819db91 100644 --- a/api/serializers.py +++ b/api/serializers.py @@ -11,7 +11,7 @@ class MakeOrderSerializer(serializers.ModelSerializer): model = Order fields = ('type','currency','amount','payment_method','is_explicit','premium','satoshis') -class UpdateOrderSerializer(serializers.ModelSerializer): - class Meta: - model = Order - fields = ('id','buyer_invoice') \ No newline at end of file +class UpdateOrderSerializer(serializers.Serializer): + invoice = serializers.CharField(max_length=300, allow_null=True, allow_blank=True, default=None) + action = serializers.ChoiceField(choices=('take','update_invoice','dispute','cancel','confirm','rate'), allow_null=False) + rating = serializers.ChoiceField(choices=('1','2','3','4','5'), allow_null=True, allow_blank=True, default=None) \ No newline at end of file diff --git a/api/urls.py b/api/urls.py index a3b73309..1b2897d5 100644 --- a/api/urls.py +++ b/api/urls.py @@ -3,7 +3,7 @@ from .views import OrderMakerView, OrderView, UserView, BookView, get_currencies urlpatterns = [ path('make/', OrderMakerView.as_view()), - path('order/', OrderView.as_view({'get':'get','post':'take_or_update'})), + path('order/', OrderView.as_view({'get':'get','post':'take_update_confirm_dispute_cancel'})), path('usergen/', UserView.as_view()), path('book/', BookView.as_view()), path('currencies/', get_currencies_json), diff --git a/api/views.py b/api/views.py index 470d02e7..4674cd29 100644 --- a/api/views.py +++ b/api/views.py @@ -1,15 +1,14 @@ -from rest_framework import status, serializers +from rest_framework import status, viewsets from rest_framework.generics import CreateAPIView, ListAPIView from rest_framework.views import APIView -from rest_framework import viewsets from rest_framework.response import Response from django.contrib.auth import authenticate, login, logout from django.contrib.auth.models import User from .serializers import ListOrderSerializer, MakeOrderSerializer, UpdateOrderSerializer -from .models import Order, LNPayment -from .lightning import LNNode +from .models import LNPayment, Order +from .logics import Logics from .nick_generator.nick_generator import NickGenerator from robohash import Robohash @@ -20,29 +19,16 @@ import hashlib from pathlib import Path from datetime import timedelta from django.utils import timezone +from decouple import config import json from django.http import HttpResponse -# .env -expiration_time = 8 +EXP_MAKER_BOND_INVOICE = int(config('EXP_MAKER_BOND_INVOICE')) avatar_path = Path('frontend/static/assets/avatars') avatar_path.mkdir(parents=True, exist_ok=True) -def validate_already_maker_or_taker(request): - '''Checks if the user is already partipant of an order''' - - queryset = Order.objects.filter(maker=request.user.id) - if queryset.exists(): - return False, Response({'Bad Request':'You are already maker of an order'}, status=status.HTTP_400_BAD_REQUEST) - - queryset = Order.objects.filter(taker=request.user.id) - if queryset.exists(): - return False, Response({'Bad Request':'You are already taker of an order'}, status=status.HTTP_400_BAD_REQUEST) - - return True, None - # Create your views here. class OrderMakerView(CreateAPIView): @@ -51,36 +37,38 @@ class OrderMakerView(CreateAPIView): def post(self,request): serializer = self.serializer_class(data=request.data) - if serializer.is_valid(): - otype = 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') + if not serializer.is_valid(): return Response(status=status.HTTP_400_BAD_REQUEST) - valid, response = validate_already_maker_or_taker(request) - if not valid: - return response + 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') - # Creates a new order in db - order = Order( - type=otype, - status=Order.Status.PUB, # TODO orders are public by default for the moment. Future it will be WFB (waiting for bond) - currency=currency, - amount=amount, - payment_method=payment_method, - premium=premium, - satoshis=satoshis, - is_explicit=is_explicit, - expires_at= timezone.now()+timedelta(hours=expiration_time), - maker=request.user) - order.save() + valid, context = Logics.validate_already_maker_or_taker(request.user) + if not valid: return Response(context, status.HTTP_409_CONFLICT) - if not serializer.is_valid(): - return Response(status=status.HTTP_400_BAD_REQUEST) - + # Creates a new order + order = Order( + type=type, + currency=currency, + amount=amount, + payment_method=payment_method, + premium=premium, + satoshis=satoshis, + is_explicit=is_explicit, + expires_at=timezone.now()+timedelta(minutes=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) + + order.save() return Response(ListOrderSerializer(order).data, status=status.HTTP_201_CREATED) @@ -89,87 +77,154 @@ class OrderView(viewsets.ViewSet): 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 order_id != None: - order = Order.objects.filter(id=order_id) + if order_id == None: + 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 : - order = order[0] - data = ListOrderSerializer(order).data - nickname = request.user.username + # 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) + + # This is our order. + order = order[0] - # Add booleans if user is maker, taker, partipant, buyer or seller - data['is_maker'] = str(order.maker) == nickname - data['is_taker'] = str(order.taker) == nickname - data['is_participant'] = data['is_maker'] or data['is_taker'] - data['is_buyer'] = (data['is_maker'] and order.type == Order.Types.BUY) or (data['is_taker'] and order.type == Order.Types.SELL) - data['is_seller'] = (data['is_maker'] and order.type == Order.Types.SELL) or (data['is_taker'] and order.type == Order.Types.BUY) - - # 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':'Not allowed to see this order'},status.HTTP_403_FORBIDDEN) + # 1) If order expired + if order.status == Order.Status.EXP: + return Response({'bad_request':'This order has expired'},status.HTTP_400_BAD_REQUEST) - # return nicks too - data['maker_nick'] = str(order.maker) - data['taker_nick'] = str(order.taker) - - data['status_message'] = Order.Status(order.status).label + # 2) If order cancelled + if order.status == Order.Status.UCA: + 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) - if data['is_participant']: - return Response(data, status=status.HTTP_200_OK) - else: - # Non participants should not see the status, who is the taker, etc - for key in ('status','status_message','taker','taker_nick','is_maker','is_taker','is_buyer','is_seller'): - del data[key] - return Response(data, status=status.HTTP_200_OK) + data = ListOrderSerializer(order).data - return Response({'Order Not Found':'Invalid Order Id'},status=status.HTTP_404_NOT_FOUND) - return Response({'Bad Request':'Order ID parameter not found in request'}, status=status.HTTP_400_BAD_REQUEST) + # 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'] + + # 3) 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) + + # 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) + # For participants add position side, nicks and status as message + 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 - def take_or_update(self, request, format=None): + # 5) If status is 'waiting for maker bond' and user is MAKER, reply with a MAKER HODL invoice. + if order.status == Order.Status.WFB and data['is_maker']: + valid, context = Logics.gen_maker_hodl_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 HODL invoice. + elif order.status == Order.Status.TAK and data['is_taker']: + valid, context = Logics.gen_taker_hodl_invoice(order, request.user) + if valid: + data = {**data, **context} + else: + return Response(context, status.HTTP_400_BAD_REQUEST) + + # 7) If status is 'WF2'or'WTC' + elif (order.status == Order.Status.WF2 or order.status == Order.Status.WFE): + + # If the two bonds are locked + if order.maker_bond.status == order.taker_bond.status == LNPayment.Status.LOCKED: + + # 7.a) And if user is Seller, reply with an ESCROW HODL invoice. + if data['is_seller']: + valid, context = Logics.gen_escrow_hodl_invoice(order, request.user) + if valid: + data = {**data, **context} + else: + return Response(context, status.HTTP_400_BAD_REQUEST) + + # 7.b) If user is Buyer, reply with an AMOUNT so he can send the buyer invoice. + elif data['is_buyer']: + valid, context = Logics.buyer_invoice_amount(order, request.user) + if valid: + data = {**data, **context} + else: + return Response(context, status.HTTP_400_BAD_REQUEST) + + # 8) If status is 'CHA'or '' or '' and all HTLCS are in LOCKED + elif order.status == Order.Status.CHA: # TODO Add the other status + if order.maker_bond.status == order.taker_bond.status == order.trade_escrow.status == LNPayment.Status.LOCKED: + # add whether a collaborative cancel is pending + data['pending_cancel'] = order.is_pending_cancel + + return Response(data, status.HTTP_200_OK) + + def take_update_confirm_dispute_cancel(self, request, format=None): + ''' + Here take place all of the user 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) + order = Order.objects.get(id=order_id) - if serializer.is_valid(): - invoice = serializer.data.get('buyer_invoice') + # action is either 1)'take', 2)'confirm', 3)'cancel', 4)'dispute' , 5)'update_invoice' 6)'rate' (counterparty) + action = serializer.data.get('action') + invoice = serializer.data.get('invoice') + rating = serializer.data.get('rating') - # If this is an empty POST request (no invoice), it must be taker request! - if not invoice and order.status == Order.Status.PUB: - - valid, response = validate_already_maker_or_taker(request) - if not valid: - return response + # 1) If action is take, it is be 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) - order.taker = self.request.user - order.status = Order.Status.TAK + Logics.take(order, request.user) + else: Response({'bad_request':'This order is not public anymore.'}, status.HTTP_400_BAD_REQUEST) - #TODO REPLY WITH HODL INVOICE - data = ListOrderSerializer(order).data - - # An invoice came in! update it - elif invoice: - if LNNode.validate_ln_invoice(invoice): - order.invoice = invoice - - #TODO Validate if request comes from PARTICIPANT AND BUYER - - #If the order status was Payment Failed. Move foward to invoice Updated. - if order.status == Order.Status.FAI: - order.status = Order.Status.UPI - - else: - return Response({'bad_request':'Invalid Lightning Network Invoice. It starts by LNTB...'}) + # 2) If action is update (invoice) + elif 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) - # Something else is going on. Probably not allowed. - else: - return Response({'bad_request':'Not allowed'}) + # 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) + + # 4) If action is confirm + elif action == 'confirm': + pass + + # 5) If action is dispute + elif action == 'dispute': + pass + + # 6) If action is dispute + elif action == 'rate' and rating: + valid, context = Logics.rate_counterparty(order,request.user, rating) + if not valid: return Response(context,status.HTTP_400_BAD_REQUEST) + + # If nothing... something else is going on. Probably not allowed! + else: + return Response({'bad_request':'The Robotic Satoshis working in the warehouse did not understand you'}) - order.save() return self.get(request) class UserView(APIView): @@ -223,7 +278,7 @@ class UserView(APIView): with open(image_path, "wb") as f: rh.img.save(f, format="png") - # Create new credentials and logsin if nickname is new + # Create new credentials and log in if nickname is new if len(User.objects.filter(username=nickname)) == 0: User.objects.create_user(username=nickname, password=token, is_staff=False) user = authenticate(request, username=nickname, password=token) @@ -279,6 +334,7 @@ class BookView(ListAPIView): user = User.objects.filter(id=data['maker']) if len(user) == 1: data['maker_nick'] = user[0].username + # Non participants should not see the status or who is the taker for key in ('status','taker'): del data[key] diff --git a/frontend/src/components/BookPage.js b/frontend/src/components/BookPage.js index 1f43c68a..961c72b9 100644 --- a/frontend/src/components/BookPage.js +++ b/frontend/src/components/BookPage.js @@ -1,5 +1,6 @@ import React, { Component } from "react"; -import { Button , Divider, Card, CardActionArea, CardContent, Typography, Grid, Select, MenuItem, FormControl, FormHelperText, List, ListItem, ListItemText, Avatar, Link, RouterLink, ListItemAvatar} from "@material-ui/core" +import { Button , Divider, Card, CardActionArea, CardContent, Typography, Grid, Select, MenuItem, FormControl, FormHelperText, List, ListItem, ListItemText, Avatar, RouterLink, ListItemAvatar} from "@material-ui/core" +import { Link } from 'react-router-dom' export default class BookPage extends Component { constructor(props) { @@ -15,7 +16,6 @@ export default class BookPage extends Component { this.state.currencyCode = this.getCurrencyCode(this.state.currency) } - // Fix needed to handle HTTP 404 error when no order is found // Show message to be the first one to make an order getOrderDetails() { fetch('/api/book' + '?currency=' + this.state.currency + "&type=" + this.state.type) @@ -26,15 +26,6 @@ export default class BookPage extends Component { not_found: data.not_found, })); } - getCurrencyDict() { - fetch('/api/currencies') - .then((response) => response.json()) - .then((data) => - this.setState({ - currencies_dict: data - })); - - } handleCardClick=(e)=>{ console.log(e) @@ -55,7 +46,15 @@ export default class BookPage extends Component { }) this.getOrderDetails(); } - + + getCurrencyDict() { + fetch('/api/currencies') + .then((response) => response.json()) + .then((data) => + this.setState({ + currencies_dict: data + })); + } // Gets currency code (3 letters) from numeric (e.g., 1 -> USD) // Improve this function so currencies are read from json getCurrencyCode(val){ @@ -101,14 +100,14 @@ export default class BookPage extends Component { ◑ Payment via {order.payment_method} - +{/* ◑ Priced {order.is_explicit ? " explicitly at " + this.pn(order.satoshis) + " Sats" : ( " at " + parseFloat(parseFloat(order.premium).toFixed(4)) + "% over the market" )} - + */} {" 42,354 "}{this.getCurrencyCode(order.currency)}/BTC (Binance API) @@ -188,13 +187,13 @@ export default class BookPage extends Component { No orders found to {this.state.type == 0 ? ' sell ' :' buy ' } BTC for {this.state.currencyCode} + + + + Be the first one to create an order - - - - ) : this.bookCards() } diff --git a/frontend/src/components/MakerPage.js b/frontend/src/components/MakerPage.js index fa344cbf..e3912385 100644 --- a/frontend/src/components/MakerPage.js +++ b/frontend/src/components/MakerPage.js @@ -1,5 +1,5 @@ import React, { Component } from 'react'; -import { Paper, Button , Grid, Typography, TextField, Select, FormHelperText, MenuItem, FormControl, Radio, FormControlLabel, RadioGroup, Menu} from "@material-ui/core" +import { Paper, Alert, AlertTitle, Button , Grid, Typography, TextField, Select, FormHelperText, MenuItem, FormControl, Radio, FormControlLabel, RadioGroup, Menu} from "@material-ui/core" import { Link } from 'react-router-dom' function getCookie(name) { @@ -37,7 +37,9 @@ export default class MakerPage extends Component { payment_method: this.defaultPaymentMethod, premium: 0, satoshis: null, + currencies_dict: {"1":"USD"} } + this.getCurrencyDict() } handleTypeChange=(e)=>{ @@ -46,10 +48,9 @@ export default class MakerPage extends Component { }); } handleCurrencyChange=(e)=>{ - var code = (e.target.value == 1 ) ? "USD": ((e.target.value == 2 ) ? "EUR":"ETH") this.setState({ currency: e.target.value, - currencyCode: code, + currencyCode: this.getCurrencyCode(e.target.value), }); } handleAmountChange=(e)=>{ @@ -104,7 +105,22 @@ export default class MakerPage extends Component { }; fetch("/api/make/",requestOptions) .then((response) => response.json()) - .then((data) => (console.log(data) & this.props.history.push('/order/' + data.id))); + .then((data) => (this.setState({badRequest:data.bad_request}) + & (data.id ? this.props.history.push('/order/' + data.id) :""))); + } + + getCurrencyDict() { + fetch('/api/currencies') + .then((response) => response.json()) + .then((data) => + this.setState({ + currencies_dict: data + })); + + } + + getCurrencyCode(val){ + return this.state.currencies_dict[val.toString()] } render() { @@ -242,6 +258,13 @@ export default class MakerPage extends Component { + + + {this.state.badRequest ? + + {this.state.badRequest}
+
+ : ""}
Create a BTC {this.state.type==0 ? "buy":"sell"} order for {this.state.amount} {this.state.currencyCode} diff --git a/frontend/src/components/OrderPage.js b/frontend/src/components/OrderPage.js index 6deef6bf..86ed50ec 100644 --- a/frontend/src/components/OrderPage.js +++ b/frontend/src/components/OrderPage.js @@ -68,6 +68,7 @@ export default class OrderPage extends Component { isBuyer:data.buyer, isSeller:data.seller, expiresAt:data.expires_at, + badRequest:data.bad_request, }); }); } @@ -87,8 +88,10 @@ export default class OrderPage extends Component { console.log(this.state) const requestOptions = { method: 'POST', - headers: {'Content-Type':'application/json', 'X-CSRFToken': getCookie('csrftoken')}, - body: JSON.stringify({}), + headers: {'Content-Type':'application/json', 'X-CSRFToken': getCookie('csrftoken'),}, + body: JSON.stringify({ + 'action':'take', + }), }; fetch('/api/order/' + '?order_id=' + this.orderId, requestOptions) .then((response) => response.json()) diff --git a/frontend/src/components/UserGenPage.js b/frontend/src/components/UserGenPage.js index 0332043b..ef7d9c98 100644 --- a/frontend/src/components/UserGenPage.js +++ b/frontend/src/components/UserGenPage.js @@ -58,7 +58,7 @@ export default class UserGenPage extends Component { delGeneratedUser() { const requestOptions = { method: 'DELETE', - headers: {'Content-Type':'application/json', 'X-CSRFToken': csrftoken}, + headers: {'Content-Type':'application/json', 'X-CSRFToken': getCookie('csrftoken')}, }; fetch("/api/usergen", requestOptions) .then((response) => response.json()) diff --git a/setup.md b/setup.md index c9c82cbf..c1cbf307 100644 --- a/setup.md +++ b/setup.md @@ -4,7 +4,10 @@ `sudo apt install python3 python3 pip` ### Install virtual environments -`pip install virtualenvwrapper` +``` +pip install virtualenvwrapper +pip install python-decouple +``` ### Add to .bashrc From 400fb2b21e28430c697dfa562742981398fb3a62 Mon Sep 17 00:00:00 2001 From: LowEntropyFace Date: Sat, 8 Jan 2022 07:48:03 -0500 Subject: [PATCH 3/3] OrderPage.js, MakerPage.js use currencies.json --- api/currencies.json | 4 +++- api/models.py | 2 +- frontend/src/components/BookPage.js | 2 +- frontend/src/components/MakerPage.js | 7 ++++--- frontend/src/components/OrderPage.js | 24 ++++++++++++++++++------ 5 files changed, 27 insertions(+), 12 deletions(-) diff --git a/api/currencies.json b/api/currencies.json index bf3a4382..f246908d 100644 --- a/api/currencies.json +++ b/api/currencies.json @@ -2,5 +2,7 @@ "1":"USD", "2":"EUR", "3":"ETH", - "4":"ABC" + "4":"AUD", + "5":"BRL", + "6":"CAD" } diff --git a/api/models.py b/api/models.py index 8dbfe09a..bda4d442 100644 --- a/api/models.py +++ b/api/models.py @@ -128,7 +128,7 @@ class Order(models.Model): def __str__(self): # Make relational back to ORDER - return (f'Order {self.id}: {self.Types(self.type).label} BTC for {self.amount} {self.Currencies(self.currency).label}') + return (f'Order {self.id}: {self.Types(self.type).label} BTC for {self.amount} {self.currency_dict[str(self.currency)]}') @receiver(pre_delete, sender=Order) def delelete_HTLCs_at_order_deletion(sender, instance, **kwargs): diff --git a/frontend/src/components/BookPage.js b/frontend/src/components/BookPage.js index 961c72b9..10a591ae 100644 --- a/frontend/src/components/BookPage.js +++ b/frontend/src/components/BookPage.js @@ -11,8 +11,8 @@ export default class BookPage extends Component { type: 1, currencies_dict: {"1":"USD"} }; - this.getOrderDetails() this.getCurrencyDict() + this.getOrderDetails() this.state.currencyCode = this.getCurrencyCode(this.state.currency) } diff --git a/frontend/src/components/MakerPage.js b/frontend/src/components/MakerPage.js index e3912385..f0635c59 100644 --- a/frontend/src/components/MakerPage.js +++ b/frontend/src/components/MakerPage.js @@ -178,9 +178,10 @@ export default class MakerPage extends Component { }} onChange={this.handleCurrencyChange} > - USD - EUR - ETH + { + Object.entries(this.state.currencies_dict) + .map( ([key, value]) => {value} ) + } diff --git a/frontend/src/components/OrderPage.js b/frontend/src/components/OrderPage.js index 86ed50ec..adc11cc3 100644 --- a/frontend/src/components/OrderPage.js +++ b/frontend/src/components/OrderPage.js @@ -40,8 +40,10 @@ export default class OrderPage extends Component { super(props); this.state = { isExplicit: false, + currencies_dict: {"1":"USD"} }; this.orderId = this.props.match.params.orderId; + this.getCurrencyDict(); this.getOrderDetails(); } @@ -49,6 +51,7 @@ export default class OrderPage extends Component { fetch('/api/order' + '?order_id=' + this.orderId) .then((response) => response.json()) .then((data) => { + console.log(data) this.setState({ statusCode: data.status, statusText: data.status_message, @@ -73,12 +76,6 @@ export default class OrderPage extends Component { }); } - // Gets currency code (3 letters) from numeric (e.g., 1 -> USD) - // Improve this function so currencies are read from json - getCurrencyCode(val){ - return (val == 1 ) ? "USD": ((val == 2 ) ? "EUR":"ETH") - } - // Fix to use proper react props handleClickBackButton=()=>{ window.history.back(); @@ -97,6 +94,21 @@ export default class OrderPage extends Component { .then((response) => response.json()) .then((data) => (console.log(data) & this.getOrderDetails(data.id))); } + getCurrencyDict() { + fetch('/api/currencies') + .then((response) => response.json()) + .then((data) => + this.setState({ + currencies_dict: data + })); + } + // Gets currency code (3 letters) from numeric (e.g., 1 -> USD) + // Improve this function so currencies are read from json + getCurrencyCode(val){ + console.log("---------------------------------") + console.log(val) + return this.state.currencies_dict[val.toString()] + } render (){ return (