diff --git a/api/admin.py b/api/admin.py index 19875946..08a947cb 100644 --- a/api/admin.py +++ b/api/admin.py @@ -25,14 +25,14 @@ class EUserAdmin(UserAdmin): @admin.register(Order) class OrderAdmin(AdminChangeLinksMixin, admin.ModelAdmin): - list_display = ('id','type','maker_link','taker_link','status','amount','currency_link','t0_satoshis','is_disputed','is_fiat_sent','created_at','expires_at', 'buyer_invoice_link','maker_bond_link','taker_bond_link','trade_escrow_link') + list_display = ('id','type','maker_link','taker_link','status','amount','currency_link','t0_satoshis','is_disputed','is_fiat_sent','created_at','expires_at', 'payout_link','maker_bond_link','taker_bond_link','trade_escrow_link') list_display_links = ('id','type') - change_links = ('maker','taker','currency','buyer_invoice','maker_bond','taker_bond','trade_escrow') + change_links = ('maker','taker','currency','payout','maker_bond','taker_bond','trade_escrow') list_filter = ('is_disputed','is_fiat_sent','type','currency','status') @admin.register(LNPayment) class LNPaymentAdmin(AdminChangeLinksMixin, admin.ModelAdmin): - list_display = ('hash','concept','status','num_satoshis','type','expires_at','sender_link','receiver_link','order_made_link','order_taken_link','order_escrow_link','order_paid_link') + list_display = ('hash','concept','status','num_satoshis','type','expires_at','expiry_height','sender_link','receiver_link','order_made_link','order_taken_link','order_escrow_link','order_paid_link') list_display_links = ('hash','concept') change_links = ('sender','receiver','order_made','order_taken','order_escrow','order_paid') list_filter = ('type','concept','status') diff --git a/api/lightning/node.py b/api/lightning/node.py index f0a1146f..f24e17ff 100644 --- a/api/lightning/node.py +++ b/api/lightning/node.py @@ -96,9 +96,9 @@ class LNNode(): return hold_payment @classmethod - def validate_hold_invoice_locked(cls, payment_hash): + def validate_hold_invoice_locked(cls, lnpayment): '''Checks if hold invoice is locked''' - request = invoicesrpc.LookupInvoiceMsg(payment_hash=bytes.fromhex(payment_hash)) + request = invoicesrpc.LookupInvoiceMsg(payment_hash=bytes.fromhex(lnpayment.payment_hash)) response = cls.invoicesstub.LookupInvoiceV2(request, metadata=[('macaroon', MACAROON.hex())]) print('status here') print(response.state) @@ -116,6 +116,8 @@ class LNNode(): pass if response.state == 3: # ACCEPTED (LOCKED) print('STATUS: ACCEPTED') + lnpayment.expiry_height = response.htlcs[0].expiry_height + lnpayment.save() return True @classmethod @@ -140,7 +142,7 @@ class LNNode(): def validate_ln_invoice(cls, invoice, num_satoshis): '''Checks if the submited LN invoice comforms to expectations''' - buyer_invoice = { + payout = { 'valid': False, 'context': None, 'description': None, @@ -153,30 +155,30 @@ class LNNode(): 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 + payout['context'] = {'bad_invoice':'Does not look like a valid lightning invoice'} + return payout if payreq_decoded.num_satoshis == 0: - buyer_invoice['context'] = {'bad_invoice':'The invoice provided has no explicit amount'} - return buyer_invoice + payout['context'] = {'bad_invoice':'The invoice provided has no explicit amount'} + return payout 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 + payout['context'] = {'bad_invoice':'The invoice provided is not for '+'{:,}'.format(num_satoshis)+ ' Sats'} + return payout - buyer_invoice['created_at'] = timezone.make_aware(datetime.fromtimestamp(payreq_decoded.timestamp)) - buyer_invoice['expires_at'] = buyer_invoice['created_at'] + timedelta(seconds=payreq_decoded.expiry) + payout['created_at'] = timezone.make_aware(datetime.fromtimestamp(payreq_decoded.timestamp)) + payout['expires_at'] = payout['created_at'] + timedelta(seconds=payreq_decoded.expiry) - if buyer_invoice['expires_at'] < timezone.now(): - buyer_invoice['context'] = {'bad_invoice':f'The invoice provided has already expired'} - return buyer_invoice + if payout['expires_at'] < timezone.now(): + payout['context'] = {'bad_invoice':f'The invoice provided has already expired'} + return payout - buyer_invoice['valid'] = True - buyer_invoice['description'] = payreq_decoded.description - buyer_invoice['payment_hash'] = payreq_decoded.payment_hash + payout['valid'] = True + payout['description'] = payreq_decoded.description + payout['payment_hash'] = payreq_decoded.payment_hash - return buyer_invoice + return payout @classmethod def pay_invoice(cls, invoice, num_satoshis): diff --git a/api/logics.py b/api/logics.py index d406661f..75f9d7bc 100644 --- a/api/logics.py +++ b/api/logics.py @@ -265,7 +265,7 @@ class Logics(): return True, None @classmethod - def buyer_invoice_amount(cls, order, user): + def payout_amount(cls, order, user): ''' Computes buyer invoice amount. Uses order.last_satoshis, that is the final trade amount set at Taker Bond time''' @@ -285,27 +285,27 @@ class Logics(): if not (order.taker_bond.status == order.maker_bond.status == LNPayment.Status.LOCKED): return False, {'bad_request':'You cannot submit a invoice while bonds are not locked.'} - num_satoshis = cls.buyer_invoice_amount(order, user)[1]['invoice_amount'] - buyer_invoice = LNNode.validate_ln_invoice(invoice, num_satoshis) + num_satoshis = cls.payout_amount(order, user)[1]['invoice_amount'] + payout = LNNode.validate_ln_invoice(invoice, num_satoshis) - if not buyer_invoice['valid']: - return False, buyer_invoice['context'] + if not payout['valid']: + return False, payout['context'] - order.buyer_invoice, _ = LNPayment.objects.update_or_create( + order.payout, _ = LNPayment.objects.update_or_create( concept = LNPayment.Concepts.PAYBUYER, type = LNPayment.Types.NORM, sender = User.objects.get(username=ESCROW_USERNAME), - order_paid = order, # In case this user has other buyer_invoices, update the one related to this order. + order_paid = order, # In case this user has other payouts, update the one related to this order. receiver= user, # if there is a LNPayment matching these above, it updates that one with defaults below. defaults={ 'invoice' : invoice, 'status' : LNPayment.Status.VALIDI, 'num_satoshis' : num_satoshis, - 'description' : buyer_invoice['description'], - 'payment_hash' : buyer_invoice['payment_hash'], - 'created_at' : buyer_invoice['created_at'], - 'expires_at' : buyer_invoice['expires_at']} + 'description' : payout['description'], + 'payment_hash' : payout['payment_hash'], + 'created_at' : payout['created_at'], + 'expires_at' : payout['expires_at']} ) # If the order status is 'Waiting for invoice'. Move forward to 'chat' @@ -323,8 +323,10 @@ class Logics(): order.status = Order.Status.WFE # If the order status is 'Failed Routing'. Retry payment. - if order.status == Order.Status.FAI: - follow_send_payment(order.buyer_invoice) + if order.status == Order.Status.FAI: + # Double check the escrow is settled. + if LNNode.double_check_htlc_is_settled(order.trade_escrow.payment_hash): + follow_send_payment(order.payout) order.save() return True, None @@ -476,7 +478,7 @@ class Logics(): def is_maker_bond_locked(cls, order): if order.maker_bond.status == LNPayment.Status.LOCKED: return True - elif LNNode.validate_hold_invoice_locked(order.maker_bond.payment_hash): + elif LNNode.validate_hold_invoice_locked(order.maker_bond): order.maker_bond.status = LNPayment.Status.LOCKED order.maker_bond.save() cls.publish_order(order) @@ -558,7 +560,7 @@ class Logics(): def is_taker_bond_locked(cls, order): if order.taker_bond.status == LNPayment.Status.LOCKED: return True - elif LNNode.validate_hold_invoice_locked(order.taker_bond.payment_hash): + elif LNNode.validate_hold_invoice_locked(order.taker_bond): cls.finalize_contract(order) return True return False @@ -625,7 +627,7 @@ class Logics(): def is_trade_escrow_locked(cls, order): if order.trade_escrow.status == LNPayment.Status.LOCKED: return True - elif LNNode.validate_hold_invoice_locked(order.trade_escrow.payment_hash): + elif LNNode.validate_hold_invoice_locked(order.trade_escrow): order.trade_escrow.status = LNPayment.Status.LOCKED order.trade_escrow.save() cls.trade_escrow_received(order) @@ -761,7 +763,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.payout.num_satoshis: return False, {'bad_request':'Woah, something broke badly. Report in the public channels, or open a Github Issue.'} if cls.settle_escrow(order): ##### !!! KEY LINE - SETTLES THE TRADE ESCROW !!! @@ -769,7 +771,7 @@ class Logics(): # Double check the escrow is settled. if LNNode.double_check_htlc_is_settled(order.trade_escrow.payment_hash): - is_payed, context = follow_send_payment(order.buyer_invoice) ##### !!! KEY LINE - PAYS THE BUYER INVOICE !!! + is_payed, context = follow_send_payment(order.payout) ##### !!! KEY LINE - PAYS THE BUYER INVOICE !!! if is_payed: # RETURN THE BONDS // Probably best also do it even if payment failed cls.return_bond(order.taker_bond) diff --git a/api/management/commands/follow_invoices.py b/api/management/commands/follow_invoices.py index 8c691d38..4f2d6124 100644 --- a/api/management/commands/follow_invoices.py +++ b/api/management/commands/follow_invoices.py @@ -70,6 +70,7 @@ class Command(BaseCommand): request = LNNode.invoicesrpc.LookupInvoiceMsg(payment_hash=bytes.fromhex(hold_lnpayment.payment_hash)) response = stub.LookupInvoiceV2(request, metadata=[('macaroon', MACAROON.hex())]) hold_lnpayment.status = lnd_state_to_lnpayment_status[response.state] + hold_lnpayment.expiry_height = response.htlcs[0].expiry_height except Exception as e: # If it fails at finding the invoice: it has been canceled. diff --git a/api/models.py b/api/models.py index 43482c78..ba780c6d 100644 --- a/api/models.py +++ b/api/models.py @@ -72,6 +72,7 @@ class LNPayment(models.Model): created_at = models.DateTimeField() expires_at = models.DateTimeField() cltv_expiry = models.PositiveSmallIntegerField(null=True, default=None, blank=True) + expiry_height = models.PositiveBigIntegerField(null=True, default=None, blank=True) # routing routing_attempts = models.PositiveSmallIntegerField(null=False, default=0) @@ -161,7 +162,7 @@ class Order(models.Model): taker_bond = models.OneToOneField(LNPayment, related_name='order_taken', on_delete=models.SET_NULL, null=True, default=None, blank=True) trade_escrow = models.OneToOneField(LNPayment, related_name='order_escrow', on_delete=models.SET_NULL, null=True, default=None, blank=True) # buyer payment LN invoice - buyer_invoice = models.OneToOneField(LNPayment, related_name='order_paid', on_delete=models.SET_NULL, null=True, default=None, blank=True) + payout = models.OneToOneField(LNPayment, related_name='order_paid', on_delete=models.SET_NULL, null=True, default=None, blank=True) # ratings maker_rated = models.BooleanField(default=False, null=False) @@ -195,7 +196,7 @@ class Order(models.Model): @receiver(pre_delete, sender=Order) def delete_lnpayment_at_order_deletion(sender, instance, **kwargs): - to_delete = (instance.maker_bond, instance.buyer_invoice, instance.taker_bond, instance.trade_escrow) + to_delete = (instance.maker_bond, instance.payout, instance.taker_bond, instance.trade_escrow) for lnpayment in to_delete: try: diff --git a/api/utils.py b/api/utils.py index ec19d873..929f7b76 100644 --- a/api/utils.py +++ b/api/utils.py @@ -84,8 +84,5 @@ def compute_premium_percentile(order): rates.append(float(similar_order.last_satoshis) / float(similar_order.amount)) rates = np.array(rates) - print(rates) - print(order_rate) - print(np.sum(rates < order_rate)) return round(np.sum(rates < order_rate) / len(rates),2) diff --git a/api/views.py b/api/views.py index b8e496bc..8817cd4a 100644 --- a/api/views.py +++ b/api/views.py @@ -170,7 +170,7 @@ class OrderView(viewsets.ViewSet): data['trade_satoshis'] = order.last_satoshis # Buyer sees the amount he receives elif data['is_buyer']: - data['trade_satoshis'] = Logics.buyer_invoice_amount(order, request.user)[1]['invoice_amount'] + data['trade_satoshis'] = Logics.payout_amount(order, request.user)[1]['invoice_amount'] # 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']: @@ -203,7 +203,7 @@ class OrderView(viewsets.ViewSet): # If the two bonds are locked, reply with an AMOUNT so he can send the buyer invoice. if order.maker_bond.status == order.taker_bond.status == LNPayment.Status.LOCKED: - valid, context = Logics.buyer_invoice_amount(order, request.user) + valid, context = Logics.payout_amount(order, request.user) if valid: data = {**data, **context} else: @@ -233,11 +233,11 @@ class OrderView(viewsets.ViewSet): data['statement_submitted'] = (order.taker_statement != None and order.maker_statement != "") # 9) If status is 'Failed routing', reply with retry amounts, time of next retry and ask for invoice at third. - elif order.status == Order.Status.FAI: - data['retries'] = order.buyer_invoice.routing_attempts - data['next_retry_time'] = order.buyer_invoice.last_routing_time + timedelta(minutes=RETRY_TIME) + elif order.status == Order.Status.FAI and order.payout.receiver == request.user: # might not be the buyer if after a dispute where winner wins + data['retries'] = order.payout.routing_attempts + data['next_retry_time'] = order.payout.last_routing_time + timedelta(minutes=RETRY_TIME) - if order.buyer_invoice.status == LNPayment.Status.EXPIRE: + if order.payout.status == LNPayment.Status.EXPIRE: data['invoice_expired'] = True # Add invoice amount once again if invoice was expired. data['invoice_amount'] = int(order.last_satoshis * (1-FEE)) diff --git a/frontend/src/components/BottomBar.js b/frontend/src/components/BottomBar.js index df924fa6..efaa1c95 100644 --- a/frontend/src/components/BottomBar.js +++ b/frontend/src/components/BottomBar.js @@ -71,7 +71,7 @@ export default class BottomBar extends Component { - + {this.state.robosats_running_commit_hash} diff --git a/frontend/src/components/OrderPage.js b/frontend/src/components/OrderPage.js index 7fb246f2..2412084f 100644 --- a/frontend/src/components/OrderPage.js +++ b/frontend/src/components/OrderPage.js @@ -285,7 +285,7 @@ export default class OrderPage extends Component { - + ) diff --git a/frontend/src/components/TradeBox.js b/frontend/src/components/TradeBox.js index f19409d3..fac8cb17 100644 --- a/frontend/src/components/TradeBox.js +++ b/frontend/src/components/TradeBox.js @@ -451,6 +451,25 @@ export default class TradeBox extends Component { )} } + showWaitForDisputeResolution=()=>{ + return ( + + + + We have the statements + + + + + Wait for the staff to resolve the dispute. The dispute winner + will be asked to submit a LN invoice. + + + {this.showBondIsLocked()} + + ) + } + showWaitingForEscrow(){ return( @@ -764,6 +783,7 @@ handleRatingChange=(e)=>{ {/* Trade Finished - TODO Needs more planning */} {this.props.data.status == 11 ? this.showInDisputeStatement() : ""} + {this.props.data.status == 16 ? this.showWaitForDisputeResolution() : ""} {/* Order has expired */} {this.props.data.status == 5 ? this.showOrderExpired() : ""}