diff --git a/api/lightning.py b/api/lightning.py index a10f258b..38230a28 100644 --- a/api/lightning.py +++ b/api/lightning.py @@ -1,3 +1,6 @@ +# import codecs, grpc, os +# import lightning_pb2 as lnrpc, lightning_pb2_grpc as lightningstub + from datetime import timedelta from django.utils import timezone @@ -12,9 +15,16 @@ class LNNode(): ''' Place holder functions to interact with Lightning Node ''' + + # macaroon = codecs.encode(open('LND_DIR/data/chain/bitcoin/simnet/admin.macaroon', 'rb').read(), 'hex') + # os.environ['GRPC_SSL_CIPHER_SUITES'] = 'HIGH+ECDSA' + # cert = open('LND_DIR/tls.cert', 'rb').read() + # ssl_creds = grpc.ssl_channel_credentials(cert) + # channel = grpc.secure_channel('localhost:10009', ssl_creds) + # stub = lightningstub.LightningStub(channel) - def gen_hodl_invoice(num_satoshis, description, expiry): - '''Generates hodl invoice to publish an order''' + def gen_hold_invoice(num_satoshis, description, expiry): + '''Generates hold invoice to publish an order''' # TODO invoice = ''.join(random.choices(string.ascii_uppercase + string.digits, k=80)) #FIX payment_hash = ''.join(random.choices(string.ascii_uppercase + string.digits, k=40)) #FIX @@ -22,12 +32,46 @@ class LNNode(): return invoice, payment_hash, expires_at - def validate_hodl_invoice_locked(payment_hash): - '''Generates hodl invoice to publish an order''' + def validate_hold_invoice_locked(payment_hash): + '''Checks if hodl invoice is locked''' + + # request = ln.InvoiceSubscription() + # When invoice is settled, return true. If time expires, return False. + # for invoice in stub.SubscribeInvoices(request): + # print(invoice) + return True - def validate_ln_invoice(invoice, num_satoshis): # num_satoshis + def validate_ln_invoice(invoice, num_satoshis): '''Checks if the submited LN invoice is as expected''' + + # request = lnrpc.PayReqString(pay_req=invoice) + # response = stub.DecodePayReq(request, metadata=[('macaroon', macaroon)]) + + # # { + # # "destination": , + # # "payment_hash": , + # # "num_satoshis": , + # # "timestamp": , + # # "expiry": , + # # "description": , + # # "description_hash": , + # # "fallback_addr": , + # # "cltv_expiry": , + # # "route_hints": , + # # "payment_addr": , + # # "num_msat": , + # # "features": , + # # } + + # if not response['num_satoshis'] == num_satoshis: + # return False, {'bad_invoice':f'The invoice provided is not for {num_satoshis}. '}, None, None, None + # description = response['description'] + # payment_hash = response['payment_hash'] + # expires_at = timezone(response['expiry']) + # if payment_hash and expires_at > timezone.now(): + # return True, None, description, payment_hash, expires_at + valid = True context = None description = 'Placeholder desc' # TODO decrypt from LN invoice @@ -40,11 +84,11 @@ class LNNode(): '''Sends sats to buyer, or cancelinvoices''' return True - def settle_hodl_htlcs(payment_hash): - '''Charges a LN hodl invoice''' + def settle_hold_htlcs(payment_hash): + '''Charges a LN hold invoice''' return True - def return_hodl_htlcs(payment_hash): + def return_hold_htlcs(payment_hash): '''Returns sats''' return True diff --git a/api/logics.py b/api/logics.py index ecfb2f38..0dd2f499 100644 --- a/api/logics.py +++ b/api/logics.py @@ -188,7 +188,7 @@ class Logics(): return False, {'bad_request':'You cannot cancel this order'} @classmethod - def gen_maker_hodl_invoice(cls, order, user): + def gen_maker_hold_invoice(cls, order, user): # Do not gen and cancel if order is more than 5 minutes old if order.expires_at < timezone.now(): @@ -206,12 +206,12 @@ class Logics(): bond_satoshis = int(order.last_satoshis * BOND_SIZE) description = f'RoboSats - Publishing {str(order)} - This bond will return to you if you do not cheat.' - # Gen HODL Invoice - invoice, payment_hash, expires_at = LNNode.gen_hodl_invoice(bond_satoshis, description, BOND_EXPIRY*3600) + # Gen hold Invoice + invoice, payment_hash, expires_at = LNNode.gen_hold_invoice(bond_satoshis, description, BOND_EXPIRY*3600) order.maker_bond = LNPayment.objects.create( concept = LNPayment.Concepts.MAKEBOND, - type = LNPayment.Types.HODL, + type = LNPayment.Types.hold, sender = user, receiver = User.objects.get(username=ESCROW_USERNAME), invoice = invoice, @@ -225,7 +225,7 @@ class Logics(): return True, {'bond_invoice':invoice,'bond_satoshis':bond_satoshis} @classmethod - def gen_taker_hodl_invoice(cls, order, user): + def gen_taker_hold_invoice(cls, order, user): # Do not gen and cancel if a taker invoice is there and older than X minutes and unpaid still if order.taker_bond: @@ -245,12 +245,12 @@ class Logics(): bond_satoshis = int(order.last_satoshis * BOND_SIZE) description = f'RoboSats - Taking {str(order)} - This bond will return to you if you do not cheat.' - # Gen HODL Invoice - invoice, payment_hash, expires_at = LNNode.gen_hodl_invoice(bond_satoshis, description, BOND_EXPIRY*3600) + # Gen hold Invoice + invoice, payment_hash, expires_at = LNNode.gen_hold_invoice(bond_satoshis, description, BOND_EXPIRY*3600) order.taker_bond = LNPayment.objects.create( concept = LNPayment.Concepts.TAKEBOND, - type = LNPayment.Types.HODL, + type = LNPayment.Types.hold, sender = user, receiver = User.objects.get(username=ESCROW_USERNAME), invoice = invoice, @@ -267,7 +267,7 @@ class Logics(): return True, {'bond_invoice':invoice,'bond_satoshis': bond_satoshis} @classmethod - def gen_escrow_hodl_invoice(cls, order, user): + def gen_escrow_hold_invoice(cls, order, user): # Do not generate and cancel if an invoice is there and older than X minutes and unpaid still if order.trade_escrow: # Check if status is INVGEN and still not expired @@ -285,12 +285,12 @@ class Logics(): escrow_satoshis = order.last_satoshis # Trade sats amount was fixed at the time of taker bond generation (order.last_satoshis) description = f'RoboSats - Escrow amount for {str(order)} - This escrow will be released to the buyer once you confirm you received the fiat.' - # Gen HODL Invoice - invoice, payment_hash, expires_at = LNNode.gen_hodl_invoice(escrow_satoshis, description, ESCROW_EXPIRY*3600) + # Gen hold Invoice + invoice, payment_hash, expires_at = LNNode.gen_hold_invoice(escrow_satoshis, description, ESCROW_EXPIRY*3600) order.trade_escrow = LNPayment.objects.create( concept = LNPayment.Concepts.TRESCROW, - type = LNPayment.Types.HODL, + type = LNPayment.Types.hold, sender = user, receiver = User.objects.get(username=ESCROW_USERNAME), invoice = invoice, @@ -307,7 +307,7 @@ class Logics(): ''' Settles the trade escrow HTLC''' # TODO ERROR HANDLING - valid = LNNode.settle_hodl_htlcs(order.trade_escrow.payment_hash) + valid = LNNode.settle_hold_htlcs(order.trade_escrow.payment_hash) return valid def pay_buyer_invoice(order): @@ -338,7 +338,7 @@ class Logics(): return False, {'bad_request':'You cannot confirm to have received the fiat before it is confirmed to be sent by the buyer.'} # Make sure the trade escrow is at least as big as the buyer invoice - if order.trade_escrow.num_satoshis <= order.buyer_invoice.num_satoshis: + if order.trade_escrow.num_satoshis > order.buyer_invoice.num_satoshis: return False, {'bad_request':'Woah, something broke badly. Report in the public channels, or open a Github Issue.'} # Double check the escrow is settled. @@ -350,4 +350,4 @@ class Logics(): return False, {'bad_request':'You cannot confirm the fiat payment at this stage'} order.save() - return True, None + return True, None \ No newline at end of file diff --git a/api/models.py b/api/models.py index e429df5f..db6d9cf8 100644 --- a/api/models.py +++ b/api/models.py @@ -18,8 +18,8 @@ BOND_SIZE = float(config('BOND_SIZE')) class LNPayment(models.Model): class Types(models.IntegerChoices): - NORM = 0, 'Regular invoice' # Only outgoing HTLCs will be regular invoices (Non-hodl) - HODL = 1, 'Hodl invoice' + NORM = 0, 'Regular invoice' # Only outgoing HTLCs will be regular invoices (Non-hold) + hold = 1, 'hold invoice' class Concepts(models.IntegerChoices): MAKEBOND = 0, 'Maker bond' @@ -38,7 +38,7 @@ class LNPayment(models.Model): FAILRO = 7, 'Failed routing' # payment use details - type = models.PositiveSmallIntegerField(choices=Types.choices, null=False, default=Types.HODL) + type = models.PositiveSmallIntegerField(choices=Types.choices, null=False, default=Types.hold) concept = models.PositiveSmallIntegerField(choices=Concepts.choices, null=False, default=Concepts.MAKEBOND) status = models.PositiveSmallIntegerField(choices=Status.choices, null=False, default=Status.INVGEN) routing_retries = models.PositiveSmallIntegerField(null=False, default=0) @@ -133,7 +133,7 @@ class Order(models.Model): return (f'Order {self.id}: {self.Types(self.type).label} BTC for {float(self.amount)} {self.currency_dict[str(self.currency)]}') @receiver(pre_delete, sender=Order) -def delelete_HTLCs_at_order_deletion(sender, instance, **kwargs): +def delete_HTLCs_at_order_deletion(sender, instance, **kwargs): to_delete = (instance.maker_bond, instance.buyer_invoice, instance.taker_bond, instance.trade_escrow) for htlc in to_delete: @@ -193,7 +193,7 @@ class MarketTick(models.Model): It is checked against current CEX price for useful insight on the historical premium of Non-KYC BTC - Price is set when both taker bond is locked. Both + Price is set when taker bond is locked. Both maker and taker are commited with bonds (contract is finished and cancellation has a cost) ''' diff --git a/api/views.py b/api/views.py index ed6dab9e..a11b7ddf 100644 --- a/api/views.py +++ b/api/views.py @@ -136,17 +136,17 @@ class OrderView(viewsets.ViewSet): elif data['is_buyer']: data['trade_satoshis'] = Logics.buyer_invoice_amount(order, request.user)[1]['invoice_amount'] - # 5) If status is 'waiting for maker bond' and user is MAKER, reply with a MAKER HODL invoice. + # 5) If status is 'waiting for maker bond' and user is MAKER, reply with a MAKER hold invoice. if order.status == Order.Status.WFB and data['is_maker']: - valid, context = Logics.gen_maker_hodl_invoice(order, request.user) + valid, context = Logics.gen_maker_hold_invoice(order, request.user) if valid: data = {**data, **context} else: return Response(context, status.HTTP_400_BAD_REQUEST) - # 6) If status is 'waiting for taker bond' and user is TAKER, reply with a TAKER HODL invoice. + # 6) If status is 'waiting for taker bond' and user is TAKER, reply with a TAKER hold invoice. elif order.status == Order.Status.TAK and data['is_taker']: - valid, context = Logics.gen_taker_hodl_invoice(order, request.user) + valid, context = Logics.gen_taker_hold_invoice(order, request.user) if valid: data = {**data, **context} else: @@ -155,9 +155,9 @@ class OrderView(viewsets.ViewSet): # 7 a. ) If seller and status is 'WF2' or 'WFE' elif data['is_seller'] and (order.status == Order.Status.WF2 or order.status == Order.Status.WFE): - # If the two bonds are locked, reply with an ESCROW HODL invoice. + # If the two bonds are locked, reply with an ESCROW hold invoice. if order.maker_bond.status == order.taker_bond.status == LNPayment.Status.LOCKED: - valid, context = Logics.gen_escrow_hodl_invoice(order, request.user) + valid, context = Logics.gen_escrow_hold_invoice(order, request.user) if valid: data = {**data, **context} else: @@ -179,10 +179,7 @@ class OrderView(viewsets.ViewSet): 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 - - # 9) if buyer confirmed FIAT SENT - elif order.status == Order.Status.FSE: - data['buyer_confirmed'] + return Response(data, status.HTTP_200_OK) diff --git a/frontend/src/components/BookPage.js b/frontend/src/components/BookPage.js index c119b37b..c423d5c1 100644 --- a/frontend/src/components/BookPage.js +++ b/frontend/src/components/BookPage.js @@ -16,7 +16,6 @@ export default class BookPage extends Component { this.state.currencyCode = this.getCurrencyCode(this.state.currency) } - // Show message to be the first one to make an order getOrderDetails(type,currency) { fetch('/api/book' + '?currency=' + currency + "&type=" + type) .then((response) => response.json()) diff --git a/frontend/src/components/OrderPage.js b/frontend/src/components/OrderPage.js index fd86b7be..32148132 100644 --- a/frontend/src/components/OrderPage.js +++ b/frontend/src/components/OrderPage.js @@ -283,9 +283,8 @@ export default class OrderPage extends Component { ) } - - render (){ - return ( + orderDetailsPage (){ + return( this.state.badRequest ?
@@ -307,6 +306,13 @@ export default class OrderPage extends Component { {this.orderBox()} ) + ) + } + + render (){ + return ( + // Only so nothing shows while requesting the first batch of data + (this.state.statusCode == null & this.state.badRequest == null) ? "" : this.orderDetailsPage() ); } } diff --git a/frontend/src/components/TradeBox.js b/frontend/src/components/TradeBox.js index 397c5aa4..df214749 100644 --- a/frontend/src/components/TradeBox.js +++ b/frontend/src/components/TradeBox.js @@ -58,7 +58,7 @@ export default class TradeBox extends Component { size="small" defaultValue={this.props.data.bondInvoice} disabled="true" - helperText="This is a HODL LN invoice. It will not be charged if the order succeeds or expires. + helperText="This is a hold invoice. It will not be charged if the order succeeds or expires. It will be charged if the order is cancelled or you lose a dispute." color = "secondary" /> @@ -66,6 +66,7 @@ export default class TradeBox extends Component { ); } + showEscrowQRInvoice=()=>{ return ( @@ -84,7 +85,7 @@ export default class TradeBox extends Component { size="small" defaultValue={this.props.data.escrowInvoice} disabled="true" - helperText="This is a HODL LN invoice. It will be charged once the buyer confirms he sent the fiat." + helperText="This is a hold LN invoice. It will be charged once the buyer confirms he sent the fiat." color = "secondary" /> @@ -162,7 +163,7 @@ export default class TradeBox extends Component { }); } - // Fix this, clunky because it takes time. this.props.data does not refresh until next refresh of OrderPage. + // Fix this. It's clunky because it takes time. this.props.data does not refresh until next refresh of OrderPage. handleClickSubmitInvoiceButton=()=>{ const requestOptions = { @@ -215,7 +216,6 @@ export default class TradeBox extends Component { } showWaitingForEscrow(){ - return( @@ -236,7 +236,6 @@ export default class TradeBox extends Component { } showWaitingForBuyerInvoice(){ - return( @@ -257,26 +256,90 @@ export default class TradeBox extends Component { ) } - handleClickFiatConfirmButton=()=>{ + handleClickConfirmButton=()=>{ const requestOptions = { method: 'POST', headers: {'Content-Type':'application/json', 'X-CSRFToken': getCookie('csrftoken'),}, body: JSON.stringify({ - 'action':'confirm', - 'invoice': this.state.invoice, + 'action': "confirm", }), }; fetch('/api/order/' + '?order_id=' + this.props.data.id, requestOptions) .then((response) => response.json()) .then((data) => (this.props.data = data)); } - +handleClickOpenDisputeButton=()=>{ + const requestOptions = { + method: 'POST', + headers: {'Content-Type':'application/json', 'X-CSRFToken': getCookie('csrftoken'),}, + body: JSON.stringify({ + 'action': "dispute", + }), + }; + fetch('/api/order/' + '?order_id=' + this.props.data.id, requestOptions) + .then((response) => response.json()) + .then((data) => (this.props.data = data)); +} showFiatSentButton(){ return( - + + + + ) + } + + showFiatReceivedButton(){ + // TODO, show alert and ask for double confirmation (Have you check you received the fiat? Confirming fiat received settles the trade.) + // Ask for double confirmation. + return( + + + + + + ) + } + + showOpenDisputeButton(){ + // TODO, show alert about how opening a dispute might involve giving away personal data and might mean losing the bond. Ask for double confirmation. + return( + + + + + + ) + } + + showChat(sendFiatButton, receivedFiatButton, openDisputeButton){ + return( + + + + Chatting with {this.props.data.isMaker ? this.props.data.takerNick : this.props.data.makerNick} + + + + {this.props.data.isSeller ? + + Say hi to your peer robot! Be helpful and concise. Let him know how to send you {this.props.data.currencyCode}. + + : + + Say hi to your peer robot! Ask for payment details and click 'Confirm {this.props.data.currencyCode} sent' as soon as you send the payment. + + } + + + CHAT PLACEHOLDER + + + {sendFiatButton ? this.showFiatSentButton() : ""} + {receivedFiatButton ? this.showFiatReceivedButton() : ""} + {openDisputeButton ? this.showOpenDisputeButton() : ""} ) @@ -316,11 +379,11 @@ export default class TradeBox extends Component { {this.props.data.isBuyer & this.props.data.statusCode == 7 ? this.showWaitingForEscrow() : ""} {this.props.data.isSeller & this.props.data.statusCode == 8 ? this.showWaitingForBuyerInvoice() : ""} - {/* In Chatroom */} - {this.props.data.isBuyer & this.props.data.statusCode == 9 ? this.showChat() & this.showFiatSentButton() : ""} - {this.props.data.isSeller & this.props.data.statusCode ==9 ? this.showChat() : ""} - {this.props.data.isBuyer & this.props.data.statusCode == 10 ? this.showChat() & this.showOpenDisputeButton() : ""} - {this.props.data.isSeller & this.props.data.statusCode == 10 ? this.showChat() & this.showFiatReceivedButton() & this.showOpenDisputeButton(): ""} + {/* In Chatroom - 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) : ""} + {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() : ""}