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:
Reckless_Satoshi 2022-01-10 18:52:41 +00:00 committed by GitHub
commit 18686c26f4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 6028 additions and 520 deletions

View File

@ -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'

View File

@ -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']

View File

@ -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

View File

@ -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

View File

@ -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}'

View File

@ -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
View 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

View File

@ -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)

File diff suppressed because it is too large Load Diff

View File

@ -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"
}
}

View File

@ -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}>

View File

@ -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>
);

View File

@ -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>;
}
}

View File

@ -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

View File

@ -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()
);
}
}
}

View 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>
);
}
}

View File

@ -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>

View File

@ -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>;
}
}

View File

@ -0,0 +1,8 @@
{
"1":"USD",
"2":"EUR",
"3":"ETH",
"4":"AUD",
"5":"BRL",
"6":"CAD"
}

View File

@ -8,5 +8,5 @@ urlpatterns = [
path('make/', index),
path('book/', index),
path('order/<int:orderId>', index),
path('wait/', index),
path('wait/', index),
]

View File

@ -29,4 +29,7 @@ module.exports = {
},
}),
],
resolve: {
extensions: ['.ts', '.js'],
},
};

View File

@ -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