mirror of
https://github.com/RoboSats/robosats.git
synced 2024-12-14 11:26:24 +00:00
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:
commit
dcb54855ee
@ -6,12 +6,15 @@ LND_GRPC_HOST='127.0.0.1:10009'
|
||||
|
||||
REDIS_URL=''
|
||||
|
||||
# Market price public API
|
||||
MARKET_PRICE_API = 'https://blockchain.info/ticker'
|
||||
# List of market price public APIs. If the currency is available in more than 1 API, will use median price.
|
||||
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 = ''
|
||||
|
||||
# e.g. robotestagw3dcxmd66r4rgksb4nmmr43fh77bzn2ia2eucduyeafnyd.onion
|
||||
ONION_LOCATION = ''
|
||||
|
||||
# Trade fee in percentage %
|
||||
FEE = 0.002
|
||||
# Bond size in percentage %
|
||||
|
3
.gitignore
vendored
3
.gitignore
vendored
@ -639,6 +639,9 @@ FodyWeavers.xsd
|
||||
*migrations*
|
||||
frontend/static/frontend/main*
|
||||
|
||||
# Celery
|
||||
django
|
||||
|
||||
# robosats
|
||||
frontend/static/assets/avatars*
|
||||
api/lightning/lightning*
|
||||
|
41
README.md
41
README.md
@ -1,25 +1,46 @@
|
||||
# RoboSats: Buy and sell non-KYC Satoshis.
|
||||
## What is RoboSats?
|
||||
## RoboSats - Buy and sell Satoshis Privately.
|
||||
[![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
|
||||
|
||||
**Bitcoin mainnet:**
|
||||
- Tor: robosatsbkpis32grrxz7vliwjuivdmsyjx4d7zrlffo3nul44ck5sad.onion
|
||||
- Url: robosats.org (Not active)
|
||||
- Tor: robosats6tkf3eva7x2voqso3a5wcorsnw34jveyxfqi2fu7oyheasid.onion (Coming soon)
|
||||
- Url: robosats.com (Coming soon)
|
||||
- Version: v0.0.0 (Last stable)
|
||||
|
||||
**Bitcoin testnet:**
|
||||
- Tor: robotescktg6eqthfvatugczhzo3rj5zzk7rrkp6n5pa5qrz2mdikwid.onion
|
||||
- Url: testnet.robosats.org (Not active)
|
||||
- Commit height: v0.0.0 Latest commit.
|
||||
- Tor: robotestagw3dcxmd66r4rgksb4nmmr43fh77bzn2ia2eucduyeafnyd.onion (Active - On Dev Node)
|
||||
- Url: testnet.robosats.com (Coming soon)
|
||||
- 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
|
||||
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
|
||||
|
||||
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.
|
||||
|
19
api/admin.py
19
api/admin.py
@ -2,7 +2,7 @@ from django.contrib import admin
|
||||
from django_admin_relation_links import AdminChangeLinksMixin
|
||||
from django.contrib.auth.models import Group, User
|
||||
from django.contrib.auth.admin import UserAdmin
|
||||
from .models import Order, LNPayment, Profile, MarketTick
|
||||
from .models import Order, LNPayment, Profile, MarketTick, Currency
|
||||
|
||||
admin.site.unregister(Group)
|
||||
admin.site.unregister(User)
|
||||
@ -22,27 +22,34 @@ class EUserAdmin(UserAdmin):
|
||||
def avatar_tag(self, obj):
|
||||
return obj.profile.avatar_tag()
|
||||
|
||||
|
||||
@admin.register(Order)
|
||||
class OrderAdmin(AdminChangeLinksMixin, admin.ModelAdmin):
|
||||
list_display = ('id','type','maker_link','taker_link','status','amount','currency','t0_satoshis','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')
|
||||
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')
|
||||
|
||||
@admin.register(LNPayment)
|
||||
class LNPaymentAdmin(AdminChangeLinksMixin, admin.ModelAdmin):
|
||||
list_display = ('id','concept','status','num_satoshis','type','invoice','expires_at','sender_link','receiver_link')
|
||||
list_display_links = ('id','concept')
|
||||
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 = ('hash','concept','order_made','order_taken','order_escrow','order_paid')
|
||||
change_links = ('sender','receiver')
|
||||
list_filter = ('type','concept','status')
|
||||
|
||||
@admin.register(Profile)
|
||||
class UserProfileAdmin(AdminChangeLinksMixin, admin.ModelAdmin):
|
||||
list_display = ('avatar_tag','id','user_link','total_ratings','avg_rating','num_disputes','lost_disputes')
|
||||
list_display = ('avatar_tag','id','user_link','total_contracts','total_ratings','avg_rating','num_disputes','lost_disputes')
|
||||
list_display_links = ('avatar_tag','id')
|
||||
change_links =['user']
|
||||
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)
|
||||
class MarketTickAdmin(admin.ModelAdmin):
|
||||
list_display = ('timestamp','price','volume','premium','currency','fee')
|
||||
|
@ -28,6 +28,10 @@ class LNNode():
|
||||
invoicesstub = invoicesstub.InvoicesStub(channel)
|
||||
routerstub = routerstub.RouterStub(channel)
|
||||
|
||||
lnrpc = lnrpc
|
||||
invoicesrpc = invoicesrpc
|
||||
routerrpc = routerrpc
|
||||
|
||||
payment_failure_context = {
|
||||
0: "Payment isn't failed (yet)",
|
||||
1: "There are more routes to try, but the payment timeout was exceeded.",
|
||||
|
391
api/logics.py
391
api/logics.py
@ -1,16 +1,17 @@
|
||||
from datetime import time, timedelta
|
||||
from datetime import timedelta
|
||||
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 .utils import get_exchange_rate
|
||||
|
||||
from api.tasks import follow_send_payment
|
||||
|
||||
import math
|
||||
import ast
|
||||
|
||||
FEE = float(config('FEE'))
|
||||
BOND_SIZE = float(config('BOND_SIZE'))
|
||||
MARKET_PRICE_API = config('MARKET_PRICE_API')
|
||||
ESCROW_USERNAME = config('ESCROW_USERNAME')
|
||||
PENALTY_TIMEOUT = int(config('PENALTY_TIMEOUT'))
|
||||
|
||||
@ -30,17 +31,24 @@ FIAT_EXCHANGE_DURATION = int(config('FIAT_EXCHANGE_DURATION'))
|
||||
class Logics():
|
||||
|
||||
def validate_already_maker_or_taker(user):
|
||||
'''Checks if the user is already partipant of an order'''
|
||||
queryset = Order.objects.filter(maker=user)
|
||||
'''Validates if a use is already not part of an active order'''
|
||||
|
||||
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():
|
||||
return False, {'bad_request':'You are already maker of an order'}
|
||||
queryset = Order.objects.filter(taker=user)
|
||||
return False, {'bad_request':'You are already maker of an active order'}
|
||||
|
||||
queryset = Order.objects.filter(taker=user, status__in=active_order_status)
|
||||
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
|
||||
|
||||
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:
|
||||
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:
|
||||
@ -55,7 +63,7 @@ class Logics():
|
||||
else:
|
||||
order.taker = user
|
||||
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()
|
||||
return True, None
|
||||
|
||||
@ -74,7 +82,7 @@ class Logics():
|
||||
if order.is_explicit:
|
||||
satoshis_now = order.satoshis
|
||||
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)
|
||||
satoshis_now = (float(order.amount) / premium_rate) * 100*1000*1000
|
||||
|
||||
@ -82,45 +90,177 @@ class Logics():
|
||||
|
||||
def price_and_premium_now(order):
|
||||
''' 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:
|
||||
premium = order.premium
|
||||
price = exchange_rate * (1+float(premium)/100)
|
||||
else:
|
||||
exchange_rate = get_exchange_rate(Order.currency_dict[str(order.currency)])
|
||||
order_rate = float(order.amount) / (float(order.satoshis) / 100000000)
|
||||
premium = order_rate / exchange_rate - 1
|
||||
premium = int(premium*100) # 2 decimals left
|
||||
premium = int(premium*10000)/100 # 2 decimals left
|
||||
price = order_rate
|
||||
|
||||
significant_digits = 6
|
||||
significant_digits = 5
|
||||
price = round(price, significant_digits - int(math.floor(math.log10(abs(price)))) - 1)
|
||||
|
||||
return price, premium
|
||||
|
||||
def order_expires(order):
|
||||
''' General case when time runs out. Only
|
||||
used when the maker does not lock a publishing bond'''
|
||||
order.status = Order.Status.EXP
|
||||
order.maker = None
|
||||
order.taker = None
|
||||
order.save()
|
||||
@classmethod
|
||||
def order_expires(cls, order):
|
||||
''' General cases when time runs out.'''
|
||||
|
||||
def kick_taker(order):
|
||||
''' The taker did not lock the taker_bond. Now he has to go'''
|
||||
# Add a time out to the taker
|
||||
profile = order.taker.profile
|
||||
profile.penalty_expiration = timezone.now() + timedelta(seconds=PENALTY_TIMEOUT)
|
||||
profile.save()
|
||||
# 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]
|
||||
|
||||
# Delete the taker_bond payment request, and 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_bond = None
|
||||
order.expires_at = timezone.now() + timedelta(hours=PUBLIC_ORDER_DURATION) ## TO FIX. Restore the remaining order durantion, not all of it!
|
||||
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'''
|
||||
# Add a time out to the taker
|
||||
if order.taker:
|
||||
profile = order.taker.profile
|
||||
profile.penalty_expiration = timezone.now() + timedelta(seconds=PENALTY_TIMEOUT)
|
||||
profile.save()
|
||||
|
||||
# Make order public again
|
||||
order.taker = None
|
||||
order.taker_bond = None
|
||||
cls.publish_order(order)
|
||||
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
|
||||
def buyer_invoice_amount(cls, order, user):
|
||||
@ -168,14 +308,14 @@ class Logics():
|
||||
# If the order status is 'Waiting for invoice'. Move forward to 'chat'
|
||||
if order.status == Order.Status.WFI:
|
||||
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 order.status == Order.Status.WF2:
|
||||
# If the escrow is lock move to Chat.
|
||||
if order.trade_escrow.status == LNPayment.Status.LOCKED:
|
||||
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:
|
||||
order.status = Order.Status.WFE
|
||||
|
||||
@ -185,16 +325,18 @@ class Logics():
|
||||
def add_profile_rating(profile, rating):
|
||||
''' 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
|
||||
latest_ratings = profile.latest_ratings
|
||||
if len(latest_ratings) <= 1:
|
||||
if latest_ratings == None:
|
||||
profile.latest_ratings = [rating]
|
||||
profile.avg_rating = rating
|
||||
|
||||
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.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()
|
||||
|
||||
@ -217,7 +359,6 @@ class Logics():
|
||||
'''The order never shows up on the book and order
|
||||
status becomes "cancelled". That's it.'''
|
||||
if order.status == Order.Status.WFB and order.maker == user:
|
||||
order.maker = None
|
||||
order.status = Order.Status.UCA
|
||||
order.save()
|
||||
return True, None
|
||||
@ -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)'''
|
||||
elif order.status == Order.Status.PUB and order.maker == user:
|
||||
#Settle the maker bond (Maker loses the bond for cancelling public order)
|
||||
if cls.settle_maker_bond(order):
|
||||
order.maker = None
|
||||
if cls.settle_bond(order.maker_bond):
|
||||
order.status = Order.Status.UCA
|
||||
order.save()
|
||||
return True, None
|
||||
@ -238,13 +378,8 @@ class Logics():
|
||||
LNPayment "order.taker_bond" is deleted() '''
|
||||
elif order.status == Order.Status.TAK and order.taker == user:
|
||||
# adds a timeout penalty
|
||||
user.profile.penalty_expiration = timezone.now() + timedelta(seconds=PENALTY_TIMEOUT)
|
||||
user.profile.save()
|
||||
|
||||
order.taker = None
|
||||
order.status = Order.Status.PUB
|
||||
order.save()
|
||||
|
||||
cls.cancel_bond(order.taker_bond)
|
||||
cls.kick_taker(order)
|
||||
return True, None
|
||||
|
||||
# 4) When taker or maker cancel after bond (before escrow)
|
||||
@ -256,27 +391,24 @@ class Logics():
|
||||
'''The order into cancelled status if maker cancels.'''
|
||||
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)
|
||||
valid = cls.settle_maker_bond(order)
|
||||
valid = cls.settle_bond(order.maker_bond)
|
||||
if valid:
|
||||
order.maker = None
|
||||
order.status = Order.Status.UCA
|
||||
order.save()
|
||||
return True, None
|
||||
|
||||
# 4.b) When taker cancel after bond (before escrow)
|
||||
'''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)
|
||||
valid = cls.settle_taker_bond(order)
|
||||
valid = cls.settle_bond(order.taker_bond)
|
||||
if valid:
|
||||
order.taker = None
|
||||
order.status = Order.Status.PUB
|
||||
# order.taker_bond = None # TODO fix this, it overrides the information about the settled taker bond. Might make admin tasks hard.
|
||||
order.save()
|
||||
cls.publish_order(order)
|
||||
return True, None
|
||||
|
||||
# 5) When trade collateral has been posted (after escrow)
|
||||
'''Always goes to cancelled status. Collaboration is needed.
|
||||
'''Always goes to cancelled status. Collaboration is needed.
|
||||
When a user asks for cancel, 'order.is_pending_cancel' goes True.
|
||||
When the second user asks for cancel. Order is totally cancelled.
|
||||
Has a small cost for both parties to prevent node DDOS.'''
|
||||
@ -284,15 +416,20 @@ class Logics():
|
||||
else:
|
||||
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
|
||||
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.save()
|
||||
order.status = Order.Status.PUB
|
||||
# With the bond confirmation the order is extended 'public_order_duration' hours
|
||||
order.expires_at = timezone.now() + timedelta(hours=PUBLIC_ORDER_DURATION)
|
||||
order.save()
|
||||
cls.publish_order(order)
|
||||
return True
|
||||
return False
|
||||
|
||||
@ -338,22 +475,38 @@ class Logics():
|
||||
return True, {'bond_invoice':hold_payment['invoice'], 'bond_satoshis':bond_satoshis}
|
||||
|
||||
@classmethod
|
||||
def is_taker_bond_locked(cls, order):
|
||||
if LNNode.validate_hold_invoice_locked(order.taker_bond.payment_hash):
|
||||
def finalize_contract(cls, order):
|
||||
''' When the taker locks the taker_bond
|
||||
the contract is final '''
|
||||
|
||||
# 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.taker_bond.status = LNPayment.Status.LOCKED
|
||||
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
|
||||
MarketTick.log_a_tick(order)
|
||||
|
||||
# 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.save()
|
||||
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
|
||||
|
||||
@classmethod
|
||||
@ -361,12 +514,12 @@ class Logics():
|
||||
|
||||
# Do not gen and kick out the taker if order is older than expiry time
|
||||
if order.expires_at < timezone.now():
|
||||
cls.cancel_bond(order.taker_bond)
|
||||
cls.kick_taker(order)
|
||||
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.
|
||||
if order.taker_bond:
|
||||
# Check if status is INVGEN and still not expired
|
||||
if cls.is_taker_bond_locked(order):
|
||||
return False, None
|
||||
elif order.taker_bond.status == LNPayment.Status.INVGEN:
|
||||
@ -376,7 +529,8 @@ class Logics():
|
||||
order.last_satoshis = cls.satoshis_now(order)
|
||||
bond_satoshis = int(order.last_satoshis * BOND_SIZE)
|
||||
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
|
||||
hold_payment = LNNode.gen_hold_invoice(bond_satoshis, description, BOND_EXPIRY*3600)
|
||||
@ -395,23 +549,29 @@ class Logics():
|
||||
created_at = hold_payment['created_at'],
|
||||
expires_at = hold_payment['expires_at'])
|
||||
|
||||
order.expires_at = timezone.now() + timedelta(seconds=Order.t_to_expire[Order.Status.TAK])
|
||||
order.save()
|
||||
return True, {'bond_invoice': hold_payment['invoice'], 'bond_satoshis': bond_satoshis}
|
||||
|
||||
def trade_escrow_received(order):
|
||||
''' Moves the order forward'''
|
||||
# If status is 'Waiting for both' move to Waiting for invoice
|
||||
if order.status == Order.Status.WF2:
|
||||
order.status = Order.Status.WFI
|
||||
# If status is 'Waiting for invoice' move to Chat
|
||||
elif order.status == Order.Status.WFE:
|
||||
order.status = Order.Status.CHA
|
||||
order.expires_at = timezone.now() + timedelta(seconds=Order.t_to_expire[Order.Status.CHA])
|
||||
order.save()
|
||||
|
||||
@classmethod
|
||||
def is_trade_escrow_locked(cls, order):
|
||||
if LNNode.validate_hold_invoice_locked(order.trade_escrow.payment_hash):
|
||||
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()
|
||||
# If status is 'Waiting for both' move to Waiting for invoice
|
||||
if order.status == Order.Status.WF2:
|
||||
order.status = Order.Status.WFI
|
||||
# If status is 'Waiting for invoice' move to Chat
|
||||
elif order.status == Order.Status.WFE:
|
||||
order.status = Order.Status.CHA
|
||||
order.expires_at = timezone.now() + timedelta(hours=FIAT_EXCHANGE_DURATION)
|
||||
order.save()
|
||||
cls.trade_escrow_received(order)
|
||||
return True
|
||||
return False
|
||||
|
||||
@ -431,7 +591,7 @@ class Logics():
|
||||
elif order.trade_escrow.status == LNPayment.Status.INVGEN:
|
||||
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
|
||||
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()
|
||||
return True
|
||||
|
||||
def settle_maker_bond(order):
|
||||
''' Settles the maker bond hold invoice'''
|
||||
def settle_bond(bond):
|
||||
''' Settles the bond hold invoice'''
|
||||
# TODO ERROR HANDLING
|
||||
if LNNode.settle_hold_invoice(order.maker_bond.preimage):
|
||||
order.maker_bond.status = LNPayment.Status.SETLED
|
||||
order.maker_bond.save()
|
||||
if LNNode.settle_hold_invoice(bond.preimage):
|
||||
bond.status = LNPayment.Status.SETLED
|
||||
bond.save()
|
||||
return True
|
||||
|
||||
def settle_taker_bond(order):
|
||||
''' Settles the taker bond hold invoice'''
|
||||
# TODO ERROR HANDLING
|
||||
if LNNode.settle_hold_invoice(order.taker_bond.preimage):
|
||||
order.taker_bond.status = LNPayment.Status.SETLED
|
||||
order.taker_bond.save()
|
||||
def return_escrow(order):
|
||||
'''returns the trade escrow'''
|
||||
if LNNode.cancel_return_hold_invoice(order.trade_escrow.payment_hash):
|
||||
order.trade_escrow.status = LNPayment.Status.RETNED
|
||||
return True
|
||||
|
||||
|
||||
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
|
||||
|
||||
def return_bond(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
|
||||
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):
|
||||
''' Pay buyer invoice'''
|
||||
# TODO ERROR HANDLING
|
||||
if LNNode.pay_invoice(order.buyer_invoice.invoice, order.buyer_invoice.num_satoshis):
|
||||
return True
|
||||
suceeded, context = follow_send_payment(order.buyer_invoice)
|
||||
return suceeded, context
|
||||
|
||||
@classmethod
|
||||
def confirm_fiat(cls, order, user):
|
||||
@ -521,7 +710,7 @@ class Logics():
|
||||
if is_payed:
|
||||
order.status = Order.Status.SUC
|
||||
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()
|
||||
|
||||
# RETURN THE BONDS
|
||||
@ -542,11 +731,15 @@ class Logics():
|
||||
# If the trade is finished
|
||||
if order.status > Order.Status.PAY:
|
||||
# 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)
|
||||
order.maker_rated = True
|
||||
order.save()
|
||||
# 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)
|
||||
order.taker_rated = True
|
||||
order.save()
|
||||
else:
|
||||
return False, {'bad_request':'You cannot rate your counterparty yet.'}
|
||||
|
||||
|
42
api/management/commands/clean_orders.py
Normal file
42
api/management/commands/clean_orders.py
Normal 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))
|
130
api/management/commands/follow_invoices.py
Normal file
130
api/management/commands/follow_invoices.py
Normal 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
|
131
api/models.py
131
api/models.py
@ -2,13 +2,13 @@ from django.db import models
|
||||
from django.contrib.auth.models import User
|
||||
from django.core.validators import MaxValueValidator, MinValueValidator, validate_comma_separated_integer_list
|
||||
from django.db.models.signals import post_save, pre_delete
|
||||
from django.template.defaultfilters import truncatechars
|
||||
from django.dispatch import receiver
|
||||
from django.utils.html import mark_safe
|
||||
import uuid
|
||||
|
||||
from decouple import config
|
||||
from pathlib import Path
|
||||
from .utils import get_exchange_rate
|
||||
import json
|
||||
|
||||
MIN_TRADE = int(config('MIN_TRADE'))
|
||||
@ -16,6 +16,24 @@ MAX_TRADE = int(config('MAX_TRADE'))
|
||||
FEE = float(config('FEE'))
|
||||
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 Types(models.IntegerChoices):
|
||||
@ -33,23 +51,23 @@ class LNPayment(models.Model):
|
||||
LOCKED = 1, 'Locked'
|
||||
SETLED = 2, 'Settled'
|
||||
RETNED = 3, 'Returned'
|
||||
EXPIRE = 4, 'Expired'
|
||||
VALIDI = 5, 'Valid'
|
||||
FLIGHT = 6, 'In flight'
|
||||
SUCCED = 7, 'Succeeded'
|
||||
FAILRO = 8, 'Routing failed'
|
||||
CANCEL = 4, 'Cancelled'
|
||||
EXPIRE = 5, 'Expired'
|
||||
VALIDI = 6, 'Valid'
|
||||
FLIGHT = 7, 'In flight'
|
||||
SUCCED = 8, 'Succeeded'
|
||||
FAILRO = 9, 'Routing failed'
|
||||
|
||||
|
||||
# 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)
|
||||
concept = models.PositiveSmallIntegerField(choices=Concepts.choices, null=False, default=Concepts.MAKEBOND)
|
||||
status = models.PositiveSmallIntegerField(choices=Status.choices, null=False, default=Status.INVGEN)
|
||||
routing_retries = models.PositiveSmallIntegerField(null=False, default=0)
|
||||
|
||||
# payment 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
|
||||
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)
|
||||
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))])
|
||||
@ -61,7 +79,18 @@ class LNPayment(models.Model):
|
||||
receiver = models.ForeignKey(User, related_name='receiver', on_delete=models.CASCADE, null=True, default=None)
|
||||
|
||||
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):
|
||||
|
||||
@ -86,11 +115,9 @@ class Order(models.Model):
|
||||
PAY = 13, 'Sending satoshis to buyer'
|
||||
SUC = 14, 'Sucessful trade'
|
||||
FAI = 15, 'Failed lightning network routing'
|
||||
MLD = 16, 'Maker lost dispute'
|
||||
TLD = 17, '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())]
|
||||
WFR = 16, 'Wait for dispute resolution'
|
||||
MLD = 17, 'Maker lost dispute'
|
||||
TLD = 18, 'Taker lost dispute'
|
||||
|
||||
# order info
|
||||
status = models.PositiveSmallIntegerField(choices=Status.choices, null=False, default=Status.WFB)
|
||||
@ -99,7 +126,7 @@ class Order(models.Model):
|
||||
|
||||
# order details
|
||||
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)])
|
||||
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
|
||||
taker = models.ForeignKey(User, related_name='taker', on_delete=models.SET_NULL, null=True, default=None, blank=True) # unique = True, a taker can only take one order
|
||||
is_pending_cancel = models.BooleanField(default=False, null=False) # When collaborative cancel is needed and one partner has cancelled.
|
||||
is_disputed = models.BooleanField(default=False, null=False)
|
||||
is_fiat_sent = models.BooleanField(default=False, null=False)
|
||||
|
||||
# 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
|
||||
maker_bond = models.ForeignKey(LNPayment, related_name='maker_bond', on_delete=models.SET_NULL, null=True, default=None, blank=True)
|
||||
taker_bond = models.ForeignKey(LNPayment, related_name='taker_bond', on_delete=models.SET_NULL, null=True, default=None, blank=True)
|
||||
trade_escrow = models.ForeignKey(LNPayment, related_name='trade_escrow', on_delete=models.SET_NULL, null=True, default=None, blank=True)
|
||||
maker_bond = models.OneToOneField(LNPayment, related_name='order_made', 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.OneToOneField(LNPayment, related_name='order_escrow', on_delete=models.SET_NULL, null=True, default=None, blank=True)
|
||||
|
||||
# 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.
|
||||
maker_cancel = models.ForeignKey(LNPayment, related_name='maker_cancel', on_delete=models.SET_NULL, null=True, default=None, blank=True)
|
||||
taker_cancel = models.ForeignKey(LNPayment, related_name='taker_cancel', on_delete=models.SET_NULL, null=True, default=None, blank=True)
|
||||
# ratings
|
||||
maker_rated = models.BooleanField(default=False, null=False)
|
||||
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):
|
||||
# Make relational back to ORDER
|
||||
return (f'Order {self.id}: {self.Types(self.type).label} BTC for {float(self.amount)} {self.currency_dict[str(self.currency)]}')
|
||||
return (f'Order {self.id}: {self.Types(self.type).label} BTC for {float(self.amount)} {self.currency}')
|
||||
|
||||
|
||||
@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)
|
||||
|
||||
for htlc in to_delete:
|
||||
for lnpayment in to_delete:
|
||||
try:
|
||||
htlc.delete()
|
||||
lnpayment.delete()
|
||||
except:
|
||||
pass
|
||||
|
||||
@ -151,6 +204,9 @@ class Profile(models.Model):
|
||||
|
||||
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
|
||||
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
|
||||
@ -159,6 +215,8 @@ class Profile(models.Model):
|
||||
# Disputes
|
||||
num_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
|
||||
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)
|
||||
def del_avatar_from_disk(sender, instance, **kwargs):
|
||||
avatar_file=Path('frontend/' + instance.profile.avatar.url)
|
||||
avatar_file.unlink() # FIX deleting user fails if avatar is not found
|
||||
try:
|
||||
avatar_file=Path('frontend/' + instance.profile.avatar.url)
|
||||
avatar_file.unlink()
|
||||
except:
|
||||
pass
|
||||
|
||||
def __str__(self):
|
||||
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)])
|
||||
volume = models.DecimalField(max_digits=8, decimal_places=8, default=None, null=True, validators=[MinValueValidator(0)])
|
||||
premium = models.DecimalField(max_digits=5, decimal_places=2, default=None, null=True, validators=[MinValueValidator(-100), MaxValueValidator(999)], blank=True)
|
||||
currency = models.PositiveSmallIntegerField(choices=Order.currency_choices, null=True)
|
||||
currency = models.ForeignKey(Currency, null=True, on_delete=models.SET_NULL)
|
||||
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
|
||||
@ -226,7 +287,8 @@ class MarketTick(models.Model):
|
||||
elif order.taker_bond.status == LNPayment.Status.LOCKED:
|
||||
volume = order.last_satoshis / 100000000
|
||||
price = float(order.amount) / volume # Amount Fiat / Amount BTC
|
||||
premium = 100 * (price / get_exchange_rate(Order.currency_dict[str(order.currency)]) - 1)
|
||||
market_exchange_rate = float(order.currency.exchange_rate)
|
||||
premium = 100 * (price / market_exchange_rate - 1)
|
||||
|
||||
tick = MarketTick.objects.create(
|
||||
price=price,
|
||||
@ -239,4 +301,7 @@ class MarketTick(models.Model):
|
||||
def __str__(self):
|
||||
return f'Tick: {str(self.id)[:8]}'
|
||||
|
||||
class Meta:
|
||||
verbose_name = 'Market tick'
|
||||
verbose_name_plural = 'Market ticks'
|
||||
|
||||
|
@ -3633,7 +3633,7 @@ nouns = [
|
||||
"Fever",
|
||||
"Few",
|
||||
"Fiance",
|
||||
"Fiancé",
|
||||
"Fiance",
|
||||
"Fiasco",
|
||||
"Fiat",
|
||||
"Fiber",
|
||||
|
@ -41,13 +41,14 @@ class NickGenerator:
|
||||
else:
|
||||
raise ValueError("Language not implemented.")
|
||||
|
||||
print(
|
||||
f"{lang} SHA256 Nick Generator initialized with:"
|
||||
+ f"\nUp to {len(adverbs)} adverbs."
|
||||
+ f"\nUp to {len(adjectives)} adjectives."
|
||||
+ f"\nUp to {len(nouns)} nouns."
|
||||
+ f"\nUp to {max_num+1} numerics.\n"
|
||||
)
|
||||
if verbose:
|
||||
print(
|
||||
f"{lang} SHA256 Nick Generator initialized with:"
|
||||
+ f"\nUp to {len(adverbs)} adverbs."
|
||||
+ f"\nUp to {len(adjectives)} adjectives."
|
||||
+ f"\nUp to {len(nouns)} nouns."
|
||||
+ f"\nUp to {max_num+1} numerics.\n"
|
||||
)
|
||||
|
||||
self.use_adv = use_adv
|
||||
self.use_adj = use_adj
|
||||
|
@ -13,5 +13,6 @@ class MakeOrderSerializer(serializers.ModelSerializer):
|
||||
|
||||
class UpdateOrderSerializer(serializers.Serializer):
|
||||
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)
|
109
api/tasks.py
Normal file
109
api/tasks.py
Normal 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
|
70
api/utils.py
70
api/utils.py
@ -1,21 +1,56 @@
|
||||
|
||||
import requests, ring, os
|
||||
from decouple import config
|
||||
import numpy as np
|
||||
|
||||
from api.models import Order
|
||||
|
||||
market_cache = {}
|
||||
|
||||
@ring.dict(market_cache, expire=30) #keeps in cache for 30 seconds
|
||||
def get_exchange_rate(currency):
|
||||
# TODO Add fallback Public APIs and error handling
|
||||
# Think about polling price data in a different way (e.g. store locally every t seconds)
|
||||
@ring.dict(market_cache, expire=3) #keeps in cache for 3 seconds
|
||||
def get_exchange_rates(currencies):
|
||||
'''
|
||||
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()
|
||||
exchange_rate = float(market_prices[currency]['last'])
|
||||
APIS = config('MARKET_PRICE_APIS', cast=lambda v: [s.strip() for s in v.split(',')])
|
||||
|
||||
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 = {}
|
||||
|
||||
@ring.dict(lnd_v_cache, expire=3600) #keeps in cache for 3600 seconds
|
||||
def get_lnd_version():
|
||||
|
||||
@ -25,7 +60,6 @@ def get_lnd_version():
|
||||
return lnd_version
|
||||
|
||||
robosats_commit_cache = {}
|
||||
|
||||
@ring.dict(robosats_commit_cache, expire=3600)
|
||||
def get_commit_robosats():
|
||||
|
||||
@ -33,4 +67,22 @@ def get_commit_robosats():
|
||||
lnd_version = stream.read()
|
||||
|
||||
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)
|
||||
|
||||
|
111
api/views.py
111
api/views.py
@ -9,9 +9,9 @@ from django.contrib.auth import authenticate, login, logout
|
||||
from django.contrib.auth.models import User
|
||||
|
||||
from .serializers import ListOrderSerializer, MakeOrderSerializer, UpdateOrderSerializer
|
||||
from .models import LNPayment, MarketTick, Order
|
||||
from .models import LNPayment, MarketTick, Order, Currency
|
||||
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 robohash import Robohash
|
||||
@ -54,7 +54,7 @@ class MakerView(CreateAPIView):
|
||||
# Creates a new order
|
||||
order = Order(
|
||||
type=type,
|
||||
currency=currency,
|
||||
currency=Currency.objects.get(id=currency),
|
||||
amount=amount,
|
||||
payment_method=payment_method,
|
||||
premium=premium,
|
||||
@ -95,10 +95,6 @@ class OrderView(viewsets.ViewSet):
|
||||
# This is our order.
|
||||
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
|
||||
if order.status == Order.Status.UCA:
|
||||
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)
|
||||
|
||||
data = ListOrderSerializer(order).data
|
||||
data['total_secs_exp'] = Order.t_to_expire[order.status]
|
||||
|
||||
# if user is under a limit (penalty), inform him.
|
||||
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_taker'] = order.taker == request.user
|
||||
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:
|
||||
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)
|
||||
elif not data['is_participant'] and order.status != Order.Status.PUB:
|
||||
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_seller'] = Logics.is_seller(order,request.user)
|
||||
data['maker_nick'] = str(order.maker)
|
||||
@ -134,6 +140,26 @@ class OrderView(viewsets.ViewSet):
|
||||
data['status_message'] = Order.Status(order.status).label
|
||||
data['is_fiat_sent'] = order.is_fiat_sent
|
||||
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 order.taker_bond:
|
||||
@ -196,7 +222,7 @@ class OrderView(viewsets.ViewSet):
|
||||
|
||||
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.
|
||||
'''
|
||||
order_id = request.GET.get(self.lookup_url_kwarg)
|
||||
@ -206,20 +232,24 @@ class OrderView(viewsets.ViewSet):
|
||||
|
||||
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')
|
||||
invoice = serializer.data.get('invoice')
|
||||
statement = serializer.data.get('statement')
|
||||
rating = serializer.data.get('rating')
|
||||
|
||||
|
||||
# 1) If action is take, it is a taker request!
|
||||
if action == 'take':
|
||||
if order.status == Order.Status.PUB:
|
||||
valid, context = Logics.validate_already_maker_or_taker(request.user)
|
||||
if not valid: return Response(context, status=status.HTTP_409_CONFLICT)
|
||||
|
||||
valid, context = Logics.take(order, request.user)
|
||||
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)
|
||||
|
||||
# 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
|
||||
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)
|
||||
|
||||
# 6) If action is rate
|
||||
@ -281,14 +315,20 @@ class UserView(APIView):
|
||||
Response with Avatar and Nickname.
|
||||
'''
|
||||
|
||||
# if request.user.id:
|
||||
# context = {}
|
||||
# context['nickname'] = request.user.username
|
||||
# participant = not Logics.validate_already_maker_or_taker(request.user)
|
||||
# context['bad_request'] = f'You are already logged in as {request.user}'
|
||||
# if participant:
|
||||
# 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)
|
||||
# If an existing user opens the main page by mistake, we do not want it to create a new nickname/profile for him
|
||||
if request.user.is_authenticated:
|
||||
context = {'nickname': request.user.username}
|
||||
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}'
|
||||
# return Response(context, status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
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'
|
||||
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()
|
||||
|
||||
# Generate nickname
|
||||
# Generate nickname deterministically
|
||||
nickname = self.NickGen.short_from_SHA256(hash, max_length=18)[0]
|
||||
context['nickname'] = nickname
|
||||
|
||||
@ -316,13 +356,12 @@ class UserView(APIView):
|
||||
rh.assemble(roboset='set1', bgset='any')# for backgrounds ON
|
||||
|
||||
# Does not replace image if existing (avoid re-avatar in case of nick collusion)
|
||||
|
||||
image_path = avatar_path.joinpath(nickname+".png")
|
||||
if not image_path.exists():
|
||||
with open(image_path, "wb") as f:
|
||||
rh.img.save(f, format="png")
|
||||
|
||||
# Create new credentials and log in if nickname is new
|
||||
# Create new credentials and login if nickname is new
|
||||
if len(User.objects.filter(username=nickname)) == 0:
|
||||
User.objects.create_user(username=nickname, password=token, is_staff=False)
|
||||
user = authenticate(request, username=nickname, password=token)
|
||||
@ -410,26 +449,26 @@ class InfoView(ListAPIView):
|
||||
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)
|
||||
active_user_time_range = (timezone.now() - timedelta(minutes=30), timezone.now())
|
||||
context['num_active_robotsats'] = len(User.objects.filter(last_login__range=active_user_time_range))
|
||||
today = datetime.today()
|
||||
context['active_robots_today'] = len(User.objects.filter(last_login__day=today.day))
|
||||
|
||||
# Compute average premium and volume of today
|
||||
today = datetime.today()
|
||||
|
||||
queryset = MarketTick.objects.filter(timestamp__day=today.day)
|
||||
if not len(queryset) == 0:
|
||||
premiums = []
|
||||
weighted_premiums = []
|
||||
volumes = []
|
||||
for tick in queryset:
|
||||
premiums.append(tick.premium)
|
||||
weighted_premiums.append(tick.premium*tick.volume)
|
||||
volumes.append(tick.volume)
|
||||
avg_premium = sum(premiums) / len(premiums)
|
||||
|
||||
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 = None
|
||||
total_volume = None
|
||||
avg_premium = 0
|
||||
total_volume = 0
|
||||
|
||||
context['today_avg_nonkyc_btc_premium'] = avg_premium
|
||||
context['today_avg_nonkyc_btc_premium'] = round(avg_premium,2)
|
||||
context['today_total_volume'] = total_volume
|
||||
context['lnd_version'] = get_lnd_version()
|
||||
context['robosats_running_commit_hash'] = get_commit_robosats()
|
||||
|
@ -21,10 +21,7 @@ class ChatRoomConsumer(AsyncWebsocketConsumer):
|
||||
# if not (Logics.is_buyer(order[0], self.user) or Logics.is_seller(order[0], self.user)):
|
||||
# print ("Outta this chat")
|
||||
# return False
|
||||
|
||||
print(self.user_nick)
|
||||
print(self.order_id)
|
||||
|
||||
|
||||
await self.channel_layer.group_add(
|
||||
self.room_group_name,
|
||||
self.channel_name
|
||||
@ -56,8 +53,16 @@ class ChatRoomConsumer(AsyncWebsocketConsumer):
|
||||
message = event['message']
|
||||
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({
|
||||
'message': message,
|
||||
'message': fix_message,
|
||||
'user_nick': nick,
|
||||
}))
|
||||
|
||||
|
@ -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>
|
@ -2,7 +2,7 @@ from django.urls import path
|
||||
|
||||
from . import views
|
||||
|
||||
urlpatterns = [
|
||||
path('', views.index, name='index'),
|
||||
path('<str:order_id>/', views.room, name='order_chat'),
|
||||
]
|
||||
# urlpatterns = [
|
||||
# path('', views.index, name='index'),
|
||||
# path('<str:order_id>/', views.room, name='order_chat'),
|
||||
# ]
|
@ -1,10 +1,7 @@
|
||||
from django.shortcuts import render
|
||||
|
||||
|
||||
def index(request):
|
||||
return render(request, 'index.html', {})
|
||||
|
||||
def room(request, order_id):
|
||||
return render(request, 'chatroom.html', {
|
||||
'order_id': order_id
|
||||
})
|
||||
# def room(request, order_id):
|
||||
# return render(request, 'chatroom.html', {
|
||||
# 'order_id': order_id
|
||||
# })
|
608
frontend/package-lock.json
generated
608
frontend/package-lock.json
generated
@ -1581,6 +1581,14 @@
|
||||
"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": {
|
||||
"version": "5.2.7",
|
||||
"resolved": "https://registry.npmjs.org/@mui/material/-/material-5.2.7.tgz",
|
||||
@ -1666,6 +1674,17 @@
|
||||
"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": {
|
||||
"version": "2.11.2",
|
||||
"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",
|
||||
"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": {
|
||||
"version": "8.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.2.1.tgz",
|
||||
@ -2148,14 +2159,6 @@
|
||||
"@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": {
|
||||
"version": "2.0.4",
|
||||
"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==",
|
||||
"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": {
|
||||
"version": "17.0.6",
|
||||
"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",
|
||||
"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": {
|
||||
"version": "16.0.4",
|
||||
"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"
|
||||
}
|
||||
},
|
||||
"bail": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz",
|
||||
"integrity": "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw=="
|
||||
},
|
||||
"balanced-match": {
|
||||
"version": "1.0.2",
|
||||
"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": {
|
||||
"version": "1.0.3",
|
||||
"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",
|
||||
"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": {
|
||||
"version": "1.2.9",
|
||||
"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",
|
||||
"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": {
|
||||
"version": "0.2.0",
|
||||
"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",
|
||||
"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": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz",
|
||||
"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": {
|
||||
"version": "5.2.1",
|
||||
"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": {
|
||||
"version": "3.0.2",
|
||||
"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": {
|
||||
"version": "0.9.0",
|
||||
"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",
|
||||
"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": {
|
||||
"version": "2.2.0",
|
||||
"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",
|
||||
"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": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz",
|
||||
@ -5121,78 +5048,11 @@
|
||||
"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": {
|
||||
"version": "2.0.14",
|
||||
"resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.14.tgz",
|
||||
"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": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz",
|
||||
@ -5661,218 +5521,6 @@
|
||||
"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": {
|
||||
"version": "4.0.4",
|
||||
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.4.tgz",
|
||||
@ -5955,11 +5603,6 @@
|
||||
"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": {
|
||||
"version": "2.1.2",
|
||||
"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": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz",
|
||||
@ -6503,6 +6141,14 @@
|
||||
"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": {
|
||||
"version": "4.22.1",
|
||||
"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",
|
||||
"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": {
|
||||
"version": "0.66.4",
|
||||
"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": {
|
||||
"version": "1.1.0",
|
||||
"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",
|
||||
"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": {
|
||||
"version": "1.20.0",
|
||||
"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",
|
||||
"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": {
|
||||
"version": "5.2.1",
|
||||
"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",
|
||||
"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": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/split-string/-/split-string-3.1.0.tgz",
|
||||
@ -7806,14 +7402,6 @@
|
||||
"integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==",
|
||||
"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": {
|
||||
"version": "4.0.13",
|
||||
"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",
|
||||
"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": {
|
||||
"version": "2.3.1",
|
||||
"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",
|
||||
"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": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.1.tgz",
|
||||
@ -8073,67 +7635,6 @@
|
||||
"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": {
|
||||
"version": "0.1.2",
|
||||
"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",
|
||||
"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": {
|
||||
"version": "1.0.1",
|
||||
"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",
|
||||
"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": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/vlq/-/vlq-1.0.1.tgz",
|
||||
|
@ -26,10 +26,12 @@
|
||||
"@emotion/styled": "^11.6.0",
|
||||
"@material-ui/core": "^4.12.3",
|
||||
"@material-ui/icons": "^4.11.2",
|
||||
"@mui/icons-material": "^5.2.5",
|
||||
"@mui/material": "^5.2.7",
|
||||
"@mui/system": "^5.2.6",
|
||||
"@mui/x-data-grid": "^5.2.2",
|
||||
"material-ui-image": "^3.3.2",
|
||||
"react-markdown": "^7.1.2",
|
||||
"react-countdown": "^2.3.2",
|
||||
"react-native": "^0.66.4",
|
||||
"react-native-svg": "^12.1.1",
|
||||
"react-qr-code": "^2.0.3",
|
||||
|
@ -2,6 +2,7 @@ import React, { Component } from "react";
|
||||
import { render } from "react-dom";
|
||||
|
||||
import HomePage from "./HomePage";
|
||||
import BottomBar from "./BottomBar";
|
||||
|
||||
export default class App extends Component {
|
||||
constructor(props) {
|
||||
@ -10,9 +11,14 @@ export default class App extends Component {
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className='appCenter'>
|
||||
<HomePage />
|
||||
</div>
|
||||
<>
|
||||
<div className='appCenter'>
|
||||
<HomePage />
|
||||
</div>
|
||||
<div className='bottomBar'>
|
||||
<BottomBar />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -1,26 +1,28 @@
|
||||
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 { DataGrid } from '@mui/x-data-grid';
|
||||
import getFlags from './getFlags'
|
||||
|
||||
export default class BookPage extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
orders: new Array(),
|
||||
orders: new Array({id:0,}),
|
||||
currency: 0,
|
||||
type: 1,
|
||||
currencies_dict: {"0":"ANY"},
|
||||
loading: true,
|
||||
};
|
||||
this.getCurrencyDict()
|
||||
this.getOrderDetails(this.state.type,this.state.currency)
|
||||
this.getOrderDetails(this.state.type, this.state.currency)
|
||||
this.state.currencyCode = this.getCurrencyCode(this.state.currency)
|
||||
}
|
||||
|
||||
getOrderDetails(type,currency) {
|
||||
getOrderDetails(type, currency) {
|
||||
fetch('/api/book' + '?currency=' + currency + "&type=" + type)
|
||||
.then((response) => response.json())
|
||||
.then((data) =>
|
||||
.then((data) => console.log(data) &
|
||||
this.setState({
|
||||
orders: data,
|
||||
not_found: data.not_found,
|
||||
@ -28,7 +30,7 @@ export default class BookPage extends Component {
|
||||
}));
|
||||
}
|
||||
|
||||
handleCardClick=(e)=>{
|
||||
handleRowClick=(e)=>{
|
||||
console.log(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, ",");
|
||||
}
|
||||
|
||||
bookListItems=()=>{
|
||||
return (this.state.orders.map((order) =>
|
||||
<>
|
||||
<ListItemButton value={order.id} onClick={() => this.handleCardClick(order.id)}>
|
||||
bookListTable=()=>{
|
||||
return (
|
||||
<div style={{ height: 475, width: '100%' }}>
|
||||
<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,
|
||||
})
|
||||
)}
|
||||
|
||||
<ListItemAvatar >
|
||||
<Avatar
|
||||
alt={order.maker_nick}
|
||||
src={window.location.origin +'/static/assets/avatars/' + order.maker_nick + '.png'}
|
||||
/>
|
||||
</ListItemAvatar>
|
||||
|
||||
<ListItemText>
|
||||
<Typography variant="h6">
|
||||
{order.maker_nick+" "}
|
||||
</Typography>
|
||||
</ListItemText>
|
||||
|
||||
<ListItemText align='left'>
|
||||
<Typography variant="subtitle1">
|
||||
<b>{order.type ? " Sells ": " Buys "} BTC </b> for {parseFloat(
|
||||
parseFloat(order.amount).toFixed(4))+" "+ this.getCurrencyCode(order.currency)+" "}
|
||||
</Typography>
|
||||
</ListItemText>
|
||||
columns={[
|
||||
// { field: 'id', headerName: 'ID', width: 40 },
|
||||
{ field: 'robosat', headerName: 'RoboSat', width: 240,
|
||||
renderCell: (params) => {return (
|
||||
<ListItemButton style={{ cursor: "pointer" }}>
|
||||
<ListItemAvatar>
|
||||
<Avatar alt={params.row.robosat} src={params.row.avatar} />
|
||||
</ListItemAvatar>
|
||||
<ListItemText primary={params.row.robosat}/>
|
||||
</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>
|
||||
)} },
|
||||
]}
|
||||
|
||||
<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>
|
||||
|
||||
<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() {
|
||||
@ -159,10 +166,10 @@ export default class BookPage extends Component {
|
||||
style: {textAlign:"center"}
|
||||
}}
|
||||
onChange={this.handleCurrencyChange}
|
||||
> <MenuItem value={0}>ANY</MenuItem>
|
||||
> <MenuItem value={0}>🌍 ANY</MenuItem>
|
||||
{
|
||||
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>
|
||||
</FormControl>
|
||||
@ -196,10 +203,10 @@ export default class BookPage extends Component {
|
||||
</Grid>)
|
||||
:
|
||||
<Grid item xs={12} align="center">
|
||||
<Paper elevation={0} style={{width: 900, maxHeight: 500, overflow: 'auto'}}>
|
||||
<List >
|
||||
{this.bookListItems()}
|
||||
</List>
|
||||
<Paper elevation={0} style={{width: 910, maxHeight: 500, overflow: 'auto'}}>
|
||||
|
||||
{this.state.loading ? null : this.bookListTable()}
|
||||
|
||||
</Paper>
|
||||
</Grid>
|
||||
}
|
||||
|
260
frontend/src/components/BottomBar.js
Normal file
260
frontend/src/components/BottomBar.js
Normal 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>
|
||||
)
|
||||
}
|
||||
}
|
@ -13,15 +13,13 @@ export default class Chat extends Component {
|
||||
state = {
|
||||
messages: [],
|
||||
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() {
|
||||
this.client.onopen = () => {
|
||||
console.log('WebSocket Client Connected')
|
||||
console.log(this.props.data)
|
||||
}
|
||||
this.client.onmessage = (message) => {
|
||||
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) => {
|
||||
this.client.send(JSON.stringify({
|
||||
type: "message",
|
||||
message: this.state.value,
|
||||
nick: this.props.data.urNick,
|
||||
nick: this.props.urNick,
|
||||
}));
|
||||
this.state.value = ''
|
||||
e.preventDefault();
|
||||
@ -60,7 +66,7 @@ export default class Chat extends Component {
|
||||
{this.state.messages.map(message => <>
|
||||
<Card elevation={5} align="left" >
|
||||
{/* 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
|
||||
avatar={
|
||||
<Avatar
|
||||
@ -86,6 +92,7 @@ export default class Chat extends Component {
|
||||
/>}
|
||||
</Card>
|
||||
</>)}
|
||||
<div style={{ float:"left", clear: "both" }} ref={(el) => { this.messagesEnd = el; }}></div>
|
||||
</Paper>
|
||||
<form noValidate onSubmit={this.onButtonClicked}>
|
||||
<Grid containter alignItems="stretch" style={{ display: "flex" }}>
|
||||
|
@ -5,8 +5,6 @@ import UserGenPage from "./UserGenPage";
|
||||
import MakerPage from "./MakerPage";
|
||||
import BookPage from "./BookPage";
|
||||
import OrderPage from "./OrderPage";
|
||||
import InfoPage from "./InfoPage";
|
||||
|
||||
export default class HomePage extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
@ -18,7 +16,6 @@ export default class HomePage extends Component {
|
||||
<Switch>
|
||||
<Route exact path='/' component={UserGenPage}/>
|
||||
<Route path='/home'><p>You are at the start page</p></Route>
|
||||
<Route path='/info' component={InfoPage}/>
|
||||
<Route path='/make' component={MakerPage}/>
|
||||
<Route path='/book' component={BookPage}/>
|
||||
<Route path="/order/:orderId" component={OrderPage}/>
|
||||
|
125
frontend/src/components/InfoDialog.js
Normal file
125
frontend/src/components/InfoDialog.js
Normal 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>
|
||||
)
|
||||
}
|
||||
}
|
@ -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>
|
||||
)
|
||||
}
|
||||
}
|
@ -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 { Link } from 'react-router-dom'
|
||||
|
||||
import getFlags from './getFlags'
|
||||
|
||||
function getCookie(name) {
|
||||
let cookieValue = null;
|
||||
if (document.cookie && document.cookie !== '') {
|
||||
@ -190,7 +192,9 @@ export default class MakerPage extends Component {
|
||||
>
|
||||
{
|
||||
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>
|
||||
|
||||
|
@ -1,44 +1,17 @@
|
||||
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 getFlags from './getFlags'
|
||||
|
||||
function msToTime(duration) {
|
||||
var seconds = Math.floor((duration / 1000) % 60),
|
||||
minutes = Math.floor((duration / (1000 * 60)) % 60),
|
||||
hours = Math.floor((duration / (1000 * 60 * 60)) % 24);
|
||||
|
||||
minutes = (minutes < 10) ? "0" + minutes : minutes;
|
||||
seconds = (seconds < 10) ? "0" + seconds : seconds;
|
||||
|
||||
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>
|
||||
);
|
||||
}
|
||||
// icons
|
||||
import AccessTimeIcon from '@mui/icons-material/AccessTime';
|
||||
import NumbersIcon from '@mui/icons-material/Numbers';
|
||||
import PriceChangeIcon from '@mui/icons-material/PriceChange';
|
||||
import PaymentsIcon from '@mui/icons-material/Payments';
|
||||
import MoneyIcon from '@mui/icons-material/Money';
|
||||
import ArticleIcon from '@mui/icons-material/Article';
|
||||
import ContentCopy from "@mui/icons-material/ContentCopy";
|
||||
|
||||
function getCookie(name) {
|
||||
let cookieValue = null;
|
||||
@ -67,12 +40,37 @@ export default class OrderPage extends Component {
|
||||
super(props);
|
||||
this.state = {
|
||||
isExplicit: false,
|
||||
delay: 2000, // Refresh every 2 seconds by default
|
||||
currencies_dict: {"1":"USD"}
|
||||
delay: 60000, // Refresh every 60 seconds by default
|
||||
currencies_dict: {"1":"USD"},
|
||||
total_secs_expiry: 300,
|
||||
loading: true,
|
||||
openCancel: false,
|
||||
};
|
||||
this.orderId = this.props.match.params.orderId;
|
||||
this.getCurrencyDict();
|
||||
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() {
|
||||
@ -81,6 +79,8 @@ export default class OrderPage extends Component {
|
||||
.then((response) => response.json())
|
||||
.then((data) => {console.log(data) &
|
||||
this.setState({
|
||||
loading: false,
|
||||
delay: this.setDelay(data.status),
|
||||
id: data.id,
|
||||
statusCode: data.status,
|
||||
statusText: data.status_message,
|
||||
@ -110,6 +110,13 @@ export default class OrderPage extends Component {
|
||||
escrowInvoice: data.escrow_invoice,
|
||||
escrowSatoshis: data.escrow_satoshis,
|
||||
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() {
|
||||
this.interval = setInterval(this.tick, this.state.delay);
|
||||
}
|
||||
componentDidUpdate(prevProps, prevState) {
|
||||
if (prevState.delay !== this.state.delay) {
|
||||
clearInterval(this.interval);
|
||||
componentDidUpdate() {
|
||||
clearInterval(this.interval);
|
||||
this.interval = setInterval(this.tick, this.state.delay);
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
clearInterval(this.interval);
|
||||
}
|
||||
@ -136,6 +142,50 @@ export default class OrderPage extends Component {
|
||||
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=()=>{
|
||||
console.log(this.state)
|
||||
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){
|
||||
let code = val ? this.state.currencies_dict[val.toString()] : ""
|
||||
return code
|
||||
}
|
||||
|
||||
handleClickCancelOrderButton=()=>{
|
||||
handleClickConfirmCancelButton=()=>{
|
||||
console.log(this.state)
|
||||
const requestOptions = {
|
||||
method: 'POST',
|
||||
@ -177,6 +232,64 @@ export default class OrderPage extends Component {
|
||||
fetch('/api/order/' + '?order_id=' + this.orderId, requestOptions)
|
||||
.then((response) => response.json())
|
||||
.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=()=>{
|
||||
@ -184,7 +297,7 @@ export default class OrderPage extends Component {
|
||||
<Grid container spacing={1}>
|
||||
<Grid item xs={12} align="center">
|
||||
<Typography component="h5" variant="h5">
|
||||
{this.state.type ? "Sell " : "Buy "} Order Details
|
||||
Order Details
|
||||
</Typography>
|
||||
<Paper elevation={12} style={{ padding: 8,}}>
|
||||
<List dense="true">
|
||||
@ -217,6 +330,9 @@ export default class OrderPage extends Component {
|
||||
""
|
||||
}
|
||||
<ListItem>
|
||||
<ListItemIcon>
|
||||
<ArticleIcon/>
|
||||
</ListItemIcon>
|
||||
<ListItemText primary={this.state.statusText} secondary="Order status"/>
|
||||
</ListItem>
|
||||
<Divider />
|
||||
@ -225,30 +341,54 @@ export default class OrderPage extends Component {
|
||||
}
|
||||
|
||||
<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>
|
||||
<Divider />
|
||||
<ListItem>
|
||||
<ListItemIcon>
|
||||
<PaymentsIcon/>
|
||||
</ListItemIcon>
|
||||
<ListItemText primary={this.state.paymentMethod} secondary="Accepted payment methods"/>
|
||||
</ListItem>
|
||||
<Divider />
|
||||
|
||||
{/* If there is live Price and Premium data, show it. Otherwise show the order maker settings */}
|
||||
<ListItem>
|
||||
{this.state.isExplicit ?
|
||||
<ListItemText primary={pn(this.state.satoshis)} secondary="Amount of Satoshis"/>
|
||||
:
|
||||
<ListItemText primary={parseFloat(parseFloat(this.state.premium).toFixed(2))+"%"} secondary="Premium over market price"/>
|
||||
}
|
||||
<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={parseFloat(parseFloat(this.state.premium).toFixed(2))+"%"} secondary="Premium over market price"/>
|
||||
)
|
||||
}
|
||||
</ListItem>
|
||||
<Divider />
|
||||
|
||||
<ListItem>
|
||||
<ListItemText primary={'#'+this.orderId} secondary="Order ID"/>
|
||||
<ListItemIcon>
|
||||
<NumbersIcon/>
|
||||
</ListItemIcon>
|
||||
<ListItemText primary={this.orderId} secondary="Order ID"/>
|
||||
</ListItem>
|
||||
<Divider />
|
||||
<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>
|
||||
<LinearDeterminate />
|
||||
<this.LinearDeterminate />
|
||||
</List>
|
||||
|
||||
{/* If the user has a penalty/limit */}
|
||||
@ -266,8 +406,10 @@ export default class OrderPage extends Component {
|
||||
</Paper>
|
||||
</Grid>
|
||||
|
||||
{/* Participants cannot see the Back or Take Order buttons */}
|
||||
{this.state.isParticipant ? "" :
|
||||
{/* Participants can see the "Cancel" Button, but cannot see the "Back" or "Take Order" buttons */}
|
||||
{this.state.isParticipant ?
|
||||
<this.CancelButton/>
|
||||
:
|
||||
<>
|
||||
<Grid item xs={12} align="center">
|
||||
<Button variant='contained' color='primary' onClick={this.handleClickTakeOrderButton}>Take Order</Button>
|
||||
@ -278,27 +420,7 @@ 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 (){
|
||||
return (
|
||||
// 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()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -1,9 +1,16 @@
|
||||
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 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) {
|
||||
let cookieValue = null;
|
||||
if (document.cookie && document.cookie !== '') {
|
||||
@ -30,10 +37,100 @@ export default class TradeBox extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
openConfirmFiatReceived: false,
|
||||
openConfirmDispute: 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=()=>{
|
||||
return (
|
||||
<Grid container spacing={1}>
|
||||
@ -60,14 +157,12 @@ export default class TradeBox extends Component {
|
||||
<Grid item xs={12} align="center">
|
||||
<TextField
|
||||
hiddenLabel
|
||||
variant="filled"
|
||||
variant="standard"
|
||||
size="small"
|
||||
defaultValue={this.props.data.bondInvoice}
|
||||
disabled="true"
|
||||
helperText="This is a hold invoice. It will simply freeze in your wallet.
|
||||
It will be charged only if you cancel the order or lose a dispute."
|
||||
helperText="This is a hold invoice. It will be charged only if you cancel or lose a dispute."
|
||||
color = "secondary"
|
||||
onClick = {this.copyCodeToClipboard}
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
@ -78,7 +173,7 @@ export default class TradeBox extends Component {
|
||||
return (
|
||||
<Grid item xs={12} 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>
|
||||
</Grid>
|
||||
);
|
||||
@ -88,7 +183,7 @@ export default class TradeBox extends Component {
|
||||
return (
|
||||
<Grid container spacing={1}>
|
||||
<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>
|
||||
</Typography>
|
||||
</Grid>
|
||||
@ -103,7 +198,7 @@ export default class TradeBox extends Component {
|
||||
size="small"
|
||||
defaultValue={this.props.data.escrowInvoice}
|
||||
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"
|
||||
/>
|
||||
</Grid>
|
||||
@ -158,17 +253,27 @@ export default class TradeBox extends Component {
|
||||
{/* TODO API sends data for a more confortable wait */}
|
||||
<Divider/>
|
||||
<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>
|
||||
|
||||
<Divider/>
|
||||
<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>
|
||||
|
||||
<Divider/>
|
||||
<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>
|
||||
<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=()=>{
|
||||
this.setState({badInvoice:false});
|
||||
|
||||
@ -205,10 +308,34 @@ export default class TradeBox extends Component {
|
||||
& 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(){
|
||||
return (
|
||||
|
||||
// TODO Camera option to read QR
|
||||
// TODO Option to upload files and images
|
||||
|
||||
<Grid container spacing={1}>
|
||||
<Grid item xs={12} align="center">
|
||||
@ -223,24 +350,68 @@ export default class TradeBox extends Component {
|
||||
valid invoice for {pn(this.props.data.invoiceAmount)} Satoshis.
|
||||
</Typography>
|
||||
</Grid>
|
||||
<form noValidate onSubmit={this.handleClickSubmitInvoiceButton}>
|
||||
<Grid item xs={12} align="center">
|
||||
<TextField
|
||||
error={this.state.badInvoice}
|
||||
helperText={this.state.badInvoice ? this.state.badInvoice : "" }
|
||||
label={"Payout Lightning Invoice"}
|
||||
required
|
||||
inputProps={{
|
||||
style: {textAlign:"center"}
|
||||
}}
|
||||
multiline
|
||||
onChange={this.handleInputInvoiceChanged}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={12} align="center">
|
||||
<Button variant='contained' color='primary'>Submit</Button>
|
||||
</Grid>
|
||||
</form>
|
||||
|
||||
<Grid item xs={12} align="center">
|
||||
<TextField
|
||||
error={this.state.badInvoice}
|
||||
helperText={this.state.badInvoice ? this.state.badInvoice : "" }
|
||||
label={"Payout Lightning Invoice"}
|
||||
required
|
||||
inputProps={{
|
||||
style: {textAlign:"center"}
|
||||
}}
|
||||
multiline
|
||||
onChange={this.handleInputInvoiceChanged}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={12} align="center">
|
||||
<Button onClick={this.handleClickSubmitInvoiceButton} variant='contained' color='primary'>Submit</Button>
|
||||
</Grid>
|
||||
|
||||
{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()}
|
||||
</Grid>
|
||||
)
|
||||
@ -251,7 +422,7 @@ export default class TradeBox extends Component {
|
||||
<Grid container spacing={1}>
|
||||
<Grid item xs={12} align="center">
|
||||
<Typography component="subtitle1" variant="subtitle1">
|
||||
<b>Your invoice looks good!</b>
|
||||
<b>Your invoice looks good!🎉</b>
|
||||
</Typography>
|
||||
</Grid>
|
||||
<Grid item xs={12} align="center">
|
||||
@ -272,7 +443,7 @@ export default class TradeBox extends Component {
|
||||
<Grid container spacing={1}>
|
||||
<Grid item xs={12} align="center">
|
||||
<Typography component="subtitle1" variant="subtitle1">
|
||||
<b>The trade collateral is locked! :D </b>
|
||||
<b>The trade collateral is locked! 🎉 </b>
|
||||
</Typography>
|
||||
</Grid>
|
||||
<Grid item xs={12} align="center">
|
||||
@ -301,18 +472,7 @@ export default class TradeBox extends Component {
|
||||
.then((response) => response.json())
|
||||
.then((data) => (this.props.data = data));
|
||||
}
|
||||
handleClickOpenDisputeButton=()=>{
|
||||
const requestOptions = {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type':'application/json', 'X-CSRFToken': getCookie('csrftoken'),},
|
||||
body: JSON.stringify({
|
||||
'action': "dispute",
|
||||
}),
|
||||
};
|
||||
fetch('/api/order/' + '?order_id=' + this.props.data.id, requestOptions)
|
||||
.then((response) => response.json())
|
||||
.then((data) => (this.props.data = data));
|
||||
}
|
||||
|
||||
handleRatingChange=(e)=>{
|
||||
const requestOptions = {
|
||||
method: 'POST',
|
||||
@ -338,11 +498,9 @@ handleRatingChange=(e)=>{
|
||||
}
|
||||
|
||||
showFiatReceivedButton(){
|
||||
// TODO, show alert and ask for double confirmation (Have you check you received the fiat? Confirming fiat received settles the trade.)
|
||||
// Ask for double confirmation.
|
||||
return(
|
||||
<Grid 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>
|
||||
)
|
||||
}
|
||||
@ -351,11 +509,23 @@ 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.
|
||||
return(
|
||||
<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>
|
||||
)
|
||||
}
|
||||
|
||||
showChat(sendFiatButton, receivedFiatButton, openDisputeButton){
|
||||
return(
|
||||
<Grid container spacing={1}>
|
||||
@ -367,7 +537,7 @@ handleRatingChange=(e)=>{
|
||||
<Grid item xs={12} align="center">
|
||||
{this.props.data.isSeller ?
|
||||
<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 component="body2" variant="body2" align="center">
|
||||
@ -377,7 +547,7 @@ handleRatingChange=(e)=>{
|
||||
<Divider/>
|
||||
</Grid>
|
||||
|
||||
<Chat data={this.props.data}/>
|
||||
<Chat orderId={this.props.data.id} urNick={this.props.data.urNick}/>
|
||||
|
||||
<Grid item xs={12} align="center">
|
||||
{openDisputeButton ? this.showOpenDisputeButton() : ""}
|
||||
@ -406,19 +576,20 @@ handleRatingChange=(e)=>{
|
||||
<Rating name="size-large" defaultValue={2} size="large" onChange={this.handleRatingChange} />
|
||||
</Grid>
|
||||
<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>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Grid container spacing={1} style={{ width:330}}>
|
||||
<this.ConfirmDisputeDialog/>
|
||||
<this.ConfirmFiatReceivedDialog/>
|
||||
<Grid item xs={12} align="center">
|
||||
<Typography component="h5" variant="h5">
|
||||
TradeBox
|
||||
Contract Box
|
||||
</Typography>
|
||||
<Paper elevation={12} style={{ padding: 8,}}>
|
||||
{/* Maker and taker Bond request */}
|
||||
@ -449,10 +620,11 @@ handleRatingChange=(e)=>{
|
||||
{/* Trade Finished - Payment Routing Failed */}
|
||||
{this.props.data.isBuyer & this.props.data.statusCode == 15 ? this.showUpdateInvoice() : ""}
|
||||
|
||||
{/* Trade Finished - Payment Routing Failed - TODO Needs more planning */}
|
||||
{this.props.data.statusCode == 11 ? this.showInDispute() : ""}
|
||||
{/* Trade Finished - TODO Needs more planning */}
|
||||
{this.props.data.statusCode == 11 ? this.showInDisputeStatement() : ""}
|
||||
|
||||
|
||||
{/* Order has expired */}
|
||||
{this.props.data.statusCode == 5 ? this.showOrderExpired() : ""}
|
||||
{/* TODO */}
|
||||
{/* */}
|
||||
{/* */}
|
||||
|
@ -1,7 +1,10 @@
|
||||
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 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) {
|
||||
let cookieValue = null;
|
||||
@ -25,6 +28,8 @@ export default class UserGenPage extends Component {
|
||||
super(props);
|
||||
this.state = {
|
||||
token: this.genBase62Token(34),
|
||||
openInfo: false,
|
||||
showRobosat: true,
|
||||
};
|
||||
this.getGeneratedUser(this.state.token);
|
||||
}
|
||||
@ -51,6 +56,7 @@ export default class UserGenPage extends Component {
|
||||
shannon_entropy: data.token_shannon_entropy,
|
||||
bad_request: data.bad_request,
|
||||
found: data.found,
|
||||
showRobosat:true,
|
||||
});
|
||||
});
|
||||
}
|
||||
@ -65,16 +71,14 @@ export default class UserGenPage extends Component {
|
||||
.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)=>{
|
||||
this.delGeneratedUser()
|
||||
this.setState({
|
||||
token: this.genBase62Token(34),
|
||||
})
|
||||
this.reload_for_csrf_to_work();
|
||||
// this.setState({
|
||||
// showRobosat: false,
|
||||
// token: this.genBase62Token(34),
|
||||
// });
|
||||
// this.getGeneratedUser(this.state.token);
|
||||
window.location.reload();
|
||||
}
|
||||
|
||||
handleChangeToken=(e)=>{
|
||||
@ -83,30 +87,55 @@ export default class UserGenPage extends Component {
|
||||
token: e.target.value,
|
||||
})
|
||||
this.getGeneratedUser(e.target.value);
|
||||
this.setState({showRobosat: false})
|
||||
}
|
||||
|
||||
// TO FIX CSRF TOKEN IS NOT UPDATED UNTIL WINDOW IS RELOADED
|
||||
reload_for_csrf_to_work=()=>{
|
||||
window.location.reload()
|
||||
handleClickOpenInfo = () => {
|
||||
this.setState({openInfo: true});
|
||||
};
|
||||
|
||||
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() {
|
||||
return (
|
||||
<Grid container spacing={1}>
|
||||
<Grid item xs={12} align="center">
|
||||
<Typography component="h5" variant="h5">
|
||||
<b>{this.state.nickname ? "⚡"+this.state.nickname+"⚡" : ""}</b>
|
||||
</Typography>
|
||||
</Grid>
|
||||
<Grid item xs={12} align="center">
|
||||
<div style={{ maxWidth: 200, maxHeight: 200 }}>
|
||||
<Image className='newAvatar'
|
||||
disableError='true'
|
||||
cover='true'
|
||||
color='null'
|
||||
src={this.state.avatar_url}
|
||||
/>
|
||||
</div><br/>
|
||||
<Grid item xs={12} align="center">
|
||||
{this.state.showRobosat ?
|
||||
<div>
|
||||
<Grid item xs={12} align="center">
|
||||
<Typography component="h5" variant="h5">
|
||||
<b>{this.state.nickname ? "⚡"+this.state.nickname+"⚡" : ""}</b>
|
||||
</Typography>
|
||||
</Grid>
|
||||
<Grid item xs={12} align="center">
|
||||
<div style={{ maxWidth: 200, maxHeight: 200 }}>
|
||||
<Image className='newAvatar'
|
||||
disableError='true'
|
||||
cover='true'
|
||||
color='null'
|
||||
src={this.state.avatar_url}
|
||||
/>
|
||||
</div><br/>
|
||||
</Grid>
|
||||
</div>
|
||||
: <CircularProgress />}
|
||||
</Grid>
|
||||
{
|
||||
this.state.found ?
|
||||
@ -118,26 +147,36 @@ export default class UserGenPage extends Component {
|
||||
:
|
||||
""
|
||||
}
|
||||
<Grid item xs={12} align="center">
|
||||
<TextField
|
||||
error={this.state.bad_request}
|
||||
label='Token - Store safely'
|
||||
required='true'
|
||||
value={this.state.token}
|
||||
variant='standard'
|
||||
helperText={this.state.bad_request}
|
||||
size='small'
|
||||
// multiline = {true}
|
||||
onChange={this.handleChangeToken}
|
||||
/>
|
||||
<Grid container align="center">
|
||||
<Grid item xs={12} align="center">
|
||||
<IconButton onClick= {()=>navigator.clipboard.writeText(this.state.token)}>
|
||||
<ContentCopy/>
|
||||
</IconButton>
|
||||
<TextField
|
||||
//sx={{ input: { color: 'purple' } }}
|
||||
InputLabelProps={{
|
||||
style: { color: 'green' },
|
||||
}}
|
||||
error={this.state.bad_request}
|
||||
label='Store your token safely'
|
||||
required='true'
|
||||
value={this.state.token}
|
||||
variant='standard'
|
||||
helperText={this.state.bad_request}
|
||||
size='small'
|
||||
// multiline = {true}
|
||||
onChange={this.handleChangeToken}
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
<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 item xs={12} align="center">
|
||||
<ButtonGroup variant="contained" aria-label="outlined primary button group">
|
||||
<Button color='primary' to='/make/' component={Link}>Make Order</Button>
|
||||
<Button 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>
|
||||
</ButtonGroup>
|
||||
</Grid>
|
||||
|
33
frontend/src/components/getFlags.js
Normal file
33
frontend/src/components/getFlags.js
Normal 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 '🏳';
|
||||
};
|
@ -22,10 +22,12 @@
|
||||
"21": "CLP",
|
||||
"22": "CZK",
|
||||
"23": "DKK",
|
||||
"24": "HKR",
|
||||
"24": "HRK",
|
||||
"25": "HUF",
|
||||
"26": "INR",
|
||||
"27": "ISK",
|
||||
"28": "PLN",
|
||||
"29": "RON"
|
||||
"29": "RON",
|
||||
"30": "ARS",
|
||||
"31": "VES"
|
||||
}
|
@ -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.
|
||||
|
||||
## That’s 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 seller’s 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} doesn’t know anything about you and doesn’t 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.
|
@ -25,6 +25,18 @@ body {
|
||||
transform: translate(-50%,-50%);
|
||||
}
|
||||
|
||||
.bottomBar {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
height: 40px;
|
||||
}
|
||||
|
||||
.bottomItem {
|
||||
margin: 0;
|
||||
top: -14px;
|
||||
}
|
||||
|
||||
.newAvatar {
|
||||
background-color:white;
|
||||
border-radius: 50%;
|
||||
|
@ -1,6 +1,10 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<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 name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>RoboSats - Simple and Private Bitcoin Exchange</title>
|
||||
|
@ -1,6 +1,7 @@
|
||||
from django.shortcuts import render
|
||||
|
||||
from decouple import config
|
||||
# Create your views here.
|
||||
|
||||
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
140
requirements.txt
Normal 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
|
@ -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',)
|
44
robosats/celery/__init__.py
Normal file
44
robosats/celery/__init__.py
Normal 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
2
robosats/celery/conf.py
Normal file
@ -0,0 +1,2 @@
|
||||
# This sets the django-celery-results backend
|
||||
CELERY_RESULT_BACKEND = 'django-db'
|
@ -40,10 +40,13 @@ INSTALLED_APPS = [
|
||||
'django.contrib.staticfiles',
|
||||
'rest_framework',
|
||||
'channels',
|
||||
'django_celery_beat',
|
||||
'django_celery_results',
|
||||
'api',
|
||||
'chat',
|
||||
'frontend.apps.FrontendConfig',
|
||||
]
|
||||
from .celery.conf import *
|
||||
|
||||
MIDDLEWARE = [
|
||||
'django.middleware.security.SecurityMiddleware',
|
||||
|
@ -19,6 +19,6 @@ from django.urls import path, include
|
||||
urlpatterns = [
|
||||
path('admin/', admin.site.urls),
|
||||
path('api/', include('api.urls')),
|
||||
path('chat/', include('chat.urls')),
|
||||
# path('chat/', include('chat.urls')),
|
||||
path('', include('frontend.urls')),
|
||||
]
|
||||
|
16
setup.md
16
setup.md
@ -45,8 +45,18 @@ pip install channels
|
||||
pip install django-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
|
||||
|
||||
@ -112,8 +122,10 @@ npm install react-native
|
||||
npm install react-native-svg
|
||||
npm install react-qr-code
|
||||
npm install @mui/material
|
||||
npm install react-markdown
|
||||
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)
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user