diff --git a/api/lightning/cln.py b/api/lightning/cln.py
index b005d4ece..8ea348162 100755
--- a/api/lightning/cln.py
+++ b/api/lightning/cln.py
@@ -585,12 +585,19 @@ def watchpayment():
time.sleep(2)
def handle_response():
+ from api.logics import Logics
+
+ if not order.transition_status(
+ Order.Status.PAY,
+ from_statuses=[Order.Status.PAY, Order.Status.FAI],
+ ):
+ return
+
try:
lnpayment.status = LNPayment.Status.FLIGHT
lnpayment.in_flight = True
lnpayment.save(update_fields=["in_flight", "status"])
- order.update_status(Order.Status.PAY)
nodestub = node_pb2_grpc.NodeStub(cls.node_channel)
response = nodestub.Pay(request)
@@ -649,7 +656,7 @@ def handle_response():
)
lnpayment.preimage = response.payment_preimage.hex()
lnpayment.save(update_fields=["status", "fee", "preimage"])
- order.update_status(Order.Status.SUC)
+ Logics.complete_order(order)
order.expires_at = timezone.now() + timedelta(
seconds=order.t_to_expire(Order.Status.SUC)
)
diff --git a/api/lightning/lnd.py b/api/lightning/lnd.py
index e43e11b7d..2a298ac86 100644
--- a/api/lightning/lnd.py
+++ b/api/lightning/lnd.py
@@ -546,11 +546,17 @@ def follow_send_payment(cls, lnpayment, fee_limit_sat, timeout_seconds):
return
def handle_response(response, was_in_transit=False):
+ from api.logics import Logics
+
+ if not order.transition_status(
+ Order.Status.PAY,
+ from_statuses=[Order.Status.PAY, Order.Status.FAI],
+ ):
+ return
+
lnpayment.status = LNPayment.Status.FLIGHT
lnpayment.in_flight = True
lnpayment.save(update_fields=["in_flight", "status"])
- order.update_status(Order.Status.PAY)
- order.save(update_fields=["status"])
if (
response.status == lightning_pb2.Payment.PaymentStatus.UNKNOWN
@@ -625,7 +631,7 @@ def handle_response(response, was_in_transit=False):
lnpayment.preimage = response.payment_preimage
lnpayment.save(update_fields=["status", "fee", "preimage"])
- order.update_status(Order.Status.SUC)
+ Logics.complete_order(order)
order.expires_at = timezone.now() + timedelta(
seconds=order.t_to_expire(Order.Status.SUC)
)
diff --git a/api/logics.py b/api/logics.py
index 4dee2a8d1..61aabb668 100644
--- a/api/logics.py
+++ b/api/logics.py
@@ -967,9 +967,10 @@ def move_state_updated_payout_method(cls, order):
order.update_status(Order.Status.WFE)
# If the order status is 'Failed Routing'. Retry payment.
- elif order.status == Order.Status.FAI:
- if LNNode.double_check_htlc_is_settled(order.trade_escrow.payment_hash):
- order.update_status(Order.Status.PAY)
+ elif LNNode.double_check_htlc_is_settled(order.trade_escrow.payment_hash):
+ if order.transition_status(
+ Order.Status.PAY, from_statuses=[Order.Status.FAI]
+ ):
order.payout.status = LNPayment.Status.FLIGHT
order.payout.routing_attempts = 0
order.payout.save(update_fields=["status", "routing_attempts"])
@@ -1657,7 +1658,7 @@ def pay_buyer(cls, order):
order.payout_tx.status = OnchainPayment.Status.QUEUE
order.payout_tx.save(update_fields=["status"])
- order.update_status(Order.Status.SUC)
+ cls.complete_order(order)
order.contract_finalization_time = timezone.now()
order.save(update_fields=["contract_finalization_time"])
@@ -1665,6 +1666,20 @@ def pay_buyer(cls, order):
order.log("Paying buyer onchain address")
return True
+ @classmethod
+ def complete_order(cls, order):
+ """
+ Completes the order after the the sats are successfully paid out
+ and computes the coordinator revenue.
+ """
+ if not order.transition_status(
+ Order.Status.SUC, from_statuses=[Order.Status.FSE, Order.Status.PAY, Order.Status.FAI]
+ ):
+ return
+
+ # Computes coordinator trade revenue
+ cls.compute_proceeds(order)
+
@classmethod
def confirm_fiat(cls, order, user):
"""If Order is in the CHAT states:
@@ -1710,9 +1725,6 @@ def confirm_fiat(cls, order, user):
# !!! KEY LINE - PAYS THE BUYER INVOICE !!!
cls.pay_buyer(order)
- # Computes coordinator trade revenue
- cls.compute_proceeds(order)
-
return True, None
else:
diff --git a/api/models/order.py b/api/models/order.py
index 612486fdd..2e95e2efe 100644
--- a/api/models/order.py
+++ b/api/models/order.py
@@ -363,12 +363,39 @@ def update_status(self, new_status):
old_status = self.status
self.status = new_status
self.save(update_fields=["status"])
- self.log(
- f"Order state went from {old_status}: {Order.Status(old_status).label} to {new_status}: {Order.Status(new_status).label}"
- )
+
+ self.log_status_transition(old_status, new_status)
+
if new_status == Order.Status.FAI:
send_notification.delay(order_id=self.id, message="lightning_failed")
+ def transition_status(self, new_status, from_statuses):
+ """
+ Atomically transition the order to `new_status` only if its current
+ database status is one of `from_statuses`.
+
+ Returns True if the transition happened, False otherwise.
+ The in-memory `self.status` is kept in sync on success.
+ """
+ updated = Order.objects.filter(
+ pk=self.pk,
+ status__in=from_statuses,
+ ).update(status=new_status)
+
+ if updated == 1:
+ old_status = self.status
+ self.status = new_status
+ self.log_status_transition(old_status, new_status)
+ return True
+
+ return False
+
+ def log_status_transition(self, old_status, new_status):
+ if old_status != new_status:
+ self.log(
+ f"Order state went from {old_status}: {Order.Status(old_status).label} to {new_status}: {Order.Status(new_status).label}"
+ )
+
@receiver(pre_delete, sender=Order)
def delete_lnpayment_at_order_deletion(sender, instance, **kwargs):