Merge pull request #43 from Reckless-Satoshi/stabilize-runtime

Stabilize runtime. Add docker dev containers. Many fixes.
This commit is contained in:
Reckless_Satoshi 2022-02-13 18:28:38 +00:00 committed by GitHub
commit 3d6fac5367
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
41 changed files with 3497 additions and 469 deletions

View File

@ -1,20 +1,41 @@
# base64 ~/.lnd/tls.cert | tr -d '\n'
LND_CERT_BASE64=''
# base64 ~/.lnd/data/chain/bitcoin/testnet/admin.macaroon | tr -d '\n'
LND_MACAROON_BASE64=''
LND_GRPC_HOST='127.0.0.1:10009'
# LND directory to read TLS cert and macaroon
LND_DIR='/lnd/'
MACAROON_PATH='data/chain/bitcoin/testnet/admin.macaroon'
REDIS_URL=''
# LND directory can not be specified, instead cert and macaroon can be provided as base64 strings
# base64 ~/.lnd/tls.cert | tr -d '\n'
LND_CERT_BASE64='LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUNLVENDQWRDZ0F3SUJBZ0lRQ0VoeGpPZXY1bGQyVFNPTXhKalFvekFLQmdncWhrak9QUVFEQWpBNE1SOHcKSFFZRFZRUUtFeFpzYm1RZ1lYVjBiMmRsYm1WeVlYUmxaQ0JqWlhKME1SVXdFd1lEVlFRREV3d3dNakJtTVRnMQpZelkwTnpVd0hoY05Nakl3TWpBNE1UWXhOalV3V2hjTk1qTXdOREExTVRZeE5qVXdXakE0TVI4d0hRWURWUVFLCkV4WnNibVFnWVhWMGIyZGxibVZ5WVhSbFpDQmpaWEowTVJVd0V3WURWUVFERXd3d01qQm1NVGcxWXpZME56VXcKV1RBVEJnY3Foa2pPUFFJQkJnZ3Foa2pPUFFNQkJ3TkNBQVNJVWdkcVMrWFZKL3EzY0JZeWd6ZDc2endaanlmdQpLK3BzcWNYVkFyeGZjU2NXQ25jbXliNGRaMy9Lc3lLWlRaamlySDE3aEY0OGtIMlp5clRZSW9hZG80RzdNSUc0Ck1BNEdBMVVkRHdFQi93UUVBd0lDcERBVEJnTlZIU1VFRERBS0JnZ3JCZ0VGQlFjREFUQVBCZ05WSFJNQkFmOEUKQlRBREFRSC9NQjBHQTFVZERnUVdCQlEwWUJjZXdsd1BqYTJPRXFyTGxzZnJscEswUFRCaEJnTlZIUkVFV2pCWQpnZ3d3TWpCbU1UZzFZelkwTnpXQ0NXeHZZMkZzYUc5emRJSUVkVzVwZUlJS2RXNXBlSEJoWTJ0bGRJSUhZblZtClkyOXVib2NFZndBQUFZY1FBQUFBQUFBQUFBQUFBQUFBQUFBQUFZY0V3S2dRQW9jRUFBQUFBREFLQmdncWhrak8KUFFRREFnTkhBREJFQWlBd0dMY05qNXVZSkVwanhYR05OUnNFSzAwWmlSUUh2Qm50NHp6M0htWHBiZ0lnSWtvUQo3cHFvNGdWNGhiczdrSmt1bnk2bkxlNVg0ZzgxYjJQOW52ZnZ2bkk9Ci0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K'
# base64 ~/.lnd/data/chain/bitcoin/testnet/admin.macaroon | tr -d '\n'
LND_MACAROON_BASE64='AgEDbG5kAvgBAwoQsyI+PK+fyb7F2UyTeZ4seRIBMBoWCgdhZGRyZXNzEgRyZWFkEgV3cml0ZRoTCgRpbmZvEgRyZWFkEgV3cml0ZRoXCghpbnZvaWNlcxIEcmVhZBIFd3JpdGUaIQoIbWFjYXJvb24SCGdlbmVyYXRlEgRyZWFkEgV3cml0ZRoWCgdtZXNzYWdlEgRyZWFkEgV3cml0ZRoXCghvZmZjaGFpbhIEcmVhZBIFd3JpdGUaFgoHb25jaGFpbhIEcmVhZBIFd3JpdGUaFAoFcGVlcnMSBHJlYWQSBXdyaXRlGhgKBnNpZ25lchIIZ2VuZXJhdGUSBHJlYWQAAAYgMt90uD6v4truTadWCjlppoeJ4hZrL1SBb09Y+4WOiI0='
# Auto unlock LND password. Only used in development docker-compose environment.
# It will fail starting up the node without it.
# To disable auto unlock, comment out 'wallet-unlock-password-file=/tmp/pwd' from 'docker/lnd/lnd.conf'
AUTO_UNLOCK_PWD='1234'
# 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. robosats.com
HOST_NAME = ''
HOST_NAME2 = ''
LOCAL_ALIAS = 'e.g:my_garbage_server'
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = 'django-insecure-6^&6uw$b5^en%(cu2kc7_o)(mgpazx#j_znwlym0vxfamn2uo-'
# e.g. robotestagw3dcxmd66r4rgksb4nmmr43fh77bzn2ia2eucduyeafnyd.onion
ONION_LOCATION = ''
# Link to robosats alternative site (shown in frontend in statsfornerds so users can switch mainnet/testnet)
ALTERNATIVE_SITE = 'RoboSats6tkf3eva7x2voqso3a5wcorsnw34jveyxfqi2fu7oyheasid.onion'
ALTERNATIVE_NAME = 'RoboSats Mainnet'
# Lightning node open info, url to amboss and 1ML
NETWORK = 'testnet'
NODE_ALIAS = '🤖RoboSats⚡(RoboDevs)'
NODE_ID = '033b58d7......'
# Trade fee in percentage %
FEE = 0.002
# Bond size in percentage %

3
.gitignore vendored
View File

@ -637,7 +637,6 @@ FodyWeavers.xsd
# Django
*migrations*
frontend/static/frontend/main*
# Celery
django
@ -648,3 +647,5 @@ api/lightning/lightning*
api/lightning/invoices*
api/lightning/router*
api/lightning/googleapis*
frontend/static/admin*
frontend/static/rest_framework*

35
Dockerfile Normal file
View File

@ -0,0 +1,35 @@
FROM python:3.10.2-bullseye
RUN mkdir -p /usr/src/robosats
# specifying the working dir inside the container
WORKDIR /usr/src/robosats
RUN python -m pip install --upgrade pip
COPY requirements.txt ./
RUN pip install --no-cache-dir -r requirements.txt
# copy current dir's content to container's WORKDIR root i.e. all the contents of the robosats app
COPY . .
# fit lnd grpc services
RUN pip install grpcio grpcio-tools googleapis-common-protos
RUN cd api/lightning && git clone https://github.com/googleapis/googleapis.git
RUN cd api/lightning && curl -o lightning.proto -s https://raw.githubusercontent.com/lightningnetwork/lnd/master/lnrpc/lightning.proto
RUN cd api/lightning && python3 -m grpc_tools.protoc --proto_path=googleapis:. --python_out=. --grpc_python_out=. lightning.proto
RUN cd api/lightning && curl -o invoices.proto -s https://raw.githubusercontent.com/lightningnetwork/lnd/master/lnrpc/invoicesrpc/invoices.proto
RUN cd api/lightning && python3 -m grpc_tools.protoc --proto_path=googleapis:. --python_out=. --grpc_python_out=. invoices.proto
RUN cd api/lightning && curl -o router.proto -s https://raw.githubusercontent.com/lightningnetwork/lnd/master/lnrpc/routerrpc/router.proto
RUN cd api/lightning && python3 -m grpc_tools.protoc --proto_path=googleapis:. --python_out=. --grpc_python_out=. router.proto
# patch generated files relative imports
RUN sed -i 's/^import .*_pb2 as/from . \0/' api/lightning/router_pb2.py
RUN sed -i 's/^import .*_pb2 as/from . \0/' api/lightning/invoices_pb2.py
RUN sed -i 's/^import .*_pb2 as/from . \0/' api/lightning/router_pb2_grpc.py
RUN sed -i 's/^import .*_pb2 as/from . \0/' api/lightning/lightning_pb2_grpc.py
RUN sed -i 's/^import .*_pb2 as/from . \0/' api/lightning/invoices_pb2_grpc.py
EXPOSE 8000
CMD ["python3", "manage.py", "runserver", "0.0.0.0:8000"]

View File

@ -41,7 +41,7 @@ class LNPaymentAdmin(AdminChangeLinksMixin, admin.ModelAdmin):
@admin.register(Profile)
class UserProfileAdmin(AdminChangeLinksMixin, admin.ModelAdmin):
list_display = ('avatar_tag','id','user_link','total_contracts','total_ratings','avg_rating','num_disputes','lost_disputes')
list_display = ('avatar_tag','id','user_link','total_contracts','platform_rating','total_ratings','avg_rating','num_disputes','lost_disputes')
list_display_links = ('avatar_tag','id')
change_links =['user']
readonly_fields = ['avatar_tag']

View File

@ -1,4 +1,4 @@
import grpc, os, hashlib, secrets, json
import grpc, os, hashlib, secrets
from . import lightning_pb2 as lnrpc, lightning_pb2_grpc as lightningstub
from . import invoices_pb2 as invoicesrpc, invoices_pb2_grpc as invoicesstub
from . import router_pb2 as routerrpc, router_pb2_grpc as routerstub
@ -14,8 +14,18 @@ from api.models import LNPayment
# Should work with LND (c-lightning in the future if there are features that deserve the work)
#######
CERT = b64decode(config('LND_CERT_BASE64'))
MACAROON = b64decode(config('LND_MACAROON_BASE64'))
# Read tls.cert from file or .env variable string encoded as base64
try:
CERT = open(os.path.join(config('LND_DIR'),'tls.cert'), 'rb').read()
except:
CERT = b64decode(config('LND_CERT_BASE64'))
# Read macaroon from file or .env variable string encoded as base64
try:
MACAROON = open(os.path.join(config('LND_DIR'), config('MACAROON_path')), 'rb').read()
except:
MACAROON = b64decode(config('LND_MACAROON_BASE64'))
LND_GRPC_HOST = config('LND_GRPC_HOST')
class LNNode():
@ -122,22 +132,11 @@ class LNNode():
lnpayment.save()
return True
# @classmethod
# def check_until_invoice_locked(cls, payment_hash, expiration):
# '''Checks until hold invoice is locked.
# When invoice is locked, returns true.
# If time expires, return False.'''
# # Experimental, might need asyncio. Best if subscribing all invoices and running a background task
# # Maybe best to pass LNpayment object and change status live.
# request = invoicesrpc.SubscribeSingleInvoiceRequest(r_hash=payment_hash)
# for invoice in cls.invoicesstub.SubscribeSingleInvoice(request):
# print(invoice)
# if timezone.now > expiration:
# break
# if invoice.state == 3: # True if hold invoice is accepted.
# return True
# return False
@classmethod
def resetmc(cls):
request = routerrpc.ResetMissionControlRequest()
response = cls.routerstub.ResetMissionControl(request, metadata=[('macaroon', MACAROON.hex())])
return True
@classmethod

View File

@ -1,6 +1,7 @@
from datetime import timedelta
from django.utils import timezone
from api.lightning.node import LNNode
from django.db.models import Q
from api.models import Order, LNPayment, MarketTick, User, Currency
from decouple import config
@ -30,7 +31,8 @@ FIAT_EXCHANGE_DURATION = int(config('FIAT_EXCHANGE_DURATION'))
class Logics():
def validate_already_maker_or_taker(user):
@classmethod
def validate_already_maker_or_taker(cls, 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,
@ -45,6 +47,14 @@ class Logics():
queryset = Order.objects.filter(taker=user, status__in=active_order_status)
if queryset.exists():
return False, {'bad_request':'You are already taker of an active order'}, queryset[0]
# Edge case when the user is in an order that is failing payment and he is the buyer
queryset = Order.objects.filter( Q(maker=user) | Q(taker=user), status=Order.Status.FAI)
if queryset.exists():
order = queryset[0]
if cls.is_buyer(order, user):
return False, {'bad_request':'You are still pending a payment from a recent order'}, order
return True, None, None
def validate_order_size(order):
@ -55,6 +65,14 @@ class Logics():
return False, {'bad_request': 'Your order is too small. It is worth '+'{:,}'.format(order.t0_satoshis)+' Sats now, but the limit is '+'{:,}'.format(MIN_TRADE)+ ' Sats'}
return True, None
def user_activity_status(last_seen):
if last_seen > (timezone.now() - timedelta(minutes=2)):
return 'Active'
elif last_seen > (timezone.now() - timedelta(minutes=10)):
return 'Seen recently'
else:
return 'Inactive'
@classmethod
def take(cls, order, user):
is_penalized, time_out = cls.is_penalized(user)
@ -284,7 +302,7 @@ class Logics():
return False, {'bad_request':'Only the buyer of this order can provide a buyer invoice.'}
if not order.taker_bond:
return False, {'bad_request':'Wait for your order to be taken.'}
if not (order.taker_bond.status == order.maker_bond.status == LNPayment.Status.LOCKED):
if not (order.taker_bond.status == order.maker_bond.status == LNPayment.Status.LOCKED) and not order.status == Order.Status.FAI:
return False, {'bad_request':'You cannot submit a invoice while bonds are not locked.'}
num_satoshis = cls.payout_amount(order, user)[1]['invoice_amount']
@ -329,9 +347,12 @@ class Logics():
# If the order status is 'Failed Routing'. Retry payment.
if order.status == Order.Status.FAI:
# Double check the escrow is settled.
if LNNode.double_check_htlc_is_settled(order.trade_escrow.payment_hash):
follow_send_payment(order.payout)
order.status = Order.Status.PAY
order.payout.status = LNPayment.Status.FLIGHT
order.payout.routing_attempts = 0
order.payout.save()
order.save()
order.save()
return True, None
@ -418,6 +439,7 @@ class Logics():
elif order.status in [Order.Status.PUB, Order.Status.TAK, Order.Status.WF2, Order.Status.WFE] and order.maker == user:
#Settle the maker bond (Maker loses the bond for canceling an ongoing trade)
valid = cls.settle_bond(order.maker_bond)
cls.return_bond(order.taker_bond) # returns taker bond
if valid:
order.status = Order.Status.UCA
order.save()
@ -508,13 +530,20 @@ class Logics():
order.last_satoshis = cls.satoshis_now(order)
bond_satoshis = int(order.last_satoshis * BOND_SIZE)
description = f"RoboSats - Publishing '{str(order)}' - This is a maker bond, it will freeze in your wallet temporarily and automatically return. It will be charged if you cheat or cancel."
description = f"RoboSats - Publishing '{str(order)}' - Maker bond - This payment WILL FREEZE IN YOUR WALLET, check on the website if it was successful. It will automatically return unless you cheat or cancel unilaterally."
# Gen hold Invoice
hold_payment = LNNode.gen_hold_invoice(bond_satoshis,
description,
invoice_expiry=Order.t_to_expire[Order.Status.WFB],
cltv_expiry_secs=BOND_EXPIRY*3600)
try:
hold_payment = LNNode.gen_hold_invoice(bond_satoshis,
description,
invoice_expiry=Order.t_to_expire[Order.Status.WFB],
cltv_expiry_secs=BOND_EXPIRY*3600)
except Exception as e:
print(str(e))
if 'failed to connect to all addresses' in str(e):
return False, {'bad_request':'The Lightning Network Daemon (LND) is down. Write in the Telegram group to make sure the staff is aware.'}
if 'wallet locked' in str(e):
return False, {'bad_request':"This is weird, RoboSats' lightning wallet is locked. Check in the Telegram group, maybe the staff has died."}
order.maker_bond = LNPayment.objects.create(
concept = LNPayment.Concepts.MAKEBOND,
@ -589,13 +618,18 @@ class Logics():
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 {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.")
+ " - Taker bond - This payment WILL FREEZE IN YOUR WALLET, check on the website if it was successful. It will automatically return unless you cheat or cancel unilaterally.")
# Gen hold Invoice
hold_payment = LNNode.gen_hold_invoice(bond_satoshis,
description,
invoice_expiry=Order.t_to_expire[Order.Status.TAK],
cltv_expiry_secs=BOND_EXPIRY*3600)
try:
hold_payment = LNNode.gen_hold_invoice(bond_satoshis,
description,
invoice_expiry=Order.t_to_expire[Order.Status.TAK],
cltv_expiry_secs=BOND_EXPIRY*3600)
except Exception as e:
if 'status = StatusCode.UNAVAILABLE' in str(e):
return False, {'bad_request':'The Lightning Network Daemon (LND) is down. Write in the Telegram group to make sure the staff is aware.'}
order.taker_bond = LNPayment.objects.create(
concept = LNPayment.Concepts.TAKEBOND,
@ -654,13 +688,19 @@ class Logics():
# 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."
description = f"RoboSats - Escrow amount for '{str(order)}' - It WILL FREEZE IN YOUR WALLET. It will be released to the buyer once you confirm you received the fiat. It will automatically return if buyer does not confirm the payment."
# Gen hold Invoice
hold_payment = LNNode.gen_hold_invoice(escrow_satoshis,
description,
invoice_expiry=Order.t_to_expire[Order.Status.WF2],
cltv_expiry_secs=ESCROW_EXPIRY*3600)
try:
hold_payment = LNNode.gen_hold_invoice(escrow_satoshis,
description,
invoice_expiry=Order.t_to_expire[Order.Status.WF2],
cltv_expiry_secs=ESCROW_EXPIRY*3600)
except Exception as e:
if 'status = StatusCode.UNAVAILABLE' in str(e):
return False, {'bad_request':'The Lightning Network Daemon (LND) is down. Write in the Telegram group to make sure the staff is aware.'}
order.trade_escrow = LNPayment.objects.create(
concept = LNPayment.Concepts.TRESCROW,
@ -776,13 +816,20 @@ class Logics():
# RETURN THE BONDS // Probably best also do it even if payment failed
cls.return_bond(order.taker_bond)
cls.return_bond(order.maker_bond)
is_payed, context = follow_send_payment(order.payout) ##### !!! KEY LINE - PAYS THE BUYER INVOICE !!!
if is_payed:
order.save()
return True, context
else:
# error handling here
return False, context
##### !!! KEY LINE - PAYS THE BUYER INVOICE !!!
##### Backgroun process "follow_invoices" will try to pay this invoice until success
order.status = Order.Status.PAY
order.payout.status = LNPayment.Status.FLIGHT
order.payout.save()
order.save()
return True, None
# is_payed, context = follow_send_payment(order.payout) ##### !!! KEY LINE - PAYS THE BUYER INVOICE !!!
# if is_payed:
# order.save()
# return True, context
# else:
# # error handling here
# return False, context
else:
return False, {'bad_request':'You cannot confirm the fiat payment at this stage'}
@ -810,3 +857,9 @@ class Logics():
return False, {'bad_request':'You cannot rate your counterparty yet.'}
return True, None
@classmethod
def rate_platform(cls, user, rating):
user.profile.platform_rating = rating
user.profile.save()
return True, None

View File

@ -27,11 +27,11 @@ class Command(BaseCommand):
try:
self.follow_hold_invoices()
self.retry_payments()
except Exception as e:
if 'database is locked' in str(e):
self.stdout.write('database is locked')
self.stdout.write(str(e))
try:
self.send_payments()
except Exception as e:
self.stdout.write(str(e))
def follow_hold_invoices(self):
@ -117,18 +117,32 @@ class Command(BaseCommand):
self.stdout.write(str(timezone.now()))
self.stdout.write(str(debug))
def retry_payments(self):
''' Checks if any payment is due for retry, and tries to pay it'''
def send_payments(self):
'''
Checks for invoices that are due to pay; i.e., INFLIGHT status and 0 routing_attempts.
Checks if any payment is due for retry, and tries to pay it.
'''
queryset = LNPayment.objects.filter(type=LNPayment.Types.NORM,
status=LNPayment.Status.FLIGHT,
routing_attempts=0)
queryset_retries = LNPayment.objects.filter(type=LNPayment.Types.NORM,
status__in=[LNPayment.Status.VALIDI, LNPayment.Status.FAILRO],
routing_attempts__lt=4,
routing_attempts__lt=5,
last_routing_time__lt=(timezone.now()-timedelta(minutes=int(config('RETRY_TIME')))))
queryset = queryset.union(queryset_retries)
for lnpayment in queryset:
success, _ = follow_send_payment(lnpayment)
success, _ = follow_send_payment(lnpayment) # Do follow_send_payment.delay() for further concurrency.
# If failed, reset mision control. (This won't scale well, just a temporary fix)
if not success:
LNNode.resetmc()
# If already 3 attempts and last failed. Make it expire (ask for a new invoice) an reset attempts.
if not success and lnpayment.routing_attempts == 3:
if not success and lnpayment.routing_attempts > 2:
lnpayment.status = LNPayment.Status.EXPIRE
lnpayment.routing_attempts = 0
lnpayment.save()

View File

@ -6,6 +6,7 @@ from django.template.defaultfilters import truncatechars
from django.dispatch import receiver
from django.utils.html import mark_safe
import uuid
from django.conf import settings
from decouple import config
from pathlib import Path
@ -169,6 +170,8 @@ class Order(models.Model):
# ratings
maker_rated = models.BooleanField(default=False, null=False)
taker_rated = models.BooleanField(default=False, null=False)
maker_platform_rated = models.BooleanField(default=False, null=False)
taker_platform_rated = models.BooleanField(default=False, null=False)
t_to_expire = {
0 : int(config('EXP_MAKER_BOND_INVOICE')) , # 'Waiting for maker bond'
@ -207,7 +210,6 @@ def delete_lnpayment_at_order_deletion(sender, instance, **kwargs):
pass
class Profile(models.Model):
user = models.OneToOneField(User,on_delete=models.CASCADE)
# Total trades
@ -225,11 +227,14 @@ class Profile(models.Model):
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)
avatar = models.ImageField(default=(settings.STATIC_ROOT+"unknown_avatar.png"), verbose_name='Avatar', blank=True)
# Penalty expiration (only used then taking/cancelling repeatedly orders in the book before comitting bond)
penalty_expiration = models.DateTimeField(null=True,default=None, blank=True)
# Platform rate
platform_rating = models.PositiveIntegerField(null=True, default=None, blank=True)
@receiver(post_save, sender=User)
def create_user_profile(sender, instance, created, **kwargs):
if created:
@ -242,7 +247,7 @@ class Profile(models.Model):
@receiver(pre_delete, sender=User)
def del_avatar_from_disk(sender, instance, **kwargs):
try:
avatar_file=Path('frontend/' + instance.profile.avatar.url)
avatar_file=Path(settings.AVATAR_ROOT + instance.profile.avatar.url)
avatar_file.unlink()
except:
pass
@ -253,7 +258,7 @@ class Profile(models.Model):
# to display avatars in admin panel
def get_avatar(self):
if not self.avatar:
return 'static/assets/misc/unknown_avatar.png'
return settings.STATIC_ROOT + 'unknown_avatar.png'
return self.avatar.url
# method to create a fake table field in read only mode

View File

@ -14,5 +14,5 @@ class MakeOrderSerializer(serializers.ModelSerializer):
class UpdateOrderSerializer(serializers.Serializer):
invoice = serializers.CharField(max_length=2000, allow_null=True, allow_blank=True, default=None)
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)
action = serializers.ChoiceField(choices=('take','update_invoice','submit_statement','dispute','cancel','confirm','rate_user','rate_platform'), allow_null=False)
rating = serializers.ChoiceField(choices=('1','2','3','4','5'), allow_null=True, allow_blank=True, default=None)

View File

@ -41,12 +41,10 @@ def follow_send_payment(lnpayment):
from django.utils import timezone
from datetime import timedelta
from api.lightning.node import LNNode
from api.lightning.node import LNNode, MACAROON
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
fee_limit_sat = int(max(lnpayment.num_satoshis * float(config('PROPORTIONAL_ROUTING_FEE_LIMIT')), float(config('MIN_FLAT_ROUTING_FEE_LIMIT')))) # 200 ppm or 10 sats
request = LNNode.routerrpc.SendPaymentRequest(
payment_request=lnpayment.invoice,
fee_limit_sat=fee_limit_sat,
@ -77,7 +75,6 @@ def follow_send_payment(lnpayment):
order.save()
context = {'routing_failed': LNNode.payment_failure_context[response.failure_reason]}
print(context)
# Call a retry in 5 mins here?
return False, context
if response.status == 2 : # Status 2 'SUCCEEDED'

View File

@ -50,23 +50,32 @@ def get_exchange_rates(currencies):
return median_rates.tolist()
lnd_v_cache = {}
@ring.dict(lnd_v_cache, expire=3600) #keeps in cache for 3600 seconds
def get_lnd_version():
stream = os.popen('lnd --version')
lnd_version = stream.read()[:-1]
# If dockerized, return LND_VERSION envvar used for docker image.
# Otherwise it would require LND's version.grpc libraries...
try:
lnd_version = config('LND_VERSION')
return lnd_version
except:
pass
return lnd_version
# If not dockerized, and LND is local, read from CLI
try:
stream = os.popen('lnd --version')
lnd_version = stream.read()[:-1]
return lnd_version
except:
return ''
robosats_commit_cache = {}
@ring.dict(robosats_commit_cache, expire=3600)
def get_commit_robosats():
stream = os.popen('git log -n 1 --pretty=format:"%H"')
lnd_version = stream.read()
commit = os.popen('git log -n 1 --pretty=format:"%H"')
commit_hash = commit.read()
return lnd_version
return commit_hash
premium_percentile = {}
@ring.dict(premium_percentile, expire=300)

View File

@ -1,3 +1,4 @@
import os
from re import T
from django.db.models import query
from rest_framework import status, viewsets
@ -22,13 +23,15 @@ import hashlib
from pathlib import Path
from datetime import timedelta, datetime
from django.utils import timezone
from django.conf import settings
from decouple import config
EXP_MAKER_BOND_INVOICE = int(config('EXP_MAKER_BOND_INVOICE'))
FEE = float(config('FEE'))
RETRY_TIME = int(config('RETRY_TIME'))
avatar_path = Path('frontend/static/assets/avatars')
avatar_path = Path(settings.AVATAR_ROOT)
avatar_path.mkdir(parents=True, exist_ok=True)
# Create your views here.
@ -136,20 +139,9 @@ class OrderView(viewsets.ViewSet):
# Add activity status of participants based on last_seen
if order.taker_last_seen != None:
if order.taker_last_seen > (timezone.now() - timedelta(minutes=2)):
data['taker_status'] = 'active'
elif order.taker_last_seen > (timezone.now() - timedelta(minutes=10)):
data['taker_status'] = 'seen_recently'
else:
data['taker_status'] = 'inactive'
data['taker_status'] = Logics.user_activity_status(order.taker_last_seen)
if order.maker_last_seen != None:
if order.maker_last_seen > (timezone.now() - timedelta(minutes=2)):
data['maker_status'] = 'active'
elif order.maker_last_seen > (timezone.now() - timedelta(minutes=10)):
data['maker_status'] = 'seen_recently'
else:
data['maker_status'] = 'inactive'
data['maker_status'] = Logics.user_activity_status(order.maker_last_seen)
# 3.b If order is between public and WF2
if order.status >= Order.Status.PUB and order.status < Order.Status.WF2:
@ -157,7 +149,6 @@ class OrderView(viewsets.ViewSet):
# 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))
@ -287,7 +278,7 @@ 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)'submit_statement' (in dispute), 7)'rate' (counterparty)
# 6)'submit_statement' (in dispute), 7)'rate_user' , 'rate_platform'
action = serializer.data.get('action')
invoice = serializer.data.get('invoice')
statement = serializer.data.get('statement')
@ -335,10 +326,15 @@ class OrderView(viewsets.ViewSet):
if not valid: return Response(context, status.HTTP_400_BAD_REQUEST)
# 6) If action is rate
elif action == 'rate' and rating:
elif action == 'rate_user' and rating:
valid, context = Logics.rate_counterparty(order,request.user, rating)
if not valid: return Response(context, status.HTTP_400_BAD_REQUEST)
# 6) If action is rate_platform
elif action == 'rate_platform' and rating:
valid, context = Logics.rate_platform(request.user, rating)
if not valid: return Response(context, status.HTTP_400_BAD_REQUEST)
# If nothing of the above... something else is going on. Probably not allowed!
else:
return Response(
@ -419,7 +415,7 @@ class UserView(APIView):
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)
user.profile.avatar = str(image_path)[9:] # removes frontend/ from url (ugly, to be fixed)
user.profile.avatar = nickname + '.png'
login(request, user)
return Response(context, status=status.HTTP_201_CREATED)
@ -427,9 +423,9 @@ class UserView(APIView):
user = authenticate(request, username=nickname, password=token)
if user is not None:
login(request, user)
# Sends the welcome back message, only if created +30 mins ago
if request.user.date_joined < (timezone.now()-timedelta(minutes=30)):
context['found'] = 'We found your Robosat. Welcome back!'
# Sends the welcome back message, only if created +3 mins ago
if request.user.date_joined < (timezone.now()-timedelta(minutes=3)):
context['found'] = 'We found your Robot avatar. Welcome back!'
return Response(context, status=status.HTTP_202_ACCEPTED)
else:
# It is unlikely, but maybe the nickname is taken (1 in 20 Billion change)
@ -484,7 +480,7 @@ class BookView(ListAPIView):
# Compute current premium for those orders that are explicitly priced.
data['price'], data['premium'] = Logics.price_and_premium_now(order)
data['maker_status'] = Logics.user_activity_status(order.maker_last_seen)
for key in ('status','taker'): # Non participants should not see the status or who is the taker
del data[key]
@ -534,6 +530,11 @@ class InfoView(ListAPIView):
context['lifetime_satoshis_settled'] = lifetime_volume_settled
context['lnd_version'] = get_lnd_version()
context['robosats_running_commit_hash'] = get_commit_robosats()
context['alternative_site'] = config('ALTERNATIVE_SITE')
context['alternative_name'] = config('ALTERNATIVE_NAME')
context['node_alias'] = config('NODE_ALIAS')
context['node_id'] = config('NODE_ID')
context['network'] = config('NETWORK')
context['fee'] = FEE
context['bond_size'] = float(config('BOND_SIZE'))
if request.user.is_authenticated:

119
docker-compose.yml Normal file
View File

@ -0,0 +1,119 @@
version: '3.9'
services:
redis:
image: redis:6.2.6
container_name: redis
restart: always
volumes:
- redisdata:/data
network_mode: service:tor
backend:
build: .
container_name: django-dev
restart: always
depends_on:
- bitcoind
- lnd
- redis
environment:
DEVELOPMENT: 1
volumes:
- .:/usr/src/robosats
- /mnt/development/database:/usr/src/database
- /mnt/development/lnd:/lnd
network_mode: service:tor
frontend:
build: ./frontend
container_name: npm-dev
restart: always
volumes:
- ./frontend:/usr/src/frontend
clean-orders:
build: .
restart: always
container_name: clord-dev
command: python3 manage.py clean_orders
volumes:
- .:/usr/src/robosats
- /mnt/development/database:/usr/src/database
follow-invoices:
build: .
container_name: invo-dev
restart: always
depends_on:
- bitcoind
- lnd
command: python3 manage.py follow_invoices
volumes:
- .:/usr/src/robosats
- /mnt/development/database:/usr/src/database
- /mnt/development/lnd:/lnd
network_mode: service:tor
celery:
build: .
container_name: cele-dev
restart: always
command: celery -A robosats worker --beat -l info -S django
environment:
REDIS_URL: redis://localhost:6379
volumes:
- .:/usr/src/robosats
- /mnt/development/database:/usr/src/database
network_mode: service:tor
tor:
build: ./docker/tor
container_name: tor-dev
restart: always
environment:
LOCAL_USER_ID: 1000
LOCAL_GROUP_ID: 1000
volumes:
- /mnt/development/tor/data:/var/lib/tor
- /mnt/development/tor/config:/etc/tor
ports:
- 8000:8000
lnd:
build: ./docker/lnd
restart: always
network_mode: service:tor
container_name: lnd-dev
depends_on:
- tor
- bitcoind
volumes:
- /mnt/development/tor/data:/var/lib/tor
- /mnt/development/tor/config:/etc/tor
- /mnt/development/lnd:/home/lnd/.lnd
- /mnt/development/lnd:/root/.lnd
command: lnd
environment:
LOCAL_USER_ID: 1000
LOCAL_GROUP_ID: 1000
LND_RPC_PORT: 10009
LND_REST_PORT: 8080
AUTO_UNLOCK_PWD: ${AUTO_UNLOCK_PWD}
bitcoind:
build: ./docker/bitcoind
container_name: btc-dev
restart: always
environment:
LOCAL_USER_ID: 1000
LOCAL_GROUP_ID: 1000
depends_on:
- tor
network_mode: service:tor
volumes:
- /mnt/development/tor/data:/var/lib/tor:ro
- /mnt/development/tor/config:/etc/tor:ro
- /mnt/development/bitcoin:/home/bitcoin/.bitcoin
volumes:
redisdata:

View File

@ -0,0 +1,15 @@
FROM ruimarinho/bitcoin-core:22-alpine
ARG LOCAL_USER_ID=9999
ARG LOCAL_GROUP_ID=9999
# Set the expected local user id
# for shared group to access tor cookie
RUN apk --no-cache --no-progress add shadow=~4 gettext=~0.21 && \
groupadd -g "$LOCAL_GROUP_ID" bitcoin && \
usermod -u "$LOCAL_USER_ID" -g bitcoin bitcoin
COPY entrypoint.sh /root/entrypoint.sh
COPY bitcoin.conf /tmp/bitcoin.conf
ENTRYPOINT [ "/root/entrypoint.sh" ]
CMD ["bitcoind"]

View File

@ -0,0 +1,33 @@
# Reference: https://en.bitcoin.it/wiki/Running_Bitcoin
# https://github.com/bitcoin/bitcoin/blob/master/share/examples/bitcoin.conf
server=1
txindex=1
onion=127.0.0.1:9050
torcontrol=127.0.0.1:9051
rpcuser=robodev
rpcpassword=robodev
zmqpubrawblock=tcp://127.0.0.1:18501
zmqpubrawtx=tcp://127.0.0.1:18502
# Allow RPC connections from outside of container localhost
rpcbind=0.0.0.0
# Only connect to typical docker IP addresses (Usually from docker host computer)
rpcallowip=172.0.0.0/255.0.0.0
# Allow access from any IP address (Usually from another computer on LAN)
#rpcallowip=0.0.0.0/0
# Run on the test network instead of the real bitcoin network.
testnet=1
[main]
# Only run on Tor
onlynet=onion
# Add Tor seed nodes
addnode=i4x66albngo3sg3w.onion:8333
# Some testnet settings needed for 0.19, if using testnet
[test]
# Allow RPC connections from outside of container localhost
rpcbind=0.0.0.0

21
docker/bitcoind/entrypoint.sh Executable file
View File

@ -0,0 +1,21 @@
#!/bin/sh
set -e
# Create bitcoin.conf if it doesn't exist
if [ ! -f "/home/bitcoin/.bitcoin/bitcoin.conf" ]; then
envsubst < /tmp/bitcoin.conf > /home/bitcoin/.bitcoin/bitcoin.conf
fi
_USER_ID="$(id -u)"
# Change local user id and group
if [ -n "${LOCAL_USER_ID:?}" ] && [ "$_USER_ID" != "${LOCAL_USER_ID:?}" ]; then
usermod -u "${LOCAL_USER_ID:?}" bitcoin
fi
groupmod -g "${LOCAL_GROUP_ID:?}" bitcoin
# Fix ownership
chown -R bitcoin /home/bitcoin
# Run original entrypoint
exec /entrypoint.sh "$@"

18
docker/lnd/Dockerfile Normal file
View File

@ -0,0 +1,18 @@
FROM lightninglabs/lnd:v0.14.2-beta
ARG LOCAL_USER_ID=9999
ARG LOCAL_GROUP_ID=9999
USER root
RUN adduser --disabled-password lnd
# Set the expected local user id
# for shared group to access tor cookie
RUN apk --no-cache --no-progress add shadow=~4 sudo=~1 gettext=~0.21 && \
usermod -u "$LOCAL_USER_ID" lnd && \
groupmod -g "$LOCAL_GROUP_ID" lnd
USER root
COPY entrypoint.sh /root/entrypoint.sh
COPY lnd.conf /tmp/lnd.conf
ENTRYPOINT [ "/root/entrypoint.sh" ]

18
docker/lnd/entrypoint.sh Executable file
View File

@ -0,0 +1,18 @@
#!/bin/sh
set -e
# Create lnd.conf if it doesn't exist
if [ ! -f "/home/lnd/.lnd/lnd.conf" ]; then
envsubst < /tmp/lnd.conf > /home/lnd/.lnd/lnd.conf
fi
# Change local user id and group
usermod -u "${LOCAL_USER_ID:?}" lnd
groupmod -g "${LOCAL_GROUP_ID:?}" lnd
# Fix ownership
chown -R lnd /home/lnd
echo ${AUTO_UNLOCK_PWD} > /tmp/pwd
# Start lnd
exec sudo -u lnd "$@"

31
docker/lnd/lnd.conf Normal file
View File

@ -0,0 +1,31 @@
# Reference: https://github.com/lightningnetwork/lnd/blob/master/sample-lnd.conf
debuglevel=info
alias=🤖RoboSats⚡(RoboDevs)
color=#4126a7
maxpendingchannels=6
bitcoin.active=1
bitcoin.testnet=1
bitcoin.node=bitcoind
bitcoind.rpcuser=bitcoindrobodevtestnet3
bitcoind.rpcpass=bitcoindrobodevtestnet3
bitcoind.zmqpubrawblock=tcp://127.0.0.1:18501
bitcoind.zmqpubrawtx=tcp://127.0.0.1:18502
wallet-unlock-password-file=/tmp/pwd
# Neutrino
neutrino.connect=faucet.lightning.community
# Configuring Tor docs:
# https://github.com/lightningnetwork/lnd/blob/master/docs/configuring_tor.md
tor.active=1
tor.v3=1
# Listening port will need to be changed if multiple LND instances are running
listen=localhost:9735
# Allow connection to gRPC from host
rpclisten=0.0.0.0:10009
restlisten=0.0.0.0:8080
tlsextraip=0.0.0.0

21
docker/tor/Dockerfile Normal file
View File

@ -0,0 +1,21 @@
FROM alpine:3
RUN apk --no-cache --no-progress add tor=~0.4
EXPOSE 9001 9050
# hadolint ignore=DL3002
USER root
ARG LOCAL_USER_ID=9999
ENV TOR_DATA=/var/lib/tor
# Add useradd and usermod
# Create user account (UID will be changed in entrypoint script)
RUN apk --no-cache --no-progress add shadow=~4 sudo=~1 && \
useradd -u $LOCAL_USER_ID --shell /bin/sh -m alice && \
usermod -g alice tor
COPY entrypoint.sh /root/entrypoint.sh
COPY torrc /tmp/torrc
ENTRYPOINT [ "/root/entrypoint.sh" ]

18
docker/tor/entrypoint.sh Executable file
View File

@ -0,0 +1,18 @@
#!/bin/sh
set -e
# Create torrc if it doesn't exist
if [ ! -f "/etc/tor/torrc" ]; then
cp /tmp/torrc /etc/tor/torrc
fi
# Change local user id and group
usermod -u "${LOCAL_USER_ID:?}" alice
groupmod -g "${LOCAL_GROUP_ID:?}" alice
# Set correct owners on volumes
chown -R tor:alice "${TOR_DATA}"
chown -R :alice /etc/tor
chown -R alice:alice /home/alice
exec sudo -u tor /usr/bin/tor

12
docker/tor/torrc Normal file
View File

@ -0,0 +1,12 @@
Log notice file /var/log/tor/notices.log
## The directory for keeping all the keys/etc. By default, we store
## things in $HOME/.tor on Unix, and in Application Data\tor on Windows.
DataDirectory /var/lib/tor
DataDirectoryGroupReadable 1
## Enable ControlPort
ControlPort 9051
CookieAuthentication 1
CookieAuthFileGroupReadable 1
CookieAuthFile /var/lib/tor/control_auth_cookie

21
frontend/Dockerfile Normal file
View File

@ -0,0 +1,21 @@
FROM ubuntu:20.04
ARG DEBIAN_FRONTEND=noninteractive
RUN mkdir -p /usr/src/frontend
# specifying the working dir inside the container
WORKDIR /usr/src/frontend
# copy current dir's content to container's WORKDIR root i.e. all the contents of the robosats app
COPY . .
RUN apt-get update
RUN apt-get -y install npm
# packages we use
RUN npm install
# launch
CMD ["npm", "run", "dev"]

View File

@ -1,8 +1,9 @@
import React, { Component } from "react";
import { Paper, Button , CircularProgress, ListItemButton, Typography, Grid, Select, MenuItem, FormControl, FormHelperText, List, ListItem, ListItemText, Avatar, RouterLink, ListItemAvatar} from "@mui/material";
import { Badge, Tooltip, Paper, Button , CircularProgress, ListItemButton, Typography, Grid, Select, MenuItem, FormControl, FormHelperText, List, ListItem, ListItemText, Avatar, RouterLink, ListItemAvatar, IconButton} from "@mui/material";
import { Link } from 'react-router-dom'
import { DataGrid } from '@mui/x-data-grid';
import MediaQuery from 'react-responsive'
import Image from 'material-ui-image'
import getFlags from './getFlags'
@ -69,6 +70,13 @@ export default class BookPage extends Component {
return x.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",");
}
// Colors for the status badges
statusBadgeColor(status){
if(status=='Active'){return("success")}
if(status=='Seen recently'){return("warning")}
if(status=='Inactive'){return('error')}
}
bookListTableDesktop=()=>{
return (
<div style={{ height: 475, width: '100%' }}>
@ -77,8 +85,9 @@ export default class BookPage extends Component {
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",
robot: order.maker_nick,
robot_status: order.maker_status,
type: order.type ? "Seller": "Buyer",
amount: parseFloat(parseFloat(order.amount).toFixed(4)),
currency: this.getCurrencyCode(order.currency),
payment_method: order.payment_method,
@ -89,22 +98,34 @@ export default class BookPage extends Component {
columns={[
// { field: 'id', headerName: 'ID', width: 40 },
{ field: 'robosat', headerName: 'RoboSat', width: 240,
{ field: 'robot', headerName: 'Robot', width: 240,
renderCell: (params) => {return (
<ListItemButton style={{ cursor: "pointer" }}>
<ListItemAvatar>
<Avatar className="flippedSmallAvatar" alt={params.row.robosat} src={params.row.avatar} />
<Tooltip placement="right" enterTouchDelay="0" title={params.row.robot_status}>
<Badge variant="dot" overlap="circular" badgeContent="" color={this.statusBadgeColor(params.row.robot_status)}>
<div style={{ width: 45, height: 45 }}>
<Image className='bookAvatar'
disableError='true'
disableSpinner='true'
color='null'
alt={params.row.robot}
src={params.row.avatar}
/>
</div>
</Badge>
</Tooltip>
</ListItemAvatar>
<ListItemText primary={params.row.robosat}/>
<ListItemText primary={params.row.robot}/>
</ListItemButton>
);
} },
{ field: 'type', headerName: 'Type', width: 60 },
{ field: 'type', headerName: 'Is', 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>
)} },
<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 (
@ -126,14 +147,15 @@ export default class BookPage extends Component {
bookListTablePhone=()=>{
return (
<div style={{ height: 425, width: '100%' }}>
<div style={{ height: 422, 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",
robot: order.maker_nick,
robot_status: order.maker_status,
type: order.type ? "Seller": "Buyer",
amount: parseFloat(parseFloat(order.amount).toFixed(4)),
currency: this.getCurrencyCode(order.currency),
payment_method: order.payment_method,
@ -144,18 +166,42 @@ export default class BookPage extends Component {
columns={[
// { field: 'id', headerName: 'ID', width: 40 },
{ field: 'robosat', headerName: 'Robot', width: 80,
{ field: 'robot', headerName: 'Robot', width: 80,
renderCell: (params) => {return (
<ListItemButton style={{ cursor: "pointer" }}>
<Avatar className="flippedSmallAvatar" alt={params.row.robosat} src={params.row.avatar} />
</ListItemButton>
<Tooltip placement="right" enterTouchDelay="0" title={params.row.robot+" ("+params.row.robot_status+")"}>
<Badge variant="dot" overlap="circular" badgeContent="" color={this.statusBadgeColor(params.row.robot_status)}>
<div style={{ width: 45, height: 45 }}>
<Image className='bookAvatar'
disableError='true'
disableSpinner='true'
color='null'
alt={params.row.robot}
src={params.row.avatar}
/>
</div>
</Badge>
</Tooltip>
);
} },
{ field: 'type', headerName: 'Type', width: 60, hide:'true'},
{ field: 'amount', headerName: 'Amount', type: 'number', width: 80 },
{ field: 'type', headerName: 'Is', width: 60, hide:'true'},
{ field: 'amount', headerName: 'Amount', type: 'number', width: 80,
renderCell: (params) => {return (
<Tooltip placement="right" enterTouchDelay="0" title={params.row.type}>
<div style={{ cursor: "pointer" }}>{this.pn(params.row.amount)}</div>
</Tooltip>
)} },
{ field: 'currency', headerName: 'Currency', width: 100,
renderCell: (params) => {return (
<div style={{ cursor: "pointer" }}>{params.row.currency + " " + getFlags(params.row.currency)}</div>
<Tooltip placement="left" enterTouchDelay="0" title={params.row.payment_method}>
<Grid container xs={12} aling="center">
<Grid item xs={6} aling="center">
<span>{params.row.currency}</span>
</Grid>
<Grid item xs={6} aling="center">
<Typography>{getFlags(params.row.currency)}</Typography>
</Grid>
</Grid>
</Tooltip>
)} },
{ field: 'payment_method', headerName: 'Payment Method', width: 180, hide:'true'},
{ field: 'price', headerName: 'Price', type: 'number', width: 140, hide:'true',
@ -164,7 +210,9 @@ export default class BookPage extends Component {
)} },
{ field: 'premium', headerName: 'Premium', type: 'number', width: 85,
renderCell: (params) => {return (
<Tooltip placement="left" enterTouchDelay="0" title={this.pn(params.row.price) + " " +params.row.currency+ "/BTC" }>
<div style={{ cursor: "pointer" }}>{parseFloat(parseFloat(params.row.premium).toFixed(4))+"%" }</div>
</Tooltip>
)} },
]}
@ -179,11 +227,9 @@ export default class BookPage extends Component {
render() {
return (
<Grid className='orderBook' container spacing={1} sx={{minWidth:400}}>
<Grid item xs={12} align="center">
<Typography component="h2" variant="h2">
Order Book
</Typography>
</Grid>
{/* <Grid item xs={12} align="center">
<Typography component="h4" variant="h4">ORDER BOOK</Typography>
</Grid> */}
<Grid item xs={6} align="right">
<FormControl >

View File

@ -1,5 +1,5 @@
import React, { Component } from 'react'
import {Badge, TextField, ListItemAvatar, Avatar,Paper, Grid, IconButton, Typography, Select, MenuItem, List, ListItemText, ListItem, ListItemIcon, ListItemButton, Divider, Dialog, DialogContent} from "@mui/material";
import {Badge, Tooltip, TextField, ListItemAvatar, Avatar,Paper, Grid, IconButton, Typography, Select, MenuItem, List, ListItemText, ListItem, ListItemIcon, ListItemButton, Divider, Dialog, DialogContent} from "@mui/material";
import MediaQuery from 'react-responsive'
import { Link } from 'react-router-dom'
@ -19,6 +19,8 @@ import PublicIcon from '@mui/icons-material/Public';
import NumbersIcon from '@mui/icons-material/Numbers';
import PasswordIcon from '@mui/icons-material/Password';
import ContentCopy from "@mui/icons-material/ContentCopy";
import DnsIcon from '@mui/icons-material/Dns';
import WebIcon from '@mui/icons-material/Web';
// pretty numbers
function pn(x) {
@ -42,6 +44,8 @@ export default class BottomBar extends Component {
robosats_running_commit_hash: '000000000000000',
openProfile: false,
profileShown: false,
alternative_site: 'robosats...',
node_id: '00000000',
};
this.getInfo();
}
@ -55,7 +59,7 @@ export default class BottomBar extends Component {
fetch('/api/info/')
.then((response) => response.json())
.then((data) => this.setState(data) &
this.props.setAppState({nickname:data.nickname}));
this.props.setAppState({nickname:data.nickname, loading:false}));
}
handleClickOpenStatsForNerds = () => {
@ -84,11 +88,38 @@ export default class BottomBar extends Component {
<ListItemText primary={this.state.lnd_version} secondary="LND version"/>
</ListItem>
<Divider/>
<ListItem>
<ListItemIcon><DnsIcon/></ListItemIcon>
{this.state.network == 'testnet'?
<ListItemText secondary={this.state.node_alias}>
<a target="_blank" href={"https://1ml.com/testnet/node/"
+ this.state.node_id}>{this.state.node_id.slice(0, 12)+"... (1ML)"}
</a>
</ListItemText>
:
<ListItemText secondary={this.state.node_alias}>
<a target="_blank" href={"https://1ml.com/node/"
+ this.state.node_id}>{this.state.node_id.slice(0, 12)+"... (1ML)"}
</a>
</ListItemText>
}
</ListItem>
<Divider/>
<ListItem>
<ListItemIcon><WebIcon/></ListItemIcon>
<ListItemText secondary={this.state.alternative_name}>
<a target="_blank" href={"http://"+this.state.alternative_site}>{this.state.alternative_site.slice(0, 12)+"...onion"}
</a>
</ListItemText>
</ListItem>
<Divider/>
<ListItem>
<ListItemIcon><GitHubIcon/></ListItemIcon>
<ListItemText secondary="Currently running commit hash">
<a href={"https://github.com/Reckless-Satoshi/robosats/tree/"
<a target="_blank" href={"https://github.com/Reckless-Satoshi/robosats/tree/"
+ this.state.robosats_running_commit_hash}>{this.state.robosats_running_commit_hash.slice(0, 12)+"..."}
</a>
</ListItemText>
@ -264,17 +295,25 @@ bottomBarDesktop =()=>{
<Grid container xs={12}>
<Grid item xs={2}>
<div style={{display: this.props.avatarLoaded ? '':'none'}}>
<ListItemButton onClick={this.handleClickOpenProfile} >
<Tooltip open={(this.state.active_order_id > 0 & !this.state.profileShown & this.props.avatarLoaded) ? true: false}
title="You have an active order">
<ListItemAvatar sx={{ width: 30, height: 30 }} >
<Badge badgeContent={(this.state.active_order_id > 0 & !this.state.profileShown) ? "": null} color="primary">
<Avatar className='flippedSmallAvatar' sx={{margin: 0, top: -13}}
alt={this.props.nickname}
imgProps={{
onLoad:() => this.props.setAppState({avatarLoaded: true}),
}}
src={this.props.nickname ? window.location.origin +'/static/assets/avatars/' + this.props.nickname + '.png' : null}
/>
</Badge>
</ListItemAvatar>
<ListItemText primary={this.props.nickname}/>
</ListItemButton>
</Tooltip>
<ListItemText primary={this.props.nickname}/>
</ListItemButton>
</div>
</Grid>
<Grid item xs={2}>
@ -354,19 +393,23 @@ bottomBarDesktop =()=>{
</Select>
</Grid>
<Grid item xs={3}>
<Tooltip enterTouchDelay="250" title="Show community and support links">
<IconButton
color="primary"
aria-label="Community"
onClick={this.handleClickOpenCommunity} >
<PeopleIcon />
</IconButton>
</Tooltip>
</Grid>
<Grid item xs={3}>
<IconButton color="primary"
aria-label="Stats for Nerds"
onClick={this.handleClickOpenStatsForNerds} >
<SettingsIcon />
</IconButton>
<Tooltip enterTouchDelay="250" title="Show stats for nerds">
<IconButton color="primary"
aria-label="Stats for Nerds"
onClick={this.handleClickOpenStatsForNerds} >
<SettingsIcon />
</IconButton>
</Tooltip>
</Grid>
</Grid>
@ -469,46 +512,63 @@ bottomBarPhone =()=>{
<Grid container xs={12}>
<Grid item xs={1.6}>
<IconButton onClick={this.handleClickOpenProfile} sx={{margin: 0, top: -13, }} >
<Badge badgeContent={(this.state.active_order_id >0 & !this.state.profileShown) ? "": null} color="primary">
<Avatar className='flippedSmallAvatar'
alt={this.props.nickname}
src={this.props.nickname ? window.location.origin +'/static/assets/avatars/' + this.props.nickname + '.png' : null}
/>
</Badge>
</IconButton>
<div style={{display: this.props.avatarLoaded ? '':'none'}}>
<Tooltip open={(this.state.active_order_id > 0 & !this.state.profileShown & this.props.avatarLoaded) ? true: false}
title="You have an active order">
<IconButton onClick={this.handleClickOpenProfile} sx={{margin: 0, bottom: 17, right: 8}} >
<Badge badgeContent={(this.state.active_order_id >0 & !this.state.profileShown) ? "": null} color="primary">
<Avatar className='phoneFlippedSmallAvatar'
sx={{ width: 55, height:55 }}
alt={this.props.nickname}
imgProps={{
onLoad:() => this.props.setAppState({avatarLoaded: true}),
}}
src={this.props.nickname ? window.location.origin +'/static/assets/avatars/' + this.props.nickname + '.png' : null}
/>
</Badge>
</IconButton>
</Tooltip>
</div>
</Grid>
<Grid item xs={1.6} align="center">
<IconButton onClick={this.handleClickOpenExchangeSummary} >
<Badge badgeContent={this.state.num_public_buy_orders} color="action">
<InventoryIcon />
</Badge>
</IconButton>
<Tooltip enterTouchDelay="300" title="Number of public BUY orders">
<IconButton onClick={this.handleClickOpenExchangeSummary} >
<Badge badgeContent={this.state.num_public_buy_orders} color="action">
<InventoryIcon />
</Badge>
</IconButton>
</Tooltip>
</Grid>
<Grid item xs={1.6} align="center">
<IconButton onClick={this.handleClickOpenExchangeSummary} >
<Badge badgeContent={this.state.num_public_sell_orders} color="action">
<SellIcon />
</Badge>
</IconButton>
<Tooltip enterTouchDelay="300" title="Number of public SELL orders">
<IconButton onClick={this.handleClickOpenExchangeSummary} >
<Badge badgeContent={this.state.num_public_sell_orders} color="action">
<SellIcon />
</Badge>
</IconButton>
</Tooltip>
</Grid>
<Grid item xs={1.6} align="center">
<IconButton onClick={this.handleClickOpenExchangeSummary} >
<Badge badgeContent={this.state.active_robots_today} color="action">
<SmartToyIcon />
</Badge>
</IconButton>
<Tooltip enterTouchDelay="300" title="Today active robots">
<IconButton onClick={this.handleClickOpenExchangeSummary} >
<Badge badgeContent={this.state.active_robots_today} color="action">
<SmartToyIcon />
</Badge>
</IconButton>
</Tooltip>
</Grid>
<Grid item xs={1.8} align="center">
<IconButton onClick={this.handleClickOpenExchangeSummary} >
<Badge badgeContent={this.state.today_avg_nonkyc_btc_premium+"%"} color="action">
<PriceChangeIcon />
</Badge>
</IconButton>
<Tooltip enterTouchDelay="300" title="Today non-KYC bitcoin premium">
<IconButton onClick={this.handleClickOpenExchangeSummary} >
<Badge badgeContent={this.state.today_avg_nonkyc_btc_premium+"%"} color="action">
<PriceChangeIcon />
</Badge>
</IconButton>
</Tooltip>
</Grid>
<Grid container item xs={3.8}>
@ -523,19 +583,23 @@ bottomBarPhone =()=>{
</Select>
</Grid>
<Grid item xs={3}>
<IconButton color="primary"
aria-label="Stats for Nerds"
onClick={this.handleClickOpenStatsForNerds} >
<SettingsIcon />
</IconButton>
</Grid>
<Grid item xs={3}>
<Tooltip enterTouchDelay="250" title="Show community and support links">
<IconButton
color="primary"
aria-label="Community"
onClick={this.handleClickOpenCommunity} >
<PeopleIcon />
</IconButton>
</Tooltip>
</Grid>
<Grid item xs={3}>
<Tooltip enterTouchDelay="250" title="Show stats for nerds">
<IconButton color="primary"
aria-label="Stats for Nerds"
onClick={this.handleClickOpenStatsForNerds} >
<SettingsIcon />
</IconButton>
</Tooltip>
</Grid>
</Grid>

View File

@ -13,6 +13,7 @@ export default class HomePage extends Component {
this.state = {
nickname: null,
token: null,
avatarLoaded: false,
}
}
@ -29,7 +30,7 @@ export default class HomePage extends Component {
<Router >
<div className='appCenter'>
<Switch>
<Route exact path='/' render={(props) => <UserGenPage setAppState={this.setAppState}/>}/>
<Route exact path='/' render={(props) => <UserGenPage {...this.state} setAppState={this.setAppState}/>}/>
<Route path='/make' component={MakerPage}/>
<Route path='/book' component={BookPage}/>
<Route path="/order/:orderId" component={OrderPage}/>

View File

@ -1,5 +1,5 @@
import React, { Component } from 'react';
import { Paper, Button , Grid, Typography, TextField, Select, FormHelperText, MenuItem, FormControl, Radio, FormControlLabel, RadioGroup} from "@mui/material"
import { Tooltip, Paper, Button , Grid, Typography, TextField, Select, FormHelperText, MenuItem, FormControl, Radio, FormControlLabel, RadioGroup, dividerClasses} from "@mui/material"
import { Link } from 'react-router-dom'
import getFlags from './getFlags'
@ -22,6 +22,9 @@ const csrftoken = getCookie('csrftoken');
// pretty numbers
function pn(x) {
if(x==null){
return(null)
}
return x.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",");
}
@ -67,37 +70,48 @@ export default class MakerPage extends Component {
handlePaymentMethodChange=(e)=>{
this.setState({
payment_method: e.target.value,
badPaymentMethod: e.target.value.length > 35,
});
}
handlePremiumChange=(e)=>{
if(e.target.value > 999){
var bad_premium = "Must be less than 999%"
}
if(e.target.value < -100){
var bad_premium = "Must be more than -100%"
}
this.setState({
premium: e.target.value,
badPremium: bad_premium,
});
}
handleSatoshisChange=(e)=>{
var bad_sats = e.target.value > this.maxTradeSats ?
("Must be less than "+pn(this.maxTradeSats)):
(e.target.value < this.minTradeSats ?
("Must be more than "+pn(this.minTradeSats)): null)
if(e.target.value > this.maxTradeSats){
var bad_sats = "Must be less than " + pn(this.maxTradeSats)
}
if(e.target.value < this.minTradeSats){
var bad_sats = "Must be more than "+pn(this.minTradeSats)
}
this.setState({
satoshis: e.target.value,
badSatoshis: bad_sats,
})
;
});
}
handleClickRelative=(e)=>{
this.setState({
is_explicit: false,
satoshis: null,
premium: 0,
});
this.handlePremiumChange();
}
handleClickExplicit=(e)=>{
this.setState({
is_explicit: true,
premium: null,
});
this.handleSatoshisChange();
}
handleCreateOfferButtonPressed=()=>{
@ -112,8 +126,8 @@ export default class MakerPage extends Component {
amount: this.state.amount,
payment_method: this.state.payment_method,
is_explicit: this.state.is_explicit,
premium: this.state.premium,
satoshis: this.state.satoshis,
premium: this.state.is_explicit ? null: this.state.premium,
satoshis: this.state.is_explicit ? this.state.satoshis: null,
}),
};
fetch("/api/make/",requestOptions)
@ -136,16 +150,17 @@ export default class MakerPage extends Component {
return this.state.currencies_dict[val.toString()]
}
render() {
return (
<Grid container xs={12} align="center" spacing={1}>
<Grid item xs={12} align="center">
<Typography component="h2" variant="h2">
Order Maker
<Grid container xs={12} align="center" spacing={1} sx={{minWidth:380}}>
{/* <Grid item xs={12} align="center" sx={{minWidth:380}}>
<Typography component="h4" variant="h4">
ORDER MAKER
</Typography>
</Grid>
</Grid> */}
<Grid item xs={12} align="center" spacing={1}>
<Paper elevation={12} style={{ padding: 8, width:350, align:'center'}}>
<Paper elevation={12} style={{ padding: 8, width:240, align:'center'}}>
<Grid item xs={12} align="center" spacing={1}>
<FormControl component="fieldset">
<FormHelperText>
@ -167,10 +182,12 @@ export default class MakerPage extends Component {
</RadioGroup>
</FormControl>
</Grid>
<Grid containter xs={8} alignItems="stretch" style={{ display: "flex" }}>
<Grid containter xs={12} alignItems="stretch" style={{ display: "flex" }}>
<div style={{maxWidth:140}}>
<Tooltip placement="top" enterTouchDelay="500" enterDelay="700" enterNextDelay="2000" title="Amount of fiat to exchange for bitcoin">
<TextField
error={this.state.amount == 0}
helperText={this.state.amount == 0 ? 'Must be more than 0' : null}
error={this.state.amount <= 0}
helperText={this.state.amount <= 0 ? 'Invalid' : null}
label="Amount"
type="number"
required="true"
@ -180,28 +197,32 @@ export default class MakerPage extends Component {
}}
onChange={this.handleAmountChange}
/>
<Select
required="true"
defaultValue={this.defaultCurrency}
inputProps={{
style: {textAlign:"center"}
}}
onChange={this.handleCurrencyChange}
>
{
Object.entries(this.state.currencies_dict)
.map( ([key, value]) => <MenuItem value={parseInt(key)}>
{getFlags(value) + " " + value}
</MenuItem> )
}
</Select>
</Tooltip>
</div>
<div >
<Select
required="true"
defaultValue={this.defaultCurrency}
inputProps={{
style: {textAlign:"center"}
}}
onChange={this.handleCurrencyChange}>
{Object.entries(this.state.currencies_dict)
.map( ([key, value]) => <MenuItem value={parseInt(key)}>
{getFlags(value) + " " + value}
</MenuItem> )}
</Select>
</div>
</Grid>
<br/>
<Grid item xs={12} align="center">
<FormControl >
<Tooltip placement="top" enterTouchDelay="500" enterDelay="700" enterNextDelay="2000" title="Enter your prefered payment methods">
<TextField
sx={{width:240}}
label="Payment Method(s)"
error={this.state.badPaymentMethod}
helperText={this.state.badPaymentMethod ? "Must be shorter than 35 characters":""}
type="text"
require={true}
inputProps={{
@ -210,7 +231,7 @@ export default class MakerPage extends Component {
}}
onChange={this.handlePaymentMethodChange}
/>
</FormControl>
</Tooltip>
</Grid>
<Grid item xs={12} align="center">
@ -221,6 +242,7 @@ export default class MakerPage extends Component {
</div>
</FormHelperText>
<RadioGroup row defaultValue="relative">
<Tooltip placement="top" enterTouchDelay="0" enterDelay="1000" enterNextDelay="2000" title="Let the price move with the market">
<FormControlLabel
value="relative"
control={<Radio color="primary"/>}
@ -228,6 +250,8 @@ export default class MakerPage extends Component {
labelPlacement="Top"
onClick={this.handleClickRelative}
/>
</Tooltip>
<Tooltip placement="top" enterTouchDelay="0" enterDelay="1000" enterNextDelay="2000" title="Set a fix amount of satoshis">
<FormControlLabel
value="explicit"
control={<Radio color="secondary"/>}
@ -235,46 +259,64 @@ export default class MakerPage extends Component {
labelPlacement="Top"
onClick={this.handleClickExplicit}
/>
</Tooltip>
</RadioGroup>
</FormControl>
</Grid>
{/* conditional shows either Premium % field or Satoshis field based on pricing method */}
{ this.state.is_explicit
? <Grid item xs={12} align="center">
<Grid item xs={12} align="center">
<div style={{display: this.state.is_explicit ? '':'none'}}>
<TextField
sx={{width:240}}
label="Satoshis"
error={this.state.badSatoshis}
helperText={this.state.badSatoshis}
type="number"
required="true"
value={this.state.satoshis}
inputProps={{
// TODO read these from .env file
min:this.minTradeSats ,
max:this.maxTradeSats ,
style: {textAlign:"center"}
}}
onChange={this.handleSatoshisChange}
// defaultValue={this.defaultSatoshis}
/>
</div>
<div style={{display: this.state.is_explicit ? 'none':''}}>
<TextField
label="Satoshis"
error={this.state.badSatoshis}
helperText={this.state.badSatoshis}
type="number"
required="true"
inputProps={{
// TODO read these from .env file
min:this.minTradeSats ,
max:this.maxTradeSats ,
style: {textAlign:"center"}
}}
onChange={this.handleSatoshisChange}
// defaultValue={this.defaultSatoshis}
/>
</Grid>
: <Grid item xs={12} align="center">
<TextField
sx={{width:240}}
error={this.state.badPremium}
helperText={this.state.badPremium}
label="Premium over Market (%)"
type="number"
// defaultValue={this.defaultPremium}
inputProps={{
min: -100,
max: 999,
style: {textAlign:"center"}
}}
onChange={this.handlePremiumChange}
/>
</Grid>
}
</div>
</Grid>
</Paper>
</Grid>
<Grid item xs={12} align="center">
<Button color="primary" variant="contained" onClick={this.handleCreateOfferButtonPressed} >
Create Order
</Button>
{/* conditions to disable the make button */}
{(this.state.amount == null ||
this.state.amount <= 0 ||
(this.state.is_explicit & (this.state.badSatoshis != null || this.state.satoshis == null)) ||
(!this.state.is_explicit & this.state.badPremium != null))
?
<Tooltip enterTouchDelay="0" title="You must fill the form correctly">
<div><Button disabled color="primary" variant="contained" onClick={this.handleCreateOfferButtonPressed} >Create Order</Button></div>
</Tooltip>
:
<Button color="primary" variant="contained" onClick={this.handleCreateOfferButtonPressed} >Create Order</Button>
}
</Grid>
<Grid item xs={12} align="center">
{this.state.badRequest ?
@ -284,8 +326,8 @@ export default class MakerPage extends Component {
: ""}
<Typography component="subtitle2" variant="subtitle2">
<div align='center'>
Create a BTC {this.state.type==0 ? "buy":"sell"} order for {this.state.amount} {this.state.currencyCode}
{this.state.is_explicit ? " of " + this.state.satoshis + " Satoshis" :
Create a BTC {this.state.type==0 ? "buy":"sell"} order for {pn(this.state.amount)} {this.state.currencyCode}
{this.state.is_explicit ? " of " + pn(this.state.satoshis) + " Satoshis" :
(this.state.premium == 0 ? " at market price" :
(this.state.premium > 0 ? " at a " + this.state.premium + "% premium":" at a " + -this.state.premium + "% discount")
)

View File

@ -1,5 +1,5 @@
import React, { Component } from "react";
import { Badge, Tab, Tabs, Alert, Paper, CircularProgress, Button , Grid, Typography, List, ListItem, ListItemIcon, ListItemText, ListItemAvatar, Avatar, Divider, Box, LinearProgress, Dialog, DialogActions, DialogContent, DialogContentText, DialogTitle} from "@mui/material"
import { Chip, Tooltip, Badge, Tab, Tabs, 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 MediaQuery from 'react-responsive'
@ -46,6 +46,7 @@ export default class OrderPage extends Component {
loading: true,
openCancel: false,
openCollaborativeCancel: false,
openInactiveMaker: false,
showContractBox: 1,
};
this.orderId = this.props.match.params.orderId;
@ -54,29 +55,29 @@ export default class OrderPage extends Component {
// Refresh delays according to Order status
this.statusToDelay = {
"0": 2000, //'Waiting for maker bond'
"1": 25000, //'Public'
"2": 9999999, //'Deleted'
"3": 2000, //'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'
"0": 2000, //'Waiting for maker bond'
"1": 25000, //'Public'
"2": 999999, //'Deleted'
"3": 2000, //'Waiting for taker bond'
"4": 999999, //'Cancelled'
"5": 999999, //'Expired'
"6": 3000, //'Waiting for trade collateral and buyer invoice'
"7": 3000, //'Waiting only for seller trade collateral'
"8": 8000, //'Waiting only for buyer invoice'
"9": 10000, //'Sending fiat - In chatroom'
"10": 10000, //'Fiat sent - In chatroom'
"11": 30000, //'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'
"9": 10000, //'Sending fiat - In chatroom'
"10": 10000, //'Fiat sent - In chatroom'
"11": 30000, //'In dispute'
"12": 999999, //'Collaboratively cancelled'
"13": 3000, //'Sending satoshis to buyer'
"14": 999999, //'Sucessful trade'
"15": 10000, //'Failed lightning network routing'
"16": 180000, //'Wait for dispute resolution'
"17": 180000, //'Maker lost dispute'
"18": 180000, //'Taker lost dispute'
}
}
completeSetState=(newStateVars)=>{
// In case the reply only has "bad_request"
// Do not substitute these two for "undefined" as
// otherStateVars will fail to assign values
@ -149,11 +150,41 @@ export default class OrderPage extends Component {
} else {
return (
<span> Wait {zeroPad(minutes)}m {zeroPad(seconds)}s </span>
<span> You cannot take an order yet! Wait {zeroPad(minutes)}m {zeroPad(seconds)}s </span>
);
}
};
countdownTakeOrderRenderer = ({ seconds, completed }) => {
if(isNaN(seconds)){
return (
<>
<this.InactiveMakerDialog/>
<Button variant='contained' color='primary'
onClick={this.state.maker_status=='Inactive' ? this.handleClickOpenInactiveMakerDialog : this.takeOrder}>
Take Order
</Button>
</>)
}
if (completed) {
// Render a completed state
return (
<>
<this.InactiveMakerDialog/>
<Button variant='contained' color='primary'
onClick={this.state.maker_status=='Inactive' ? this.handleClickOpenInactiveMakerDialog : this.takeOrder}>
Take Order
</Button>
</>
);
} else{
return(
<Tooltip enterTouchDelay="0" title="Wait until you can take an order"><div>
<Button disabled={true} variant='contained' color='primary' onClick={this.takeOrder}>Take Order</Button>
</div></Tooltip>)
}
};
LinearDeterminate =()=> {
const [progress, setProgress] = React.useState(0);
@ -177,18 +208,21 @@ export default class OrderPage extends Component {
);
}
handleClickTakeOrderButton=()=>{
const requestOptions = {
method: 'POST',
headers: {'Content-Type':'application/json', 'X-CSRFToken': getCookie('csrftoken'),},
body: JSON.stringify({
'action':'take',
}),
takeOrder=()=>{
this.setState({loading:true})
const requestOptions = {
method: 'POST',
headers: {'Content-Type':'application/json', 'X-CSRFToken': getCookie('csrftoken'),},
body: JSON.stringify({
'action':'take',
}),
};
fetch('/api/order/' + '?order_id=' + this.orderId, requestOptions)
.then((response) => response.json())
.then((data) => this.completeSetState(data));
}
getCurrencyDict() {
fetch('/static/assets/currencies.json')
.then((response) => response.json())
@ -209,16 +243,17 @@ export default class OrderPage extends Component {
}
handleClickConfirmCancelButton=()=>{
const requestOptions = {
method: 'POST',
headers: {'Content-Type':'application/json', 'X-CSRFToken': getCookie('csrftoken'),},
body: JSON.stringify({
'action':'cancel',
}),
};
fetch('/api/order/' + '?order_id=' + this.orderId, requestOptions)
.then((response) => response.json())
.then((data) => this.getOrderDetails(data.id));
this.setState({loading:true})
const requestOptions = {
method: 'POST',
headers: {'Content-Type':'application/json', 'X-CSRFToken': getCookie('csrftoken'),},
body: JSON.stringify({
'action':'cancel',
}),
};
fetch('/api/order/' + '?order_id=' + this.orderId, requestOptions)
.then((response) => response.json())
.then((data) => this.getOrderDetails(data.id));
this.handleClickCloseConfirmCancelDialog();
}
@ -253,6 +288,36 @@ export default class OrderPage extends Component {
)
}
handleClickOpenInactiveMakerDialog = () => {
this.setState({openInactiveMaker: true});
};
handleClickCloseInactiveMakerDialog = () => {
this.setState({openInactiveMaker: false});
};
InactiveMakerDialog =() =>{
return(
<Dialog
open={this.state.openInactiveMaker}
onClose={this.handleClickCloseInactiveMakerDialog}
aria-labelledby="inactive-maker-dialog-title"
aria-describedby="inactive-maker-description"
>
<DialogTitle id="inactive-maker-dialog-title">
{"The maker is away"}
</DialogTitle>
<DialogContent>
<DialogContentText id="cancel-dialog-description">
By taking this order you risk wasting your time.
</DialogContentText>
</DialogContent>
<DialogActions>
<Button onClick={this.handleClickCloseInactiveMakerDialog} autoFocus>Go back</Button>
<Button onClick={this.takeOrder}> Take Order </Button>
</DialogActions>
</Dialog>
)
}
handleClickConfirmCollaborativeCancelButton=()=>{
const requestOptions = {
method: 'POST',
@ -337,68 +402,67 @@ export default class OrderPage extends Component {
// Colors for the status badges
statusBadgeColor(status){
if(status=='active'){
return("success")
}
if(status=='seen_recently'){
return("warning")
}
if(status=='inactive'){
return('error')
}
if(status=='Active'){return("success")}
if(status=='Seen recently'){return("warning")}
if(status=='Inactive'){return('error')}
}
orderBox=()=>{
return(
<Grid container spacing={1} >
<Grid item xs={12} align="center">
<MediaQuery minWidth={920}>
<Typography component="h5" variant="h5">
Order Details
Order Box
</Typography>
</MediaQuery>
<Paper elevation={12} style={{ padding: 8,}}>
<List dense="true">
<ListItem >
<ListItemAvatar sx={{ width: 56, height: 56 }}>
<Badge variant="dot" badgeContent="" color={this.statusBadgeColor(this.state.maker_status)}>
<Tooltip placement="top" enterTouchDelay="0" title={this.state.maker_status} >
<Badge variant="dot" overlap="circular" badgeContent="" color={this.statusBadgeColor(this.state.maker_status)}>
<Avatar className="flippedSmallAvatar"
alt={this.state.maker_nick}
src={window.location.origin +'/static/assets/avatars/' + this.state.maker_nick + '.png'}
/>
</Badge>
</Tooltip>
</ListItemAvatar>
<ListItemText primary={this.state.maker_nick + (this.state.type ? " (Seller)" : " (Buyer)")} secondary="Order maker" align="right"/>
</ListItem>
<Divider />
{this.state.is_participant ?
<>
{this.state.taker_nick!='None' ?
<>
<Divider />
<ListItem align="left">
<ListItemText primary={this.state.taker_nick + (this.state.type ? " (Buyer)" : " (Seller)")} secondary="Order taker"/>
<ListItemAvatar >
<Badge variant="dot" badgeContent="" color={this.statusBadgeColor(this.state.taker_status)}>
<Tooltip enterTouchDelay="0" title={this.state.taker_status} >
<Badge variant="dot" overlap="circular" badgeContent="" color={this.statusBadgeColor(this.state.taker_status)}>
<Avatar className="smallAvatar"
alt={this.state.taker_nick}
src={window.location.origin +'/static/assets/avatars/' + this.state.taker_nick + '.png'}
/>
</Badge>
</Tooltip>
</ListItemAvatar>
</ListItem>
<Divider />
</>:
""
}
<Divider><Chip label='Order Details'/></Divider>
<ListItem>
<ListItemIcon>
<ArticleIcon/>
</ListItemIcon>
<ListItemText primary={this.state.status_message} secondary="Order status"/>
</ListItem>
<Divider />
<Divider/>
</>
:""
:<Divider><Chip label='Order Details'/></Divider>
}
<ListItem>
@ -458,7 +522,7 @@ export default class OrderPage extends Component {
<Divider />
<Grid item xs={12} align="center">
<Alert severity="warning" sx={{maxWidth:360}}>
You cannot take an order yet! <Countdown date={new Date(this.state.penalty)} renderer={this.countdownPenaltyRenderer} />
<Countdown date={new Date(this.state.penalty)} renderer={this.countdownPenaltyRenderer} />
</Alert>
</Grid>
</>
@ -498,7 +562,7 @@ export default class OrderPage extends Component {
:
<Grid container spacing={1}>
<Grid item xs={12} align="center">
<Button variant='contained' color='primary' onClick={this.handleClickTakeOrderButton}>Take Order</Button>
<Countdown date={new Date(this.state.penalty)} renderer={this.countdownTakeOrderRenderer} />
</Grid>
<Grid item xs={12} align="center">
<Button variant='contained' color='secondary' onClick={this.props.history.goBack}>Back</Button>
@ -517,7 +581,7 @@ export default class OrderPage extends Component {
{this.orderBox()}
</Grid>
<Grid item xs={6} align="left">
<TradeBox width={330} data={this.state} completeSetState={this.completeSetState} />
<TradeBox push={this.props.history.push} width={330} data={this.state} completeSetState={this.completeSetState} />
</Grid>
</Grid>
)
@ -543,8 +607,8 @@ export default class OrderPage extends Component {
<Box sx={{ width: '100%' }}>
<Box sx={{ borderBottom: 1, borderColor: 'divider' }}>
<Tabs value={value} onChange={handleChange} variant="fullWidth" >
<Tab label="Order Details" {...this.a11yProps(0)} />
<Tab label="Contract Box" {...this.a11yProps(1)} />
<Tab label="Order" {...this.a11yProps(0)} />
<Tab label="Contract" {...this.a11yProps(1)} />
</Tabs>
</Box>
<Grid container spacing={2}>
@ -553,7 +617,7 @@ export default class OrderPage extends Component {
{this.orderBox()}
</div>
<div style={{display: this.state.showContractBox == 1 ? '':'none'}}>
<TradeBox width={330} data={this.state} completeSetState={this.completeSetState} />
<TradeBox push={this.props.history.push} width={330} data={this.state} completeSetState={this.completeSetState} />
</div>
</Grid>
</Grid>

View File

@ -1,7 +1,7 @@
import React, { Component } from "react";
import { IconButton, Paper, Rating, Button, Grid, Typography, TextField, List, ListItem, ListItemText, Divider, ListItemIcon, Dialog, DialogActions, DialogContent, DialogContentText, DialogTitle} from "@mui/material"
import { IconButton, Paper, Rating, Button, CircularProgress, Grid, Typography, TextField, List, ListItem, ListItemText, Divider, ListItemIcon, Dialog, DialogActions, DialogContent, DialogContentText, DialogTitle} from "@mui/material"
import QRCode from "react-qr-code";
import Countdown from 'react-countdown';
import Countdown, { zeroPad} from 'react-countdown';
import Chat from "./Chat"
import MediaQuery from 'react-responsive'
import QrReader from 'react-qr-reader'
@ -234,8 +234,8 @@ export default class TradeBox extends Component {
<Divider/>
<Grid item xs={12} align="center">
<Typography component="body2" variant="body2">
Please wait for the taker to confirm by locking a bond.
If the taker does not lock a bond in time the orer will be made
Please wait for the taker to lock a bond.
If the taker does not lock a bond in time, the order will be made
public again.
</Typography>
</Grid>
@ -361,6 +361,8 @@ export default class TradeBox extends Component {
<Grid container spacing={1}>
<Grid item xs={12} align="center">
{/* Make confirmation sound for HTLC received. */}
<this.Sound soundFileName="locked-invoice"/>
<Typography color="primary" component="subtitle1" variant="subtitle1">
<b> Submit a LN invoice for {pn(this.props.data.invoice_amount)} Sats </b>
</Typography>
@ -547,12 +549,31 @@ export default class TradeBox extends Component {
.then((data) => this.props.completeSetState(data));
}
handleRatingChange=(e)=>{
handleRatingUserChange=(e)=>{
const requestOptions = {
method: 'POST',
headers: {'Content-Type':'application/json', 'X-CSRFToken': getCookie('csrftoken'),},
body: JSON.stringify({
'action': "rate",
'action': "rate_user",
'rating': e.target.value,
}),
};
fetch('/api/order/' + '?order_id=' + this.props.data.id, requestOptions)
.then((response) => response.json())
.then((data) => this.props.completeSetState(data));
}
handleRatingRobosatsChange=(e)=>{
if (this.state.rating_platform != null){
return null
}
this.setState({rating_platform:e.target.value});
const requestOptions = {
method: 'POST',
headers: {'Content-Type':'application/json', 'X-CSRFToken': getCookie('csrftoken'),},
body: JSON.stringify({
'action': "rate_platform",
'rating': e.target.value,
}),
};
@ -670,14 +691,38 @@ handleRatingChange=(e)=>{
</Grid>
<Grid item xs={12} align="center">
<Typography component="body2" variant="body2" align="center">
What do you think of <b>{this.props.data.is_maker ? this.props.data.taker_nick : this.props.data.maker_nick}</b>?
What do you think of <b>{this.props.data.is_maker ? this.props.data.taker_nick : this.props.data.maker_nick}</b>?
</Typography>
</Grid>
<Grid item xs={12} align="center">
<Rating name="size-large" defaultValue={2} size="large" onChange={this.handleRatingChange} />
<Rating name="size-large" defaultValue={0} size="large" onChange={this.handleRatingUserChange} />
</Grid>
<Grid item xs={12} align="center">
<Button color='primary' href='/' component="a">Start Again</Button>
<Typography component="body2" variant="body2" align="center">
What do you think of 🤖<b>RoboSats</b>🤖?
</Typography>
</Grid>
<Grid item xs={12} align="center">
<Rating name="size-large" defaultValue={0} size="large" onChange={this.handleRatingRobosatsChange} />
</Grid>
{this.state.rating_platform==5 ?
<Grid item xs={12} align="center">
<Typography component="body2" variant="body2" align="center">
<p>Thank you! RoboSats loves you too </p>
<p>RoboSats gets better with more liquidity and users. Tell a bitcoiner friend about Robosats!</p>
</Typography>
</Grid>
: null}
{this.state.rating_platform!=5 & this.state.rating_platform!=null ?
<Grid item xs={12} align="center">
<Typography component="body2" variant="body2" align="center">
Thank you for using Robosats! Let us know what you did not like and how the platform could improve
(<a href="https://t.me/robosats">Telegram</a> / <a href="https://github.com/Reckless-Satoshi/robosats/issues">Github</a>)
</Typography>
</Grid>
: null}
<Grid item xs={12} align="center">
<Button color='primary' onClick={() => {this.props.push('/')}}>Start Again</Button>
</Grid>
</Grid>
)
@ -696,11 +741,28 @@ handleRatingChange=(e)=>{
RoboSats is trying to pay your lightning invoice. Remember that lightning nodes must
be online in order to receive payments.
</Typography>
<br/>
<Grid item xs={12} align="center">
<CircularProgress/>
</Grid>
</Grid>
</Grid>
)
}
// Countdown Renderer callback with condition
countdownRenderer = ({ minutes, seconds, completed }) => {
if (completed) {
// Render a completed state
return (<div align="center"><span> Retrying! </span><br/><CircularProgress/></div> );
} else {
return (
<span>{zeroPad(minutes)}m {zeroPad(seconds)}s </span>
);
}
};
showRoutingFailed=()=>{
// TODO If it has failed 3 times, ask for a new invoice.
if(this.props.data.invoice_expired){
@ -713,7 +775,7 @@ handleRatingChange=(e)=>{
</Grid>
<Grid item xs={12} align="center">
<Typography component="body2" variant="body2" align="center">
Your invoice has expires or more than 3 payments have been attempted.
Your invoice has expires or more than 3 payments attempts have been made.
</Typography>
</Grid>
<Grid item xs={12} align="center">

View File

@ -1,5 +1,5 @@
import React, { Component } from "react";
import { Button , Dialog, Grid, Typography, TextField, ButtonGroup, CircularProgress, IconButton} from "@mui/material"
import { Button , Tooltip, 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'
@ -28,11 +28,12 @@ export default class UserGenPage extends Component {
constructor(props) {
super(props);
this.state = {
token: this.genBase62Token(34),
token: this.genBase62Token(36),
openInfo: false,
loadingRobot: true,
tokenHasChanged: false,
};
this.props.setAppState({avatarLoaded: false, nickname: null, token: null});
this.getGeneratedUser(this.state.token);
}
@ -64,11 +65,13 @@ export default class UserGenPage extends Component {
// Add nick and token to App state (token only if not a bad request)
(data.bad_request ? this.props.setAppState({
nickname: data.nickname,
avatarLoaded: false,
})
:
this.props.setAppState({
nickname: data.nickname,
token: this.state.token,
avatarLoaded: false,
}));
});
}
@ -85,8 +88,9 @@ export default class UserGenPage extends Component {
handleClickNewRandomToken=()=>{
this.setState({
token: this.genBase62Token(34),
token: this.genBase62Token(36),
tokenHasChanged: true,
copied: true,
});
}
@ -98,9 +102,10 @@ export default class UserGenPage extends Component {
}
handleClickSubmitToken=()=>{
this.delGeneratedUser()
this.delGeneratedUser();
this.getGeneratedUser(this.state.token);
this.setState({loadingRobot: true, tokenHasChanged: false})
this.setState({loadingRobot: true, tokenHasChanged: false, copied: false});
this.props.setAppState({avatarLoaded: false, nickname: null, token: null});
}
handleClickOpenInfo = () => {
@ -137,6 +142,7 @@ export default class UserGenPage extends Component {
</Typography>
</Grid>
<Grid item xs={12} align="center">
<Tooltip enterTouchDelay="0" title="This is your trading avatar">
<div style={{ maxWidth: 200, maxHeight: 200 }}>
<Image className='newAvatar'
disableError='true'
@ -144,7 +150,8 @@ export default class UserGenPage extends Component {
color='null'
src={this.state.avatar_url}
/>
</div><br/>
</div>
</Tooltip><br/>
</Grid>
</div>
: <CircularProgress sx={{position: 'relative', top: 100, }}/>}
@ -167,7 +174,7 @@ export default class UserGenPage extends Component {
// style: { color: 'green' },
// }}
error={this.state.bad_request}
label='Store your token safely'
label={"Store your token safely"}
required='true'
value={this.state.token}
variant='standard'
@ -182,20 +189,35 @@ export default class UserGenPage extends Component {
}}
InputProps={{
startAdornment:
<IconButton onClick= {()=>navigator.clipboard.writeText(this.state.token)}>
<ContentCopy color={this.state.tokenHasChanged ? 'inherit' : 'primary' } sx={{width:18, height:18}} />
</IconButton>,
<Tooltip disableHoverListener open={this.state.copied} enterTouchDelay="0" title="Copied!">
<IconButton onClick= {()=> (navigator.clipboard.writeText(this.state.token) & this.setState({copied:true}))}>
<ContentCopy color={this.props.avatarLoaded & !this.state.copied & !this.state.bad_request ? 'primary' : 'inherit' } sx={{width:18, height:18}}/>
</IconButton>
</Tooltip>,
endAdornment:
<IconButton onClick={this.handleClickNewRandomToken}><CasinoIcon/></IconButton>,
<Tooltip enterTouchDelay="250" title="Generate a new token">
<IconButton onClick={this.handleClickNewRandomToken}><CasinoIcon/></IconButton>
</Tooltip>,
}}
/>
</Grid>
</Grid>
<Grid item xs={12} align="center">
<Button disabled={!this.state.tokenHasChanged} type="submit" size='small' onClick= {this.handleClickSubmitToken}>
{this.state.tokenHasChanged ?
<Button type="submit" size='small' onClick= {this.handleClickSubmitToken}>
<SmartToyIcon sx={{width:18, height:18}} />
<span> Generate Robot</span>
</Button>
:
<Tooltip enterTouchDelay="0" enterDelay="500" enterNextDelay="2000" title="You must enter a new token first">
<div>
<Button disabled={true} type="submit" size='small' >
<SmartToyIcon sx={{width:18, height:18}} />
<span> Generate Robot</span>
</Button>
</div>
</Tooltip>
}
</Grid>
<Grid item xs={12} align="center">
<ButtonGroup variant="contained" aria-label="outlined primary button group">

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

View File

@ -43,7 +43,6 @@ body {
}
.newAvatar {
background-color:white;
border-radius: 50%;
border: 2px solid #555;
filter: drop-shadow(1px 1px 1px #000000);
@ -67,3 +66,26 @@ body {
border: 0.3px solid #555;
filter: drop-shadow(0.5px 0.5px 0.5px #000000);
}
.phoneFlippedSmallAvatar img{
transform: scaleX(-1);
border: 1.3px solid #1976d2;
-webkit-filter: grayscale(100%);
filter: grayscale(100%) brightness(150%) contrast(150%) drop-shadow(0.7px 0.7px 0.7px #000000);
}
.phoneFlippedSmallAvatar:after {
content: '';
position: absolute;
top: 0; left: 0; bottom: 0; right: 0;
border-radius: 50%;
border: 2.4px solid #1976d2;
box-shadow: inset 0px 0px 35px rgb(255, 255, 255);
}
.bookAvatar {
border-radius: 50%;
transform: scaleX(-1);
border: 0.3px solid #555;
filter: drop-shadow(0.5px 0.5px 0.5px #000000);
}

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

View File

@ -2,8 +2,10 @@
<html>
<head>
<meta http-equiv="onion-location" content="{{ ONION_LOCATION }}" />
{% comment %} TODO Add a proper fav icon {% endcomment %}
<link rel="shortcut icon" href="#" />
<link rel="shortcut icon" href="/static/assets/images/favicon-96x96.png" />
<link rel="icon" type="image/png" href="/static/assets/images/favicon-32x32.png" sizes="32x32">
<link rel="icon" type="image/png" href="/static/assets/images/favicon-96x96.png" sizes="96x96">
<link rel="icon" type="image/png" href="/static/assets/images/favicon-192x192.png" sizes="192x192">
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />

View File

@ -1,41 +1,4 @@
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==3.2.11
django-admin-relation-links==0.2.5
django-celery-beat==2.2.1
django-celery-results==2.2.0
@ -44,97 +7,17 @@ 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
channels==3.0.4
channels-redis==3.3.1
celery==5.2.3
googleapis-common-protos==1.53.0
grpcio==1.39.0
grpcio==1.43.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
numpy==1.22.2
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
scipy==1.8.0
gunicorn==20.1.0

View File

@ -8,9 +8,11 @@ https://docs.djangoproject.com/en/4.0/howto/deployment/asgi/
"""
import os
import django
from channels.routing import get_default_application
from django.core.asgi import get_asgi_application
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'tabulator.settings')
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'robosats.settings')
django.setup()
application = get_asgi_application()
application = get_default_application()

View File

@ -10,24 +10,33 @@ For the full list of settings and their values, see
https://docs.djangoproject.com/en/4.0/ref/settings/
"""
import os
from pathlib import Path
from decouple import config
# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent
STATIC_URL = '/static/'
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/4.0/howto/deployment/checklist/
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = 'django-insecure-6^&6uw$b5^en%(cu2kc7_o)(mgpazx#j_znwlym0vxfamn2uo-'
SECRET_KEY = config('SECRET_KEY')
DEBUG = False
STATIC_URL = 'static/'
STATIC_ROOT ='/usr/src/static/'
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True
if os.environ.get('DEVELOPMENT'):
DEBUG = True
STATIC_ROOT = 'frontend/static/'
ALLOWED_HOSTS = [config('HOST_NAME'),'127.0.0.1']
AVATAR_ROOT = STATIC_ROOT + 'assets/avatars/'
ALLOWED_HOSTS = [config('HOST_NAME'),config('HOST_NAME2'),config('LOCAL_ALIAS'),'127.0.0.1']
# Application definition
@ -82,13 +91,16 @@ WSGI_APPLICATION = 'robosats.wsgi.application'
# Database
# https://docs.djangoproject.com/en/4.0/ref/settings/#databases
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': BASE_DIR / 'db.sqlite3',
}
}
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': '/usr/src/database/db.sqlite3',
'OPTIONS': {
'timeout': 20, # in seconds
}
}
}
# Password validation
# https://docs.djangoproject.com/en/4.0/ref/settings/#auth-password-validators

View File

@ -1,4 +1,46 @@
# Set up
# The easy way
## With Docker (-dev containers running on testnet)
Spinning up docker for the first time
```
docker-compose build --no-cache
docker-compose up -d
docker exec -it django-dev python3 manage.py makemigrations
docker exec -it django-dev python3 manage.py migrate
docker exec -it django-dev python3 manage.py createsuperuser
docker-compose restart
```
Spinning up any other time:
`docker-compose up -d`
Then monitor in a terminal the Django dev docker service
`docker attach django-dev`
And the NPM dev docker service
`docker attach npm-dev`
You could also just check all services logs
`docker-compose logs -f`
Ready to roll! But maybe you also are interested on these:
Unlock or 'create' the lnd node
`docker exec -it lnd-dev lncli unlock`
Create p2wkh addresses
`docker exec -it lnd-dev lncli --network=testnet newaddress p2wkh`
Wallet balance
`docker exec -it lnd-dev lncli --network=testnet walletbalance`
Connect
`docker exec -it lnd-dev lncli --network=testnet connect node_id@ip:9735`
Open channel
`docker exec -it lnd-dev lncli --network=testnet openchannel node_id --local_amt LOCAL_AMT --push_amt PUSH_AMT`
# The harder way
## Django development environment
### Install Python and pip
`sudo apt install python3 python3 pip`
@ -99,7 +141,16 @@ to
`from . import lightning_pb2 as lightning__pb2`
Same for every other file
Same for every other file.
Generated files can be automatically patched like this:
```
sed -i 's/^import .*_pb2 as/from . \0/' api/lightning/router_pb2.py
sed -i 's/^import .*_pb2 as/from . \0/' api/lightning/invoices_pb2.py
sed -i 's/^import .*_pb2 as/from . \0/' api/lightning/router_pb2_grpc.py
sed -i 's/^import .*_pb2 as/from . \0/' api/lightning/lightning_pb2_grpc.py
sed -i 's/^import .*_pb2 as/from . \0/' api/lightning/invoices_pb2_grpc.py
```
## React development environment
### Install npm
@ -131,7 +182,7 @@ npm install react-qr-reader
```
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)
### Launch the React render
### Launch
from frontend/ directory
`npm run dev`