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 directory to read TLS cert and macaroon
LND_CERT_BASE64='' LND_DIR='/lnd/'
# base64 ~/.lnd/data/chain/bitcoin/testnet/admin.macaroon | tr -d '\n' MACAROON_PATH='data/chain/bitcoin/testnet/admin.macaroon'
LND_MACAROON_BASE64=''
LND_GRPC_HOST='127.0.0.1:10009'
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. # 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 MARKET_PRICE_APIS = https://blockchain.info/ticker, https://api.yadio.io/exrates/BTC
# Host e.g. robosats.com # Host e.g. robosats.com
HOST_NAME = '' 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 # e.g. robotestagw3dcxmd66r4rgksb4nmmr43fh77bzn2ia2eucduyeafnyd.onion
ONION_LOCATION = '' 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 % # Trade fee in percentage %
FEE = 0.002 FEE = 0.002
# Bond size in percentage % # Bond size in percentage %

3
.gitignore vendored
View File

@ -637,7 +637,6 @@ FodyWeavers.xsd
# Django # Django
*migrations* *migrations*
frontend/static/frontend/main*
# Celery # Celery
django django
@ -648,3 +647,5 @@ api/lightning/lightning*
api/lightning/invoices* api/lightning/invoices*
api/lightning/router* api/lightning/router*
api/lightning/googleapis* 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) @admin.register(Profile)
class UserProfileAdmin(AdminChangeLinksMixin, admin.ModelAdmin): 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') list_display_links = ('avatar_tag','id')
change_links =['user'] change_links =['user']
readonly_fields = ['avatar_tag'] 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 lightning_pb2 as lnrpc, lightning_pb2_grpc as lightningstub
from . import invoices_pb2 as invoicesrpc, invoices_pb2_grpc as invoicesstub from . import invoices_pb2 as invoicesrpc, invoices_pb2_grpc as invoicesstub
from . import router_pb2 as routerrpc, router_pb2_grpc as routerstub 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) # Should work with LND (c-lightning in the future if there are features that deserve the work)
####### #######
CERT = b64decode(config('LND_CERT_BASE64')) # Read tls.cert from file or .env variable string encoded as base64
MACAROON = b64decode(config('LND_MACAROON_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') LND_GRPC_HOST = config('LND_GRPC_HOST')
class LNNode(): class LNNode():
@ -122,22 +132,11 @@ class LNNode():
lnpayment.save() lnpayment.save()
return True return True
# @classmethod @classmethod
# def check_until_invoice_locked(cls, payment_hash, expiration): def resetmc(cls):
# '''Checks until hold invoice is locked. request = routerrpc.ResetMissionControlRequest()
# When invoice is locked, returns true. response = cls.routerstub.ResetMissionControl(request, metadata=[('macaroon', MACAROON.hex())])
# If time expires, return False.''' return True
# # 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 @classmethod

View File

@ -1,6 +1,7 @@
from datetime import timedelta from datetime import timedelta
from django.utils import timezone from django.utils import timezone
from api.lightning.node import LNNode from api.lightning.node import LNNode
from django.db.models import Q
from api.models import Order, LNPayment, MarketTick, User, Currency from api.models import Order, LNPayment, MarketTick, User, Currency
from decouple import config from decouple import config
@ -30,7 +31,8 @@ FIAT_EXCHANGE_DURATION = int(config('FIAT_EXCHANGE_DURATION'))
class Logics(): 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''' '''Validates if a use is already not part of an active order'''
active_order_status = [Order.Status.WFB, Order.Status.PUB, Order.Status.TAK, 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) queryset = Order.objects.filter(taker=user, status__in=active_order_status)
if queryset.exists(): if queryset.exists():
return False, {'bad_request':'You are already taker of an active order'}, queryset[0] 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 return True, None, None
def validate_order_size(order): 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 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 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 @classmethod
def take(cls, order, user): def take(cls, order, user):
is_penalized, time_out = cls.is_penalized(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.'} return False, {'bad_request':'Only the buyer of this order can provide a buyer invoice.'}
if not order.taker_bond: if not order.taker_bond:
return False, {'bad_request':'Wait for your order to be taken.'} 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.'} return False, {'bad_request':'You cannot submit a invoice while bonds are not locked.'}
num_satoshis = cls.payout_amount(order, user)[1]['invoice_amount'] 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 the order status is 'Failed Routing'. Retry payment.
if order.status == Order.Status.FAI: if order.status == Order.Status.FAI:
# Double check the escrow is settled.
if LNNode.double_check_htlc_is_settled(order.trade_escrow.payment_hash): 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() order.save()
return True, None 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: 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) #Settle the maker bond (Maker loses the bond for canceling an ongoing trade)
valid = cls.settle_bond(order.maker_bond) valid = cls.settle_bond(order.maker_bond)
cls.return_bond(order.taker_bond) # returns taker bond
if valid: if valid:
order.status = Order.Status.UCA order.status = Order.Status.UCA
order.save() order.save()
@ -508,13 +530,20 @@ class Logics():
order.last_satoshis = cls.satoshis_now(order) order.last_satoshis = cls.satoshis_now(order)
bond_satoshis = int(order.last_satoshis * BOND_SIZE) bond_satoshis = int(order.last_satoshis * BOND_SIZE)
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 # Gen hold Invoice
hold_payment = LNNode.gen_hold_invoice(bond_satoshis, try:
description, hold_payment = LNNode.gen_hold_invoice(bond_satoshis,
invoice_expiry=Order.t_to_expire[Order.Status.WFB], description,
cltv_expiry_secs=BOND_EXPIRY*3600) 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( order.maker_bond = LNPayment.objects.create(
concept = LNPayment.Concepts.MAKEBOND, concept = LNPayment.Concepts.MAKEBOND,
@ -589,13 +618,18 @@ class Logics():
bond_satoshis = int(order.last_satoshis * BOND_SIZE) bond_satoshis = int(order.last_satoshis * BOND_SIZE)
pos_text = 'Buying' if cls.is_buyer(order, user) else 'Selling' pos_text = 'Buying' if cls.is_buyer(order, user) else 'Selling'
description = (f"RoboSats - Taking 'Order {order.id}' {pos_text} BTC for {str(float(order.amount)) + Currency.currency_dict[str(order.currency.currency)]}" 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 # Gen hold Invoice
hold_payment = LNNode.gen_hold_invoice(bond_satoshis, try:
description, hold_payment = LNNode.gen_hold_invoice(bond_satoshis,
invoice_expiry=Order.t_to_expire[Order.Status.TAK], description,
cltv_expiry_secs=BOND_EXPIRY*3600) 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( order.taker_bond = LNPayment.objects.create(
concept = LNPayment.Concepts.TAKEBOND, concept = LNPayment.Concepts.TAKEBOND,
@ -654,13 +688,19 @@ class Logics():
# If there was no taker_bond object yet, generate one # If there was no taker_bond object yet, generate one
escrow_satoshis = order.last_satoshis # Amount was fixed when taker bond was locked escrow_satoshis = order.last_satoshis # Amount was fixed when taker bond was locked
description = f"RoboSats - Escrow amount for '{str(order)}' - The escrow will be released to the buyer once you confirm you received the fiat. It will automatically return if buyer does not confirm the payment." description = f"RoboSats - Escrow amount for '{str(order)}' - 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 # Gen hold Invoice
hold_payment = LNNode.gen_hold_invoice(escrow_satoshis, try:
description, hold_payment = LNNode.gen_hold_invoice(escrow_satoshis,
invoice_expiry=Order.t_to_expire[Order.Status.WF2], description,
cltv_expiry_secs=ESCROW_EXPIRY*3600) 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( order.trade_escrow = LNPayment.objects.create(
concept = LNPayment.Concepts.TRESCROW, concept = LNPayment.Concepts.TRESCROW,
@ -776,13 +816,20 @@ class Logics():
# RETURN THE BONDS // Probably best also do it even if payment failed # RETURN THE BONDS // Probably best also do it even if payment failed
cls.return_bond(order.taker_bond) cls.return_bond(order.taker_bond)
cls.return_bond(order.maker_bond) cls.return_bond(order.maker_bond)
is_payed, context = follow_send_payment(order.payout) ##### !!! KEY LINE - PAYS THE BUYER INVOICE !!! ##### !!! KEY LINE - PAYS THE BUYER INVOICE !!!
if is_payed: ##### Backgroun process "follow_invoices" will try to pay this invoice until success
order.save() order.status = Order.Status.PAY
return True, context order.payout.status = LNPayment.Status.FLIGHT
else: order.payout.save()
# error handling here order.save()
return False, context 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: else:
return False, {'bad_request':'You cannot confirm the fiat payment at this stage'} 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 False, {'bad_request':'You cannot rate your counterparty yet.'}
return True, None 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: try:
self.follow_hold_invoices() self.follow_hold_invoices()
self.retry_payments()
except Exception as e: except Exception as e:
if 'database is locked' in str(e): self.stdout.write(str(e))
self.stdout.write('database is locked') try:
self.send_payments()
except Exception as e:
self.stdout.write(str(e)) self.stdout.write(str(e))
def follow_hold_invoices(self): def follow_hold_invoices(self):
@ -117,18 +117,32 @@ class Command(BaseCommand):
self.stdout.write(str(timezone.now())) self.stdout.write(str(timezone.now()))
self.stdout.write(str(debug)) self.stdout.write(str(debug))
def retry_payments(self): def send_payments(self):
''' Checks if any payment is due for retry, and tries to pay it''' '''
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, 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], 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'))))) last_routing_time__lt=(timezone.now()-timedelta(minutes=int(config('RETRY_TIME')))))
queryset = queryset.union(queryset_retries)
for lnpayment in queryset: 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 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.status = LNPayment.Status.EXPIRE
lnpayment.routing_attempts = 0 lnpayment.routing_attempts = 0
lnpayment.save() lnpayment.save()

View File

@ -6,6 +6,7 @@ from django.template.defaultfilters import truncatechars
from django.dispatch import receiver from django.dispatch import receiver
from django.utils.html import mark_safe from django.utils.html import mark_safe
import uuid import uuid
from django.conf import settings
from decouple import config from decouple import config
from pathlib import Path from pathlib import Path
@ -169,6 +170,8 @@ class Order(models.Model):
# ratings # ratings
maker_rated = models.BooleanField(default=False, null=False) maker_rated = models.BooleanField(default=False, null=False)
taker_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 = { t_to_expire = {
0 : int(config('EXP_MAKER_BOND_INVOICE')) , # 'Waiting for maker bond' 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 pass
class Profile(models.Model): class Profile(models.Model):
user = models.OneToOneField(User,on_delete=models.CASCADE) user = models.OneToOneField(User,on_delete=models.CASCADE)
# Total trades # 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 orders_disputes_started = models.CharField(max_length=999, null=True, default=None, validators=[validate_comma_separated_integer_list], blank=True) # Will only store ID of orders
# RoboHash # RoboHash
avatar = models.ImageField(default="static/assets/misc/unknown_avatar.png", verbose_name='Avatar', blank=True) avatar = models.ImageField(default=(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 (only used then taking/cancelling repeatedly orders in the book before comitting bond)
penalty_expiration = models.DateTimeField(null=True,default=None, blank=True) 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) @receiver(post_save, sender=User)
def create_user_profile(sender, instance, created, **kwargs): def create_user_profile(sender, instance, created, **kwargs):
if created: if created:
@ -242,7 +247,7 @@ class Profile(models.Model):
@receiver(pre_delete, sender=User) @receiver(pre_delete, sender=User)
def del_avatar_from_disk(sender, instance, **kwargs): def del_avatar_from_disk(sender, instance, **kwargs):
try: try:
avatar_file=Path('frontend/' + instance.profile.avatar.url) avatar_file=Path(settings.AVATAR_ROOT + instance.profile.avatar.url)
avatar_file.unlink() avatar_file.unlink()
except: except:
pass pass
@ -253,7 +258,7 @@ class Profile(models.Model):
# to display avatars in admin panel # to display avatars in admin panel
def get_avatar(self): def get_avatar(self):
if not self.avatar: if not self.avatar:
return 'static/assets/misc/unknown_avatar.png' return settings.STATIC_ROOT + 'unknown_avatar.png'
return self.avatar.url return self.avatar.url
# method to create a fake table field in read only mode # 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): class UpdateOrderSerializer(serializers.Serializer):
invoice = serializers.CharField(max_length=2000, allow_null=True, allow_blank=True, default=None) invoice = serializers.CharField(max_length=2000, allow_null=True, allow_blank=True, default=None)
statement = serializers.CharField(max_length=10000, 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) 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 django.utils import timezone
from datetime import timedelta from datetime import timedelta
from api.lightning.node import LNNode from api.lightning.node import LNNode, MACAROON
from api.models import LNPayment, Order from api.models import LNPayment, Order
MACAROON = b64decode(config('LND_MACAROON_BASE64')) 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
fee_limit_sat = max(lnpayment.num_satoshis * 0.0002, 10) # 200 ppm or 10 sats max
request = LNNode.routerrpc.SendPaymentRequest( request = LNNode.routerrpc.SendPaymentRequest(
payment_request=lnpayment.invoice, payment_request=lnpayment.invoice,
fee_limit_sat=fee_limit_sat, fee_limit_sat=fee_limit_sat,
@ -77,7 +75,6 @@ def follow_send_payment(lnpayment):
order.save() order.save()
context = {'routing_failed': LNNode.payment_failure_context[response.failure_reason]} context = {'routing_failed': LNNode.payment_failure_context[response.failure_reason]}
print(context) print(context)
# Call a retry in 5 mins here?
return False, context return False, context
if response.status == 2 : # Status 2 'SUCCEEDED' if response.status == 2 : # Status 2 'SUCCEEDED'

View File

@ -50,23 +50,32 @@ def get_exchange_rates(currencies):
return median_rates.tolist() return median_rates.tolist()
lnd_v_cache = {}
@ring.dict(lnd_v_cache, expire=3600) #keeps in cache for 3600 seconds
def get_lnd_version(): def get_lnd_version():
stream = os.popen('lnd --version') # If dockerized, return LND_VERSION envvar used for docker image.
lnd_version = stream.read()[:-1] # 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 = {} robosats_commit_cache = {}
@ring.dict(robosats_commit_cache, expire=3600) @ring.dict(robosats_commit_cache, expire=3600)
def get_commit_robosats(): def get_commit_robosats():
stream = os.popen('git log -n 1 --pretty=format:"%H"') commit = os.popen('git log -n 1 --pretty=format:"%H"')
lnd_version = stream.read() commit_hash = commit.read()
return lnd_version return commit_hash
premium_percentile = {} premium_percentile = {}
@ring.dict(premium_percentile, expire=300) @ring.dict(premium_percentile, expire=300)

View File

@ -1,3 +1,4 @@
import os
from re import T from re import T
from django.db.models import query from django.db.models import query
from rest_framework import status, viewsets from rest_framework import status, viewsets
@ -22,13 +23,15 @@ import hashlib
from pathlib import Path from pathlib import Path
from datetime import timedelta, datetime from datetime import timedelta, datetime
from django.utils import timezone from django.utils import timezone
from django.conf import settings
from decouple import config from decouple import config
EXP_MAKER_BOND_INVOICE = int(config('EXP_MAKER_BOND_INVOICE')) EXP_MAKER_BOND_INVOICE = int(config('EXP_MAKER_BOND_INVOICE'))
FEE = float(config('FEE')) FEE = float(config('FEE'))
RETRY_TIME = int(config('RETRY_TIME')) 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) avatar_path.mkdir(parents=True, exist_ok=True)
# Create your views here. # Create your views here.
@ -136,20 +139,9 @@ class OrderView(viewsets.ViewSet):
# Add activity status of participants based on last_seen # Add activity status of participants based on last_seen
if order.taker_last_seen != None: if order.taker_last_seen != None:
if order.taker_last_seen > (timezone.now() - timedelta(minutes=2)): data['taker_status'] = Logics.user_activity_status(order.taker_last_seen)
data['taker_status'] = 'active'
elif order.taker_last_seen > (timezone.now() - timedelta(minutes=10)):
data['taker_status'] = 'seen_recently'
else:
data['taker_status'] = 'inactive'
if order.maker_last_seen != None: if order.maker_last_seen != None:
if order.maker_last_seen > (timezone.now() - timedelta(minutes=2)): data['maker_status'] = Logics.user_activity_status(order.maker_last_seen)
data['maker_status'] = 'active'
elif order.maker_last_seen > (timezone.now() - timedelta(minutes=10)):
data['maker_status'] = 'seen_recently'
else:
data['maker_status'] = 'inactive'
# 3.b If order is between public and WF2 # 3.b If order is between public and WF2
if order.status >= Order.Status.PUB and order.status < Order.Status.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. # 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: if data['is_maker'] and order.status == Order.Status.PUB:
data['robots_in_book'] = None # TODO
data['premium_percentile'] = compute_premium_percentile(order) data['premium_percentile'] = compute_premium_percentile(order)
data['num_similar_orders'] = len(Order.objects.filter(currency=order.currency, status=Order.Status.PUB)) 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) order = Order.objects.get(id=order_id)
# action is either 1)'take', 2)'confirm', 3)'cancel', 4)'dispute' , 5)'update_invoice' # 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') action = serializer.data.get('action')
invoice = serializer.data.get('invoice') invoice = serializer.data.get('invoice')
statement = serializer.data.get('statement') statement = serializer.data.get('statement')
@ -335,10 +326,15 @@ class OrderView(viewsets.ViewSet):
if not valid: return Response(context, status.HTTP_400_BAD_REQUEST) if not valid: return Response(context, status.HTTP_400_BAD_REQUEST)
# 6) If action is rate # 6) If action is rate
elif action == 'rate' and rating: elif action == 'rate_user' and rating:
valid, context = Logics.rate_counterparty(order,request.user, rating) valid, context = Logics.rate_counterparty(order,request.user, rating)
if not valid: return Response(context, status.HTTP_400_BAD_REQUEST) if not valid: return Response(context, status.HTTP_400_BAD_REQUEST)
# 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! # If nothing of the above... something else is going on. Probably not allowed!
else: else:
return Response( return Response(
@ -419,7 +415,7 @@ class UserView(APIView):
if len(User.objects.filter(username=nickname)) == 0: if len(User.objects.filter(username=nickname)) == 0:
User.objects.create_user(username=nickname, password=token, is_staff=False) User.objects.create_user(username=nickname, password=token, is_staff=False)
user = authenticate(request, username=nickname, password=token) 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) login(request, user)
return Response(context, status=status.HTTP_201_CREATED) return Response(context, status=status.HTTP_201_CREATED)
@ -427,9 +423,9 @@ class UserView(APIView):
user = authenticate(request, username=nickname, password=token) user = authenticate(request, username=nickname, password=token)
if user is not None: if user is not None:
login(request, user) login(request, user)
# Sends the welcome back message, only if created +30 mins ago # Sends the welcome back message, only if created +3 mins ago
if request.user.date_joined < (timezone.now()-timedelta(minutes=30)): if request.user.date_joined < (timezone.now()-timedelta(minutes=3)):
context['found'] = 'We found your Robosat. Welcome back!' context['found'] = 'We found your Robot avatar. Welcome back!'
return Response(context, status=status.HTTP_202_ACCEPTED) return Response(context, status=status.HTTP_202_ACCEPTED)
else: else:
# It is unlikely, but maybe the nickname is taken (1 in 20 Billion change) # 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. # Compute current premium for those orders that are explicitly priced.
data['price'], data['premium'] = Logics.price_and_premium_now(order) 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 for key in ('status','taker'): # Non participants should not see the status or who is the taker
del data[key] del data[key]
@ -534,6 +530,11 @@ class InfoView(ListAPIView):
context['lifetime_satoshis_settled'] = lifetime_volume_settled context['lifetime_satoshis_settled'] = lifetime_volume_settled
context['lnd_version'] = get_lnd_version() context['lnd_version'] = get_lnd_version()
context['robosats_running_commit_hash'] = get_commit_robosats() context['robosats_running_commit_hash'] = get_commit_robosats()
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['fee'] = FEE
context['bond_size'] = float(config('BOND_SIZE')) context['bond_size'] = float(config('BOND_SIZE'))
if request.user.is_authenticated: 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 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 { Link } from 'react-router-dom'
import { DataGrid } from '@mui/x-data-grid'; import { DataGrid } from '@mui/x-data-grid';
import MediaQuery from 'react-responsive' import MediaQuery from 'react-responsive'
import Image from 'material-ui-image'
import getFlags from './getFlags' import getFlags from './getFlags'
@ -69,6 +70,13 @@ export default class BookPage extends Component {
return x.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ","); 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=()=>{ bookListTableDesktop=()=>{
return ( return (
<div style={{ height: 475, width: '100%' }}> <div style={{ height: 475, width: '100%' }}>
@ -77,8 +85,9 @@ export default class BookPage extends Component {
this.state.orders.map((order) => this.state.orders.map((order) =>
({id: order.id, ({id: order.id,
avatar: window.location.origin +'/static/assets/avatars/' + order.maker_nick + '.png', avatar: window.location.origin +'/static/assets/avatars/' + order.maker_nick + '.png',
robosat: order.maker_nick, robot: order.maker_nick,
type: order.type ? "Sell": "Buy", robot_status: order.maker_status,
type: order.type ? "Seller": "Buyer",
amount: parseFloat(parseFloat(order.amount).toFixed(4)), amount: parseFloat(parseFloat(order.amount).toFixed(4)),
currency: this.getCurrencyCode(order.currency), currency: this.getCurrencyCode(order.currency),
payment_method: order.payment_method, payment_method: order.payment_method,
@ -89,22 +98,34 @@ export default class BookPage extends Component {
columns={[ columns={[
// { field: 'id', headerName: 'ID', width: 40 }, // { field: 'id', headerName: 'ID', width: 40 },
{ field: 'robosat', headerName: 'RoboSat', width: 240, { field: 'robot', headerName: 'Robot', width: 240,
renderCell: (params) => {return ( renderCell: (params) => {return (
<ListItemButton style={{ cursor: "pointer" }}> <ListItemButton style={{ cursor: "pointer" }}>
<ListItemAvatar> <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> </ListItemAvatar>
<ListItemText primary={params.row.robosat}/> <ListItemText primary={params.row.robot}/>
</ListItemButton> </ListItemButton>
); );
} }, } },
{ field: 'type', headerName: 'Type', width: 60 }, { field: 'type', headerName: 'Is', width: 60 },
{ field: 'amount', headerName: 'Amount', type: 'number', width: 80 }, { field: 'amount', headerName: 'Amount', type: 'number', width: 80 },
{ field: 'currency', headerName: 'Currency', width: 100, { field: 'currency', headerName: 'Currency', width: 100,
renderCell: (params) => {return ( 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: 'payment_method', headerName: 'Payment Method', width: 180 },
{ field: 'price', headerName: 'Price', type: 'number', width: 140, { field: 'price', headerName: 'Price', type: 'number', width: 140,
renderCell: (params) => {return ( renderCell: (params) => {return (
@ -126,14 +147,15 @@ export default class BookPage extends Component {
bookListTablePhone=()=>{ bookListTablePhone=()=>{
return ( return (
<div style={{ height: 425, width: '100%' }}> <div style={{ height: 422, width: '100%' }}>
<DataGrid <DataGrid
rows={ rows={
this.state.orders.map((order) => this.state.orders.map((order) =>
({id: order.id, ({id: order.id,
avatar: window.location.origin +'/static/assets/avatars/' + order.maker_nick + '.png', avatar: window.location.origin +'/static/assets/avatars/' + order.maker_nick + '.png',
robosat: order.maker_nick, robot: order.maker_nick,
type: order.type ? "Sell": "Buy", robot_status: order.maker_status,
type: order.type ? "Seller": "Buyer",
amount: parseFloat(parseFloat(order.amount).toFixed(4)), amount: parseFloat(parseFloat(order.amount).toFixed(4)),
currency: this.getCurrencyCode(order.currency), currency: this.getCurrencyCode(order.currency),
payment_method: order.payment_method, payment_method: order.payment_method,
@ -144,18 +166,42 @@ export default class BookPage extends Component {
columns={[ columns={[
// { field: 'id', headerName: 'ID', width: 40 }, // { field: 'id', headerName: 'ID', width: 40 },
{ field: 'robosat', headerName: 'Robot', width: 80, { field: 'robot', headerName: 'Robot', width: 80,
renderCell: (params) => {return ( renderCell: (params) => {return (
<ListItemButton style={{ cursor: "pointer" }}> <Tooltip placement="right" enterTouchDelay="0" title={params.row.robot+" ("+params.row.robot_status+")"}>
<Avatar className="flippedSmallAvatar" alt={params.row.robosat} src={params.row.avatar} /> <Badge variant="dot" overlap="circular" badgeContent="" color={this.statusBadgeColor(params.row.robot_status)}>
</ListItemButton> <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: 'type', headerName: 'Is', width: 60, hide:'true'},
{ field: 'amount', headerName: 'Amount', type: 'number', width: 80 }, { 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, { field: 'currency', headerName: 'Currency', width: 100,
renderCell: (params) => {return ( 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: 'payment_method', headerName: 'Payment Method', width: 180, hide:'true'},
{ field: 'price', headerName: 'Price', type: 'number', width: 140, 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, { field: 'premium', headerName: 'Premium', type: 'number', width: 85,
renderCell: (params) => {return ( 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> <div style={{ cursor: "pointer" }}>{parseFloat(parseFloat(params.row.premium).toFixed(4))+"%" }</div>
</Tooltip>
)} }, )} },
]} ]}
@ -179,11 +227,9 @@ export default class BookPage extends Component {
render() { render() {
return ( return (
<Grid className='orderBook' container spacing={1} sx={{minWidth:400}}> <Grid className='orderBook' container spacing={1} sx={{minWidth:400}}>
<Grid item xs={12} align="center"> {/* <Grid item xs={12} align="center">
<Typography component="h2" variant="h2"> <Typography component="h4" variant="h4">ORDER BOOK</Typography>
Order Book </Grid> */}
</Typography>
</Grid>
<Grid item xs={6} align="right"> <Grid item xs={6} align="right">
<FormControl > <FormControl >

View File

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

View File

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

View File

@ -1,5 +1,5 @@
import React, { Component } from 'react'; 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 { Link } from 'react-router-dom'
import getFlags from './getFlags' import getFlags from './getFlags'
@ -22,6 +22,9 @@ const csrftoken = getCookie('csrftoken');
// pretty numbers // pretty numbers
function pn(x) { function pn(x) {
if(x==null){
return(null)
}
return x.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ","); return x.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",");
} }
@ -67,37 +70,48 @@ export default class MakerPage extends Component {
handlePaymentMethodChange=(e)=>{ handlePaymentMethodChange=(e)=>{
this.setState({ this.setState({
payment_method: e.target.value, payment_method: e.target.value,
badPaymentMethod: e.target.value.length > 35,
}); });
} }
handlePremiumChange=(e)=>{ 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({ this.setState({
premium: e.target.value, premium: e.target.value,
badPremium: bad_premium,
}); });
} }
handleSatoshisChange=(e)=>{ handleSatoshisChange=(e)=>{
var bad_sats = e.target.value > this.maxTradeSats ? if(e.target.value > this.maxTradeSats){
("Must be less than "+pn(this.maxTradeSats)): var bad_sats = "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.minTradeSats){
var bad_sats = "Must be more than "+pn(this.minTradeSats)
}
this.setState({ this.setState({
satoshis: e.target.value, satoshis: e.target.value,
badSatoshis: bad_sats, badSatoshis: bad_sats,
}) });
;
} }
handleClickRelative=(e)=>{ handleClickRelative=(e)=>{
this.setState({ this.setState({
is_explicit: false, is_explicit: false,
satoshis: null,
premium: 0,
}); });
this.handlePremiumChange();
} }
handleClickExplicit=(e)=>{ handleClickExplicit=(e)=>{
this.setState({ this.setState({
is_explicit: true, is_explicit: true,
premium: null,
}); });
this.handleSatoshisChange();
} }
handleCreateOfferButtonPressed=()=>{ handleCreateOfferButtonPressed=()=>{
@ -112,8 +126,8 @@ export default class MakerPage extends Component {
amount: this.state.amount, amount: this.state.amount,
payment_method: this.state.payment_method, payment_method: this.state.payment_method,
is_explicit: this.state.is_explicit, is_explicit: this.state.is_explicit,
premium: this.state.premium, premium: this.state.is_explicit ? null: this.state.premium,
satoshis: this.state.satoshis, satoshis: this.state.is_explicit ? this.state.satoshis: null,
}), }),
}; };
fetch("/api/make/",requestOptions) fetch("/api/make/",requestOptions)
@ -136,16 +150,17 @@ export default class MakerPage extends Component {
return this.state.currencies_dict[val.toString()] return this.state.currencies_dict[val.toString()]
} }
render() { render() {
return ( return (
<Grid container xs={12} align="center" spacing={1}> <Grid container xs={12} align="center" spacing={1} sx={{minWidth:380}}>
<Grid item xs={12} align="center"> {/* <Grid item xs={12} align="center" sx={{minWidth:380}}>
<Typography component="h2" variant="h2"> <Typography component="h4" variant="h4">
Order Maker ORDER MAKER
</Typography> </Typography>
</Grid> </Grid> */}
<Grid item xs={12} align="center" spacing={1}> <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}> <Grid item xs={12} align="center" spacing={1}>
<FormControl component="fieldset"> <FormControl component="fieldset">
<FormHelperText> <FormHelperText>
@ -167,10 +182,12 @@ export default class MakerPage extends Component {
</RadioGroup> </RadioGroup>
</FormControl> </FormControl>
</Grid> </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 <TextField
error={this.state.amount == 0} error={this.state.amount <= 0}
helperText={this.state.amount == 0 ? 'Must be more than 0' : null} helperText={this.state.amount <= 0 ? 'Invalid' : null}
label="Amount" label="Amount"
type="number" type="number"
required="true" required="true"
@ -180,28 +197,32 @@ export default class MakerPage extends Component {
}} }}
onChange={this.handleAmountChange} onChange={this.handleAmountChange}
/> />
<Select </Tooltip>
required="true" </div>
defaultValue={this.defaultCurrency} <div >
inputProps={{ <Select
style: {textAlign:"center"} required="true"
}} defaultValue={this.defaultCurrency}
onChange={this.handleCurrencyChange} inputProps={{
> style: {textAlign:"center"}
{ }}
Object.entries(this.state.currencies_dict) onChange={this.handleCurrencyChange}>
.map( ([key, value]) => <MenuItem value={parseInt(key)}> {Object.entries(this.state.currencies_dict)
{getFlags(value) + " " + value} .map( ([key, value]) => <MenuItem value={parseInt(key)}>
</MenuItem> ) {getFlags(value) + " " + value}
} </MenuItem> )}
</Select> </Select>
</div>
</Grid> </Grid>
<br/> <br/>
<Grid item xs={12} align="center"> <Grid item xs={12} align="center">
<FormControl > <Tooltip placement="top" enterTouchDelay="500" enterDelay="700" enterNextDelay="2000" title="Enter your prefered payment methods">
<TextField <TextField
sx={{width:240}}
label="Payment Method(s)" label="Payment Method(s)"
error={this.state.badPaymentMethod}
helperText={this.state.badPaymentMethod ? "Must be shorter than 35 characters":""}
type="text" type="text"
require={true} require={true}
inputProps={{ inputProps={{
@ -210,7 +231,7 @@ export default class MakerPage extends Component {
}} }}
onChange={this.handlePaymentMethodChange} onChange={this.handlePaymentMethodChange}
/> />
</FormControl> </Tooltip>
</Grid> </Grid>
<Grid item xs={12} align="center"> <Grid item xs={12} align="center">
@ -221,6 +242,7 @@ export default class MakerPage extends Component {
</div> </div>
</FormHelperText> </FormHelperText>
<RadioGroup row defaultValue="relative"> <RadioGroup row defaultValue="relative">
<Tooltip placement="top" enterTouchDelay="0" enterDelay="1000" enterNextDelay="2000" title="Let the price move with the market">
<FormControlLabel <FormControlLabel
value="relative" value="relative"
control={<Radio color="primary"/>} control={<Radio color="primary"/>}
@ -228,6 +250,8 @@ export default class MakerPage extends Component {
labelPlacement="Top" labelPlacement="Top"
onClick={this.handleClickRelative} onClick={this.handleClickRelative}
/> />
</Tooltip>
<Tooltip placement="top" enterTouchDelay="0" enterDelay="1000" enterNextDelay="2000" title="Set a fix amount of satoshis">
<FormControlLabel <FormControlLabel
value="explicit" value="explicit"
control={<Radio color="secondary"/>} control={<Radio color="secondary"/>}
@ -235,46 +259,64 @@ export default class MakerPage extends Component {
labelPlacement="Top" labelPlacement="Top"
onClick={this.handleClickExplicit} onClick={this.handleClickExplicit}
/> />
</Tooltip>
</RadioGroup> </RadioGroup>
</FormControl> </FormControl>
</Grid> </Grid>
{/* conditional shows either Premium % field or Satoshis field based on pricing method */} {/* 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 <TextField
label="Satoshis" sx={{width:240}}
error={this.state.badSatoshis} error={this.state.badPremium}
helperText={this.state.badSatoshis} helperText={this.state.badPremium}
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
label="Premium over Market (%)" label="Premium over Market (%)"
type="number" type="number"
// defaultValue={this.defaultPremium} // defaultValue={this.defaultPremium}
inputProps={{ inputProps={{
min: -100,
max: 999,
style: {textAlign:"center"} style: {textAlign:"center"}
}} }}
onChange={this.handlePremiumChange} onChange={this.handlePremiumChange}
/> />
</Grid> </div>
} </Grid>
</Paper> </Paper>
</Grid> </Grid>
<Grid item xs={12} align="center"> <Grid item xs={12} align="center">
<Button color="primary" variant="contained" onClick={this.handleCreateOfferButtonPressed} > {/* conditions to disable the make button */}
Create Order {(this.state.amount == null ||
</Button> 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>
<Grid item xs={12} align="center"> <Grid item xs={12} align="center">
{this.state.badRequest ? {this.state.badRequest ?
@ -284,8 +326,8 @@ export default class MakerPage extends Component {
: ""} : ""}
<Typography component="subtitle2" variant="subtitle2"> <Typography component="subtitle2" variant="subtitle2">
<div align='center'> <div align='center'>
Create a BTC {this.state.type==0 ? "buy":"sell"} order for {this.state.amount} {this.state.currencyCode} Create a BTC {this.state.type==0 ? "buy":"sell"} order for {pn(this.state.amount)} {this.state.currencyCode}
{this.state.is_explicit ? " of " + this.state.satoshis + " Satoshis" : {this.state.is_explicit ? " of " + pn(this.state.satoshis) + " Satoshis" :
(this.state.premium == 0 ? " at market price" : (this.state.premium == 0 ? " at market price" :
(this.state.premium > 0 ? " at a " + this.state.premium + "% premium":" at a " + -this.state.premium + "% discount") (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 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 Countdown, { zeroPad, calcTimeDelta } from 'react-countdown';
import MediaQuery from 'react-responsive' import MediaQuery from 'react-responsive'
@ -46,6 +46,7 @@ export default class OrderPage extends Component {
loading: true, loading: true,
openCancel: false, openCancel: false,
openCollaborativeCancel: false, openCollaborativeCancel: false,
openInactiveMaker: false,
showContractBox: 1, showContractBox: 1,
}; };
this.orderId = this.props.match.params.orderId; this.orderId = this.props.match.params.orderId;
@ -54,29 +55,29 @@ export default class OrderPage extends Component {
// Refresh delays according to Order status // Refresh delays according to Order status
this.statusToDelay = { this.statusToDelay = {
"0": 2000, //'Waiting for maker bond' "0": 2000, //'Waiting for maker bond'
"1": 25000, //'Public' "1": 25000, //'Public'
"2": 9999999, //'Deleted' "2": 999999, //'Deleted'
"3": 2000, //'Waiting for taker bond' "3": 2000, //'Waiting for taker bond'
"4": 9999999, //'Cancelled' "4": 999999, //'Cancelled'
"5": 999999, //'Expired' "5": 999999, //'Expired'
"6": 3000, //'Waiting for trade collateral and buyer invoice' "6": 3000, //'Waiting for trade collateral and buyer invoice'
"7": 3000, //'Waiting only for seller trade collateral' "7": 3000, //'Waiting only for seller trade collateral'
"8": 8000, //'Waiting only for buyer invoice' "8": 8000, //'Waiting only for buyer invoice'
"9": 10000, //'Sending fiat - In chatroom' "9": 10000, //'Sending fiat - In chatroom'
"10": 10000, //'Fiat sent - In chatroom' "10": 10000, //'Fiat sent - In chatroom'
"11": 30000, //'In dispute' "11": 30000, //'In dispute'
"12": 9999999, //'Collaboratively cancelled' "12": 999999, //'Collaboratively cancelled'
"13": 3000, //'Sending satoshis to buyer' "13": 3000, //'Sending satoshis to buyer'
"14": 9999999, //'Sucessful trade' "14": 999999, //'Sucessful trade'
"15": 10000, //'Failed lightning network routing' "15": 10000, //'Failed lightning network routing'
"16": 9999999, //'Maker lost dispute' "16": 180000, //'Wait for dispute resolution'
"17": 9999999, //'Taker lost dispute' "17": 180000, //'Maker lost dispute'
"18": 180000, //'Taker lost dispute'
} }
} }
completeSetState=(newStateVars)=>{ completeSetState=(newStateVars)=>{
// In case the reply only has "bad_request" // In case the reply only has "bad_request"
// Do not substitute these two for "undefined" as // Do not substitute these two for "undefined" as
// otherStateVars will fail to assign values // otherStateVars will fail to assign values
@ -149,11 +150,41 @@ export default class OrderPage extends Component {
} else { } else {
return ( 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 =()=> { LinearDeterminate =()=> {
const [progress, setProgress] = React.useState(0); const [progress, setProgress] = React.useState(0);
@ -177,18 +208,21 @@ export default class OrderPage extends Component {
); );
} }
handleClickTakeOrderButton=()=>{ takeOrder=()=>{
const requestOptions = { this.setState({loading:true})
method: 'POST',
headers: {'Content-Type':'application/json', 'X-CSRFToken': getCookie('csrftoken'),}, const requestOptions = {
body: JSON.stringify({ method: 'POST',
'action':'take', headers: {'Content-Type':'application/json', 'X-CSRFToken': getCookie('csrftoken'),},
}), body: JSON.stringify({
'action':'take',
}),
}; };
fetch('/api/order/' + '?order_id=' + this.orderId, requestOptions) fetch('/api/order/' + '?order_id=' + this.orderId, requestOptions)
.then((response) => response.json()) .then((response) => response.json())
.then((data) => this.completeSetState(data)); .then((data) => this.completeSetState(data));
} }
getCurrencyDict() { getCurrencyDict() {
fetch('/static/assets/currencies.json') fetch('/static/assets/currencies.json')
.then((response) => response.json()) .then((response) => response.json())
@ -209,16 +243,17 @@ export default class OrderPage extends Component {
} }
handleClickConfirmCancelButton=()=>{ handleClickConfirmCancelButton=()=>{
const requestOptions = { this.setState({loading:true})
method: 'POST', const requestOptions = {
headers: {'Content-Type':'application/json', 'X-CSRFToken': getCookie('csrftoken'),}, method: 'POST',
body: JSON.stringify({ headers: {'Content-Type':'application/json', 'X-CSRFToken': getCookie('csrftoken'),},
'action':'cancel', body: JSON.stringify({
}), 'action':'cancel',
}; }),
fetch('/api/order/' + '?order_id=' + this.orderId, requestOptions) };
.then((response) => response.json()) fetch('/api/order/' + '?order_id=' + this.orderId, requestOptions)
.then((data) => this.getOrderDetails(data.id)); .then((response) => response.json())
.then((data) => this.getOrderDetails(data.id));
this.handleClickCloseConfirmCancelDialog(); 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=()=>{ handleClickConfirmCollaborativeCancelButton=()=>{
const requestOptions = { const requestOptions = {
method: 'POST', method: 'POST',
@ -337,68 +402,67 @@ export default class OrderPage extends Component {
// Colors for the status badges // Colors for the status badges
statusBadgeColor(status){ statusBadgeColor(status){
if(status=='active'){ if(status=='Active'){return("success")}
return("success") if(status=='Seen recently'){return("warning")}
} if(status=='Inactive'){return('error')}
if(status=='seen_recently'){
return("warning")
}
if(status=='inactive'){
return('error')
}
} }
orderBox=()=>{ orderBox=()=>{
return( return(
<Grid container spacing={1} > <Grid container spacing={1} >
<Grid item xs={12} align="center"> <Grid item xs={12} align="center">
<MediaQuery minWidth={920}> <MediaQuery minWidth={920}>
<Typography component="h5" variant="h5"> <Typography component="h5" variant="h5">
Order Details Order Box
</Typography> </Typography>
</MediaQuery> </MediaQuery>
<Paper elevation={12} style={{ padding: 8,}}> <Paper elevation={12} style={{ padding: 8,}}>
<List dense="true"> <List dense="true">
<ListItem > <ListItem >
<ListItemAvatar sx={{ width: 56, height: 56 }}> <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" <Avatar className="flippedSmallAvatar"
alt={this.state.maker_nick} alt={this.state.maker_nick}
src={window.location.origin +'/static/assets/avatars/' + this.state.maker_nick + '.png'} src={window.location.origin +'/static/assets/avatars/' + this.state.maker_nick + '.png'}
/> />
</Badge> </Badge>
</Tooltip>
</ListItemAvatar> </ListItemAvatar>
<ListItemText primary={this.state.maker_nick + (this.state.type ? " (Seller)" : " (Buyer)")} secondary="Order maker" align="right"/> <ListItemText primary={this.state.maker_nick + (this.state.type ? " (Seller)" : " (Buyer)")} secondary="Order maker" align="right"/>
</ListItem> </ListItem>
<Divider />
{this.state.is_participant ? {this.state.is_participant ?
<> <>
{this.state.taker_nick!='None' ? {this.state.taker_nick!='None' ?
<> <>
<Divider />
<ListItem align="left"> <ListItem align="left">
<ListItemText primary={this.state.taker_nick + (this.state.type ? " (Buyer)" : " (Seller)")} secondary="Order taker"/> <ListItemText primary={this.state.taker_nick + (this.state.type ? " (Buyer)" : " (Seller)")} secondary="Order taker"/>
<ListItemAvatar > <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" <Avatar className="smallAvatar"
alt={this.state.taker_nick} alt={this.state.taker_nick}
src={window.location.origin +'/static/assets/avatars/' + this.state.taker_nick + '.png'} src={window.location.origin +'/static/assets/avatars/' + this.state.taker_nick + '.png'}
/> />
</Badge> </Badge>
</Tooltip>
</ListItemAvatar> </ListItemAvatar>
</ListItem> </ListItem>
<Divider />
</>: </>:
"" ""
} }
<Divider><Chip label='Order Details'/></Divider>
<ListItem> <ListItem>
<ListItemIcon> <ListItemIcon>
<ArticleIcon/> <ArticleIcon/>
</ListItemIcon> </ListItemIcon>
<ListItemText primary={this.state.status_message} secondary="Order status"/> <ListItemText primary={this.state.status_message} secondary="Order status"/>
</ListItem> </ListItem>
<Divider /> <Divider/>
</> </>
:"" :<Divider><Chip label='Order Details'/></Divider>
} }
<ListItem> <ListItem>
@ -458,7 +522,7 @@ export default class OrderPage extends Component {
<Divider /> <Divider />
<Grid item xs={12} align="center"> <Grid item xs={12} align="center">
<Alert severity="warning" sx={{maxWidth:360}}> <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> </Alert>
</Grid> </Grid>
</> </>
@ -498,7 +562,7 @@ export default class OrderPage extends Component {
: :
<Grid container spacing={1}> <Grid container spacing={1}>
<Grid item xs={12} align="center"> <Grid item xs={12} align="center">
<Button variant='contained' color='primary' onClick={this.handleClickTakeOrderButton}>Take Order</Button> <Countdown date={new Date(this.state.penalty)} renderer={this.countdownTakeOrderRenderer} />
</Grid> </Grid>
<Grid item xs={12} align="center"> <Grid item xs={12} align="center">
<Button variant='contained' color='secondary' onClick={this.props.history.goBack}>Back</Button> <Button variant='contained' color='secondary' onClick={this.props.history.goBack}>Back</Button>
@ -517,7 +581,7 @@ export default class OrderPage extends Component {
{this.orderBox()} {this.orderBox()}
</Grid> </Grid>
<Grid item xs={6} align="left"> <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>
</Grid> </Grid>
) )
@ -543,8 +607,8 @@ export default class OrderPage extends Component {
<Box sx={{ width: '100%' }}> <Box sx={{ width: '100%' }}>
<Box sx={{ borderBottom: 1, borderColor: 'divider' }}> <Box sx={{ borderBottom: 1, borderColor: 'divider' }}>
<Tabs value={value} onChange={handleChange} variant="fullWidth" > <Tabs value={value} onChange={handleChange} variant="fullWidth" >
<Tab label="Order Details" {...this.a11yProps(0)} /> <Tab label="Order" {...this.a11yProps(0)} />
<Tab label="Contract Box" {...this.a11yProps(1)} /> <Tab label="Contract" {...this.a11yProps(1)} />
</Tabs> </Tabs>
</Box> </Box>
<Grid container spacing={2}> <Grid container spacing={2}>
@ -553,7 +617,7 @@ export default class OrderPage extends Component {
{this.orderBox()} {this.orderBox()}
</div> </div>
<div style={{display: this.state.showContractBox == 1 ? '':'none'}}> <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> </div>
</Grid> </Grid>
</Grid> </Grid>

View File

@ -1,7 +1,7 @@
import React, { Component } from "react"; 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 QRCode from "react-qr-code";
import Countdown from 'react-countdown'; import Countdown, { zeroPad} from 'react-countdown';
import Chat from "./Chat" import Chat from "./Chat"
import MediaQuery from 'react-responsive' import MediaQuery from 'react-responsive'
import QrReader from 'react-qr-reader' import QrReader from 'react-qr-reader'
@ -234,8 +234,8 @@ export default class TradeBox extends Component {
<Divider/> <Divider/>
<Grid item xs={12} align="center"> <Grid item xs={12} align="center">
<Typography component="body2" variant="body2"> <Typography component="body2" variant="body2">
Please wait for the taker to confirm by locking a bond. Please wait for the taker to lock a bond.
If the taker does not lock a bond in time the orer will be made If the taker does not lock a bond in time, the order will be made
public again. public again.
</Typography> </Typography>
</Grid> </Grid>
@ -361,6 +361,8 @@ export default class TradeBox extends Component {
<Grid container spacing={1}> <Grid container spacing={1}>
<Grid item xs={12} align="center"> <Grid item xs={12} align="center">
{/* Make confirmation sound for HTLC received. */}
<this.Sound soundFileName="locked-invoice"/>
<Typography color="primary" component="subtitle1" variant="subtitle1"> <Typography color="primary" component="subtitle1" variant="subtitle1">
<b> Submit a LN invoice for {pn(this.props.data.invoice_amount)} Sats </b> <b> Submit a LN invoice for {pn(this.props.data.invoice_amount)} Sats </b>
</Typography> </Typography>
@ -547,12 +549,31 @@ export default class TradeBox extends Component {
.then((data) => this.props.completeSetState(data)); .then((data) => this.props.completeSetState(data));
} }
handleRatingChange=(e)=>{ handleRatingUserChange=(e)=>{
const requestOptions = { const requestOptions = {
method: 'POST', method: 'POST',
headers: {'Content-Type':'application/json', 'X-CSRFToken': getCookie('csrftoken'),}, headers: {'Content-Type':'application/json', 'X-CSRFToken': getCookie('csrftoken'),},
body: JSON.stringify({ 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, 'rating': e.target.value,
}), }),
}; };
@ -670,14 +691,38 @@ handleRatingChange=(e)=>{
</Grid> </Grid>
<Grid item xs={12} align="center"> <Grid item xs={12} align="center">
<Typography component="body2" variant="body2" 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> </Typography>
</Grid> </Grid>
<Grid item xs={12} align="center"> <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>
<Grid item xs={12} align="center"> <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>
</Grid> </Grid>
) )
@ -696,11 +741,28 @@ handleRatingChange=(e)=>{
RoboSats is trying to pay your lightning invoice. Remember that lightning nodes must RoboSats is trying to pay your lightning invoice. Remember that lightning nodes must
be online in order to receive payments. be online in order to receive payments.
</Typography> </Typography>
<br/>
<Grid item xs={12} align="center">
<CircularProgress/>
</Grid>
</Grid> </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=()=>{ showRoutingFailed=()=>{
// TODO If it has failed 3 times, ask for a new invoice. // TODO If it has failed 3 times, ask for a new invoice.
if(this.props.data.invoice_expired){ if(this.props.data.invoice_expired){
@ -713,7 +775,7 @@ handleRatingChange=(e)=>{
</Grid> </Grid>
<Grid item xs={12} align="center"> <Grid item xs={12} align="center">
<Typography component="body2" variant="body2" 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> </Typography>
</Grid> </Grid>
<Grid item xs={12} align="center"> <Grid item xs={12} align="center">

View File

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

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 { .newAvatar {
background-color:white;
border-radius: 50%; border-radius: 50%;
border: 2px solid #555; border: 2px solid #555;
filter: drop-shadow(1px 1px 1px #000000); filter: drop-shadow(1px 1px 1px #000000);
@ -67,3 +66,26 @@ body {
border: 0.3px solid #555; border: 0.3px solid #555;
filter: drop-shadow(0.5px 0.5px 0.5px #000000); 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> <html>
<head> <head>
<meta http-equiv="onion-location" content="{{ ONION_LOCATION }}" /> <meta http-equiv="onion-location" content="{{ ONION_LOCATION }}" />
{% comment %} TODO Add a proper fav icon {% endcomment %} <link rel="shortcut icon" href="/static/assets/images/favicon-96x96.png" />
<link rel="shortcut icon" href="#" /> <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 charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1" />

View File

@ -1,41 +1,4 @@
aioredis==1.3.1 django==3.2.11
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-admin-relation-links==0.2.5
django-celery-beat==2.2.1 django-celery-beat==2.2.1
django-celery-results==2.2.0 django-celery-results==2.2.0
@ -44,97 +7,17 @@ django-private-chat2==1.0.2
django-redis==5.2.0 django-redis==5.2.0
django-timezone-field==4.2.3 django-timezone-field==4.2.3
djangorestframework==3.13.1 djangorestframework==3.13.1
duplicity==0.8.12.0 channels==3.0.4
entrypoints==0.3 channels-redis==3.3.1
fasteners==0.14.1 celery==5.2.3
filelock==3.4.2
future==0.18.2
googleapis-common-protos==1.53.0 googleapis-common-protos==1.53.0
grpcio==1.39.0 grpcio==1.43.0
grpcio-tools==1.43.0 grpcio-tools==1.43.0
hiredis==2.0.0 numpy==1.22.2
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 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 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==2.22.0
requests-unixsocket==0.2.0
ring==0.9.1 ring==0.9.1
robohash==1.1 robohash==1.1
scipy==1.7.3 scipy==1.8.0
SecretStorage==2.3.1 gunicorn==20.1.0
service-identity==21.1.0
simplejson==3.16.0
six==1.16.0
sqlparse==0.4.2
stevedore==3.5.0
systemd-python==234
termcolor==1.1.0
Twisted==21.7.0
txaio==21.2.1
typing-extensions==4.0.1
ubuntu-advantage-tools==27.2
ubuntu-drivers-common==0.0.0
ufw==0.36
unattended-upgrades==0.1
urllib3==1.25.8
usb-creator==0.3.7
vine==5.0.0
virtualenv==20.12.1
virtualenv-clone==0.5.7
virtualenvwrapper==4.8.4
wadllib==1.3.3
wcwidth==0.2.5
wirerope==0.4.5
wrapt==1.13.3
xkit==0.0.0
zope.interface==5.4.0

View File

@ -8,9 +8,11 @@ https://docs.djangoproject.com/en/4.0/howto/deployment/asgi/
""" """
import os 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/ https://docs.djangoproject.com/en/4.0/ref/settings/
""" """
import os
from pathlib import Path from pathlib import Path
from decouple import config from decouple import config
# Build paths inside the project like this: BASE_DIR / 'subdir'. # Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent BASE_DIR = Path(__file__).resolve().parent.parent
STATIC_URL = '/static/'
# Quick-start development settings - unsuitable for production # Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/4.0/howto/deployment/checklist/ # See https://docs.djangoproject.com/en/4.0/howto/deployment/checklist/
# SECURITY WARNING: keep the secret key used in production secret! # 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! # 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 # Application definition
@ -82,13 +91,16 @@ WSGI_APPLICATION = 'robosats.wsgi.application'
# Database # Database
# https://docs.djangoproject.com/en/4.0/ref/settings/#databases # 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 # Password validation
# https://docs.djangoproject.com/en/4.0/ref/settings/#auth-password-validators # https://docs.djangoproject.com/en/4.0/ref/settings/#auth-password-validators

View File

@ -1,4 +1,46 @@
# Set up # 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 ## Django development environment
### Install Python and pip ### Install Python and pip
`sudo apt install python3 python3 pip` `sudo apt install python3 python3 pip`
@ -99,7 +141,16 @@ to
`from . import lightning_pb2 as lightning__pb2` `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 ## React development environment
### Install npm ### 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) 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 from frontend/ directory
`npm run dev` `npm run dev`