Merge pull request #29 from Reckless-Satoshi/logics-third-iteration

Logics third iteration. A myriad of changes in both frontend and backend.
This commit is contained in:
Reckless_Satoshi 2022-01-19 13:35:10 +00:00 committed by GitHub
commit dcb54855ee
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
45 changed files with 2182 additions and 1254 deletions

View File

@ -6,12 +6,15 @@ LND_GRPC_HOST='127.0.0.1:10009'
REDIS_URL='' REDIS_URL=''
# Market price public API # List of market price public APIs. If the currency is available in more than 1 API, will use median price.
MARKET_PRICE_API = 'https://blockchain.info/ticker' MARKET_PRICE_APIS = https://blockchain.info/ticker, https://api.yadio.io/exrates/BTC
# Host e.g. robotestagw3dcxmd66r4rgksb4nmmr43fh77bzn2ia2eucduyeafnyd.onion # Host e.g. robosats.com
HOST_NAME = '' HOST_NAME = ''
# e.g. robotestagw3dcxmd66r4rgksb4nmmr43fh77bzn2ia2eucduyeafnyd.onion
ONION_LOCATION = ''
# Trade fee in percentage % # Trade fee in percentage %
FEE = 0.002 FEE = 0.002
# Bond size in percentage % # Bond size in percentage %

3
.gitignore vendored
View File

@ -639,6 +639,9 @@ FodyWeavers.xsd
*migrations* *migrations*
frontend/static/frontend/main* frontend/static/frontend/main*
# Celery
django
# robosats # robosats
frontend/static/assets/avatars* frontend/static/assets/avatars*
api/lightning/lightning* api/lightning/lightning*

View File

@ -1,25 +1,46 @@
# RoboSats: Buy and sell non-KYC Satoshis. ## RoboSats - Buy and sell Satoshis Privately.
## What is RoboSats? [![release](https://img.shields.io/badge/release-v0.1.0%20MVP-orange)](https://github.com/Reckless-Satoshi/robosats/releases)
[![AGPL-3.0 license](https://img.shields.io/badge/license-AGPL--3.0-blue)](https://github.com/Reckless-Satoshi/robosats/blob/main/LICENSE)
[![Telegram](https://img.shields.io/badge/chat-telegram-brightgreen)](https://t.me/robosats)
RoboSats is a simple and private way to exchange bitcoin for national currencies. Robosats aims to simplify the peer-to-peer experience and uses lightning hodl invoices to minimize the trust needed to trade. In addition, your Robotic Satoshi will help you stick to best privacy practices. RoboSats is a simple and private way to exchange bitcoin for national currencies. Robosats simplifies the peer-to-peer user experience and uses lightning hold invoices to minimize custody and trust requirements. The deterministically generated avatars help users stick to best privacy practices.
## Try it out ## Try it out
**Bitcoin mainnet:** **Bitcoin mainnet:**
- Tor: robosatsbkpis32grrxz7vliwjuivdmsyjx4d7zrlffo3nul44ck5sad.onion - Tor: robosats6tkf3eva7x2voqso3a5wcorsnw34jveyxfqi2fu7oyheasid.onion (Coming soon)
- Url: robosats.org (Not active) - Url: robosats.com (Coming soon)
- Version: v0.0.0 (Last stable) - Version: v0.0.0 (Last stable)
**Bitcoin testnet:** **Bitcoin testnet:**
- Tor: robotescktg6eqthfvatugczhzo3rj5zzk7rrkp6n5pa5qrz2mdikwid.onion - Tor: robotestagw3dcxmd66r4rgksb4nmmr43fh77bzn2ia2eucduyeafnyd.onion (Active - On Dev Node)
- Url: testnet.robosats.org (Not active) - Url: testnet.robosats.com (Coming soon)
- Commit height: v0.0.0 Latest commit. - Commit height: Latest commit.
*Always use [Tor Browser](https://www.torproject.org/download/) and .onion for best anonymity.*
## How it works
Alice wants to buy satoshis privately:
1. Alice generates an avatar (AdequateAlice01) using her private random token.
2. Alice stores safely the token in case she needs to recover AdequateAlice01 in the future.
3. Alice makes a new order and locks a small hold invoice to publish it (maker bond).
4. Bob wants to sell satoshis, sees Alice's order in the book and takes it.
5. Bob scans a small hold invoice as his taker bond. The contract is final.
6. Bob posts the traded satoshis with a hold invoice. While Alice submits her payout invoice.
7. On a private chat, Bob tells Alice how to send him fiat.
8. Alice pays Bob, then they confirm the fiat has been sent and received.
9. Bob's trade hold invoice is charged and the satoshis are sent to Alice.
10. Bob and Alice's bonds return automatically, since they complied by the rules.
11. The bonds would be charged (lost) in case of unilateral cancellation or cheating (lost dispute).
*Use [Tor Browser](https://www.torproject.org/download/) and .onion for best anonymity.*
## Contribute to the Robotic Satoshis Open Source Project ## Contribute to the Robotic Satoshis Open Source Project
See [CONTRIBUTING.md](CONTRIBUTING.md) See [CONTRIBUTING.md](CONTRIBUTING.md)
## Original idea
The concept of a simple custody-minimized lightning exchange using hold invoices is heavily inspired by [P2PLNBOT](https://github.com/grunch/p2plnbot) by @grunch
## License ## License
RoboSats is released under the terms of the AGPL3.0 license. See [LICENSE](LICENSE) for more details. The Robotic Satoshis Open Source Project is released under the terms of the AGPL3.0 license. See [LICENSE](LICENSE) for more details.

View File

@ -2,7 +2,7 @@ from django.contrib import admin
from django_admin_relation_links import AdminChangeLinksMixin from django_admin_relation_links import AdminChangeLinksMixin
from django.contrib.auth.models import Group, User from django.contrib.auth.models import Group, User
from django.contrib.auth.admin import UserAdmin from django.contrib.auth.admin import UserAdmin
from .models import Order, LNPayment, Profile, MarketTick from .models import Order, LNPayment, Profile, MarketTick, Currency
admin.site.unregister(Group) admin.site.unregister(Group)
admin.site.unregister(User) admin.site.unregister(User)
@ -22,27 +22,34 @@ class EUserAdmin(UserAdmin):
def avatar_tag(self, obj): def avatar_tag(self, obj):
return obj.profile.avatar_tag() return obj.profile.avatar_tag()
@admin.register(Order) @admin.register(Order)
class OrderAdmin(AdminChangeLinksMixin, admin.ModelAdmin): class OrderAdmin(AdminChangeLinksMixin, admin.ModelAdmin):
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 = ('id','type','maker_link','taker_link','status','amount','currency_link','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') list_display_links = ('id','type')
change_links = ('maker','taker','buyer_invoice','maker_bond','taker_bond','trade_escrow') change_links = ('maker','taker','currency','buyer_invoice','maker_bond','taker_bond','trade_escrow')
list_filter = ('is_disputed','is_fiat_sent','type','currency','status') list_filter = ('is_disputed','is_fiat_sent','type','currency','status')
@admin.register(LNPayment) @admin.register(LNPayment)
class LNPaymentAdmin(AdminChangeLinksMixin, admin.ModelAdmin): class LNPaymentAdmin(AdminChangeLinksMixin, admin.ModelAdmin):
list_display = ('id','concept','status','num_satoshis','type','invoice','expires_at','sender_link','receiver_link') list_display = ('hash','concept','status','num_satoshis','type','expires_at','sender_link','receiver_link','order_made','order_taken','order_escrow','order_paid')
list_display_links = ('id','concept') list_display_links = ('hash','concept','order_made','order_taken','order_escrow','order_paid')
change_links = ('sender','receiver') change_links = ('sender','receiver')
list_filter = ('type','concept','status') list_filter = ('type','concept','status')
@admin.register(Profile) @admin.register(Profile)
class UserProfileAdmin(AdminChangeLinksMixin, admin.ModelAdmin): class UserProfileAdmin(AdminChangeLinksMixin, admin.ModelAdmin):
list_display = ('avatar_tag','id','user_link','total_ratings','avg_rating','num_disputes','lost_disputes') list_display = ('avatar_tag','id','user_link','total_contracts','total_ratings','avg_rating','num_disputes','lost_disputes')
list_display_links = ('avatar_tag','id') list_display_links = ('avatar_tag','id')
change_links =['user'] change_links =['user']
readonly_fields = ['avatar_tag'] readonly_fields = ['avatar_tag']
@admin.register(Currency)
class CurrencieAdmin(admin.ModelAdmin):
list_display = ('id','currency','exchange_rate','timestamp')
list_display_links = ('id','currency')
readonly_fields = ('currency','exchange_rate','timestamp')
@admin.register(MarketTick) @admin.register(MarketTick)
class MarketTickAdmin(admin.ModelAdmin): class MarketTickAdmin(admin.ModelAdmin):
list_display = ('timestamp','price','volume','premium','currency','fee') list_display = ('timestamp','price','volume','premium','currency','fee')

View File

@ -28,6 +28,10 @@ class LNNode():
invoicesstub = invoicesstub.InvoicesStub(channel) invoicesstub = invoicesstub.InvoicesStub(channel)
routerstub = routerstub.RouterStub(channel) routerstub = routerstub.RouterStub(channel)
lnrpc = lnrpc
invoicesrpc = invoicesrpc
routerrpc = routerrpc
payment_failure_context = { payment_failure_context = {
0: "Payment isn't failed (yet)", 0: "Payment isn't failed (yet)",
1: "There are more routes to try, but the payment timeout was exceeded.", 1: "There are more routes to try, but the payment timeout was exceeded.",

View File

@ -1,16 +1,17 @@
from datetime import time, timedelta from datetime import timedelta
from django.utils import timezone from django.utils import timezone
from .lightning.node import LNNode from api.lightning.node import LNNode
from .models import Order, LNPayment, MarketTick, User from api.models import Order, LNPayment, MarketTick, User, Currency
from decouple import config from decouple import config
from .utils import get_exchange_rate
from api.tasks import follow_send_payment
import math import math
import ast
FEE = float(config('FEE')) FEE = float(config('FEE'))
BOND_SIZE = float(config('BOND_SIZE')) BOND_SIZE = float(config('BOND_SIZE'))
MARKET_PRICE_API = config('MARKET_PRICE_API')
ESCROW_USERNAME = config('ESCROW_USERNAME') ESCROW_USERNAME = config('ESCROW_USERNAME')
PENALTY_TIMEOUT = int(config('PENALTY_TIMEOUT')) PENALTY_TIMEOUT = int(config('PENALTY_TIMEOUT'))
@ -30,17 +31,24 @@ FIAT_EXCHANGE_DURATION = int(config('FIAT_EXCHANGE_DURATION'))
class Logics(): class Logics():
def validate_already_maker_or_taker(user): def validate_already_maker_or_taker(user):
'''Checks if the user is already partipant of an order''' '''Validates if a use is already not part of an active order'''
queryset = Order.objects.filter(maker=user)
active_order_status = [Order.Status.WFB, Order.Status.PUB, Order.Status.TAK,
Order.Status.WF2, Order.Status.WFE, Order.Status.WFI,
Order.Status.CHA, Order.Status.FSE, Order.Status.DIS,
Order.Status.WFR]
'''Checks if the user is already partipant of an active order'''
queryset = Order.objects.filter(maker=user, status__in=active_order_status)
if queryset.exists(): if queryset.exists():
return False, {'bad_request':'You are already maker of an order'} return False, {'bad_request':'You are already maker of an active order'}
queryset = Order.objects.filter(taker=user)
queryset = Order.objects.filter(taker=user, status__in=active_order_status)
if queryset.exists(): if queryset.exists():
return False, {'bad_request':'You are already taker of an order'} return False, {'bad_request':'You are already taker of an active order'}
return True, None return True, None
def validate_order_size(order): def validate_order_size(order):
'''Checks if order is withing limits at t0''' '''Validates if order is withing limits in satoshis at t0'''
if order.t0_satoshis > MAX_TRADE: if order.t0_satoshis > 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'} 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: if order.t0_satoshis < MIN_TRADE:
@ -55,7 +63,7 @@ class Logics():
else: else:
order.taker = user order.taker = user
order.status = Order.Status.TAK order.status = Order.Status.TAK
order.expires_at = timezone.now() + timedelta(minutes=EXP_TAKER_BOND_INVOICE) order.expires_at = timezone.now() + timedelta(seconds=Order.t_to_expire[Order.Status.TAK])
order.save() order.save()
return True, None return True, None
@ -74,7 +82,7 @@ class Logics():
if order.is_explicit: if order.is_explicit:
satoshis_now = order.satoshis satoshis_now = order.satoshis
else: else:
exchange_rate = get_exchange_rate(Order.currency_dict[str(order.currency)]) exchange_rate = float(order.currency.exchange_rate)
premium_rate = exchange_rate * (1+float(order.premium)/100) premium_rate = exchange_rate * (1+float(order.premium)/100)
satoshis_now = (float(order.amount) / premium_rate) * 100*1000*1000 satoshis_now = (float(order.amount) / premium_rate) * 100*1000*1000
@ -82,46 +90,178 @@ class Logics():
def price_and_premium_now(order): def price_and_premium_now(order):
''' computes order premium live ''' ''' computes order premium live '''
exchange_rate = get_exchange_rate(Order.currency_dict[str(order.currency)]) exchange_rate = float(order.currency.exchange_rate)
if not order.is_explicit: if not order.is_explicit:
premium = order.premium premium = order.premium
price = exchange_rate * (1+float(premium)/100) price = exchange_rate * (1+float(premium)/100)
else: else:
exchange_rate = get_exchange_rate(Order.currency_dict[str(order.currency)])
order_rate = float(order.amount) / (float(order.satoshis) / 100000000) order_rate = float(order.amount) / (float(order.satoshis) / 100000000)
premium = order_rate / exchange_rate - 1 premium = order_rate / exchange_rate - 1
premium = int(premium*100) # 2 decimals left premium = int(premium*10000)/100 # 2 decimals left
price = order_rate price = order_rate
significant_digits = 6 significant_digits = 5
price = round(price, significant_digits - int(math.floor(math.log10(abs(price)))) - 1) price = round(price, significant_digits - int(math.floor(math.log10(abs(price)))) - 1)
return price, premium return price, premium
def order_expires(order): @classmethod
''' General case when time runs out. Only def order_expires(cls, order):
used when the maker does not lock a publishing bond''' ''' General cases when time runs out.'''
order.status = Order.Status.EXP
order.maker = None
order.taker = None
order.save()
def kick_taker(order): # Do not change order status if an order in any with
# any of these status is sent to expire here
do_nothing = [Order.Status.DEL, Order.Status.UCA,
Order.Status.EXP, Order.Status.FSE,
Order.Status.DIS, Order.Status.CCA,
Order.Status.PAY, Order.Status.SUC,
Order.Status.FAI, Order.Status.MLD,
Order.Status.TLD]
if order.status in do_nothing:
return False
elif order.status == Order.Status.WFB:
order.status = Order.Status.EXP
cls.cancel_bond(order.maker_bond)
order.save()
return True
elif order.status == Order.Status.PUB:
cls.return_bond(order.maker_bond)
order.status = Order.Status.EXP
order.save()
return True
elif order.status == Order.Status.TAK:
cls.cancel_bond(order.taker_bond)
cls.kick_taker(order)
return True
elif order.status == Order.Status.WF2:
'''Weird case where an order expires and both participants
did not proceed with the contract. Likely the site was
down or there was a bug. Still bonds must be charged
to avoid service DDOS. '''
cls.settle_bond(order.maker_bond)
cls.settle_bond(order.taker_bond)
cls.cancel_escrow(order)
order.status = Order.Status.EXP
order.save()
return True
elif order.status == Order.Status.WFE:
maker_is_seller = cls.is_seller(order, order.maker)
# If maker is seller, settle the bond and order goes to expired
if maker_is_seller:
cls.settle_bond(order.maker_bond)
cls.return_bond(order.taker_bond)
cls.cancel_escrow(order)
order.status = Order.Status.EXP
order.save()
return True
# If maker is buyer, settle the taker's bond order goes back to public
else:
cls.settle_bond(order.taker_bond)
cls.cancel_escrow(order)
order.taker = None
order.taker_bond = None
order.trade_escrow = None
cls.publish_order(order)
return True
elif order.status == Order.Status.WFI:
# The trade could happen without a buyer invoice. However, this user
# is likely AFK; will probably desert the contract as well.
maker_is_buyer = cls.is_buyer(order, order.maker)
# If maker is buyer, settle the bond and order goes to expired
if maker_is_buyer:
cls.settle_bond(order.maker_bond)
cls.return_bond(order.taker_bond)
cls.return_escrow(order)
order.status = Order.Status.EXP
order.save()
return True
# If maker is seller settle the taker's bond, order goes back to public
else:
cls.settle_bond(order.taker_bond)
cls.return_escrow(order)
order.taker = None
order.taker_bond = None
order.trade_escrow = None
cls.publish_order(order)
return True
elif order.status == Order.Status.CHA:
# Another weird case. The time to confirm 'fiat sent' expired. Yet no dispute
# was opened. Hint: a seller-scammer could persuade a buyer to not click "fiat
# sent", we assume this is a dispute case by default.
cls.open_dispute(order)
return True
@classmethod
def kick_taker(cls, order):
''' The taker did not lock the taker_bond. Now he has to go''' ''' The taker did not lock the taker_bond. Now he has to go'''
# Add a time out to the taker # Add a time out to the taker
if order.taker:
profile = order.taker.profile profile = order.taker.profile
profile.penalty_expiration = timezone.now() + timedelta(seconds=PENALTY_TIMEOUT) profile.penalty_expiration = timezone.now() + timedelta(seconds=PENALTY_TIMEOUT)
profile.save() profile.save()
# Delete the taker_bond payment request, and make order public again # Make order public again
if LNNode.cancel_return_hold_invoice(order.taker_bond.payment_hash):
order.status = Order.Status.PUB
order.taker = None order.taker = None
order.taker_bond = None order.taker_bond = None
order.expires_at = timezone.now() + timedelta(hours=PUBLIC_ORDER_DURATION) ## TO FIX. Restore the remaining order durantion, not all of it! cls.publish_order(order)
order.save()
return True return True
@classmethod
def open_dispute(cls, order, user=None):
# Always settle the escrow during a dispute (same as with 'Fiat Sent')
# Dispute winner will have to submit a new invoice.
if not order.trade_escrow.status == LNPayment.Status.SETLED:
cls.settle_escrow(order)
order.is_disputed = True
order.status = Order.Status.DIS
order.expires_at = timezone.now() + timedelta(seconds=Order.t_to_expire[Order.Status.DIS])
order.save()
# User could be None if a dispute is open automatically due to weird expiration.
if not user == None:
profile = user.profile
profile.num_disputes = profile.num_disputes + 1
profile.orders_disputes_started = list(profile.orders_disputes_started).append(str(order.id))
profile.save()
return True, None
def dispute_statement(order, user, statement):
''' Updates the dispute statements in DB'''
if not order.status == Order.Status.DIS:
return False, {'bad_request':'Only orders in dispute accept a dispute statements'}
if len(statement) > 5000:
return False, {'bad_statement':'The statement is longer than 5000 characters'}
if order.maker == user:
order.maker_statement = statement
else:
order.taker_statement = statement
# If both statements are in, move status to wait for dispute resolution
if order.maker_statement != None and order.taker_statement != None:
order.status = Order.Status.WFR
order.expires_at = timezone.now() + timedelta(seconds=Order.t_to_expire[Order.Status.WFR])
order.save()
return True, None
@classmethod @classmethod
def buyer_invoice_amount(cls, order, user): def buyer_invoice_amount(cls, order, user):
''' Computes buyer invoice amount. Uses order.last_satoshis, ''' Computes buyer invoice amount. Uses order.last_satoshis,
@ -168,14 +308,14 @@ class Logics():
# If the order status is 'Waiting for invoice'. Move forward to 'chat' # If the order status is 'Waiting for invoice'. Move forward to 'chat'
if order.status == Order.Status.WFI: if order.status == Order.Status.WFI:
order.status = Order.Status.CHA order.status = Order.Status.CHA
order.expires_at = timezone.now() + timedelta(hours=FIAT_EXCHANGE_DURATION) order.expires_at = timezone.now() + timedelta(seconds=Order.t_to_expire[Order.Status.CHA])
# If the order status is 'Waiting for both'. Move forward to 'waiting for escrow' # If the order status is 'Waiting for both'. Move forward to 'waiting for escrow'
if order.status == Order.Status.WF2: if order.status == Order.Status.WF2:
# If the escrow is lock move to Chat. # If the escrow is lock move to Chat.
if order.trade_escrow.status == LNPayment.Status.LOCKED: if order.trade_escrow.status == LNPayment.Status.LOCKED:
order.status = Order.Status.CHA order.status = Order.Status.CHA
order.expires_at = timezone.now() + timedelta(hours=FIAT_EXCHANGE_DURATION) order.expires_at = timezone.now() + timedelta(seconds=Order.t_to_expire[Order.Status.CHA])
else: else:
order.status = Order.Status.WFE order.status = Order.Status.WFE
@ -185,16 +325,18 @@ class Logics():
def add_profile_rating(profile, rating): def add_profile_rating(profile, rating):
''' adds a new rating to a user profile''' ''' adds a new rating to a user profile'''
# TODO Unsafe, does not update ratings, it adds more ratings everytime a new rating is clicked.
profile.total_ratings = profile.total_ratings + 1 profile.total_ratings = profile.total_ratings + 1
latest_ratings = profile.latest_ratings latest_ratings = profile.latest_ratings
if len(latest_ratings) <= 1: if latest_ratings == None:
profile.latest_ratings = [rating] profile.latest_ratings = [rating]
profile.avg_rating = rating profile.avg_rating = rating
else: else:
latest_ratings = list(latest_ratings).append(rating) latest_ratings = ast.literal_eval(latest_ratings)
latest_ratings.append(rating)
profile.latest_ratings = latest_ratings profile.latest_ratings = latest_ratings
profile.avg_rating = sum(latest_ratings) / len(latest_ratings) profile.avg_rating = sum(list(map(int, latest_ratings))) / len(latest_ratings) # Just an average, but it is a list of strings. Has to be converted to int.
profile.save() profile.save()
@ -217,7 +359,6 @@ class Logics():
'''The order never shows up on the book and order '''The order never shows up on the book and order
status becomes "cancelled". That's it.''' status becomes "cancelled". That's it.'''
if order.status == Order.Status.WFB and order.maker == user: if order.status == Order.Status.WFB and order.maker == user:
order.maker = None
order.status = Order.Status.UCA order.status = Order.Status.UCA
order.save() order.save()
return True, None return True, None
@ -227,8 +368,7 @@ class Logics():
on the LN node and order book. TODO Only charge a small part of the bond (requires maker submitting an invoice)''' on the LN node and order book. TODO Only charge a small part of the bond (requires maker submitting an invoice)'''
elif order.status == Order.Status.PUB and order.maker == user: elif order.status == Order.Status.PUB and order.maker == user:
#Settle the maker bond (Maker loses the bond for cancelling public order) #Settle the maker bond (Maker loses the bond for cancelling public order)
if cls.settle_maker_bond(order): if cls.settle_bond(order.maker_bond):
order.maker = None
order.status = Order.Status.UCA order.status = Order.Status.UCA
order.save() order.save()
return True, None return True, None
@ -238,13 +378,8 @@ class Logics():
LNPayment "order.taker_bond" is deleted() ''' LNPayment "order.taker_bond" is deleted() '''
elif order.status == Order.Status.TAK and order.taker == user: elif order.status == Order.Status.TAK and order.taker == user:
# adds a timeout penalty # adds a timeout penalty
user.profile.penalty_expiration = timezone.now() + timedelta(seconds=PENALTY_TIMEOUT) cls.cancel_bond(order.taker_bond)
user.profile.save() cls.kick_taker(order)
order.taker = None
order.status = Order.Status.PUB
order.save()
return True, None return True, None
# 4) When taker or maker cancel after bond (before escrow) # 4) When taker or maker cancel after bond (before escrow)
@ -256,23 +391,20 @@ class Logics():
'''The order into cancelled status if maker cancels.''' '''The order into cancelled status if maker cancels.'''
elif order.status > Order.Status.PUB and order.status < Order.Status.CHA and order.maker == user: elif order.status > Order.Status.PUB and order.status < Order.Status.CHA and order.maker == user:
#Settle the maker bond (Maker loses the bond for canceling an ongoing trade) #Settle the maker bond (Maker loses the bond for canceling an ongoing trade)
valid = cls.settle_maker_bond(order) valid = cls.settle_bond(order.maker_bond)
if valid: if valid:
order.maker = None
order.status = Order.Status.UCA order.status = Order.Status.UCA
order.save() order.save()
return True, None return True, None
# 4.b) When taker cancel after bond (before escrow) # 4.b) When taker cancel after bond (before escrow)
'''The order into cancelled status if maker cancels.''' '''The order into cancelled status if maker cancels.'''
elif order.status > Order.Status.TAK and order.status < Order.Status.CHA and order.taker == user: elif order.status in [Order.Status.WF2, Order.Status.WFE] and order.taker == user:
# Settle the maker bond (Maker loses the bond for canceling an ongoing trade) # Settle the maker bond (Maker loses the bond for canceling an ongoing trade)
valid = cls.settle_taker_bond(order) valid = cls.settle_bond(order.taker_bond)
if valid: if valid:
order.taker = None order.taker = None
order.status = Order.Status.PUB cls.publish_order(order)
# order.taker_bond = None # TODO fix this, it overrides the information about the settled taker bond. Might make admin tasks hard.
order.save()
return True, None return True, None
# 5) When trade collateral has been posted (after escrow) # 5) When trade collateral has been posted (after escrow)
@ -284,15 +416,20 @@ class Logics():
else: else:
return False, {'bad_request':'You cannot cancel this order'} return False, {'bad_request':'You cannot cancel this order'}
def publish_order(order):
order.status = Order.Status.PUB
order.expires_at = order.created_at + timedelta(seconds=Order.t_to_expire[Order.Status.PUB])
order.save()
return
@classmethod @classmethod
def is_maker_bond_locked(cls, order): def is_maker_bond_locked(cls, order):
if LNNode.validate_hold_invoice_locked(order.maker_bond.payment_hash): if order.maker_bond.status == LNPayment.Status.LOCKED:
return True
elif LNNode.validate_hold_invoice_locked(order.maker_bond.payment_hash):
order.maker_bond.status = LNPayment.Status.LOCKED order.maker_bond.status = LNPayment.Status.LOCKED
order.maker_bond.save() order.maker_bond.save()
order.status = Order.Status.PUB cls.publish_order(order)
# With the bond confirmation the order is extended 'public_order_duration' hours
order.expires_at = timezone.now() + timedelta(hours=PUBLIC_ORDER_DURATION)
order.save()
return True return True
return False return False
@ -338,22 +475,38 @@ class Logics():
return True, {'bond_invoice':hold_payment['invoice'], 'bond_satoshis':bond_satoshis} return True, {'bond_invoice':hold_payment['invoice'], 'bond_satoshis':bond_satoshis}
@classmethod @classmethod
def is_taker_bond_locked(cls, order): def finalize_contract(cls, order):
if LNNode.validate_hold_invoice_locked(order.taker_bond.payment_hash): ''' When the taker locks the taker_bond
the contract is final '''
# THE TRADE AMOUNT IS FINAL WITH THE CONFIRMATION OF THE TAKER BOND! # THE TRADE AMOUNT IS FINAL WITH THE CONFIRMATION OF THE TAKER BOND!
# (This is the last update to "last_satoshis", it becomes the escrow amount next!) # (This is the last update to "last_satoshis", it becomes the escrow amount next)
order.last_satoshis = cls.satoshis_now(order) order.last_satoshis = cls.satoshis_now(order)
order.taker_bond.status = LNPayment.Status.LOCKED order.taker_bond.status = LNPayment.Status.LOCKED
order.taker_bond.save() order.taker_bond.save()
# Both users profiles are added one more contract // Unsafe can add more than once.
order.maker.profile.total_contracts += 1
order.taker.profile.total_contracts += 1
order.maker.profile.save()
order.taker.profile.save()
# Log a market tick # Log a market tick
MarketTick.log_a_tick(order) MarketTick.log_a_tick(order)
# With the bond confirmation the order is extended 'public_order_duration' hours # With the bond confirmation the order is extended 'public_order_duration' hours
order.expires_at = timezone.now() + timedelta(minutes=INVOICE_AND_ESCROW_DURATION) order.expires_at = timezone.now() + timedelta(seconds=Order.t_to_expire[Order.Status.WF2])
order.status = Order.Status.WF2 order.status = Order.Status.WF2
order.save() order.save()
return True return True
@classmethod
def is_taker_bond_locked(cls, order):
if order.taker_bond.status == LNPayment.Status.LOCKED:
return True
elif LNNode.validate_hold_invoice_locked(order.taker_bond.payment_hash):
cls.finalize_contract(order)
return True
return False return False
@classmethod @classmethod
@ -361,12 +514,12 @@ class Logics():
# Do not gen and kick out the taker if order is older than expiry time # Do not gen and kick out the taker if order is older than expiry time
if order.expires_at < timezone.now(): if order.expires_at < timezone.now():
cls.cancel_bond(order.taker_bond)
cls.kick_taker(order) cls.kick_taker(order)
return False, {'bad_request':'Invoice expired. You did not confirm taking the order in time.'} return False, {'bad_request':'Invoice expired. You did not confirm taking the order in time.'}
# Do not gen if a taker invoice exist. Do not return if it is already locked. Return the old one if still waiting. # Do not gen if a taker invoice exist. Do not return if it is already locked. Return the old one if still waiting.
if order.taker_bond: if order.taker_bond:
# Check if status is INVGEN and still not expired
if cls.is_taker_bond_locked(order): if cls.is_taker_bond_locked(order):
return False, None return False, None
elif order.taker_bond.status == LNPayment.Status.INVGEN: elif order.taker_bond.status == LNPayment.Status.INVGEN:
@ -376,7 +529,8 @@ class Logics():
order.last_satoshis = cls.satoshis_now(order) order.last_satoshis = cls.satoshis_now(order)
bond_satoshis = int(order.last_satoshis * BOND_SIZE) bond_satoshis = int(order.last_satoshis * BOND_SIZE)
pos_text = 'Buying' if cls.is_buyer(order, user) else 'Selling' pos_text = 'Buying' if cls.is_buyer(order, user) else 'Selling'
description = f"RoboSats - Taking 'Order {order.id}' {pos_text} BTC for {order.amount} - This is a taker bond, it will freeze in your wallet temporarily and automatically return. It will be charged if you cheat or cancel." description = (f"RoboSats - Taking 'Order {order.id}' {pos_text} BTC for {str(float(order.amount)) + Currency.currency_dict[str(order.currency.currency)]}"
+ " - This is a taker bond, it will freeze in your wallet temporarily and automatically return. It will be charged if you cheat or cancel.")
# Gen hold Invoice # Gen hold Invoice
hold_payment = LNNode.gen_hold_invoice(bond_satoshis, description, BOND_EXPIRY*3600) hold_payment = LNNode.gen_hold_invoice(bond_satoshis, description, BOND_EXPIRY*3600)
@ -395,23 +549,29 @@ class Logics():
created_at = hold_payment['created_at'], created_at = hold_payment['created_at'],
expires_at = hold_payment['expires_at']) expires_at = hold_payment['expires_at'])
order.expires_at = timezone.now() + timedelta(seconds=Order.t_to_expire[Order.Status.TAK])
order.save() order.save()
return True, {'bond_invoice': hold_payment['invoice'], 'bond_satoshis': bond_satoshis} return True, {'bond_invoice': hold_payment['invoice'], 'bond_satoshis': bond_satoshis}
def trade_escrow_received(order):
@classmethod ''' Moves the order forward'''
def is_trade_escrow_locked(cls, order):
if LNNode.validate_hold_invoice_locked(order.trade_escrow.payment_hash):
order.trade_escrow.status = LNPayment.Status.LOCKED
order.trade_escrow.save()
# If status is 'Waiting for both' move to Waiting for invoice # If status is 'Waiting for both' move to Waiting for invoice
if order.status == Order.Status.WF2: if order.status == Order.Status.WF2:
order.status = Order.Status.WFI order.status = Order.Status.WFI
# If status is 'Waiting for invoice' move to Chat # If status is 'Waiting for invoice' move to Chat
elif order.status == Order.Status.WFE: elif order.status == Order.Status.WFE:
order.status = Order.Status.CHA order.status = Order.Status.CHA
order.expires_at = timezone.now() + timedelta(hours=FIAT_EXCHANGE_DURATION) order.expires_at = timezone.now() + timedelta(seconds=Order.t_to_expire[Order.Status.CHA])
order.save() order.save()
@classmethod
def is_trade_escrow_locked(cls, order):
if order.trade_escrow.status == LNPayment.Status.LOCKED:
return True
elif LNNode.validate_hold_invoice_locked(order.trade_escrow.payment_hash):
order.trade_escrow.status = LNPayment.Status.LOCKED
order.trade_escrow.save()
cls.trade_escrow_received(order)
return True return True
return False return False
@ -431,7 +591,7 @@ class Logics():
elif order.trade_escrow.status == LNPayment.Status.INVGEN: elif order.trade_escrow.status == LNPayment.Status.INVGEN:
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}
# If there was no taker_bond object yet, generates one # If there was no taker_bond object yet, generate one
escrow_satoshis = order.last_satoshis # Amount was fixed when taker bond was locked escrow_satoshis = order.last_satoshis # Amount was fixed when taker bond was locked
description = f"RoboSats - Escrow amount for '{str(order)}' - The escrow will be released to the buyer once you confirm you received the fiat. It will automatically return if buyer does not confirm the payment." description = f"RoboSats - Escrow amount for '{str(order)}' - The escrow will be released to the buyer once you confirm you received the fiat. It will automatically return if buyer does not confirm the payment."
@ -463,33 +623,62 @@ class Logics():
order.trade_escrow.save() order.trade_escrow.save()
return True return True
def settle_maker_bond(order): def settle_bond(bond):
''' Settles the maker bond hold invoice''' ''' Settles the bond hold invoice'''
# TODO ERROR HANDLING # TODO ERROR HANDLING
if LNNode.settle_hold_invoice(order.maker_bond.preimage): if LNNode.settle_hold_invoice(bond.preimage):
order.maker_bond.status = LNPayment.Status.SETLED bond.status = LNPayment.Status.SETLED
order.maker_bond.save() bond.save()
return True return True
def settle_taker_bond(order): def return_escrow(order):
''' Settles the taker bond hold invoice''' '''returns the trade escrow'''
# TODO ERROR HANDLING if LNNode.cancel_return_hold_invoice(order.trade_escrow.payment_hash):
if LNNode.settle_hold_invoice(order.taker_bond.preimage): order.trade_escrow.status = LNPayment.Status.RETNED
order.taker_bond.status = LNPayment.Status.SETLED return True
order.taker_bond.save()
def cancel_escrow(order):
'''returns the trade escrow'''
# Same as return escrow, but used when the invoice was never LOCKED
if LNNode.cancel_return_hold_invoice(order.trade_escrow.payment_hash):
order.trade_escrow.status = LNPayment.Status.CANCEL
return True return True
def return_bond(bond): def return_bond(bond):
'''returns a bond''' '''returns a bond'''
if LNNode.cancel_return_hold_invoice(bond.payment_hash): if bond == None:
return
try:
LNNode.cancel_return_hold_invoice(bond.payment_hash)
bond.status = LNPayment.Status.RETNED bond.status = LNPayment.Status.RETNED
return True return True
except Exception as e:
if 'invoice already settled' in str(e):
bond.status = LNPayment.Status.SETLED
return True
else:
raise e
def cancel_bond(bond):
'''cancel a bond'''
# Same as return bond, but used when the invoice was never LOCKED
if bond == None:
return True
try:
LNNode.cancel_return_hold_invoice(bond.payment_hash)
bond.status = LNPayment.Status.CANCEL
return True
except Exception as e:
if 'invoice already settled' in str(e):
bond.status = LNPayment.Status.SETLED
return True
else:
raise e
def pay_buyer_invoice(order): def pay_buyer_invoice(order):
''' Pay buyer invoice''' ''' Pay buyer invoice'''
# TODO ERROR HANDLING suceeded, context = follow_send_payment(order.buyer_invoice)
if LNNode.pay_invoice(order.buyer_invoice.invoice, order.buyer_invoice.num_satoshis): return suceeded, context
return True
@classmethod @classmethod
def confirm_fiat(cls, order, user): def confirm_fiat(cls, order, user):
@ -521,7 +710,7 @@ class Logics():
if is_payed: if is_payed:
order.status = Order.Status.SUC order.status = Order.Status.SUC
order.buyer_invoice.status = LNPayment.Status.SUCCED order.buyer_invoice.status = LNPayment.Status.SUCCED
order.expires_at = timezone.now() + timedelta(days=1) # One day to rate / see this order. order.expires_at = timezone.now() + timedelta(seconds=Order.t_to_expire[Order.Status.SUC])
order.save() order.save()
# RETURN THE BONDS # RETURN THE BONDS
@ -542,11 +731,15 @@ class Logics():
# If the trade is finished # If the trade is finished
if order.status > Order.Status.PAY: if order.status > Order.Status.PAY:
# if maker, rates taker # if maker, rates taker
if order.maker == user: if order.maker == user and order.maker_rated == False:
cls.add_profile_rating(order.taker.profile, rating) cls.add_profile_rating(order.taker.profile, rating)
order.maker_rated = True
order.save()
# if taker, rates maker # if taker, rates maker
if order.taker == user: if order.taker == user and order.taker_rated == False:
cls.add_profile_rating(order.maker.profile, rating) cls.add_profile_rating(order.maker.profile, rating)
order.taker_rated = True
order.save()
else: else:
return False, {'bad_request':'You cannot rate your counterparty yet.'} return False, {'bad_request':'You cannot rate your counterparty yet.'}

View File

@ -0,0 +1,42 @@
from django.core.management.base import BaseCommand, CommandError
import time
from api.models import Order
from api.logics import Logics
from django.utils import timezone
class Command(BaseCommand):
help = 'Follows all active hold invoices'
# def add_arguments(self, parser):
# parser.add_argument('debug', nargs='+', type=boolean)
def handle(self, *args, **options):
''' Continuously checks order expiration times for 1 hour. If order
has expires, it calls the logics module for expiration handling.'''
do_nothing = [Order.Status.DEL, Order.Status.UCA,
Order.Status.EXP, Order.Status.FSE,
Order.Status.DIS, Order.Status.CCA,
Order.Status.PAY, Order.Status.SUC,
Order.Status.FAI, Order.Status.MLD,
Order.Status.TLD]
while True:
time.sleep(5)
queryset = Order.objects.exclude(status__in=do_nothing)
queryset = queryset.filter(expires_at__lt=timezone.now()) # expires at lower than now
debug = {}
debug['num_expired_orders'] = len(queryset)
debug['expired_orders'] = []
for idx, order in enumerate(queryset):
context = str(order)+ " was "+ Order.Status(order.status).label
if Logics.order_expires(order): # Order send to expire here
debug['expired_orders'].append({idx:context})
if debug['num_expired_orders'] > 0:
self.stdout.write(str(timezone.now()))
self.stdout.write(str(debug))

View File

@ -0,0 +1,130 @@
from django.core.management.base import BaseCommand, CommandError
from api.lightning.node import LNNode
from api.models import LNPayment, Order
from api.logics import Logics
from django.utils import timezone
from datetime import timedelta
from decouple import config
from base64 import b64decode
import time
MACAROON = b64decode(config('LND_MACAROON_BASE64'))
class Command(BaseCommand):
'''
Background: SubscribeInvoices stub iterator would be great to use here.
However, it only sends updates when the invoice is OPEN (new) or SETTLED.
We are very interested on the other two states (CANCELLED and ACCEPTED).
Therefore, this thread (follow_invoices) will iterate over all LNpayment
objects and do InvoiceLookupV2 every X seconds to update their state 'live'
'''
help = 'Follows all active hold invoices'
rest = 5 # seconds between consecutive checks for invoice updates
# def add_arguments(self, parser):
# parser.add_argument('debug', nargs='+', type=boolean)
def handle(self, *args, **options):
''' Follows and updates LNpayment objects
until settled or canceled'''
lnd_state_to_lnpayment_status = {
0: LNPayment.Status.INVGEN, # OPEN
1: LNPayment.Status.SETLED, # SETTLED
2: LNPayment.Status.CANCEL, # CANCELLED
3: LNPayment.Status.LOCKED # ACCEPTED
}
stub = LNNode.invoicesstub
while True:
time.sleep(self.rest)
# time it for debugging
t0 = time.time()
queryset = LNPayment.objects.filter(type=LNPayment.Types.HOLD, status__in=[LNPayment.Status.INVGEN, LNPayment.Status.LOCKED])
debug = {}
debug['num_active_invoices'] = len(queryset)
debug['invoices'] = []
at_least_one_changed = False
for idx, hold_lnpayment in enumerate(queryset):
old_status = LNPayment.Status(hold_lnpayment.status).label
try:
request = LNNode.invoicesrpc.LookupInvoiceMsg(payment_hash=bytes.fromhex(hold_lnpayment.payment_hash))
response = stub.LookupInvoiceV2(request, metadata=[('macaroon', MACAROON.hex())])
hold_lnpayment.status = lnd_state_to_lnpayment_status[response.state]
except Exception as e:
# If it fails at finding the invoice it has been canceled.
# On RoboSats DB we make a distinction between cancelled and returned (LND does not)
if 'unable to locate invoice' in str(e):
hold_lnpayment.status = LNPayment.Status.CANCEL
# LND restarted.
if 'wallet locked, unlock it' in str(e):
self.stdout.write(str(timezone.now())+':: Wallet Locked')
# Other write to logs
else:
self.stdout.write(str(e))
new_status = LNPayment.Status(hold_lnpayment.status).label
# Only save the hold_payments that change (otherwise this function does not scale)
changed = not old_status==new_status
if changed:
# self.handle_status_change(hold_lnpayment, old_status)
hold_lnpayment.save()
self.update_order_status(hold_lnpayment)
# Report for debugging
new_status = LNPayment.Status(hold_lnpayment.status).label
debug['invoices'].append({idx:{
'payment_hash': str(hold_lnpayment.payment_hash),
'old_status': old_status,
'new_status': new_status,
}})
at_least_one_changed = at_least_one_changed or changed
debug['time']=time.time()-t0
if at_least_one_changed:
self.stdout.write(str(timezone.now()))
self.stdout.write(str(debug))
def update_order_status(self, lnpayment):
''' Background process following LND hold invoices
can catch LNpayments changing status. If they do,
the order status might have to change too.'''
# If the LNPayment goes to LOCKED (ACCEPTED)
if lnpayment.status == LNPayment.Status.LOCKED:
try:
# It is a maker bond => Publish order.
if hasattr(lnpayment, 'order_made' ):
Logics.publish_order(lnpayment.order_made)
return
# It is a taker bond => close contract.
elif hasattr(lnpayment, 'order_taken' ):
if lnpayment.order_taken.status == Order.Status.TAK:
Logics.finalize_contract(lnpayment.order_taken)
return
# It is a trade escrow => move foward order status.
elif hasattr(lnpayment, 'order_escrow' ):
Logics.trade_escrow_received(lnpayment.order_escrow)
return
except Exception as e:
self.stdout.write(str(e))
# TODO If a lnpayment goes from LOCKED to INVGED. Totally weird
# halt the order
if lnpayment.status == LNPayment.Status.LOCKED:
pass

View File

@ -2,13 +2,13 @@ from django.db import models
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.core.validators import MaxValueValidator, MinValueValidator, validate_comma_separated_integer_list from django.core.validators import MaxValueValidator, MinValueValidator, validate_comma_separated_integer_list
from django.db.models.signals import post_save, pre_delete from django.db.models.signals import post_save, pre_delete
from django.template.defaultfilters import truncatechars
from django.dispatch import receiver from django.dispatch import receiver
from django.utils.html import mark_safe from django.utils.html import mark_safe
import uuid import uuid
from decouple import config from decouple import config
from pathlib import Path from pathlib import Path
from .utils import get_exchange_rate
import json import json
MIN_TRADE = int(config('MIN_TRADE')) MIN_TRADE = int(config('MIN_TRADE'))
@ -16,6 +16,24 @@ MAX_TRADE = int(config('MAX_TRADE'))
FEE = float(config('FEE')) FEE = float(config('FEE'))
BOND_SIZE = float(config('BOND_SIZE')) BOND_SIZE = float(config('BOND_SIZE'))
class Currency(models.Model):
currency_dict = json.load(open('./frontend/static/assets/currencies.json'))
currency_choices = [(int(val), label) for val, label in list(currency_dict.items())]
currency = models.PositiveSmallIntegerField(choices=currency_choices, null=False, unique=True)
exchange_rate = models.DecimalField(max_digits=10, decimal_places=2, default=None, null=True, validators=[MinValueValidator(0)])
timestamp = models.DateTimeField(auto_now_add=True)
def __str__(self):
# returns currency label ( 3 letters code)
return self.currency_dict[str(self.currency)]
class Meta:
verbose_name = 'Cached market currency'
verbose_name_plural = 'Currencies'
class LNPayment(models.Model): class LNPayment(models.Model):
class Types(models.IntegerChoices): class Types(models.IntegerChoices):
@ -33,23 +51,23 @@ class LNPayment(models.Model):
LOCKED = 1, 'Locked' LOCKED = 1, 'Locked'
SETLED = 2, 'Settled' SETLED = 2, 'Settled'
RETNED = 3, 'Returned' RETNED = 3, 'Returned'
EXPIRE = 4, 'Expired' CANCEL = 4, 'Cancelled'
VALIDI = 5, 'Valid' EXPIRE = 5, 'Expired'
FLIGHT = 6, 'In flight' VALIDI = 6, 'Valid'
SUCCED = 7, 'Succeeded' FLIGHT = 7, 'In flight'
FAILRO = 8, 'Routing failed' SUCCED = 8, 'Succeeded'
FAILRO = 9, 'Routing failed'
# payment use details # payment use details
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
type = models.PositiveSmallIntegerField(choices=Types.choices, null=False, default=Types.HOLD) type = models.PositiveSmallIntegerField(choices=Types.choices, null=False, default=Types.HOLD)
concept = models.PositiveSmallIntegerField(choices=Concepts.choices, null=False, default=Concepts.MAKEBOND) concept = models.PositiveSmallIntegerField(choices=Concepts.choices, null=False, default=Concepts.MAKEBOND)
status = models.PositiveSmallIntegerField(choices=Status.choices, null=False, default=Status.INVGEN) status = models.PositiveSmallIntegerField(choices=Status.choices, null=False, default=Status.INVGEN)
routing_retries = models.PositiveSmallIntegerField(null=False, default=0) routing_retries = models.PositiveSmallIntegerField(null=False, default=0)
# payment info # payment info
payment_hash = models.CharField(max_length=100, unique=True, default=None, blank=True, primary_key=True)
invoice = models.CharField(max_length=1200, unique=True, null=True, default=None, blank=True) # Some invoices with lots of routing hints might be long invoice = models.CharField(max_length=1200, unique=True, null=True, default=None, blank=True) # Some invoices with lots of routing hints might be long
payment_hash = models.CharField(max_length=100, unique=True, null=True, default=None, blank=True)
preimage = models.CharField(max_length=64, unique=True, null=True, default=None, blank=True) preimage = models.CharField(max_length=64, unique=True, null=True, default=None, blank=True)
description = models.CharField(max_length=500, unique=False, null=True, default=None, blank=True) description = models.CharField(max_length=500, unique=False, null=True, default=None, blank=True)
num_satoshis = models.PositiveBigIntegerField(validators=[MinValueValidator(MIN_TRADE*BOND_SIZE), MaxValueValidator(MAX_TRADE*(1+BOND_SIZE+FEE))]) num_satoshis = models.PositiveBigIntegerField(validators=[MinValueValidator(MIN_TRADE*BOND_SIZE), MaxValueValidator(MAX_TRADE*(1+BOND_SIZE+FEE))])
@ -61,7 +79,18 @@ class LNPayment(models.Model):
receiver = models.ForeignKey(User, related_name='receiver', on_delete=models.CASCADE, null=True, default=None) receiver = models.ForeignKey(User, related_name='receiver', on_delete=models.CASCADE, null=True, default=None)
def __str__(self): def __str__(self):
return (f'LN-{str(self.id)[:8]}: {self.Concepts(self.concept).label} - {self.Status(self.status).label}') return (f'LN-{str(self.payment_hash)[:8]}: {self.Concepts(self.concept).label} - {self.Status(self.status).label}')
class Meta:
verbose_name = 'Lightning payment'
verbose_name_plural = 'Lightning payments'
@property
def hash(self):
# Payment hash is the primary key of LNpayments
# However it is too long for the admin panel.
# We created a truncated property for display 'hash'
return truncatechars(self.payment_hash, 10)
class Order(models.Model): class Order(models.Model):
@ -86,11 +115,9 @@ class Order(models.Model):
PAY = 13, 'Sending satoshis to buyer' PAY = 13, 'Sending satoshis to buyer'
SUC = 14, 'Sucessful trade' SUC = 14, 'Sucessful trade'
FAI = 15, 'Failed lightning network routing' FAI = 15, 'Failed lightning network routing'
MLD = 16, 'Maker lost dispute' WFR = 16, 'Wait for dispute resolution'
TLD = 17, 'Taker lost dispute' MLD = 17, 'Maker lost dispute'
TLD = 18, 'Taker lost dispute'
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 # order info
status = models.PositiveSmallIntegerField(choices=Status.choices, null=False, default=Status.WFB) status = models.PositiveSmallIntegerField(choices=Status.choices, null=False, default=Status.WFB)
@ -99,7 +126,7 @@ class Order(models.Model):
# order details # order details
type = models.PositiveSmallIntegerField(choices=Types.choices, null=False) type = models.PositiveSmallIntegerField(choices=Types.choices, null=False)
currency = models.PositiveSmallIntegerField(choices=currency_choices, null=False) currency = models.ForeignKey(Currency, null=True, on_delete=models.SET_NULL)
amount = models.DecimalField(max_digits=9, decimal_places=4, validators=[MinValueValidator(0.00001)]) amount = models.DecimalField(max_digits=9, decimal_places=4, validators=[MinValueValidator(0.00001)])
payment_method = models.CharField(max_length=35, null=False, default="not specified", blank=True) payment_method = models.CharField(max_length=35, null=False, default="not specified", blank=True)
@ -117,33 +144,59 @@ 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 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 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_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) is_fiat_sent = models.BooleanField(default=False, null=False)
# HTLCs # in dispute
is_disputed = models.BooleanField(default=False, null=False)
maker_statement = models.TextField(max_length=5000, null=True, default=None, blank=True)
taker_statement = models.TextField(max_length=5000, null=True, default=None, blank=True)
# LNpayments
# Order collateral # Order collateral
maker_bond = models.ForeignKey(LNPayment, related_name='maker_bond', on_delete=models.SET_NULL, null=True, default=None, blank=True) maker_bond = models.OneToOneField(LNPayment, related_name='order_made', on_delete=models.SET_NULL, null=True, default=None, blank=True)
taker_bond = models.ForeignKey(LNPayment, related_name='taker_bond', on_delete=models.SET_NULL, null=True, default=None, blank=True) taker_bond = models.OneToOneField(LNPayment, related_name='order_taken', on_delete=models.SET_NULL, null=True, default=None, blank=True)
trade_escrow = models.ForeignKey(LNPayment, related_name='trade_escrow', on_delete=models.SET_NULL, null=True, default=None, blank=True) trade_escrow = models.OneToOneField(LNPayment, related_name='order_escrow', on_delete=models.SET_NULL, null=True, default=None, blank=True)
# buyer payment LN invoice # buyer payment LN invoice
buyer_invoice = models.ForeignKey(LNPayment, related_name='buyer_invoice', on_delete=models.SET_NULL, null=True, default=None, blank=True) buyer_invoice = models.OneToOneField(LNPayment, related_name='order_paid', 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. # ratings
maker_cancel = models.ForeignKey(LNPayment, related_name='maker_cancel', on_delete=models.SET_NULL, null=True, default=None, blank=True) maker_rated = models.BooleanField(default=False, null=False)
taker_cancel = models.ForeignKey(LNPayment, related_name='taker_cancel', on_delete=models.SET_NULL, null=True, default=None, blank=True) taker_rated = models.BooleanField(default=False, null=False)
t_to_expire = {
0 : int(config('EXP_MAKER_BOND_INVOICE')) , # 'Waiting for maker bond'
1 : 60*60*int(config('PUBLIC_ORDER_DURATION')), # 'Public'
2 : 0, # 'Deleted'
3 : int(config('EXP_TAKER_BOND_INVOICE')), # 'Waiting for taker bond'
4 : 0, # 'Cancelled'
5 : 0, # 'Expired'
6 : 60*int(config('INVOICE_AND_ESCROW_DURATION')), # 'Waiting for trade collateral and buyer invoice'
7 : 60*int(config('INVOICE_AND_ESCROW_DURATION')), # 'Waiting only for seller trade collateral'
8 : 60*int(config('INVOICE_AND_ESCROW_DURATION')), # 'Waiting only for buyer invoice'
9 : 60*60*int(config('FIAT_EXCHANGE_DURATION')), # 'Sending fiat - In chatroom'
10 : 60*60*int(config('FIAT_EXCHANGE_DURATION')), # 'Fiat sent - In chatroom'
11 : 10*24*60*60, # 'In dispute'
12 : 0, # 'Collaboratively cancelled'
13 : 24*60*60, # 'Sending satoshis to buyer'
14 : 24*60*60, # 'Sucessful trade'
15 : 24*60*60, # 'Failed lightning network routing'
16 : 24*60*60, # 'Wait for dispute resolution'
17 : 24*60*60, # 'Maker lost dispute'
18 : 24*60*60, # 'Taker lost dispute'
}
def __str__(self): def __str__(self):
# Make relational back to ORDER return (f'Order {self.id}: {self.Types(self.type).label} BTC for {float(self.amount)} {self.currency}')
return (f'Order {self.id}: {self.Types(self.type).label} BTC for {float(self.amount)} {self.currency_dict[str(self.currency)]}')
@receiver(pre_delete, sender=Order) @receiver(pre_delete, sender=Order)
def delete_HTLCs_at_order_deletion(sender, instance, **kwargs): def delete_lnpayment_at_order_deletion(sender, instance, **kwargs):
to_delete = (instance.maker_bond, instance.buyer_invoice, instance.taker_bond, instance.trade_escrow) to_delete = (instance.maker_bond, instance.buyer_invoice, instance.taker_bond, instance.trade_escrow)
for htlc in to_delete: for lnpayment in to_delete:
try: try:
htlc.delete() lnpayment.delete()
except: except:
pass pass
@ -151,6 +204,9 @@ class Profile(models.Model):
user = models.OneToOneField(User,on_delete=models.CASCADE) user = models.OneToOneField(User,on_delete=models.CASCADE)
# Total trades
total_contracts = models.PositiveIntegerField(null=False, default=0)
# Ratings stored as a comma separated integer list # Ratings stored as a comma separated integer list
total_ratings = models.PositiveIntegerField(null=False, default=0) total_ratings = models.PositiveIntegerField(null=False, default=0)
latest_ratings = models.CharField(max_length=999, null=True, default=None, validators=[validate_comma_separated_integer_list], blank=True) # Will only store latest ratings latest_ratings = models.CharField(max_length=999, null=True, default=None, validators=[validate_comma_separated_integer_list], blank=True) # Will only store latest ratings
@ -159,6 +215,8 @@ class Profile(models.Model):
# Disputes # Disputes
num_disputes = models.PositiveIntegerField(null=False, default=0) num_disputes = models.PositiveIntegerField(null=False, default=0)
lost_disputes = models.PositiveIntegerField(null=False, default=0) lost_disputes = models.PositiveIntegerField(null=False, default=0)
num_disputes_started = models.PositiveIntegerField(null=False, default=0)
orders_disputes_started = models.CharField(max_length=999, null=True, default=None, validators=[validate_comma_separated_integer_list], blank=True) # Will only store ID of orders
# RoboHash # RoboHash
avatar = models.ImageField(default="static/assets/misc/unknown_avatar.png", verbose_name='Avatar', blank=True) avatar = models.ImageField(default="static/assets/misc/unknown_avatar.png", verbose_name='Avatar', blank=True)
@ -177,8 +235,11 @@ class Profile(models.Model):
@receiver(pre_delete, sender=User) @receiver(pre_delete, sender=User)
def del_avatar_from_disk(sender, instance, **kwargs): def del_avatar_from_disk(sender, instance, **kwargs):
try:
avatar_file=Path('frontend/' + instance.profile.avatar.url) avatar_file=Path('frontend/' + instance.profile.avatar.url)
avatar_file.unlink() # FIX deleting user fails if avatar is not found avatar_file.unlink()
except:
pass
def __str__(self): def __str__(self):
return self.user.username return self.user.username
@ -209,7 +270,7 @@ class MarketTick(models.Model):
price = models.DecimalField(max_digits=10, decimal_places=2, default=None, null=True, validators=[MinValueValidator(0)]) 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)]) 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) 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) currency = models.ForeignKey(Currency, null=True, on_delete=models.SET_NULL)
timestamp = models.DateTimeField(auto_now_add=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 # Relevant to keep record of the historical fee, so the insight on the premium can be better analyzed
@ -226,7 +287,8 @@ class MarketTick(models.Model):
elif order.taker_bond.status == LNPayment.Status.LOCKED: elif order.taker_bond.status == LNPayment.Status.LOCKED:
volume = order.last_satoshis / 100000000 volume = order.last_satoshis / 100000000
price = float(order.amount) / volume # Amount Fiat / Amount BTC price = float(order.amount) / volume # Amount Fiat / Amount BTC
premium = 100 * (price / get_exchange_rate(Order.currency_dict[str(order.currency)]) - 1) market_exchange_rate = float(order.currency.exchange_rate)
premium = 100 * (price / market_exchange_rate - 1)
tick = MarketTick.objects.create( tick = MarketTick.objects.create(
price=price, price=price,
@ -239,4 +301,7 @@ class MarketTick(models.Model):
def __str__(self): def __str__(self):
return f'Tick: {str(self.id)[:8]}' return f'Tick: {str(self.id)[:8]}'
class Meta:
verbose_name = 'Market tick'
verbose_name_plural = 'Market ticks'

View File

@ -3633,7 +3633,7 @@ nouns = [
"Fever", "Fever",
"Few", "Few",
"Fiance", "Fiance",
"Fiancé", "Fiance",
"Fiasco", "Fiasco",
"Fiat", "Fiat",
"Fiber", "Fiber",

View File

@ -41,6 +41,7 @@ class NickGenerator:
else: else:
raise ValueError("Language not implemented.") raise ValueError("Language not implemented.")
if verbose:
print( print(
f"{lang} SHA256 Nick Generator initialized with:" f"{lang} SHA256 Nick Generator initialized with:"
+ f"\nUp to {len(adverbs)} adverbs." + f"\nUp to {len(adverbs)} adverbs."

View File

@ -13,5 +13,6 @@ class MakeOrderSerializer(serializers.ModelSerializer):
class UpdateOrderSerializer(serializers.Serializer): class UpdateOrderSerializer(serializers.Serializer):
invoice = serializers.CharField(max_length=2000, allow_null=True, allow_blank=True, default=None) invoice = serializers.CharField(max_length=2000, allow_null=True, allow_blank=True, default=None)
action = serializers.ChoiceField(choices=('take','update_invoice','dispute','cancel','confirm','rate'), allow_null=False) statement = serializers.CharField(max_length=10000, allow_null=True, allow_blank=True, default=None)
action = serializers.ChoiceField(choices=('take','update_invoice','submit_statement','dispute','cancel','confirm','rate'), allow_null=False)
rating = serializers.ChoiceField(choices=('1','2','3','4','5'), allow_null=True, allow_blank=True, default=None) rating = serializers.ChoiceField(choices=('1','2','3','4','5'), allow_null=True, allow_blank=True, default=None)

109
api/tasks.py Normal file
View File

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

View File

@ -1,21 +1,56 @@
import requests, ring, os import requests, ring, os
from decouple import config from decouple import config
import numpy as np
from api.models import Order
market_cache = {} market_cache = {}
@ring.dict(market_cache, expire=30) #keeps in cache for 30 seconds @ring.dict(market_cache, expire=3) #keeps in cache for 3 seconds
def get_exchange_rate(currency): def get_exchange_rates(currencies):
# TODO Add fallback Public APIs and error handling '''
# Think about polling price data in a different way (e.g. store locally every t seconds) Params: list of currency codes.
Checks for exchange rates in several public APIs.
Returns the median price list.
'''
market_prices = requests.get(config('MARKET_PRICE_API')).json() APIS = config('MARKET_PRICE_APIS', cast=lambda v: [s.strip() for s in v.split(',')])
exchange_rate = float(market_prices[currency]['last'])
return exchange_rate api_rates = []
for api_url in APIS:
try: # If one API is unavailable pass
if 'blockchain.info' in api_url:
blockchain_prices = requests.get(api_url).json()
blockchain_rates = []
for currency in currencies:
try: # If a currency is missing place a None
blockchain_rates.append(float(blockchain_prices[currency]['last']))
except:
blockchain_rates.append(np.nan)
api_rates.append(blockchain_rates)
elif 'yadio.io' in api_url:
yadio_prices = requests.get(api_url).json()
yadio_rates = []
for currency in currencies:
try:
yadio_rates.append(float(yadio_prices['BTC'][currency]))
except:
yadio_rates.append(np.nan)
api_rates.append(yadio_rates)
except:
pass
if len(api_rates) == 0:
return None # Wops there is not API available!
exchange_rates = np.array(api_rates)
median_rates = np.nanmedian(exchange_rates, axis=0)
return median_rates.tolist()
lnd_v_cache = {} lnd_v_cache = {}
@ring.dict(lnd_v_cache, expire=3600) #keeps in cache for 3600 seconds @ring.dict(lnd_v_cache, expire=3600) #keeps in cache for 3600 seconds
def get_lnd_version(): def get_lnd_version():
@ -25,7 +60,6 @@ def get_lnd_version():
return lnd_version return lnd_version
robosats_commit_cache = {} robosats_commit_cache = {}
@ring.dict(robosats_commit_cache, expire=3600) @ring.dict(robosats_commit_cache, expire=3600)
def get_commit_robosats(): def get_commit_robosats():
@ -34,3 +68,21 @@ def get_commit_robosats():
return lnd_version return lnd_version
premium_percentile = {}
@ring.dict(premium_percentile, expire=300)
def compute_premium_percentile(order):
queryset = Order.objects.filter(currency=order.currency, status=Order.Status.PUB)
print(len(queryset))
if len(queryset) <= 1:
return 0.5
order_rate = float(order.last_satoshis) / float(order.amount)
rates = []
for similar_order in queryset:
rates.append(float(similar_order.last_satoshis) / float(similar_order.amount))
rates = np.array(rates)
return round(np.sum(rates < order_rate) / len(rates),2)

View File

@ -9,9 +9,9 @@ from django.contrib.auth import authenticate, login, logout
from django.contrib.auth.models import User from django.contrib.auth.models import User
from .serializers import ListOrderSerializer, MakeOrderSerializer, UpdateOrderSerializer from .serializers import ListOrderSerializer, MakeOrderSerializer, UpdateOrderSerializer
from .models import LNPayment, MarketTick, Order from .models import LNPayment, MarketTick, Order, Currency
from .logics import Logics from .logics import Logics
from .utils import get_lnd_version, get_commit_robosats from .utils import get_lnd_version, get_commit_robosats, compute_premium_percentile
from .nick_generator.nick_generator import NickGenerator from .nick_generator.nick_generator import NickGenerator
from robohash import Robohash from robohash import Robohash
@ -54,7 +54,7 @@ class MakerView(CreateAPIView):
# Creates a new order # Creates a new order
order = Order( order = Order(
type=type, type=type,
currency=currency, currency=Currency.objects.get(id=currency),
amount=amount, amount=amount,
payment_method=payment_method, payment_method=payment_method,
premium=premium, premium=premium,
@ -95,10 +95,6 @@ class OrderView(viewsets.ViewSet):
# This is our order. # This is our order.
order = order[0] order = order[0]
# 1) If order has expired
if order.status == Order.Status.EXP:
return Response({'bad_request':'This order has expired'},status.HTTP_400_BAD_REQUEST)
# 2) If order has been cancelled # 2) If order has been cancelled
if order.status == Order.Status.UCA: if order.status == Order.Status.UCA:
return Response({'bad_request':'This order has been cancelled by the maker'},status.HTTP_400_BAD_REQUEST) return Response({'bad_request':'This order has been cancelled by the maker'},status.HTTP_400_BAD_REQUEST)
@ -106,6 +102,7 @@ class OrderView(viewsets.ViewSet):
return Response({'bad_request':'This order has been cancelled collaborativelly'},status.HTTP_400_BAD_REQUEST) return Response({'bad_request':'This order has been cancelled collaborativelly'},status.HTTP_400_BAD_REQUEST)
data = ListOrderSerializer(order).data data = ListOrderSerializer(order).data
data['total_secs_exp'] = Order.t_to_expire[order.status]
# if user is under a limit (penalty), inform him. # if user is under a limit (penalty), inform him.
is_penalized, time_out = Logics.is_penalized(request.user) is_penalized, time_out = Logics.is_penalized(request.user)
@ -116,17 +113,26 @@ class OrderView(viewsets.ViewSet):
data['is_maker'] = order.maker == request.user data['is_maker'] = order.maker == request.user
data['is_taker'] = order.taker == request.user data['is_taker'] = order.taker == request.user
data['is_participant'] = data['is_maker'] or data['is_taker'] data['is_participant'] = data['is_maker'] or data['is_taker']
data['ur_nick'] = request.user.username
# 3) If not a participant and order is not public, forbid. # 3.a) If not a participant and order is not public, forbid.
if not data['is_participant'] and order.status != Order.Status.PUB: if not data['is_participant'] and order.status != Order.Status.PUB:
return Response({'bad_request':'You are not allowed to see this order'},status.HTTP_403_FORBIDDEN) return Response({'bad_request':'You are not allowed to see this order'},status.HTTP_403_FORBIDDEN)
# 3.b If order is between public and WF2
if order.status >= Order.Status.PUB and order.status < Order.Status.WF2:
data['price_now'], data['premium_now'] = Logics.price_and_premium_now(order)
# 3. c) If maker and Public, add num robots in book, premium percentile and num similar orders.
if data['is_maker'] and order.status == Order.Status.PUB:
data['robots_in_book'] = None # TODO
data['premium_percentile'] = compute_premium_percentile(order)
data['num_similar_orders'] = len(Order.objects.filter(currency=order.currency, status=Order.Status.PUB))
# 4) Non participants can view details (but only if PUB) # 4) Non participants can view details (but only if PUB)
elif not data['is_participant'] and order.status != Order.Status.PUB: elif not data['is_participant'] and order.status != Order.Status.PUB:
return Response(data, status=status.HTTP_200_OK) return Response(data, status=status.HTTP_200_OK)
# For participants add positions, nicks and status as a message # For participants add positions, nicks and status as a message and hold invoices status
data['is_buyer'] = Logics.is_buyer(order,request.user) data['is_buyer'] = Logics.is_buyer(order,request.user)
data['is_seller'] = Logics.is_seller(order,request.user) data['is_seller'] = Logics.is_seller(order,request.user)
data['maker_nick'] = str(order.maker) data['maker_nick'] = str(order.maker)
@ -134,6 +140,26 @@ class OrderView(viewsets.ViewSet):
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_fiat_sent'] = order.is_fiat_sent
data['is_disputed'] = order.is_disputed data['is_disputed'] = order.is_disputed
data['ur_nick'] = request.user.username
# Add whether hold invoices are LOCKED (ACCEPTED)
# Is there a maker bond? If so, True if locked, False otherwise
if order.maker_bond:
data['maker_locked'] = order.maker_bond.status == LNPayment.Status.LOCKED
else:
data['maker_locked'] = False
# Is there a taker bond? If so, True if locked, False otherwise
if order.taker_bond:
data['taker_locked'] = order.taker_bond.status == LNPayment.Status.LOCKED
else:
data['taker_locked'] = False
# Is there an escrow? If so, True if locked, False otherwise
if order.trade_escrow:
data['escrow_locked'] = order.trade_escrow.status == LNPayment.Status.LOCKED
else:
data['escrow_locked'] = False
# If both bonds are locked, participants can see the final trade amount in sats. # If both bonds are locked, participants can see the final trade amount in sats.
if order.taker_bond: if order.taker_bond:
@ -196,7 +222,7 @@ class OrderView(viewsets.ViewSet):
def take_update_confirm_dispute_cancel(self, request, format=None): def take_update_confirm_dispute_cancel(self, request, format=None):
''' '''
Here takes place all of updatesto the order object. Here takes place all of the updates to the order object.
That is: take, confim, cancel, dispute, update_invoice or rate. That is: take, confim, cancel, dispute, update_invoice or rate.
''' '''
order_id = request.GET.get(self.lookup_url_kwarg) order_id = request.GET.get(self.lookup_url_kwarg)
@ -206,20 +232,24 @@ class OrderView(viewsets.ViewSet):
order = Order.objects.get(id=order_id) order = Order.objects.get(id=order_id)
# action is either 1)'take', 2)'confirm', 3)'cancel', 4)'dispute' , 5)'update_invoice' 6)'rate' (counterparty) # action is either 1)'take', 2)'confirm', 3)'cancel', 4)'dispute' , 5)'update_invoice'
# 6)'submit_statement' (in dispute), 7)'rate' (counterparty)
action = serializer.data.get('action') action = serializer.data.get('action')
invoice = serializer.data.get('invoice') invoice = serializer.data.get('invoice')
statement = serializer.data.get('statement')
rating = serializer.data.get('rating') rating = serializer.data.get('rating')
# 1) If action is take, it is a taker request! # 1) If action is take, it is a taker request!
if action == 'take': if action == 'take':
if order.status == Order.Status.PUB: if order.status == Order.Status.PUB:
valid, context = Logics.validate_already_maker_or_taker(request.user) valid, context = Logics.validate_already_maker_or_taker(request.user)
if not valid: return Response(context, status=status.HTTP_409_CONFLICT) if not valid: return Response(context, status=status.HTTP_409_CONFLICT)
valid, context = Logics.take(order, request.user) valid, context = Logics.take(order, request.user)
if not valid: return Response(context, status=status.HTTP_403_FORBIDDEN) if not valid: return Response(context, status=status.HTTP_403_FORBIDDEN)
return self.get(request)
else: Response({'bad_request':'This order is not public anymore.'}, status.HTTP_400_BAD_REQUEST) else: Response({'bad_request':'This order is not public anymore.'}, status.HTTP_400_BAD_REQUEST)
# Any other action is only allowed if the user is a participant # Any other action is only allowed if the user is a participant
@ -243,7 +273,11 @@ class OrderView(viewsets.ViewSet):
# 5) If action is dispute # 5) If action is dispute
elif action == 'dispute': elif action == 'dispute':
valid, context = Logics.open_dispute(order,request.user, rating) valid, context = Logics.open_dispute(order,request.user)
if not valid: return Response(context, status.HTTP_400_BAD_REQUEST)
elif action == 'submit_statement':
valid, context = Logics.dispute_statement(order,request.user, statement)
if not valid: return Response(context, status.HTTP_400_BAD_REQUEST) if not valid: return Response(context, status.HTTP_400_BAD_REQUEST)
# 6) If action is rate # 6) If action is rate
@ -281,14 +315,20 @@ class UserView(APIView):
Response with Avatar and Nickname. Response with Avatar and Nickname.
''' '''
# if request.user.id: # If an existing user opens the main page by mistake, we do not want it to create a new nickname/profile for him
# context = {} if request.user.is_authenticated:
# context['nickname'] = request.user.username context = {'nickname': request.user.username}
# participant = not Logics.validate_already_maker_or_taker(request.user) not_participant, _ = Logics.validate_already_maker_or_taker(request.user)
# Does not allow this 'mistake' if an active order
if not not_participant:
context['bad_request'] = f'You are already logged in as {request.user} and have an active order'
return Response(context, status.HTTP_400_BAD_REQUEST)
# Does not allow this 'mistake' if the last login was sometime ago (5 minutes)
# if request.user.last_login < timezone.now() - timedelta(minutes=5):
# context['bad_request'] = f'You are already logged in as {request.user}' # context['bad_request'] = f'You are already logged in as {request.user}'
# if participant: # return Response(context, status.HTTP_400_BAD_REQUEST)
# context['bad_request'] = f'You are already logged in as as {request.user} and have an active order'
# return Response(context,status.HTTP_200_OK)
token = request.GET.get(self.lookup_url_kwarg) token = request.GET.get(self.lookup_url_kwarg)
@ -304,10 +344,10 @@ class UserView(APIView):
context['bad_request'] = 'The token does not have enough entropy' context['bad_request'] = 'The token does not have enough entropy'
return Response(context, status=status.HTTP_400_BAD_REQUEST) return Response(context, status=status.HTTP_400_BAD_REQUEST)
# Hashes the token, only 1 iteration. Maybe more is better. # Hash the token, only 1 iteration.
hash = hashlib.sha256(str.encode(token)).hexdigest() hash = hashlib.sha256(str.encode(token)).hexdigest()
# Generate nickname # Generate nickname deterministically
nickname = self.NickGen.short_from_SHA256(hash, max_length=18)[0] nickname = self.NickGen.short_from_SHA256(hash, max_length=18)[0]
context['nickname'] = nickname context['nickname'] = nickname
@ -316,7 +356,6 @@ class UserView(APIView):
rh.assemble(roboset='set1', bgset='any')# for backgrounds ON rh.assemble(roboset='set1', bgset='any')# for backgrounds ON
# Does not replace image if existing (avoid re-avatar in case of nick collusion) # Does not replace image if existing (avoid re-avatar in case of nick collusion)
image_path = avatar_path.joinpath(nickname+".png") image_path = avatar_path.joinpath(nickname+".png")
if not image_path.exists(): if not image_path.exists():
with open(image_path, "wb") as f: with open(image_path, "wb") as f:
@ -410,26 +449,26 @@ class InfoView(ListAPIView):
context['num_public_sell_orders'] = len(Order.objects.filter(type=Order.Types.SELL, status=Order.Status.PUB)) context['num_public_sell_orders'] = len(Order.objects.filter(type=Order.Types.SELL, status=Order.Status.PUB))
# Number of active users (logged in in last 30 minutes) # Number of active users (logged in in last 30 minutes)
active_user_time_range = (timezone.now() - timedelta(minutes=30), timezone.now()) today = datetime.today()
context['num_active_robotsats'] = len(User.objects.filter(last_login__range=active_user_time_range)) context['active_robots_today'] = len(User.objects.filter(last_login__day=today.day))
# Compute average premium and volume of today # Compute average premium and volume of today
today = datetime.today()
queryset = MarketTick.objects.filter(timestamp__day=today.day) queryset = MarketTick.objects.filter(timestamp__day=today.day)
if not len(queryset) == 0: if not len(queryset) == 0:
premiums = [] weighted_premiums = []
volumes = [] volumes = []
for tick in queryset: for tick in queryset:
premiums.append(tick.premium) weighted_premiums.append(tick.premium*tick.volume)
volumes.append(tick.volume) volumes.append(tick.volume)
avg_premium = sum(premiums) / len(premiums)
total_volume = sum(volumes)
else:
avg_premium = None
total_volume = None
context['today_avg_nonkyc_btc_premium'] = avg_premium total_volume = sum(volumes)
# Avg_premium is the weighted average of the premiums by volume
avg_premium = sum(weighted_premiums) / total_volume
else:
avg_premium = 0
total_volume = 0
context['today_avg_nonkyc_btc_premium'] = round(avg_premium,2)
context['today_total_volume'] = total_volume context['today_total_volume'] = total_volume
context['lnd_version'] = get_lnd_version() context['lnd_version'] = get_lnd_version()
context['robosats_running_commit_hash'] = get_commit_robosats() context['robosats_running_commit_hash'] = get_commit_robosats()

View File

@ -22,9 +22,6 @@ class ChatRoomConsumer(AsyncWebsocketConsumer):
# print ("Outta this chat") # print ("Outta this chat")
# return False # return False
print(self.user_nick)
print(self.order_id)
await self.channel_layer.group_add( await self.channel_layer.group_add(
self.room_group_name, self.room_group_name,
self.channel_name self.channel_name
@ -56,8 +53,16 @@ class ChatRoomConsumer(AsyncWebsocketConsumer):
message = event['message'] message = event['message']
nick = event['nick'] nick = event['nick']
# Insert a white space in words longer than 22 characters.
# Helps when messages overflow in a single line.
words = message.split(' ')
fix_message = ''
for word in words:
word = ' '.join(word[i:i+22] for i in range(0, len(word), 22))
fix_message = fix_message +' '+ word
await self.send(text_data=json.dumps({ await self.send(text_data=json.dumps({
'message': message, 'message': fix_message,
'user_nick': nick, 'user_nick': nick,
})) }))

View File

@ -1,82 +0,0 @@
<html lang="en">
<head>
<!-- Required meta tags -->
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<!-- Bootstrap CSS -->
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css"
integrity="sha384-JcKb8q3iqJ61gNV9KGb8thSsNjpSL0n8PARn9HuZOnIxN0hoP+VmmDGMN5t9UJ0Z" crossorigin="anonymous">
<title>Hello, world!</title>
</head>
<body>
<div class="container">
<div class="row d-flex justify-content-center">
<div class="col-6">
<form>
<div class="form-group">
<label for="exampleFormControlTextarea1" class="h4 pt-5">Chatroom</label>
<textarea class="form-control" id="chat-text" rows="10"></textarea><br>
</div>
<div class="form-group">
<input class="form-control" id="input" type="text"></br>
</div>
<input class="btn btn-secondary btn-lg btn-block" id="submit" type="button" value="Send">
</form>
</div>
</div>
</div>
{{ request.user.username|json_script:"user_username" }}
{{ order_id|json_script:"order-id" }}
<script>
const user_username = JSON.parse(document.getElementById('user_username').textContent);
document.querySelector('#submit').onclick = function (e) {
const messageInputDom = document.querySelector('#input');
const message = messageInputDom.value;
chatSocket.send(JSON.stringify({
'message': message,
'username': user_username,
}));
messageInputDom.value = '';
};
const orderId = JSON.parse(document.getElementById('order-id').textContent);
const chatSocket = new WebSocket(
'ws://' +
window.location.host +
'/ws/chat/' +
orderId +
'/'
);
chatSocket.onmessage = function (e) {
const data = JSON.parse(e.data);
console.log(data)
document.querySelector('#chat-text').value += (data.username + ': ' + data.message + '\n')
}
</script>
<!-- Optional JavaScript -->
<!-- jQuery first, then Popper.js, then Bootstrap JS -->
<script src="https://code.jquery.com/jquery-3.5.1.slim.min.js"
integrity="sha384-DfXdz2htPH0lsSSs5nCTpuj/zy4C+OGpamoFVy38MVBnE+IbbVYUew+OrCXaRkfj" crossorigin="anonymous">
</script>
<script src="https://cdn.jsdelivr.net/npm/popper.js@1.16.1/dist/umd/popper.min.js"
integrity="sha384-9/reFTGAW83EW2RDu2S0VKaIzap3H66lZH81PoYlFhbGU+6BZp6G7niu735Sk7lN" crossorigin="anonymous">
</script>
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/js/bootstrap.min.js"
integrity="sha384-B4gt1jrGC7Jh4AgTPSdUtOBvfO8shuf57BaghqFfPlYxofvL8/KUEfYiJOMMV+rV" crossorigin="anonymous">
</script>
</body>
</html>

View File

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

View File

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

View File

@ -1581,6 +1581,14 @@
"react-is": "^17.0.2" "react-is": "^17.0.2"
} }
}, },
"@mui/icons-material": {
"version": "5.2.5",
"resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-5.2.5.tgz",
"integrity": "sha512-uQiUz+l0xy+2jExyKyU19MkMAR2F7bQFcsQ5hdqAtsB14Jw2zlmIAD55mV6f0NxKCut7Rx6cA3ZpfzlzAfoK8Q==",
"requires": {
"@babel/runtime": "^7.16.3"
}
},
"@mui/material": { "@mui/material": {
"version": "5.2.7", "version": "5.2.7",
"resolved": "https://registry.npmjs.org/@mui/material/-/material-5.2.7.tgz", "resolved": "https://registry.npmjs.org/@mui/material/-/material-5.2.7.tgz",
@ -1666,6 +1674,17 @@
"react-is": "^17.0.2" "react-is": "^17.0.2"
} }
}, },
"@mui/x-data-grid": {
"version": "5.2.2",
"resolved": "https://registry.npmjs.org/@mui/x-data-grid/-/x-data-grid-5.2.2.tgz",
"integrity": "sha512-FI/fwsMMATUdEHwiGGkBdiw/p3G6+iUlkoklBzzsB6MY0Mb+Voj+s/waoFM3uyNJ+h4jof8NTS/Gs8IfDiyciA==",
"requires": {
"@mui/utils": "^5.2.3",
"clsx": "^1.1.1",
"prop-types": "^15.8.0",
"reselect": "^4.1.5"
}
},
"@popperjs/core": { "@popperjs/core": {
"version": "2.11.2", "version": "2.11.2",
"resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.2.tgz", "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.2.tgz",
@ -2106,14 +2125,6 @@
"resolved": "https://registry.npmjs.org/@sideway/pinpoint/-/pinpoint-2.0.0.tgz", "resolved": "https://registry.npmjs.org/@sideway/pinpoint/-/pinpoint-2.0.0.tgz",
"integrity": "sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ==" "integrity": "sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ=="
}, },
"@types/debug": {
"version": "4.1.7",
"resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.7.tgz",
"integrity": "sha512-9AonUzyTjXXhEOa0DnqpzZi6VHlqKMswga9EXjpXnnqxwLtdvPPtlO8evrI5D9S6asFRCQ6v+wpiUKbw+vKqyg==",
"requires": {
"@types/ms": "*"
}
},
"@types/eslint": { "@types/eslint": {
"version": "8.2.1", "version": "8.2.1",
"resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.2.1.tgz", "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.2.1.tgz",
@ -2148,14 +2159,6 @@
"@types/node": "*" "@types/node": "*"
} }
}, },
"@types/hast": {
"version": "2.3.4",
"resolved": "https://registry.npmjs.org/@types/hast/-/hast-2.3.4.tgz",
"integrity": "sha512-wLEm0QvaoawEDoTRwzTXp4b4jpwiJDvR5KMnFnVodm3scufTlBOWRD6N1OBf9TZMhjlNsSfcO5V+7AF4+Vy+9g==",
"requires": {
"@types/unist": "*"
}
},
"@types/istanbul-lib-coverage": { "@types/istanbul-lib-coverage": {
"version": "2.0.4", "version": "2.0.4",
"resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.4.tgz", "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.4.tgz",
@ -2183,24 +2186,6 @@
"integrity": "sha512-qcUXuemtEu+E5wZSJHNxUXeCZhAfXKQ41D+duX+VYPde7xyEVZci+/oXKJL13tnRs9lR2pr4fod59GT6/X1/yQ==", "integrity": "sha512-qcUXuemtEu+E5wZSJHNxUXeCZhAfXKQ41D+duX+VYPde7xyEVZci+/oXKJL13tnRs9lR2pr4fod59GT6/X1/yQ==",
"dev": true "dev": true
}, },
"@types/mdast": {
"version": "3.0.10",
"resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-3.0.10.tgz",
"integrity": "sha512-W864tg/Osz1+9f4lrGTZpCSO5/z4608eUp19tbozkq2HJK6i3z1kT0H9tlADXuYIb1YYOBByU4Jsqkk75q48qA==",
"requires": {
"@types/unist": "*"
}
},
"@types/mdurl": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-1.0.2.tgz",
"integrity": "sha512-eC4U9MlIcu2q0KQmXszyn5Akca/0jrQmwDRgpAMJai7qBWq4amIQhZyNau4VYGtCeALvW1/NtjzJJ567aZxfKA=="
},
"@types/ms": {
"version": "0.7.31",
"resolved": "https://registry.npmjs.org/@types/ms/-/ms-0.7.31.tgz",
"integrity": "sha512-iiUgKzV9AuaEkZqkOLDIvlQiL6ltuZd9tGcW3gwpnX8JbuiuhFlEGmmFXEXkN50Cvq7Os88IY2v0dkDqXYWVgA=="
},
"@types/node": { "@types/node": {
"version": "17.0.6", "version": "17.0.6",
"resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.6.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.6.tgz",
@ -2254,11 +2239,6 @@
"resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.2.tgz", "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.2.tgz",
"integrity": "sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew==" "integrity": "sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew=="
}, },
"@types/unist": {
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.6.tgz",
"integrity": "sha512-PBjIUxZHOuj0R15/xuwJYjFi+KZdNFrehocChv4g5hu6aFroHue8m0lBP0POdK2nKzbw0cgV1mws8+V/JAcEkQ=="
},
"@types/yargs": { "@types/yargs": {
"version": "16.0.4", "version": "16.0.4",
"resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-16.0.4.tgz", "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-16.0.4.tgz",
@ -2750,11 +2730,6 @@
"babel-plugin-syntax-trailing-function-commas": "^7.0.0-beta.0" "babel-plugin-syntax-trailing-function-commas": "^7.0.0-beta.0"
} }
}, },
"bail": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz",
"integrity": "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw=="
},
"balanced-match": { "balanced-match": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
@ -2998,11 +2973,6 @@
} }
} }
}, },
"character-entities": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/character-entities/-/character-entities-2.0.1.tgz",
"integrity": "sha512-OzmutCf2Kmc+6DrFrrPS8/tDh2+DpnrfzdICHWhcVC9eOd0N1PXmQEE1a8iM4IziIAG+8tmTq3K+oo0ubH6RRQ=="
},
"chrome-trace-event": { "chrome-trace-event": {
"version": "1.0.3", "version": "1.0.3",
"resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.3.tgz", "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.3.tgz",
@ -3126,11 +3096,6 @@
"resolved": "https://registry.npmjs.org/colors/-/colors-1.4.0.tgz", "resolved": "https://registry.npmjs.org/colors/-/colors-1.4.0.tgz",
"integrity": "sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==" "integrity": "sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA=="
}, },
"comma-separated-tokens": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.2.tgz",
"integrity": "sha512-G5yTt3KQN4Yn7Yk4ed73hlZ1evrFKXeUW3086p3PRFNp7m2vIjI6Pg+Kgb+oyzhd9F2qdcoj67+y3SdxL5XWsg=="
},
"command-exists": { "command-exists": {
"version": "1.2.9", "version": "1.2.9",
"resolved": "https://registry.npmjs.org/command-exists/-/command-exists-1.2.9.tgz", "resolved": "https://registry.npmjs.org/command-exists/-/command-exists-1.2.9.tgz",
@ -3354,14 +3319,6 @@
"resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz",
"integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=" "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA="
}, },
"decode-named-character-reference": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.0.1.tgz",
"integrity": "sha512-YV/0HQHreRwKb7uBopyIkLG17jG6Sv2qUchk9qSoVJ2f+flwRsPNBO0hAnjt6mTNYUT+vw9Gy2ihXg4sUWPi2w==",
"requires": {
"character-entities": "^2.0.0"
}
},
"decode-uri-component": { "decode-uri-component": {
"version": "0.2.0", "version": "0.2.0",
"resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz", "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz",
@ -3435,21 +3392,11 @@
"resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz",
"integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=" "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak="
}, },
"dequal": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.2.tgz",
"integrity": "sha512-q9K8BlJVxK7hQYqa6XISGmBZbtQQWVXSrRrWreHC94rMt1QL/Impruc+7p2CYSYuVIUr+YCt6hjrs1kkdJRTug=="
},
"destroy": { "destroy": {
"version": "1.0.4", "version": "1.0.4",
"resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz",
"integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=" "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA="
}, },
"diff": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/diff/-/diff-5.0.0.tgz",
"integrity": "sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w=="
},
"dom-helpers": { "dom-helpers": {
"version": "5.2.1", "version": "5.2.1",
"resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz", "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz",
@ -3767,11 +3714,6 @@
} }
} }
}, },
"extend": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
"integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="
},
"extend-shallow": { "extend-shallow": {
"version": "3.0.2", "version": "3.0.2",
"resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz",
@ -4112,11 +4054,6 @@
} }
} }
}, },
"hast-util-whitespace": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-2.0.0.tgz",
"integrity": "sha512-Pkw+xBHuV6xFeJprJe2BBEoDV+AvQySaz3pPDRUs5PNZEMQjpXJJueqrpcHIXxnWTcAGi/UOCgVShlkY6kLoqg=="
},
"hermes-engine": { "hermes-engine": {
"version": "0.9.0", "version": "0.9.0",
"resolved": "https://registry.npmjs.org/hermes-engine/-/hermes-engine-0.9.0.tgz", "resolved": "https://registry.npmjs.org/hermes-engine/-/hermes-engine-0.9.0.tgz",
@ -4243,11 +4180,6 @@
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
}, },
"inline-style-parser": {
"version": "0.1.1",
"resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.1.1.tgz",
"integrity": "sha512-7NXolsK4CAS5+xvdj5OMMbI962hU/wvwoxk+LWR9Ek9bVtyuuYScDN6eS0rUm6TxApFpw7CX1o4uJzcd4AyD3Q=="
},
"interpret": { "interpret": {
"version": "2.2.0", "version": "2.2.0",
"resolved": "https://registry.npmjs.org/interpret/-/interpret-2.2.0.tgz", "resolved": "https://registry.npmjs.org/interpret/-/interpret-2.2.0.tgz",
@ -4371,11 +4303,6 @@
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
"integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==" "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="
}, },
"is-plain-obj": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.0.0.tgz",
"integrity": "sha512-NXRbBtUdBioI73y/HmOhogw/U5msYPC9DAtGkJXeFcFWSFZw0mCUsPxk/snTuJHzNKA8kLBK4rH97RMB1BfCXw=="
},
"is-plain-object": { "is-plain-object": {
"version": "2.0.4", "version": "2.0.4",
"resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz",
@ -5121,78 +5048,11 @@
"prop-types": "^15.5.8" "prop-types": "^15.5.8"
} }
}, },
"mdast-util-definitions": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/mdast-util-definitions/-/mdast-util-definitions-5.1.0.tgz",
"integrity": "sha512-5hcR7FL2EuZ4q6lLMUK5w4lHT2H3vqL9quPvYZ/Ku5iifrirfMHiGdhxdXMUbUkDmz5I+TYMd7nbaxUhbQkfpQ==",
"requires": {
"@types/mdast": "^3.0.0",
"@types/unist": "^2.0.0",
"unist-util-visit": "^3.0.0"
},
"dependencies": {
"unist-util-visit": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-3.1.0.tgz",
"integrity": "sha512-Szoh+R/Ll68QWAyQyZZpQzZQm2UPbxibDvaY8Xc9SUtYgPsDzx5AWSk++UUt2hJuow8mvwR+rG+LQLw+KsuAKA==",
"requires": {
"@types/unist": "^2.0.0",
"unist-util-is": "^5.0.0",
"unist-util-visit-parents": "^4.0.0"
}
}
}
},
"mdast-util-from-markdown": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-1.2.0.tgz",
"integrity": "sha512-iZJyyvKD1+K7QX1b5jXdE7Sc5dtoTry1vzV28UZZe8Z1xVnB/czKntJ7ZAkG0tANqRnBF6p3p7GpU1y19DTf2Q==",
"requires": {
"@types/mdast": "^3.0.0",
"@types/unist": "^2.0.0",
"decode-named-character-reference": "^1.0.0",
"mdast-util-to-string": "^3.1.0",
"micromark": "^3.0.0",
"micromark-util-decode-numeric-character-reference": "^1.0.0",
"micromark-util-decode-string": "^1.0.0",
"micromark-util-normalize-identifier": "^1.0.0",
"micromark-util-symbol": "^1.0.0",
"micromark-util-types": "^1.0.0",
"unist-util-stringify-position": "^3.0.0",
"uvu": "^0.5.0"
}
},
"mdast-util-to-hast": {
"version": "11.3.0",
"resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-11.3.0.tgz",
"integrity": "sha512-4o3Cli3hXPmm1LhB+6rqhfsIUBjnKFlIUZvudaermXB+4/KONdd/W4saWWkC+LBLbPMqhFSSTSRgafHsT5fVJw==",
"requires": {
"@types/hast": "^2.0.0",
"@types/mdast": "^3.0.0",
"@types/mdurl": "^1.0.0",
"mdast-util-definitions": "^5.0.0",
"mdurl": "^1.0.0",
"unist-builder": "^3.0.0",
"unist-util-generated": "^2.0.0",
"unist-util-position": "^4.0.0",
"unist-util-visit": "^4.0.0"
}
},
"mdast-util-to-string": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-3.1.0.tgz",
"integrity": "sha512-n4Vypz/DZgwo0iMHLQL49dJzlp7YtAJP+N07MZHpjPf/5XJuHUWstviF4Mn2jEiR/GNmtnRRqnwsXExk3igfFA=="
},
"mdn-data": { "mdn-data": {
"version": "2.0.14", "version": "2.0.14",
"resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.14.tgz", "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.14.tgz",
"integrity": "sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==" "integrity": "sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow=="
}, },
"mdurl": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/mdurl/-/mdurl-1.0.1.tgz",
"integrity": "sha1-/oWy7HWlkDfyrf7BAP1sYBdhFS4="
},
"merge-stream": { "merge-stream": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz",
@ -5661,218 +5521,6 @@
"nullthrows": "^1.1.1" "nullthrows": "^1.1.1"
} }
}, },
"micromark": {
"version": "3.0.10",
"resolved": "https://registry.npmjs.org/micromark/-/micromark-3.0.10.tgz",
"integrity": "sha512-ryTDy6UUunOXy2HPjelppgJ2sNfcPz1pLlMdA6Rz9jPzhLikWXv/irpWV/I2jd68Uhmny7hHxAlAhk4+vWggpg==",
"requires": {
"@types/debug": "^4.0.0",
"debug": "^4.0.0",
"decode-named-character-reference": "^1.0.0",
"micromark-core-commonmark": "^1.0.1",
"micromark-factory-space": "^1.0.0",
"micromark-util-character": "^1.0.0",
"micromark-util-chunked": "^1.0.0",
"micromark-util-combine-extensions": "^1.0.0",
"micromark-util-decode-numeric-character-reference": "^1.0.0",
"micromark-util-encode": "^1.0.0",
"micromark-util-normalize-identifier": "^1.0.0",
"micromark-util-resolve-all": "^1.0.0",
"micromark-util-sanitize-uri": "^1.0.0",
"micromark-util-subtokenize": "^1.0.0",
"micromark-util-symbol": "^1.0.0",
"micromark-util-types": "^1.0.1",
"uvu": "^0.5.0"
}
},
"micromark-core-commonmark": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-1.0.6.tgz",
"integrity": "sha512-K+PkJTxqjFfSNkfAhp4GB+cZPfQd6dxtTXnf+RjZOV7T4EEXnvgzOcnp+eSTmpGk9d1S9sL6/lqrgSNn/s0HZA==",
"requires": {
"decode-named-character-reference": "^1.0.0",
"micromark-factory-destination": "^1.0.0",
"micromark-factory-label": "^1.0.0",
"micromark-factory-space": "^1.0.0",
"micromark-factory-title": "^1.0.0",
"micromark-factory-whitespace": "^1.0.0",
"micromark-util-character": "^1.0.0",
"micromark-util-chunked": "^1.0.0",
"micromark-util-classify-character": "^1.0.0",
"micromark-util-html-tag-name": "^1.0.0",
"micromark-util-normalize-identifier": "^1.0.0",
"micromark-util-resolve-all": "^1.0.0",
"micromark-util-subtokenize": "^1.0.0",
"micromark-util-symbol": "^1.0.0",
"micromark-util-types": "^1.0.1",
"uvu": "^0.5.0"
}
},
"micromark-factory-destination": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-1.0.0.tgz",
"integrity": "sha512-eUBA7Rs1/xtTVun9TmV3gjfPz2wEwgK5R5xcbIM5ZYAtvGF6JkyaDsj0agx8urXnO31tEO6Ug83iVH3tdedLnw==",
"requires": {
"micromark-util-character": "^1.0.0",
"micromark-util-symbol": "^1.0.0",
"micromark-util-types": "^1.0.0"
}
},
"micromark-factory-label": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/micromark-factory-label/-/micromark-factory-label-1.0.2.tgz",
"integrity": "sha512-CTIwxlOnU7dEshXDQ+dsr2n+yxpP0+fn271pu0bwDIS8uqfFcumXpj5mLn3hSC8iw2MUr6Gx8EcKng1dD7i6hg==",
"requires": {
"micromark-util-character": "^1.0.0",
"micromark-util-symbol": "^1.0.0",
"micromark-util-types": "^1.0.0",
"uvu": "^0.5.0"
}
},
"micromark-factory-space": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-1.0.0.tgz",
"integrity": "sha512-qUmqs4kj9a5yBnk3JMLyjtWYN6Mzfcx8uJfi5XAveBniDevmZasdGBba5b4QsvRcAkmvGo5ACmSUmyGiKTLZew==",
"requires": {
"micromark-util-character": "^1.0.0",
"micromark-util-types": "^1.0.0"
}
},
"micromark-factory-title": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/micromark-factory-title/-/micromark-factory-title-1.0.2.tgz",
"integrity": "sha512-zily+Nr4yFqgMGRKLpTVsNl5L4PMu485fGFDOQJQBl2NFpjGte1e86zC0da93wf97jrc4+2G2GQudFMHn3IX+A==",
"requires": {
"micromark-factory-space": "^1.0.0",
"micromark-util-character": "^1.0.0",
"micromark-util-symbol": "^1.0.0",
"micromark-util-types": "^1.0.0",
"uvu": "^0.5.0"
}
},
"micromark-factory-whitespace": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/micromark-factory-whitespace/-/micromark-factory-whitespace-1.0.0.tgz",
"integrity": "sha512-Qx7uEyahU1lt1RnsECBiuEbfr9INjQTGa6Err+gF3g0Tx4YEviPbqqGKNv/NrBaE7dVHdn1bVZKM/n5I/Bak7A==",
"requires": {
"micromark-factory-space": "^1.0.0",
"micromark-util-character": "^1.0.0",
"micromark-util-symbol": "^1.0.0",
"micromark-util-types": "^1.0.0"
}
},
"micromark-util-character": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-1.1.0.tgz",
"integrity": "sha512-agJ5B3unGNJ9rJvADMJ5ZiYjBRyDpzKAOk01Kpi1TKhlT1APx3XZk6eN7RtSz1erbWHC2L8T3xLZ81wdtGRZzg==",
"requires": {
"micromark-util-symbol": "^1.0.0",
"micromark-util-types": "^1.0.0"
}
},
"micromark-util-chunked": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/micromark-util-chunked/-/micromark-util-chunked-1.0.0.tgz",
"integrity": "sha512-5e8xTis5tEZKgesfbQMKRCyzvffRRUX+lK/y+DvsMFdabAicPkkZV6gO+FEWi9RfuKKoxxPwNL+dFF0SMImc1g==",
"requires": {
"micromark-util-symbol": "^1.0.0"
}
},
"micromark-util-classify-character": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/micromark-util-classify-character/-/micromark-util-classify-character-1.0.0.tgz",
"integrity": "sha512-F8oW2KKrQRb3vS5ud5HIqBVkCqQi224Nm55o5wYLzY/9PwHGXC01tr3d7+TqHHz6zrKQ72Okwtvm/xQm6OVNZA==",
"requires": {
"micromark-util-character": "^1.0.0",
"micromark-util-symbol": "^1.0.0",
"micromark-util-types": "^1.0.0"
}
},
"micromark-util-combine-extensions": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/micromark-util-combine-extensions/-/micromark-util-combine-extensions-1.0.0.tgz",
"integrity": "sha512-J8H058vFBdo/6+AsjHp2NF7AJ02SZtWaVUjsayNFeAiydTxUwViQPxN0Hf8dp4FmCQi0UUFovFsEyRSUmFH3MA==",
"requires": {
"micromark-util-chunked": "^1.0.0",
"micromark-util-types": "^1.0.0"
}
},
"micromark-util-decode-numeric-character-reference": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-1.0.0.tgz",
"integrity": "sha512-OzO9AI5VUtrTD7KSdagf4MWgHMtET17Ua1fIpXTpuhclCqD8egFWo85GxSGvxgkGS74bEahvtM0WP0HjvV0e4w==",
"requires": {
"micromark-util-symbol": "^1.0.0"
}
},
"micromark-util-decode-string": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/micromark-util-decode-string/-/micromark-util-decode-string-1.0.2.tgz",
"integrity": "sha512-DLT5Ho02qr6QWVNYbRZ3RYOSSWWFuH3tJexd3dgN1odEuPNxCngTCXJum7+ViRAd9BbdxCvMToPOD/IvVhzG6Q==",
"requires": {
"decode-named-character-reference": "^1.0.0",
"micromark-util-character": "^1.0.0",
"micromark-util-decode-numeric-character-reference": "^1.0.0",
"micromark-util-symbol": "^1.0.0"
}
},
"micromark-util-encode": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-1.0.1.tgz",
"integrity": "sha512-U2s5YdnAYexjKDel31SVMPbfi+eF8y1U4pfiRW/Y8EFVCy/vgxk/2wWTxzcqE71LHtCuCzlBDRU2a5CQ5j+mQA=="
},
"micromark-util-html-tag-name": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/micromark-util-html-tag-name/-/micromark-util-html-tag-name-1.0.0.tgz",
"integrity": "sha512-NenEKIshW2ZI/ERv9HtFNsrn3llSPZtY337LID/24WeLqMzeZhBEE6BQ0vS2ZBjshm5n40chKtJ3qjAbVV8S0g=="
},
"micromark-util-normalize-identifier": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-1.0.0.tgz",
"integrity": "sha512-yg+zrL14bBTFrQ7n35CmByWUTFsgst5JhA4gJYoty4Dqzj4Z4Fr/DHekSS5aLfH9bdlfnSvKAWsAgJhIbogyBg==",
"requires": {
"micromark-util-symbol": "^1.0.0"
}
},
"micromark-util-resolve-all": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/micromark-util-resolve-all/-/micromark-util-resolve-all-1.0.0.tgz",
"integrity": "sha512-CB/AGk98u50k42kvgaMM94wzBqozSzDDaonKU7P7jwQIuH2RU0TeBqGYJz2WY1UdihhjweivStrJ2JdkdEmcfw==",
"requires": {
"micromark-util-types": "^1.0.0"
}
},
"micromark-util-sanitize-uri": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-1.0.0.tgz",
"integrity": "sha512-cCxvBKlmac4rxCGx6ejlIviRaMKZc0fWm5HdCHEeDWRSkn44l6NdYVRyU+0nT1XC72EQJMZV8IPHF+jTr56lAg==",
"requires": {
"micromark-util-character": "^1.0.0",
"micromark-util-encode": "^1.0.0",
"micromark-util-symbol": "^1.0.0"
}
},
"micromark-util-subtokenize": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/micromark-util-subtokenize/-/micromark-util-subtokenize-1.0.2.tgz",
"integrity": "sha512-d90uqCnXp/cy4G881Ub4psE57Sf8YD0pim9QdjCRNjfas2M1u6Lbt+XZK9gnHL2XFhnozZiEdCa9CNfXSfQ6xA==",
"requires": {
"micromark-util-chunked": "^1.0.0",
"micromark-util-symbol": "^1.0.0",
"micromark-util-types": "^1.0.0",
"uvu": "^0.5.0"
}
},
"micromark-util-symbol": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-1.0.1.tgz",
"integrity": "sha512-oKDEMK2u5qqAptasDAwWDXq0tG9AssVwAx3E9bBF3t/shRIGsWIRG+cGafs2p/SnDSOecnt6hZPCE2o6lHfFmQ=="
},
"micromark-util-types": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-1.0.2.tgz",
"integrity": "sha512-DCfg/T8fcrhrRKTPjRrw/5LLvdGV7BHySf/1LOZx7TzWZdYRjogNtyNq885z3nNallwr3QUKARjqvHqX1/7t+w=="
},
"micromatch": { "micromatch": {
"version": "4.0.4", "version": "4.0.4",
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.4.tgz", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.4.tgz",
@ -5955,11 +5603,6 @@
"minimist": "^1.2.5" "minimist": "^1.2.5"
} }
}, },
"mri": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz",
"integrity": "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA=="
},
"ms": { "ms": {
"version": "2.1.2", "version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
@ -6454,11 +6097,6 @@
} }
} }
}, },
"property-information": {
"version": "6.1.1",
"resolved": "https://registry.npmjs.org/property-information/-/property-information-6.1.1.tgz",
"integrity": "sha512-hrzC564QIl0r0vy4l6MvRLhafmUowhO/O3KgVSoXIbbA2Sz4j8HGpJc6T2cubRVwMwpdiG/vKGfhT4IixmKN9w=="
},
"pump": { "pump": {
"version": "3.0.0", "version": "3.0.0",
"resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz",
@ -6503,6 +6141,14 @@
"object-assign": "^4.1.1" "object-assign": "^4.1.1"
} }
}, },
"react-countdown": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/react-countdown/-/react-countdown-2.3.2.tgz",
"integrity": "sha512-Q4SADotHtgOxNWhDdvgupmKVL0pMB9DvoFcxv5AzjsxVhzOVxnttMbAywgqeOdruwEAmnPhOhNv/awAgkwru2w==",
"requires": {
"prop-types": "^15.7.2"
}
},
"react-devtools-core": { "react-devtools-core": {
"version": "4.22.1", "version": "4.22.1",
"resolved": "https://registry.npmjs.org/react-devtools-core/-/react-devtools-core-4.22.1.tgz", "resolved": "https://registry.npmjs.org/react-devtools-core/-/react-devtools-core-4.22.1.tgz",
@ -6535,27 +6181,6 @@
"resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
"integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==" "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w=="
}, },
"react-markdown": {
"version": "7.1.2",
"resolved": "https://registry.npmjs.org/react-markdown/-/react-markdown-7.1.2.tgz",
"integrity": "sha512-ibMcc0EbfmbwApqJD8AUr0yls8BSrKzIbHaUsPidQljxToCqFh34nwtu3CXNEItcVJNzpjDHrhK8A+MAh2JW3A==",
"requires": {
"@types/hast": "^2.0.0",
"@types/unist": "^2.0.0",
"comma-separated-tokens": "^2.0.0",
"hast-util-whitespace": "^2.0.0",
"prop-types": "^15.0.0",
"property-information": "^6.0.0",
"react-is": "^17.0.0",
"remark-parse": "^10.0.0",
"remark-rehype": "^9.0.0",
"space-separated-tokens": "^2.0.0",
"style-to-object": "^0.3.0",
"unified": "^10.0.0",
"unist-util-visit": "^4.0.0",
"vfile": "^5.0.0"
}
},
"react-native": { "react-native": {
"version": "0.66.4", "version": "0.66.4",
"resolved": "https://registry.npmjs.org/react-native/-/react-native-0.66.4.tgz", "resolved": "https://registry.npmjs.org/react-native/-/react-native-0.66.4.tgz",
@ -6987,27 +6612,6 @@
} }
} }
}, },
"remark-parse": {
"version": "10.0.1",
"resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-10.0.1.tgz",
"integrity": "sha512-1fUyHr2jLsVOkhbvPRBJ5zTKZZyD6yZzYaWCS6BPBdQ8vEMBCH+9zNCDA6tET/zHCi/jLqjCWtlJZUPk+DbnFw==",
"requires": {
"@types/mdast": "^3.0.0",
"mdast-util-from-markdown": "^1.0.0",
"unified": "^10.0.0"
}
},
"remark-rehype": {
"version": "9.1.0",
"resolved": "https://registry.npmjs.org/remark-rehype/-/remark-rehype-9.1.0.tgz",
"integrity": "sha512-oLa6YmgAYg19zb0ZrBACh40hpBLteYROaPLhBXzLgjqyHQrN+gVP9N/FJvfzuNNuzCutktkroXEZBrxAxKhh7Q==",
"requires": {
"@types/hast": "^2.0.0",
"@types/mdast": "^3.0.0",
"mdast-util-to-hast": "^11.0.0",
"unified": "^10.0.0"
}
},
"remove-trailing-separator": { "remove-trailing-separator": {
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz", "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz",
@ -7033,6 +6637,11 @@
"resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz",
"integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==" "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg=="
}, },
"reselect": {
"version": "4.1.5",
"resolved": "https://registry.npmjs.org/reselect/-/reselect-4.1.5.tgz",
"integrity": "sha512-uVdlz8J7OO+ASpBYoz1Zypgx0KasCY20H+N8JD13oUMtPvSHQuscrHop4KbXrbsBcdB9Ds7lVK7eRkBIfO43vQ=="
},
"resolve": { "resolve": {
"version": "1.20.0", "version": "1.20.0",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.20.0.tgz", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.20.0.tgz",
@ -7109,14 +6718,6 @@
"resolved": "https://registry.npmjs.org/rsvp/-/rsvp-4.8.5.tgz", "resolved": "https://registry.npmjs.org/rsvp/-/rsvp-4.8.5.tgz",
"integrity": "sha512-nfMOlASu9OnRJo1mbEk2cz0D56a1MBNrJ7orjRZQG10XDyuvwksKbuXNp6qa+kbn839HwjwhBzhFmdsaEAfauA==" "integrity": "sha512-nfMOlASu9OnRJo1mbEk2cz0D56a1MBNrJ7orjRZQG10XDyuvwksKbuXNp6qa+kbn839HwjwhBzhFmdsaEAfauA=="
}, },
"sade": {
"version": "1.8.1",
"resolved": "https://registry.npmjs.org/sade/-/sade-1.8.1.tgz",
"integrity": "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==",
"requires": {
"mri": "^1.1.0"
}
},
"safe-buffer": { "safe-buffer": {
"version": "5.2.1", "version": "5.2.1",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
@ -7682,11 +7283,6 @@
"resolved": "https://registry.npmjs.org/source-map-url/-/source-map-url-0.4.1.tgz", "resolved": "https://registry.npmjs.org/source-map-url/-/source-map-url-0.4.1.tgz",
"integrity": "sha512-cPiFOTLUKvJFIg4SKVScy4ilPPW6rFgMgfuZJPNoDuMs3nC1HbMUycBoJw77xFIp6z1UJQJOfx6C9GMH80DiTw==" "integrity": "sha512-cPiFOTLUKvJFIg4SKVScy4ilPPW6rFgMgfuZJPNoDuMs3nC1HbMUycBoJw77xFIp6z1UJQJOfx6C9GMH80DiTw=="
}, },
"space-separated-tokens": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.1.tgz",
"integrity": "sha512-ekwEbFp5aqSPKaqeY1PGrlGQxPNaq+Cnx4+bE2D8sciBQrHpbwoBbawqTN2+6jPs9IdWxxiUcN0K2pkczD3zmw=="
},
"split-string": { "split-string": {
"version": "3.1.0", "version": "3.1.0",
"resolved": "https://registry.npmjs.org/split-string/-/split-string-3.1.0.tgz", "resolved": "https://registry.npmjs.org/split-string/-/split-string-3.1.0.tgz",
@ -7806,14 +7402,6 @@
"integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==",
"dev": true "dev": true
}, },
"style-to-object": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-0.3.0.tgz",
"integrity": "sha512-CzFnRRXhzWIdItT3OmF8SQfWyahHhjq3HwcMNCNLn+N7klOOqPjMeG/4JSu77D7ypZdGvSzvkrbyeTMizz2VrA==",
"requires": {
"inline-style-parser": "0.1.1"
}
},
"stylis": { "stylis": {
"version": "4.0.13", "version": "4.0.13",
"resolved": "https://registry.npmjs.org/stylis/-/stylis-4.0.13.tgz", "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.0.13.tgz",
@ -7968,11 +7556,6 @@
"resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
"integrity": "sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o=" "integrity": "sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o="
}, },
"trough": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/trough/-/trough-2.0.2.tgz",
"integrity": "sha512-FnHq5sTMxC0sk957wHDzRnemFnNBvt/gSY99HzK8F7UP5WAbvP70yX5bd7CjEQkN+TjdxwI7g7lJ6podqrG2/w=="
},
"tslib": { "tslib": {
"version": "2.3.1", "version": "2.3.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz",
@ -8041,27 +7624,6 @@
"resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.0.0.tgz", "resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.0.0.tgz",
"integrity": "sha512-5Zfuy9q/DFr4tfO7ZPeVXb1aPoeQSdeFMLpYuFebehDAhbuevLs5yxSZmIFN1tP5F9Wl4IpJrYojg85/zgyZHQ==" "integrity": "sha512-5Zfuy9q/DFr4tfO7ZPeVXb1aPoeQSdeFMLpYuFebehDAhbuevLs5yxSZmIFN1tP5F9Wl4IpJrYojg85/zgyZHQ=="
}, },
"unified": {
"version": "10.1.1",
"resolved": "https://registry.npmjs.org/unified/-/unified-10.1.1.tgz",
"integrity": "sha512-v4ky1+6BN9X3pQrOdkFIPWAaeDsHPE1svRDxq7YpTc2plkIqFMwukfqM+l0ewpP9EfwARlt9pPFAeWYhHm8X9w==",
"requires": {
"@types/unist": "^2.0.0",
"bail": "^2.0.0",
"extend": "^3.0.0",
"is-buffer": "^2.0.0",
"is-plain-obj": "^4.0.0",
"trough": "^2.0.0",
"vfile": "^5.0.0"
},
"dependencies": {
"is-buffer": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.5.tgz",
"integrity": "sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ=="
}
}
},
"union-value": { "union-value": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.1.tgz", "resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.1.tgz",
@ -8073,67 +7635,6 @@
"set-value": "^2.0.1" "set-value": "^2.0.1"
} }
}, },
"unist-builder": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/unist-builder/-/unist-builder-3.0.0.tgz",
"integrity": "sha512-GFxmfEAa0vi9i5sd0R2kcrI9ks0r82NasRq5QHh2ysGngrc6GiqD5CDf1FjPenY4vApmFASBIIlk/jj5J5YbmQ==",
"requires": {
"@types/unist": "^2.0.0"
}
},
"unist-util-generated": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/unist-util-generated/-/unist-util-generated-2.0.0.tgz",
"integrity": "sha512-TiWE6DVtVe7Ye2QxOVW9kqybs6cZexNwTwSMVgkfjEReqy/xwGpAXb99OxktoWwmL+Z+Epb0Dn8/GNDYP1wnUw=="
},
"unist-util-is": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-5.1.1.tgz",
"integrity": "sha512-F5CZ68eYzuSvJjGhCLPL3cYx45IxkqXSetCcRgUXtbcm50X2L9oOWQlfUfDdAf+6Pd27YDblBfdtmsThXmwpbQ=="
},
"unist-util-position": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-4.0.1.tgz",
"integrity": "sha512-mgy/zI9fQ2HlbOtTdr2w9lhVaiFUHWQnZrFF2EUoVOqtAUdzqMtNiD99qA5a1IcjWVR8O6aVYE9u7Z2z1v0SQA=="
},
"unist-util-stringify-position": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-3.0.0.tgz",
"integrity": "sha512-SdfAl8fsDclywZpfMDTVDxA2V7LjtRDTOFd44wUJamgl6OlVngsqWjxvermMYf60elWHbxhuRCZml7AnuXCaSA==",
"requires": {
"@types/unist": "^2.0.0"
}
},
"unist-util-visit": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-4.1.0.tgz",
"integrity": "sha512-n7lyhFKJfVZ9MnKtqbsqkQEk5P1KShj0+//V7mAcoI6bpbUjh3C/OG8HVD+pBihfh6Ovl01m8dkcv9HNqYajmQ==",
"requires": {
"@types/unist": "^2.0.0",
"unist-util-is": "^5.0.0",
"unist-util-visit-parents": "^5.0.0"
},
"dependencies": {
"unist-util-visit-parents": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-5.1.0.tgz",
"integrity": "sha512-y+QVLcY5eR/YVpqDsLf/xh9R3Q2Y4HxkZTp7ViLDU6WtJCEcPmRzW1gpdWDCDIqIlhuPDXOgttqPlykrHYDekg==",
"requires": {
"@types/unist": "^2.0.0",
"unist-util-is": "^5.0.0"
}
}
}
},
"unist-util-visit-parents": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-4.1.1.tgz",
"integrity": "sha512-1xAFJXAKpnnJl8G7K5KgU7FY55y3GcLIXqkzUj5QF/QVP7biUm0K0O2oqVkYsdjzJKifYeWn9+o6piAK2hGSHw==",
"requires": {
"@types/unist": "^2.0.0",
"unist-util-is": "^5.0.0"
}
},
"universalify": { "universalify": {
"version": "0.1.2", "version": "0.1.2",
"resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz",
@ -8235,24 +7736,6 @@
"resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz",
"integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==" "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A=="
}, },
"uvu": {
"version": "0.5.3",
"resolved": "https://registry.npmjs.org/uvu/-/uvu-0.5.3.tgz",
"integrity": "sha512-brFwqA3FXzilmtnIyJ+CxdkInkY/i4ErvP7uV0DnUVxQcQ55reuHphorpF+tZoVHK2MniZ/VJzI7zJQoc9T9Yw==",
"requires": {
"dequal": "^2.0.0",
"diff": "^5.0.0",
"kleur": "^4.0.3",
"sade": "^1.7.3"
},
"dependencies": {
"kleur": {
"version": "4.1.4",
"resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.4.tgz",
"integrity": "sha512-8QADVssbrFjivHWQU7KkMgptGTl6WAcSdlbBPY4uNF+mWr6DGcKrvY2w4FQJoXch7+fKMjj0dRrL75vk3k23OA=="
}
}
},
"value-equal": { "value-equal": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/value-equal/-/value-equal-1.0.1.tgz", "resolved": "https://registry.npmjs.org/value-equal/-/value-equal-1.0.1.tgz",
@ -8263,33 +7746,6 @@
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
"integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=" "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw="
}, },
"vfile": {
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/vfile/-/vfile-5.3.0.tgz",
"integrity": "sha512-Tj44nY/48OQvarrE4FAjUfrv7GZOYzPbl5OD65HxVKwLJKMPU7zmfV8cCgCnzKWnSfYG2f3pxu+ALqs7j22xQQ==",
"requires": {
"@types/unist": "^2.0.0",
"is-buffer": "^2.0.0",
"unist-util-stringify-position": "^3.0.0",
"vfile-message": "^3.0.0"
},
"dependencies": {
"is-buffer": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.5.tgz",
"integrity": "sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ=="
}
}
},
"vfile-message": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-3.1.0.tgz",
"integrity": "sha512-4QJbBk+DkPEhBXq3f260xSaWtjE4gPKOfulzfMFF8ZNwaPZieWsg3iVlcmF04+eebzpcpeXOOFMfrYzJHVYg+g==",
"requires": {
"@types/unist": "^2.0.0",
"unist-util-stringify-position": "^3.0.0"
}
},
"vlq": { "vlq": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/vlq/-/vlq-1.0.1.tgz", "resolved": "https://registry.npmjs.org/vlq/-/vlq-1.0.1.tgz",

View File

@ -26,10 +26,12 @@
"@emotion/styled": "^11.6.0", "@emotion/styled": "^11.6.0",
"@material-ui/core": "^4.12.3", "@material-ui/core": "^4.12.3",
"@material-ui/icons": "^4.11.2", "@material-ui/icons": "^4.11.2",
"@mui/icons-material": "^5.2.5",
"@mui/material": "^5.2.7", "@mui/material": "^5.2.7",
"@mui/system": "^5.2.6", "@mui/system": "^5.2.6",
"@mui/x-data-grid": "^5.2.2",
"material-ui-image": "^3.3.2", "material-ui-image": "^3.3.2",
"react-markdown": "^7.1.2", "react-countdown": "^2.3.2",
"react-native": "^0.66.4", "react-native": "^0.66.4",
"react-native-svg": "^12.1.1", "react-native-svg": "^12.1.1",
"react-qr-code": "^2.0.3", "react-qr-code": "^2.0.3",

View File

@ -2,6 +2,7 @@ import React, { Component } from "react";
import { render } from "react-dom"; import { render } from "react-dom";
import HomePage from "./HomePage"; import HomePage from "./HomePage";
import BottomBar from "./BottomBar";
export default class App extends Component { export default class App extends Component {
constructor(props) { constructor(props) {
@ -10,9 +11,14 @@ export default class App extends Component {
render() { render() {
return ( return (
<>
<div className='appCenter'> <div className='appCenter'>
<HomePage /> <HomePage />
</div> </div>
<div className='bottomBar'>
<BottomBar />
</div>
</>
); );
} }
} }

View File

@ -1,12 +1,14 @@
import React, { Component } from "react"; import React, { Component } from "react";
import { Paper, Button , Divider, CircularProgress, ListItemButton, Typography, Grid, Select, MenuItem, FormControl, FormHelperText, List, ListItem, ListItemText, Avatar, RouterLink, ListItemAvatar} from "@mui/material"; import { Paper, Button , CircularProgress, ListItemButton, Typography, Grid, Select, MenuItem, FormControl, FormHelperText, List, ListItem, ListItemText, Avatar, RouterLink, ListItemAvatar} from "@mui/material";
import { Link } from 'react-router-dom' import { Link } from 'react-router-dom'
import { DataGrid } from '@mui/x-data-grid';
import getFlags from './getFlags'
export default class BookPage extends Component { export default class BookPage extends Component {
constructor(props) { constructor(props) {
super(props); super(props);
this.state = { this.state = {
orders: new Array(), orders: new Array({id:0,}),
currency: 0, currency: 0,
type: 1, type: 1,
currencies_dict: {"0":"ANY"}, currencies_dict: {"0":"ANY"},
@ -20,7 +22,7 @@ export default class BookPage extends Component {
getOrderDetails(type, currency) { getOrderDetails(type, currency) {
fetch('/api/book' + '?currency=' + currency + "&type=" + type) fetch('/api/book' + '?currency=' + currency + "&type=" + type)
.then((response) => response.json()) .then((response) => response.json())
.then((data) => .then((data) => console.log(data) &
this.setState({ this.setState({
orders: data, orders: data,
not_found: data.not_found, not_found: data.not_found,
@ -28,7 +30,7 @@ export default class BookPage extends Component {
})); }));
} }
handleCardClick=(e)=>{ handleRowClick=(e)=>{
console.log(e) console.log(e)
this.props.history.push('/order/' + e); this.props.history.push('/order/' + e);
} }
@ -67,54 +69,59 @@ export default class BookPage extends Component {
return x.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ","); return x.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",");
} }
bookListItems=()=>{ bookListTable=()=>{
return (this.state.orders.map((order) => return (
<> <div style={{ height: 475, width: '100%' }}>
<ListItemButton value={order.id} onClick={() => this.handleCardClick(order.id)}> <DataGrid
rows={
this.state.orders.map((order) =>
({id: order.id,
avatar: window.location.origin +'/static/assets/avatars/' + order.maker_nick + '.png',
robosat: order.maker_nick,
type: order.type ? "Sell": "Buy",
amount: parseFloat(parseFloat(order.amount).toFixed(4)),
currency: this.getCurrencyCode(order.currency),
payment_method: order.payment_method,
price: order.price,
premium: order.premium,
})
)}
columns={[
// { field: 'id', headerName: 'ID', width: 40 },
{ field: 'robosat', headerName: 'RoboSat', width: 240,
renderCell: (params) => {return (
<ListItemButton style={{ cursor: "pointer" }}>
<ListItemAvatar> <ListItemAvatar>
<Avatar <Avatar alt={params.row.robosat} src={params.row.avatar} />
alt={order.maker_nick}
src={window.location.origin +'/static/assets/avatars/' + order.maker_nick + '.png'}
/>
</ListItemAvatar> </ListItemAvatar>
<ListItemText primary={params.row.robosat}/>
<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>
<ListItemText align='left'>
<Typography variant="subtitle1">
via <b>{order.payment_method}</b>
</Typography>
</ListItemText>
<ListItemText align='right'>
<Typography variant="subtitle1">
at <b>{this.pn(order.price) + " " + this.getCurrencyCode(order.currency)}/BTC</b>
</Typography>
</ListItemText>
<ListItemText align='right'>
<Typography variant="subtitle1">
{order.premium > 1 ? "🔴" : "🔵" } <b>{parseFloat(parseFloat(order.premium).toFixed(4))}%</b>
</Typography>
</ListItemText>
</ListItemButton> </ListItemButton>
);
} },
{ field: 'type', headerName: 'Type', width: 60 },
{ field: 'amount', headerName: 'Amount', type: 'number', width: 80 },
{ field: 'currency', headerName: 'Currency', width: 100,
renderCell: (params) => {return (
<div style={{ cursor: "pointer" }}>{params.row.currency + " " + getFlags(params.row.currency)}</div>
)} },
{ field: 'payment_method', headerName: 'Payment Method', width: 180 },
{ field: 'price', headerName: 'Price', type: 'number', width: 140,
renderCell: (params) => {return (
<div style={{ cursor: "pointer" }}>{this.pn(params.row.price) + " " +params.row.currency+ "/BTC" }</div>
)} },
{ field: 'premium', headerName: 'Premium', type: 'number', width: 100,
renderCell: (params) => {return (
<div style={{ cursor: "pointer" }}>{parseFloat(parseFloat(params.row.premium).toFixed(4))+"%" }</div>
)} },
]}
<Divider/> pageSize={7}
</> onRowClick={(params) => this.handleRowClick(params.row.id)} // Whole row is clickable, but the mouse only looks clickly in some places.
)); rowsPerPageOptions={[7]}
/>
</div>
);
} }
render() { render() {
@ -159,10 +166,10 @@ export default class BookPage extends Component {
style: {textAlign:"center"} style: {textAlign:"center"}
}} }}
onChange={this.handleCurrencyChange} onChange={this.handleCurrencyChange}
> <MenuItem value={0}>ANY</MenuItem> > <MenuItem value={0}>🌍 ANY</MenuItem>
{ {
Object.entries(this.state.currencies_dict) Object.entries(this.state.currencies_dict)
.map( ([key, value]) => <MenuItem value={parseInt(key)}>{value}</MenuItem> ) .map( ([key, value]) => <MenuItem value={parseInt(key)}>{getFlags(value) + " " + value}</MenuItem> )
} }
</Select> </Select>
</FormControl> </FormControl>
@ -196,10 +203,10 @@ export default class BookPage extends Component {
</Grid>) </Grid>)
: :
<Grid item xs={12} align="center"> <Grid item xs={12} align="center">
<Paper elevation={0} style={{width: 900, maxHeight: 500, overflow: 'auto'}}> <Paper elevation={0} style={{width: 910, maxHeight: 500, overflow: 'auto'}}>
<List >
{this.bookListItems()} {this.state.loading ? null : this.bookListTable()}
</List>
</Paper> </Paper>
</Grid> </Grid>
} }

View File

@ -0,0 +1,260 @@
import React, { Component } from 'react'
import {Paper, Grid, IconButton, Typography, Select, MenuItem, List, ListItemText, ListItem, ListItemIcon, ListItemButton, Divider, Dialog, DialogContent} from "@mui/material";
// Icons
import SettingsIcon from '@mui/icons-material/Settings';
import PeopleIcon from '@mui/icons-material/People';
import InventoryIcon from '@mui/icons-material/Inventory';
import SellIcon from '@mui/icons-material/Sell';
import SmartToyIcon from '@mui/icons-material/SmartToy';
import PercentIcon from '@mui/icons-material/Percent';
import PriceChangeIcon from '@mui/icons-material/PriceChange';
import BoltIcon from '@mui/icons-material/Bolt';
import GitHubIcon from '@mui/icons-material/GitHub';
import EqualizerIcon from '@mui/icons-material/Equalizer';
import SendIcon from '@mui/icons-material/Send';
import PublicIcon from '@mui/icons-material/Public';
export default class BottomBar extends Component {
constructor(props) {
super(props);
this.state = {
openStatsForNerds: false,
openCommuniy: false,
num_public_buy_orders: 0,
num_public_sell_orders: 0,
active_robots_today: 0,
fee: 0,
today_avg_nonkyc_btc_premium: 0,
today_total_volume: 0,
};
this.getInfo();
}
handleClickSuppport = () => {
window.open("https://t.me/robosats");
};
getInfo() {
this.setState(null)
fetch('/api/info/')
.then((response) => response.json())
.then((data) => {console.log(data) &
this.setState(data)
});
}
handleClickOpenStatsForNerds = () => {
this.setState({openStatsForNerds: true});
};
handleClickCloseStatsForNerds = () => {
this.setState({openStatsForNerds: false});
};
StatsDialog =() =>{
return(
<Dialog
open={this.state.openStatsForNerds}
onClose={this.handleClickCloseStatsForNerds}
aria-labelledby="stats-for-nerds-dialog-title"
aria-describedby="stats-for-nerds-description"
>
<DialogContent>
<Typography component="h5" variant="h5">Stats For Nerds</Typography>
<List>
<Divider/>
<ListItem>
<ListItemIcon><BoltIcon/></ListItemIcon>
<ListItemText primary={this.state.lnd_version} secondary="LND version"/>
</ListItem>
<Divider/>
<ListItem>
<ListItemIcon><GitHubIcon/></ListItemIcon>
<ListItemText secondary="Currently running commit height">
<a href={"https://github.com/Reckless-Satoshi/robosats/tree/"
+ this.state.robosats_running_commit_hash}>{this.state.robosats_running_commit_hash}
</a>
</ListItemText>
</ListItem>
<Divider/>
<ListItem>
<ListItemIcon><EqualizerIcon/></ListItemIcon>
<ListItemText primary={this.state.today_total_volume+" BTC"} secondary="Today traded volume"/>
</ListItem>
<Divider/>
<ListItem>
<ListItemIcon><PublicIcon/></ListItemIcon>
<ListItemText primary="Made with ❤️ and ⚡" secondary="... somewhere on Earth!"/>
</ListItem>
</List>
</DialogContent>
</Dialog>
)
}
handleClickOpenCommunity = () => {
this.setState({openCommuniy: true});
};
handleClickCloseCommunity = () => {
this.setState({openCommuniy: false});
};
CommunityDialog =() =>{
return(
<Dialog
open={this.state.openCommuniy}
onClose={this.handleClickCloseCommunity}
aria-labelledby="community-dialog-title"
aria-describedby="community-description"
>
<DialogContent>
<Typography component="h5" variant="h5">Community</Typography>
<Typography component="body2" variant="body2">
<p> Support is only offered via public channels.
Join our Telegram community if you have
questions or want to hang out with other cool robots.
Please, use our Github Issues if you find a bug or want
to see new features!
</p>
</Typography>
<List>
<Divider/>
<ListItemButton component="a" href="https://t.me/robosats">
<ListItemIcon><SendIcon/></ListItemIcon>
<ListItemText primary="Join the RoboSats group"
secondary="Telegram (English / Main)"/>
</ListItemButton>
<Divider/>
<ListItemButton component="a" href="https://t.me/robosats_es">
<ListItemIcon><SendIcon/></ListItemIcon>
<ListItemText primary="Unase al grupo RoboSats"
secondary="Telegram (Español)"/>
</ListItemButton>
<Divider/>
<ListItemButton component="a" href="https://github.com/Reckless-Satoshi/robosats/issues">
<ListItemIcon><GitHubIcon/></ListItemIcon>
<ListItemText primary="Tell us about a new feature or a bug"
secondary="Github Issues - The Robotic Satoshis Open Source Project"/>
</ListItemButton>
</List>
</DialogContent>
</Dialog>
)
}
render() {
return (
<Paper elevation={6} style={{height:40}}>
<this.StatsDialog/>
<this.CommunityDialog/>
<Grid container xs={12}>
<Grid item xs={1}>
<IconButton color="primary"
aria-label="Stats for Nerds"
onClick={this.handleClickOpenStatsForNerds} >
<SettingsIcon />
</IconButton>
</Grid>
<Grid item xs={2}>
<ListItem className="bottomItem">
<ListItemIcon size="small">
<InventoryIcon/>
</ListItemIcon>
<ListItemText
primaryTypographyProps={{fontSize: '14px'}}
secondaryTypographyProps={{fontSize: '12px'}}
primary={this.state.num_public_buy_orders}
secondary="Public Buy Orders" />
</ListItem>
</Grid>
<Grid item xs={2}>
<ListItem className="bottomItem">
<ListItemIcon size="small">
<SellIcon/>
</ListItemIcon>
<ListItemText
primaryTypographyProps={{fontSize: '14px'}}
secondaryTypographyProps={{fontSize: '12px'}}
primary={this.state.num_public_sell_orders}
secondary="Public Sell Orders" />
</ListItem>
</Grid>
<Grid item xs={2}>
<ListItem className="bottomItem">
<ListItemIcon size="small">
<SmartToyIcon/>
</ListItemIcon>
<ListItemText
primaryTypographyProps={{fontSize: '14px'}}
secondaryTypographyProps={{fontSize: '12px'}}
primary={this.state.active_robots_today}
secondary="Today Active Robots" />
</ListItem>
</Grid>
<Grid item xs={2}>
<ListItem className="bottomItem">
<ListItemIcon size="small">
<PriceChangeIcon/>
</ListItemIcon>
<ListItemText
primaryTypographyProps={{fontSize: '14px'}}
secondaryTypographyProps={{fontSize: '12px'}}
primary={this.state.today_avg_nonkyc_btc_premium+"%"}
secondary="Today Non-KYC Avg Premium" />
</ListItem>
</Grid>
<Grid item xs={2}>
<ListItem className="bottomItem">
<ListItemIcon size="small">
<PercentIcon/>
</ListItemIcon>
<ListItemText
primaryTypographyProps={{fontSize: '14px'}}
secondaryTypographyProps={{fontSize: '12px'}}
primary={this.state.fee*100}
secondary="Trading Fee" />
</ListItem>
</Grid>
<Grid container item xs={1}>
<Grid item xs={2}/>
<Grid item xs={6}>
<Select
size = 'small'
defaultValue={1}
inputProps={{
style: {textAlign:"center"}
}}>
<MenuItem value={1}>EN</MenuItem>
</Select>
</Grid>
<Grid item xs={4}>
<IconButton
color="primary"
aria-label="Telegram"
onClick={this.handleClickOpenCommunity} >
<PeopleIcon />
</IconButton>
</Grid>
</Grid>
</Grid>
</Paper>
)
}
}

View File

@ -13,15 +13,13 @@ export default class Chat extends Component {
state = { state = {
messages: [], messages: [],
value:'', value:'',
orderId: 2,
}; };
client = new W3CWebSocket('ws://' + window.location.host + '/ws/chat/' + this.props.data.orderId + '/'); client = new W3CWebSocket('ws://' + window.location.host + '/ws/chat/' + this.props.orderId + '/');
componentDidMount() { componentDidMount() {
this.client.onopen = () => { this.client.onopen = () => {
console.log('WebSocket Client Connected') console.log('WebSocket Client Connected')
console.log(this.props.data)
} }
this.client.onmessage = (message) => { this.client.onmessage = (message) => {
const dataFromServer = JSON.parse(message.data); const dataFromServer = JSON.parse(message.data);
@ -43,11 +41,19 @@ export default class Chat extends Component {
} }
} }
componentDidUpdate() {
this.scrollToBottom();
}
scrollToBottom = () => {
this.messagesEnd.scrollIntoView({ behavior: "smooth" });
}
onButtonClicked = (e) => { onButtonClicked = (e) => {
this.client.send(JSON.stringify({ this.client.send(JSON.stringify({
type: "message", type: "message",
message: this.state.value, message: this.state.value,
nick: this.props.data.urNick, nick: this.props.urNick,
})); }));
this.state.value = '' this.state.value = ''
e.preventDefault(); e.preventDefault();
@ -60,7 +66,7 @@ export default class Chat extends Component {
{this.state.messages.map(message => <> {this.state.messages.map(message => <>
<Card elevation={5} align="left" > <Card elevation={5} align="left" >
{/* If message sender is not our nick, gray color, if it is our nick, green color */} {/* If message sender is not our nick, gray color, if it is our nick, green color */}
{message.userNick == this.props.data.urNick ? {message.userNick == this.props.urNick ?
<CardHeader <CardHeader
avatar={ avatar={
<Avatar <Avatar
@ -86,6 +92,7 @@ export default class Chat extends Component {
/>} />}
</Card> </Card>
</>)} </>)}
<div style={{ float:"left", clear: "both" }} ref={(el) => { this.messagesEnd = el; }}></div>
</Paper> </Paper>
<form noValidate onSubmit={this.onButtonClicked}> <form noValidate onSubmit={this.onButtonClicked}>
<Grid containter alignItems="stretch" style={{ display: "flex" }}> <Grid containter alignItems="stretch" style={{ display: "flex" }}>

View File

@ -5,8 +5,6 @@ import UserGenPage from "./UserGenPage";
import MakerPage from "./MakerPage"; import MakerPage from "./MakerPage";
import BookPage from "./BookPage"; import BookPage from "./BookPage";
import OrderPage from "./OrderPage"; import OrderPage from "./OrderPage";
import InfoPage from "./InfoPage";
export default class HomePage extends Component { export default class HomePage extends Component {
constructor(props) { constructor(props) {
super(props); super(props);
@ -18,7 +16,6 @@ export default class HomePage extends Component {
<Switch> <Switch>
<Route exact path='/' component={UserGenPage}/> <Route exact path='/' component={UserGenPage}/>
<Route path='/home'><p>You are at the start page</p></Route> <Route path='/home'><p>You are at the start page</p></Route>
<Route path='/info' component={InfoPage}/>
<Route path='/make' component={MakerPage}/> <Route path='/make' component={MakerPage}/>
<Route path='/book' component={BookPage}/> <Route path='/book' component={BookPage}/>
<Route path="/order/:orderId" component={OrderPage}/> <Route path="/order/:orderId" component={OrderPage}/>

View File

@ -0,0 +1,125 @@
import {Typography, DialogTitle, DialogContent, DialogContentText, Button } from "@mui/material"
import React, { Component } from 'react'
export default class InfoDialog extends Component {
render() {
return (
<div>
<DialogContent>
<Typography component="h5" variant="h5">What is <i>RoboSats</i>?</Typography>
<Typography component="body2" variant="body2">
<p>It is a BTC/FIAT peer-to-peer exchange over lightning. It simplifies
matchmaking and minimizes the trust needed to trade with a peer.</p>
<p>RoboSats is an open source project <a
href='https://github.com/reckless-satoshi/robosats'>(GitHub).</a>
</p>
</Typography>
<Typography component="h5" variant="h5">How does it work?</Typography>
<Typography component="body2" variant="body2">
<p>AdequateAlice01 wants to sell bitcoin. She posts a sell order.
BafflingBob02 wants to buy bitcoin and he takes Alice's order.
Both have to post a small bond using lightning to prove they are real
robots. Then, Alice posts the trade collateral also using a lightning
hold invoice. <i>RoboSats</i> locks the invoice until Bob confirms he sent
the fiat to Alice. Once Alice confirms she received the fiat, she
tells <i>RoboSats</i> to release the satoshis to Bob. Enjoy your satoshis,
Bob!</p>
<p>At no point, AdequateAlice01 and BafflingBob02 have to trust the
bitcoin to each other. In case they have a conflict, <i>RoboSats</i> staff
will help resolving the dispute.</p>
</Typography>
<Typography component="h5" variant="h5">What payment methods are accepted?</Typography>
<Typography component="body2" variant="body2">
<p>Basically all of them. You can write down your preferred payment
method(s). You will have to match with a peer who also accepts
that method. Lightning is fast, so we highly recommend using instant
fiat payment rails. </p>
</Typography>
<Typography component="h5" variant="h5">Are there trade limits?</Typography>
<Typography component="body2" variant="body2">
<p>Maximum single trade size is 500,000 Satoshis to minimize lightning
routing failure. There is no limits to the number of trades per day. A robot
can only have one order at a time. However, you can use multiple
robots simultatenously in different browsers (remember to back up your robot tokens!). </p>
</Typography>
<Typography component="h5" variant="h5">Is <i>RoboSats</i> private?</Typography>
<Typography component="body2" variant="body2">
<p> RoboSats will never ask you for your name, country or ID. For
best anonymity use Tor Browser and access the .onion hidden service. </p>
<p>Your trading peer is the only one who can potentially guess
anything about you. Keep your chat short and concise. Avoid
providing non-essential information other than strictly necessary
for the fiat payment. </p>
</Typography>
<Typography component="h5" variant="h5">What are the risks?</Typography>
<Typography component="body2" variant="body2">
<p> This is an experimental application, things could go wrong.
Trade small amounts! </p>
<p>The seller faces the same chargeback risk as with any
other peer-to-peer service. Paypal or credit cards are
not recommened.</p>
</Typography>
<Typography component="h5" variant="h5">What is the trust model?</Typography>
<Typography component="body2" variant="body2">
<p> The buyer and the seller never have to trust each other.
Some trust on <i>RoboSats</i> staff is needed since linking
the seller's hold invoice and buyer payment is not atomic (yet).
In addition, disputes are solved by the <i>RoboSats</i> staff.
</p>
<p> While trust requirements are minimized, <i>RoboSats</i> could
run away with your satoshis. It could be argued that it is not
worth it, as it would instantly destroy <i>RoboSats</i> reputation.
However, you should hesitate and only trade small quantities at a
time. For large amounts use an onchain escrow service such as <i>Bisq</i>
</p>
<p> You can build more trust on <i>RoboSats</i> by <a href='https://github.com/reckless-satoshi/robosats'>
inspecting the source code </a> </p>
</Typography>
<Typography component="h5" variant="h5">What happens if <i>RoboSats</i> suddenly disapears?</Typography>
<Typography component="body2" variant="body2">
<p> Your sats will most likely return to you. Any hold invoice that is not
settled would be automatically returned even if <i>RoboSats</i> goes down
forever. This is true for both, locked bonds and trading escrows. However,
there is a small window between the buyer confirms FIAT SENT and the moment
the seller releases the satoshis when the funds could be lost.
</p>
</Typography>
<Typography component="h5" variant="h5">Is <i>RoboSats</i> legal in my country?</Typography>
<Typography component="body2" variant="body2">
<p> In many countries using <i>RoboSats</i> is no different than using Ebay
or Craiglist. Your regulation may vary. It is your responsibility
to comply.
</p>
</Typography>
<Typography component="h5" variant="h5">Disclaimer</Typography>
<Typography component="body2" variant="body2">
<p> This lightning application is provided as is. It is in active
development: trade with the utmost caution. There is no private
support. Support is only offered via public channels <a href='https://t.me/robosats'>
(Telegram)</a>. <i>RoboSats</i> will never contact you. <i>
RoboSats</i> will definitely never ask for your robot token.
</p>
</Typography>
</DialogContent>
</div>
)
}
}

View File

@ -1,38 +0,0 @@
import ReactMarkdown from 'react-markdown'
import {Paper, Grid, CircularProgress, Button, Link} from "@mui/material"
import React, { Component } from 'react'
export default class InfoPage extends Component {
constructor(props) {
super(props);
this.state = {
info: null,
loading: true,
};
this.getInfoMarkdown()
}
getInfoMarkdown() {
fetch('/static/assets/info.md')
.then((response) => response.text())
.then((data) => this.setState({info:data, loading:false}));
}
render() {
return (
<Grid container spacing={1}>
{this.state.loading ? <Grid item xs={12} align="center">
<CircularProgress />
</Grid> : ""}
<Paper elevation={12} style={{ padding: 10, width: 900, maxHeight: 500, overflow: 'auto'}}>
<ReactMarkdown children={this.state.info} />
</Paper>
<Grid item xs={12} align="center">
<Button color="secondary" variant="contained" to="/" component={Link}>
Back
</Button>
</Grid>
</Grid>
)
}
}

View File

@ -2,6 +2,8 @@ import React, { Component } from 'react';
import { Paper, Alert, AlertTitle, Button , Grid, Typography, TextField, Select, FormHelperText, MenuItem, FormControl, Radio, FormControlLabel, RadioGroup, Menu} from "@mui/material" 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' import { Link } from 'react-router-dom'
import getFlags from './getFlags'
function getCookie(name) { function getCookie(name) {
let cookieValue = null; let cookieValue = null;
if (document.cookie && document.cookie !== '') { if (document.cookie && document.cookie !== '') {
@ -190,7 +192,9 @@ export default class MakerPage extends Component {
> >
{ {
Object.entries(this.state.currencies_dict) Object.entries(this.state.currencies_dict)
.map( ([key, value]) => <MenuItem value={parseInt(key)}>{value}</MenuItem> ) .map( ([key, value]) => <MenuItem value={parseInt(key)}>
{getFlags(value) + " " + value}
</MenuItem> )
} }
</Select> </Select>

View File

@ -1,44 +1,17 @@
import React, { Component } from "react"; import React, { Component } from "react";
import { Alert, Paper, CircularProgress, Button , Grid, Typography, List, ListItem, ListItemText, ListItemAvatar, Avatar, Divider, Box, LinearProgress} from "@mui/material" import { Alert, Paper, CircularProgress, Button , Grid, Typography, List, ListItem, ListItemIcon, ListItemText, ListItemAvatar, Avatar, Divider, Box, LinearProgress, Dialog, DialogActions, DialogContent, DialogContentText, DialogTitle} from "@mui/material"
import Countdown, { zeroPad, calcTimeDelta } from 'react-countdown';
import TradeBox from "./TradeBox"; import TradeBox from "./TradeBox";
import getFlags from './getFlags'
function msToTime(duration) { // icons
var seconds = Math.floor((duration / 1000) % 60), import AccessTimeIcon from '@mui/icons-material/AccessTime';
minutes = Math.floor((duration / (1000 * 60)) % 60), import NumbersIcon from '@mui/icons-material/Numbers';
hours = Math.floor((duration / (1000 * 60 * 60)) % 24); import PriceChangeIcon from '@mui/icons-material/PriceChange';
import PaymentsIcon from '@mui/icons-material/Payments';
minutes = (minutes < 10) ? "0" + minutes : minutes; import MoneyIcon from '@mui/icons-material/Money';
seconds = (seconds < 10) ? "0" + seconds : seconds; import ArticleIcon from '@mui/icons-material/Article';
import ContentCopy from "@mui/icons-material/ContentCopy";
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) { function getCookie(name) {
let cookieValue = null; let cookieValue = null;
@ -67,12 +40,37 @@ export default class OrderPage extends Component {
super(props); super(props);
this.state = { this.state = {
isExplicit: false, isExplicit: false,
delay: 2000, // Refresh every 2 seconds by default delay: 60000, // Refresh every 60 seconds by default
currencies_dict: {"1":"USD"} currencies_dict: {"1":"USD"},
total_secs_expiry: 300,
loading: true,
openCancel: false,
}; };
this.orderId = this.props.match.params.orderId; this.orderId = this.props.match.params.orderId;
this.getCurrencyDict(); this.getCurrencyDict();
this.getOrderDetails(); this.getOrderDetails();
// Refresh delais according to Order status
this.statusToDelay = {
"0": 3000, //'Waiting for maker bond'
"1": 30000, //'Public'
"2": 9999999, //'Deleted'
"3": 3000, //'Waiting for taker bond'
"4": 9999999, //'Cancelled'
"5": 999999, //'Expired'
"6": 3000, //'Waiting for trade collateral and buyer invoice'
"7": 3000, //'Waiting only for seller trade collateral'
"8": 10000, //'Waiting only for buyer invoice'
"9": 10000, //'Sending fiat - In chatroom'
"10": 15000, //'Fiat sent - In chatroom'
"11": 60000, //'In dispute'
"12": 9999999,//'Collaboratively cancelled'
"13": 3000, //'Sending satoshis to buyer'
"14": 9999999,//'Sucessful trade'
"15": 10000, //'Failed lightning network routing'
"16": 9999999,//'Maker lost dispute'
"17": 9999999,//'Taker lost dispute'
}
} }
getOrderDetails() { getOrderDetails() {
@ -81,6 +79,8 @@ export default class OrderPage extends Component {
.then((response) => response.json()) .then((response) => response.json())
.then((data) => {console.log(data) & .then((data) => {console.log(data) &
this.setState({ this.setState({
loading: false,
delay: this.setDelay(data.status),
id: data.id, id: data.id,
statusCode: data.status, statusCode: data.status,
statusText: data.status_message, statusText: data.status_message,
@ -110,6 +110,13 @@ export default class OrderPage extends Component {
escrowInvoice: data.escrow_invoice, escrowInvoice: data.escrow_invoice,
escrowSatoshis: data.escrow_satoshis, escrowSatoshis: data.escrow_satoshis,
invoiceAmount: data.invoice_amount, invoiceAmount: data.invoice_amount,
total_secs_expiry: data.total_secs_exp,
numSimilarOrders: data.num_similar_orders,
priceNow: data.price_now,
premiumNow: data.premium_now,
robotsInBook: data.robots_in_book,
premiumPercentile: data.premium_percentile,
numSimilarOrders: data.num_similar_orders
}) })
}); });
} }
@ -118,12 +125,11 @@ export default class OrderPage extends Component {
componentDidMount() { componentDidMount() {
this.interval = setInterval(this.tick, this.state.delay); this.interval = setInterval(this.tick, this.state.delay);
} }
componentDidUpdate(prevProps, prevState) { componentDidUpdate() {
if (prevState.delay !== this.state.delay) {
clearInterval(this.interval); clearInterval(this.interval);
this.interval = setInterval(this.tick, this.state.delay); this.interval = setInterval(this.tick, this.state.delay);
} }
}
componentWillUnmount() { componentWillUnmount() {
clearInterval(this.interval); clearInterval(this.interval);
} }
@ -136,6 +142,50 @@ export default class OrderPage extends Component {
window.history.back(); window.history.back();
} }
// Countdown Renderer callback with condition
countdownRenderer = ({ total, hours, minutes, seconds, completed }) => {
if (completed) {
// Render a completed state
return (<span> The order has expired</span>);
} else {
var col = 'black'
var fraction_left = (total/1000) / this.state.total_secs_expiry
// Make orange at 25% of time left
if (fraction_left < 0.25){col = 'orange'}
// Make red at 10% of time left
if (fraction_left < 0.1){col = 'red'}
// Render a countdown, bold when less than 25%
return (
fraction_left < 0.25 ? <b><span style={{color:col}}>{hours}h {zeroPad(minutes)}m {zeroPad(seconds)}s </span></b>
:<span style={{color:col}}>{hours}h {zeroPad(minutes)}m {zeroPad(seconds)}s </span>
);
}
};
LinearDeterminate =()=> {
const [progress, setProgress] = React.useState(0);
React.useEffect(() => {
const timer = setInterval(() => {
setProgress((oldProgress) => {
var left = calcTimeDelta( new Date(this.state.expiresAt)).total /1000;
return (left / this.state.total_secs_expiry) * 100;
});
}, 1000);
return () => {
clearInterval(timer);
};
}, []);
return (
<Box sx={{ width: '100%' }}>
<LinearProgress variant="determinate" value={progress} />
</Box>
);
}
handleClickTakeOrderButton=()=>{ handleClickTakeOrderButton=()=>{
console.log(this.state) console.log(this.state)
const requestOptions = { const requestOptions = {
@ -160,12 +210,17 @@ export default class OrderPage extends Component {
})); }));
} }
// set delay to the one matching the order status. If null order status, delay goes to 9999999.
setDelay = (status)=>{
return status >= 0 ? this.statusToDelay[status.toString()] : 99999999;
}
getCurrencyCode(val){ getCurrencyCode(val){
let code = val ? this.state.currencies_dict[val.toString()] : "" let code = val ? this.state.currencies_dict[val.toString()] : ""
return code return code
} }
handleClickCancelOrderButton=()=>{ handleClickConfirmCancelButton=()=>{
console.log(this.state) console.log(this.state)
const requestOptions = { const requestOptions = {
method: 'POST', method: 'POST',
@ -177,6 +232,64 @@ export default class OrderPage extends Component {
fetch('/api/order/' + '?order_id=' + this.orderId, requestOptions) fetch('/api/order/' + '?order_id=' + this.orderId, requestOptions)
.then((response) => response.json()) .then((response) => response.json())
.then((data) => (console.log(data) & this.getOrderDetails(data.id))); .then((data) => (console.log(data) & this.getOrderDetails(data.id)));
this.handleClickCloseConfirmCancelDialog();
}
handleClickOpenConfirmCancelDialog = () => {
this.setState({openCancel: true});
};
handleClickCloseConfirmCancelDialog = () => {
this.setState({openCancel: false});
};
CancelDialog =() =>{
return(
<Dialog
open={this.state.openCancel}
onClose={this.handleClickCloseConfirmCancelDialog}
aria-labelledby="cancel-dialog-title"
aria-describedby="cancel-dialog-description"
>
<DialogTitle id="cancel-dialog-title">
{"Cancel the order?"}
</DialogTitle>
<DialogContent>
<DialogContentText id="cancel-dialog-description">
If the order is cancelled now you will lose your bond.
</DialogContentText>
</DialogContent>
<DialogActions>
<Button onClick={this.handleClickCloseConfirmCancelDialog} autoFocus>Go back</Button>
<Button onClick={this.handleClickConfirmCancelButton}> Confirm Cancel </Button>
</DialogActions>
</Dialog>
)
}
CancelButton = () => {
// If maker and Waiting for Bond. Or if taker and Waiting for bond.
// Simply allow to cancel without showing the cancel dialog.
if ((this.state.isMaker & this.state.statusCode == 0) || this.state.isTaker & this.state.statusCode == 3){
return(
<Grid item xs={12} align="center">
<Button variant='contained' color='secondary' onClick={this.handleClickConfirmCancelButton}>Cancel</Button>
</Grid>
)}
// If the order does not yet have an escrow deposited. Show dialog
// to confirm forfeiting the bond
if (this.state.statusCode < 8){
return(
<Grid item xs={12} align="center">
<this.CancelDialog/>
<Button variant='contained' color='secondary' onClick={this.handleClickOpenConfirmCancelDialog}>Cancel</Button>
</Grid>
)}
// TODO If the escrow is Locked, show the collaborative cancel button.
// If none of the above do not return a cancel button.
return(null)
} }
orderBox=()=>{ orderBox=()=>{
@ -184,7 +297,7 @@ export default class OrderPage extends Component {
<Grid container spacing={1}> <Grid container spacing={1}>
<Grid item xs={12} align="center"> <Grid item xs={12} align="center">
<Typography component="h5" variant="h5"> <Typography component="h5" variant="h5">
{this.state.type ? "Sell " : "Buy "} Order Details Order Details
</Typography> </Typography>
<Paper elevation={12} style={{ padding: 8,}}> <Paper elevation={12} style={{ padding: 8,}}>
<List dense="true"> <List dense="true">
@ -217,6 +330,9 @@ export default class OrderPage extends Component {
"" ""
} }
<ListItem> <ListItem>
<ListItemIcon>
<ArticleIcon/>
</ListItemIcon>
<ListItemText primary={this.state.statusText} secondary="Order status"/> <ListItemText primary={this.state.statusText} secondary="Order status"/>
</ListItem> </ListItem>
<Divider /> <Divider />
@ -225,30 +341,54 @@ export default class OrderPage extends Component {
} }
<ListItem> <ListItem>
<ListItemText primary={parseFloat(parseFloat(this.state.amount).toFixed(4))+" "+this.state.currencyCode} secondary="Amount"/> <ListItemIcon>
{getFlags(this.state.currencyCode)}
</ListItemIcon>
<ListItemText primary={parseFloat(parseFloat(this.state.amount).toFixed(4))
+" "+this.state.currencyCode} secondary="Amount"/>
</ListItem> </ListItem>
<Divider /> <Divider />
<ListItem> <ListItem>
<ListItemIcon>
<PaymentsIcon/>
</ListItemIcon>
<ListItemText primary={this.state.paymentMethod} secondary="Accepted payment methods"/> <ListItemText primary={this.state.paymentMethod} secondary="Accepted payment methods"/>
</ListItem> </ListItem>
<Divider /> <Divider />
{/* If there is live Price and Premium data, show it. Otherwise show the order maker settings */}
<ListItem> <ListItem>
{this.state.isExplicit ? <ListItemIcon>
<PriceChangeIcon/>
</ListItemIcon>
{this.state.priceNow?
<ListItemText primary={pn(this.state.priceNow)+" "+this.state.currencyCode+"/BTC - Premium: "+this.state.premiumNow+"%"} secondary="Price and Premium"/>
:
(this.state.isExplicit ?
<ListItemText primary={pn(this.state.satoshis)} secondary="Amount of Satoshis"/> <ListItemText primary={pn(this.state.satoshis)} secondary="Amount of Satoshis"/>
: :
<ListItemText primary={parseFloat(parseFloat(this.state.premium).toFixed(2))+"%"} secondary="Premium over market price"/> <ListItemText primary={parseFloat(parseFloat(this.state.premium).toFixed(2))+"%"} secondary="Premium over market price"/>
)
} }
</ListItem> </ListItem>
<Divider /> <Divider />
<ListItem> <ListItem>
<ListItemText primary={'#'+this.orderId} secondary="Order ID"/> <ListItemIcon>
<NumbersIcon/>
</ListItemIcon>
<ListItemText primary={this.orderId} secondary="Order ID"/>
</ListItem> </ListItem>
<Divider /> <Divider />
<ListItem> <ListItem>
<ListItemText primary={msToTime( new Date(this.state.expiresAt) - Date.now())} secondary="Expires"/> <ListItemIcon>
<AccessTimeIcon/>
</ListItemIcon>
<ListItemText secondary="Expires in">
<Countdown date={new Date(this.state.expiresAt)} renderer={this.countdownRenderer} />
</ListItemText>
</ListItem> </ListItem>
<LinearDeterminate /> <this.LinearDeterminate />
</List> </List>
{/* If the user has a penalty/limit */} {/* If the user has a penalty/limit */}
@ -266,8 +406,10 @@ export default class OrderPage extends Component {
</Paper> </Paper>
</Grid> </Grid>
{/* Participants cannot see the Back or Take Order buttons */} {/* Participants can see the "Cancel" Button, but cannot see the "Back" or "Take Order" buttons */}
{this.state.isParticipant ? "" : {this.state.isParticipant ?
<this.CancelButton/>
:
<> <>
<Grid item xs={12} align="center"> <Grid item xs={12} align="center">
<Button variant='contained' color='primary' onClick={this.handleClickTakeOrderButton}>Take Order</Button> <Button variant='contained' color='primary' onClick={this.handleClickTakeOrderButton}>Take Order</Button>
@ -278,26 +420,6 @@ export default class OrderPage extends Component {
</> </>
} }
{/* Makers can cancel before trade escrow deposited (status <9)*/}
{/* Only free cancel before bond locked (status 0)*/}
{this.state.isMaker & this.state.statusCode < 9 ?
<Grid item xs={12} align="center">
<Button variant='contained' color='secondary' onClick={this.handleClickCancelOrderButton}>Cancel</Button>
</Grid>
:""}
{this.state.isMaker & this.state.statusCode > 0 & this.state.statusCode < 9 ?
<Grid item xs={12} align="center">
<Typography color="secondary" variant="subtitle2" component="subtitle2">Cancelling now forfeits the maker bond</Typography>
</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>
) )
} }
@ -331,7 +453,7 @@ export default class OrderPage extends Component {
render (){ render (){
return ( return (
// Only so nothing shows while requesting the first batch of data // Only so nothing shows while requesting the first batch of data
(this.state.statusCode == null & this.state.badRequest == null) ? <CircularProgress /> : this.orderDetailsPage() this.state.loading ? <CircularProgress /> : this.orderDetailsPage()
); );
} }
} }

View File

@ -1,9 +1,16 @@
import React, { Component } from "react"; import React, { Component } from "react";
import { Link, Paper, Rating, Button, Grid, Typography, TextField, List, ListItem, ListItemText, Divider} from "@mui/material" import { Link, Paper, Rating, Button, Grid, Typography, TextField, List, ListItem, ListItemText, Divider, ListItemIcon, Dialog, DialogActions, DialogContent, DialogContentText, DialogTitle} from "@mui/material"
import QRCode from "react-qr-code"; import QRCode from "react-qr-code";
import Chat from "./Chat" import Chat from "./Chat"
// Icons
import SmartToyIcon from '@mui/icons-material/SmartToy';
import PercentIcon from '@mui/icons-material/Percent';
import BookIcon from '@mui/icons-material/Book';
function getCookie(name) { function getCookie(name) {
let cookieValue = null; let cookieValue = null;
if (document.cookie && document.cookie !== '') { if (document.cookie && document.cookie !== '') {
@ -30,10 +37,100 @@ export default class TradeBox extends Component {
constructor(props) { constructor(props) {
super(props); super(props);
this.state = { this.state = {
openConfirmFiatReceived: false,
openConfirmDispute: false,
badInvoice: false, badInvoice: false,
badStatement: false,
} }
} }
handleClickOpenConfirmDispute = () => {
this.setState({openConfirmDispute: true});
};
handleClickCloseConfirmDispute = () => {
this.setState({openConfirmDispute: false});
};
handleClickAgreeDisputeButton=()=>{
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));
this.handleClickCloseConfirmDispute();
}
ConfirmDisputeDialog =() =>{
return(
<Dialog
open={this.state.openConfirmDispute}
onClose={this.handleClickCloseConfirmDispute}
aria-labelledby="open-dispute-dialog-title"
aria-describedby="open-dispute-dialog-description"
>
<DialogTitle id="open-dispute-dialog-title">
{"Do you want to open a dispute?"}
</DialogTitle>
<DialogContent>
<DialogContentText id="alert-dialog-description">
The RoboSats staff will examine the statements and evidence provided by the participants.
It is best if you provide a burner contact method on your statement for the staff to contact you.
The satoshis in the trade escrow will be sent to the dispute winner, while the dispute
loser will lose the bond.
</DialogContentText>
</DialogContent>
<DialogActions>
<Button onClick={this.handleClickCloseConfirmDispute} autoFocus>Disagree</Button>
<Button onClick={this.handleClickAgreeDisputeButton}> Agree </Button>
</DialogActions>
</Dialog>
)
}
handleClickOpenConfirmFiatReceived = () => {
this.setState({openConfirmFiatReceived: true});
};
handleClickCloseConfirmFiatReceived = () => {
this.setState({openConfirmFiatReceived: false});
};
handleClickTotallyConfirmFiatReceived = () =>{
this.handleClickConfirmButton();
this.handleClickCloseConfirmFiatReceived();
};
ConfirmFiatReceivedDialog =() =>{
return(
<Dialog
open={this.state.openConfirmFiatReceived}
onClose={this.handleClickCloseConfirmFiatReceived}
aria-labelledby="fiat-received-dialog-title"
aria-describedby="fiat-received-dialog-description"
>
<DialogTitle id="open-dispute-dialog-title">
{"Confirm you received " +this.props.data.currencyCode+ "?"}
</DialogTitle>
<DialogContent>
<DialogContentText id="alert-dialog-description">
Confirming that you received the fiat will finalize the trade. The satoshis
in the escrow will be released to the buyer. Only confirm after the {this.props.data.currencyCode+ " "}
has arrived to your account. In addition, if you have received {this.props.data.currencyCode+ " "}
and do not confirm the receipt, you risk losing your bond.
</DialogContentText>
</DialogContent>
<DialogActions>
<Button onClick={this.handleClickCloseConfirmFiatReceived} autoFocus>Go back</Button>
<Button onClick={this.handleClickTotallyConfirmFiatReceived}> Confirm </Button>
</DialogActions>
</Dialog>
)
}
showQRInvoice=()=>{ showQRInvoice=()=>{
return ( return (
<Grid container spacing={1}> <Grid container spacing={1}>
@ -60,14 +157,12 @@ export default class TradeBox extends Component {
<Grid item xs={12} align="center"> <Grid item xs={12} align="center">
<TextField <TextField
hiddenLabel hiddenLabel
variant="filled" variant="standard"
size="small" size="small"
defaultValue={this.props.data.bondInvoice} defaultValue={this.props.data.bondInvoice}
disabled="true" disabled="true"
helperText="This is a hold invoice. It will simply freeze in your wallet. helperText="This is a hold invoice. It will be charged only if you cancel or lose a dispute."
It will be charged only if you cancel the order or lose a dispute."
color = "secondary" color = "secondary"
onClick = {this.copyCodeToClipboard}
/> />
</Grid> </Grid>
</Grid> </Grid>
@ -78,7 +173,7 @@ export default class TradeBox extends Component {
return ( return (
<Grid item xs={12} align="center"> <Grid item xs={12} align="center">
<Typography color="primary" component="subtitle1" variant="subtitle1" align="center"> <Typography color="primary" component="subtitle1" variant="subtitle1" align="center">
🔒 Your {this.props.data.isMaker ? 'maker' : 'taker'} bond is safely locked 🔒 Your {this.props.data.isMaker ? 'maker' : 'taker'} bond is locked
</Typography> </Typography>
</Grid> </Grid>
); );
@ -88,7 +183,7 @@ export default class TradeBox extends Component {
return ( return (
<Grid container spacing={1}> <Grid container spacing={1}>
<Grid item xs={12} align="center"> <Grid item xs={12} align="center">
<Typography color="primary" component="subtitle1" variant="subtitle1"> <Typography color="green" component="subtitle1" variant="subtitle1">
<b>Deposit {pn(this.props.data.escrowSatoshis)} Sats as trade collateral </b> <b>Deposit {pn(this.props.data.escrowSatoshis)} Sats as trade collateral </b>
</Typography> </Typography>
</Grid> </Grid>
@ -103,7 +198,7 @@ export default class TradeBox extends Component {
size="small" size="small"
defaultValue={this.props.data.escrowInvoice} defaultValue={this.props.data.escrowInvoice}
disabled="true" disabled="true"
helperText="This is a hold invoice. It will simply freeze in your wallet. It will be charged once the buyer confirms he sent the fiat." helperText="This is a hold invoice. It will be charged once the buyer confirms he sent the fiat."
color = "secondary" color = "secondary"
/> />
</Grid> </Grid>
@ -158,17 +253,27 @@ export default class TradeBox extends Component {
{/* TODO API sends data for a more confortable wait */} {/* TODO API sends data for a more confortable wait */}
<Divider/> <Divider/>
<ListItem> <ListItem>
<ListItemText primary={999} secondary="Robots looking at the book"/> <ListItemIcon>
<SmartToyIcon/>
</ListItemIcon>
<ListItemText primary={'000 coming soon'} secondary="Robots looking at the book"/>
</ListItem> </ListItem>
<Divider/> <Divider/>
<ListItem> <ListItem>
<ListItemText primary={999} secondary={"Active orders for " + this.props.data.currencyCode}/> <ListItemIcon>
<BookIcon/>
</ListItemIcon>
<ListItemText primary={this.props.data.numSimilarOrders} secondary={"Public orders for " + this.props.data.currencyCode}/>
</ListItem> </ListItem>
<Divider/> <Divider/>
<ListItem> <ListItem>
<ListItemText primary="33%" secondary="Premium percentile" /> <ListItemIcon>
<PercentIcon/>
</ListItemIcon>
<ListItemText primary={"Premium rank " + this.props.data.premiumPercentile*100+"%"}
secondary={"Among public " + this.props.data.currencyCode + " orders (higher is cheaper)"} />
</ListItem> </ListItem>
<Divider/> <Divider/>
@ -186,8 +291,6 @@ export default class TradeBox extends Component {
}); });
} }
// Fix this. It's clunky because it takes time. this.props.data does not refresh until next refresh of OrderPage.
handleClickSubmitInvoiceButton=()=>{ handleClickSubmitInvoiceButton=()=>{
this.setState({badInvoice:false}); this.setState({badInvoice:false});
@ -205,10 +308,34 @@ export default class TradeBox extends Component {
& console.log(data)); & console.log(data));
} }
handleInputDisputeChanged=(e)=>{
this.setState({
statement: e.target.value,
badStatement: false,
});
}
handleClickSubmitStatementButton=()=>{
this.setState({badInvoice:false});
const requestOptions = {
method: 'POST',
headers: {'Content-Type':'application/json', 'X-CSRFToken': getCookie('csrftoken'),},
body: JSON.stringify({
'action':'submit_statement',
'statement': this.state.statement,
}),
};
fetch('/api/order/' + '?order_id=' + this.props.data.id, requestOptions)
.then((response) => response.json())
.then((data) => this.setState({badStatement:data.bad_statement})
& console.log(data));
}
showInputInvoice(){ showInputInvoice(){
return ( return (
// TODO Camera option to read QR // TODO Option to upload files and images
<Grid container spacing={1}> <Grid container spacing={1}>
<Grid item xs={12} align="center"> <Grid item xs={12} align="center">
@ -223,7 +350,7 @@ export default class TradeBox extends Component {
valid invoice for {pn(this.props.data.invoiceAmount)} Satoshis. valid invoice for {pn(this.props.data.invoiceAmount)} Satoshis.
</Typography> </Typography>
</Grid> </Grid>
<form noValidate onSubmit={this.handleClickSubmitInvoiceButton}>
<Grid item xs={12} align="center"> <Grid item xs={12} align="center">
<TextField <TextField
error={this.state.badInvoice} error={this.state.badInvoice}
@ -238,9 +365,53 @@ export default class TradeBox extends Component {
/> />
</Grid> </Grid>
<Grid item xs={12} align="center"> <Grid item xs={12} align="center">
<Button variant='contained' color='primary'>Submit</Button> <Button onClick={this.handleClickSubmitInvoiceButton} variant='contained' color='primary'>Submit</Button>
</Grid> </Grid>
</form>
{this.showBondIsLocked()}
</Grid>
)
}
// Asks the user for a dispute statement.
showInDisputeStatement(){
return (
// TODO Option to upload files
<Grid container spacing={1}>
<Grid item xs={12} align="center">
<Typography color="primary" component="subtitle1" variant="subtitle1">
<b> A dispute has been opened </b>
</Typography>
</Grid>
<Grid item xs={12} align="left">
<Typography component="body2" variant="body2">
Please, submit your statement. Be clear and specific about what happened and provide the necessary
evidence. It is best to provide a burner email, XMPP or telegram username to follow up with the staff.
Disputes are solved at the discretion of real robots <i>(aka humans)</i>, so be as helpful
as possible to ensure a fair outcome. Max 5000 chars.
</Typography>
</Grid>
<Grid item xs={12} align="center">
<TextField
error={this.state.badStatement}
helperText={this.state.badStatement ? this.state.badStatement : "" }
label={"Submit dispute statement"}
required
inputProps={{
style: {textAlign:"center"}
}}
multiline
rows={4}
onChange={this.handleInputDisputeChanged}
/>
</Grid>
<Grid item xs={12} align="center">
<Button onClick={this.handleClickSubmitStatementButton} variant='contained' color='primary'>Submit</Button>
</Grid>
{this.showBondIsLocked()} {this.showBondIsLocked()}
</Grid> </Grid>
) )
@ -251,7 +422,7 @@ export default class TradeBox extends Component {
<Grid container spacing={1}> <Grid container spacing={1}>
<Grid item xs={12} align="center"> <Grid item xs={12} align="center">
<Typography component="subtitle1" variant="subtitle1"> <Typography component="subtitle1" variant="subtitle1">
<b>Your invoice looks good!</b> <b>Your invoice looks good!🎉</b>
</Typography> </Typography>
</Grid> </Grid>
<Grid item xs={12} align="center"> <Grid item xs={12} align="center">
@ -272,7 +443,7 @@ export default class TradeBox extends Component {
<Grid container spacing={1}> <Grid container spacing={1}>
<Grid item xs={12} align="center"> <Grid item xs={12} align="center">
<Typography component="subtitle1" variant="subtitle1"> <Typography component="subtitle1" variant="subtitle1">
<b>The trade collateral is locked! :D </b> <b>The trade collateral is locked! 🎉 </b>
</Typography> </Typography>
</Grid> </Grid>
<Grid item xs={12} align="center"> <Grid item xs={12} align="center">
@ -301,18 +472,7 @@ export default class TradeBox extends Component {
.then((response) => response.json()) .then((response) => response.json())
.then((data) => (this.props.data = data)); .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));
}
handleRatingChange=(e)=>{ handleRatingChange=(e)=>{
const requestOptions = { const requestOptions = {
method: 'POST', method: 'POST',
@ -338,11 +498,9 @@ handleRatingChange=(e)=>{
} }
showFiatReceivedButton(){ 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( return(
<Grid item xs={12} align="center"> <Grid item xs={12} align="center">
<Button defaultValue="confirm" variant='contained' color='secondary' onClick={this.handleClickConfirmButton}>Confirm {this.props.data.currencyCode} received</Button> <Button defaultValue="confirm" variant='contained' color='secondary' onClick={this.handleClickOpenConfirmFiatReceived}>Confirm {this.props.data.currencyCode} received</Button>
</Grid> </Grid>
) )
} }
@ -351,7 +509,19 @@ handleRatingChange=(e)=>{
// TODO, show alert about how opening a dispute might involve giving away personal data and might mean losing the bond. Ask for double confirmation. // 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( return(
<Grid item xs={12} align="center"> <Grid item xs={12} align="center">
<Button color="inherit" onClick={this.handleClickOpenDisputeButton}>Open Dispute</Button> <Button color="inherit" onClick={this.handleClickOpenConfirmDispute}>Open Dispute</Button>
</Grid>
)
}
showOrderExpired(){
return(
<Grid container spacing={1}>
<Grid item xs={12} align="center">
<Typography component="subtitle1" variant="subtitle1">
<b>The order has expired</b>
</Typography>
</Grid>
</Grid> </Grid>
) )
} }
@ -367,7 +537,7 @@ handleRatingChange=(e)=>{
<Grid item xs={12} align="center"> <Grid item xs={12} align="center">
{this.props.data.isSeller ? {this.props.data.isSeller ?
<Typography component="body2" variant="body2" align="center"> <Typography component="body2" variant="body2" align="center">
Say hi! Be helpful and concise. Let him know how to send you {this.props.data.currencyCode}. Say hi! Be helpful and concise. Let them know how to send you {this.props.data.currencyCode}.
</Typography> </Typography>
: :
<Typography component="body2" variant="body2" align="center"> <Typography component="body2" variant="body2" align="center">
@ -377,7 +547,7 @@ handleRatingChange=(e)=>{
<Divider/> <Divider/>
</Grid> </Grid>
<Chat data={this.props.data}/> <Chat orderId={this.props.data.id} urNick={this.props.data.urNick}/>
<Grid item xs={12} align="center"> <Grid item xs={12} align="center">
{openDisputeButton ? this.showOpenDisputeButton() : ""} {openDisputeButton ? this.showOpenDisputeButton() : ""}
@ -406,19 +576,20 @@ handleRatingChange=(e)=>{
<Rating name="size-large" defaultValue={2} size="large" onChange={this.handleRatingChange} /> <Rating name="size-large" defaultValue={2} size="large" onChange={this.handleRatingChange} />
</Grid> </Grid>
<Grid item xs={12} align="center"> <Grid item xs={12} align="center">
<Button color='primary' to='/' component={Link}>Start Again</Button> <Button color='primary' href='/' component="a">Start Again</Button>
</Grid> </Grid>
</Grid> </Grid>
) )
} }
render() { render() {
return ( return (
<Grid container spacing={1} style={{ width:330}}> <Grid container spacing={1} style={{ width:330}}>
<this.ConfirmDisputeDialog/>
<this.ConfirmFiatReceivedDialog/>
<Grid item xs={12} align="center"> <Grid item xs={12} align="center">
<Typography component="h5" variant="h5"> <Typography component="h5" variant="h5">
TradeBox Contract Box
</Typography> </Typography>
<Paper elevation={12} style={{ padding: 8,}}> <Paper elevation={12} style={{ padding: 8,}}>
{/* Maker and taker Bond request */} {/* Maker and taker Bond request */}
@ -449,10 +620,11 @@ handleRatingChange=(e)=>{
{/* Trade Finished - Payment Routing Failed */} {/* Trade Finished - Payment Routing Failed */}
{this.props.data.isBuyer & this.props.data.statusCode == 15 ? this.showUpdateInvoice() : ""} {this.props.data.isBuyer & this.props.data.statusCode == 15 ? this.showUpdateInvoice() : ""}
{/* Trade Finished - Payment Routing Failed - TODO Needs more planning */} {/* Trade Finished - TODO Needs more planning */}
{this.props.data.statusCode == 11 ? this.showInDispute() : ""} {this.props.data.statusCode == 11 ? this.showInDisputeStatement() : ""}
{/* Order has expired */}
{this.props.data.statusCode == 5 ? this.showOrderExpired() : ""}
{/* TODO */} {/* TODO */}
{/* */} {/* */}
{/* */} {/* */}

View File

@ -1,7 +1,10 @@
import React, { Component } from "react"; import React, { Component } from "react";
import { Button , Grid, Typography, TextField, ButtonGroup} from "@mui/material" import { Button , Dialog, Grid, Typography, TextField, ButtonGroup, CircularProgress, IconButton} from "@mui/material"
import { Link } from 'react-router-dom' import { Link } from 'react-router-dom'
import Image from 'material-ui-image' import Image from 'material-ui-image'
import InfoDialog from './InfoDialog'
import ContentCopyIcon from '@mui/icons-material/ContentCopy';
import ContentCopy from "@mui/icons-material/ContentCopy";
function getCookie(name) { function getCookie(name) {
let cookieValue = null; let cookieValue = null;
@ -25,6 +28,8 @@ export default class UserGenPage extends Component {
super(props); super(props);
this.state = { this.state = {
token: this.genBase62Token(34), token: this.genBase62Token(34),
openInfo: false,
showRobosat: true,
}; };
this.getGeneratedUser(this.state.token); this.getGeneratedUser(this.state.token);
} }
@ -51,6 +56,7 @@ export default class UserGenPage extends Component {
shannon_entropy: data.token_shannon_entropy, shannon_entropy: data.token_shannon_entropy,
bad_request: data.bad_request, bad_request: data.bad_request,
found: data.found, found: data.found,
showRobosat:true,
}); });
}); });
} }
@ -65,16 +71,14 @@ export default class UserGenPage extends Component {
.then((data) => console.log(data)); .then((data) => console.log(data));
} }
// Fix next two handler functions so they work sequentially
// at the moment they make the request generate a new user in parallel
// to updating the token in the state. So the it works a bit weird.
handleAnotherButtonPressed=(e)=>{ handleAnotherButtonPressed=(e)=>{
this.delGeneratedUser() this.delGeneratedUser()
this.setState({ // this.setState({
token: this.genBase62Token(34), // showRobosat: false,
}) // token: this.genBase62Token(34),
this.reload_for_csrf_to_work(); // });
// this.getGeneratedUser(this.state.token);
window.location.reload();
} }
handleChangeToken=(e)=>{ handleChangeToken=(e)=>{
@ -83,16 +87,38 @@ export default class UserGenPage extends Component {
token: e.target.value, token: e.target.value,
}) })
this.getGeneratedUser(e.target.value); this.getGeneratedUser(e.target.value);
this.setState({showRobosat: false})
} }
// TO FIX CSRF TOKEN IS NOT UPDATED UNTIL WINDOW IS RELOADED handleClickOpenInfo = () => {
reload_for_csrf_to_work=()=>{ this.setState({openInfo: true});
window.location.reload() };
handleCloseInfo = () => {
this.setState({openInfo: false});
};
InfoDialog =() =>{
return(
<Dialog
open={this.state.openInfo}
onClose={this.handleCloseInfo}
aria-labelledby="info-dialog-title"
aria-describedby="info-dialog-description"
scroll="paper"
>
<InfoDialog/>
</Dialog>
)
} }
render() { render() {
return ( return (
<Grid container spacing={1}> <Grid container spacing={1}>
<Grid item xs={12} align="center">
{this.state.showRobosat ?
<div>
<Grid item xs={12} align="center"> <Grid item xs={12} align="center">
<Typography component="h5" variant="h5"> <Typography component="h5" variant="h5">
<b>{this.state.nickname ? "⚡"+this.state.nickname+"⚡" : ""}</b> <b>{this.state.nickname ? "⚡"+this.state.nickname+"⚡" : ""}</b>
@ -108,6 +134,9 @@ export default class UserGenPage extends Component {
/> />
</div><br/> </div><br/>
</Grid> </Grid>
</div>
: <CircularProgress />}
</Grid>
{ {
this.state.found ? this.state.found ?
<Grid item xs={12} align="center"> <Grid item xs={12} align="center">
@ -118,10 +147,18 @@ export default class UserGenPage extends Component {
: :
"" ""
} }
<Grid container align="center">
<Grid item xs={12} align="center"> <Grid item xs={12} align="center">
<IconButton onClick= {()=>navigator.clipboard.writeText(this.state.token)}>
<ContentCopy/>
</IconButton>
<TextField <TextField
//sx={{ input: { color: 'purple' } }}
InputLabelProps={{
style: { color: 'green' },
}}
error={this.state.bad_request} error={this.state.bad_request}
label='Token - Store safely' label='Store your token safely'
required='true' required='true'
value={this.state.token} value={this.state.token}
variant='standard' variant='standard'
@ -131,13 +168,15 @@ export default class UserGenPage extends Component {
onChange={this.handleChangeToken} onChange={this.handleChangeToken}
/> />
</Grid> </Grid>
</Grid>
<Grid item xs={12} align="center"> <Grid item xs={12} align="center">
<Button onClick={this.handleAnotherButtonPressed}>Generate Another Robosat</Button> <Button size='small' onClick={this.handleAnotherButtonPressed}>Generate Another Robosat</Button>
</Grid> </Grid>
<Grid item xs={12} align="center"> <Grid item xs={12} align="center">
<ButtonGroup variant="contained" aria-label="outlined primary button group"> <ButtonGroup variant="contained" aria-label="outlined primary button group">
<Button color='primary' to='/make/' component={Link}>Make Order</Button> <Button color='primary' to='/make/' component={Link}>Make Order</Button>
<Button color='inherit' to='/info' component={Link}>INFO</Button> <Button color='inherit' onClick={this.handleClickOpenInfo}>Info</Button>
<this.InfoDialog/>
<Button color='secondary' to='/book/' component={Link}>View Book</Button> <Button color='secondary' to='/book/' component={Link}>View Book</Button>
</ButtonGroup> </ButtonGroup>
</Grid> </Grid>

View File

@ -0,0 +1,33 @@
export default function getFlags(code){
if(code == 'AUD') return '🇦🇺';
if(code == 'ARS') return '🇦🇷';
if(code == 'BRL') return '🇧🇷';
if(code == 'CAD') return '🇨🇦';
if(code == 'CHF') return '🇨🇭';
if(code == 'CLP') return '🇨🇱';
if(code == 'CNY') return '🇨🇳';
if(code == 'EUR') return '🇪🇺';
if(code == 'HRK') return '🇨🇷';
if(code == 'CZK') return '🇨🇿';
if(code == 'DKK') return '🇩🇰';
if(code == 'GBP') return '🇬🇧';
if(code == 'HKD') return '🇭🇰';
if(code == 'HUF') return '🇭🇺';
if(code == 'INR') return '🇮🇳';
if(code == 'ISK') return '🇮🇸';
if(code == 'JPY') return '🇯🇵';
if(code == 'KRW') return '🇰🇷';
if(code == 'MXN') return '🇲🇽';
if(code == 'NOK') return '🇳🇴';
if(code == 'NZD') return '🇳🇿';
if(code == 'PLN') return '🇵🇱';
if(code == 'RON') return '🇷🇴';
if(code == 'RUB') return '🇷🇺';
if(code == 'SEK') return '🇸🇪';
if(code == 'SGD') return '🇸🇬';
if(code == 'VES') return '🇻🇪';
if(code == 'TRY') return '🇹🇷';
if(code == 'USD') return '🇺🇸';
if(code == 'ZAR') return '🇿🇦';
return '🏳';
};

View File

@ -22,10 +22,12 @@
"21": "CLP", "21": "CLP",
"22": "CZK", "22": "CZK",
"23": "DKK", "23": "DKK",
"24": "HKR", "24": "HRK",
"25": "HUF", "25": "HUF",
"26": "INR", "26": "INR",
"27": "ISK", "27": "ISK",
"28": "PLN", "28": "PLN",
"29": "RON" "29": "RON",
"30": "ARS",
"31": "VES"
} }

View File

@ -1,81 +0,0 @@
# Buy and sell non-KYC Bitcoin using the lightning network.
## What is this?
{project_name} is a BTC/FIAT peer-to-peer exchange over lightning. It simplifies matchmaking and minimizes the trust needed to trade with a peer.
## Thats cool, so how it works?
Alice wants to sell sats, posts a sell order. Bob wants to buy sats, and takes Alice's order. Alice posts the sats as collateral using a hodl LN invoice. Bob also posts some sats as a bond to prove he is real. {project_name} locks the sats until Bob confirms he sent the fiat to Alice. Once Alice confirms she received the fiat, she tells {project_name} to release her sats to Bob. Enjoy your sats Bob!
At no point, Alice and Bob have to trust the funds to each other. In case Alice and Bob have a conflict, {project_name} staff will resolve the dispute.
(TODO: Long explanation and tutorial step by step, link)
## Nice, and fiat payments method are...?
Basically all of them. It is up to you to select your preferred payment methods. You will need to search for a peer who also accepts that method. Lightning is fast, so we highly recommend using instant fiat payment rails. Be aware trades have a expiry time of 8 hours. Paypal or credit card are not advice due to chargeback risk.
## Trust
The buyer and the seller never have to trust each other. Some trust on {project_name} is needed. Linking the sellers hodl invoice and buyer payment is not atomic (yet, research ongoing). In addition, disputes are solved by the {project_name} staff.
Note: this is not an escrow service. While trust requirements are minimized, {project_name} could run away with your sats. It could be argued that it is not worth it, as it would instantly destroy {project_name} reputation. However, you should hesitate and only trade small quantities at a time. For larger amounts and safety assurance use an escrow service such as Bisq or Hodlhodl.
You can build more trust on {project_name} by inspecting the source code, link.
## If {project_name} suddenly disappears during a trade my sats…
Your sats will most likely return to you. Any hodl invoice that is not settled would be automatically returned even if {project_name} goes down forever. This is true for both, locked bonds and traded sats. However, in the window between the buyer confirms FIAT SENT and the sats have not been released yet by the seller, the fund could be lost.
## Limits
Max trade size is 500K Sats to minimize failures in lightning routing. The limit will be raised as LN grows.
## Privacy
User token is generated locally as the unique identifier (back it up on paper! If lost {project_name} cannot help recover it). {project_name} doesnt know anything about you and doesnt want to know.
Your trading peer is the only one who can potentially guess anything about you. Keep chat short and concise. Avoid providing non-essential information other than strictly necessary for the fiat payment.
The chat with your peer is end-to-end encrypted, {project_name} cannot read. It can only be decrypted with your user token. The chat encryption makes it hard to resolve disputes. Therefore, by opening a dispute you are sending a viewkey to {project_name} staff. The encrypted chat cannot be revisited as it is deleted automatically when the trade is finalized (check the source code).
For best anonymity use Tor Browser and access the .onion hidden service.
## So {project_name} is a decentralized exchange?
Not quite, though it shares some elements.
A simple comparisson:
* Privacy worst to best: Coinbase/Binance/others < hodlhodl < {project_name} < Bisq
* Safety (not your keys, not your coins): Coinbase/Binance/others < {project_name} < hodlhodl < Bisq
*(take with a pinch of salt)*
So, if bisq is best for both privacy and safety, why {project_name} exists? Bisq is great, but it is difficult, slow, high-fee and needs extra steps to move to lightning. {project_name} aims to be as easy as Binance/Coinbase greatly improving on privacy and requiring minimal trust.
## Any risk?
Sure, this is a beta bot, things could go wrong. Trade small amounts!
The seller faces the same chargeback risk as with any other peer-to-peer exchange. Avoid accepting payment methods with easy chargeback!
## What are the fees?
{project_name} takes a 0.2% fee of the trade to cover lightning routing costs. This is akin to a Binance trade fee (but hey, you do not have to sell your soul to the devil, nor pay the withdrawal fine...).
The loser of a dispute pays a 1% fee that is slashed from the collateral posted when the trade starts. This fee is necessary to disincentive cheating and keep the site healthy. It also helps to cover the staff cost of dispute solving.
Note: your selected fiat payment rails might have other fees, these are to be covered by the buyer.
## I am a pro and {project_name} is too simple, it lacks features…
Indeed, this site is a simple front-end that aims for user friendliness and forces best privacy for casual users.
If you are a big maker, liquidity provider, or want to create many trades simultaneously use the API: {API_LINK_DOCUMENTATION}
## Is it legal to use {project_name} in my country?
In many countries using {project_name} is not different than buying something from a peer on Ebay or Craiglist. Your regulation may vary, you need to figure out.
## Disclaimer
This tool is provided as is. It is in active development and can be buggy. Be aware that you could lose your funds: trade with the utmost caution. There is no private support. Support is only offered via public channels (link telegram groups). {project_name} will never contact you. And {project_name} will definitely never ask for your user token.

View File

@ -25,6 +25,18 @@ body {
transform: translate(-50%,-50%); transform: translate(-50%,-50%);
} }
.bottomBar {
position: fixed;
bottom: 0;
width: 100%;
height: 40px;
}
.bottomItem {
margin: 0;
top: -14px;
}
.newAvatar { .newAvatar {
background-color:white; background-color:white;
border-radius: 50%; border-radius: 50%;

View File

@ -1,6 +1,10 @@
<!DOCTYPE html> <!DOCTYPE html>
<html> <html>
<head> <head>
<meta http-equiv="onion-location" content="{{ ONION_LOCATION }}" />
{% comment %} TODO Add a proper fav icon {% endcomment %}
<link rel="shortcut icon" href="#" />
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1" />
<title>RoboSats - Simple and Private Bitcoin Exchange</title> <title>RoboSats - Simple and Private Bitcoin Exchange</title>

View File

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

140
requirements.txt Normal file
View File

@ -0,0 +1,140 @@
aioredis==1.3.1
aiorpcX==0.18.7
amqp==5.0.9
apturl==0.5.2
asgiref==3.4.1
async-timeout==4.0.2
attrs==21.4.0
autobahn==21.11.1
Automat==20.2.0
backports.zoneinfo==0.2.1
bcrypt==3.1.7
billiard==3.6.4.0
blinker==1.4
Brlapi==0.7.0
celery==5.2.3
certifi==2019.11.28
cffi==1.15.0
channels==3.0.4
channels-redis==3.3.1
chardet==3.0.4
charge-lnd==0.2.4
click==8.0.3
click-didyoumean==0.3.0
click-plugins==1.1.1
click-repl==0.2.0
colorama==0.4.4
command-not-found==0.3
constantly==15.1.0
cryptography==36.0.1
cupshelpers==1.0
daphne==3.0.2
dbus-python==1.2.16
defer==1.0.6
Deprecated==1.2.13
distlib==0.3.4
distro==1.4.0
distro-info===0.23ubuntu1
Django==3.2.11
django-admin-relation-links==0.2.5
django-celery-beat==2.2.1
django-celery-results==2.2.0
django-model-utils==4.2.0
django-private-chat2==1.0.2
django-redis==5.2.0
django-timezone-field==4.2.3
djangorestframework==3.13.1
duplicity==0.8.12.0
entrypoints==0.3
fasteners==0.14.1
filelock==3.4.2
future==0.18.2
googleapis-common-protos==1.53.0
grpcio==1.39.0
grpcio-tools==1.43.0
hiredis==2.0.0
httplib2==0.14.0
hyperlink==21.0.0
idna==2.8
incremental==21.3.0
keyring==18.0.1
kombu==5.2.3
language-selector==0.1
launchpadlib==1.10.13
lazr.restfulclient==0.14.2
lazr.uri==1.0.3
lockfile==0.12.2
louis==3.12.0
macaroonbakery==1.3.1
Mako==1.1.0
MarkupSafe==1.1.0
monotonic==1.5
msgpack==1.0.3
natsort==8.0.2
netifaces==0.10.4
numpy==1.22.0
oauthlib==3.1.0
olefile==0.46
packaging==21.3
paramiko==2.6.0
pbr==5.8.0
pexpect==4.6.0
Pillow==7.0.0
platformdirs==2.4.1
prompt-toolkit==3.0.24
protobuf==3.17.3
pyasn1==0.4.8
pyasn1-modules==0.2.8
pycairo==1.16.2
pycparser==2.21
pycups==1.9.73
PyGObject==3.36.0
PyJWT==1.7.1
pymacaroons==0.13.0
PyNaCl==1.3.0
pyOpenSSL==21.0.0
pyparsing==3.0.6
pyRFC3339==1.1
PySocks==1.7.1
python-apt==2.0.0+ubuntu0.20.4.5
python-crontab==2.6.0
python-dateutil==2.7.3
python-debian===0.1.36ubuntu1
python-decouple==3.5
pytz==2021.3
pyxdg==0.26
PyYAML==5.3.1
redis==4.1.0
reportlab==3.5.34
requests==2.22.0
requests-unixsocket==0.2.0
ring==0.9.1
robohash==1.1
scipy==1.7.3
SecretStorage==2.3.1
service-identity==21.1.0
simplejson==3.16.0
six==1.16.0
sqlparse==0.4.2
stevedore==3.5.0
systemd-python==234
termcolor==1.1.0
Twisted==21.7.0
txaio==21.2.1
typing-extensions==4.0.1
ubuntu-advantage-tools==27.2
ubuntu-drivers-common==0.0.0
ufw==0.36
unattended-upgrades==0.1
urllib3==1.25.8
usb-creator==0.3.7
vine==5.0.0
virtualenv==20.12.1
virtualenv-clone==0.5.7
virtualenvwrapper==4.8.4
wadllib==1.3.3
wcwidth==0.2.5
wirerope==0.4.5
wrapt==1.13.3
xkit==0.0.0
zope.interface==5.4.0

View File

@ -0,0 +1,7 @@
from __future__ import absolute_import, unicode_literals
# This will make sure the app is always imported when
# Django starts so that shared_task will use this app.
from .celery import app as celery_app
__all__ = ('celery_app',)

View File

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

2
robosats/celery/conf.py Normal file
View File

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

View File

@ -40,10 +40,13 @@ INSTALLED_APPS = [
'django.contrib.staticfiles', 'django.contrib.staticfiles',
'rest_framework', 'rest_framework',
'channels', 'channels',
'django_celery_beat',
'django_celery_results',
'api', 'api',
'chat', 'chat',
'frontend.apps.FrontendConfig', 'frontend.apps.FrontendConfig',
] ]
from .celery.conf import *
MIDDLEWARE = [ MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware', 'django.middleware.security.SecurityMiddleware',

View File

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

View File

@ -45,8 +45,18 @@ pip install channels
pip install django-redis pip install django-redis
pip install channels-redis pip install channels-redis
``` ```
## Install Celery for Django tasks
```
pip install celery
pip install django-celery-beat
pip install django-celery-results
```
*Django 4.0 at the time of writting* Start up celery worker
`celery -A robosats worker --beat -l info -S django`
*Django 3.2.11 at the time of writting*
*Celery 5.2.3*
### Launch the local development node ### Launch the local development node
@ -112,8 +122,10 @@ npm install react-native
npm install react-native-svg npm install react-native-svg
npm install react-qr-code npm install react-qr-code
npm install @mui/material npm install @mui/material
npm install react-markdown
npm install websocket npm install websocket
npm install react-countdown
npm install @mui/icons-material
npm install @mui/x-data-grid
``` ```
Note we are using mostly MaterialUI V5 (@mui/material) but Image loading from V4 (@material-ui/core) extentions (so both V4 and V5 are needed) Note we are using mostly MaterialUI V5 (@mui/material) but Image loading from V4 (@material-ui/core) extentions (so both V4 and V5 are needed)