mirror of
https://github.com/RoboSats/robosats.git
synced 2025-02-20 12:19:00 +00:00
Merge pull request #25 from Reckless-Satoshi/logics-second-iteration
Not yet finished. Lot's of work left. Very buggy. A third iteration needed before v0.1.0 minimum viable product. Yet, it is time to start integrating the lightning services and these logics will be a nice place to start.
This commit is contained in:
commit
18686c26f4
12
.env-sample
12
.env-sample
@ -5,13 +5,21 @@ MARKET_PRICE_API = 'https://blockchain.info/ticker'
|
||||
FEE = 0.002
|
||||
# Bond size in percentage %
|
||||
BOND_SIZE = 0.01
|
||||
# Time out penalty for canceling takers in MINUTES
|
||||
PENALTY_TIMEOUT = 2
|
||||
|
||||
# Trade limits in satoshis
|
||||
MIN_TRADE = 10000
|
||||
MAX_TRADE = 500000
|
||||
|
||||
# Expiration time in minutes
|
||||
EXPIRATION_MAKE = 5
|
||||
# Expiration time for HODL invoices and returning collateral in HOURS
|
||||
BOND_EXPIRY = 8
|
||||
ESCROW_EXPIRY = 8
|
||||
|
||||
# Expiration time for locking collateral in MINUTES
|
||||
EXP_MAKER_BOND_INVOICE = 300
|
||||
EXP_TAKER_BOND_INVOICE = 200
|
||||
EXP_TRADE_ESCR_INVOICE = 200
|
||||
|
||||
# Username for HTLCs escrows
|
||||
ESCROW_USERNAME = 'admin'
|
18
api/admin.py
18
api/admin.py
@ -2,7 +2,7 @@ from django.contrib import admin
|
||||
from django_admin_relation_links import AdminChangeLinksMixin
|
||||
from django.contrib.auth.models import Group, User
|
||||
from django.contrib.auth.admin import UserAdmin
|
||||
from .models import Order, LNPayment, Profile
|
||||
from .models import Order, LNPayment, Profile, MarketTick
|
||||
|
||||
admin.site.unregister(Group)
|
||||
admin.site.unregister(User)
|
||||
@ -17,26 +17,34 @@ class ProfileInline(admin.StackedInline):
|
||||
@admin.register(User)
|
||||
class EUserAdmin(UserAdmin):
|
||||
inlines = [ProfileInline]
|
||||
list_display = ('avatar_tag',) + UserAdmin.list_display
|
||||
list_display_links = ['username']
|
||||
list_display = ('avatar_tag','id','username','last_login','date_joined','is_staff')
|
||||
list_display_links = ('id','username')
|
||||
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','t0_satoshis','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','is_disputed','is_fiat_sent','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')
|
||||
list_filter = ('is_disputed','is_fiat_sent','type','currency','status')
|
||||
|
||||
@admin.register(LNPayment)
|
||||
class LNPaymentAdmin(AdminChangeLinksMixin, admin.ModelAdmin):
|
||||
list_display = ('id','concept','status','num_satoshis','type','invoice','expires_at','sender_link','receiver_link')
|
||||
list_display_links = ('id','concept')
|
||||
change_links = ('sender','receiver')
|
||||
list_filter = ('type','concept','status')
|
||||
|
||||
@admin.register(Profile)
|
||||
class UserProfileAdmin(AdminChangeLinksMixin, admin.ModelAdmin):
|
||||
list_display = ('avatar_tag','id','user_link','total_ratings','avg_rating','num_disputes','lost_disputes')
|
||||
list_display_links = ('avatar_tag','id')
|
||||
change_links =['user']
|
||||
readonly_fields = ['avatar_tag']
|
||||
readonly_fields = ['avatar_tag']
|
||||
|
||||
@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']
|
@ -1,3 +1,6 @@
|
||||
# import codecs, grpc, os
|
||||
# import lightning_pb2 as lnrpc, lightning_pb2_grpc as lightningstub
|
||||
|
||||
from datetime import timedelta
|
||||
from django.utils import timezone
|
||||
|
||||
@ -12,9 +15,16 @@ class LNNode():
|
||||
'''
|
||||
Place holder functions to interact with Lightning Node
|
||||
'''
|
||||
|
||||
# macaroon = codecs.encode(open('LND_DIR/data/chain/bitcoin/simnet/admin.macaroon', 'rb').read(), 'hex')
|
||||
# os.environ['GRPC_SSL_CIPHER_SUITES'] = 'HIGH+ECDSA'
|
||||
# cert = open('LND_DIR/tls.cert', 'rb').read()
|
||||
# ssl_creds = grpc.ssl_channel_credentials(cert)
|
||||
# channel = grpc.secure_channel('localhost:10009', ssl_creds)
|
||||
# stub = lightningstub.LightningStub(channel)
|
||||
|
||||
def gen_hodl_invoice(num_satoshis, description, expiry):
|
||||
'''Generates hodl invoice to publish an order'''
|
||||
def gen_hold_invoice(num_satoshis, description, expiry):
|
||||
'''Generates hold invoice to publish an order'''
|
||||
# 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
|
||||
@ -22,32 +32,77 @@ class LNNode():
|
||||
|
||||
return invoice, payment_hash, expires_at
|
||||
|
||||
def validate_hodl_invoice_locked():
|
||||
'''Generates hodl invoice to publish an order'''
|
||||
def validate_hold_invoice_locked(payment_hash):
|
||||
'''Checks if hodl invoice is locked'''
|
||||
|
||||
# request = ln.InvoiceSubscription()
|
||||
# When invoice is settled, return true. If time expires, return False.
|
||||
# for invoice in stub.SubscribeInvoices(request):
|
||||
# print(invoice)
|
||||
|
||||
return True
|
||||
|
||||
def validate_ln_invoice(invoice): # num_satoshis
|
||||
def validate_ln_invoice(invoice, num_satoshis):
|
||||
'''Checks if the submited LN invoice is as expected'''
|
||||
|
||||
# request = lnrpc.PayReqString(pay_req=invoice)
|
||||
# response = stub.DecodePayReq(request, metadata=[('macaroon', macaroon)])
|
||||
|
||||
# # {
|
||||
# # "destination": <string>,
|
||||
# # "payment_hash": <string>,
|
||||
# # "num_satoshis": <int64>,
|
||||
# # "timestamp": <int64>,
|
||||
# # "expiry": <int64>,
|
||||
# # "description": <string>,
|
||||
# # "description_hash": <string>,
|
||||
# # "fallback_addr": <string>,
|
||||
# # "cltv_expiry": <int64>,
|
||||
# # "route_hints": <array RouteHint>,
|
||||
# # "payment_addr": <bytes>,
|
||||
# # "num_msat": <int64>,
|
||||
# # "features": <array FeaturesEntry>,
|
||||
# # }
|
||||
|
||||
# if not response['num_satoshis'] == num_satoshis:
|
||||
# return False, {'bad_invoice':f'The invoice provided is not for {num_satoshis}. '}, None, None, None
|
||||
# description = response['description']
|
||||
# payment_hash = response['payment_hash']
|
||||
# expires_at = timezone(response['expiry'])
|
||||
# if payment_hash and expires_at > timezone.now():
|
||||
# return True, None, description, payment_hash, expires_at
|
||||
|
||||
valid = True
|
||||
num_satoshis = 50000 # TODO decrypt and confirm sats are as expected
|
||||
context = None
|
||||
description = 'Placeholder desc' # TODO decrypt from LN invoice
|
||||
payment_hash = '567126' # TODO decrypt
|
||||
payment_hash = '567&*GIHU126' # TODO decrypt
|
||||
expires_at = timezone.now() # TODO decrypt
|
||||
|
||||
return valid, num_satoshis, description, payment_hash, expires_at
|
||||
return valid, context, description, payment_hash, expires_at
|
||||
|
||||
def pay_buyer_invoice(invoice):
|
||||
'''Sends sats to buyer'''
|
||||
def pay_invoice(invoice):
|
||||
'''Sends sats to buyer, or cancelinvoices'''
|
||||
return True
|
||||
|
||||
def charge_hodl_htlcs(invoice):
|
||||
'''Charges a LN hodl invoice'''
|
||||
def check_if_hold_invoice_is_locked(payment_hash):
|
||||
'''Every hodl invoice that is in state INVGEN
|
||||
Has to be checked for payment received until
|
||||
the window expires'''
|
||||
|
||||
return True
|
||||
|
||||
def free_hodl_htlcs(invoice):
|
||||
def settle_hold_htlcs(payment_hash):
|
||||
'''Charges a LN hold invoice'''
|
||||
return True
|
||||
|
||||
def return_hold_htlcs(payment_hash):
|
||||
'''Returns sats'''
|
||||
return True
|
||||
|
||||
def double_check_htlc_is_settled(payment_hash):
|
||||
''' Just as it sounds. Better safe than sorry!'''
|
||||
return True
|
||||
|
||||
|
||||
|
||||
|
||||
|
218
api/logics.py
218
api/logics.py
@ -1,15 +1,16 @@
|
||||
from datetime import timedelta
|
||||
from django.utils import timezone
|
||||
import requests
|
||||
from .lightning import LNNode
|
||||
|
||||
from .models import Order, LNPayment, User
|
||||
from .models import Order, LNPayment, MarketTick, User
|
||||
from decouple import config
|
||||
from .utils import get_exchange_rate
|
||||
|
||||
FEE = float(config('FEE'))
|
||||
BOND_SIZE = float(config('BOND_SIZE'))
|
||||
MARKET_PRICE_API = config('MARKET_PRICE_API')
|
||||
ESCROW_USERNAME = config('ESCROW_USERNAME')
|
||||
PENALTY_TIMEOUT = int(config('PENALTY_TIMEOUT'))
|
||||
|
||||
MIN_TRADE = int(config('MIN_TRADE'))
|
||||
MAX_TRADE = int(config('MAX_TRADE'))
|
||||
@ -21,9 +22,8 @@ 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)
|
||||
class Logics():
|
||||
|
||||
def validate_already_maker_or_taker(user):
|
||||
'''Checks if the user is already partipant of an order'''
|
||||
@ -38,15 +38,21 @@ class Logics():
|
||||
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}'}
|
||||
return False, {'bad_request': 'Your order is too big. It is worth '+'{:,}'.format(order.t0_satoshis)+' Sats now. But limit is '+'{:,}'.format(MAX_TRADE)+ ' Sats'}
|
||||
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 False, {'bad_request': 'Your order is too small. It is worth '+'{:,}'.format(order.t0_satoshis)+' Sats now. But limit is '+'{:,}'.format(MIN_TRADE)+ ' Sats'}
|
||||
return True, None
|
||||
|
||||
def take(order, user):
|
||||
order.taker = user
|
||||
order.status = Order.Status.TAK
|
||||
order.save()
|
||||
|
||||
@classmethod
|
||||
def take(cls, order, user):
|
||||
is_penalized, time_out = cls.is_penalized(user)
|
||||
if is_penalized:
|
||||
return False, {'bad_request',f'You need to wait {time_out} seconds to take an order'}
|
||||
else:
|
||||
order.taker = user
|
||||
order.status = Order.Status.TAK
|
||||
order.save()
|
||||
return True, None
|
||||
|
||||
def is_buyer(order, user):
|
||||
is_maker = order.maker == user
|
||||
@ -63,14 +69,27 @@ class Logics():
|
||||
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.Currencies(order.currency).label]['last'])
|
||||
satoshis_now = ((float(order.amount) * 1+float(order.premium)) / exchange_rate) * 100*1000*1000
|
||||
exchange_rate = get_exchange_rate(Order.currency_dict[str(order.currency)])
|
||||
premium_rate = exchange_rate * (1+float(order.premium)/100)
|
||||
satoshis_now = (float(order.amount) / premium_rate) * 100*1000*1000
|
||||
|
||||
return int(satoshis_now)
|
||||
|
||||
|
||||
def price_and_premium_now(order):
|
||||
''' computes order premium live '''
|
||||
exchange_rate = get_exchange_rate(Order.currency_dict[str(order.currency)])
|
||||
if not order.is_explicit:
|
||||
premium = order.premium
|
||||
price = exchange_rate
|
||||
else:
|
||||
exchange_rate = get_exchange_rate(Order.currency_dict[str(order.currency)])
|
||||
order_rate = float(order.amount) / (float(order.satoshis) / 100000000)
|
||||
premium = order_rate / exchange_rate - 1
|
||||
price = order_rate
|
||||
|
||||
premium = int(premium*100) # 2 decimals left
|
||||
return price, premium
|
||||
|
||||
def order_expires(order):
|
||||
order.status = Order.Status.EXP
|
||||
order.maker = None
|
||||
@ -89,10 +108,19 @@ class Logics():
|
||||
|
||||
@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...'}
|
||||
|
||||
# only the buyer can post a buyer invoice
|
||||
if not cls.is_buyer(order, user):
|
||||
return False, {'bad_request':'Only the buyer of this order can provide a buyer invoice.'}
|
||||
if not order.taker_bond:
|
||||
return False, {'bad_request':'Wait for your order to be taken.'}
|
||||
if not (order.taker_bond.status == order.maker_bond.status == LNPayment.Status.LOCKED):
|
||||
return False, {'bad_request':'You cannot a invoice while bonds are not posted.'}
|
||||
|
||||
num_satoshis = cls.buyer_invoice_amount(order, user)[1]['invoice_amount']
|
||||
valid, context, description, payment_hash, expires_at = LNNode.validate_ln_invoice(invoice, num_satoshis)
|
||||
if not valid:
|
||||
return False, context
|
||||
|
||||
order.buyer_invoice, _ = LNPayment.objects.update_or_create(
|
||||
concept = LNPayment.Concepts.PAYBUYER,
|
||||
@ -121,31 +149,46 @@ class Logics():
|
||||
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)
|
||||
|
||||
# If the trade is finished
|
||||
if order.status > Order.Status.PAY:
|
||||
|
||||
# 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)
|
||||
else:
|
||||
return False, {'bad_request':'You cannot rate your counterparty yet.'}
|
||||
|
||||
order.save()
|
||||
return True, None
|
||||
|
||||
def is_penalized(user):
|
||||
''' Checks if a user that is not participant of orders
|
||||
has a limit on taking or making a order'''
|
||||
|
||||
if user.profile.penalty_expiration:
|
||||
if user.profile.penalty_expiration > timezone.now():
|
||||
time_out = (user.profile.penalty_expiration - timezone.now()).seconds
|
||||
return True, time_out
|
||||
|
||||
return False, None
|
||||
|
||||
|
||||
@classmethod
|
||||
def cancel_order(cls, order, user, state):
|
||||
def cancel_order(cls, order, user, state=None):
|
||||
|
||||
# 1) When maker cancels before bond
|
||||
'''The order never shows up on the book and order
|
||||
@ -158,12 +201,24 @@ class Logics():
|
||||
|
||||
# 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'''
|
||||
Maker is charged the bond to prevent DDOS
|
||||
on the LN node and order book. TODO Only charge a small part
|
||||
of the bond (requires maker submitting an invoice)'''
|
||||
|
||||
|
||||
# 3) When taker cancels before bond
|
||||
''' The order goes back to the book as public.
|
||||
LNPayment "order.taker_bond" is deleted() '''
|
||||
elif order.status == Order.Status.TAK and order.taker == user:
|
||||
# adds a timeout penalty
|
||||
user.profile.penalty_expiration = timezone.now() + timedelta(seconds=PENALTY_TIMEOUT)
|
||||
user.save()
|
||||
|
||||
order.taker = None
|
||||
order.status = Order.Status.PUB
|
||||
order.save()
|
||||
|
||||
return True, None
|
||||
|
||||
# 4) When taker or maker cancel after bond (before escrow)
|
||||
'''The order goes into cancelled status if maker cancels.
|
||||
@ -180,7 +235,7 @@ class Logics():
|
||||
return False, {'bad_request':'You cannot cancel this order'}
|
||||
|
||||
@classmethod
|
||||
def gen_maker_hodl_invoice(cls, order, user):
|
||||
def gen_maker_hold_invoice(cls, order, user):
|
||||
|
||||
# Do not gen and cancel if order is more than 5 minutes old
|
||||
if order.expires_at < timezone.now():
|
||||
@ -198,12 +253,12 @@ class Logics():
|
||||
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)
|
||||
# Gen hold Invoice
|
||||
invoice, payment_hash, expires_at = LNNode.gen_hold_invoice(bond_satoshis, description, BOND_EXPIRY*3600)
|
||||
|
||||
order.maker_bond = LNPayment.objects.create(
|
||||
concept = LNPayment.Concepts.MAKEBOND,
|
||||
type = LNPayment.Types.HODL,
|
||||
type = LNPayment.Types.hold,
|
||||
sender = user,
|
||||
receiver = User.objects.get(username=ESCROW_USERNAME),
|
||||
invoice = invoice,
|
||||
@ -217,7 +272,7 @@ class Logics():
|
||||
return True, {'bond_invoice':invoice,'bond_satoshis':bond_satoshis}
|
||||
|
||||
@classmethod
|
||||
def gen_taker_hodl_invoice(cls, order, user):
|
||||
def gen_taker_hold_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:
|
||||
@ -237,12 +292,12 @@ class Logics():
|
||||
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)
|
||||
# Gen hold Invoice
|
||||
invoice, payment_hash, expires_at = LNNode.gen_hold_invoice(bond_satoshis, description, BOND_EXPIRY*3600)
|
||||
|
||||
order.taker_bond = LNPayment.objects.create(
|
||||
concept = LNPayment.Concepts.TAKEBOND,
|
||||
type = LNPayment.Types.HODL,
|
||||
type = LNPayment.Types.hold,
|
||||
sender = user,
|
||||
receiver = User.objects.get(username=ESCROW_USERNAME),
|
||||
invoice = invoice,
|
||||
@ -252,22 +307,24 @@ class Logics():
|
||||
payment_hash = payment_hash,
|
||||
expires_at = expires_at)
|
||||
|
||||
# Extend expiry time to allow for escrow deposit
|
||||
## Not here, on func for confirming taker collar. order.expires_at = timezone.now() + timedelta(minutes=EXP_TRADE_ESCR_INVOICE)
|
||||
|
||||
order.save()
|
||||
return True, {'bond_invoice':invoice,'bond_satoshis': bond_satoshis}
|
||||
|
||||
@classmethod
|
||||
def gen_escrow_hodl_invoice(cls, order, user):
|
||||
|
||||
def gen_escrow_hold_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
|
||||
if order.trade_escrow.status == LNPayment.Status.INVGEN:
|
||||
if order.trade_escrow.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}
|
||||
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
|
||||
@ -275,12 +332,12 @@ class Logics():
|
||||
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)
|
||||
# Gen hold Invoice
|
||||
invoice, payment_hash, expires_at = LNNode.gen_hold_invoice(escrow_satoshis, description, ESCROW_EXPIRY*3600)
|
||||
|
||||
order.taker_bond = LNPayment.objects.create(
|
||||
order.trade_escrow = LNPayment.objects.create(
|
||||
concept = LNPayment.Concepts.TRESCROW,
|
||||
type = LNPayment.Types.HODL,
|
||||
type = LNPayment.Types.hold,
|
||||
sender = user,
|
||||
receiver = User.objects.get(username=ESCROW_USERNAME),
|
||||
invoice = invoice,
|
||||
@ -291,4 +348,53 @@ class Logics():
|
||||
expires_at = expires_at)
|
||||
|
||||
order.save()
|
||||
return True, {'escrow_invoice':invoice,'escrow_satoshis': escrow_satoshis}
|
||||
return True, {'escrow_invoice':invoice,'escrow_satoshis': escrow_satoshis}
|
||||
|
||||
def settle_escrow(order):
|
||||
''' Settles the trade escrow HTLC'''
|
||||
# TODO ERROR HANDLING
|
||||
|
||||
valid = LNNode.settle_hold_htlcs(order.trade_escrow.payment_hash)
|
||||
return valid
|
||||
|
||||
def pay_buyer_invoice(order):
|
||||
''' Settles the trade escrow HTLC'''
|
||||
# TODO ERROR HANDLING
|
||||
|
||||
valid = LNNode.pay_invoice(order.buyer_invoice.payment_hash)
|
||||
return valid
|
||||
|
||||
@classmethod
|
||||
def confirm_fiat(cls, order, user):
|
||||
''' If Order is in the CHAT states:
|
||||
If user is buyer: mark FIAT SENT and settle escrow!
|
||||
If User is the seller and FIAT is SENT: Pay buyer invoice!'''
|
||||
|
||||
if order.status == Order.Status.CHA or order.status == Order.Status.FSE: # TODO Alternatively, if all collateral is locked? test out
|
||||
|
||||
# If buyer, settle escrow and mark fiat sent
|
||||
if cls.is_buyer(order, user):
|
||||
if cls.settle_escrow(order): ##### !!! KEY LINE - SETTLES THE TRADE ESCROW !!!
|
||||
order.trade_escrow.status = LNPayment.Status.SETLED
|
||||
order.status = Order.Status.FSE
|
||||
order.is_fiat_sent = True
|
||||
|
||||
# If seller and fiat sent, pay buyer invoice
|
||||
elif cls.is_seller(order, user):
|
||||
if not order.is_fiat_sent:
|
||||
return False, {'bad_request':'You cannot confirm to have received the fiat before it is confirmed to be sent by the buyer.'}
|
||||
|
||||
# Make sure the trade escrow is at least as big as the buyer invoice
|
||||
if order.trade_escrow.num_satoshis > order.buyer_invoice.num_satoshis:
|
||||
return False, {'bad_request':'Woah, something broke badly. Report in the public channels, or open a Github Issue.'}
|
||||
|
||||
# Double check the escrow is settled.
|
||||
if LNNode.double_check_htlc_is_settled(order.trade_escrow.payment_hash):
|
||||
if cls.pay_buyer_invoice(order): ##### !!! KEY LINE - PAYS THE BUYER INVOICE !!!
|
||||
order.status = Order.Status.PAY
|
||||
order.buyer_invoice.status = LNPayment.Status.PAYING
|
||||
else:
|
||||
return False, {'bad_request':'You cannot confirm the fiat payment at this stage'}
|
||||
|
||||
order.save()
|
||||
return True, None
|
113
api/models.py
113
api/models.py
@ -7,23 +7,19 @@ from django.utils.html import mark_safe
|
||||
|
||||
from decouple import config
|
||||
from pathlib import Path
|
||||
|
||||
#############################
|
||||
# TODO
|
||||
# Load hparams from .env file
|
||||
from .utils import get_exchange_rate
|
||||
import json
|
||||
|
||||
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):
|
||||
|
||||
class Types(models.IntegerChoices):
|
||||
NORM = 0, 'Regular invoice' # Only outgoing HTLCs will be regular invoices (Non-hodl)
|
||||
HODL = 1, 'Hodl invoice'
|
||||
NORM = 0, 'Regular invoice' # Only outgoing HTLCs will be regular invoices (Non-hold)
|
||||
hold = 1, 'hold invoice'
|
||||
|
||||
class Concepts(models.IntegerChoices):
|
||||
MAKEBOND = 0, 'Maker bond'
|
||||
@ -38,10 +34,11 @@ class LNPayment(models.Model):
|
||||
RETNED = 3, 'Returned'
|
||||
MISSNG = 4, 'Missing'
|
||||
VALIDI = 5, 'Valid'
|
||||
INFAIL = 6, 'Failed routing'
|
||||
PAYING = 6, 'Paying ongoing'
|
||||
FAILRO = 7, 'Failed routing'
|
||||
|
||||
# payment use details
|
||||
type = models.PositiveSmallIntegerField(choices=Types.choices, null=False, default=Types.HODL)
|
||||
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)
|
||||
routing_retries = models.PositiveSmallIntegerField(null=False, default=0)
|
||||
@ -67,31 +64,28 @@ 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 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'
|
||||
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'
|
||||
FCO = 11, 'Fiat confirmed'
|
||||
SUC = 12, 'Sucessfully settled'
|
||||
FAI = 13, 'Failed lightning network routing'
|
||||
UPI = 14, 'Updated invoice'
|
||||
DIS = 15, 'In dispute'
|
||||
DIS = 11, 'In dispute'
|
||||
CCA = 12, 'Collaboratively cancelled'
|
||||
PAY = 13, 'Sending satoshis to buyer'
|
||||
SUC = 14, 'Sucessfully settled'
|
||||
FAI = 15, 'Failed lightning network routing'
|
||||
MLD = 16, 'Maker lost dispute'
|
||||
TLD = 17, 'Taker lost dispute'
|
||||
EXP = 18, 'Expired'
|
||||
|
||||
currency_dict = json.load(open('./frontend/static/assets/currencies.json'))
|
||||
currency_choices = [(int(val), label) for val, label in list(currency_dict.items())]
|
||||
|
||||
# order info
|
||||
status = models.PositiveSmallIntegerField(choices=Status.choices, null=False, default=Status.WFB)
|
||||
@ -100,9 +94,9 @@ 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)
|
||||
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)
|
||||
@ -118,8 +112,11 @@ class Order(models.Model):
|
||||
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.
|
||||
is_disputed = models.BooleanField(default=False, null=False)
|
||||
is_fiat_sent = models.BooleanField(default=False, null=False)
|
||||
|
||||
# order collateral
|
||||
# HTLCs
|
||||
# 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)
|
||||
trade_escrow = models.ForeignKey(LNPayment, related_name='trade_escrow', on_delete=models.SET_NULL, null=True, default=None, blank=True)
|
||||
@ -127,12 +124,16 @@ 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)
|
||||
|
||||
# cancel LN invoice // these are only needed to charge lower-than-bond amounts. E.g., a taken order has a small cost if cancelled, to avoid DDOSing.
|
||||
maker_cancel = models.ForeignKey(LNPayment, related_name='maker_cancel', on_delete=models.SET_NULL, null=True, default=None, blank=True)
|
||||
taker_cancel = models.ForeignKey(LNPayment, related_name='taker_cancel', 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}')
|
||||
return (f'Order {self.id}: {self.Types(self.type).label} BTC for {float(self.amount)} {self.currency_dict[str(self.currency)]}')
|
||||
|
||||
@receiver(pre_delete, sender=Order)
|
||||
def delelete_HTLCs_at_order_deletion(sender, instance, **kwargs):
|
||||
def delete_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:
|
||||
@ -157,6 +158,9 @@ class Profile(models.Model):
|
||||
# RoboHash
|
||||
avatar = models.ImageField(default="static/assets/misc/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)
|
||||
|
||||
@receiver(post_save, sender=User)
|
||||
def create_user_profile(sender, instance, created, **kwargs):
|
||||
if created:
|
||||
@ -184,3 +188,50 @@ class Profile(models.Model):
|
||||
def avatar_tag(self):
|
||||
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.
|
||||
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
|
||||
is finished and cancellation has a cost)
|
||||
'''
|
||||
|
||||
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.PositiveSmallIntegerField(choices=Order.currency_choices, null=True)
|
||||
timestamp = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
# Relevant to keep record of the historical fee, so the insight on the premium can be better analyzed
|
||||
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
|
||||
|
||||
elif order.taker_bond.status == LNPayment.Status.LOCKED:
|
||||
volume = order.last_satoshis / 100000000
|
||||
price = float(order.amount) / volume # Amount Fiat / Amount BTC
|
||||
premium = 100 * (price / get_exchange_rate(Order.currency_dict[str(order.currency)]) - 1)
|
||||
|
||||
tick = MarketTick.objects.create(
|
||||
price=price,
|
||||
volume=volume,
|
||||
premium=premium,
|
||||
currency=order.currency)
|
||||
|
||||
tick.save()
|
||||
|
||||
def __str__(self):
|
||||
return f'Tick: {self.id}'
|
||||
|
||||
|
||||
|
@ -1,9 +1,11 @@
|
||||
from django.urls import path
|
||||
from .views import OrderMakerView, OrderView, UserView, BookView
|
||||
from .views import MakerView, OrderView, UserView, BookView, InfoView
|
||||
|
||||
urlpatterns = [
|
||||
path('make/', OrderMakerView.as_view()),
|
||||
path('make/', MakerView.as_view()),
|
||||
path('order/', OrderView.as_view({'get':'get','post':'take_update_confirm_dispute_cancel'})),
|
||||
path('usergen/', UserView.as_view()),
|
||||
path('user/', UserView.as_view()),
|
||||
path('book/', BookView.as_view()),
|
||||
# path('robot/') # Profile Info
|
||||
path('info/', InfoView.as_view()),
|
||||
]
|
16
api/utils.py
Normal file
16
api/utils.py
Normal file
@ -0,0 +1,16 @@
|
||||
|
||||
from decouple import config
|
||||
import requests
|
||||
import ring
|
||||
|
||||
storage = {}
|
||||
|
||||
@ring.dict(storage, expire=30) #keeps in cache for 30 seconds
|
||||
def get_exchange_rate(currency):
|
||||
# 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(config('MARKET_PRICE_API')).json()
|
||||
exchange_rate = float(market_prices[currency]['last'])
|
||||
|
||||
return exchange_rate
|
195
api/views.py
195
api/views.py
@ -1,3 +1,4 @@
|
||||
from re import T
|
||||
from rest_framework import status, viewsets
|
||||
from rest_framework.generics import CreateAPIView, ListAPIView
|
||||
from rest_framework.views import APIView
|
||||
@ -22,13 +23,14 @@ from django.utils import timezone
|
||||
from decouple import config
|
||||
|
||||
EXP_MAKER_BOND_INVOICE = int(config('EXP_MAKER_BOND_INVOICE'))
|
||||
FEE = float(config('FEE'))
|
||||
|
||||
avatar_path = Path('frontend/static/assets/avatars')
|
||||
avatar_path.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Create your views here.
|
||||
|
||||
class OrderMakerView(CreateAPIView):
|
||||
class MakerView(CreateAPIView):
|
||||
serializer_class = MakeOrderSerializer
|
||||
|
||||
def post(self,request):
|
||||
@ -103,6 +105,11 @@ class OrderView(viewsets.ViewSet):
|
||||
|
||||
data = ListOrderSerializer(order).data
|
||||
|
||||
# if user is under a limit (penalty), inform him
|
||||
is_penalized, time_out = Logics.is_penalized(request.user)
|
||||
if is_penalized:
|
||||
data['penalty'] = time_out
|
||||
|
||||
# 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
|
||||
@ -121,45 +128,57 @@ class OrderView(viewsets.ViewSet):
|
||||
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['status_message'] = Order.Status(order.status).label
|
||||
data['is_fiat_sent'] = order.is_fiat_sent
|
||||
data['is_disputed'] = order.is_disputed
|
||||
|
||||
# 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 both bonds are locked, participants can see the trade in sats is also final.
|
||||
if order.taker_bond:
|
||||
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.
|
||||
# Seller sees the amount he pays
|
||||
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.
|
||||
data['trade_satoshis'] = order.last_satoshis
|
||||
# Buyer sees the amount he receives
|
||||
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)
|
||||
data['trade_satoshis'] = Logics.buyer_invoice_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']:
|
||||
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']:
|
||||
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):
|
||||
|
||||
# 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 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):
|
||||
|
||||
# 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:
|
||||
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
|
||||
@ -167,6 +186,7 @@ class OrderView(viewsets.ViewSet):
|
||||
# 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):
|
||||
@ -192,35 +212,47 @@ class OrderView(viewsets.ViewSet):
|
||||
valid, context = Logics.validate_already_maker_or_taker(request.user)
|
||||
if not valid: return Response(context, status=status.HTTP_409_CONFLICT)
|
||||
|
||||
Logics.take(order, request.user)
|
||||
valid, context = Logics.take(order, request.user)
|
||||
if not valid: return Response(context, status=status.HTTP_403_FORBIDDEN)
|
||||
|
||||
else: Response({'bad_request':'This order is not public anymore.'}, status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
# 2) If action is update (invoice)
|
||||
elif action == 'update_invoice' and invoice:
|
||||
# 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)
|
||||
|
||||
# 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 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)
|
||||
if not valid: return Response(context, status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
# 4) If action is confirm
|
||||
elif action == 'confirm':
|
||||
pass
|
||||
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':
|
||||
pass
|
||||
valid, context = Logics.open_dispute(order,request.user, rating)
|
||||
if not valid: return Response(context, status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
# 6) If action is dispute
|
||||
# 6) If action is rate
|
||||
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 not valid: return Response(context, status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
# If nothing... something else is going on. Probably not allowed!
|
||||
# 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'})
|
||||
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)
|
||||
|
||||
return self.get(request)
|
||||
|
||||
@ -295,49 +327,76 @@ class UserView(APIView):
|
||||
# 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'
|
||||
return Response(context, status=status.HTTP_403_FORBIDDEN)
|
||||
return Response(context, status.HTTP_403_FORBIDDEN)
|
||||
|
||||
def delete(self,request):
|
||||
user = User.objects.get(id = request.user.id)
|
||||
''' Pressing "give me another" deletes the logged in user '''
|
||||
user = request.user
|
||||
if not user:
|
||||
return Response(status.HTTP_403_FORBIDDEN)
|
||||
|
||||
# TO DO. Pressing "give me another" deletes the logged in user
|
||||
# However it might be a long time recovered user
|
||||
# Only delete if user live is < 5 minutes
|
||||
# Only delete if user life is shorter than 30 minutes. Helps deleting users by mistake
|
||||
if user.date_joined < (timezone.now() - timedelta(minutes=30)):
|
||||
return Response(status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
# TODO check if user exists AND it is not a maker or taker!
|
||||
if user is not None:
|
||||
logout(request)
|
||||
user.delete()
|
||||
# Check if it is not a maker or taker!
|
||||
if not Logics.validate_already_maker_or_taker(user):
|
||||
return Response({'bad_request':'User cannot be deleted while he is part of an order'}, status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
return Response({'user_deleted':'User deleted permanently'},status=status.HTTP_302_FOUND)
|
||||
logout(request)
|
||||
user.delete()
|
||||
return Response({'user_deleted':'User deleted permanently'}, status.HTTP_301_MOVED_PERMANENTLY)
|
||||
|
||||
return Response(status=status.HTTP_403_FORBIDDEN)
|
||||
|
||||
|
||||
class BookView(ListAPIView):
|
||||
serializer_class = ListOrderSerializer
|
||||
queryset = Order.objects.filter(status=Order.Status.PUB)
|
||||
|
||||
def get(self,request, format=None):
|
||||
currency = request.GET.get('currency')
|
||||
type = request.GET.get('type')
|
||||
queryset = Order.objects.filter(currency=currency, type=type, status=int(Order.Status.PUB))
|
||||
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)
|
||||
elif int(type) == 2 and int(currency) != 0:
|
||||
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)
|
||||
|
||||
if len(queryset)== 0:
|
||||
return Response({'not_found':'No orders found, be the first to make one'}, status=status.HTTP_404_NOT_FOUND)
|
||||
|
||||
queryset = queryset.order_by('created_at')
|
||||
# queryset = queryset.order_by('created_at')
|
||||
book_data = []
|
||||
for order in queryset:
|
||||
data = ListOrderSerializer(order).data
|
||||
user = User.objects.filter(id=data['maker'])
|
||||
if len(user) == 1:
|
||||
data['maker_nick'] = user[0].username
|
||||
data['maker_nick'] = str(order.maker)
|
||||
|
||||
# Non participants should not see the status or who is the taker
|
||||
for key in ('status','taker'):
|
||||
# Compute current premium for those orders that are explicitly priced.
|
||||
data['price'], data['premium'] = Logics.price_and_premium_now(order)
|
||||
|
||||
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.BUY, status=Order.Status.PUB))
|
||||
context['last_day_avg_btc_premium'] = None # Todo
|
||||
context['num_active_robots'] = None
|
||||
context['total_volume'] = None
|
||||
|
||||
return Response(context, status.HTTP_200_OK)
|
||||
|
||||
|
||||
|
4915
frontend/package-lock.json
generated
4915
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -26,8 +26,12 @@
|
||||
"@emotion/styled": "^11.6.0",
|
||||
"@material-ui/core": "^4.12.3",
|
||||
"@material-ui/icons": "^4.11.2",
|
||||
"@mui/material": "^5.2.7",
|
||||
"@mui/system": "^5.2.6",
|
||||
"material-ui-image": "^3.3.2",
|
||||
"react-native": "^0.66.4",
|
||||
"react-native-svg": "^12.1.1",
|
||||
"react-qr-code": "^2.0.3",
|
||||
"react-router-dom": "^5.2.0"
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
import React, { Component } from "react";
|
||||
import { Button , Divider, Card, CardActionArea, CardContent, Typography, Grid, Select, MenuItem, FormControl, FormHelperText, List, ListItem, ListItemText, Avatar, RouterLink, ListItemAvatar} from "@material-ui/core"
|
||||
import { Paper, Button , Divider, CircularProgress, ListItemButton, Typography, Grid, Select, MenuItem, FormControl, FormHelperText, List, ListItem, ListItemText, Avatar, RouterLink, ListItemAvatar} from "@mui/material";
|
||||
import { Link } from 'react-router-dom'
|
||||
|
||||
export default class BookPage extends Component {
|
||||
@ -7,21 +7,24 @@ export default class BookPage extends Component {
|
||||
super(props);
|
||||
this.state = {
|
||||
orders: new Array(),
|
||||
currency: 1,
|
||||
currency: 0,
|
||||
type: 1,
|
||||
currencies_dict: {"0":"ANY"},
|
||||
loading: true,
|
||||
};
|
||||
this.getOrderDetails()
|
||||
this.getCurrencyDict()
|
||||
this.getOrderDetails(this.state.type,this.state.currency)
|
||||
this.state.currencyCode = this.getCurrencyCode(this.state.currency)
|
||||
}
|
||||
|
||||
// Show message to be the first one to make an order
|
||||
getOrderDetails() {
|
||||
fetch('/api/book' + '?currency=' + this.state.currency + "&type=" + this.state.type)
|
||||
getOrderDetails(type,currency) {
|
||||
fetch('/api/book' + '?currency=' + currency + "&type=" + type)
|
||||
.then((response) => response.json())
|
||||
.then((data) => //console.log(data));
|
||||
.then((data) =>
|
||||
this.setState({
|
||||
orders: data,
|
||||
not_found: data.not_found,
|
||||
loading: false,
|
||||
}));
|
||||
}
|
||||
|
||||
@ -30,25 +33,33 @@ export default class BookPage extends Component {
|
||||
this.props.history.push('/order/' + e);
|
||||
}
|
||||
|
||||
// Make these two functions sequential. getOrderDetails needs setState to be finish beforehand.
|
||||
handleTypeChange=(e)=>{
|
||||
this.setState({
|
||||
type: e.target.value,
|
||||
type: e.target.value,
|
||||
loading: true,
|
||||
});
|
||||
this.getOrderDetails();
|
||||
this.getOrderDetails(e.target.value,this.state.currency);
|
||||
}
|
||||
handleCurrencyChange=(e)=>{
|
||||
this.setState({
|
||||
currency: e.target.value,
|
||||
currencyCode: this.getCurrencyCode(e.target.value),
|
||||
loading: true,
|
||||
})
|
||||
this.getOrderDetails();
|
||||
this.getOrderDetails(this.state.type, e.target.value);
|
||||
}
|
||||
|
||||
getCurrencyDict() {
|
||||
fetch('/static/assets/currencies.json')
|
||||
.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){
|
||||
return (val == 1 ) ? "USD": ((val == 2 ) ? "EUR":"ETH")
|
||||
return this.state.currencies_dict[val.toString()]
|
||||
}
|
||||
|
||||
// pretty numbers
|
||||
@ -56,60 +67,53 @@ export default class BookPage extends Component {
|
||||
return x.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",");
|
||||
}
|
||||
|
||||
bookCards=()=>{
|
||||
bookListItems=()=>{
|
||||
return (this.state.orders.map((order) =>
|
||||
<Grid container item sm={4}>
|
||||
<Card elevation={6} sx={{ width: 945 }}>
|
||||
<>
|
||||
<ListItemButton value={order.id} onClick={() => this.handleCardClick(order.id)}>
|
||||
|
||||
<CardActionArea value={order.id} onClick={() => this.handleCardClick(order.id)}>
|
||||
<CardContent>
|
||||
<ListItemAvatar >
|
||||
<Avatar
|
||||
alt={order.maker_nick}
|
||||
src={window.location.origin +'/static/assets/avatars/' + order.maker_nick + '.png'}
|
||||
/>
|
||||
</ListItemAvatar>
|
||||
|
||||
<ListItemText>
|
||||
<Typography variant="h6">
|
||||
{order.maker_nick+" "}
|
||||
</Typography>
|
||||
</ListItemText>
|
||||
|
||||
<ListItemText align='left'>
|
||||
<Typography variant="subtitle1">
|
||||
<b>{order.type ? " Sells ": " Buys "} BTC </b> for {parseFloat(
|
||||
parseFloat(order.amount).toFixed(4))+" "+ this.getCurrencyCode(order.currency)+" "}
|
||||
</Typography>
|
||||
</ListItemText>
|
||||
|
||||
<List dense="true">
|
||||
<ListItem >
|
||||
<ListItemAvatar >
|
||||
<Avatar
|
||||
alt={order.maker_nick}
|
||||
src={window.location.origin +'/static/assets/avatars/' + order.maker_nick + '.png'}
|
||||
/>
|
||||
</ListItemAvatar>
|
||||
<ListItemText>
|
||||
<Typography gutterBottom variant="h6">
|
||||
{order.maker_nick}
|
||||
</Typography>
|
||||
</ListItemText>
|
||||
</ListItem>
|
||||
<ListItemText align='left'>
|
||||
<Typography variant="subtitle1">
|
||||
via <b>{order.payment_method}</b>
|
||||
</Typography>
|
||||
</ListItemText>
|
||||
|
||||
{/* CARD PARAGRAPH CONTENT */}
|
||||
<ListItemText>
|
||||
<Typography variant="subtitle1" color="text.secondary">
|
||||
◑{order.type == 0 ? <b> Buys </b>: <b> Sells </b>}
|
||||
<b>{parseFloat(parseFloat(order.amount).toFixed(4))}
|
||||
{" " +this.getCurrencyCode(order.currency)}</b> <a> worth of bitcoin</a>
|
||||
</Typography>
|
||||
<ListItemText align='right'>
|
||||
<Typography variant="subtitle1">
|
||||
at <b>{this.pn(order.price) + " " + this.getCurrencyCode(order.currency)}/BTC</b>
|
||||
</Typography>
|
||||
</ListItemText>
|
||||
|
||||
<Typography variant="subtitle1" color="text.secondary">
|
||||
◑ Payment via <b>{order.payment_method}</b>
|
||||
</Typography>
|
||||
{/*
|
||||
<Typography variant="subtitle1" color="text.secondary">
|
||||
◑ Priced {order.is_explicit ?
|
||||
" explicitly at " + this.pn(order.satoshis) + " Sats" : (
|
||||
" at " +
|
||||
parseFloat(parseFloat(order.premium).toFixed(4)) + "% over the market"
|
||||
)}
|
||||
</Typography> */}
|
||||
<ListItemText align='right'>
|
||||
<Typography variant="subtitle1">
|
||||
{order.premium > 1 ? "🔴" : "🔵" } <b>{parseFloat(parseFloat(order.premium).toFixed(4))}%</b>
|
||||
</Typography>
|
||||
</ListItemText>
|
||||
|
||||
<Typography variant="subtitle1" color="text.secondary">
|
||||
◑ <b>{" 42,354 "}{this.getCurrencyCode(order.currency)}/BTC</b> (Binance API)
|
||||
</Typography>
|
||||
</ListItemText>
|
||||
|
||||
</List>
|
||||
|
||||
</CardContent>
|
||||
</CardActionArea>
|
||||
</Card>
|
||||
</Grid>
|
||||
</ListItemButton>
|
||||
|
||||
<Divider/>
|
||||
</>
|
||||
));
|
||||
}
|
||||
|
||||
@ -117,7 +121,7 @@ export default class BookPage extends Component {
|
||||
return (
|
||||
<Grid className='orderBook' container spacing={1}>
|
||||
<Grid item xs={12} align="center">
|
||||
<Typography component="h4" variant="h4">
|
||||
<Typography component="h2" variant="h2">
|
||||
Order Book
|
||||
</Typography>
|
||||
</Grid>
|
||||
@ -135,7 +139,7 @@ export default class BookPage extends Component {
|
||||
style: {textAlign:"center"}
|
||||
}}
|
||||
onChange={this.handleTypeChange}
|
||||
>
|
||||
> <MenuItem value={2}>ANY</MenuItem>
|
||||
<MenuItem value={1}>BUY</MenuItem>
|
||||
<MenuItem value={0}>SELL</MenuItem>
|
||||
</Select>
|
||||
@ -155,20 +159,26 @@ export default class BookPage extends Component {
|
||||
style: {textAlign:"center"}
|
||||
}}
|
||||
onChange={this.handleCurrencyChange}
|
||||
>
|
||||
<MenuItem value={1}>USD</MenuItem>
|
||||
<MenuItem value={2}>EUR</MenuItem>
|
||||
<MenuItem value={3}>ETH</MenuItem>
|
||||
> <MenuItem value={0}>ANY</MenuItem>
|
||||
{
|
||||
Object.entries(this.state.currencies_dict)
|
||||
.map( ([key, value]) => <MenuItem value={parseInt(key)}>{value}</MenuItem> )
|
||||
}
|
||||
</Select>
|
||||
</FormControl>
|
||||
</Grid>
|
||||
{ this.state.not_found ? "" :
|
||||
<Grid item xs={12} align="center">
|
||||
<Typography component="h5" variant="h5">
|
||||
You are {this.state.type == 0 ? " selling " : " buying "} BTC for {this.state.currencyCode}
|
||||
You are {this.state.type == 0 ? <b> selling </b> : (this.state.type == 1 ? <b> buying </b> :" looking at all ")} BTC for {this.state.currencyCode}
|
||||
</Typography>
|
||||
</Grid>
|
||||
}
|
||||
{/* If loading, show circular progressbar */}
|
||||
{this.state.loading ?
|
||||
<Grid item xs={12} align="center">
|
||||
<CircularProgress />
|
||||
</Grid> : ""}
|
||||
|
||||
{ this.state.not_found ?
|
||||
(<Grid item xs={12} align="center">
|
||||
@ -184,7 +194,14 @@ export default class BookPage extends Component {
|
||||
Be the first one to create an order
|
||||
</Typography>
|
||||
</Grid>)
|
||||
: this.bookCards()
|
||||
:
|
||||
<Grid item xs={12} align="center">
|
||||
<Paper elevation={0} style={{width: 1100, maxHeight: 600, overflow: 'auto'}}>
|
||||
<List >
|
||||
{this.bookListItems()}
|
||||
</List>
|
||||
</Paper>
|
||||
</Grid>
|
||||
}
|
||||
<Grid item xs={12} align="center">
|
||||
<Button color="secondary" variant="contained" to="/" component={Link}>
|
||||
|
@ -2,11 +2,9 @@ import React, { Component } from "react";
|
||||
import { BrowserRouter as Router, Switch, Route, Link, Redirect } from "react-router-dom";
|
||||
|
||||
import UserGenPage from "./UserGenPage";
|
||||
import LoginPage from "./LoginPage.js";
|
||||
import MakerPage from "./MakerPage";
|
||||
import BookPage from "./BookPage";
|
||||
import OrderPage from "./OrderPage";
|
||||
import WaitingRoomPage from "./WaitingRoomPage";
|
||||
|
||||
export default class HomePage extends Component {
|
||||
constructor(props) {
|
||||
@ -19,11 +17,9 @@ export default class HomePage extends Component {
|
||||
<Switch>
|
||||
<Route exact path='/' component={UserGenPage}/>
|
||||
<Route path='/home'><p>You are at the start page</p></Route>
|
||||
<Route path='/login'component={LoginPage}/>
|
||||
<Route path='/make' component={MakerPage}/>
|
||||
<Route path='/book' component={BookPage}/>
|
||||
<Route path="/order/:orderId" component={OrderPage}/>
|
||||
<Route path='/wait' component={WaitingRoomPage}/>
|
||||
</Switch>
|
||||
</Router>
|
||||
);
|
||||
|
@ -1,11 +0,0 @@
|
||||
import React, { Component } from "react";
|
||||
|
||||
export default class LoginPage extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
render() {
|
||||
return <p>This is the login page</p>;
|
||||
}
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
import React, { Component } from 'react';
|
||||
import { Paper, Alert, AlertTitle, 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 "@mui/material"
|
||||
import { Link } from 'react-router-dom'
|
||||
|
||||
function getCookie(name) {
|
||||
@ -19,12 +19,18 @@ function getCookie(name) {
|
||||
}
|
||||
const csrftoken = getCookie('csrftoken');
|
||||
|
||||
// pretty numbers
|
||||
function pn(x) {
|
||||
return x.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",");
|
||||
}
|
||||
|
||||
export default class MakerPage extends Component {
|
||||
defaultCurrency = 1;
|
||||
defaultCurrencyCode = 'USD';
|
||||
defaultAmount = 0 ;
|
||||
defaultPaymentMethod = "Not specified";
|
||||
defaultPaymentMethod = "not specified";
|
||||
defaultPremium = 0;
|
||||
minTradeSats = 10000;
|
||||
maxTradeSats = 500000;
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
@ -33,11 +39,12 @@ export default class MakerPage extends Component {
|
||||
type: 0,
|
||||
currency: this.defaultCurrency,
|
||||
currencyCode: this.defaultCurrencyCode,
|
||||
amount: this.defaultAmount,
|
||||
payment_method: this.defaultPaymentMethod,
|
||||
premium: 0,
|
||||
satoshis: null,
|
||||
currencies_dict: {"1":"USD"}
|
||||
}
|
||||
this.getCurrencyDict()
|
||||
}
|
||||
|
||||
handleTypeChange=(e)=>{
|
||||
@ -46,10 +53,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)=>{
|
||||
@ -68,9 +74,16 @@ export default class MakerPage extends Component {
|
||||
});
|
||||
}
|
||||
handleSatoshisChange=(e)=>{
|
||||
var bad_sats = e.target.value > this.maxTradeSats ?
|
||||
("Must be less than "+pn(this.maxTradeSats)):
|
||||
(e.target.value < this.minTradeSats ?
|
||||
("Must be more than "+pn(this.minTradeSats)): null)
|
||||
|
||||
this.setState({
|
||||
satoshis: e.target.value,
|
||||
});
|
||||
satoshis: e.target.value,
|
||||
badSatoshis: bad_sats,
|
||||
})
|
||||
;
|
||||
}
|
||||
handleClickRelative=(e)=>{
|
||||
this.setState({
|
||||
@ -82,12 +95,13 @@ export default class MakerPage extends Component {
|
||||
handleClickExplicit=(e)=>{
|
||||
this.setState({
|
||||
isExplicit: true,
|
||||
satoshis: 10000,
|
||||
premium: null,
|
||||
});
|
||||
}
|
||||
|
||||
handleCreateOfferButtonPressed=()=>{
|
||||
this.state.amount == null ? this.setState({amount: 0}) : null;
|
||||
|
||||
console.log(this.state)
|
||||
const requestOptions = {
|
||||
method: 'POST',
|
||||
@ -108,23 +122,35 @@ export default class MakerPage extends Component {
|
||||
& (data.id ? this.props.history.push('/order/' + data.id) :"")));
|
||||
}
|
||||
|
||||
getCurrencyDict() {
|
||||
fetch('/static/assets/currencies.json')
|
||||
.then((response) => response.json())
|
||||
.then((data) =>
|
||||
this.setState({
|
||||
currencies_dict: data
|
||||
}));
|
||||
|
||||
}
|
||||
|
||||
getCurrencyCode(val){
|
||||
return this.state.currencies_dict[val.toString()]
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Grid container spacing={1}>
|
||||
<Grid item xs={12} align="center">
|
||||
<Grid container xs={12} align="center" spacing={1}>
|
||||
<Grid item xs={12} align="center">
|
||||
<Typography component="h4" variant="h4">
|
||||
Make an Order
|
||||
<Typography component="h2" variant="h2">
|
||||
Order Maker
|
||||
</Typography>
|
||||
</Grid>
|
||||
<Paper elevation={12} style={{ padding: 8,}}>
|
||||
<Grid item xs={12} align="center">
|
||||
<Grid item xs={12} align="center" spacing={1}>
|
||||
<Paper elevation={12} style={{ padding: 8, width:350, align:'center'}}>
|
||||
<Grid item xs={12} align="center" spacing={1}>
|
||||
<FormControl component="fieldset">
|
||||
<FormHelperText>
|
||||
<div align='center'>
|
||||
Choose Buy or Sell Bitcoin
|
||||
</div>
|
||||
</FormHelperText>
|
||||
<FormHelperText>
|
||||
Buy or Sell Bitcoin?
|
||||
</FormHelperText>
|
||||
<RadioGroup row defaultValue="0" onChange={this.handleTypeChange}>
|
||||
<FormControlLabel
|
||||
value="0"
|
||||
@ -141,35 +167,36 @@ export default class MakerPage extends Component {
|
||||
</RadioGroup>
|
||||
</FormControl>
|
||||
</Grid>
|
||||
<Grid item xs={12} align="center">
|
||||
<FormControl >
|
||||
<TextField
|
||||
label="Amount of Fiat to Trade"
|
||||
type="number"
|
||||
required="true"
|
||||
defaultValue={this.defaultAmount}
|
||||
inputProps={{
|
||||
min:0 ,
|
||||
style: {textAlign:"center"}
|
||||
}}
|
||||
onChange={this.handleAmountChange}
|
||||
/>
|
||||
<Select
|
||||
label="Select Payment Currency"
|
||||
required="true"
|
||||
defaultValue={this.defaultCurrency}
|
||||
inputProps={{
|
||||
style: {textAlign:"center"}
|
||||
}}
|
||||
onChange={this.handleCurrencyChange}
|
||||
>
|
||||
<MenuItem value={1}>USD</MenuItem>
|
||||
<MenuItem value={2}>EUR</MenuItem>
|
||||
<MenuItem value={3}>ETH</MenuItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
</Grid>
|
||||
<Grid container xs={11} align="center">
|
||||
<TextField
|
||||
error={this.state.amount == 0}
|
||||
helperText={this.state.amount == 0 ? 'Must be more than 0' : null}
|
||||
label="Amount"
|
||||
type="number"
|
||||
required="true"
|
||||
inputProps={{
|
||||
min:0 ,
|
||||
style: {textAlign:"center"}
|
||||
}}
|
||||
onChange={this.handleAmountChange}
|
||||
/>
|
||||
<Select
|
||||
label="Select Payment Currency"
|
||||
required="true"
|
||||
defaultValue={this.defaultCurrency}
|
||||
inputProps={{
|
||||
style: {textAlign:"center"}
|
||||
}}
|
||||
onChange={this.handleCurrencyChange}
|
||||
>
|
||||
{
|
||||
Object.entries(this.state.currencies_dict)
|
||||
.map( ([key, value]) => <MenuItem value={parseInt(key)}>{value}</MenuItem> )
|
||||
}
|
||||
</Select>
|
||||
|
||||
</Grid>
|
||||
<br/>
|
||||
<Grid item xs={12} align="center">
|
||||
<FormControl >
|
||||
<TextField
|
||||
@ -177,14 +204,21 @@ export default class MakerPage extends Component {
|
||||
type="text"
|
||||
require={true}
|
||||
inputProps={{
|
||||
style: {textAlign:"center"}
|
||||
style: {textAlign:"center"},
|
||||
maxLength: 35
|
||||
}}
|
||||
onChange={this.handlePaymentMethodChange}
|
||||
/>
|
||||
</FormControl>
|
||||
</Grid>
|
||||
|
||||
<Grid item xs={12} align="center">
|
||||
<FormControl component="fieldset">
|
||||
<FormHelperText >
|
||||
<div align='center'>
|
||||
Choose a Pricing Method
|
||||
</div>
|
||||
</FormHelperText>
|
||||
<RadioGroup row defaultValue="relative">
|
||||
<FormControlLabel
|
||||
value="relative"
|
||||
@ -201,24 +235,21 @@ export default class MakerPage extends Component {
|
||||
onClick={this.handleClickExplicit}
|
||||
/>
|
||||
</RadioGroup>
|
||||
<FormHelperText >
|
||||
<div align='center'>
|
||||
Choose a Pricing Method
|
||||
</div>
|
||||
</FormHelperText>
|
||||
</FormControl>
|
||||
</Grid>
|
||||
{/* conditional shows either Premium % field or Satoshis field based on pricing method */}
|
||||
{ this.state.isExplicit
|
||||
? <Grid item xs={12} align="center">
|
||||
<TextField
|
||||
label="Explicit Amount in Satoshis"
|
||||
label="Satoshis"
|
||||
error={this.state.badSatoshis}
|
||||
helperText={this.state.badSatoshis}
|
||||
type="number"
|
||||
required="true"
|
||||
inputProps={{
|
||||
// TODO read these from .env file
|
||||
min:10000 ,
|
||||
max:500000 ,
|
||||
min:this.minTradeSats ,
|
||||
max:this.maxTradeSats ,
|
||||
style: {textAlign:"center"}
|
||||
}}
|
||||
onChange={this.handleSatoshisChange}
|
||||
@ -238,7 +269,7 @@ export default class MakerPage extends Component {
|
||||
</Grid>
|
||||
}
|
||||
</Paper>
|
||||
</Grid>
|
||||
</Grid>
|
||||
<Grid item xs={12} align="center">
|
||||
<Button color="primary" variant="contained" onClick={this.handleCreateOfferButtonPressed} >
|
||||
Create Order
|
||||
|
@ -1,6 +1,6 @@
|
||||
import React, { Component } from "react";
|
||||
import { Paper, Button , Grid, Typography, List, ListItem, ListItemText, ListItemAvatar, Avatar, Divider} from "@material-ui/core"
|
||||
import { Link } from 'react-router-dom'
|
||||
import { Alert, Paper, CircularProgress, Button , Grid, Typography, List, ListItem, ListItemText, ListItemAvatar, Avatar, Divider, Box, LinearProgress} from "@mui/material"
|
||||
import TradeBox from "./TradeBox";
|
||||
|
||||
function msToTime(duration) {
|
||||
var seconds = Math.floor((duration / 1000) % 60),
|
||||
@ -13,6 +13,33 @@ function msToTime(duration) {
|
||||
return hours + "h " + minutes + "m " + seconds + "s";
|
||||
}
|
||||
|
||||
// TO DO fix Progress bar to go from 100 to 0, from total_expiration time, showing time_left
|
||||
function LinearDeterminate() {
|
||||
const [progress, setProgress] = React.useState(0);
|
||||
|
||||
React.useEffect(() => {
|
||||
const timer = setInterval(() => {
|
||||
setProgress((oldProgress) => {
|
||||
if (oldProgress === 0) {
|
||||
return 100;
|
||||
}
|
||||
const diff = 1;
|
||||
return Math.max(oldProgress - diff, 0);
|
||||
});
|
||||
}, 500);
|
||||
|
||||
return () => {
|
||||
clearInterval(timer);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Box sx={{ width: '100%' }}>
|
||||
<LinearProgress variant="determinate" value={progress} />
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
function getCookie(name) {
|
||||
let cookieValue = null;
|
||||
if (document.cookie && document.cookie !== '') {
|
||||
@ -40,16 +67,21 @@ export default class OrderPage extends Component {
|
||||
super(props);
|
||||
this.state = {
|
||||
isExplicit: false,
|
||||
delay: 10000, // Refresh every 10 seconds
|
||||
currencies_dict: {"1":"USD"}
|
||||
};
|
||||
this.orderId = this.props.match.params.orderId;
|
||||
this.getCurrencyDict();
|
||||
this.getOrderDetails();
|
||||
}
|
||||
|
||||
getOrderDetails() {
|
||||
this.setState(null)
|
||||
fetch('/api/order' + '?order_id=' + this.orderId)
|
||||
.then((response) => response.json())
|
||||
.then((data) => {
|
||||
.then((data) => {console.log(data) &
|
||||
this.setState({
|
||||
id: data.id,
|
||||
statusCode: data.status,
|
||||
statusText: data.status_message,
|
||||
type: data.type,
|
||||
@ -65,18 +97,40 @@ export default class OrderPage extends Component {
|
||||
makerNick: data.maker_nick,
|
||||
takerId: data.taker,
|
||||
takerNick: data.taker_nick,
|
||||
isBuyer:data.buyer,
|
||||
isSeller:data.seller,
|
||||
expiresAt:data.expires_at,
|
||||
badRequest:data.bad_request,
|
||||
isMaker: data.is_maker,
|
||||
isTaker: data.is_taker,
|
||||
isBuyer: data.is_buyer,
|
||||
isSeller: data.is_seller,
|
||||
penalty: data.penalty,
|
||||
expiresAt: data.expires_at,
|
||||
badRequest: data.bad_request,
|
||||
bondInvoice: data.bond_invoice,
|
||||
bondSatoshis: data.bond_satoshis,
|
||||
escrowInvoice: data.escrow_invoice,
|
||||
escrowSatoshis: data.escrow_satoshis,
|
||||
invoiceAmount: data.invoice_amount,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// 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")
|
||||
// These are used to refresh the data
|
||||
componentDidMount() {
|
||||
this.interval = setInterval(this.tick, this.state.delay);
|
||||
}
|
||||
componentDidUpdate(prevProps, prevState) {
|
||||
if (prevState.delay !== this.state.delay) {
|
||||
clearInterval(this.interval);
|
||||
this.interval = setInterval(this.tick, this.state.delay);
|
||||
}
|
||||
}
|
||||
componentWillUnmount() {
|
||||
clearInterval(this.interval);
|
||||
}
|
||||
tick = () => {
|
||||
this.getOrderDetails();
|
||||
}
|
||||
handleDelayChange = (e) => {
|
||||
this.setState({ delay: Number(e.target.value) });
|
||||
}
|
||||
|
||||
// Fix to use proper react props
|
||||
@ -97,13 +151,40 @@ export default class OrderPage extends Component {
|
||||
.then((response) => response.json())
|
||||
.then((data) => (console.log(data) & this.getOrderDetails(data.id)));
|
||||
}
|
||||
getCurrencyDict() {
|
||||
fetch('/static/assets/currencies.json')
|
||||
.then((response) => response.json())
|
||||
.then((data) =>
|
||||
this.setState({
|
||||
currencies_dict: data
|
||||
}));
|
||||
}
|
||||
|
||||
getCurrencyCode(val){
|
||||
let code = val ? this.state.currencies_dict[val.toString()] : ""
|
||||
return code
|
||||
}
|
||||
|
||||
render (){
|
||||
return (
|
||||
handleClickCancelOrderButton=()=>{
|
||||
console.log(this.state)
|
||||
const requestOptions = {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type':'application/json', 'X-CSRFToken': getCookie('csrftoken'),},
|
||||
body: JSON.stringify({
|
||||
'action':'cancel',
|
||||
}),
|
||||
};
|
||||
fetch('/api/order/' + '?order_id=' + this.orderId, requestOptions)
|
||||
.then((response) => response.json())
|
||||
.then((data) => (console.log(data) & this.getOrderDetails(data.id)));
|
||||
}
|
||||
|
||||
orderBox=()=>{
|
||||
return(
|
||||
<Grid container spacing={1}>
|
||||
<Grid item xs={12} align="center">
|
||||
<Typography component="h5" variant="h5">
|
||||
BTC {this.state.type ? " Sell " : " Buy "} Order
|
||||
{this.state.type ? "Sell " : "Buy "} Order Details
|
||||
</Typography>
|
||||
<Paper elevation={12} style={{ padding: 8,}}>
|
||||
<List dense="true">
|
||||
@ -114,7 +195,7 @@ export default class OrderPage extends Component {
|
||||
src={window.location.origin +'/static/assets/avatars/' + this.state.makerNick + '.png'}
|
||||
/>
|
||||
</ListItemAvatar>
|
||||
<ListItemText primary={this.state.makerNick} secondary="Order maker" align="right"/>
|
||||
<ListItemText primary={this.state.makerNick + (this.state.type ? " (Seller)" : " (Buyer)")} secondary="Order maker" align="right"/>
|
||||
</ListItem>
|
||||
<Divider />
|
||||
|
||||
@ -123,7 +204,7 @@ export default class OrderPage extends Component {
|
||||
{this.state.takerNick!='None' ?
|
||||
<>
|
||||
<ListItem align="left">
|
||||
<ListItemText primary={this.state.takerNick} secondary="Order taker"/>
|
||||
<ListItemText primary={this.state.takerNick + (this.state.type ? " (Buyer)" : " (Seller)")} secondary="Order taker"/>
|
||||
<ListItemAvatar >
|
||||
<Avatar
|
||||
alt={this.state.makerNick}
|
||||
@ -144,7 +225,7 @@ export default class OrderPage extends Component {
|
||||
}
|
||||
|
||||
<ListItem>
|
||||
<ListItemText primary={parseFloat(parseFloat(this.state.amount).toFixed(4))+" "+this.state.currencyCode} secondary="Amount and currency requested"/>
|
||||
<ListItemText primary={parseFloat(parseFloat(this.state.amount).toFixed(4))+" "+this.state.currencyCode} secondary="Amount"/>
|
||||
</ListItem>
|
||||
<Divider />
|
||||
<ListItem>
|
||||
@ -164,23 +245,87 @@ export default class OrderPage extends Component {
|
||||
<ListItemText primary={'#'+this.orderId} secondary="Order ID"/>
|
||||
</ListItem>
|
||||
<Divider />
|
||||
|
||||
<ListItem>
|
||||
<ListItemText primary={msToTime( new Date(this.state.expiresAt) - Date.now())} secondary="Expires in "/>
|
||||
<ListItemText primary={msToTime( new Date(this.state.expiresAt) - Date.now())} secondary="Expires"/>
|
||||
</ListItem>
|
||||
<LinearDeterminate />
|
||||
</List>
|
||||
|
||||
{/* If the user has a penalty/limit */}
|
||||
{this.state.penalty ?
|
||||
<>
|
||||
<Divider />
|
||||
<Grid item xs={12} align="center">
|
||||
<Alert severity="warning" sx={{maxWidth:360}}>
|
||||
You cannot take an order yet! Wait {this.state.penalty} seconds
|
||||
</Alert>
|
||||
</Grid>
|
||||
</>
|
||||
: null}
|
||||
|
||||
</Paper>
|
||||
</Grid>
|
||||
|
||||
<Grid item xs={12} align="center">
|
||||
{this.state.isParticipant ? "" : <Button variant='contained' color='primary' onClick={this.handleClickTakeOrderButton}>Take Order</Button>}
|
||||
</Grid>
|
||||
<Grid item xs={12} align="center">
|
||||
<Button variant='contained' color='secondary' onClick={this.handleClickBackButton}>Back</Button>
|
||||
</Grid>
|
||||
{/* Participants cannot see the Back or Take Order buttons */}
|
||||
{this.state.isParticipant ? "" :
|
||||
<>
|
||||
<Grid item xs={12} align="center">
|
||||
<Button variant='contained' color='primary' onClick={this.handleClickTakeOrderButton}>Take Order</Button>
|
||||
</Grid>
|
||||
<Grid item xs={12} align="center">
|
||||
<Button variant='contained' color='secondary' onClick={this.handleClickBackButton}>Back</Button>
|
||||
</Grid>
|
||||
</>
|
||||
}
|
||||
|
||||
{/* Makers can cancel before commiting the bond (status 0)*/}
|
||||
{this.state.isMaker & this.state.statusCode == 0 ?
|
||||
<Grid item xs={12} align="center">
|
||||
<Button variant='contained' color='secondary' onClick={this.handleClickCancelOrderButton}>Cancel</Button>
|
||||
</Grid>
|
||||
:""}
|
||||
|
||||
{/* Takers can cancel before commiting the bond (status 3)*/}
|
||||
{this.state.isTaker & this.state.statusCode == 3 ?
|
||||
<Grid item xs={12} align="center">
|
||||
<Button variant='contained' color='secondary' onClick={this.handleClickCancelOrderButton}>Cancel</Button>
|
||||
</Grid>
|
||||
:""}
|
||||
|
||||
</Grid>
|
||||
</Grid>
|
||||
)
|
||||
}
|
||||
|
||||
orderDetailsPage (){
|
||||
return(
|
||||
this.state.badRequest ?
|
||||
<div align='center'>
|
||||
<Typography component="subtitle2" variant="subtitle2" color="secondary" >
|
||||
{this.state.badRequest}<br/>
|
||||
</Typography>
|
||||
<Button variant='contained' color='secondary' onClick={this.handleClickBackButton}>Back</Button>
|
||||
</div>
|
||||
:
|
||||
(this.state.isParticipant ?
|
||||
<Grid container xs={12} align="center" spacing={2}>
|
||||
<Grid item xs={6} align="left">
|
||||
{this.orderBox()}
|
||||
</Grid>
|
||||
<Grid item xs={6} align="left">
|
||||
<TradeBox data={this.state}/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
:
|
||||
<Grid item xs={12} align="center">
|
||||
{this.orderBox()}
|
||||
</Grid>)
|
||||
)
|
||||
}
|
||||
|
||||
render (){
|
||||
return (
|
||||
// Only so nothing shows while requesting the first batch of data
|
||||
(this.state.statusCode == null & this.state.badRequest == null) ? <CircularProgress /> : this.orderDetailsPage()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
400
frontend/src/components/TradeBox.js
Normal file
400
frontend/src/components/TradeBox.js
Normal file
@ -0,0 +1,400 @@
|
||||
import React, { Component } from "react";
|
||||
import { Paper, Button, Grid, Typography, TextField, List, ListItem, ListItemText, Divider} from "@mui/material"
|
||||
import QRCode from "react-qr-code";
|
||||
|
||||
function getCookie(name) {
|
||||
let cookieValue = null;
|
||||
if (document.cookie && document.cookie !== '') {
|
||||
const cookies = document.cookie.split(';');
|
||||
for (let i = 0; i < cookies.length; i++) {
|
||||
const cookie = cookies[i].trim();
|
||||
// Does this cookie string begin with the name we want?
|
||||
if (cookie.substring(0, name.length + 1) === (name + '=')) {
|
||||
cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return cookieValue;
|
||||
}
|
||||
const csrftoken = getCookie('csrftoken');
|
||||
|
||||
// pretty numbers
|
||||
function pn(x) {
|
||||
return x.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",");
|
||||
}
|
||||
|
||||
export default class TradeBox extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
showQRInvoice=()=>{
|
||||
return (
|
||||
<Grid container spacing={1}>
|
||||
<Grid item xs={12} align="center">
|
||||
<Typography component="body2" variant="body2">
|
||||
Robots around here usually show commitment
|
||||
</Typography>
|
||||
</Grid>
|
||||
<Grid item xs={12} align="center">
|
||||
{this.props.data.isMaker ?
|
||||
<Typography component="subtitle1" variant="subtitle1">
|
||||
<b>Lock {pn(this.props.data.bondSatoshis)} Sats to PUBLISH order </b>
|
||||
</Typography>
|
||||
:
|
||||
<Typography component="subtitle1" variant="subtitle1">
|
||||
<b>Lock {pn(this.props.data.bondSatoshis)} Sats to TAKE the order </b>
|
||||
</Typography>
|
||||
}
|
||||
</Grid>
|
||||
<Grid item xs={12} align="center">
|
||||
<QRCode value={this.props.data.bondInvoice} size={305}/>
|
||||
</Grid>
|
||||
<Grid item xs={12} align="center">
|
||||
<TextField
|
||||
hiddenLabel
|
||||
variant="filled"
|
||||
size="small"
|
||||
defaultValue={this.props.data.bondInvoice}
|
||||
disabled="true"
|
||||
helperText="This is a hold invoice. It will not be charged if the order succeeds or expires.
|
||||
It will be charged if the order is cancelled or you lose a dispute."
|
||||
color = "secondary"
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
);
|
||||
}
|
||||
|
||||
showEscrowQRInvoice=()=>{
|
||||
return (
|
||||
<Grid container spacing={1}>
|
||||
<Grid item xs={12} align="center">
|
||||
<Typography component="subtitle1" variant="subtitle1">
|
||||
<b>Deposit {pn(this.props.data.escrowSatoshis)} Sats as trade collateral </b>
|
||||
</Typography>
|
||||
</Grid>
|
||||
<Grid item xs={12} align="center">
|
||||
<QRCode value={this.props.data.escrowInvoice} size={305}/>
|
||||
</Grid>
|
||||
<Grid item xs={12} align="center">
|
||||
<TextField
|
||||
hiddenLabel
|
||||
variant="filled"
|
||||
size="small"
|
||||
defaultValue={this.props.data.escrowInvoice}
|
||||
disabled="true"
|
||||
helperText="This is a hold LN invoice. It will be charged once the buyer confirms he sent the fiat."
|
||||
color = "secondary"
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
);
|
||||
}
|
||||
|
||||
showTakerFound=()=>{
|
||||
|
||||
// TODO Make some sound here! The maker might have been waiting for long
|
||||
|
||||
return (
|
||||
<Grid container spacing={1}>
|
||||
<Grid item xs={12} align="center">
|
||||
<Typography component="subtitle1" variant="subtitle1">
|
||||
<b>A taker has been found! </b>
|
||||
</Typography>
|
||||
</Grid>
|
||||
<Divider/>
|
||||
<Grid item xs={12} align="center">
|
||||
<Typography component="body2" variant="body2">
|
||||
Please wait for the taker to confirm his commitment by locking a bond.
|
||||
</Typography>
|
||||
</Grid>
|
||||
</Grid>
|
||||
);
|
||||
}
|
||||
|
||||
showMakerWait=()=>{
|
||||
return (
|
||||
<Grid container spacing={1}>
|
||||
<Grid item xs={12} align="center">
|
||||
<Typography component="subtitle1" variant="subtitle1">
|
||||
<b> Your order is public, wait for a taker. </b>
|
||||
</Typography>
|
||||
</Grid>
|
||||
<Grid item xs={12} align="center">
|
||||
|
||||
<List dense="true">
|
||||
<Divider/>
|
||||
<ListItem>
|
||||
<Typography component="body2" variant="body2" align="left">
|
||||
<p>Be patient while robots check the book.
|
||||
It might take some time. This box will ring 🔊 once a robot takes your order. </p>
|
||||
<p>Please note that if your premium is too high, or if your currency or payment
|
||||
methods are not popular, your order might expire untaken. Your bond will
|
||||
return to you (no action needed).</p>
|
||||
</Typography>
|
||||
</ListItem>
|
||||
|
||||
{/* TODO API sends data for a more confortable wait */}
|
||||
<Divider/>
|
||||
<ListItem>
|
||||
<ListItemText primary={999} secondary="Robots looking at the book"/>
|
||||
</ListItem>
|
||||
|
||||
<Divider/>
|
||||
<ListItem>
|
||||
<ListItemText primary={999} secondary={"Active orders for " + this.props.data.currencyCode}/>
|
||||
</ListItem>
|
||||
|
||||
<Divider/>
|
||||
<ListItem>
|
||||
<ListItemText primary="33%" secondary="Premium percentile" />
|
||||
</ListItem>
|
||||
</List>
|
||||
</Grid>
|
||||
</Grid>
|
||||
)
|
||||
}
|
||||
|
||||
handleInputInvoiceChanged=(e)=>{
|
||||
this.setState({
|
||||
invoice: e.target.value,
|
||||
});
|
||||
}
|
||||
|
||||
// Fix this. It's clunky because it takes time. this.props.data does not refresh until next refresh of OrderPage.
|
||||
|
||||
handleClickSubmitInvoiceButton=()=>{
|
||||
const requestOptions = {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type':'application/json', 'X-CSRFToken': getCookie('csrftoken'),},
|
||||
body: JSON.stringify({
|
||||
'action':'update_invoice',
|
||||
'invoice': this.state.invoice,
|
||||
}),
|
||||
};
|
||||
fetch('/api/order/' + '?order_id=' + this.props.data.id, requestOptions)
|
||||
.then((response) => response.json())
|
||||
.then((data) => (this.props.data = data));
|
||||
}
|
||||
|
||||
showInputInvoice(){
|
||||
return (
|
||||
|
||||
// TODO Camera option to read QR
|
||||
|
||||
<Grid container spacing={1}>
|
||||
<Grid item xs={12} align="center">
|
||||
<Typography component="subtitle1" variant="subtitle1">
|
||||
<b> Submit a LN invoice for {pn(this.props.data.invoiceAmount)} Sats </b>
|
||||
</Typography>
|
||||
</Grid>
|
||||
<Grid item xs={12} align="left">
|
||||
<Typography component="body2" variant="body2">
|
||||
The taker is committed! Before letting you send {" "+ parseFloat(parseFloat(this.props.data.amount).toFixed(4))+
|
||||
" "+ this.props.data.currencyCode}, we want to make sure you are able to receive the BTC. Please provide a
|
||||
valid invoice for {pn(this.props.data.invoiceAmount)} Satoshis.
|
||||
</Typography>
|
||||
</Grid>
|
||||
<Grid item xs={12} align="center">
|
||||
<TextField
|
||||
label={"Payout Lightning Invoice"}
|
||||
required
|
||||
inputProps={{
|
||||
style: {textAlign:"center"}
|
||||
}}
|
||||
multiline
|
||||
onChange={this.handleInputInvoiceChanged}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={12} align="center">
|
||||
<Button variant='contained' color='primary' onClick={this.handleClickSubmitInvoiceButton}>Submit</Button>
|
||||
</Grid>
|
||||
</Grid>
|
||||
)
|
||||
}
|
||||
|
||||
showWaitingForEscrow(){
|
||||
return(
|
||||
<Grid container spacing={1}>
|
||||
<Grid item xs={12} align="center">
|
||||
<Typography component="subtitle1" variant="subtitle1">
|
||||
<b>Your invoice looks good!</b>
|
||||
</Typography>
|
||||
</Grid>
|
||||
<Grid item xs={12} align="center">
|
||||
<Typography component="body2" variant="body2" align="left">
|
||||
<p>We are waiting for the seller to deposit the full trade BTC amount
|
||||
into the escrow.</p>
|
||||
<p> Just hang on for a moment. If the seller does not deposit,
|
||||
you will get your bond back automatically.</p>
|
||||
</Typography>
|
||||
</Grid>
|
||||
</Grid>
|
||||
)
|
||||
}
|
||||
|
||||
showWaitingForBuyerInvoice(){
|
||||
return(
|
||||
<Grid container spacing={1}>
|
||||
<Grid item xs={12} align="center">
|
||||
<Typography component="subtitle1" variant="subtitle1">
|
||||
<b>The trade collateral is locked! :D </b>
|
||||
</Typography>
|
||||
</Grid>
|
||||
<Grid item xs={12} align="center">
|
||||
<Typography component="body2" variant="body2" align="left">
|
||||
<p> We are waiting for the buyer to post a lightning invoice. Once
|
||||
he does, you will be able to directly communicate the fiat payment
|
||||
details. </p>
|
||||
<p> Just hang on for a moment. If the buyer does not cooperate,
|
||||
you will get back the trade collateral and your bond automatically.</p>
|
||||
</Typography>
|
||||
</Grid>
|
||||
</Grid>
|
||||
)
|
||||
}
|
||||
|
||||
handleClickConfirmButton=()=>{
|
||||
const requestOptions = {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type':'application/json', 'X-CSRFToken': getCookie('csrftoken'),},
|
||||
body: JSON.stringify({
|
||||
'action': "confirm",
|
||||
}),
|
||||
};
|
||||
fetch('/api/order/' + '?order_id=' + this.props.data.id, requestOptions)
|
||||
.then((response) => response.json())
|
||||
.then((data) => (this.props.data = data));
|
||||
}
|
||||
handleClickOpenDisputeButton=()=>{
|
||||
const requestOptions = {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type':'application/json', 'X-CSRFToken': getCookie('csrftoken'),},
|
||||
body: JSON.stringify({
|
||||
'action': "dispute",
|
||||
}),
|
||||
};
|
||||
fetch('/api/order/' + '?order_id=' + this.props.data.id, requestOptions)
|
||||
.then((response) => response.json())
|
||||
.then((data) => (this.props.data = data));
|
||||
}
|
||||
|
||||
showFiatSentButton(){
|
||||
return(
|
||||
<Grid container spacing={1}>
|
||||
<Grid item xs={12} align="center">
|
||||
<Button defaultValue="confirm" variant='contained' color='primary' onClick={this.handleClickConfirmButton}>Confirm {this.props.data.currencyCode} sent</Button>
|
||||
</Grid>
|
||||
</Grid>
|
||||
)
|
||||
}
|
||||
|
||||
showFiatReceivedButton(){
|
||||
// TODO, show alert and ask for double confirmation (Have you check you received the fiat? Confirming fiat received settles the trade.)
|
||||
// Ask for double confirmation.
|
||||
return(
|
||||
<Grid container spacing={1}>
|
||||
<Grid item xs={12} align="center">
|
||||
<Button defaultValue="confirm" variant='contained' color='primary' onClick={this.handleClickConfirmButton}>Confirm {this.props.data.currencyCode} received</Button>
|
||||
</Grid>
|
||||
</Grid>
|
||||
)
|
||||
}
|
||||
|
||||
showOpenDisputeButton(){
|
||||
// TODO, show alert about how opening a dispute might involve giving away personal data and might mean losing the bond. Ask for double confirmation.
|
||||
return(
|
||||
<Grid container spacing={1}>
|
||||
<Grid item xs={12} align="center">
|
||||
<Button defaultValue="dispute" variant='contained' onClick={this.handleClickOpenDisputeButton}>Open Dispute</Button>
|
||||
</Grid>
|
||||
</Grid>
|
||||
)
|
||||
}
|
||||
|
||||
showChat(sendFiatButton, receivedFiatButton, openDisputeButton){
|
||||
return(
|
||||
<Grid container spacing={1}>
|
||||
<Grid item xs={12} align="center">
|
||||
<Typography component="subtitle1" variant="subtitle1">
|
||||
<b>Chatting with {this.props.data.isMaker ? this.props.data.takerNick : this.props.data.makerNick}</b>
|
||||
</Typography>
|
||||
</Grid>
|
||||
<Grid item xs={12} align="left">
|
||||
{this.props.data.isSeller ?
|
||||
<Typography component="body2" variant="body2">
|
||||
Say hi to your peer robot! Be helpful and concise. Let him know how to send you {this.props.data.currencyCode}.
|
||||
</Typography>
|
||||
:
|
||||
<Typography component="body2" variant="body2">
|
||||
Say hi to your peer robot! Ask for payment details and click 'Confirm {this.props.data.currencyCode} sent' as soon as you send the payment.
|
||||
</Typography>
|
||||
}
|
||||
</Grid>
|
||||
<Grid item xs={12} style={{ width:330, height:360}}>
|
||||
CHAT PLACEHOLDER
|
||||
</Grid>
|
||||
<Grid item xs={12} align="center">
|
||||
{sendFiatButton ? this.showFiatSentButton() : ""}
|
||||
{receivedFiatButton ? this.showFiatReceivedButton() : ""}
|
||||
{openDisputeButton ? this.showOpenDisputeButton() : ""}
|
||||
</Grid>
|
||||
</Grid>
|
||||
)
|
||||
}
|
||||
|
||||
// showFiatReceivedButton(){
|
||||
|
||||
// }
|
||||
|
||||
// showOpenDisputeButton(){
|
||||
|
||||
// }
|
||||
|
||||
// showRateSelect(){
|
||||
|
||||
// }
|
||||
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Grid container spacing={1} style={{ width:330}}>
|
||||
<Grid item xs={12} align="center">
|
||||
<Typography component="h5" variant="h5">
|
||||
TradeBox
|
||||
</Typography>
|
||||
<Paper elevation={12} style={{ padding: 8,}}>
|
||||
{/* Maker and taker Bond request */}
|
||||
{this.props.data.bondInvoice ? this.showQRInvoice() : ""}
|
||||
|
||||
{/* Waiting for taker and taker bond request */}
|
||||
{this.props.data.isMaker & this.props.data.statusCode == 1 ? this.showMakerWait() : ""}
|
||||
{this.props.data.isMaker & this.props.data.statusCode == 3 ? this.showTakerFound() : ""}
|
||||
|
||||
{/* Send Invoice (buyer) and deposit collateral (seller) */}
|
||||
{this.props.data.isSeller & this.props.data.escrowInvoice != null ? this.showEscrowQRInvoice() : ""}
|
||||
{this.props.data.isBuyer & this.props.data.invoiceAmount != null ? this.showInputInvoice() : ""}
|
||||
{this.props.data.isBuyer & this.props.data.statusCode == 7 ? this.showWaitingForEscrow() : ""}
|
||||
{this.props.data.isSeller & this.props.data.statusCode == 8 ? this.showWaitingForBuyerInvoice() : ""}
|
||||
|
||||
{/* In Chatroom - showChat(showSendButton, showReveiceButton, showDisputeButton) */}
|
||||
{this.props.data.isBuyer & this.props.data.statusCode == 9 ? this.showChat(true,false,true) : ""}
|
||||
{this.props.data.isSeller & this.props.data.statusCode == 9 ? this.showChat(false,false,true) : ""}
|
||||
{this.props.data.isBuyer & this.props.data.statusCode == 10 ? this.showChat(false,false,true) : ""}
|
||||
{this.props.data.isSeller & this.props.data.statusCode == 10 ? this.showChat(false,true,true) : ""}
|
||||
|
||||
{/* Trade Finished */}
|
||||
{this.props.data.isSeller & this.props.data.statusCode > 12 & this.props.data.statusCode < 15 ? this.showRateSelect() : ""}
|
||||
{/* TODO */}
|
||||
{/* */}
|
||||
{/* */}
|
||||
|
||||
|
||||
</Paper>
|
||||
</Grid>
|
||||
</Grid>
|
||||
);
|
||||
}
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
import React, { Component } from "react";
|
||||
import { Button , Grid, Typography, TextField, ButtonGroup} from "@material-ui/core"
|
||||
import { Button , Grid, Typography, TextField, ButtonGroup} from "@mui/material"
|
||||
import { Link } from 'react-router-dom'
|
||||
import Image from 'material-ui-image'
|
||||
|
||||
@ -24,9 +24,9 @@ export default class UserGenPage extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
token: this.genBase62Token(32),
|
||||
token: this.genBase62Token(34),
|
||||
};
|
||||
this.getGeneratedUser();
|
||||
this.getGeneratedUser(this.state.token);
|
||||
}
|
||||
|
||||
// sort of cryptographically strong function to generate Base62 token client-side
|
||||
@ -40,8 +40,8 @@ export default class UserGenPage extends Component {
|
||||
.substring(0, length);
|
||||
}
|
||||
|
||||
getGeneratedUser() {
|
||||
fetch('/api/usergen' + '?token=' + this.state.token)
|
||||
getGeneratedUser(token) {
|
||||
fetch('/api/user' + '?token=' + token)
|
||||
.then((response) => response.json())
|
||||
.then((data) => {
|
||||
this.setState({
|
||||
@ -60,7 +60,7 @@ export default class UserGenPage extends Component {
|
||||
method: 'DELETE',
|
||||
headers: {'Content-Type':'application/json', 'X-CSRFToken': getCookie('csrftoken')},
|
||||
};
|
||||
fetch("/api/usergen", requestOptions)
|
||||
fetch("/api/user", requestOptions)
|
||||
.then((response) => response.json())
|
||||
.then((data) => console.log(data));
|
||||
}
|
||||
@ -72,7 +72,7 @@ export default class UserGenPage extends Component {
|
||||
handleAnotherButtonPressed=(e)=>{
|
||||
this.delGeneratedUser()
|
||||
this.setState({
|
||||
token: this.genBase62Token(32),
|
||||
token: this.genBase62Token(34),
|
||||
})
|
||||
this.reload_for_csrf_to_work();
|
||||
}
|
||||
@ -82,7 +82,7 @@ export default class UserGenPage extends Component {
|
||||
this.setState({
|
||||
token: e.target.value,
|
||||
})
|
||||
this.getGeneratedUser();
|
||||
this.getGeneratedUser(e.target.value);
|
||||
}
|
||||
|
||||
// TO FIX CSRF TOKEN IS NOT UPDATED UNTIL WINDOW IS RELOADED
|
||||
@ -137,13 +137,13 @@ export default class UserGenPage extends Component {
|
||||
<Grid item xs={12} align="center">
|
||||
<ButtonGroup variant="contained" aria-label="outlined primary button group">
|
||||
<Button color='primary' to='/make/' component={Link}>Make Order</Button>
|
||||
<Button to='/home' component={Link}>INFO</Button>
|
||||
<Button color='inherit' to='/home' component={Link}>INFO</Button>
|
||||
<Button color='secondary' to='/book/' component={Link}>View Book</Button>
|
||||
</ButtonGroup>
|
||||
</Grid>
|
||||
<Grid item xs={12} align="center">
|
||||
<Typography component="h5" variant="h5">
|
||||
Easy and Private Lightning peer-to-peer Exchange
|
||||
Simple and Private Lightning peer-to-peer Exchange
|
||||
</Typography>
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
@ -1,11 +0,0 @@
|
||||
import React, { Component } from "react";
|
||||
|
||||
export default class WaitingRoomPage extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
render() {
|
||||
return <p>This is the waiting room</p>;
|
||||
}
|
||||
}
|
8
frontend/static/assets/currencies.json
Normal file
8
frontend/static/assets/currencies.json
Normal file
@ -0,0 +1,8 @@
|
||||
{
|
||||
"1":"USD",
|
||||
"2":"EUR",
|
||||
"3":"ETH",
|
||||
"4":"AUD",
|
||||
"5":"BRL",
|
||||
"6":"CAD"
|
||||
}
|
@ -8,5 +8,5 @@ urlpatterns = [
|
||||
path('make/', index),
|
||||
path('book/', index),
|
||||
path('order/<int:orderId>', index),
|
||||
path('wait/', index),
|
||||
path('wait/', index),
|
||||
]
|
@ -29,4 +29,7 @@ module.exports = {
|
||||
},
|
||||
}),
|
||||
],
|
||||
resolve: {
|
||||
extensions: ['.ts', '.js'],
|
||||
},
|
||||
};
|
6
setup.md
6
setup.md
@ -7,6 +7,7 @@
|
||||
```
|
||||
pip install virtualenvwrapper
|
||||
pip install python-decouple
|
||||
pip install ring
|
||||
```
|
||||
|
||||
### Add to .bashrc
|
||||
@ -66,7 +67,12 @@ npm install react-router-dom@5.2.0
|
||||
npm install @material-ui/icons
|
||||
npm install material-ui-image
|
||||
npm install @mui/system @emotion/react @emotion/styled
|
||||
npm install react-native
|
||||
npm install react-native-svg
|
||||
npm install react-qr-code
|
||||
npm install @mui/material
|
||||
```
|
||||
Note we are using mostly MaterialUI V5, but Image loading from V4 extentions (so both V4 and V5 are needed)
|
||||
|
||||
### Launch the React render
|
||||
from frontend/ directory
|
||||
|
Loading…
Reference in New Issue
Block a user