mirror of
https://github.com/RoboSats/robosats.git
synced 2025-01-31 10:31:35 +00:00
Merge pull request #43 from Reckless-Satoshi/stabilize-runtime
Stabilize runtime. Add docker dev containers. Many fixes.
This commit is contained in:
commit
3d6fac5367
33
.env-sample
33
.env-sample
@ -1,20 +1,41 @@
|
||||
# base64 ~/.lnd/tls.cert | tr -d '\n'
|
||||
LND_CERT_BASE64=''
|
||||
# base64 ~/.lnd/data/chain/bitcoin/testnet/admin.macaroon | tr -d '\n'
|
||||
LND_MACAROON_BASE64=''
|
||||
LND_GRPC_HOST='127.0.0.1:10009'
|
||||
# LND directory to read TLS cert and macaroon
|
||||
LND_DIR='/lnd/'
|
||||
MACAROON_PATH='data/chain/bitcoin/testnet/admin.macaroon'
|
||||
|
||||
REDIS_URL=''
|
||||
# LND directory can not be specified, instead cert and macaroon can be provided as base64 strings
|
||||
# base64 ~/.lnd/tls.cert | tr -d '\n'
|
||||
LND_CERT_BASE64='LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUNLVENDQWRDZ0F3SUJBZ0lRQ0VoeGpPZXY1bGQyVFNPTXhKalFvekFLQmdncWhrak9QUVFEQWpBNE1SOHcKSFFZRFZRUUtFeFpzYm1RZ1lYVjBiMmRsYm1WeVlYUmxaQ0JqWlhKME1SVXdFd1lEVlFRREV3d3dNakJtTVRnMQpZelkwTnpVd0hoY05Nakl3TWpBNE1UWXhOalV3V2hjTk1qTXdOREExTVRZeE5qVXdXakE0TVI4d0hRWURWUVFLCkV4WnNibVFnWVhWMGIyZGxibVZ5WVhSbFpDQmpaWEowTVJVd0V3WURWUVFERXd3d01qQm1NVGcxWXpZME56VXcKV1RBVEJnY3Foa2pPUFFJQkJnZ3Foa2pPUFFNQkJ3TkNBQVNJVWdkcVMrWFZKL3EzY0JZeWd6ZDc2endaanlmdQpLK3BzcWNYVkFyeGZjU2NXQ25jbXliNGRaMy9Lc3lLWlRaamlySDE3aEY0OGtIMlp5clRZSW9hZG80RzdNSUc0Ck1BNEdBMVVkRHdFQi93UUVBd0lDcERBVEJnTlZIU1VFRERBS0JnZ3JCZ0VGQlFjREFUQVBCZ05WSFJNQkFmOEUKQlRBREFRSC9NQjBHQTFVZERnUVdCQlEwWUJjZXdsd1BqYTJPRXFyTGxzZnJscEswUFRCaEJnTlZIUkVFV2pCWQpnZ3d3TWpCbU1UZzFZelkwTnpXQ0NXeHZZMkZzYUc5emRJSUVkVzVwZUlJS2RXNXBlSEJoWTJ0bGRJSUhZblZtClkyOXVib2NFZndBQUFZY1FBQUFBQUFBQUFBQUFBQUFBQUFBQUFZY0V3S2dRQW9jRUFBQUFBREFLQmdncWhrak8KUFFRREFnTkhBREJFQWlBd0dMY05qNXVZSkVwanhYR05OUnNFSzAwWmlSUUh2Qm50NHp6M0htWHBiZ0lnSWtvUQo3cHFvNGdWNGhiczdrSmt1bnk2bkxlNVg0ZzgxYjJQOW52ZnZ2bkk9Ci0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K'
|
||||
# base64 ~/.lnd/data/chain/bitcoin/testnet/admin.macaroon | tr -d '\n'
|
||||
LND_MACAROON_BASE64='AgEDbG5kAvgBAwoQsyI+PK+fyb7F2UyTeZ4seRIBMBoWCgdhZGRyZXNzEgRyZWFkEgV3cml0ZRoTCgRpbmZvEgRyZWFkEgV3cml0ZRoXCghpbnZvaWNlcxIEcmVhZBIFd3JpdGUaIQoIbWFjYXJvb24SCGdlbmVyYXRlEgRyZWFkEgV3cml0ZRoWCgdtZXNzYWdlEgRyZWFkEgV3cml0ZRoXCghvZmZjaGFpbhIEcmVhZBIFd3JpdGUaFgoHb25jaGFpbhIEcmVhZBIFd3JpdGUaFAoFcGVlcnMSBHJlYWQSBXdyaXRlGhgKBnNpZ25lchIIZ2VuZXJhdGUSBHJlYWQAAAYgMt90uD6v4truTadWCjlppoeJ4hZrL1SBb09Y+4WOiI0='
|
||||
|
||||
# Auto unlock LND password. Only used in development docker-compose environment.
|
||||
# It will fail starting up the node without it.
|
||||
# To disable auto unlock, comment out 'wallet-unlock-password-file=/tmp/pwd' from 'docker/lnd/lnd.conf'
|
||||
AUTO_UNLOCK_PWD='1234'
|
||||
|
||||
# List of market price public APIs. If the currency is available in more than 1 API, will use median price.
|
||||
MARKET_PRICE_APIS = https://blockchain.info/ticker, https://api.yadio.io/exrates/BTC
|
||||
|
||||
# Host e.g. robosats.com
|
||||
HOST_NAME = ''
|
||||
HOST_NAME2 = ''
|
||||
LOCAL_ALIAS = 'e.g:my_garbage_server'
|
||||
|
||||
# SECURITY WARNING: keep the secret key used in production secret!
|
||||
SECRET_KEY = 'django-insecure-6^&6uw$b5^en%(cu2kc7_o)(mgpazx#j_znwlym0vxfamn2uo-'
|
||||
|
||||
# e.g. robotestagw3dcxmd66r4rgksb4nmmr43fh77bzn2ia2eucduyeafnyd.onion
|
||||
ONION_LOCATION = ''
|
||||
|
||||
# Link to robosats alternative site (shown in frontend in statsfornerds so users can switch mainnet/testnet)
|
||||
ALTERNATIVE_SITE = 'RoboSats6tkf3eva7x2voqso3a5wcorsnw34jveyxfqi2fu7oyheasid.onion'
|
||||
ALTERNATIVE_NAME = 'RoboSats Mainnet'
|
||||
|
||||
# Lightning node open info, url to amboss and 1ML
|
||||
NETWORK = 'testnet'
|
||||
NODE_ALIAS = '🤖RoboSats⚡(RoboDevs)'
|
||||
NODE_ID = '033b58d7......'
|
||||
|
||||
# Trade fee in percentage %
|
||||
FEE = 0.002
|
||||
# Bond size in percentage %
|
||||
|
3
.gitignore
vendored
3
.gitignore
vendored
@ -637,7 +637,6 @@ FodyWeavers.xsd
|
||||
|
||||
# Django
|
||||
*migrations*
|
||||
frontend/static/frontend/main*
|
||||
|
||||
# Celery
|
||||
django
|
||||
@ -648,3 +647,5 @@ api/lightning/lightning*
|
||||
api/lightning/invoices*
|
||||
api/lightning/router*
|
||||
api/lightning/googleapis*
|
||||
frontend/static/admin*
|
||||
frontend/static/rest_framework*
|
35
Dockerfile
Normal file
35
Dockerfile
Normal 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"]
|
@ -41,7 +41,7 @@ class LNPaymentAdmin(AdminChangeLinksMixin, admin.ModelAdmin):
|
||||
|
||||
@admin.register(Profile)
|
||||
class UserProfileAdmin(AdminChangeLinksMixin, admin.ModelAdmin):
|
||||
list_display = ('avatar_tag','id','user_link','total_contracts','total_ratings','avg_rating','num_disputes','lost_disputes')
|
||||
list_display = ('avatar_tag','id','user_link','total_contracts','platform_rating','total_ratings','avg_rating','num_disputes','lost_disputes')
|
||||
list_display_links = ('avatar_tag','id')
|
||||
change_links =['user']
|
||||
readonly_fields = ['avatar_tag']
|
||||
|
@ -1,4 +1,4 @@
|
||||
import grpc, os, hashlib, secrets, json
|
||||
import grpc, os, hashlib, secrets
|
||||
from . import lightning_pb2 as lnrpc, lightning_pb2_grpc as lightningstub
|
||||
from . import invoices_pb2 as invoicesrpc, invoices_pb2_grpc as invoicesstub
|
||||
from . import router_pb2 as routerrpc, router_pb2_grpc as routerstub
|
||||
@ -14,8 +14,18 @@ from api.models import LNPayment
|
||||
# Should work with LND (c-lightning in the future if there are features that deserve the work)
|
||||
#######
|
||||
|
||||
CERT = b64decode(config('LND_CERT_BASE64'))
|
||||
MACAROON = b64decode(config('LND_MACAROON_BASE64'))
|
||||
# Read tls.cert from file or .env variable string encoded as base64
|
||||
try:
|
||||
CERT = open(os.path.join(config('LND_DIR'),'tls.cert'), 'rb').read()
|
||||
except:
|
||||
CERT = b64decode(config('LND_CERT_BASE64'))
|
||||
|
||||
# Read macaroon from file or .env variable string encoded as base64
|
||||
try:
|
||||
MACAROON = open(os.path.join(config('LND_DIR'), config('MACAROON_path')), 'rb').read()
|
||||
except:
|
||||
MACAROON = b64decode(config('LND_MACAROON_BASE64'))
|
||||
|
||||
LND_GRPC_HOST = config('LND_GRPC_HOST')
|
||||
|
||||
class LNNode():
|
||||
@ -122,22 +132,11 @@ class LNNode():
|
||||
lnpayment.save()
|
||||
return True
|
||||
|
||||
# @classmethod
|
||||
# def check_until_invoice_locked(cls, payment_hash, expiration):
|
||||
# '''Checks until hold invoice is locked.
|
||||
# When invoice is locked, returns true.
|
||||
# If time expires, return False.'''
|
||||
# # Experimental, might need asyncio. Best if subscribing all invoices and running a background task
|
||||
# # Maybe best to pass LNpayment object and change status live.
|
||||
|
||||
# request = invoicesrpc.SubscribeSingleInvoiceRequest(r_hash=payment_hash)
|
||||
# for invoice in cls.invoicesstub.SubscribeSingleInvoice(request):
|
||||
# print(invoice)
|
||||
# if timezone.now > expiration:
|
||||
# break
|
||||
# if invoice.state == 3: # True if hold invoice is accepted.
|
||||
# return True
|
||||
# return False
|
||||
@classmethod
|
||||
def resetmc(cls):
|
||||
request = routerrpc.ResetMissionControlRequest()
|
||||
response = cls.routerstub.ResetMissionControl(request, metadata=[('macaroon', MACAROON.hex())])
|
||||
return True
|
||||
|
||||
|
||||
@classmethod
|
||||
|
105
api/logics.py
105
api/logics.py
@ -1,6 +1,7 @@
|
||||
from datetime import timedelta
|
||||
from django.utils import timezone
|
||||
from api.lightning.node import LNNode
|
||||
from django.db.models import Q
|
||||
|
||||
from api.models import Order, LNPayment, MarketTick, User, Currency
|
||||
from decouple import config
|
||||
@ -30,7 +31,8 @@ FIAT_EXCHANGE_DURATION = int(config('FIAT_EXCHANGE_DURATION'))
|
||||
|
||||
class Logics():
|
||||
|
||||
def validate_already_maker_or_taker(user):
|
||||
@classmethod
|
||||
def validate_already_maker_or_taker(cls, user):
|
||||
'''Validates if a use is already not part of an active order'''
|
||||
|
||||
active_order_status = [Order.Status.WFB, Order.Status.PUB, Order.Status.TAK,
|
||||
@ -45,6 +47,14 @@ class Logics():
|
||||
queryset = Order.objects.filter(taker=user, status__in=active_order_status)
|
||||
if queryset.exists():
|
||||
return False, {'bad_request':'You are already taker of an active order'}, queryset[0]
|
||||
|
||||
# Edge case when the user is in an order that is failing payment and he is the buyer
|
||||
queryset = Order.objects.filter( Q(maker=user) | Q(taker=user), status=Order.Status.FAI)
|
||||
if queryset.exists():
|
||||
order = queryset[0]
|
||||
if cls.is_buyer(order, user):
|
||||
return False, {'bad_request':'You are still pending a payment from a recent order'}, order
|
||||
|
||||
return True, None, None
|
||||
|
||||
def validate_order_size(order):
|
||||
@ -55,6 +65,14 @@ class Logics():
|
||||
return False, {'bad_request': 'Your order is too small. It is worth '+'{:,}'.format(order.t0_satoshis)+' Sats now, but the limit is '+'{:,}'.format(MIN_TRADE)+ ' Sats'}
|
||||
return True, None
|
||||
|
||||
def user_activity_status(last_seen):
|
||||
if last_seen > (timezone.now() - timedelta(minutes=2)):
|
||||
return 'Active'
|
||||
elif last_seen > (timezone.now() - timedelta(minutes=10)):
|
||||
return 'Seen recently'
|
||||
else:
|
||||
return 'Inactive'
|
||||
|
||||
@classmethod
|
||||
def take(cls, order, user):
|
||||
is_penalized, time_out = cls.is_penalized(user)
|
||||
@ -284,7 +302,7 @@ class Logics():
|
||||
return False, {'bad_request':'Only the buyer of this order can provide a buyer invoice.'}
|
||||
if not order.taker_bond:
|
||||
return False, {'bad_request':'Wait for your order to be taken.'}
|
||||
if not (order.taker_bond.status == order.maker_bond.status == LNPayment.Status.LOCKED):
|
||||
if not (order.taker_bond.status == order.maker_bond.status == LNPayment.Status.LOCKED) and not order.status == Order.Status.FAI:
|
||||
return False, {'bad_request':'You cannot submit a invoice while bonds are not locked.'}
|
||||
|
||||
num_satoshis = cls.payout_amount(order, user)[1]['invoice_amount']
|
||||
@ -329,9 +347,12 @@ class Logics():
|
||||
|
||||
# If the order status is 'Failed Routing'. Retry payment.
|
||||
if order.status == Order.Status.FAI:
|
||||
# Double check the escrow is settled.
|
||||
if LNNode.double_check_htlc_is_settled(order.trade_escrow.payment_hash):
|
||||
follow_send_payment(order.payout)
|
||||
order.status = Order.Status.PAY
|
||||
order.payout.status = LNPayment.Status.FLIGHT
|
||||
order.payout.routing_attempts = 0
|
||||
order.payout.save()
|
||||
order.save()
|
||||
|
||||
order.save()
|
||||
return True, None
|
||||
@ -418,6 +439,7 @@ class Logics():
|
||||
elif order.status in [Order.Status.PUB, Order.Status.TAK, Order.Status.WF2, Order.Status.WFE] and order.maker == user:
|
||||
#Settle the maker bond (Maker loses the bond for canceling an ongoing trade)
|
||||
valid = cls.settle_bond(order.maker_bond)
|
||||
cls.return_bond(order.taker_bond) # returns taker bond
|
||||
if valid:
|
||||
order.status = Order.Status.UCA
|
||||
order.save()
|
||||
@ -508,13 +530,20 @@ class Logics():
|
||||
order.last_satoshis = cls.satoshis_now(order)
|
||||
bond_satoshis = int(order.last_satoshis * BOND_SIZE)
|
||||
|
||||
description = f"RoboSats - Publishing '{str(order)}' - This is a maker bond, it will freeze in your wallet temporarily and automatically return. It will be charged if you cheat or cancel."
|
||||
description = f"RoboSats - Publishing '{str(order)}' - Maker bond - This payment WILL FREEZE IN YOUR WALLET, check on the website if it was successful. It will automatically return unless you cheat or cancel unilaterally."
|
||||
|
||||
# Gen hold Invoice
|
||||
hold_payment = LNNode.gen_hold_invoice(bond_satoshis,
|
||||
description,
|
||||
invoice_expiry=Order.t_to_expire[Order.Status.WFB],
|
||||
cltv_expiry_secs=BOND_EXPIRY*3600)
|
||||
try:
|
||||
hold_payment = LNNode.gen_hold_invoice(bond_satoshis,
|
||||
description,
|
||||
invoice_expiry=Order.t_to_expire[Order.Status.WFB],
|
||||
cltv_expiry_secs=BOND_EXPIRY*3600)
|
||||
except Exception as e:
|
||||
print(str(e))
|
||||
if 'failed to connect to all addresses' in str(e):
|
||||
return False, {'bad_request':'The Lightning Network Daemon (LND) is down. Write in the Telegram group to make sure the staff is aware.'}
|
||||
if 'wallet locked' in str(e):
|
||||
return False, {'bad_request':"This is weird, RoboSats' lightning wallet is locked. Check in the Telegram group, maybe the staff has died."}
|
||||
|
||||
order.maker_bond = LNPayment.objects.create(
|
||||
concept = LNPayment.Concepts.MAKEBOND,
|
||||
@ -589,13 +618,18 @@ class Logics():
|
||||
bond_satoshis = int(order.last_satoshis * BOND_SIZE)
|
||||
pos_text = 'Buying' if cls.is_buyer(order, user) else 'Selling'
|
||||
description = (f"RoboSats - Taking 'Order {order.id}' {pos_text} BTC for {str(float(order.amount)) + Currency.currency_dict[str(order.currency.currency)]}"
|
||||
+ " - This is a taker bond, it will freeze in your wallet temporarily and automatically return. It will be charged if you cheat or cancel.")
|
||||
+ " - Taker bond - This payment WILL FREEZE IN YOUR WALLET, check on the website if it was successful. It will automatically return unless you cheat or cancel unilaterally.")
|
||||
|
||||
# Gen hold Invoice
|
||||
hold_payment = LNNode.gen_hold_invoice(bond_satoshis,
|
||||
description,
|
||||
invoice_expiry=Order.t_to_expire[Order.Status.TAK],
|
||||
cltv_expiry_secs=BOND_EXPIRY*3600)
|
||||
try:
|
||||
hold_payment = LNNode.gen_hold_invoice(bond_satoshis,
|
||||
description,
|
||||
invoice_expiry=Order.t_to_expire[Order.Status.TAK],
|
||||
cltv_expiry_secs=BOND_EXPIRY*3600)
|
||||
|
||||
except Exception as e:
|
||||
if 'status = StatusCode.UNAVAILABLE' in str(e):
|
||||
return False, {'bad_request':'The Lightning Network Daemon (LND) is down. Write in the Telegram group to make sure the staff is aware.'}
|
||||
|
||||
order.taker_bond = LNPayment.objects.create(
|
||||
concept = LNPayment.Concepts.TAKEBOND,
|
||||
@ -654,13 +688,19 @@ class Logics():
|
||||
|
||||
# If there was no taker_bond object yet, generate one
|
||||
escrow_satoshis = order.last_satoshis # Amount was fixed when taker bond was locked
|
||||
description = f"RoboSats - Escrow amount for '{str(order)}' - The escrow will be released to the buyer once you confirm you received the fiat. It will automatically return if buyer does not confirm the payment."
|
||||
description = f"RoboSats - Escrow amount for '{str(order)}' - It WILL FREEZE IN YOUR WALLET. It will be released to the buyer once you confirm you received the fiat. It will automatically return if buyer does not confirm the payment."
|
||||
|
||||
# Gen hold Invoice
|
||||
hold_payment = LNNode.gen_hold_invoice(escrow_satoshis,
|
||||
description,
|
||||
invoice_expiry=Order.t_to_expire[Order.Status.WF2],
|
||||
cltv_expiry_secs=ESCROW_EXPIRY*3600)
|
||||
try:
|
||||
hold_payment = LNNode.gen_hold_invoice(escrow_satoshis,
|
||||
description,
|
||||
invoice_expiry=Order.t_to_expire[Order.Status.WF2],
|
||||
cltv_expiry_secs=ESCROW_EXPIRY*3600)
|
||||
|
||||
except Exception as e:
|
||||
if 'status = StatusCode.UNAVAILABLE' in str(e):
|
||||
return False, {'bad_request':'The Lightning Network Daemon (LND) is down. Write in the Telegram group to make sure the staff is aware.'}
|
||||
|
||||
|
||||
order.trade_escrow = LNPayment.objects.create(
|
||||
concept = LNPayment.Concepts.TRESCROW,
|
||||
@ -776,13 +816,20 @@ class Logics():
|
||||
# RETURN THE BONDS // Probably best also do it even if payment failed
|
||||
cls.return_bond(order.taker_bond)
|
||||
cls.return_bond(order.maker_bond)
|
||||
is_payed, context = follow_send_payment(order.payout) ##### !!! KEY LINE - PAYS THE BUYER INVOICE !!!
|
||||
if is_payed:
|
||||
order.save()
|
||||
return True, context
|
||||
else:
|
||||
# error handling here
|
||||
return False, context
|
||||
##### !!! KEY LINE - PAYS THE BUYER INVOICE !!!
|
||||
##### Backgroun process "follow_invoices" will try to pay this invoice until success
|
||||
order.status = Order.Status.PAY
|
||||
order.payout.status = LNPayment.Status.FLIGHT
|
||||
order.payout.save()
|
||||
order.save()
|
||||
return True, None
|
||||
# is_payed, context = follow_send_payment(order.payout) ##### !!! KEY LINE - PAYS THE BUYER INVOICE !!!
|
||||
# if is_payed:
|
||||
# order.save()
|
||||
# return True, context
|
||||
# else:
|
||||
# # error handling here
|
||||
# return False, context
|
||||
else:
|
||||
return False, {'bad_request':'You cannot confirm the fiat payment at this stage'}
|
||||
|
||||
@ -810,3 +857,9 @@ class Logics():
|
||||
return False, {'bad_request':'You cannot rate your counterparty yet.'}
|
||||
|
||||
return True, None
|
||||
|
||||
@classmethod
|
||||
def rate_platform(cls, user, rating):
|
||||
user.profile.platform_rating = rating
|
||||
user.profile.save()
|
||||
return True, None
|
||||
|
@ -27,11 +27,11 @@ class Command(BaseCommand):
|
||||
|
||||
try:
|
||||
self.follow_hold_invoices()
|
||||
self.retry_payments()
|
||||
except Exception as e:
|
||||
if 'database is locked' in str(e):
|
||||
self.stdout.write('database is locked')
|
||||
|
||||
self.stdout.write(str(e))
|
||||
try:
|
||||
self.send_payments()
|
||||
except Exception as e:
|
||||
self.stdout.write(str(e))
|
||||
|
||||
def follow_hold_invoices(self):
|
||||
@ -117,18 +117,32 @@ class Command(BaseCommand):
|
||||
self.stdout.write(str(timezone.now()))
|
||||
self.stdout.write(str(debug))
|
||||
|
||||
def retry_payments(self):
|
||||
''' Checks if any payment is due for retry, and tries to pay it'''
|
||||
def send_payments(self):
|
||||
'''
|
||||
Checks for invoices that are due to pay; i.e., INFLIGHT status and 0 routing_attempts.
|
||||
Checks if any payment is due for retry, and tries to pay it.
|
||||
'''
|
||||
|
||||
queryset = LNPayment.objects.filter(type=LNPayment.Types.NORM,
|
||||
status=LNPayment.Status.FLIGHT,
|
||||
routing_attempts=0)
|
||||
|
||||
queryset_retries = LNPayment.objects.filter(type=LNPayment.Types.NORM,
|
||||
status__in=[LNPayment.Status.VALIDI, LNPayment.Status.FAILRO],
|
||||
routing_attempts__lt=4,
|
||||
routing_attempts__lt=5,
|
||||
last_routing_time__lt=(timezone.now()-timedelta(minutes=int(config('RETRY_TIME')))))
|
||||
|
||||
queryset = queryset.union(queryset_retries)
|
||||
|
||||
for lnpayment in queryset:
|
||||
success, _ = follow_send_payment(lnpayment)
|
||||
success, _ = follow_send_payment(lnpayment) # Do follow_send_payment.delay() for further concurrency.
|
||||
|
||||
# If failed, reset mision control. (This won't scale well, just a temporary fix)
|
||||
if not success:
|
||||
LNNode.resetmc()
|
||||
|
||||
# If already 3 attempts and last failed. Make it expire (ask for a new invoice) an reset attempts.
|
||||
if not success and lnpayment.routing_attempts == 3:
|
||||
if not success and lnpayment.routing_attempts > 2:
|
||||
lnpayment.status = LNPayment.Status.EXPIRE
|
||||
lnpayment.routing_attempts = 0
|
||||
lnpayment.save()
|
||||
|
@ -6,6 +6,7 @@ from django.template.defaultfilters import truncatechars
|
||||
from django.dispatch import receiver
|
||||
from django.utils.html import mark_safe
|
||||
import uuid
|
||||
from django.conf import settings
|
||||
|
||||
from decouple import config
|
||||
from pathlib import Path
|
||||
@ -169,6 +170,8 @@ class Order(models.Model):
|
||||
# ratings
|
||||
maker_rated = models.BooleanField(default=False, null=False)
|
||||
taker_rated = models.BooleanField(default=False, null=False)
|
||||
maker_platform_rated = models.BooleanField(default=False, null=False)
|
||||
taker_platform_rated = models.BooleanField(default=False, null=False)
|
||||
|
||||
t_to_expire = {
|
||||
0 : int(config('EXP_MAKER_BOND_INVOICE')) , # 'Waiting for maker bond'
|
||||
@ -207,7 +210,6 @@ def delete_lnpayment_at_order_deletion(sender, instance, **kwargs):
|
||||
pass
|
||||
|
||||
class Profile(models.Model):
|
||||
|
||||
user = models.OneToOneField(User,on_delete=models.CASCADE)
|
||||
|
||||
# Total trades
|
||||
@ -225,11 +227,14 @@ class Profile(models.Model):
|
||||
orders_disputes_started = models.CharField(max_length=999, null=True, default=None, validators=[validate_comma_separated_integer_list], blank=True) # Will only store ID of orders
|
||||
|
||||
# RoboHash
|
||||
avatar = models.ImageField(default="static/assets/misc/unknown_avatar.png", verbose_name='Avatar', blank=True)
|
||||
avatar = models.ImageField(default=(settings.STATIC_ROOT+"unknown_avatar.png"), verbose_name='Avatar', blank=True)
|
||||
|
||||
# Penalty expiration (only used then taking/cancelling repeatedly orders in the book before comitting bond)
|
||||
penalty_expiration = models.DateTimeField(null=True,default=None, blank=True)
|
||||
|
||||
# Platform rate
|
||||
platform_rating = models.PositiveIntegerField(null=True, default=None, blank=True)
|
||||
|
||||
@receiver(post_save, sender=User)
|
||||
def create_user_profile(sender, instance, created, **kwargs):
|
||||
if created:
|
||||
@ -242,7 +247,7 @@ class Profile(models.Model):
|
||||
@receiver(pre_delete, sender=User)
|
||||
def del_avatar_from_disk(sender, instance, **kwargs):
|
||||
try:
|
||||
avatar_file=Path('frontend/' + instance.profile.avatar.url)
|
||||
avatar_file=Path(settings.AVATAR_ROOT + instance.profile.avatar.url)
|
||||
avatar_file.unlink()
|
||||
except:
|
||||
pass
|
||||
@ -253,7 +258,7 @@ class Profile(models.Model):
|
||||
# to display avatars in admin panel
|
||||
def get_avatar(self):
|
||||
if not self.avatar:
|
||||
return 'static/assets/misc/unknown_avatar.png'
|
||||
return settings.STATIC_ROOT + 'unknown_avatar.png'
|
||||
return self.avatar.url
|
||||
|
||||
# method to create a fake table field in read only mode
|
||||
|
@ -14,5 +14,5 @@ class MakeOrderSerializer(serializers.ModelSerializer):
|
||||
class UpdateOrderSerializer(serializers.Serializer):
|
||||
invoice = serializers.CharField(max_length=2000, allow_null=True, allow_blank=True, default=None)
|
||||
statement = serializers.CharField(max_length=10000, allow_null=True, allow_blank=True, default=None)
|
||||
action = serializers.ChoiceField(choices=('take','update_invoice','submit_statement','dispute','cancel','confirm','rate'), allow_null=False)
|
||||
action = serializers.ChoiceField(choices=('take','update_invoice','submit_statement','dispute','cancel','confirm','rate_user','rate_platform'), allow_null=False)
|
||||
rating = serializers.ChoiceField(choices=('1','2','3','4','5'), allow_null=True, allow_blank=True, default=None)
|
@ -41,12 +41,10 @@ def follow_send_payment(lnpayment):
|
||||
from django.utils import timezone
|
||||
from datetime import timedelta
|
||||
|
||||
from api.lightning.node import LNNode
|
||||
from api.lightning.node import LNNode, MACAROON
|
||||
from api.models import LNPayment, Order
|
||||
|
||||
MACAROON = b64decode(config('LND_MACAROON_BASE64'))
|
||||
|
||||
fee_limit_sat = max(lnpayment.num_satoshis * 0.0002, 10) # 200 ppm or 10 sats max
|
||||
fee_limit_sat = int(max(lnpayment.num_satoshis * float(config('PROPORTIONAL_ROUTING_FEE_LIMIT')), float(config('MIN_FLAT_ROUTING_FEE_LIMIT')))) # 200 ppm or 10 sats
|
||||
request = LNNode.routerrpc.SendPaymentRequest(
|
||||
payment_request=lnpayment.invoice,
|
||||
fee_limit_sat=fee_limit_sat,
|
||||
@ -77,7 +75,6 @@ def follow_send_payment(lnpayment):
|
||||
order.save()
|
||||
context = {'routing_failed': LNNode.payment_failure_context[response.failure_reason]}
|
||||
print(context)
|
||||
# Call a retry in 5 mins here?
|
||||
return False, context
|
||||
|
||||
if response.status == 2 : # Status 2 'SUCCEEDED'
|
||||
|
25
api/utils.py
25
api/utils.py
@ -50,23 +50,32 @@ def get_exchange_rates(currencies):
|
||||
|
||||
return median_rates.tolist()
|
||||
|
||||
lnd_v_cache = {}
|
||||
@ring.dict(lnd_v_cache, expire=3600) #keeps in cache for 3600 seconds
|
||||
def get_lnd_version():
|
||||
|
||||
stream = os.popen('lnd --version')
|
||||
lnd_version = stream.read()[:-1]
|
||||
# If dockerized, return LND_VERSION envvar used for docker image.
|
||||
# Otherwise it would require LND's version.grpc libraries...
|
||||
try:
|
||||
lnd_version = config('LND_VERSION')
|
||||
return lnd_version
|
||||
except:
|
||||
pass
|
||||
|
||||
return lnd_version
|
||||
# If not dockerized, and LND is local, read from CLI
|
||||
try:
|
||||
stream = os.popen('lnd --version')
|
||||
lnd_version = stream.read()[:-1]
|
||||
return lnd_version
|
||||
except:
|
||||
return ''
|
||||
|
||||
robosats_commit_cache = {}
|
||||
@ring.dict(robosats_commit_cache, expire=3600)
|
||||
def get_commit_robosats():
|
||||
|
||||
stream = os.popen('git log -n 1 --pretty=format:"%H"')
|
||||
lnd_version = stream.read()
|
||||
commit = os.popen('git log -n 1 --pretty=format:"%H"')
|
||||
commit_hash = commit.read()
|
||||
|
||||
return lnd_version
|
||||
return commit_hash
|
||||
|
||||
premium_percentile = {}
|
||||
@ring.dict(premium_percentile, expire=300)
|
||||
|
45
api/views.py
45
api/views.py
@ -1,3 +1,4 @@
|
||||
import os
|
||||
from re import T
|
||||
from django.db.models import query
|
||||
from rest_framework import status, viewsets
|
||||
@ -22,13 +23,15 @@ import hashlib
|
||||
from pathlib import Path
|
||||
from datetime import timedelta, datetime
|
||||
from django.utils import timezone
|
||||
from django.conf import settings
|
||||
from decouple import config
|
||||
|
||||
EXP_MAKER_BOND_INVOICE = int(config('EXP_MAKER_BOND_INVOICE'))
|
||||
FEE = float(config('FEE'))
|
||||
RETRY_TIME = int(config('RETRY_TIME'))
|
||||
|
||||
avatar_path = Path('frontend/static/assets/avatars')
|
||||
|
||||
avatar_path = Path(settings.AVATAR_ROOT)
|
||||
avatar_path.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Create your views here.
|
||||
@ -136,20 +139,9 @@ class OrderView(viewsets.ViewSet):
|
||||
|
||||
# Add activity status of participants based on last_seen
|
||||
if order.taker_last_seen != None:
|
||||
if order.taker_last_seen > (timezone.now() - timedelta(minutes=2)):
|
||||
data['taker_status'] = 'active'
|
||||
elif order.taker_last_seen > (timezone.now() - timedelta(minutes=10)):
|
||||
data['taker_status'] = 'seen_recently'
|
||||
else:
|
||||
data['taker_status'] = 'inactive'
|
||||
|
||||
data['taker_status'] = Logics.user_activity_status(order.taker_last_seen)
|
||||
if order.maker_last_seen != None:
|
||||
if order.maker_last_seen > (timezone.now() - timedelta(minutes=2)):
|
||||
data['maker_status'] = 'active'
|
||||
elif order.maker_last_seen > (timezone.now() - timedelta(minutes=10)):
|
||||
data['maker_status'] = 'seen_recently'
|
||||
else:
|
||||
data['maker_status'] = 'inactive'
|
||||
data['maker_status'] = Logics.user_activity_status(order.maker_last_seen)
|
||||
|
||||
# 3.b If order is between public and WF2
|
||||
if order.status >= Order.Status.PUB and order.status < Order.Status.WF2:
|
||||
@ -157,7 +149,6 @@ class OrderView(viewsets.ViewSet):
|
||||
|
||||
# 3. c) If maker and Public, add num robots in book, premium percentile and num similar orders.
|
||||
if data['is_maker'] and order.status == Order.Status.PUB:
|
||||
data['robots_in_book'] = None # TODO
|
||||
data['premium_percentile'] = compute_premium_percentile(order)
|
||||
data['num_similar_orders'] = len(Order.objects.filter(currency=order.currency, status=Order.Status.PUB))
|
||||
|
||||
@ -287,7 +278,7 @@ class OrderView(viewsets.ViewSet):
|
||||
order = Order.objects.get(id=order_id)
|
||||
|
||||
# action is either 1)'take', 2)'confirm', 3)'cancel', 4)'dispute' , 5)'update_invoice'
|
||||
# 6)'submit_statement' (in dispute), 7)'rate' (counterparty)
|
||||
# 6)'submit_statement' (in dispute), 7)'rate_user' , 'rate_platform'
|
||||
action = serializer.data.get('action')
|
||||
invoice = serializer.data.get('invoice')
|
||||
statement = serializer.data.get('statement')
|
||||
@ -335,10 +326,15 @@ class OrderView(viewsets.ViewSet):
|
||||
if not valid: return Response(context, status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
# 6) If action is rate
|
||||
elif action == 'rate' and rating:
|
||||
elif action == 'rate_user' and rating:
|
||||
valid, context = Logics.rate_counterparty(order,request.user, rating)
|
||||
if not valid: return Response(context, status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
# 6) If action is rate_platform
|
||||
elif action == 'rate_platform' and rating:
|
||||
valid, context = Logics.rate_platform(request.user, rating)
|
||||
if not valid: return Response(context, status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
# If nothing of the above... something else is going on. Probably not allowed!
|
||||
else:
|
||||
return Response(
|
||||
@ -419,7 +415,7 @@ class UserView(APIView):
|
||||
if len(User.objects.filter(username=nickname)) == 0:
|
||||
User.objects.create_user(username=nickname, password=token, is_staff=False)
|
||||
user = authenticate(request, username=nickname, password=token)
|
||||
user.profile.avatar = str(image_path)[9:] # removes frontend/ from url (ugly, to be fixed)
|
||||
user.profile.avatar = nickname + '.png'
|
||||
login(request, user)
|
||||
return Response(context, status=status.HTTP_201_CREATED)
|
||||
|
||||
@ -427,9 +423,9 @@ class UserView(APIView):
|
||||
user = authenticate(request, username=nickname, password=token)
|
||||
if user is not None:
|
||||
login(request, user)
|
||||
# Sends the welcome back message, only if created +30 mins ago
|
||||
if request.user.date_joined < (timezone.now()-timedelta(minutes=30)):
|
||||
context['found'] = 'We found your Robosat. Welcome back!'
|
||||
# Sends the welcome back message, only if created +3 mins ago
|
||||
if request.user.date_joined < (timezone.now()-timedelta(minutes=3)):
|
||||
context['found'] = 'We found your Robot avatar. Welcome back!'
|
||||
return Response(context, status=status.HTTP_202_ACCEPTED)
|
||||
else:
|
||||
# It is unlikely, but maybe the nickname is taken (1 in 20 Billion change)
|
||||
@ -484,7 +480,7 @@ class BookView(ListAPIView):
|
||||
|
||||
# Compute current premium for those orders that are explicitly priced.
|
||||
data['price'], data['premium'] = Logics.price_and_premium_now(order)
|
||||
|
||||
data['maker_status'] = Logics.user_activity_status(order.maker_last_seen)
|
||||
for key in ('status','taker'): # Non participants should not see the status or who is the taker
|
||||
del data[key]
|
||||
|
||||
@ -534,6 +530,11 @@ class InfoView(ListAPIView):
|
||||
context['lifetime_satoshis_settled'] = lifetime_volume_settled
|
||||
context['lnd_version'] = get_lnd_version()
|
||||
context['robosats_running_commit_hash'] = get_commit_robosats()
|
||||
context['alternative_site'] = config('ALTERNATIVE_SITE')
|
||||
context['alternative_name'] = config('ALTERNATIVE_NAME')
|
||||
context['node_alias'] = config('NODE_ALIAS')
|
||||
context['node_id'] = config('NODE_ID')
|
||||
context['network'] = config('NETWORK')
|
||||
context['fee'] = FEE
|
||||
context['bond_size'] = float(config('BOND_SIZE'))
|
||||
if request.user.is_authenticated:
|
||||
|
119
docker-compose.yml
Normal file
119
docker-compose.yml
Normal 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:
|
15
docker/bitcoind/Dockerfile
Normal file
15
docker/bitcoind/Dockerfile
Normal 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"]
|
33
docker/bitcoind/bitcoin.conf
Normal file
33
docker/bitcoind/bitcoin.conf
Normal 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
21
docker/bitcoind/entrypoint.sh
Executable 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
18
docker/lnd/Dockerfile
Normal 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
18
docker/lnd/entrypoint.sh
Executable 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
31
docker/lnd/lnd.conf
Normal 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
21
docker/tor/Dockerfile
Normal 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
18
docker/tor/entrypoint.sh
Executable 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
12
docker/tor/torrc
Normal 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
21
frontend/Dockerfile
Normal 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"]
|
@ -1,8 +1,9 @@
|
||||
import React, { Component } from "react";
|
||||
import { Paper, Button , CircularProgress, ListItemButton, Typography, Grid, Select, MenuItem, FormControl, FormHelperText, List, ListItem, ListItemText, Avatar, RouterLink, ListItemAvatar} from "@mui/material";
|
||||
import { Badge, Tooltip, Paper, Button , CircularProgress, ListItemButton, Typography, Grid, Select, MenuItem, FormControl, FormHelperText, List, ListItem, ListItemText, Avatar, RouterLink, ListItemAvatar, IconButton} from "@mui/material";
|
||||
import { Link } from 'react-router-dom'
|
||||
import { DataGrid } from '@mui/x-data-grid';
|
||||
import MediaQuery from 'react-responsive'
|
||||
import Image from 'material-ui-image'
|
||||
|
||||
import getFlags from './getFlags'
|
||||
|
||||
@ -69,6 +70,13 @@ export default class BookPage extends Component {
|
||||
return x.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",");
|
||||
}
|
||||
|
||||
// Colors for the status badges
|
||||
statusBadgeColor(status){
|
||||
if(status=='Active'){return("success")}
|
||||
if(status=='Seen recently'){return("warning")}
|
||||
if(status=='Inactive'){return('error')}
|
||||
}
|
||||
|
||||
bookListTableDesktop=()=>{
|
||||
return (
|
||||
<div style={{ height: 475, width: '100%' }}>
|
||||
@ -77,8 +85,9 @@ export default class BookPage extends Component {
|
||||
this.state.orders.map((order) =>
|
||||
({id: order.id,
|
||||
avatar: window.location.origin +'/static/assets/avatars/' + order.maker_nick + '.png',
|
||||
robosat: order.maker_nick,
|
||||
type: order.type ? "Sell": "Buy",
|
||||
robot: order.maker_nick,
|
||||
robot_status: order.maker_status,
|
||||
type: order.type ? "Seller": "Buyer",
|
||||
amount: parseFloat(parseFloat(order.amount).toFixed(4)),
|
||||
currency: this.getCurrencyCode(order.currency),
|
||||
payment_method: order.payment_method,
|
||||
@ -89,22 +98,34 @@ export default class BookPage extends Component {
|
||||
|
||||
columns={[
|
||||
// { field: 'id', headerName: 'ID', width: 40 },
|
||||
{ field: 'robosat', headerName: 'RoboSat', width: 240,
|
||||
{ field: 'robot', headerName: 'Robot', width: 240,
|
||||
renderCell: (params) => {return (
|
||||
<ListItemButton style={{ cursor: "pointer" }}>
|
||||
<ListItemAvatar>
|
||||
<Avatar className="flippedSmallAvatar" alt={params.row.robosat} src={params.row.avatar} />
|
||||
<Tooltip placement="right" enterTouchDelay="0" title={params.row.robot_status}>
|
||||
<Badge variant="dot" overlap="circular" badgeContent="" color={this.statusBadgeColor(params.row.robot_status)}>
|
||||
<div style={{ width: 45, height: 45 }}>
|
||||
<Image className='bookAvatar'
|
||||
disableError='true'
|
||||
disableSpinner='true'
|
||||
color='null'
|
||||
alt={params.row.robot}
|
||||
src={params.row.avatar}
|
||||
/>
|
||||
</div>
|
||||
</Badge>
|
||||
</Tooltip>
|
||||
</ListItemAvatar>
|
||||
<ListItemText primary={params.row.robosat}/>
|
||||
<ListItemText primary={params.row.robot}/>
|
||||
</ListItemButton>
|
||||
);
|
||||
} },
|
||||
{ field: 'type', headerName: 'Type', width: 60 },
|
||||
{ field: 'type', headerName: 'Is', width: 60 },
|
||||
{ field: 'amount', headerName: 'Amount', type: 'number', width: 80 },
|
||||
{ field: 'currency', headerName: 'Currency', width: 100,
|
||||
renderCell: (params) => {return (
|
||||
<div style={{ cursor: "pointer" }}>{params.row.currency + " " + getFlags(params.row.currency)}</div>
|
||||
)} },
|
||||
<div style={{ cursor: "pointer" }}>{params.row.currency+" "+getFlags(params.row.currency)}</div>)
|
||||
}},
|
||||
{ field: 'payment_method', headerName: 'Payment Method', width: 180 },
|
||||
{ field: 'price', headerName: 'Price', type: 'number', width: 140,
|
||||
renderCell: (params) => {return (
|
||||
@ -126,14 +147,15 @@ export default class BookPage extends Component {
|
||||
|
||||
bookListTablePhone=()=>{
|
||||
return (
|
||||
<div style={{ height: 425, width: '100%' }}>
|
||||
<div style={{ height: 422, width: '100%' }}>
|
||||
<DataGrid
|
||||
rows={
|
||||
this.state.orders.map((order) =>
|
||||
({id: order.id,
|
||||
avatar: window.location.origin +'/static/assets/avatars/' + order.maker_nick + '.png',
|
||||
robosat: order.maker_nick,
|
||||
type: order.type ? "Sell": "Buy",
|
||||
robot: order.maker_nick,
|
||||
robot_status: order.maker_status,
|
||||
type: order.type ? "Seller": "Buyer",
|
||||
amount: parseFloat(parseFloat(order.amount).toFixed(4)),
|
||||
currency: this.getCurrencyCode(order.currency),
|
||||
payment_method: order.payment_method,
|
||||
@ -144,18 +166,42 @@ export default class BookPage extends Component {
|
||||
|
||||
columns={[
|
||||
// { field: 'id', headerName: 'ID', width: 40 },
|
||||
{ field: 'robosat', headerName: 'Robot', width: 80,
|
||||
{ field: 'robot', headerName: 'Robot', width: 80,
|
||||
renderCell: (params) => {return (
|
||||
<ListItemButton style={{ cursor: "pointer" }}>
|
||||
<Avatar className="flippedSmallAvatar" alt={params.row.robosat} src={params.row.avatar} />
|
||||
</ListItemButton>
|
||||
<Tooltip placement="right" enterTouchDelay="0" title={params.row.robot+" ("+params.row.robot_status+")"}>
|
||||
<Badge variant="dot" overlap="circular" badgeContent="" color={this.statusBadgeColor(params.row.robot_status)}>
|
||||
<div style={{ width: 45, height: 45 }}>
|
||||
<Image className='bookAvatar'
|
||||
disableError='true'
|
||||
disableSpinner='true'
|
||||
color='null'
|
||||
alt={params.row.robot}
|
||||
src={params.row.avatar}
|
||||
/>
|
||||
</div>
|
||||
</Badge>
|
||||
</Tooltip>
|
||||
);
|
||||
} },
|
||||
{ field: 'type', headerName: 'Type', width: 60, hide:'true'},
|
||||
{ field: 'amount', headerName: 'Amount', type: 'number', width: 80 },
|
||||
{ field: 'type', headerName: 'Is', width: 60, hide:'true'},
|
||||
{ field: 'amount', headerName: 'Amount', type: 'number', width: 80,
|
||||
renderCell: (params) => {return (
|
||||
<Tooltip placement="right" enterTouchDelay="0" title={params.row.type}>
|
||||
<div style={{ cursor: "pointer" }}>{this.pn(params.row.amount)}</div>
|
||||
</Tooltip>
|
||||
)} },
|
||||
{ field: 'currency', headerName: 'Currency', width: 100,
|
||||
renderCell: (params) => {return (
|
||||
<div style={{ cursor: "pointer" }}>{params.row.currency + " " + getFlags(params.row.currency)}</div>
|
||||
<Tooltip placement="left" enterTouchDelay="0" title={params.row.payment_method}>
|
||||
<Grid container xs={12} aling="center">
|
||||
<Grid item xs={6} aling="center">
|
||||
<span>{params.row.currency}</span>
|
||||
</Grid>
|
||||
<Grid item xs={6} aling="center">
|
||||
<Typography>{getFlags(params.row.currency)}</Typography>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Tooltip>
|
||||
)} },
|
||||
{ field: 'payment_method', headerName: 'Payment Method', width: 180, hide:'true'},
|
||||
{ field: 'price', headerName: 'Price', type: 'number', width: 140, hide:'true',
|
||||
@ -164,7 +210,9 @@ export default class BookPage extends Component {
|
||||
)} },
|
||||
{ field: 'premium', headerName: 'Premium', type: 'number', width: 85,
|
||||
renderCell: (params) => {return (
|
||||
<Tooltip placement="left" enterTouchDelay="0" title={this.pn(params.row.price) + " " +params.row.currency+ "/BTC" }>
|
||||
<div style={{ cursor: "pointer" }}>{parseFloat(parseFloat(params.row.premium).toFixed(4))+"%" }</div>
|
||||
</Tooltip>
|
||||
)} },
|
||||
]}
|
||||
|
||||
@ -179,11 +227,9 @@ export default class BookPage extends Component {
|
||||
render() {
|
||||
return (
|
||||
<Grid className='orderBook' container spacing={1} sx={{minWidth:400}}>
|
||||
<Grid item xs={12} align="center">
|
||||
<Typography component="h2" variant="h2">
|
||||
Order Book
|
||||
</Typography>
|
||||
</Grid>
|
||||
{/* <Grid item xs={12} align="center">
|
||||
<Typography component="h4" variant="h4">ORDER BOOK</Typography>
|
||||
</Grid> */}
|
||||
|
||||
<Grid item xs={6} align="right">
|
||||
<FormControl >
|
||||
|
@ -1,5 +1,5 @@
|
||||
import React, { Component } from 'react'
|
||||
import {Badge, TextField, ListItemAvatar, Avatar,Paper, Grid, IconButton, Typography, Select, MenuItem, List, ListItemText, ListItem, ListItemIcon, ListItemButton, Divider, Dialog, DialogContent} from "@mui/material";
|
||||
import {Badge, Tooltip, TextField, ListItemAvatar, Avatar,Paper, Grid, IconButton, Typography, Select, MenuItem, List, ListItemText, ListItem, ListItemIcon, ListItemButton, Divider, Dialog, DialogContent} from "@mui/material";
|
||||
import MediaQuery from 'react-responsive'
|
||||
import { Link } from 'react-router-dom'
|
||||
|
||||
@ -19,6 +19,8 @@ import PublicIcon from '@mui/icons-material/Public';
|
||||
import NumbersIcon from '@mui/icons-material/Numbers';
|
||||
import PasswordIcon from '@mui/icons-material/Password';
|
||||
import ContentCopy from "@mui/icons-material/ContentCopy";
|
||||
import DnsIcon from '@mui/icons-material/Dns';
|
||||
import WebIcon from '@mui/icons-material/Web';
|
||||
|
||||
// pretty numbers
|
||||
function pn(x) {
|
||||
@ -42,6 +44,8 @@ export default class BottomBar extends Component {
|
||||
robosats_running_commit_hash: '000000000000000',
|
||||
openProfile: false,
|
||||
profileShown: false,
|
||||
alternative_site: 'robosats...',
|
||||
node_id: '00000000',
|
||||
};
|
||||
this.getInfo();
|
||||
}
|
||||
@ -55,7 +59,7 @@ export default class BottomBar extends Component {
|
||||
fetch('/api/info/')
|
||||
.then((response) => response.json())
|
||||
.then((data) => this.setState(data) &
|
||||
this.props.setAppState({nickname:data.nickname}));
|
||||
this.props.setAppState({nickname:data.nickname, loading:false}));
|
||||
}
|
||||
|
||||
handleClickOpenStatsForNerds = () => {
|
||||
@ -84,11 +88,38 @@ export default class BottomBar extends Component {
|
||||
<ListItemText primary={this.state.lnd_version} secondary="LND version"/>
|
||||
</ListItem>
|
||||
|
||||
<Divider/>
|
||||
<ListItem>
|
||||
<ListItemIcon><DnsIcon/></ListItemIcon>
|
||||
{this.state.network == 'testnet'?
|
||||
<ListItemText secondary={this.state.node_alias}>
|
||||
<a target="_blank" href={"https://1ml.com/testnet/node/"
|
||||
+ this.state.node_id}>{this.state.node_id.slice(0, 12)+"... (1ML)"}
|
||||
</a>
|
||||
</ListItemText>
|
||||
:
|
||||
<ListItemText secondary={this.state.node_alias}>
|
||||
<a target="_blank" href={"https://1ml.com/node/"
|
||||
+ this.state.node_id}>{this.state.node_id.slice(0, 12)+"... (1ML)"}
|
||||
</a>
|
||||
</ListItemText>
|
||||
}
|
||||
</ListItem>
|
||||
|
||||
<Divider/>
|
||||
<ListItem>
|
||||
<ListItemIcon><WebIcon/></ListItemIcon>
|
||||
<ListItemText secondary={this.state.alternative_name}>
|
||||
<a target="_blank" href={"http://"+this.state.alternative_site}>{this.state.alternative_site.slice(0, 12)+"...onion"}
|
||||
</a>
|
||||
</ListItemText>
|
||||
</ListItem>
|
||||
|
||||
<Divider/>
|
||||
<ListItem>
|
||||
<ListItemIcon><GitHubIcon/></ListItemIcon>
|
||||
<ListItemText secondary="Currently running commit hash">
|
||||
<a href={"https://github.com/Reckless-Satoshi/robosats/tree/"
|
||||
<a target="_blank" href={"https://github.com/Reckless-Satoshi/robosats/tree/"
|
||||
+ this.state.robosats_running_commit_hash}>{this.state.robosats_running_commit_hash.slice(0, 12)+"..."}
|
||||
</a>
|
||||
</ListItemText>
|
||||
@ -264,17 +295,25 @@ bottomBarDesktop =()=>{
|
||||
<Grid container xs={12}>
|
||||
|
||||
<Grid item xs={2}>
|
||||
<div style={{display: this.props.avatarLoaded ? '':'none'}}>
|
||||
<ListItemButton onClick={this.handleClickOpenProfile} >
|
||||
<Tooltip open={(this.state.active_order_id > 0 & !this.state.profileShown & this.props.avatarLoaded) ? true: false}
|
||||
title="You have an active order">
|
||||
<ListItemAvatar sx={{ width: 30, height: 30 }} >
|
||||
<Badge badgeContent={(this.state.active_order_id > 0 & !this.state.profileShown) ? "": null} color="primary">
|
||||
<Avatar className='flippedSmallAvatar' sx={{margin: 0, top: -13}}
|
||||
alt={this.props.nickname}
|
||||
imgProps={{
|
||||
onLoad:() => this.props.setAppState({avatarLoaded: true}),
|
||||
}}
|
||||
src={this.props.nickname ? window.location.origin +'/static/assets/avatars/' + this.props.nickname + '.png' : null}
|
||||
/>
|
||||
</Badge>
|
||||
</ListItemAvatar>
|
||||
<ListItemText primary={this.props.nickname}/>
|
||||
</ListItemButton>
|
||||
</Tooltip>
|
||||
<ListItemText primary={this.props.nickname}/>
|
||||
</ListItemButton>
|
||||
</div>
|
||||
</Grid>
|
||||
|
||||
<Grid item xs={2}>
|
||||
@ -354,19 +393,23 @@ bottomBarDesktop =()=>{
|
||||
</Select>
|
||||
</Grid>
|
||||
<Grid item xs={3}>
|
||||
<Tooltip enterTouchDelay="250" title="Show community and support links">
|
||||
<IconButton
|
||||
color="primary"
|
||||
aria-label="Community"
|
||||
onClick={this.handleClickOpenCommunity} >
|
||||
<PeopleIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</Grid>
|
||||
<Grid item xs={3}>
|
||||
<IconButton color="primary"
|
||||
aria-label="Stats for Nerds"
|
||||
onClick={this.handleClickOpenStatsForNerds} >
|
||||
<SettingsIcon />
|
||||
</IconButton>
|
||||
<Tooltip enterTouchDelay="250" title="Show stats for nerds">
|
||||
<IconButton color="primary"
|
||||
aria-label="Stats for Nerds"
|
||||
onClick={this.handleClickOpenStatsForNerds} >
|
||||
<SettingsIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</Grid>
|
||||
|
||||
</Grid>
|
||||
@ -469,46 +512,63 @@ bottomBarPhone =()=>{
|
||||
<Grid container xs={12}>
|
||||
|
||||
<Grid item xs={1.6}>
|
||||
<IconButton onClick={this.handleClickOpenProfile} sx={{margin: 0, top: -13, }} >
|
||||
<Badge badgeContent={(this.state.active_order_id >0 & !this.state.profileShown) ? "": null} color="primary">
|
||||
<Avatar className='flippedSmallAvatar'
|
||||
alt={this.props.nickname}
|
||||
src={this.props.nickname ? window.location.origin +'/static/assets/avatars/' + this.props.nickname + '.png' : null}
|
||||
/>
|
||||
</Badge>
|
||||
</IconButton>
|
||||
<div style={{display: this.props.avatarLoaded ? '':'none'}}>
|
||||
<Tooltip open={(this.state.active_order_id > 0 & !this.state.profileShown & this.props.avatarLoaded) ? true: false}
|
||||
title="You have an active order">
|
||||
<IconButton onClick={this.handleClickOpenProfile} sx={{margin: 0, bottom: 17, right: 8}} >
|
||||
<Badge badgeContent={(this.state.active_order_id >0 & !this.state.profileShown) ? "": null} color="primary">
|
||||
<Avatar className='phoneFlippedSmallAvatar'
|
||||
sx={{ width: 55, height:55 }}
|
||||
alt={this.props.nickname}
|
||||
imgProps={{
|
||||
onLoad:() => this.props.setAppState({avatarLoaded: true}),
|
||||
}}
|
||||
src={this.props.nickname ? window.location.origin +'/static/assets/avatars/' + this.props.nickname + '.png' : null}
|
||||
/>
|
||||
</Badge>
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</Grid>
|
||||
|
||||
<Grid item xs={1.6} align="center">
|
||||
<IconButton onClick={this.handleClickOpenExchangeSummary} >
|
||||
<Badge badgeContent={this.state.num_public_buy_orders} color="action">
|
||||
<InventoryIcon />
|
||||
</Badge>
|
||||
</IconButton>
|
||||
<Tooltip enterTouchDelay="300" title="Number of public BUY orders">
|
||||
<IconButton onClick={this.handleClickOpenExchangeSummary} >
|
||||
<Badge badgeContent={this.state.num_public_buy_orders} color="action">
|
||||
<InventoryIcon />
|
||||
</Badge>
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</Grid>
|
||||
|
||||
<Grid item xs={1.6} align="center">
|
||||
<IconButton onClick={this.handleClickOpenExchangeSummary} >
|
||||
<Badge badgeContent={this.state.num_public_sell_orders} color="action">
|
||||
<SellIcon />
|
||||
</Badge>
|
||||
</IconButton>
|
||||
<Tooltip enterTouchDelay="300" title="Number of public SELL orders">
|
||||
<IconButton onClick={this.handleClickOpenExchangeSummary} >
|
||||
<Badge badgeContent={this.state.num_public_sell_orders} color="action">
|
||||
<SellIcon />
|
||||
</Badge>
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</Grid>
|
||||
|
||||
<Grid item xs={1.6} align="center">
|
||||
<IconButton onClick={this.handleClickOpenExchangeSummary} >
|
||||
<Badge badgeContent={this.state.active_robots_today} color="action">
|
||||
<SmartToyIcon />
|
||||
</Badge>
|
||||
</IconButton>
|
||||
<Tooltip enterTouchDelay="300" title="Today active robots">
|
||||
<IconButton onClick={this.handleClickOpenExchangeSummary} >
|
||||
<Badge badgeContent={this.state.active_robots_today} color="action">
|
||||
<SmartToyIcon />
|
||||
</Badge>
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</Grid>
|
||||
|
||||
<Grid item xs={1.8} align="center">
|
||||
<IconButton onClick={this.handleClickOpenExchangeSummary} >
|
||||
<Badge badgeContent={this.state.today_avg_nonkyc_btc_premium+"%"} color="action">
|
||||
<PriceChangeIcon />
|
||||
</Badge>
|
||||
</IconButton>
|
||||
<Tooltip enterTouchDelay="300" title="Today non-KYC bitcoin premium">
|
||||
<IconButton onClick={this.handleClickOpenExchangeSummary} >
|
||||
<Badge badgeContent={this.state.today_avg_nonkyc_btc_premium+"%"} color="action">
|
||||
<PriceChangeIcon />
|
||||
</Badge>
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</Grid>
|
||||
|
||||
<Grid container item xs={3.8}>
|
||||
@ -523,19 +583,23 @@ bottomBarPhone =()=>{
|
||||
</Select>
|
||||
</Grid>
|
||||
<Grid item xs={3}>
|
||||
<IconButton color="primary"
|
||||
aria-label="Stats for Nerds"
|
||||
onClick={this.handleClickOpenStatsForNerds} >
|
||||
<SettingsIcon />
|
||||
</IconButton>
|
||||
</Grid>
|
||||
<Grid item xs={3}>
|
||||
<Tooltip enterTouchDelay="250" title="Show community and support links">
|
||||
<IconButton
|
||||
color="primary"
|
||||
aria-label="Community"
|
||||
onClick={this.handleClickOpenCommunity} >
|
||||
<PeopleIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</Grid>
|
||||
<Grid item xs={3}>
|
||||
<Tooltip enterTouchDelay="250" title="Show stats for nerds">
|
||||
<IconButton color="primary"
|
||||
aria-label="Stats for Nerds"
|
||||
onClick={this.handleClickOpenStatsForNerds} >
|
||||
<SettingsIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</Grid>
|
||||
|
||||
</Grid>
|
||||
|
@ -13,6 +13,7 @@ export default class HomePage extends Component {
|
||||
this.state = {
|
||||
nickname: null,
|
||||
token: null,
|
||||
avatarLoaded: false,
|
||||
}
|
||||
}
|
||||
|
||||
@ -29,7 +30,7 @@ export default class HomePage extends Component {
|
||||
<Router >
|
||||
<div className='appCenter'>
|
||||
<Switch>
|
||||
<Route exact path='/' render={(props) => <UserGenPage setAppState={this.setAppState}/>}/>
|
||||
<Route exact path='/' render={(props) => <UserGenPage {...this.state} setAppState={this.setAppState}/>}/>
|
||||
<Route path='/make' component={MakerPage}/>
|
||||
<Route path='/book' component={BookPage}/>
|
||||
<Route path="/order/:orderId" component={OrderPage}/>
|
||||
|
@ -1,5 +1,5 @@
|
||||
import React, { Component } from 'react';
|
||||
import { Paper, Button , Grid, Typography, TextField, Select, FormHelperText, MenuItem, FormControl, Radio, FormControlLabel, RadioGroup} from "@mui/material"
|
||||
import { Tooltip, Paper, Button , Grid, Typography, TextField, Select, FormHelperText, MenuItem, FormControl, Radio, FormControlLabel, RadioGroup, dividerClasses} from "@mui/material"
|
||||
import { Link } from 'react-router-dom'
|
||||
import getFlags from './getFlags'
|
||||
|
||||
@ -22,6 +22,9 @@ const csrftoken = getCookie('csrftoken');
|
||||
|
||||
// pretty numbers
|
||||
function pn(x) {
|
||||
if(x==null){
|
||||
return(null)
|
||||
}
|
||||
return x.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",");
|
||||
}
|
||||
|
||||
@ -67,37 +70,48 @@ export default class MakerPage extends Component {
|
||||
handlePaymentMethodChange=(e)=>{
|
||||
this.setState({
|
||||
payment_method: e.target.value,
|
||||
badPaymentMethod: e.target.value.length > 35,
|
||||
});
|
||||
}
|
||||
handlePremiumChange=(e)=>{
|
||||
if(e.target.value > 999){
|
||||
var bad_premium = "Must be less than 999%"
|
||||
}
|
||||
if(e.target.value < -100){
|
||||
var bad_premium = "Must be more than -100%"
|
||||
}
|
||||
|
||||
this.setState({
|
||||
premium: e.target.value,
|
||||
badPremium: bad_premium,
|
||||
});
|
||||
}
|
||||
|
||||
handleSatoshisChange=(e)=>{
|
||||
var bad_sats = e.target.value > this.maxTradeSats ?
|
||||
("Must be less than "+pn(this.maxTradeSats)):
|
||||
(e.target.value < this.minTradeSats ?
|
||||
("Must be more than "+pn(this.minTradeSats)): null)
|
||||
if(e.target.value > this.maxTradeSats){
|
||||
var bad_sats = "Must be less than " + pn(this.maxTradeSats)
|
||||
}
|
||||
if(e.target.value < this.minTradeSats){
|
||||
var bad_sats = "Must be more than "+pn(this.minTradeSats)
|
||||
}
|
||||
|
||||
this.setState({
|
||||
satoshis: e.target.value,
|
||||
badSatoshis: bad_sats,
|
||||
})
|
||||
;
|
||||
});
|
||||
}
|
||||
handleClickRelative=(e)=>{
|
||||
this.setState({
|
||||
is_explicit: false,
|
||||
satoshis: null,
|
||||
premium: 0,
|
||||
});
|
||||
this.handlePremiumChange();
|
||||
}
|
||||
|
||||
handleClickExplicit=(e)=>{
|
||||
this.setState({
|
||||
is_explicit: true,
|
||||
premium: null,
|
||||
});
|
||||
this.handleSatoshisChange();
|
||||
}
|
||||
|
||||
handleCreateOfferButtonPressed=()=>{
|
||||
@ -112,8 +126,8 @@ export default class MakerPage extends Component {
|
||||
amount: this.state.amount,
|
||||
payment_method: this.state.payment_method,
|
||||
is_explicit: this.state.is_explicit,
|
||||
premium: this.state.premium,
|
||||
satoshis: this.state.satoshis,
|
||||
premium: this.state.is_explicit ? null: this.state.premium,
|
||||
satoshis: this.state.is_explicit ? this.state.satoshis: null,
|
||||
}),
|
||||
};
|
||||
fetch("/api/make/",requestOptions)
|
||||
@ -136,16 +150,17 @@ export default class MakerPage extends Component {
|
||||
return this.state.currencies_dict[val.toString()]
|
||||
}
|
||||
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Grid container xs={12} align="center" spacing={1}>
|
||||
<Grid item xs={12} align="center">
|
||||
<Typography component="h2" variant="h2">
|
||||
Order Maker
|
||||
<Grid container xs={12} align="center" spacing={1} sx={{minWidth:380}}>
|
||||
{/* <Grid item xs={12} align="center" sx={{minWidth:380}}>
|
||||
<Typography component="h4" variant="h4">
|
||||
ORDER MAKER
|
||||
</Typography>
|
||||
</Grid>
|
||||
</Grid> */}
|
||||
<Grid item xs={12} align="center" spacing={1}>
|
||||
<Paper elevation={12} style={{ padding: 8, width:350, align:'center'}}>
|
||||
<Paper elevation={12} style={{ padding: 8, width:240, align:'center'}}>
|
||||
<Grid item xs={12} align="center" spacing={1}>
|
||||
<FormControl component="fieldset">
|
||||
<FormHelperText>
|
||||
@ -167,10 +182,12 @@ export default class MakerPage extends Component {
|
||||
</RadioGroup>
|
||||
</FormControl>
|
||||
</Grid>
|
||||
<Grid containter xs={8} alignItems="stretch" style={{ display: "flex" }}>
|
||||
<Grid containter xs={12} alignItems="stretch" style={{ display: "flex" }}>
|
||||
<div style={{maxWidth:140}}>
|
||||
<Tooltip placement="top" enterTouchDelay="500" enterDelay="700" enterNextDelay="2000" title="Amount of fiat to exchange for bitcoin">
|
||||
<TextField
|
||||
error={this.state.amount == 0}
|
||||
helperText={this.state.amount == 0 ? 'Must be more than 0' : null}
|
||||
error={this.state.amount <= 0}
|
||||
helperText={this.state.amount <= 0 ? 'Invalid' : null}
|
||||
label="Amount"
|
||||
type="number"
|
||||
required="true"
|
||||
@ -180,28 +197,32 @@ export default class MakerPage extends Component {
|
||||
}}
|
||||
onChange={this.handleAmountChange}
|
||||
/>
|
||||
<Select
|
||||
required="true"
|
||||
defaultValue={this.defaultCurrency}
|
||||
inputProps={{
|
||||
style: {textAlign:"center"}
|
||||
}}
|
||||
onChange={this.handleCurrencyChange}
|
||||
>
|
||||
{
|
||||
Object.entries(this.state.currencies_dict)
|
||||
.map( ([key, value]) => <MenuItem value={parseInt(key)}>
|
||||
{getFlags(value) + " " + value}
|
||||
</MenuItem> )
|
||||
}
|
||||
</Select>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<div >
|
||||
<Select
|
||||
required="true"
|
||||
defaultValue={this.defaultCurrency}
|
||||
inputProps={{
|
||||
style: {textAlign:"center"}
|
||||
}}
|
||||
onChange={this.handleCurrencyChange}>
|
||||
{Object.entries(this.state.currencies_dict)
|
||||
.map( ([key, value]) => <MenuItem value={parseInt(key)}>
|
||||
{getFlags(value) + " " + value}
|
||||
</MenuItem> )}
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
</Grid>
|
||||
<br/>
|
||||
<Grid item xs={12} align="center">
|
||||
<FormControl >
|
||||
<Tooltip placement="top" enterTouchDelay="500" enterDelay="700" enterNextDelay="2000" title="Enter your prefered payment methods">
|
||||
<TextField
|
||||
sx={{width:240}}
|
||||
label="Payment Method(s)"
|
||||
error={this.state.badPaymentMethod}
|
||||
helperText={this.state.badPaymentMethod ? "Must be shorter than 35 characters":""}
|
||||
type="text"
|
||||
require={true}
|
||||
inputProps={{
|
||||
@ -210,7 +231,7 @@ export default class MakerPage extends Component {
|
||||
}}
|
||||
onChange={this.handlePaymentMethodChange}
|
||||
/>
|
||||
</FormControl>
|
||||
</Tooltip>
|
||||
</Grid>
|
||||
|
||||
<Grid item xs={12} align="center">
|
||||
@ -221,6 +242,7 @@ export default class MakerPage extends Component {
|
||||
</div>
|
||||
</FormHelperText>
|
||||
<RadioGroup row defaultValue="relative">
|
||||
<Tooltip placement="top" enterTouchDelay="0" enterDelay="1000" enterNextDelay="2000" title="Let the price move with the market">
|
||||
<FormControlLabel
|
||||
value="relative"
|
||||
control={<Radio color="primary"/>}
|
||||
@ -228,6 +250,8 @@ export default class MakerPage extends Component {
|
||||
labelPlacement="Top"
|
||||
onClick={this.handleClickRelative}
|
||||
/>
|
||||
</Tooltip>
|
||||
<Tooltip placement="top" enterTouchDelay="0" enterDelay="1000" enterNextDelay="2000" title="Set a fix amount of satoshis">
|
||||
<FormControlLabel
|
||||
value="explicit"
|
||||
control={<Radio color="secondary"/>}
|
||||
@ -235,46 +259,64 @@ export default class MakerPage extends Component {
|
||||
labelPlacement="Top"
|
||||
onClick={this.handleClickExplicit}
|
||||
/>
|
||||
</Tooltip>
|
||||
</RadioGroup>
|
||||
</FormControl>
|
||||
</Grid>
|
||||
{/* conditional shows either Premium % field or Satoshis field based on pricing method */}
|
||||
{ this.state.is_explicit
|
||||
? <Grid item xs={12} align="center">
|
||||
<Grid item xs={12} align="center">
|
||||
<div style={{display: this.state.is_explicit ? '':'none'}}>
|
||||
<TextField
|
||||
sx={{width:240}}
|
||||
label="Satoshis"
|
||||
error={this.state.badSatoshis}
|
||||
helperText={this.state.badSatoshis}
|
||||
type="number"
|
||||
required="true"
|
||||
value={this.state.satoshis}
|
||||
inputProps={{
|
||||
// TODO read these from .env file
|
||||
min:this.minTradeSats ,
|
||||
max:this.maxTradeSats ,
|
||||
style: {textAlign:"center"}
|
||||
}}
|
||||
onChange={this.handleSatoshisChange}
|
||||
// defaultValue={this.defaultSatoshis}
|
||||
/>
|
||||
</div>
|
||||
<div style={{display: this.state.is_explicit ? 'none':''}}>
|
||||
<TextField
|
||||
label="Satoshis"
|
||||
error={this.state.badSatoshis}
|
||||
helperText={this.state.badSatoshis}
|
||||
type="number"
|
||||
required="true"
|
||||
inputProps={{
|
||||
// TODO read these from .env file
|
||||
min:this.minTradeSats ,
|
||||
max:this.maxTradeSats ,
|
||||
style: {textAlign:"center"}
|
||||
}}
|
||||
onChange={this.handleSatoshisChange}
|
||||
// defaultValue={this.defaultSatoshis}
|
||||
/>
|
||||
</Grid>
|
||||
: <Grid item xs={12} align="center">
|
||||
<TextField
|
||||
sx={{width:240}}
|
||||
error={this.state.badPremium}
|
||||
helperText={this.state.badPremium}
|
||||
label="Premium over Market (%)"
|
||||
type="number"
|
||||
// defaultValue={this.defaultPremium}
|
||||
inputProps={{
|
||||
min: -100,
|
||||
max: 999,
|
||||
style: {textAlign:"center"}
|
||||
}}
|
||||
onChange={this.handlePremiumChange}
|
||||
/>
|
||||
</Grid>
|
||||
}
|
||||
</div>
|
||||
</Grid>
|
||||
</Paper>
|
||||
</Grid>
|
||||
<Grid item xs={12} align="center">
|
||||
<Button color="primary" variant="contained" onClick={this.handleCreateOfferButtonPressed} >
|
||||
Create Order
|
||||
</Button>
|
||||
{/* conditions to disable the make button */}
|
||||
{(this.state.amount == null ||
|
||||
this.state.amount <= 0 ||
|
||||
(this.state.is_explicit & (this.state.badSatoshis != null || this.state.satoshis == null)) ||
|
||||
(!this.state.is_explicit & this.state.badPremium != null))
|
||||
?
|
||||
<Tooltip enterTouchDelay="0" title="You must fill the form correctly">
|
||||
<div><Button disabled color="primary" variant="contained" onClick={this.handleCreateOfferButtonPressed} >Create Order</Button></div>
|
||||
</Tooltip>
|
||||
:
|
||||
<Button color="primary" variant="contained" onClick={this.handleCreateOfferButtonPressed} >Create Order</Button>
|
||||
}
|
||||
|
||||
</Grid>
|
||||
<Grid item xs={12} align="center">
|
||||
{this.state.badRequest ?
|
||||
@ -284,8 +326,8 @@ export default class MakerPage extends Component {
|
||||
: ""}
|
||||
<Typography component="subtitle2" variant="subtitle2">
|
||||
<div align='center'>
|
||||
Create a BTC {this.state.type==0 ? "buy":"sell"} order for {this.state.amount} {this.state.currencyCode}
|
||||
{this.state.is_explicit ? " of " + this.state.satoshis + " Satoshis" :
|
||||
Create a BTC {this.state.type==0 ? "buy":"sell"} order for {pn(this.state.amount)} {this.state.currencyCode}
|
||||
{this.state.is_explicit ? " of " + pn(this.state.satoshis) + " Satoshis" :
|
||||
(this.state.premium == 0 ? " at market price" :
|
||||
(this.state.premium > 0 ? " at a " + this.state.premium + "% premium":" at a " + -this.state.premium + "% discount")
|
||||
)
|
||||
|
@ -1,5 +1,5 @@
|
||||
import React, { Component } from "react";
|
||||
import { Badge, Tab, Tabs, Alert, Paper, CircularProgress, Button , Grid, Typography, List, ListItem, ListItemIcon, ListItemText, ListItemAvatar, Avatar, Divider, Box, LinearProgress, Dialog, DialogActions, DialogContent, DialogContentText, DialogTitle} from "@mui/material"
|
||||
import { Chip, Tooltip, Badge, Tab, Tabs, Alert, Paper, CircularProgress, Button , Grid, Typography, List, ListItem, ListItemIcon, ListItemText, ListItemAvatar, Avatar, Divider, Box, LinearProgress, Dialog, DialogActions, DialogContent, DialogContentText, DialogTitle} from "@mui/material"
|
||||
import Countdown, { zeroPad, calcTimeDelta } from 'react-countdown';
|
||||
import MediaQuery from 'react-responsive'
|
||||
|
||||
@ -46,6 +46,7 @@ export default class OrderPage extends Component {
|
||||
loading: true,
|
||||
openCancel: false,
|
||||
openCollaborativeCancel: false,
|
||||
openInactiveMaker: false,
|
||||
showContractBox: 1,
|
||||
};
|
||||
this.orderId = this.props.match.params.orderId;
|
||||
@ -54,29 +55,29 @@ export default class OrderPage extends Component {
|
||||
|
||||
// Refresh delays according to Order status
|
||||
this.statusToDelay = {
|
||||
"0": 2000, //'Waiting for maker bond'
|
||||
"1": 25000, //'Public'
|
||||
"2": 9999999, //'Deleted'
|
||||
"3": 2000, //'Waiting for taker bond'
|
||||
"4": 9999999, //'Cancelled'
|
||||
"5": 999999, //'Expired'
|
||||
"6": 3000, //'Waiting for trade collateral and buyer invoice'
|
||||
"7": 3000, //'Waiting only for seller trade collateral'
|
||||
"0": 2000, //'Waiting for maker bond'
|
||||
"1": 25000, //'Public'
|
||||
"2": 999999, //'Deleted'
|
||||
"3": 2000, //'Waiting for taker bond'
|
||||
"4": 999999, //'Cancelled'
|
||||
"5": 999999, //'Expired'
|
||||
"6": 3000, //'Waiting for trade collateral and buyer invoice'
|
||||
"7": 3000, //'Waiting only for seller trade collateral'
|
||||
"8": 8000, //'Waiting only for buyer invoice'
|
||||
"9": 10000, //'Sending fiat - In chatroom'
|
||||
"10": 10000, //'Fiat sent - In chatroom'
|
||||
"11": 30000, //'In dispute'
|
||||
"12": 9999999, //'Collaboratively cancelled'
|
||||
"13": 3000, //'Sending satoshis to buyer'
|
||||
"14": 9999999, //'Sucessful trade'
|
||||
"15": 10000, //'Failed lightning network routing'
|
||||
"16": 9999999, //'Maker lost dispute'
|
||||
"17": 9999999, //'Taker lost dispute'
|
||||
"9": 10000, //'Sending fiat - In chatroom'
|
||||
"10": 10000, //'Fiat sent - In chatroom'
|
||||
"11": 30000, //'In dispute'
|
||||
"12": 999999, //'Collaboratively cancelled'
|
||||
"13": 3000, //'Sending satoshis to buyer'
|
||||
"14": 999999, //'Sucessful trade'
|
||||
"15": 10000, //'Failed lightning network routing'
|
||||
"16": 180000, //'Wait for dispute resolution'
|
||||
"17": 180000, //'Maker lost dispute'
|
||||
"18": 180000, //'Taker lost dispute'
|
||||
}
|
||||
}
|
||||
|
||||
completeSetState=(newStateVars)=>{
|
||||
|
||||
// In case the reply only has "bad_request"
|
||||
// Do not substitute these two for "undefined" as
|
||||
// otherStateVars will fail to assign values
|
||||
@ -149,11 +150,41 @@ export default class OrderPage extends Component {
|
||||
|
||||
} else {
|
||||
return (
|
||||
<span> Wait {zeroPad(minutes)}m {zeroPad(seconds)}s </span>
|
||||
<span> You cannot take an order yet! Wait {zeroPad(minutes)}m {zeroPad(seconds)}s </span>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
countdownTakeOrderRenderer = ({ seconds, completed }) => {
|
||||
if(isNaN(seconds)){
|
||||
return (
|
||||
<>
|
||||
<this.InactiveMakerDialog/>
|
||||
<Button variant='contained' color='primary'
|
||||
onClick={this.state.maker_status=='Inactive' ? this.handleClickOpenInactiveMakerDialog : this.takeOrder}>
|
||||
Take Order
|
||||
</Button>
|
||||
</>)
|
||||
}
|
||||
if (completed) {
|
||||
// Render a completed state
|
||||
return (
|
||||
<>
|
||||
<this.InactiveMakerDialog/>
|
||||
<Button variant='contained' color='primary'
|
||||
onClick={this.state.maker_status=='Inactive' ? this.handleClickOpenInactiveMakerDialog : this.takeOrder}>
|
||||
Take Order
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
} else{
|
||||
return(
|
||||
<Tooltip enterTouchDelay="0" title="Wait until you can take an order"><div>
|
||||
<Button disabled={true} variant='contained' color='primary' onClick={this.takeOrder}>Take Order</Button>
|
||||
</div></Tooltip>)
|
||||
}
|
||||
};
|
||||
|
||||
LinearDeterminate =()=> {
|
||||
const [progress, setProgress] = React.useState(0);
|
||||
|
||||
@ -177,18 +208,21 @@ export default class OrderPage extends Component {
|
||||
);
|
||||
}
|
||||
|
||||
handleClickTakeOrderButton=()=>{
|
||||
const requestOptions = {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type':'application/json', 'X-CSRFToken': getCookie('csrftoken'),},
|
||||
body: JSON.stringify({
|
||||
'action':'take',
|
||||
}),
|
||||
takeOrder=()=>{
|
||||
this.setState({loading:true})
|
||||
|
||||
const requestOptions = {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type':'application/json', 'X-CSRFToken': getCookie('csrftoken'),},
|
||||
body: JSON.stringify({
|
||||
'action':'take',
|
||||
}),
|
||||
};
|
||||
fetch('/api/order/' + '?order_id=' + this.orderId, requestOptions)
|
||||
.then((response) => response.json())
|
||||
.then((data) => this.completeSetState(data));
|
||||
}
|
||||
|
||||
getCurrencyDict() {
|
||||
fetch('/static/assets/currencies.json')
|
||||
.then((response) => response.json())
|
||||
@ -209,16 +243,17 @@ export default class OrderPage extends Component {
|
||||
}
|
||||
|
||||
handleClickConfirmCancelButton=()=>{
|
||||
const requestOptions = {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type':'application/json', 'X-CSRFToken': getCookie('csrftoken'),},
|
||||
body: JSON.stringify({
|
||||
'action':'cancel',
|
||||
}),
|
||||
};
|
||||
fetch('/api/order/' + '?order_id=' + this.orderId, requestOptions)
|
||||
.then((response) => response.json())
|
||||
.then((data) => this.getOrderDetails(data.id));
|
||||
this.setState({loading:true})
|
||||
const requestOptions = {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type':'application/json', 'X-CSRFToken': getCookie('csrftoken'),},
|
||||
body: JSON.stringify({
|
||||
'action':'cancel',
|
||||
}),
|
||||
};
|
||||
fetch('/api/order/' + '?order_id=' + this.orderId, requestOptions)
|
||||
.then((response) => response.json())
|
||||
.then((data) => this.getOrderDetails(data.id));
|
||||
this.handleClickCloseConfirmCancelDialog();
|
||||
}
|
||||
|
||||
@ -253,6 +288,36 @@ export default class OrderPage extends Component {
|
||||
)
|
||||
}
|
||||
|
||||
handleClickOpenInactiveMakerDialog = () => {
|
||||
this.setState({openInactiveMaker: true});
|
||||
};
|
||||
handleClickCloseInactiveMakerDialog = () => {
|
||||
this.setState({openInactiveMaker: false});
|
||||
};
|
||||
|
||||
InactiveMakerDialog =() =>{
|
||||
return(
|
||||
<Dialog
|
||||
open={this.state.openInactiveMaker}
|
||||
onClose={this.handleClickCloseInactiveMakerDialog}
|
||||
aria-labelledby="inactive-maker-dialog-title"
|
||||
aria-describedby="inactive-maker-description"
|
||||
>
|
||||
<DialogTitle id="inactive-maker-dialog-title">
|
||||
{"The maker is away"}
|
||||
</DialogTitle>
|
||||
<DialogContent>
|
||||
<DialogContentText id="cancel-dialog-description">
|
||||
By taking this order you risk wasting your time.
|
||||
</DialogContentText>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={this.handleClickCloseInactiveMakerDialog} autoFocus>Go back</Button>
|
||||
<Button onClick={this.takeOrder}> Take Order </Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
handleClickConfirmCollaborativeCancelButton=()=>{
|
||||
const requestOptions = {
|
||||
method: 'POST',
|
||||
@ -337,68 +402,67 @@ export default class OrderPage extends Component {
|
||||
|
||||
// Colors for the status badges
|
||||
statusBadgeColor(status){
|
||||
if(status=='active'){
|
||||
return("success")
|
||||
}
|
||||
if(status=='seen_recently'){
|
||||
return("warning")
|
||||
}
|
||||
if(status=='inactive'){
|
||||
return('error')
|
||||
}
|
||||
if(status=='Active'){return("success")}
|
||||
if(status=='Seen recently'){return("warning")}
|
||||
if(status=='Inactive'){return('error')}
|
||||
}
|
||||
|
||||
orderBox=()=>{
|
||||
return(
|
||||
<Grid container spacing={1} >
|
||||
<Grid item xs={12} align="center">
|
||||
<MediaQuery minWidth={920}>
|
||||
<Typography component="h5" variant="h5">
|
||||
Order Details
|
||||
Order Box
|
||||
</Typography>
|
||||
</MediaQuery>
|
||||
<Paper elevation={12} style={{ padding: 8,}}>
|
||||
<List dense="true">
|
||||
<ListItem >
|
||||
<ListItemAvatar sx={{ width: 56, height: 56 }}>
|
||||
<Badge variant="dot" badgeContent="" color={this.statusBadgeColor(this.state.maker_status)}>
|
||||
<Tooltip placement="top" enterTouchDelay="0" title={this.state.maker_status} >
|
||||
<Badge variant="dot" overlap="circular" badgeContent="" color={this.statusBadgeColor(this.state.maker_status)}>
|
||||
<Avatar className="flippedSmallAvatar"
|
||||
alt={this.state.maker_nick}
|
||||
src={window.location.origin +'/static/assets/avatars/' + this.state.maker_nick + '.png'}
|
||||
/>
|
||||
</Badge>
|
||||
</Tooltip>
|
||||
</ListItemAvatar>
|
||||
<ListItemText primary={this.state.maker_nick + (this.state.type ? " (Seller)" : " (Buyer)")} secondary="Order maker" align="right"/>
|
||||
</ListItem>
|
||||
<Divider />
|
||||
|
||||
{this.state.is_participant ?
|
||||
<>
|
||||
{this.state.taker_nick!='None' ?
|
||||
<>
|
||||
<Divider />
|
||||
<ListItem align="left">
|
||||
<ListItemText primary={this.state.taker_nick + (this.state.type ? " (Buyer)" : " (Seller)")} secondary="Order taker"/>
|
||||
<ListItemAvatar >
|
||||
<Badge variant="dot" badgeContent="" color={this.statusBadgeColor(this.state.taker_status)}>
|
||||
<Tooltip enterTouchDelay="0" title={this.state.taker_status} >
|
||||
<Badge variant="dot" overlap="circular" badgeContent="" color={this.statusBadgeColor(this.state.taker_status)}>
|
||||
<Avatar className="smallAvatar"
|
||||
alt={this.state.taker_nick}
|
||||
src={window.location.origin +'/static/assets/avatars/' + this.state.taker_nick + '.png'}
|
||||
/>
|
||||
</Badge>
|
||||
</Tooltip>
|
||||
</ListItemAvatar>
|
||||
</ListItem>
|
||||
<Divider />
|
||||
</>:
|
||||
""
|
||||
}
|
||||
<Divider><Chip label='Order Details'/></Divider>
|
||||
<ListItem>
|
||||
<ListItemIcon>
|
||||
<ArticleIcon/>
|
||||
</ListItemIcon>
|
||||
<ListItemText primary={this.state.status_message} secondary="Order status"/>
|
||||
</ListItem>
|
||||
<Divider />
|
||||
<Divider/>
|
||||
</>
|
||||
:""
|
||||
:<Divider><Chip label='Order Details'/></Divider>
|
||||
}
|
||||
|
||||
<ListItem>
|
||||
@ -458,7 +522,7 @@ export default class OrderPage extends Component {
|
||||
<Divider />
|
||||
<Grid item xs={12} align="center">
|
||||
<Alert severity="warning" sx={{maxWidth:360}}>
|
||||
You cannot take an order yet! <Countdown date={new Date(this.state.penalty)} renderer={this.countdownPenaltyRenderer} />
|
||||
<Countdown date={new Date(this.state.penalty)} renderer={this.countdownPenaltyRenderer} />
|
||||
</Alert>
|
||||
</Grid>
|
||||
</>
|
||||
@ -498,7 +562,7 @@ export default class OrderPage extends Component {
|
||||
:
|
||||
<Grid container spacing={1}>
|
||||
<Grid item xs={12} align="center">
|
||||
<Button variant='contained' color='primary' onClick={this.handleClickTakeOrderButton}>Take Order</Button>
|
||||
<Countdown date={new Date(this.state.penalty)} renderer={this.countdownTakeOrderRenderer} />
|
||||
</Grid>
|
||||
<Grid item xs={12} align="center">
|
||||
<Button variant='contained' color='secondary' onClick={this.props.history.goBack}>Back</Button>
|
||||
@ -517,7 +581,7 @@ export default class OrderPage extends Component {
|
||||
{this.orderBox()}
|
||||
</Grid>
|
||||
<Grid item xs={6} align="left">
|
||||
<TradeBox width={330} data={this.state} completeSetState={this.completeSetState} />
|
||||
<TradeBox push={this.props.history.push} width={330} data={this.state} completeSetState={this.completeSetState} />
|
||||
</Grid>
|
||||
</Grid>
|
||||
)
|
||||
@ -543,8 +607,8 @@ export default class OrderPage extends Component {
|
||||
<Box sx={{ width: '100%' }}>
|
||||
<Box sx={{ borderBottom: 1, borderColor: 'divider' }}>
|
||||
<Tabs value={value} onChange={handleChange} variant="fullWidth" >
|
||||
<Tab label="Order Details" {...this.a11yProps(0)} />
|
||||
<Tab label="Contract Box" {...this.a11yProps(1)} />
|
||||
<Tab label="Order" {...this.a11yProps(0)} />
|
||||
<Tab label="Contract" {...this.a11yProps(1)} />
|
||||
</Tabs>
|
||||
</Box>
|
||||
<Grid container spacing={2}>
|
||||
@ -553,7 +617,7 @@ export default class OrderPage extends Component {
|
||||
{this.orderBox()}
|
||||
</div>
|
||||
<div style={{display: this.state.showContractBox == 1 ? '':'none'}}>
|
||||
<TradeBox width={330} data={this.state} completeSetState={this.completeSetState} />
|
||||
<TradeBox push={this.props.history.push} width={330} data={this.state} completeSetState={this.completeSetState} />
|
||||
</div>
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
@ -1,7 +1,7 @@
|
||||
import React, { Component } from "react";
|
||||
import { IconButton, Paper, Rating, Button, Grid, Typography, TextField, List, ListItem, ListItemText, Divider, ListItemIcon, Dialog, DialogActions, DialogContent, DialogContentText, DialogTitle} from "@mui/material"
|
||||
import { IconButton, Paper, Rating, Button, CircularProgress, Grid, Typography, TextField, List, ListItem, ListItemText, Divider, ListItemIcon, Dialog, DialogActions, DialogContent, DialogContentText, DialogTitle} from "@mui/material"
|
||||
import QRCode from "react-qr-code";
|
||||
import Countdown from 'react-countdown';
|
||||
import Countdown, { zeroPad} from 'react-countdown';
|
||||
import Chat from "./Chat"
|
||||
import MediaQuery from 'react-responsive'
|
||||
import QrReader from 'react-qr-reader'
|
||||
@ -234,8 +234,8 @@ export default class TradeBox extends Component {
|
||||
<Divider/>
|
||||
<Grid item xs={12} align="center">
|
||||
<Typography component="body2" variant="body2">
|
||||
Please wait for the taker to confirm by locking a bond.
|
||||
If the taker does not lock a bond in time the orer will be made
|
||||
Please wait for the taker to lock a bond.
|
||||
If the taker does not lock a bond in time, the order will be made
|
||||
public again.
|
||||
</Typography>
|
||||
</Grid>
|
||||
@ -361,6 +361,8 @@ export default class TradeBox extends Component {
|
||||
|
||||
<Grid container spacing={1}>
|
||||
<Grid item xs={12} align="center">
|
||||
{/* Make confirmation sound for HTLC received. */}
|
||||
<this.Sound soundFileName="locked-invoice"/>
|
||||
<Typography color="primary" component="subtitle1" variant="subtitle1">
|
||||
<b> Submit a LN invoice for {pn(this.props.data.invoice_amount)} Sats </b>
|
||||
</Typography>
|
||||
@ -547,12 +549,31 @@ export default class TradeBox extends Component {
|
||||
.then((data) => this.props.completeSetState(data));
|
||||
}
|
||||
|
||||
handleRatingChange=(e)=>{
|
||||
handleRatingUserChange=(e)=>{
|
||||
const requestOptions = {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type':'application/json', 'X-CSRFToken': getCookie('csrftoken'),},
|
||||
body: JSON.stringify({
|
||||
'action': "rate",
|
||||
'action': "rate_user",
|
||||
'rating': e.target.value,
|
||||
}),
|
||||
};
|
||||
fetch('/api/order/' + '?order_id=' + this.props.data.id, requestOptions)
|
||||
.then((response) => response.json())
|
||||
.then((data) => this.props.completeSetState(data));
|
||||
}
|
||||
|
||||
handleRatingRobosatsChange=(e)=>{
|
||||
if (this.state.rating_platform != null){
|
||||
return null
|
||||
}
|
||||
this.setState({rating_platform:e.target.value});
|
||||
|
||||
const requestOptions = {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type':'application/json', 'X-CSRFToken': getCookie('csrftoken'),},
|
||||
body: JSON.stringify({
|
||||
'action': "rate_platform",
|
||||
'rating': e.target.value,
|
||||
}),
|
||||
};
|
||||
@ -670,14 +691,38 @@ handleRatingChange=(e)=>{
|
||||
</Grid>
|
||||
<Grid item xs={12} align="center">
|
||||
<Typography component="body2" variant="body2" align="center">
|
||||
What do you think of <b>{this.props.data.is_maker ? this.props.data.taker_nick : this.props.data.maker_nick}</b>?
|
||||
What do you think of ⚡<b>{this.props.data.is_maker ? this.props.data.taker_nick : this.props.data.maker_nick}</b>⚡?
|
||||
</Typography>
|
||||
</Grid>
|
||||
<Grid item xs={12} align="center">
|
||||
<Rating name="size-large" defaultValue={2} size="large" onChange={this.handleRatingChange} />
|
||||
<Rating name="size-large" defaultValue={0} size="large" onChange={this.handleRatingUserChange} />
|
||||
</Grid>
|
||||
<Grid item xs={12} align="center">
|
||||
<Button color='primary' href='/' component="a">Start Again</Button>
|
||||
<Typography component="body2" variant="body2" align="center">
|
||||
What do you think of 🤖<b>RoboSats</b>🤖?
|
||||
</Typography>
|
||||
</Grid>
|
||||
<Grid item xs={12} align="center">
|
||||
<Rating name="size-large" defaultValue={0} size="large" onChange={this.handleRatingRobosatsChange} />
|
||||
</Grid>
|
||||
{this.state.rating_platform==5 ?
|
||||
<Grid item xs={12} align="center">
|
||||
<Typography component="body2" variant="body2" align="center">
|
||||
<p>Thank you! RoboSats loves you too ❤️</p>
|
||||
<p>RoboSats gets better with more liquidity and users. Tell a bitcoiner friend about Robosats!</p>
|
||||
</Typography>
|
||||
</Grid>
|
||||
: null}
|
||||
{this.state.rating_platform!=5 & this.state.rating_platform!=null ?
|
||||
<Grid item xs={12} align="center">
|
||||
<Typography component="body2" variant="body2" align="center">
|
||||
Thank you for using Robosats! Let us know what you did not like and how the platform could improve
|
||||
(<a href="https://t.me/robosats">Telegram</a> / <a href="https://github.com/Reckless-Satoshi/robosats/issues">Github</a>)
|
||||
</Typography>
|
||||
</Grid>
|
||||
: null}
|
||||
<Grid item xs={12} align="center">
|
||||
<Button color='primary' onClick={() => {this.props.push('/')}}>Start Again</Button>
|
||||
</Grid>
|
||||
</Grid>
|
||||
)
|
||||
@ -696,11 +741,28 @@ handleRatingChange=(e)=>{
|
||||
RoboSats is trying to pay your lightning invoice. Remember that lightning nodes must
|
||||
be online in order to receive payments.
|
||||
</Typography>
|
||||
<br/>
|
||||
<Grid item xs={12} align="center">
|
||||
<CircularProgress/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Grid>
|
||||
)
|
||||
}
|
||||
|
||||
// Countdown Renderer callback with condition
|
||||
countdownRenderer = ({ minutes, seconds, completed }) => {
|
||||
if (completed) {
|
||||
// Render a completed state
|
||||
return (<div align="center"><span> Retrying! </span><br/><CircularProgress/></div> );
|
||||
|
||||
} else {
|
||||
return (
|
||||
<span>{zeroPad(minutes)}m {zeroPad(seconds)}s </span>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
showRoutingFailed=()=>{
|
||||
// TODO If it has failed 3 times, ask for a new invoice.
|
||||
if(this.props.data.invoice_expired){
|
||||
@ -713,7 +775,7 @@ handleRatingChange=(e)=>{
|
||||
</Grid>
|
||||
<Grid item xs={12} align="center">
|
||||
<Typography component="body2" variant="body2" align="center">
|
||||
Your invoice has expires or more than 3 payments have been attempted.
|
||||
Your invoice has expires or more than 3 payments attempts have been made.
|
||||
</Typography>
|
||||
</Grid>
|
||||
<Grid item xs={12} align="center">
|
||||
|
@ -1,5 +1,5 @@
|
||||
import React, { Component } from "react";
|
||||
import { Button , Dialog, Grid, Typography, TextField, ButtonGroup, CircularProgress, IconButton} from "@mui/material"
|
||||
import { Button , Tooltip, Dialog, Grid, Typography, TextField, ButtonGroup, CircularProgress, IconButton} from "@mui/material"
|
||||
import { Link } from 'react-router-dom'
|
||||
import Image from 'material-ui-image'
|
||||
import InfoDialog from './InfoDialog'
|
||||
@ -28,11 +28,12 @@ export default class UserGenPage extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
token: this.genBase62Token(34),
|
||||
token: this.genBase62Token(36),
|
||||
openInfo: false,
|
||||
loadingRobot: true,
|
||||
tokenHasChanged: false,
|
||||
};
|
||||
this.props.setAppState({avatarLoaded: false, nickname: null, token: null});
|
||||
this.getGeneratedUser(this.state.token);
|
||||
}
|
||||
|
||||
@ -64,11 +65,13 @@ export default class UserGenPage extends Component {
|
||||
// Add nick and token to App state (token only if not a bad request)
|
||||
(data.bad_request ? this.props.setAppState({
|
||||
nickname: data.nickname,
|
||||
avatarLoaded: false,
|
||||
})
|
||||
:
|
||||
this.props.setAppState({
|
||||
nickname: data.nickname,
|
||||
token: this.state.token,
|
||||
avatarLoaded: false,
|
||||
}));
|
||||
});
|
||||
}
|
||||
@ -85,8 +88,9 @@ export default class UserGenPage extends Component {
|
||||
|
||||
handleClickNewRandomToken=()=>{
|
||||
this.setState({
|
||||
token: this.genBase62Token(34),
|
||||
token: this.genBase62Token(36),
|
||||
tokenHasChanged: true,
|
||||
copied: true,
|
||||
});
|
||||
}
|
||||
|
||||
@ -98,9 +102,10 @@ export default class UserGenPage extends Component {
|
||||
}
|
||||
|
||||
handleClickSubmitToken=()=>{
|
||||
this.delGeneratedUser()
|
||||
this.delGeneratedUser();
|
||||
this.getGeneratedUser(this.state.token);
|
||||
this.setState({loadingRobot: true, tokenHasChanged: false})
|
||||
this.setState({loadingRobot: true, tokenHasChanged: false, copied: false});
|
||||
this.props.setAppState({avatarLoaded: false, nickname: null, token: null});
|
||||
}
|
||||
|
||||
handleClickOpenInfo = () => {
|
||||
@ -137,6 +142,7 @@ export default class UserGenPage extends Component {
|
||||
</Typography>
|
||||
</Grid>
|
||||
<Grid item xs={12} align="center">
|
||||
<Tooltip enterTouchDelay="0" title="This is your trading avatar">
|
||||
<div style={{ maxWidth: 200, maxHeight: 200 }}>
|
||||
<Image className='newAvatar'
|
||||
disableError='true'
|
||||
@ -144,7 +150,8 @@ export default class UserGenPage extends Component {
|
||||
color='null'
|
||||
src={this.state.avatar_url}
|
||||
/>
|
||||
</div><br/>
|
||||
</div>
|
||||
</Tooltip><br/>
|
||||
</Grid>
|
||||
</div>
|
||||
: <CircularProgress sx={{position: 'relative', top: 100, }}/>}
|
||||
@ -167,7 +174,7 @@ export default class UserGenPage extends Component {
|
||||
// style: { color: 'green' },
|
||||
// }}
|
||||
error={this.state.bad_request}
|
||||
label='Store your token safely'
|
||||
label={"Store your token safely"}
|
||||
required='true'
|
||||
value={this.state.token}
|
||||
variant='standard'
|
||||
@ -182,20 +189,35 @@ export default class UserGenPage extends Component {
|
||||
}}
|
||||
InputProps={{
|
||||
startAdornment:
|
||||
<IconButton onClick= {()=>navigator.clipboard.writeText(this.state.token)}>
|
||||
<ContentCopy color={this.state.tokenHasChanged ? 'inherit' : 'primary' } sx={{width:18, height:18}} />
|
||||
</IconButton>,
|
||||
<Tooltip disableHoverListener open={this.state.copied} enterTouchDelay="0" title="Copied!">
|
||||
<IconButton onClick= {()=> (navigator.clipboard.writeText(this.state.token) & this.setState({copied:true}))}>
|
||||
<ContentCopy color={this.props.avatarLoaded & !this.state.copied & !this.state.bad_request ? 'primary' : 'inherit' } sx={{width:18, height:18}}/>
|
||||
</IconButton>
|
||||
</Tooltip>,
|
||||
endAdornment:
|
||||
<IconButton onClick={this.handleClickNewRandomToken}><CasinoIcon/></IconButton>,
|
||||
<Tooltip enterTouchDelay="250" title="Generate a new token">
|
||||
<IconButton onClick={this.handleClickNewRandomToken}><CasinoIcon/></IconButton>
|
||||
</Tooltip>,
|
||||
}}
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
<Grid item xs={12} align="center">
|
||||
<Button disabled={!this.state.tokenHasChanged} type="submit" size='small' onClick= {this.handleClickSubmitToken}>
|
||||
{this.state.tokenHasChanged ?
|
||||
<Button type="submit" size='small' onClick= {this.handleClickSubmitToken}>
|
||||
<SmartToyIcon sx={{width:18, height:18}} />
|
||||
<span> Generate Robot</span>
|
||||
</Button>
|
||||
:
|
||||
<Tooltip enterTouchDelay="0" enterDelay="500" enterNextDelay="2000" title="You must enter a new token first">
|
||||
<div>
|
||||
<Button disabled={true} type="submit" size='small' >
|
||||
<SmartToyIcon sx={{width:18, height:18}} />
|
||||
<span> Generate Robot</span>
|
||||
</Button>
|
||||
</div>
|
||||
</Tooltip>
|
||||
}
|
||||
</Grid>
|
||||
<Grid item xs={12} align="center">
|
||||
<ButtonGroup variant="contained" aria-label="outlined primary button group">
|
||||
|
BIN
frontend/static/assets/images/favicon-192x192.png
Normal file
BIN
frontend/static/assets/images/favicon-192x192.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 29 KiB |
BIN
frontend/static/assets/images/favicon-32x32.png
Normal file
BIN
frontend/static/assets/images/favicon-32x32.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.4 KiB |
BIN
frontend/static/assets/images/favicon-96x96.png
Normal file
BIN
frontend/static/assets/images/favicon-96x96.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 11 KiB |
@ -43,7 +43,6 @@ body {
|
||||
}
|
||||
|
||||
.newAvatar {
|
||||
background-color:white;
|
||||
border-radius: 50%;
|
||||
border: 2px solid #555;
|
||||
filter: drop-shadow(1px 1px 1px #000000);
|
||||
@ -67,3 +66,26 @@ body {
|
||||
border: 0.3px solid #555;
|
||||
filter: drop-shadow(0.5px 0.5px 0.5px #000000);
|
||||
}
|
||||
|
||||
.phoneFlippedSmallAvatar img{
|
||||
transform: scaleX(-1);
|
||||
border: 1.3px solid #1976d2;
|
||||
-webkit-filter: grayscale(100%);
|
||||
filter: grayscale(100%) brightness(150%) contrast(150%) drop-shadow(0.7px 0.7px 0.7px #000000);
|
||||
}
|
||||
|
||||
.phoneFlippedSmallAvatar:after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0; left: 0; bottom: 0; right: 0;
|
||||
border-radius: 50%;
|
||||
border: 2.4px solid #1976d2;
|
||||
box-shadow: inset 0px 0px 35px rgb(255, 255, 255);
|
||||
}
|
||||
|
||||
.bookAvatar {
|
||||
border-radius: 50%;
|
||||
transform: scaleX(-1);
|
||||
border: 0.3px solid #555;
|
||||
filter: drop-shadow(0.5px 0.5px 0.5px #000000);
|
||||
}
|
2
frontend/static/frontend/main.js
Normal file
2
frontend/static/frontend/main.js
Normal file
File diff suppressed because one or more lines are too long
2291
frontend/static/frontend/main.js.LICENSE.txt
Normal file
2291
frontend/static/frontend/main.js.LICENSE.txt
Normal file
File diff suppressed because it is too large
Load Diff
@ -2,8 +2,10 @@
|
||||
<html>
|
||||
<head>
|
||||
<meta http-equiv="onion-location" content="{{ ONION_LOCATION }}" />
|
||||
{% comment %} TODO Add a proper fav icon {% endcomment %}
|
||||
<link rel="shortcut icon" href="#" />
|
||||
<link rel="shortcut icon" href="/static/assets/images/favicon-96x96.png" />
|
||||
<link rel="icon" type="image/png" href="/static/assets/images/favicon-32x32.png" sizes="32x32">
|
||||
<link rel="icon" type="image/png" href="/static/assets/images/favicon-96x96.png" sizes="96x96">
|
||||
<link rel="icon" type="image/png" href="/static/assets/images/favicon-192x192.png" sizes="192x192">
|
||||
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
|
133
requirements.txt
133
requirements.txt
@ -1,41 +1,4 @@
|
||||
aioredis==1.3.1
|
||||
aiorpcX==0.18.7
|
||||
amqp==5.0.9
|
||||
apturl==0.5.2
|
||||
asgiref==3.4.1
|
||||
async-timeout==4.0.2
|
||||
attrs==21.4.0
|
||||
autobahn==21.11.1
|
||||
Automat==20.2.0
|
||||
backports.zoneinfo==0.2.1
|
||||
bcrypt==3.1.7
|
||||
billiard==3.6.4.0
|
||||
blinker==1.4
|
||||
Brlapi==0.7.0
|
||||
celery==5.2.3
|
||||
certifi==2019.11.28
|
||||
cffi==1.15.0
|
||||
channels==3.0.4
|
||||
channels-redis==3.3.1
|
||||
chardet==3.0.4
|
||||
charge-lnd==0.2.4
|
||||
click==8.0.3
|
||||
click-didyoumean==0.3.0
|
||||
click-plugins==1.1.1
|
||||
click-repl==0.2.0
|
||||
colorama==0.4.4
|
||||
command-not-found==0.3
|
||||
constantly==15.1.0
|
||||
cryptography==36.0.1
|
||||
cupshelpers==1.0
|
||||
daphne==3.0.2
|
||||
dbus-python==1.2.16
|
||||
defer==1.0.6
|
||||
Deprecated==1.2.13
|
||||
distlib==0.3.4
|
||||
distro==1.4.0
|
||||
distro-info===0.23ubuntu1
|
||||
Django==3.2.11
|
||||
django==3.2.11
|
||||
django-admin-relation-links==0.2.5
|
||||
django-celery-beat==2.2.1
|
||||
django-celery-results==2.2.0
|
||||
@ -44,97 +7,17 @@ django-private-chat2==1.0.2
|
||||
django-redis==5.2.0
|
||||
django-timezone-field==4.2.3
|
||||
djangorestframework==3.13.1
|
||||
duplicity==0.8.12.0
|
||||
entrypoints==0.3
|
||||
fasteners==0.14.1
|
||||
filelock==3.4.2
|
||||
future==0.18.2
|
||||
channels==3.0.4
|
||||
channels-redis==3.3.1
|
||||
celery==5.2.3
|
||||
googleapis-common-protos==1.53.0
|
||||
grpcio==1.39.0
|
||||
grpcio==1.43.0
|
||||
grpcio-tools==1.43.0
|
||||
hiredis==2.0.0
|
||||
httplib2==0.14.0
|
||||
hyperlink==21.0.0
|
||||
idna==2.8
|
||||
incremental==21.3.0
|
||||
keyring==18.0.1
|
||||
kombu==5.2.3
|
||||
language-selector==0.1
|
||||
launchpadlib==1.10.13
|
||||
lazr.restfulclient==0.14.2
|
||||
lazr.uri==1.0.3
|
||||
lockfile==0.12.2
|
||||
louis==3.12.0
|
||||
macaroonbakery==1.3.1
|
||||
Mako==1.1.0
|
||||
MarkupSafe==1.1.0
|
||||
monotonic==1.5
|
||||
msgpack==1.0.3
|
||||
natsort==8.0.2
|
||||
netifaces==0.10.4
|
||||
numpy==1.22.0
|
||||
oauthlib==3.1.0
|
||||
olefile==0.46
|
||||
packaging==21.3
|
||||
paramiko==2.6.0
|
||||
pbr==5.8.0
|
||||
pexpect==4.6.0
|
||||
numpy==1.22.2
|
||||
Pillow==7.0.0
|
||||
platformdirs==2.4.1
|
||||
prompt-toolkit==3.0.24
|
||||
protobuf==3.17.3
|
||||
pyasn1==0.4.8
|
||||
pyasn1-modules==0.2.8
|
||||
pycairo==1.16.2
|
||||
pycparser==2.21
|
||||
pycups==1.9.73
|
||||
PyGObject==3.36.0
|
||||
PyJWT==1.7.1
|
||||
pymacaroons==0.13.0
|
||||
PyNaCl==1.3.0
|
||||
pyOpenSSL==21.0.0
|
||||
pyparsing==3.0.6
|
||||
pyRFC3339==1.1
|
||||
PySocks==1.7.1
|
||||
python-apt==2.0.0+ubuntu0.20.4.5
|
||||
python-crontab==2.6.0
|
||||
python-dateutil==2.7.3
|
||||
python-debian===0.1.36ubuntu1
|
||||
python-decouple==3.5
|
||||
pytz==2021.3
|
||||
pyxdg==0.26
|
||||
PyYAML==5.3.1
|
||||
redis==4.1.0
|
||||
reportlab==3.5.34
|
||||
requests==2.22.0
|
||||
requests-unixsocket==0.2.0
|
||||
ring==0.9.1
|
||||
robohash==1.1
|
||||
scipy==1.7.3
|
||||
SecretStorage==2.3.1
|
||||
service-identity==21.1.0
|
||||
simplejson==3.16.0
|
||||
six==1.16.0
|
||||
sqlparse==0.4.2
|
||||
stevedore==3.5.0
|
||||
systemd-python==234
|
||||
termcolor==1.1.0
|
||||
Twisted==21.7.0
|
||||
txaio==21.2.1
|
||||
typing-extensions==4.0.1
|
||||
ubuntu-advantage-tools==27.2
|
||||
ubuntu-drivers-common==0.0.0
|
||||
ufw==0.36
|
||||
unattended-upgrades==0.1
|
||||
urllib3==1.25.8
|
||||
usb-creator==0.3.7
|
||||
vine==5.0.0
|
||||
virtualenv==20.12.1
|
||||
virtualenv-clone==0.5.7
|
||||
virtualenvwrapper==4.8.4
|
||||
wadllib==1.3.3
|
||||
wcwidth==0.2.5
|
||||
wirerope==0.4.5
|
||||
wrapt==1.13.3
|
||||
xkit==0.0.0
|
||||
zope.interface==5.4.0
|
||||
scipy==1.8.0
|
||||
gunicorn==20.1.0
|
||||
|
@ -8,9 +8,11 @@ https://docs.djangoproject.com/en/4.0/howto/deployment/asgi/
|
||||
"""
|
||||
|
||||
import os
|
||||
import django
|
||||
from channels.routing import get_default_application
|
||||
|
||||
from django.core.asgi import get_asgi_application
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'tabulator.settings')
|
||||
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'robosats.settings')
|
||||
django.setup()
|
||||
|
||||
application = get_asgi_application()
|
||||
application = get_default_application()
|
||||
|
@ -10,24 +10,33 @@ For the full list of settings and their values, see
|
||||
https://docs.djangoproject.com/en/4.0/ref/settings/
|
||||
"""
|
||||
|
||||
import os
|
||||
from pathlib import Path
|
||||
from decouple import config
|
||||
|
||||
# Build paths inside the project like this: BASE_DIR / 'subdir'.
|
||||
BASE_DIR = Path(__file__).resolve().parent.parent
|
||||
|
||||
STATIC_URL = '/static/'
|
||||
|
||||
# Quick-start development settings - unsuitable for production
|
||||
# See https://docs.djangoproject.com/en/4.0/howto/deployment/checklist/
|
||||
|
||||
# SECURITY WARNING: keep the secret key used in production secret!
|
||||
SECRET_KEY = 'django-insecure-6^&6uw$b5^en%(cu2kc7_o)(mgpazx#j_znwlym0vxfamn2uo-'
|
||||
SECRET_KEY = config('SECRET_KEY')
|
||||
|
||||
DEBUG = False
|
||||
STATIC_URL = 'static/'
|
||||
STATIC_ROOT ='/usr/src/static/'
|
||||
|
||||
# SECURITY WARNING: don't run with debug turned on in production!
|
||||
DEBUG = True
|
||||
if os.environ.get('DEVELOPMENT'):
|
||||
DEBUG = True
|
||||
STATIC_ROOT = 'frontend/static/'
|
||||
|
||||
ALLOWED_HOSTS = [config('HOST_NAME'),'127.0.0.1']
|
||||
AVATAR_ROOT = STATIC_ROOT + 'assets/avatars/'
|
||||
|
||||
ALLOWED_HOSTS = [config('HOST_NAME'),config('HOST_NAME2'),config('LOCAL_ALIAS'),'127.0.0.1']
|
||||
|
||||
# Application definition
|
||||
|
||||
@ -82,13 +91,16 @@ WSGI_APPLICATION = 'robosats.wsgi.application'
|
||||
# Database
|
||||
# https://docs.djangoproject.com/en/4.0/ref/settings/#databases
|
||||
|
||||
DATABASES = {
|
||||
'default': {
|
||||
'ENGINE': 'django.db.backends.sqlite3',
|
||||
'NAME': BASE_DIR / 'db.sqlite3',
|
||||
}
|
||||
}
|
||||
|
||||
DATABASES = {
|
||||
'default': {
|
||||
'ENGINE': 'django.db.backends.sqlite3',
|
||||
'NAME': '/usr/src/database/db.sqlite3',
|
||||
'OPTIONS': {
|
||||
'timeout': 20, # in seconds
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Password validation
|
||||
# https://docs.djangoproject.com/en/4.0/ref/settings/#auth-password-validators
|
||||
|
55
setup.md
55
setup.md
@ -1,4 +1,46 @@
|
||||
# Set up
|
||||
# The easy way
|
||||
## With Docker (-dev containers running on testnet)
|
||||
Spinning up docker for the first time
|
||||
```
|
||||
docker-compose build --no-cache
|
||||
docker-compose up -d
|
||||
docker exec -it django-dev python3 manage.py makemigrations
|
||||
docker exec -it django-dev python3 manage.py migrate
|
||||
docker exec -it django-dev python3 manage.py createsuperuser
|
||||
docker-compose restart
|
||||
```
|
||||
|
||||
Spinning up any other time:
|
||||
`docker-compose up -d`
|
||||
|
||||
Then monitor in a terminal the Django dev docker service
|
||||
`docker attach django-dev`
|
||||
|
||||
And the NPM dev docker service
|
||||
`docker attach npm-dev`
|
||||
|
||||
You could also just check all services logs
|
||||
`docker-compose logs -f`
|
||||
|
||||
Ready to roll! But maybe you also are interested on these:
|
||||
|
||||
Unlock or 'create' the lnd node
|
||||
`docker exec -it lnd-dev lncli unlock`
|
||||
|
||||
Create p2wkh addresses
|
||||
`docker exec -it lnd-dev lncli --network=testnet newaddress p2wkh`
|
||||
|
||||
Wallet balance
|
||||
`docker exec -it lnd-dev lncli --network=testnet walletbalance`
|
||||
|
||||
Connect
|
||||
`docker exec -it lnd-dev lncli --network=testnet connect node_id@ip:9735`
|
||||
|
||||
Open channel
|
||||
`docker exec -it lnd-dev lncli --network=testnet openchannel node_id --local_amt LOCAL_AMT --push_amt PUSH_AMT`
|
||||
|
||||
# The harder way
|
||||
## Django development environment
|
||||
### Install Python and pip
|
||||
`sudo apt install python3 python3 pip`
|
||||
@ -99,7 +141,16 @@ to
|
||||
|
||||
`from . import lightning_pb2 as lightning__pb2`
|
||||
|
||||
Same for every other file
|
||||
Same for every other file.
|
||||
|
||||
Generated files can be automatically patched like this:
|
||||
```
|
||||
sed -i 's/^import .*_pb2 as/from . \0/' api/lightning/router_pb2.py
|
||||
sed -i 's/^import .*_pb2 as/from . \0/' api/lightning/invoices_pb2.py
|
||||
sed -i 's/^import .*_pb2 as/from . \0/' api/lightning/router_pb2_grpc.py
|
||||
sed -i 's/^import .*_pb2 as/from . \0/' api/lightning/lightning_pb2_grpc.py
|
||||
sed -i 's/^import .*_pb2 as/from . \0/' api/lightning/invoices_pb2_grpc.py
|
||||
```
|
||||
|
||||
## React development environment
|
||||
### Install npm
|
||||
@ -131,7 +182,7 @@ npm install react-qr-reader
|
||||
```
|
||||
Note we are using mostly MaterialUI V5 (@mui/material) but Image loading from V4 (@material-ui/core) extentions (so both V4 and V5 are needed)
|
||||
|
||||
### Launch the React render
|
||||
### Launch
|
||||
from frontend/ directory
|
||||
`npm run dev`
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user