import json
import os
import time
from argparse import ArgumentParser
from datetime import timedelta
from typing import Any

from django.conf import settings
from django.core.management.base import BaseCommand
from django.db import connection
from eth_account import Account
from eth_account.signers.local import LocalAccount
from eth_typing import (
    ChecksumAddress,
    HexAddress,
    HexStr,
)
from web3 import Web3
from web3.middleware import SignAndSendRawMiddlewareBuilder

EVENT_MANAGER_ADDRESS = ChecksumAddress(
    HexAddress(HexStr(os.environ["PUBLIC_EVENT_MANAGER_ADDRESS"]))
)
EVENT_MANAGER_ABI = json.loads(settings.CONTRACTS_INFO[2].read_text())["abi"]
PRIVATE_KEY = os.environ.get("BACKEND_EOA_PRIVATE_KEY")


class Command(BaseCommand):
    help = r"Closes \ cancells events via polling DB."

    def add_arguments(self, parser: ArgumentParser) -> None:
        parser.add_argument(
            "--poll-interval",
            type=int,
            help=(
                "How frequently DB should be checked. In minutes\n"
                "It should be higher then worse ETH block calculation,"
                r" otherwise double close \ cancel can happen"
            ),
            default=60,
        )

    def handle(self, *_args: list[Any], **options: Any) -> None:
        poll_interval = timedelta(minutes=options["poll_interval"])
        self.stdout.write(f"Starting reaper with {poll_interval=}")

        w3 = Web3(Web3.HTTPProvider(settings.HTTP_ETH_NODE_HOST))
        assert w3.is_connected()

        account: LocalAccount = Account.from_key(PRIVATE_KEY)
        w3.eth.default_account = account.address
        w3.middleware_onion.inject(
            SignAndSendRawMiddlewareBuilder.build(account), layer=0
        )
        self.stdout.write(f"Imported {account.address} EOA")

        contract = w3.eth.contract(abi=EVENT_MANAGER_ABI, address=EVENT_MANAGER_ADDRESS)
        self.stdout.write(f"Will interact with {EVENT_MANAGER_ADDRESS}")

        while True:
            with connection.cursor() as cursor:
                cursor.execute(
                    """
                    SELECT e.id, e.status
                    FROM events_event e
                    INNER JOIN events_eventplace p ON e.place_id = p.id
                    WHERE e.status in ('approved', 'submitted')
                    AND now()::date + interval '1 day' * p.days_before_cancel
                      >= e.start_date::date
                    AND e.tickets_bought < p.min_tickets
                    """,
                )
                to_cancel = cursor.fetchall()

            if to_cancel:
                self.stdout.write(f"Got {len(to_cancel)} events to cancel: {to_cancel}")
                for event_id, status in to_cancel:
                    tx: Any
                    match status:
                        case "approved":
                            tx = contract.functions.cancelEvent(event_id)
                        case "submitted":
                            tx = contract.functions.declineEvent(event_id)
                        case _:
                            self.stderr.write(f"unexpected event {status=}")
                            raise ValueError

                    try:
                        tx.transact()
                    except Exception as e:
                        self.stderr.write(
                            f"Failed to exeucte {tx=} {event_id=} with {e}"
                        )

            time.sleep(poll_interval.total_seconds())

Graph