diff --git a/api/admin.py b/api/admin.py index e78b32d2..7158b8d1 100644 --- a/api/admin.py +++ b/api/admin.py @@ -153,10 +153,11 @@ class OrderAdmin(AdminChangeLinksMixin, admin.ModelAdmin): else: trade_sats = order.trade_escrow.num_satoshis - order.status = Order.Status.TLD order.maker.robot.earned_rewards = own_bond_sats + trade_sats - order.maker.robot.save() - order.save() + order.maker.robot.save(update_fields=["earned_rewards"]) + order.status = Order.Status.TLD + order.save(update_fields=["status"]) + self.message_user( request, f"Dispute of order {order.id} solved successfully on favor of the maker", @@ -190,10 +191,12 @@ class OrderAdmin(AdminChangeLinksMixin, admin.ModelAdmin): else: trade_sats = order.trade_escrow.num_satoshis - order.status = Order.Status.MLD order.taker.robot.earned_rewards = own_bond_sats + trade_sats - order.taker.robot.save() - order.save() + order.taker.robot.save(update_fields=["earned_rewards"]) + + order.status = Order.Status.MLD + order.save(update_fields=["status"]) + self.message_user( request, f"Dispute of order {order.id} solved successfully on favor of the taker", @@ -220,17 +223,21 @@ class OrderAdmin(AdminChangeLinksMixin, admin.ModelAdmin): order.maker_bond.sender.robot.earned_rewards += ( order.maker_bond.num_satoshis ) - order.maker_bond.sender.robot.save() + order.maker_bond.sender.robot.save(update_fields=["earned_rewards"]) + order.taker_bond.sender.robot.earned_rewards += ( order.taker_bond.num_satoshis ) - order.taker_bond.sender.robot.save() + + order.taker_bond.sender.robot.save(update_fields=["earned_rewards"]) order.trade_escrow.sender.robot.earned_rewards += ( order.trade_escrow.num_satoshis ) - order.trade_escrow.sender.robot.save() + order.trade_escrow.sender.robot.save(update_fields=["earned_rewards"]) + order.status = Order.Status.CCA - order.save() + order.save(update_fields=["status"]) + self.message_user( request, f"Dispute of order {order.id} solved successfully, everything returned as compensations", diff --git a/api/lightning/node.py b/api/lightning/node.py index 0ec5318a..4f552b60 100644 --- a/api/lightning/node.py +++ b/api/lightning/node.py @@ -164,13 +164,13 @@ class LNNode: if onchainpayment.status == queue_code: # Changing the state to "MEMPO" should be atomic with SendCoins. onchainpayment.status = on_mempool_code - onchainpayment.save() + onchainpayment.save(update_fields=["status"]) response = cls.lightningstub.SendCoins(request) if response.txid: onchainpayment.txid = response.txid onchainpayment.broadcasted = True - onchainpayment.save() + onchainpayment.save(update_fields=["txid", "broadcasted"]) return True elif onchainpayment.status == on_mempool_code: @@ -253,7 +253,7 @@ class LNNode: if response.state == 3: # ACCEPTED (LOCKED) lnpayment.expiry_height = response.htlcs[0].expiry_height lnpayment.status = LNPayment.Status.LOCKED - lnpayment.save() + lnpayment.save(update_fields=["expiry_height", "status"]) return True @classmethod @@ -442,14 +442,14 @@ class LNNode: failure_reason = cls.payment_failure_context[response.failure_reason] lnpayment.failure_reason = response.failure_reason lnpayment.status = LNPayment.Status.FAILRO - lnpayment.save() + lnpayment.save(update_fields=["failure_reason", "status"]) return False, failure_reason if response.status == 2: # STATUS 'SUCCEEDED' lnpayment.status = LNPayment.Status.SUCCED lnpayment.fee = float(response.fee_msat) / 1000 lnpayment.preimage = response.payment_preimage - lnpayment.save() + lnpayment.save(update_fields=["fee", "status", "preimage"]) return True, None return False @@ -479,15 +479,15 @@ class LNNode: def handle_response(response, was_in_transit=False): lnpayment.status = LNPayment.Status.FLIGHT lnpayment.in_flight = True - lnpayment.save() + lnpayment.save(update_fields=["in_flight", "status"]) order.status = Order.Status.PAY - order.save() + order.save(update_fields=["status"]) if response.status == 0: # Status 0 'UNKNOWN' # Not sure when this status happens print(f"Order: {order.id} UNKNOWN. Hash {hash}") lnpayment.in_flight = False - lnpayment.save() + lnpayment.save(update_fields=["in_flight"]) if response.status == 1: # Status 1 'IN_FLIGHT' print(f"Order: {order.id} IN_FLIGHT. Hash {hash}") @@ -498,7 +498,7 @@ class LNNode: # 20 minutes in the future so another thread spawns. if was_in_transit: lnpayment.last_routing_time = timezone.now() + timedelta(minutes=20) - lnpayment.save() + lnpayment.save(update_fields=["last_routing_time"]) if response.status == 3: # Status 3 'FAILED' lnpayment.status = LNPayment.Status.FAILRO @@ -509,13 +509,21 @@ class LNNode: if lnpayment.routing_attempts > 2: lnpayment.status = LNPayment.Status.EXPIRE lnpayment.routing_attempts = 0 - lnpayment.save() + lnpayment.save( + update_fields=[ + "status", + "last_routing_time", + "routing_attempts", + "failure_reason", + "in_flight", + ] + ) order.status = Order.Status.FAI order.expires_at = timezone.now() + timedelta( seconds=order.t_to_expire(Order.Status.FAI) ) - order.save() + order.save(update_fields=["status", "expires_at"]) print( f"Order: {order.id} FAILED. Hash: {hash} Reason: {cls.payment_failure_context[response.failure_reason]}" ) @@ -529,12 +537,14 @@ class LNNode: lnpayment.status = LNPayment.Status.SUCCED lnpayment.fee = float(response.fee_msat) / 1000 lnpayment.preimage = response.payment_preimage - lnpayment.save() + lnpayment.save(update_fields=["status", "fee", "preimage"]) + order.status = Order.Status.SUC order.expires_at = timezone.now() + timedelta( seconds=order.t_to_expire(Order.Status.SUC) ) - order.save() + order.save(update_fields=["status", "expires_at"]) + results = {"succeded": True} return results @@ -565,12 +575,16 @@ class LNNode: lnpayment.status = LNPayment.Status.EXPIRE lnpayment.last_routing_time = timezone.now() lnpayment.in_flight = False - lnpayment.save() + lnpayment.save( + update_fields=["status", "last_routing_time", "in_flight"] + ) + order.status = Order.Status.FAI order.expires_at = timezone.now() + timedelta( seconds=order.t_to_expire(Order.Status.FAI) ) - order.save() + order.save(update_fields=["status", "expires_at"]) + results = { "succeded": False, "context": "The payout invoice has expired", diff --git a/api/logics.py b/api/logics.py index e44e7f6a..a65432a8 100644 --- a/api/logics.py +++ b/api/logics.py @@ -1,4 +1,3 @@ -import ast import math from datetime import timedelta @@ -173,7 +172,7 @@ class Logics: order.expires_at = timezone.now() + timedelta( seconds=order.t_to_expire(Order.Status.TAK) ) - order.save() + order.save(update_fields=["amount", "taker", "status", "expires_at"]) return True, None def is_buyer(order, user): @@ -257,14 +256,14 @@ class Logics: order.status = Order.Status.EXP order.expiry_reason = Order.ExpiryReasons.NMBOND cls.cancel_bond(order.maker_bond) - order.save() + order.save(update_fields=["status", "expiry_reason"]) return True elif order.status in [Order.Status.PUB, Order.Status.PAU]: cls.return_bond(order.maker_bond) order.status = Order.Status.EXP order.expiry_reason = Order.ExpiryReasons.NTAKEN - order.save() + order.save(update_fields=["status", "expiry_reason"]) send_notification.delay(order_id=order.id, message="order_expired_untaken") return True @@ -284,7 +283,7 @@ class Logics: cls.cancel_escrow(order) order.status = Order.Status.EXP order.expiry_reason = Order.ExpiryReasons.NESINV - order.save() + order.save(update_fields=["status", "expiry_reason"]) return True elif order.status == Order.Status.WFE: @@ -300,9 +299,9 @@ class Logics: pass order.status = Order.Status.EXP order.expiry_reason = Order.ExpiryReasons.NESCRO - order.save() + order.save(update_fields=["status", "expiry_reason"]) # Reward taker with part of the maker bond - cls.add_slashed_rewards(order.maker_bond, order.taker_bond) + cls.add_slashed_rewards(order, order.maker_bond, order.taker_bond) return True # If maker is buyer, settle the taker's bond order goes back to public @@ -314,14 +313,10 @@ class Logics: except Exception: pass taker_bond = order.taker_bond - order.taker = None - order.taker_bond = None - order.trade_escrow = None - order.payout = None cls.publish_order(order) send_notification.delay(order_id=order.id, message="order_published") # Reward maker with part of the taker bond - cls.add_slashed_rewards(taker_bond, order.maker_bond) + cls.add_slashed_rewards(order, taker_bond, order.maker_bond) return True elif order.status == Order.Status.WFI: @@ -336,9 +331,9 @@ class Logics: cls.return_escrow(order) order.status = Order.Status.EXP order.expiry_reason = Order.ExpiryReasons.NINVOI - order.save() + order.save(update_fields=["status", "expiry_reason"]) # Reward taker with part of the maker bond - cls.add_slashed_rewards(order.maker_bond, order.taker_bond) + cls.add_slashed_rewards(order, order.maker_bond, order.taker_bond) return True # If maker is seller settle the taker's bond, order goes back to public @@ -346,13 +341,10 @@ class Logics: cls.settle_bond(order.taker_bond) cls.return_escrow(order) taker_bond = order.taker_bond - order.taker = None - order.taker_bond = None - order.trade_escrow = None cls.publish_order(order) send_notification.delay(order_id=order.id, message="order_published") # Reward maker with part of the taker bond - cls.add_slashed_rewards(taker_bond, order.maker_bond) + cls.add_slashed_rewards(order, taker_bond, order.maker_bond) return True elif order.status in [Order.Status.CHA, Order.Status.FSE]: @@ -371,11 +363,9 @@ class Logics: robot.penalty_expiration = timezone.now() + timedelta( seconds=PENALTY_TIMEOUT ) - robot.save() + robot.save(update_fields=["penalty_expiration"]) # Make order public again - order.taker = None - order.taker_bond = None cls.publish_order(order) return True @@ -417,14 +407,14 @@ class Logics: cls.return_escrow(order) cls.settle_bond(order.maker_bond) cls.return_bond(order.taker_bond) - cls.add_slashed_rewards(order.maker_bond, order.taker_bond) + cls.add_slashed_rewards(order, order.maker_bond, order.taker_bond) order.status = Order.Status.MLD elif num_messages_maker == 0: cls.return_escrow(order) cls.settle_bond(order.maker_bond) cls.return_bond(order.taker_bond) - cls.add_slashed_rewards(order.taker_bond, order.maker_bond) + cls.add_slashed_rewards(order, order.taker_bond, order.maker_bond) order.status = Order.Status.TLD else: return False @@ -433,7 +423,7 @@ class Logics: order.expires_at = timezone.now() + timedelta( seconds=order.t_to_expire(Order.Status.DIS) ) - order.save() + order.save(update_fields=["status", "is_disputed", "expires_at"]) send_notification.delay(order_id=order.id, message="dispute_opened") return True @@ -471,7 +461,7 @@ class Logics: order.expires_at = timezone.now() + timedelta( seconds=order.t_to_expire(Order.Status.DIS) ) - order.save() + order.save(update_fields=["is_disputed", "status", "expires_at"]) # User could be None if a dispute is open automatically due to weird expiration. if user is not None: @@ -483,7 +473,7 @@ class Logics: robot.orders_disputes_started = list( robot.orders_disputes_started ).append(str(order.id)) - robot.save() + robot.save(update_fields=["num_disputes", "orders_disputes_started"]) send_notification.delay(order_id=order.id, message="dispute_opened") return True, None @@ -508,8 +498,10 @@ class Logics: if order.maker == user: order.maker_statement = statement + order.save(update_fields=["maker_statement"]) else: order.taker_statement = statement + order.save(update_fields=["taker_statement"]) # If both statements are in, move status to wait for dispute resolution if order.maker_statement not in [None, ""] and order.taker_statement not in [ @@ -520,8 +512,8 @@ class Logics: order.expires_at = timezone.now() + timedelta( seconds=order.t_to_expire(Order.Status.WFR) ) + order.save(update_fields=["status", "expires_at"]) - order.save() return True, None def compute_swap_fee_rate(balance): @@ -586,20 +578,18 @@ class Logics: target_conf=config("SUGGESTED_TARGET_CONF", cast=int, default=2), )["mining_fee_rate"] - # Hardcap mining fee suggested at 300 sats/vbyte - if suggested_mining_fee_rate > 300: - suggested_mining_fee_rate = 300 + # Hardcap mining fee suggested at 1000 sats/vbyte + if suggested_mining_fee_rate > 1000: + suggested_mining_fee_rate = 1000 - onchain_payment.suggested_mining_fee_rate = max( - 2.05, LNNode.estimate_fee(amount_sats=preliminary_amount)["mining_fee_rate"] - ) + onchain_payment.suggested_mining_fee_rate = max(2.05, suggested_mining_fee_rate) onchain_payment.swap_fee_rate = cls.compute_swap_fee_rate( onchain_payment.balance ) onchain_payment.save() order.payout_tx = onchain_payment - order.save() + order.save(update_fields=["payout_tx"]) return True @classmethod @@ -747,7 +737,7 @@ class Logics: tx.save() order.is_swap = True - order.save() + order.save(update_fields=["is_swap"]) cls.move_state_updated_payout_method(order) @@ -817,7 +807,7 @@ class Logics: ) order.is_swap = False - order.save() + order.save(update_fields=["payout", "is_swap"]) cls.move_state_updated_payout_method(order) @@ -856,31 +846,11 @@ class Logics: order.status = Order.Status.PAY order.payout.status = LNPayment.Status.FLIGHT order.payout.routing_attempts = 0 - order.payout.save() + order.payout.save(update_fields=["status", "routing_attempts"]) - order.save() + order.save(update_fields=["status", "expires_at"]) return True - def add_robot_rating(robot, rating): - """adds a new rating to a user robot""" - - # TODO Unsafe, does not update ratings, it adds more ratings everytime a new rating is clicked. - robot.total_ratings += 1 - latest_ratings = robot.latest_ratings - if latest_ratings is None: - robot.latest_ratings = [rating] - robot.avg_rating = rating - - else: - latest_ratings = ast.literal_eval(latest_ratings) - latest_ratings.append(rating) - robot.latest_ratings = latest_ratings - robot.avg_rating = sum(list(map(int, latest_ratings))) / len( - latest_ratings - ) # Just an average, but it is a list of strings. Has to be converted to int. - - robot.save() - def is_penalized(user): """Checks if a user that is not participant of orders has a limit on taking or making a order""" @@ -918,7 +888,7 @@ class Logics: if order.status == Order.Status.WFB and order.maker == user: cls.cancel_bond(order.maker_bond) order.status = Order.Status.UCA - order.save() + order.save(update_fields=["status"]) return True, None # 2.a) When maker cancels after bond @@ -932,7 +902,7 @@ class Logics: # Return the maker bond (Maker gets returned the bond for cancelling public order) if cls.return_bond(order.maker_bond): order.status = Order.Status.UCA - order.save() + order.save(update_fields=["status"]) send_notification.delay( order_id=order.id, message="public_order_cancelled" ) @@ -947,7 +917,7 @@ class Logics: if cls.return_bond(order.maker_bond): cls.cancel_bond(order.taker_bond) order.status = Order.Status.UCA - order.save() + order.save(update_fields=["status"]) send_notification.delay( order_id=order.id, message="public_order_cancelled" ) @@ -981,9 +951,9 @@ class Logics: if valid: order.status = Order.Status.UCA - order.save() + order.save(update_fields=["status"]) # Reward taker with part of the maker bond - cls.add_slashed_rewards(order.maker_bond, order.taker_bond) + cls.add_slashed_rewards(order, order.maker_bond, order.taker_bond) return True, None # 4.b) When taker cancel after bond (before escrow) @@ -996,13 +966,11 @@ class Logics: # Settle the maker bond (Maker loses the bond for canceling an ongoing trade) valid = cls.settle_bond(order.taker_bond) if valid: - order.taker = None - order.payout = None - order.trade_escrow = None + taker_bond = order.taker_bond cls.publish_order(order) send_notification.delay(order_id=order.id, message="order_published") # Reward maker with part of the taker bond - cls.add_slashed_rewards(order.taker_bond, order.maker_bond) + cls.add_slashed_rewards(order, taker_bond, order.maker_bond) return True, None # 5) When trade collateral has been posted (after escrow) @@ -1026,12 +994,12 @@ class Logics: # Otherwise just make true the asked for cancel flags elif user == order.taker: order.taker_asked_cancel = True - order.save() + order.save(update_fields=["taker_asked_cancel"]) return True, None elif user == order.maker: order.maker_asked_cancel = True - order.save() + order.save(update_fields=["maker_asked_cancel"]) return True, None else: @@ -1047,7 +1015,7 @@ class Logics: cls.return_bond(order.taker_bond) cls.return_escrow(order) order.status = Order.Status.CCA - order.save() + order.save(update_fields=["status"]) send_notification.delay(order_id=order.id, message="collaborative_cancelled") return @@ -1061,7 +1029,16 @@ class Logics: order.amount = None order.last_satoshis = cls.satoshis_now(order) order.last_satoshis_time = timezone.now() - order.save() + + # clear fields in case of re-publishing after expiry + order.taker = None + order.taker_bond = None + order.trade_escrow = None + order.payout = None + order.payout_tx = None + + order.save() # update all fields + # send_notification.delay(order_id=order.id,'order_published') # too spammy return @@ -1087,16 +1064,6 @@ class Logics: return cltv_expiry_blocks - @classmethod - 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): - cls.publish_order(order) - send_notification.delay(order_id=order.id, message="order_published") - return True - return False - @classmethod def gen_maker_hold_invoice(cls, order, user): @@ -1109,13 +1076,10 @@ class Logics: # Return the previous invoice if there was one and is still unpaid if order.maker_bond: - if cls.is_maker_bond_locked(order): - return False, None - elif order.maker_bond.status == LNPayment.Status.INVGEN: - return True, { - "bond_invoice": order.maker_bond.invoice, - "bond_satoshis": order.maker_bond.num_satoshis, - } + return True, { + "bond_invoice": order.maker_bond.invoice, + "bond_satoshis": order.maker_bond.num_satoshis, + } # If there was no maker_bond object yet, generates one order.last_satoshis = cls.satoshis_now(order) @@ -1162,7 +1126,7 @@ class Logics: cltv_expiry=hold_payment["cltv_expiry"], ) - order.save() + order.save(update_fields=["last_satoshis", "last_satoshis_time", "maker_bond"]) return True, { "bond_invoice": hold_payment["invoice"], "bond_satoshis": bond_satoshis, @@ -1178,20 +1142,27 @@ class Logics: order.last_satoshis = cls.satoshis_now(order) order.last_satoshis_time = timezone.now() order.taker_bond.status = LNPayment.Status.LOCKED - order.taker_bond.save() + order.taker_bond.save(update_fields=["status"]) # With the bond confirmation the order is extended 'public_order_duration' hours order.expires_at = timezone.now() + timedelta( seconds=order.t_to_expire(Order.Status.WF2) ) order.status = Order.Status.WF2 - order.save() + order.save( + update_fields=[ + "last_satoshis", + "last_satoshis_time", + "expires_at", + "status", + ] + ) # Both users robots are added one more contract // Unsafe can add more than once. order.maker.robot.total_contracts += 1 order.taker.robot.total_contracts += 1 - order.maker.robot.save() - order.taker.robot.save() + order.maker.robot.save(update_fields=["total_contracts"]) + order.taker.robot.save(update_fields=["total_contracts"]) # Log a market tick try: @@ -1201,15 +1172,6 @@ class Logics: send_notification.delay(order_id=order.id, message="order_taken_confirmed") return True - @classmethod - 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): - cls.finalize_contract(order) - return True - return False - @classmethod def gen_taker_hold_invoice(cls, order, user): @@ -1222,13 +1184,10 @@ class Logics: # Do not gen if a taker invoice exist. Do not return if it is already locked. Return the old one if still waiting. if order.taker_bond: - if cls.is_taker_bond_locked(order): - return False, None - elif order.taker_bond.status == LNPayment.Status.INVGEN: - return True, { - "bond_invoice": order.taker_bond.invoice, - "bond_satoshis": order.taker_bond.num_satoshis, - } + return True, { + "bond_invoice": order.taker_bond.invoice, + "bond_satoshis": order.taker_bond.num_satoshis, + } # If there was no taker_bond object yet, generates one order.last_satoshis = cls.satoshis_now(order) @@ -1277,7 +1236,14 @@ class Logics: order.expires_at = timezone.now() + timedelta( seconds=order.t_to_expire(Order.Status.TAK) ) - order.save() + order.save( + update_fields=[ + "expires_at", + "last_satoshis_time", + "taker_bond", + "expires_at", + ] + ) return True, { "bond_invoice": hold_payment["invoice"], "bond_satoshis": bond_satoshis, @@ -1288,24 +1254,15 @@ class Logics: # If status is 'Waiting for both' move to Waiting for invoice if order.status == Order.Status.WF2: order.status = Order.Status.WFI + order.save(update_fields=["status"]) # If status is 'Waiting for invoice' move to Chat elif order.status == Order.Status.WFE: order.status = Order.Status.CHA order.expires_at = timezone.now() + timedelta( seconds=order.t_to_expire(Order.Status.CHA) ) + order.save(update_fields=["status", "expires_at"]) send_notification.delay(order_id=order.id, message="fiat_exchange_starts") - order.save() - - @classmethod - def is_trade_escrow_locked(cls, order): - if order.trade_escrow.status == LNPayment.Status.LOCKED: - cls.trade_escrow_received(order) - return True - elif LNNode.validate_hold_invoice_locked(order.trade_escrow): - cls.trade_escrow_received(order) - return True - return False @classmethod def gen_escrow_hold_invoice(cls, order, user): @@ -1319,14 +1276,10 @@ class Logics: # Do not gen if an escrow invoice exist. Do not return if it is already locked. Return the old one if still waiting. if order.trade_escrow: - # Check if status is INVGEN and still not expired - if cls.is_trade_escrow_locked(order): - return False, None - elif order.trade_escrow.status == LNPayment.Status.INVGEN: - return True, { - "escrow_invoice": order.trade_escrow.invoice, - "escrow_satoshis": order.trade_escrow.num_satoshis, - } + return True, { + "escrow_invoice": order.trade_escrow.invoice, + "escrow_satoshis": order.trade_escrow.num_satoshis, + } # If there was no taker_bond object yet, generate one escrow_satoshis = cls.escrow_amount(order, user)[1][ @@ -1370,7 +1323,7 @@ class Logics: cltv_expiry=hold_payment["cltv_expiry"], ) - order.save() + order.save(update_fields=["trade_escrow"]) return True, { "escrow_invoice": hold_payment["invoice"], "escrow_satoshis": escrow_satoshis, @@ -1380,21 +1333,21 @@ class Logics: """Settles the trade escrow hold invoice""" if LNNode.settle_hold_invoice(order.trade_escrow.preimage): order.trade_escrow.status = LNPayment.Status.SETLED - order.trade_escrow.save() + order.trade_escrow.save(update_fields=["status"]) return True def settle_bond(bond): """Settles the bond hold invoice""" if LNNode.settle_hold_invoice(bond.preimage): bond.status = LNPayment.Status.SETLED - bond.save() + bond.save(update_fields=["status"]) return True def return_escrow(order): """returns the trade escrow""" if LNNode.cancel_return_hold_invoice(order.trade_escrow.payment_hash): order.trade_escrow.status = LNPayment.Status.RETNED - order.trade_escrow.save() + order.trade_escrow.save(update_fields=["status"]) return True def cancel_escrow(order): @@ -1402,7 +1355,7 @@ class Logics: # Same as return escrow, but used when the invoice was never LOCKED if LNNode.cancel_return_hold_invoice(order.trade_escrow.payment_hash): order.trade_escrow.status = LNPayment.Status.CANCEL - order.trade_escrow.save() + order.trade_escrow.save(update_fields=["status"]) return True def return_bond(bond): @@ -1412,12 +1365,12 @@ class Logics: try: LNNode.cancel_return_hold_invoice(bond.payment_hash) bond.status = LNPayment.Status.RETNED - bond.save() + bond.save(update_fields=["status"]) return True except Exception as e: if "invoice already settled" in str(e): bond.status = LNPayment.Status.SETLED - bond.save() + bond.save(update_fields=["status"]) return True else: raise e @@ -1427,7 +1380,7 @@ class Logics: if order.payout_tx: order.payout_tx.status = OnchainPayment.Status.CANCE - order.payout_tx.save() + order.payout_tx.save(update_fields=["status"]) return True else: return False @@ -1440,12 +1393,12 @@ class Logics: try: LNNode.cancel_return_hold_invoice(bond.payment_hash) bond.status = LNPayment.Status.CANCEL - bond.save() + bond.save(update_fields=["status"]) return True except Exception as e: if "invoice already settled" in str(e): bond.status = LNPayment.Status.SETLED - bond.save() + bond.save(update_fields=["status"]) return True else: raise e @@ -1457,13 +1410,14 @@ class Logics: # Pay to buyer invoice if not order.is_swap: # Background process "follow_invoices" will try to pay this invoice until success - order.status = Order.Status.PAY order.payout.status = LNPayment.Status.FLIGHT - order.payout.save() - order.save() - send_notification.delay(order_id=order.id, message="trade_successful") + order.payout.save(update_fields=["status"]) + + order.status = Order.Status.PAY order.contract_finalization_time = timezone.now() - order.save() + order.save(update_fields=["status", "contract_finalization_time"]) + + send_notification.delay(order_id=order.id, message="trade_successful") return True # Pay onchain to address @@ -1472,13 +1426,14 @@ class Logics: return False else: # Add onchain payment to queue - order.status = Order.Status.SUC order.payout_tx.status = OnchainPayment.Status.QUEUE - order.payout_tx.save() - order.save() - send_notification.delay(order_id=order.id, message="trade_successful") + order.payout_tx.save(update_fields=["status"]) + + order.status = Order.Status.SUC order.contract_finalization_time = timezone.now() - order.save() + order.save(update_fields=["status", "contract_finalization_time"]) + + send_notification.delay(order_id=order.id, message="trade_successful") return True @classmethod @@ -1493,6 +1448,7 @@ class Logics: if cls.is_buyer(order, user): order.status = Order.Status.FSE order.is_fiat_sent = True + order.save(update_fields=["status", "is_fiat_sent"]) # If seller and fiat was sent, SETTLE ESCROW AND PAY BUYER INVOICE elif cls.is_seller(order, user): @@ -1515,6 +1471,7 @@ class Logics: # !!! KEY LINE - SETTLES THE TRADE ESCROW !!! if cls.settle_escrow(order): order.trade_escrow.status = LNPayment.Status.SETLED + order.trade_escrow.save(update_fields=["status"]) # Double check the escrow is settled. if LNNode.double_check_htlc_is_settled(order.trade_escrow.payment_hash): @@ -1524,6 +1481,9 @@ class Logics: # !!! KEY LINE - PAYS THE BUYER INVOICE !!! cls.pay_buyer(order) + # Computes coordinator trade revenue + cls.compute_proceeds(order) + return True, None else: @@ -1531,7 +1491,6 @@ class Logics: "bad_request": "You cannot confirm the fiat payment at this stage" } - order.save() return True, None @classmethod @@ -1551,7 +1510,7 @@ class Logics: order.status = Order.Status.CHA order.is_fiat_sent = False order.reverted_fiat_sent = True - order.save() + order.save(update_fields=["status", "is_fiat_sent", "reverted_fiat_sent"]) return True, None @@ -1569,17 +1528,18 @@ class Logics: return False, { "bad_request": "You can only pause/unpause an order that is either public or paused" } - order.save() + order.save(update_fields=["status"]) + return True, None @classmethod def rate_platform(cls, user, rating): user.robot.platform_rating = rating - user.robot.save() + user.robot.save(update_fields=["platform_rating"]) return True, None @classmethod - def add_slashed_rewards(cls, slashed_bond, staked_bond): + def add_slashed_rewards(cls, order, slashed_bond, staked_bond): """ When a bond is slashed due to overtime, rewards the user that was waiting. @@ -1603,12 +1563,16 @@ class Logics: reward = int(slashed_satoshis * reward_fraction) rewarded_robot = staked_bond.sender.robot rewarded_robot.earned_rewards += reward - rewarded_robot.save() + rewarded_robot.save(update_fields=["earned_rewards"]) if slashed_return > 100: slashed_robot = slashed_bond.sender.robot slashed_robot.earned_rewards += slashed_return - slashed_robot.save() + slashed_robot.save(update_fields=["earned_rewards"]) + + proceeds = int(slashed_satoshis * (1 - reward_fraction)) + order.proceeds += proceeds + order.save(update_fields=["proceeds"]) return @@ -1656,24 +1620,39 @@ class Logics: return False, {"bad_invoice": "Give me a new invoice"} user.robot.earned_rewards = 0 - user.robot.save() + user.robot.save(update_fields=["earned_rewards"]) # Pays the invoice. paid, failure_reason = LNNode.pay_invoice(lnpayment) if paid: user.robot.earned_rewards = 0 user.robot.claimed_rewards += num_satoshis - user.robot.save() + user.robot.save(update_fields=["earned_rewards", "claimed_rewards"]) return True, None # If fails, adds the rewards again. else: user.robot.earned_rewards = num_satoshis - user.robot.save() + user.robot.save(update_fields=["earned_rewards"]) context = {} context["bad_invoice"] = failure_reason return False, context + @classmethod + def compute_proceeds(cls, order): + """ + Computes Coordinator trade proceeds for finished orders. + """ + + if order.is_swap: + payout_sats = order.payout_tx.sent_satoshis + order.payout_tx.mining_fee + order.proceeds += int(order.trade_escrow.num_satoshis - payout_sats) + else: + payout_sats = order.payout.num_satoshis + order.payout.fee + order.proceeds += int(order.trade_escrow.num_satoshis - payout_sats) + + order.save(update_fields=["proceeds"]) + @classmethod def summarize_trade(cls, order, user): """ @@ -1750,7 +1729,7 @@ class Logics: platform_summary["contract_timestamp"] = order.last_satoshis_time if order.contract_finalization_time is None: order.contract_finalization_time = timezone.now() - order.save() + order.save(update_fields=["contract_finalization_time"]) platform_summary["contract_total_time"] = ( order.contract_finalization_time - order.last_satoshis_time ) diff --git a/api/management/commands/clean_orders.py b/api/management/commands/clean_orders.py index 61153a77..2c4b8477 100644 --- a/api/management/commands/clean_orders.py +++ b/api/management/commands/clean_orders.py @@ -17,8 +17,6 @@ class Command(BaseCommand): """Continuously checks order expiration times for 1 hour. If order has expires, it calls the logics module for expiration handling.""" - # TODO handle 'database is locked' - do_nothing = [ Order.Status.UCA, Order.Status.EXP, @@ -61,7 +59,7 @@ class Command(BaseCommand): if "unable to locate invoice" in str(e): self.stdout.write(str(e)) order.status = Order.Status.EXP - order.save() + order.save(update_fields=["status"]) debug["expired_orders"].append({idx: context}) if debug["num_expired_orders"] > 0: @@ -69,7 +67,8 @@ class Command(BaseCommand): self.stdout.write(str(debug)) def handle(self, *args, **options): - """Never mind database locked error, keep going, print them out""" + """Never mind database locked error, keep going, print them out. + Not an issue with PostgresQL""" try: self.clean_orders() except Exception as e: diff --git a/api/management/commands/follow_invoices.py b/api/management/commands/follow_invoices.py index be70b53f..ae51e84a 100644 --- a/api/management/commands/follow_invoices.py +++ b/api/management/commands/follow_invoices.py @@ -70,7 +70,7 @@ class Command(BaseCommand): # self.handle_status_change(hold_lnpayment, old_status) hold_lnpayment.status = new_status self.update_order_status(hold_lnpayment) - hold_lnpayment.save() + hold_lnpayment.save(update_fields=["status"]) # Report for debugging old = LNPayment.Status(old_status).label @@ -174,7 +174,6 @@ class Command(BaseCommand): OnchainPayment.Status.QUEUE, OnchainPayment.Status.MEMPO, ) - onchainpayment.save() else: self.stderr.write( @@ -215,9 +214,9 @@ class Command(BaseCommand): self.stderr.write( f"Weird! bond with hash {lnpayment.payment_hash} was locked, yet it is not related to any order. It will be instantly cancelled." ) - LNNode.cancel_return_hold_invoice(lnpayment.payment_hash) - lnpayment.status = LNPayment.Status.RETNED - lnpayment.save() + if LNNode.cancel_return_hold_invoice(lnpayment.payment_hash): + lnpayment.status = LNPayment.Status.RETNED + lnpayment.save(update_fields=["status"]) return except Exception as e: diff --git a/api/management/commands/telegram_watcher.py b/api/management/commands/telegram_watcher.py index c937c4eb..efc57a04 100644 --- a/api/management/commands/telegram_watcher.py +++ b/api/management/commands/telegram_watcher.py @@ -74,7 +74,13 @@ class Command(BaseCommand): ] self.telegram.welcome(robot.user) robot.telegram_enabled = True - robot.save() + robot.save( + update_fields=[ + "telegram_lang_code", + "telegram_chat_id", + "telegram_enabled", + ] + ) break except Exception: time.sleep(5) diff --git a/api/models/market_tick.py b/api/models/market_tick.py index 73352b5d..fe58189c 100644 --- a/api/models/market_tick.py +++ b/api/models/market_tick.py @@ -70,12 +70,10 @@ class MarketTick(models.Model): market_exchange_rate = float(order.currency.exchange_rate) premium = 100 * (price / market_exchange_rate - 1) - tick = MarketTick.objects.create( + MarketTick.objects.create( price=price, volume=volume, premium=premium, currency=order.currency ) - tick.save() - def __str__(self): return f"Tick: {str(self.id)[:8]}" diff --git a/api/models/order.py b/api/models/order.py index 95f54a70..a9b79bcb 100644 --- a/api/models/order.py +++ b/api/models/order.py @@ -221,6 +221,14 @@ class Order(models.Model): blank=True, ) + # coordinator proceeds (sats revenue for this order) + proceeds = models.PositiveBigIntegerField( + default=0, + null=True, + validators=[MinValueValidator(0)], + blank=True, + ) + # ratings maker_rated = models.BooleanField(default=False, null=False) taker_rated = models.BooleanField(default=False, null=False) diff --git a/api/notifications.py b/api/notifications.py index 0f666f67..2b07310c 100644 --- a/api/notifications.py +++ b/api/notifications.py @@ -22,7 +22,7 @@ class Telegram: if user.robot.telegram_token is None: user.robot.telegram_token = token_urlsafe(15) - user.robot.save() + user.robot.save(update_fields=["telegram_token"]) context["tg_token"] = user.robot.telegram_token context["tg_bot_name"] = config("TELEGRAM_BOT_NAME") @@ -54,7 +54,7 @@ class Telegram: text = f"🔔 Hey {user.username}, I will send you notifications about your RoboSats orders." self.send_message(user.robot.telegram_chat_id, text) user.robot.telegram_welcomed = True - user.robot.save() + user.robot.save(update_fields=["telegram_welcomed"]) return def order_taken_confirmed(self, order): diff --git a/api/tasks.py b/api/tasks.py index 078d1ddf..587b1204 100644 --- a/api/tasks.py +++ b/api/tasks.py @@ -61,7 +61,7 @@ def follow_send_payment(hash): lnpayment = LNPayment.objects.get(payment_hash=hash) lnpayment.last_routing_time = timezone.now() - lnpayment.save() + lnpayment.save(update_fields=["last_routing_time"]) # Default is 0ppm. Set by the user over API. Client's default is 1000 ppm. fee_limit_sat = int( diff --git a/api/views.py b/api/views.py index 8efd854d..8efb1a48 100644 --- a/api/views.py +++ b/api/views.py @@ -1102,6 +1102,6 @@ class StealthView(APIView): stealth = serializer.data.get("wantsStealth") request.user.robot.wants_stealth = stealth - request.user.robot.save() + request.user.robot.save(update_fields=["wants_stealth"]) return Response({"wantsStealth": stealth}) diff --git a/chat/views.py b/chat/views.py index 10ba9735..9f0839f1 100644 --- a/chat/views.py +++ b/chat/views.py @@ -76,7 +76,6 @@ class ChatView(viewsets.ViewSet): timezone.now() - timedelta(minutes=1) ) chatroom.maker_connected = True - chatroom.save() peer_connected = chatroom.taker_connected peer_public_key = order.taker.robot.public_key elif chatroom.taker == request.user: @@ -84,10 +83,11 @@ class ChatView(viewsets.ViewSet): timezone.now() - timedelta(minutes=1) ) chatroom.taker_connected = True - chatroom.save() peer_connected = chatroom.maker_connected peer_public_key = order.maker.robot.public_key + chatroom.save(update_fields=["maker_connected", "taker_connected"]) + messages = [] for message in queryset: d = ChatSerializer(message).data diff --git a/frontend/src/contexts/AppContext.ts b/frontend/src/contexts/AppContext.ts index 35d5126e..9c069614 100644 --- a/frontend/src/contexts/AppContext.ts +++ b/frontend/src/contexts/AppContext.ts @@ -297,8 +297,7 @@ export const useAppStore = () => { if (currentOrder) { apiClient .get(baseUrl, '/api/order/?order_id=' + currentOrder, { tokenSHA256: robot.tokenSHA256 }) - .then(orderReceived) - .catch(orderReceived); + .then(orderReceived); } };