diff --git a/.env-sample b/.env-sample new file mode 100644 index 00000000..2e88f3ec --- /dev/null +++ b/.env-sample @@ -0,0 +1,17 @@ +# Market price public API +MARKET_PRICE_API = 'https://blockchain.info/ticker' + +# Trade fee in percentage % +FEE = 0.002 +# Bond size in percentage % +BOND_SIZE = 0.01 + +# Trade limits in satoshis +MIN_TRADE = 10000 +MAX_TRADE = 500000 + +# Expiration time in minutes +EXPIRATION_MAKE = 5 + +# Username for HTLCs escrows +ESCROW_USERNAME = 'admin' \ No newline at end of file diff --git a/api/logics.py b/api/logics.py new file mode 100644 index 00000000..77c87399 --- /dev/null +++ b/api/logics.py @@ -0,0 +1,154 @@ +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') + + +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 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: + market_prices = requests.get(MARKET_PRICE_API).json() + print(market_prices) + exchange_rate = float(market_prices[Order.Currencies(order.currency).label]['last']) + print(exchange_rate) + satoshis_now = ((float(order.amount) * 1+float(order.premium)) / exchange_rate) * 100*1000*1000 + print(satoshis_now) + + return satoshis_now + + def order_expires(order): + order.status = Order.Status.EXP + order.maker = None + order.taker = None + order.save() + + @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 cls.is_buyer(order, user) and is_valid_invoice: + 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 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 was Payment Failed. Move foward to invoice Updated. + if order.status == Order.Status.FAI: + order.status = Order.Status.UPI + order.save() + return True + + return False + + @classmethod + def gen_maker_hodl_invoice(cls, order, user): + + # Do not and delete if order is more than 5 minutes old + if order.expires_at < timezone.now(): + cls.order_expires(order) + return False, {'Order expired':'cannot generate a bond invoice for an expired order. Make a new one.'} + + if order.maker_bond: + return True, {'invoice':order.maker_bond.invoice,'bond_satoshis':order.maker_bond.num_satoshis} + + order.satoshis_now = cls.satoshis_now(order) + bond_satoshis = order.satoshis_now * BOND_SIZE + description = f'Robosats maker bond for order ID {order.id}. Will return to you if you do not cheat!' + invoice, payment_hash, expires_at = LNNode.gen_hodl_invoice(num_satoshis = bond_satoshis, description=description) + + 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, {'invoice':invoice,'bond_satoshis':bond_satoshis} + + @classmethod + def gen_taker_buyer_hodl_invoice(cls, order, user): + + # Do not and delete if order is more than 5 minutes old + if order.expires_at < timezone.now(): + cls.order_expires(order) + return False, {'Order expired':'cannot generate a bond invoice for an expired order. Make a new one.'} + + if order.maker_bond: + return True, {'invoice':order.maker_bond.invoice,'bond_satoshis':order.maker_bond.num_satoshis} + + order.satoshis_now = cls.satoshis_now(order) + bond_satoshis = order.satoshis_now * BOND_SIZE + description = f'Robosats maker bond for order ID {order.id}. Will return to you if you do not cheat!' + invoice, payment_hash, expires_at = LNNode.gen_hodl_invoice(num_satoshis = bond_satoshis, description=description) + + 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, {'invoice':invoice,'bond_satoshis':bond_satoshis} \ No newline at end of file diff --git a/api/models.py b/api/models.py index ad6de46e..655189cc 100644 --- a/api/models.py +++ b/api/models.py @@ -5,24 +5,18 @@ from django.db.models.signals import post_save, pre_delete from django.dispatch import receiver from django.utils.html import mark_safe -from datetime import timedelta -from django.utils import timezone - +from decouple import config from pathlib import Path -import requests - -from .lightning import LNNode ############################# # 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 % -ESCROW_USERNAME = 'admin' -MARKET_PRICE_API = 'https://blockchain.info/ticker' +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): @@ -189,113 +183,3 @@ class Profile(models.Model): def avatar_tag(self): return mark_safe('' % self.get_avatar()) -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 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: - market_prices = requests.get(MARKET_PRICE_API).json() - print(market_prices) - exchange_rate = float(market_prices[Order.Currencies(order.currency).label]['last']) - print(exchange_rate) - satoshis_now = ((float(order.amount) * 1+float(order.premium)) / exchange_rate) * 100*1000*1000 - print(satoshis_now) - - return satoshis_now - - def order_expires(order): - order.status = Order.Status.EXP - order.maker = None - order.taker = None - order.save() - - @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 cls.is_buyer(order, user) and is_valid_invoice: - 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 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 was Payment Failed. Move foward to invoice Updated. - if order.status == Order.Status.FAI: - order.status = Order.Status.UPI - order.save() - return True - - return False - - @classmethod - def gen_maker_hodl_invoice(cls, order, user): - - # Do not and delete if order is more than 5 minutes old - if order.expires_at < timezone.now(): - cls.order_expires(order) - return False, {'Order expired':'cannot generate a bond invoice for an expired order. Make a new one.'} - - if order.maker_bond: - return True, {'invoice':order.maker_bond.invoice,'bond_satoshis':order.maker_bond.num_satoshis} - - order.satoshis_now = cls.satoshis_now(order) - bond_satoshis = order.satoshis_now * BOND_SIZE - description = f'Robosats maker bond for order ID {order.id}. Will return to you if you do not cheat!' - invoice, payment_hash, expires_at = LNNode.gen_hodl_invoice(num_satoshis = bond_satoshis, description=description) - - 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, {'invoice':invoice,'bond_satoshis':bond_satoshis} - diff --git a/api/views.py b/api/views.py index a583f52e..68a09041 100644 --- a/api/views.py +++ b/api/views.py @@ -8,8 +8,8 @@ from django.contrib.auth import authenticate, login, logout from django.contrib.auth.models import User from .serializers import ListOrderSerializer, MakeOrderSerializer, UpdateInvoiceSerializer -from .models import Order, LNPayment, Logics -from .lightning import LNNode +from .models import Order +from .logics import Logics from .nick_generator.nick_generator import NickGenerator from robohash import Robohash @@ -21,8 +21,9 @@ from pathlib import Path from datetime import timedelta from django.utils import timezone -# .env -EXPIRATION_MAKE = 5 # minutes +from decouple import config + +EXPIRATION_MAKE = config('EXPIRATION_MAKE') avatar_path = Path('frontend/static/assets/avatars') avatar_path.mkdir(parents=True, exist_ok=True) 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