Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
"""
Management command: import_contracts

Reads a JSON file with address/name mappings and creates TrackedContract records.
Skips contracts that already exist (by contract_id).

Usage:
python manage.py import_contracts --file contracts.json

JSON format:
{"contracts": [{"address": "C...", "name": "My Contract"}, ...]}
or a top-level list: [{"address": "C...", "name": "My Contract"}, ...]
"""
import json

from django.contrib.auth import get_user_model
from django.core.management.base import BaseCommand, CommandError

from soroscan.ingest.models import TrackedContract

User = get_user_model()


class Command(BaseCommand):
help = "Bulk import contracts from a JSON file (skips duplicates)."

def add_arguments(self, parser):
parser.add_argument("--file", required=True, help="Path to JSON file")
parser.add_argument(
"--owner",
default=None,
help="Username to assign as owner (defaults to first superuser)",
)

def handle(self, *args, **options):
file_path = options["file"]
owner_username = options["owner"]

try:
with open(file_path, encoding="utf-8") as f:
data = json.load(f)
except FileNotFoundError:
raise CommandError(f"File not found: {file_path}")
except json.JSONDecodeError as exc:
raise CommandError(f"Invalid JSON: {exc}")

contracts = data if isinstance(data, list) else data.get("contracts", [])
if not isinstance(contracts, list):
raise CommandError("JSON must be a list or an object with a 'contracts' list.")

# Resolve owner
if owner_username:
try:
owner = User.objects.get(username=owner_username)
except User.DoesNotExist:
raise CommandError(f"User '{owner_username}' not found.")
else:
owner = User.objects.filter(is_superuser=True).first()
if owner is None:
owner = User.objects.first()
if owner is None:
raise CommandError(
"No users exist. Create a user first or pass --owner."
)

created_count = 0
skipped_count = 0

for entry in contracts:
address = entry.get("address") or entry.get("contract_id", "")
name = entry.get("name", "")

if not address:
self.stderr.write(self.style.WARNING(f"Skipping entry with no address: {entry}"))
skipped_count += 1
continue

_, created = TrackedContract.objects.get_or_create(
contract_id=address,
defaults={"name": name or address, "owner": owner},
)
if created:
created_count += 1
self.stdout.write(f" Created: {address} ({name})")
else:
skipped_count += 1
self.stdout.write(f" Skipped (exists): {address}")

self.stdout.write(
self.style.SUCCESS(
f"Done. Created: {created_count}, Skipped: {skipped_count}"
)
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Generated migration for BlacklistedContract model

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
("ingest", "0040_alter_trackedcontract_contract_id"),
]

operations = [
migrations.CreateModel(
name="BlacklistedContract",
fields=[
("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")),
("contract_id", models.CharField(db_index=True, help_text="Stellar contract address to block from indexing", max_length=56, unique=True)),
("reason", models.TextField(blank=True, help_text="Optional reason for blacklisting")),
("created_at", models.DateTimeField(auto_now_add=True)),
],
options={
"ordering": ["-created_at"],
},
),
]
21 changes: 21 additions & 0 deletions django-backend/soroscan/ingest/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,27 @@ def __str__(self):
return f"{self.user} @ {self.team} ({self.role})"


class BlacklistedContract(models.Model):
"""
Contracts whose events should never be indexed (spam prevention).
"""

contract_id = models.CharField(
max_length=56,
unique=True,
db_index=True,
help_text="Stellar contract address to block from indexing",
)
reason = models.TextField(blank=True, help_text="Optional reason for blacklisting")
created_at = models.DateTimeField(auto_now_add=True)

class Meta:
ordering = ["-created_at"]

def __str__(self):
return f"Blacklisted: {self.contract_id}"


class TrackedContract(models.Model):
"""
Contracts registered for event indexing.
Expand Down
15 changes: 15 additions & 0 deletions django-backend/soroscan/ingest/tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@
OrganizationBudget,
OrganizationCostSnapshot,
WebhookDeadLetter,
BlacklistedContract,
)
from .rate_limit import check_ingest_rate
from .stellar_client import SorobanClient
Expand Down Expand Up @@ -2017,6 +2018,20 @@ def ingest_latest_events() -> int:
).inc()
continue

# Skip blacklisted contracts
if BlacklistedContract.objects.filter(contract_id=event.contract_id).exists():
logger.info(
"Skipping blacklisted contract %s",
event.contract_id,
extra={"contract_id": event.contract_id},
)
m.events_skipped_total.labels(
contract_id=_short_contract_id(event.contract_id),
network=network,
reason="blacklisted",
).inc()
continue

# Check rate limit before processing
if not check_ingest_rate(contract):
m.events_rate_limited_total.labels(
Expand Down
Loading
Loading