mirror of
https://github.com/RoboSats/robosats.git
synced 2025-07-21 02:03:14 +00:00
Coordinator db performance clean up (#561)
* Improve DB writes performance and concurrency. Add order coordinator proceeds field. * Fix checks on order GET inducing 400
This commit is contained in:

committed by
Reckless_Satoshi

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