robosats/api/tasks.py

328 lines
10 KiB
Python
Raw Permalink Normal View History

2024-08-07 08:13:48 +00:00
from asgiref.sync import async_to_sync
from celery import shared_task
2023-05-10 19:07:28 +00:00
from celery.exceptions import SoftTimeLimitExceeded
2022-10-20 09:56:10 +00:00
@shared_task(name="users_cleansing", time_limit=600)
def users_cleansing():
2022-02-17 19:50:10 +00:00
"""
Deletes users never used 12 hours after creation
2022-02-17 19:50:10 +00:00
"""
2022-10-25 18:04:12 +00:00
from datetime import timedelta
from django.contrib.auth.models import User
from django.db.models import Q
from django.utils import timezone
2022-10-25 18:04:12 +00:00
from api.logics import Logics
2022-01-18 17:52:48 +00:00
# Users who's last login has not been in the last 6 hours
active_time_range = (timezone.now() - timedelta(hours=6), timezone.now())
queryset = User.objects.filter(~Q(last_login__range=active_time_range))
2022-02-17 19:50:10 +00:00
queryset = queryset.filter(is_staff=False) # Do not delete staff users
# And do not have an active trade, any past contract or any reward.
deleted_users = []
for user in queryset:
# Try an except, due to unknown cause for users lacking robots.
try:
2022-10-20 09:56:10 +00:00
if (
user.robot.earned_rewards > 0
or user.robot.claimed_rewards > 0
or user.robot.telegram_enabled is True
2022-10-20 09:56:10 +00:00
):
continue
if not user.robot.total_contracts == 0:
continue
valid, _, _ = Logics.validate_already_maker_or_taker(user)
if valid:
deleted_users.append(str(user))
user.delete()
except Exception:
pass
results = {
2022-02-17 19:50:10 +00:00
"num_deleted": len(deleted_users),
"deleted_users": deleted_users,
}
return results
2022-10-20 09:56:10 +00:00
2023-05-10 19:07:28 +00:00
@shared_task(name="follow_send_payment", time_limit=180, soft_time_limit=175)
def follow_send_payment(hash):
2022-02-17 19:50:10 +00:00
"""Sends sats to buyer, continuous update"""
2023-05-12 10:10:14 +00:00
from datetime import timedelta
from decouple import config
from django.utils import timezone
from api.lightning.node import LNNode
from api.models import LNPayment
if config("DEBUG_PERMISSIONED_PAYOUTS", cast=bool, default=False):
return
lnpayment = LNPayment.objects.get(payment_hash=hash)
lnpayment.last_routing_time = timezone.now()
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(
float(lnpayment.num_satoshis) * float(lnpayment.routing_budget_ppm) / 1_000_000
)
timeout_seconds = config("PAYOUT_TIMEOUT_SECONDS", cast=int, default=90)
2023-05-10 19:07:28 +00:00
try:
results = LNNode.follow_send_payment(lnpayment, fee_limit_sat, timeout_seconds)
except SoftTimeLimitExceeded:
2023-05-12 10:10:14 +00:00
# If the 175 seconds have been consumed without follow_send_payment()
# returning, we set the last routing time as 'in 10 minutes'
# so the next check happens in 10 minutes, instead of right now.
lnpayment.last_routing_time = timezone.now() + timedelta(minutes=10)
2023-05-10 19:07:28 +00:00
lnpayment.save(update_fields=["last_routing_time"])
2023-05-12 10:10:14 +00:00
print(
f"Order: {lnpayment.order_paid_LN.id} SOFT TIME LIMIT REACHED. Hash: {hash}"
)
2023-05-10 19:07:28 +00:00
results = {}
return results
2023-03-16 18:11:39 +00:00
2022-10-20 09:56:10 +00:00
@shared_task(name="send_devfund_donation", time_limit=300, soft_time_limit=295)
def send_devfund_donation(order_id, proceeds, reason):
"""Sends a fraction of order.proceeds via keysend as
donation to the RoboSats Open Source project devfund.
"""
from decouple import config
from django.contrib.auth.models import User
from api.lightning.node import LNNode
from api.models import LNPayment, Order
from api.utils import get_devfund_pubkey
target_pubkey = get_devfund_pubkey(config("NETWORK", cast=str))
order = Order.objects.get(id=order_id)
coordinator_alias = config("COORDINATOR_ALIAS", cast=str, default="NoAlias")
2024-01-22 16:28:08 +00:00
donation_fraction = min(1.0, max(0.00, config("DEVFUND", cast=float, default=0.2)))
message = f"Devfund donation; {coordinator_alias}; {order}; {donation_fraction}; {reason};"
num_satoshis = int(proceeds * donation_fraction)
routing_budget_sats = int(max(5, num_satoshis * 0.000_1))
timeout = 280
sign = False
valid, keysend_payment = LNNode.send_keysend(
target_pubkey, message, num_satoshis, routing_budget_sats, timeout, sign
)
if not valid:
return False
lnpayment = LNPayment.objects.create(
concept=LNPayment.Concepts.DEVDONAT,
type=LNPayment.Types.KEYS,
sender=User.objects.get(
username=config("ESCROW_USERNAME", cast=str, default="admin")
),
invoice=f"Target pubkey: {target_pubkey}; At: {keysend_payment['created_at']}",
routing_budget_sats=routing_budget_sats,
description=message,
num_satoshis=num_satoshis,
order_donated=order,
**keysend_payment,
)
order.log(
f"Development fund donation LNPayment({lnpayment.payment_hash},{str(lnpayment)}) was made via keysend for {num_satoshis} Sats"
)
return True
@shared_task(name="payments_cleansing", time_limit=600)
def payments_cleansing():
"""
2022-10-20 09:56:10 +00:00
Deletes cancelled payments (hodl invoices never locked) that
belong to orders expired more than 3 days ago.
2022-10-20 09:56:10 +00:00
Deletes 'cancelled' or 'create' onchain_payments
"""
from datetime import timedelta
2022-10-25 18:04:12 +00:00
from django.db.models import Q
from django.utils import timezone
2022-10-25 18:04:12 +00:00
from api.models import LNPayment, OnchainPayment
# Orders that have expired more than -3 days ago
# Usually expiry is 1 day for every finished order. So ~4 days until
# a never locked hodl invoice is removed.
finished_time = timezone.now() - timedelta(days=3)
2022-10-20 09:56:10 +00:00
queryset = LNPayment.objects.filter(
Q(status=LNPayment.Status.CANCEL),
Q(order_made__expires_at__lt=finished_time)
| Q(order_taken__expires_at__lt=finished_time),
)
# And do not have an active trade, any past contract or any reward.
deleted_lnpayments = []
for lnpayment in queryset:
# Try and except. In case some payment is already missing.
try:
name = str(lnpayment)
lnpayment.delete()
deleted_lnpayments.append(name)
except Exception:
pass
2022-10-20 09:56:10 +00:00
# same for onchain payments
2022-10-20 09:56:10 +00:00
queryset = OnchainPayment.objects.filter(
Q(status__in=[OnchainPayment.Status.CANCE, OnchainPayment.Status.CREAT]),
Q(order_paid_TX__expires_at__lt=finished_time) | Q(order_paid_TX__isnull=True),
)
# And do not have an active trade, any past contract or any reward.
deleted_onchainpayments = []
for onchainpayment in queryset:
# Try and except. In case some payment is already missing.
try:
name = str(onchainpayment)
onchainpayment.delete()
deleted_onchainpayments.append(name)
except Exception:
pass
results = {
"num_lnpayments_deleted": len(deleted_lnpayments),
"deleted_lnpayments": deleted_lnpayments,
"num_onchainpayments_deleted": len(deleted_onchainpayments),
"deleted_onchainpayments": deleted_onchainpayments,
}
return results
2022-10-20 09:56:10 +00:00
2023-05-10 19:07:28 +00:00
@shared_task(
name="cache_external_market_prices",
ignore_result=True,
time_limit=120,
soft_time_limit=115,
)
def cache_market():
2023-05-10 19:07:28 +00:00
import math
2022-10-25 18:04:12 +00:00
from django.utils import timezone
from .models import Currency
from .utils import get_exchange_rates
currency_codes = list(Currency.currency_dict.values())
2023-05-10 19:07:28 +00:00
try:
exchange_rates = get_exchange_rates(currency_codes)
2023-05-12 10:10:14 +00:00
if not exchange_rates:
return
2023-05-12 10:10:14 +00:00
results = {}
for i in range(
len(Currency.currency_dict.values())
): # currencies are indexed starting at 1 (USD)
rate = exchange_rates[i]
results[i] = {currency_codes[i], rate}
# Do not update if no new rate was found
if math.isnan(rate):
continue
# Create / Update database cached prices
currency_key = list(Currency.currency_dict.keys())[i]
Currency.objects.update_or_create(
id=int(currency_key),
currency=int(currency_key),
# if there is a Cached market prices matching that id, it updates it with defaults below
defaults={
"exchange_rate": float(rate),
"timestamp": timezone.now(),
},
)
return results
except SoftTimeLimitExceeded:
print("SOFT LIMIT REACHED. Could not fetch current external market prices.")
return
2022-10-20 09:56:10 +00:00
2024-07-01 16:00:17 +00:00
@shared_task(name="", ignore_result=True, time_limit=120)
2024-07-15 10:33:09 +00:00
def nostr_send_order_event(order_id=None):
2024-07-01 16:00:17 +00:00
if order_id:
from api.models import Order
from api.nostr import Nostr
order = Order.objects.get(id=order_id)
nostr = Nostr()
2024-08-07 08:13:48 +00:00
async_to_sync(nostr.send_order_event)(order)
2024-07-01 16:00:17 +00:00
return
2024-07-01 16:43:35 +00:00
@shared_task(name="send_notification", ignore_result=True, time_limit=120)
def send_notification(order_id=None, chat_message_id=None, message=None):
if order_id:
from api.models import Order
order = Order.objects.get(id=order_id)
elif chat_message_id:
from chat.models import Message
chat_message = Message.objects.get(id=chat_message_id)
order = chat_message.order
2022-10-20 09:56:10 +00:00
from api.notifications import Notifications
2022-10-20 09:56:10 +00:00
notifications = Notifications()
2022-10-20 09:56:10 +00:00
if message == "welcome":
notifications.welcome(order)
2022-10-20 09:56:10 +00:00
elif message == "order_expired_untaken":
notifications.order_expired_untaken(order)
2022-10-20 09:56:10 +00:00
elif message == "trade_successful":
notifications.trade_successful(order)
2022-10-20 09:56:10 +00:00
elif message == "public_order_cancelled":
notifications.public_order_cancelled(order)
2022-10-20 09:56:10 +00:00
elif message == "taker_expired_b4bond":
notifications.taker_expired_b4bond(order)
2022-10-20 09:56:10 +00:00
elif message == "order_published":
notifications.order_published(order)
2022-10-20 09:56:10 +00:00
elif message == "order_taken_confirmed":
notifications.order_taken_confirmed(order)
2022-10-20 09:56:10 +00:00
elif message == "fiat_exchange_starts":
notifications.fiat_exchange_starts(order)
2022-10-20 09:56:10 +00:00
elif message == "dispute_opened":
notifications.dispute_opened(order)
2022-10-20 09:56:10 +00:00
elif message == "collaborative_cancelled":
notifications.collaborative_cancelled(order)
elif message == "new_chat_message":
notifications.new_chat_message(order, chat_message)
elif message == "coordinator_cancelled":
notifications.coordinator_cancelled(order)
2024-06-28 13:56:58 +00:00
elif message == "dispute_closed":
notifications.dispute_closed(order)
elif message == "lightning_failed":
notifications.lightning_failed(order)
2022-10-20 09:56:10 +00:00
return