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