Work on LN bonds. Maker bond works. Yet, this is not the best way probably.

This commit is contained in:
Reckless_Satoshi 2022-01-11 12:49:53 -08:00
parent 17df987630
commit 8bc8f539d0
No known key found for this signature in database
GPG Key ID: 9C4585B561315571
8 changed files with 191 additions and 74 deletions

1
.gitignore vendored
View File

@ -643,4 +643,5 @@ frontend/static/frontend/main*
frontend/static/assets/avatars*
api/lightning/lightning*
api/lightning/invoices*
api/lightning/router*
api/lightning/googleapis*

View File

@ -1,6 +1,7 @@
import grpc, os, hashlib, secrets, json
from . import lightning_pb2 as lnrpc, lightning_pb2_grpc as lightningstub
from . import invoices_pb2 as invoicesrpc, invoices_pb2_grpc as invoicesstub
from . import router_pb2 as routerrpc, router_pb2_grpc as routerstub
from decouple import config
from base64 import b64decode
@ -19,10 +20,13 @@ LND_GRPC_HOST = config('LND_GRPC_HOST')
class LNNode():
os.environ["GRPC_SSL_CIPHER_SUITES"] = 'HIGH+ECDSA'
creds = grpc.ssl_channel_credentials(CERT)
channel = grpc.secure_channel(LND_GRPC_HOST, creds)
lightningstub = lightningstub.LightningStub(channel)
invoicesstub = invoicesstub.InvoicesStub(channel)
routerstub = routerstub.RouterStub(channel)
@classmethod
def decode_payreq(cls, invoice):
@ -46,8 +50,8 @@ class LNNode():
@classmethod
def settle_hold_invoice(cls, preimage):
'''settles a hold invoice'''
request = invoicesrpc.SettleInvoiceMsg(preimage=preimage)
response = invoicesstub.SettleInvoice(request, metadata=[('macaroon', MACAROON.hex())])
request = invoicesrpc.SettleInvoiceMsg(preimage=bytes.fromhex(preimage))
response = cls.invoicesstub.SettleInvoice(request, metadata=[('macaroon', MACAROON.hex())])
# Fix this: tricky because settling sucessfully an invoice has no response. TODO
if response == None:
return True
@ -84,32 +88,30 @@ class LNNode():
@classmethod
def validate_hold_invoice_locked(cls, payment_hash):
'''Checks if hold invoice is locked'''
request = invoicesrpc.LookupInvoiceMsg(payment_hash=payment_hash)
response = invoicesstub.LookupInvoiceV2(request, metadata=[('macaroon', MACAROON.hex())])
# What is the state for locked ???
if response.state == 'OPEN' or response.state == 'SETTLED':
return False
else:
return True
request = invoicesrpc.LookupInvoiceMsg(payment_hash=bytes.fromhex(payment_hash))
response = cls.invoicesstub.LookupInvoiceV2(request, metadata=[('macaroon', MACAROON.hex())])
print('status here')
print(response.state) # LND states: 0 OPEN, 1 SETTLED, 3 ACCEPTED, GRPC_ERROR status 5 when cancelled
return response.state == 3 # True if hold invoice is accepted.
@classmethod
def check_until_invoice_locked(cls, payment_hash, expiration):
'''Checks until hold invoice is locked.
When invoice is locked, returns true.
If time expires, return False.'''
request = invoicesrpc.SubscribeSingleInvoiceRequest(r_hash=payment_hash)
for invoice in invoicesstub.SubscribeSingleInvoice(request):
# Experimental, needs asyncio
# Maybe best to pass LNpayment object and change status live.
request = cls.invoicesrpc.SubscribeSingleInvoiceRequest(r_hash=payment_hash)
for invoice in cls.invoicesstub.SubscribeSingleInvoice(request):
print(invoice)
if timezone.now > expiration:
break
if invoice.state == 'LOCKED':
if invoice.state == 'ACCEPTED':
return True
return False
@classmethod
def validate_ln_invoice(cls, invoice, num_satoshis):
'''Checks if the submited LN invoice comforms to expectations'''
@ -125,10 +127,15 @@ class LNNode():
try:
payreq_decoded = cls.decode_payreq(invoice)
print(payreq_decoded)
except:
buyer_invoice['context'] = {'bad_invoice':'Does not look like a valid lightning invoice'}
return buyer_invoice
if payreq_decoded.num_satoshis == 0:
buyer_invoice['context'] = {'bad_invoice':'The invoice provided has no explicit amount'}
return buyer_invoice
if not payreq_decoded.num_satoshis == num_satoshis:
buyer_invoice['context'] = {'bad_invoice':'The invoice provided is not for '+'{:,}'.format(num_satoshis)+ ' Sats'}
return buyer_invoice
@ -147,11 +154,28 @@ class LNNode():
return buyer_invoice
@classmethod
def pay_invoice(cls, invoice):
def pay_invoice(cls, invoice, num_satoshis):
'''Sends sats to buyer'''
# Needs router subservice
# Maybe best to pass order and change status live.
fee_limit_sat = max(num_satoshis * 0.0002, 10) # 200 ppm or 10 sats
return True
request = routerrpc.SendPaymentRequest(
payment_request=invoice,
amt_msat=num_satoshis,
fee_limit_sat=fee_limit_sat,
timeout_seconds=60,
)
for response in routerstub.SendPaymentV2(request, metadata=[('macaroon', MACAROON.hex())]):
print(response)
print(response.status)
if response.status == True:
return True
return False
@classmethod
def double_check_htlc_is_settled(cls, payment_hash):

View File

@ -149,7 +149,6 @@ class Logics():
# If the order status is 'Waiting for both'. Move forward to 'waiting for escrow'
if order.status == Order.Status.WF2:
print(order.trade_escrow)
if order.trade_escrow:
if order.trade_escrow.status == LNPayment.Status.LOCKED:
order.status = Order.Status.CHA
@ -159,27 +158,36 @@ class Logics():
order.save()
return True, None
def add_profile_rating(profile, rating):
''' adds a new rating to a user profile'''
profile.total_ratings = profile.total_ratings + 1
latest_ratings = profile.latest_ratings
if len(latest_ratings) <= 1:
profile.latest_ratings = [rating]
profile.avg_rating = rating
else:
latest_ratings = list(latest_ratings).append(rating)
profile.latest_ratings = latest_ratings
profile.avg_rating = sum(latest_ratings) / len(latest_ratings)
profile.save()
@classmethod
def rate_counterparty(cls, order, user, rating):
# If the trade is finished
if order.status > Order.Status.PAY:
# if maker, rates taker
if order.maker == user:
order.taker.profile.total_ratings = order.taker.profile.total_ratings + 1
last_ratings = list(order.taker.profile.last_ratings).append(rating)
order.taker.profile.total_ratings = sum(last_ratings) / len(last_ratings)
cls.add_profile_rating(order.taker.profile, rating)
# if taker, rates maker
if order.taker == user:
order.maker.profile.total_ratings = order.maker.profile.total_ratings + 1
last_ratings = list(order.maker.profile.last_ratings).append(rating)
order.maker.profile.total_ratings = sum(last_ratings) / len(last_ratings)
cls.add_profile_rating(order.maker.profile, rating)
else:
return False, {'bad_request':'You cannot rate your counterparty yet.'}
order.save()
return True, None
def is_penalized(user):
@ -204,7 +212,7 @@ class Logics():
order.maker = None
order.status = Order.Status.UCA
order.save()
return True, None
return True, {}
# 2) When maker cancels after bond
'''The order dissapears from book and goes to cancelled.
@ -213,12 +221,14 @@ class Logics():
of the bond (requires maker submitting an invoice)'''
elif order.status == Order.Status.PUB and order.maker == user:
#Settle the maker bond (Maker loses the bond for a public order)
valid = cls.settle_maker_bond(order)
if valid:
if cls.settle_maker_bond(order):
order.maker_bond.status = LNPayment.Status.SETLED
order.maker_bond.save()
order.maker = None
order.status = Order.Status.UCA
order.save()
return True, None
return True, {}
# 3) When taker cancels before bond
''' The order goes back to the book as public.
@ -226,13 +236,13 @@ class Logics():
elif order.status == Order.Status.TAK and order.taker == user:
# adds a timeout penalty
user.profile.penalty_expiration = timezone.now() + timedelta(seconds=PENALTY_TIMEOUT)
user.save()
user.profile.save()
order.taker = None
order.status = Order.Status.PUB
order.save()
return True, None
return True, {}
# 4) When taker or maker cancel after bond (before escrow)
'''The order goes into cancelled status if maker cancels.
@ -248,19 +258,19 @@ class Logics():
order.maker = None
order.status = Order.Status.UCA
order.save()
return True, None
return True, {}
# 4.b) When taker cancel after bond (before escrow)
'''The order into cancelled status if maker cancels.'''
elif order.status > Order.Status.TAK and order.status < Order.Status.CHA and order.taker == user:
#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_taker_bond(order)
if valid:
order.taker = None
order.status = Order.Status.PUB
# order.taker_bond = None # TODO fix this, it overrides the information about the settled taker bond. Might make admin tasks hard.
order.save()
return True, None
return True, {}
# 5) When trade collateral has been posted (after escrow)
'''Always goes to cancelled status. Collaboration is needed.
@ -281,6 +291,7 @@ class Logics():
# Return the previous invoice if there was one and is still unpaid
if order.maker_bond:
cls.check_maker_bond_locked(order)
if order.maker_bond.status == LNPayment.Status.INVGEN:
return True, {'bond_invoice':order.maker_bond.invoice,'bond_satoshis':order.maker_bond.num_satoshis}
else:
@ -289,7 +300,7 @@ class Logics():
order.last_satoshis = cls.satoshis_now(order)
bond_satoshis = int(order.last_satoshis * BOND_SIZE)
description = f"RoboSats - Publishing '{str(order)}' - This is a maker bond. It will automatically return if you do not cancel or cheat"
description = f"RoboSats - Publishing '{str(order)}' - This is a maker bond, it will freeze in your wallet. It automatically returns. It will be charged if you cheat or cancel."
# Gen hold Invoice
hold_payment = LNNode.gen_hold_invoice(bond_satoshis, description, BOND_EXPIRY*3600)
@ -311,6 +322,29 @@ class Logics():
order.save()
return True, {'bond_invoice':hold_payment['invoice'], 'bond_satoshis':bond_satoshis}
@classmethod
def check_until_maker_bond_locked(cls, order):
expiration = order.maker_bond.created_at + timedelta(seconds=EXP_MAKER_BOND_INVOICE)
is_locked = LNNode.check_until_invoice_locked(order.payment_hash, expiration)
if is_locked:
order.maker_bond.status = LNPayment.Status.LOCKED
order.maker_bond.save()
order.status = Order.Status.PUB
order.save()
return is_locked
@classmethod
def check_maker_bond_locked(cls, order):
if LNNode.validate_hold_invoice_locked(order.maker_bond.payment_hash):
order.maker_bond.status = LNPayment.Status.LOCKED
order.maker_bond.save()
order.status = Order.Status.PUB
order.save()
return True
return False
@classmethod
def gen_taker_hold_invoice(cls, order, user):
@ -330,7 +364,7 @@ class Logics():
order.last_satoshis = cls.satoshis_now(order) # LOCKS THE AMOUNT OF SATOSHIS FOR THE TRADE
bond_satoshis = int(order.last_satoshis * BOND_SIZE)
description = f"RoboSats - Taking '{str(order)}' - This is a taker bond. It will automatically return if you do not cancel or cheat"
description = f"RoboSats - Taking '{str(order)}' - This is a taker bond, it will freeze in your wallet. It automatically returns. It will be charged if you cheat or cancel."
# Gen hold Invoice
hold_payment = LNNode.gen_hold_invoice(bond_satoshis, description, BOND_EXPIRY*3600)
@ -407,12 +441,10 @@ class Logics():
def settle_maker_bond(order):
''' Settles the maker bond hold invoice'''
# TODO ERROR HANDLING
valid = LNNode.settle_hold_invoice(order.maker_bond.preimage)
if valid:
if LNNode.settle_hold_invoice(order.maker_bond.preimage):
order.maker_bond.status = LNPayment.Status.SETLED
order.save()
return valid
return True
def settle_taker_bond(order):
''' Settles the taker bond hold invoice'''

View File

@ -34,8 +34,8 @@ class LNPayment(models.Model):
RETNED = 3, 'Returned'
MISSNG = 4, 'Missing'
VALIDI = 5, 'Valid'
PAYING = 6, 'Paying ongoing'
FAILRO = 7, 'Failed routing'
FLIGHT = 6, 'On flight'
FAILRO = 7, 'Routing failed'
# payment use details
type = models.PositiveSmallIntegerField(choices=Types.choices, null=False, default=Types.HOLD)

View File

@ -60,7 +60,7 @@ class MakerView(CreateAPIView):
premium=premium,
satoshis=satoshis,
is_explicit=is_explicit,
expires_at=timezone.now()+timedelta(minutes=EXP_MAKER_BOND_INVOICE), # TODO Move to class method
expires_at=timezone.now()+timedelta(seconds=EXP_MAKER_BOND_INVOICE), # TODO Move to class method
maker=request.user)
# TODO move to Order class method when new instance is created!
@ -95,11 +95,11 @@ class OrderView(viewsets.ViewSet):
# This is our order.
order = order[0]
# 1) If order expired
# 1) If order has expired
if order.status == Order.Status.EXP:
return Response({'bad_request':'This order has expired'},status.HTTP_400_BAD_REQUEST)
# 2) If order cancelled
# 2) If order has been cancelled
if order.status == Order.Status.UCA:
return Response({'bad_request':'This order has been cancelled by the maker'},status.HTTP_400_BAD_REQUEST)
if order.status == Order.Status.CCA:
@ -107,7 +107,7 @@ class OrderView(viewsets.ViewSet):
data = ListOrderSerializer(order).data
# if user is under a limit (penalty), inform him
# if user is under a limit (penalty), inform him.
is_penalized, time_out = Logics.is_penalized(request.user)
if is_penalized:
data['penalty'] = time_out
@ -125,7 +125,7 @@ class OrderView(viewsets.ViewSet):
elif not data['is_participant'] and order.status != Order.Status.PUB:
return Response(data, status=status.HTTP_200_OK)
# For participants add position side, nicks and status as message
# For participants add positions, nicks and status as a message
data['is_buyer'] = Logics.is_buyer(order,request.user)
data['is_seller'] = Logics.is_seller(order,request.user)
data['maker_nick'] = str(order.maker)
@ -134,7 +134,7 @@ class OrderView(viewsets.ViewSet):
data['is_fiat_sent'] = order.is_fiat_sent
data['is_disputed'] = order.is_disputed
# If both bonds are locked, participants can see the trade in sats is also final.
# If both bonds are locked, participants can see the final trade amount in sats.
if order.taker_bond:
if order.maker_bond.status == order.taker_bond.status == LNPayment.Status.LOCKED:
# Seller sees the amount he pays
@ -182,8 +182,10 @@ class OrderView(viewsets.ViewSet):
else:
return Response(context, status.HTTP_400_BAD_REQUEST)
# 8) If status is 'CHA'or '' or '' and all HTLCS are in LOCKED
elif order.status == Order.Status.CHA: # TODO Add the other status
# 8) If status is 'CHA' or 'FSE' and all HTLCS are in LOCKED
elif order.status == Order.Status.CHA or order.status == Order.Status.FSE: # TODO Add the other status
# If all bonds are locked.
if order.maker_bond.status == order.taker_bond.status == order.trade_escrow.status == LNPayment.Status.LOCKED:
# add whether a collaborative cancel is pending
data['pending_cancel'] = order.is_pending_cancel
@ -193,7 +195,7 @@ class OrderView(viewsets.ViewSet):
def take_update_confirm_dispute_cancel(self, request, format=None):
'''
Here take place all of the user updates to the order object.
Here takes place all of updatesto the order object.
That is: take, confim, cancel, dispute, update_invoice or rate.
'''
order_id = request.GET.get(self.lookup_url_kwarg)
@ -208,7 +210,7 @@ class OrderView(viewsets.ViewSet):
invoice = serializer.data.get('invoice')
rating = serializer.data.get('rating')
# 1) If action is take, it is be taker request!
# 1) If action is take, it is a taker request!
if action == 'take':
if order.status == Order.Status.PUB:
valid, context = Logics.validate_already_maker_or_taker(request.user)
@ -253,7 +255,7 @@ class OrderView(viewsets.ViewSet):
return Response(
{'bad_request':
'The Robotic Satoshis working in the warehouse did not understand you. ' +
'Please, fill a Bug Issue in Github https://github.com/Reckless-Satoshi/robosats/issues'},
'Please, fill a Bug Issue in Github https://github.com/reckless-satoshi/robosats/issues'},
status.HTTP_501_NOT_IMPLEMENTED)
return self.get(request)
@ -277,6 +279,16 @@ class UserView(APIView):
- Creates login credentials (new User object)
Response with Avatar and Nickname.
'''
# if request.user.id:
# context = {}
# context['nickname'] = request.user.username
# participant = not Logics.validate_already_maker_or_taker(request.user)
# context['bad_request'] = f'You are already logged in as {request.user}'
# if participant:
# context['bad_request'] = f'You are already logged in as as {request.user} and have an active order'
# return Response(context,status.HTTP_200_OK)
token = request.GET.get(self.lookup_url_kwarg)
# Compute token entropy

View File

@ -67,7 +67,7 @@ export default class OrderPage extends Component {
super(props);
this.state = {
isExplicit: false,
delay: 10000, // Refresh every 10 seconds
delay: 2000, // Refresh every 2 seconds by default
currencies_dict: {"1":"USD"}
};
this.orderId = this.props.match.params.orderId;
@ -109,7 +109,7 @@ export default class OrderPage extends Component {
escrowInvoice: data.escrow_invoice,
escrowSatoshis: data.escrow_satoshis,
invoiceAmount: data.invoice_amount,
});
})
});
}
@ -129,9 +129,6 @@ export default class OrderPage extends Component {
tick = () => {
this.getOrderDetails();
}
handleDelayChange = (e) => {
this.setState({ delay: Number(e.target.value) });
}
// Fix to use proper react props
handleClickBackButton=()=>{
@ -149,7 +146,9 @@ export default class OrderPage extends Component {
};
fetch('/api/order/' + '?order_id=' + this.orderId, requestOptions)
.then((response) => response.json())
.then((data) => (console.log(data) & this.getOrderDetails(data.id)));
.then((data) => (this.setState({badRequest:data.bad_request})
& console.log(data)
& this.getOrderDetails(data.id)));
}
getCurrencyDict() {
fetch('/static/assets/currencies.json')
@ -278,8 +277,9 @@ export default class OrderPage extends Component {
</>
}
{/* Makers can cancel before commiting the bond (status 0)*/}
{this.state.isMaker & this.state.statusCode == 0 ?
{/* Makers can cancel before trade escrow deposited (status <9)*/}
{/* Only free cancel before bond locked (status 0)*/}
{this.state.isMaker & this.state.statusCode < 9 ?
<Grid item xs={12} align="center">
<Button variant='contained' color='secondary' onClick={this.handleClickCancelOrderButton}>Cancel</Button>
</Grid>

View File

@ -1,5 +1,5 @@
import React, { Component } from "react";
import { Paper, Button, Grid, Typography, TextField, List, ListItem, ListItemText, Divider} from "@mui/material"
import { Link, Paper, Rating, Button, Grid, Typography, TextField, List, ListItem, ListItemText, Divider} from "@mui/material"
import QRCode from "react-qr-code";
function getCookie(name) {
@ -294,6 +294,19 @@ handleClickOpenDisputeButton=()=>{
.then((response) => response.json())
.then((data) => (this.props.data = data));
}
handleRatingChange=(e)=>{
const requestOptions = {
method: 'POST',
headers: {'Content-Type':'application/json', 'X-CSRFToken': getCookie('csrftoken'),},
body: JSON.stringify({
'action': "rate",
'rating': e.target.value,
}),
};
fetch('/api/order/' + '?order_id=' + this.props.data.id, requestOptions)
.then((response) => response.json())
.then((data) => (this.props.data = data));
}
showFiatSentButton(){
return(
@ -359,6 +372,7 @@ handleClickOpenDisputeButton=()=>{
)
}
// showFiatReceivedButton(){
// }
@ -367,9 +381,28 @@ handleClickOpenDisputeButton=()=>{
// }
// showRateSelect(){
// }
showRateSelect(){
return(
<Grid container spacing={1}>
<Grid item xs={12} align="center">
<Typography component="h6" variant="h6">
🎉Trade finished!🥳
</Typography>
</Grid>
<Grid item xs={12} align="center">
<Typography component="body2" variant="body2" align="center">
What do you think of <b>{this.props.data.isMaker ? this.props.data.takerNick : this.props.data.makerNick}</b>?
</Typography>
</Grid>
<Grid item xs={12} align="center">
<Rating name="size-large" defaultValue={2} size="large" onChange={this.handleRatingChange} />
</Grid>
<Grid item xs={12} align="center">
<Button color='primary' to='/' component={Link}>Start Again</Button>
</Grid>
</Grid>
)
}
render() {
@ -393,14 +426,25 @@ handleClickOpenDisputeButton=()=>{
{this.props.data.isBuyer & this.props.data.statusCode == 7 ? this.showWaitingForEscrow() : ""}
{this.props.data.isSeller & this.props.data.statusCode == 8 ? this.showWaitingForBuyerInvoice() : ""}
{/* In Chatroom - showChat(showSendButton, showReveiceButton, showDisputeButton) */}
{/* In Chatroom - No fiat sent - showChat(showSendButton, showReveiceButton, showDisputeButton) */}
{this.props.data.isBuyer & this.props.data.statusCode == 9 ? this.showChat(true,false,true) : ""}
{this.props.data.isSeller & this.props.data.statusCode == 9 ? this.showChat(false,false,true) : ""}
{/* In Chatroom - Fiat sent - showChat(showSendButton, showReveiceButton, showDisputeButton) */}
{this.props.data.isBuyer & this.props.data.statusCode == 10 ? this.showChat(false,false,true) : ""}
{this.props.data.isSeller & this.props.data.statusCode == 10 ? this.showChat(false,true,true) : ""}
{/* Trade Finished */}
{this.props.data.isSeller & this.props.data.statusCode > 12 & this.props.data.statusCode < 15 ? this.showRateSelect() : ""}
{this.props.data.isBuyer & this.props.data.statusCode == 14 ? this.showRateSelect() : ""}
{/* Trade Finished - Payment Routing Failed */}
{this.props.data.isBuyer & this.props.data.statusCode == 15 ? this.showUpdateInvoice() : ""}
{/* Trade Finished - Payment Routing Failed - TODO Needs more planning */}
{this.props.data.statusCode == 11 ? this.showInDispute() : ""}
{/* TODO */}
{/* */}
{/* */}

View File

@ -58,15 +58,17 @@ git clone https://github.com/googleapis/googleapis.git
curl -o lightning.proto -s https://raw.githubusercontent.com/lightningnetwork/lnd/master/lnrpc/lightning.proto
python3 -m grpc_tools.protoc --proto_path=googleapis:. --python_out=. --grpc_python_out=. lightning.proto
```
We also use the *Invoices* subservice for invoice validation.
We also use the *Invoices* and *Router* subservices for invoice validation and payment routing.
```
curl -o invoices.proto -s https://raw.githubusercontent.com/lightningnetwork/lnd/master/lnrpc/invoicesrpc/invoices.proto
python3 -m grpc_tools.protoc --proto_path=googleapis:. --python_out=. --grpc_python_out=. invoices.proto
curl -o router.proto -s https://raw.githubusercontent.com/lightningnetwork/lnd/master/lnrpc/routerrpc/router.proto
python3 -m grpc_tools.protoc --proto_path=googleapis:. --python_out=. --grpc_python_out=. router.proto
```
Relative imports are not working at the moment, so some editing is needed in
`api/lightning` files `lightning_pb2_grpc.py`, `invoices_pb2_grpc.py` and `invoices_pb2.py`.
`api/lightning` files `lightning_pb2_grpc.py`, `invoices_pb2_grpc.py`, `invoices_pb2.py`, `router_pb2_grpc.py` and `router_pb2.py`.
Example, change line :
For example in `lightning_pb2_grpc.py` , add "from . " :
`import lightning_pb2 as lightning__pb2`
@ -74,6 +76,8 @@ to
`from . import lightning_pb2 as lightning__pb2`
Same for every other file
## React development environment
### Install npm
`sudo apt install npm`
@ -96,7 +100,7 @@ npm install react-native-svg
npm install react-qr-code
npm install @mui/material
```
Note we are using mostly MaterialUI V5, but Image loading from V4 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
from frontend/ directory