From 8bc8f539d04d0dc2e579de6cccd476131c07971a Mon Sep 17 00:00:00 2001 From: Reckless_Satoshi Date: Tue, 11 Jan 2022 12:49:53 -0800 Subject: [PATCH] Work on LN bonds. Maker bond works. Yet, this is not the best way probably. --- .gitignore | 1 + api/lightning/node.py | 62 ++++++++++++++------- api/logics.py | 82 +++++++++++++++++++--------- api/models.py | 4 +- api/views.py | 34 ++++++++---- frontend/src/components/OrderPage.js | 16 +++--- frontend/src/components/TradeBox.js | 54 ++++++++++++++++-- setup.md | 12 ++-- 8 files changed, 191 insertions(+), 74 deletions(-) diff --git a/.gitignore b/.gitignore index 51480282..07c2094f 100755 --- a/.gitignore +++ b/.gitignore @@ -643,4 +643,5 @@ frontend/static/frontend/main* frontend/static/assets/avatars* api/lightning/lightning* api/lightning/invoices* +api/lightning/router* api/lightning/googleapis* diff --git a/api/lightning/node.py b/api/lightning/node.py index 11ec9b34..a78c72a3 100644 --- a/api/lightning/node.py +++ b/api/lightning/node.py @@ -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): diff --git a/api/logics.py b/api/logics.py index 1041f502..bf3e8851 100644 --- a/api/logics.py +++ b/api/logics.py @@ -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''' diff --git a/api/models.py b/api/models.py index e1707561..b5b5bb28 100644 --- a/api/models.py +++ b/api/models.py @@ -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) diff --git a/api/views.py b/api/views.py index ae891419..b4d3ec70 100644 --- a/api/views.py +++ b/api/views.py @@ -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 diff --git a/frontend/src/components/OrderPage.js b/frontend/src/components/OrderPage.js index 65340f0d..5fb3c490 100644 --- a/frontend/src/components/OrderPage.js +++ b/frontend/src/components/OrderPage.js @@ -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 ? diff --git a/frontend/src/components/TradeBox.js b/frontend/src/components/TradeBox.js index a7ec4e88..f27ef3e3 100644 --- a/frontend/src/components/TradeBox.js +++ b/frontend/src/components/TradeBox.js @@ -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( + + + + 🎉Trade finished!🥳 + + + + + What do you think of {this.props.data.isMaker ? this.props.data.takerNick : this.props.data.makerNick}? + + + + + + + + + + ) + } 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 */} {/* */} {/* */} diff --git a/setup.md b/setup.md index 26307531..73c95a9d 100644 --- a/setup.md +++ b/setup.md @@ -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