Skip to content

Commit 2e193ab

Browse files
committed
index lists on stellar
1 parent e651c61 commit 2e193ab

File tree

9 files changed

+274
-25
lines changed

9 files changed

+274
-25
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ media/
6565
# Celery beat schedule file
6666
celerybeat-schedule
6767
dump.rdb
68+
celerybeat-schedule.db
6869

6970
# Flask stuff:
7071
instance/

base/celery.py

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -25,21 +25,21 @@
2525
app.autodiscover_tasks()
2626

2727
app.conf.beat_schedule = {
28-
"update_account_statistics_every_5_minutes": {
29-
"task": "indexer_app.tasks.update_account_statistics",
30-
"schedule": crontab(minute="*/5"), # Executes every 5 minutes
31-
"options": {"queue": "beat_tasks"},
32-
},
33-
"fetch_usd_prices_every_5_minutes": {
34-
"task": "indexer_app.tasks.fetch_usd_prices",
35-
"schedule": crontab(minute="*/5"), # Executes every 5 minutes
36-
"options": {"queue": "beat_tasks"},
37-
},
38-
"update_pot_statistics_every_5_minutes": {
39-
"task": "indexer_app.tasks.update_pot_statistics",
40-
"schedule": crontab(minute="*/5"), # Executes every 5 minutes
41-
"options": {"queue": "beat_tasks"},
42-
},
28+
# "update_account_statistics_every_5_minutes": {
29+
# "task": "indexer_app.tasks.update_account_statistics",
30+
# "schedule": crontab(minute="*/5"), # Executes every 5 minutes
31+
# "options": {"queue": "beat_tasks"},
32+
# },
33+
# "fetch_usd_prices_every_5_minutes": {
34+
# "task": "indexer_app.tasks.fetch_usd_prices",
35+
# "schedule": crontab(minute="*/5"), # Executes every 5 minutes
36+
# "options": {"queue": "beat_tasks"},
37+
# },
38+
# "update_pot_statistics_every_5_minutes": {
39+
# "task": "indexer_app.tasks.update_pot_statistics",
40+
# "schedule": crontab(minute="*/5"), # Executes every 5 minutes
41+
# "options": {"queue": "beat_tasks"},
42+
# },
4343
"fetch_stellar_events_every_minute": {
4444
"task": "indexer_app.tasks.stellar_event_indexer",
4545
"schedule": crontab(minute="*/1"), # Executes every 1 minutes

base/settings.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@
7070
NADABOT_TLA = "nadabot.testnet" if ENVIRONMENT == "testnet" else ("staging.nadabot.near" if ENVIRONMENT == "dev" else "nadabot.near")
7171
STELLAR_CONTRACT_ID = os.environ.get("PL_STELLAR_CONTRACT_ID", "")
7272
STELLAR_PROJECTS_REGISTRY_CONTRACT = os.environ.get("PL_STELLAR_PROJECTS_REGISTRY_CONTRACT", "")
73+
STELLAR_LIST_CONTRACT = os.environ.get("PL_STELLAR_LIST_CONTRACT", "")
7374
NEAR_SOCIAL_CONTRACT_ADDRESS = (
7475
"v1.social08.testnet" if ENVIRONMENT == "testnet" else "social.near"
7576
)
@@ -93,7 +94,7 @@
9394

9495
STELLAR_RPC_URL = (
9596
"https://soroban-testnet.stellar.org"
96-
if ENVIRONMENT == "testnet"
97+
if ENVIRONMENT == "local"
9798
else "https://stellar-soroban-public.nodies.app"
9899
)
99100

indexer_app/tasks.py

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,13 @@
2525
from pots.models import Pot, PotApplication, PotApplicationStatus, PotPayout
2626

2727
from .logging import logger
28-
from .utils import create_or_update_round, create_round_application, create_round_payout, get_block_height, get_ledger_sequence, process_application_to_round, process_project_event, process_rounds_deposit_event, process_vote_event, save_block_height, update_application, update_approved_projects, update_ledger_sequence, update_round_payout
28+
from .utils import (
29+
create_or_update_round, create_round_application, create_round_payout,
30+
get_block_height, get_ledger_sequence, handle_stellar_list_admin_ops, handle_stellar_list_update, process_application_to_round,
31+
process_project_event, process_rounds_deposit_event, process_vote_event,
32+
save_block_height, update_application, update_approved_projects,
33+
update_ledger_sequence, update_round_payout, handle_stellar_list
34+
)
2935

3036
CURRENT_BLOCK_HEIGHT_KEY = "current_block_height"
3137

@@ -85,7 +91,7 @@ async def indexer(from_block: int, to_block: int):
8591
logger.info(
8692
f"Total time for one iteration: {iteration_end_time - fetch_start_time:.4f} seconds"
8793
)
88-
94+
8995

9096
except asyncio.TimeoutError:
9197
logger.warning("Stream stalled: no new blocks within timeout, restarting...") # raise Exception so sytemd can restart the worker
@@ -333,16 +339,22 @@ def address_to_string(obj):
333339
return obj.address
334340
raise TypeError(f"Object of type {obj.__class__.__name__} is not JSON serializable")
335341

342+
343+
344+
# Todo: Change model so thatthe event indexer saves the event and queues a task to immediately process the event,
345+
# so we don;t have a separate beat that's looping through
346+
347+
336348
@shared_task
337349
def stellar_event_indexer():
338350
server = stellar_sdk.SorobanServer(
339351
settings.STELLAR_RPC_URL
340352
)
341-
contract_ids = [settings.STELLAR_CONTRACT_ID, settings.STELLAR_PROJECTS_REGISTRY_CONTRACT]
342-
if contract_ids == ['', '']:
353+
contract_ids = [settings.STELLAR_CONTRACT_ID, settings.STELLAR_PROJECTS_REGISTRY_CONTRACT, settings.STELLAR_LIST_CONTRACT]
354+
if contract_ids == ['', '', '']:
343355
return
344356
start_sequence = get_ledger_sequence()
345-
# start_sequence = 12169
357+
# start_sequence = 668843
346358
if not start_sequence:
347359
start_sequence = 58655649
348360
jobs_logger.info(f"Ingesting Stellar events from ledger {start_sequence}... contracts: {contract_ids}")
@@ -433,6 +445,14 @@ def process_stellar_events():
433445
elif event_name == "u_pay":
434446

435447
event.processed = update_round_payout(event_data, event.transaction_hash)
448+
elif event_name == "c_list":
449+
event.processed = handle_stellar_list(event_data, event.contract_id, event.ingested_at)
450+
elif event_name == "u_list":
451+
event.processed = handle_stellar_list_update(event_data, event.contract_id, event.ingested_at)
452+
elif event_name == "c_reg":
453+
event.processed = handle_stellar_list(event_data, event.contract_id, event.transaction_hash)
454+
elif event_name == "u_adm":
455+
event.processed = handle_stellar_list_admin_ops(event_data, event.contract_id, event.ingested_at, event.transaction_hash)
436456
event.save()
437457

438458
except Exception as e:

indexer_app/utils.py

Lines changed: 142 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -906,7 +906,7 @@ async def handle_list_admin_ops(data, receiver_id, signer_id, receiptId):
906906
}
907907

908908
activity, activity_created = await Activity.objects.aupdate_or_create(
909-
type="List_Admin_Ops", defaults=activity
909+
type="Add_List_Admin", defaults=activity
910910
)
911911
except Exception as e:
912912
logger.error(f"Failed to remove list admin, Error: {e}")
@@ -1612,7 +1612,7 @@ def process_project_event(event_data, chain_id="stellar"):
16121612
project.admins.add(admin)
16131613

16141614
# Associate team members
1615-
for team_member_data in project_data['team_members']:
1615+
for team_member_data in project_data.get('team_members', []):
16161616
team_member, _ = Account.objects.get_or_create(id=team_member_data['value'])
16171617
project.team_members.add(team_member)
16181618

@@ -1888,6 +1888,146 @@ def update_round_payout(event_data, tx_hash, chain_id="stellar"):
18881888
logger.error(f"Error updating Payout. {str(e)}")
18891889
return False
18901890

1891+
1892+
1893+
1894+
def handle_stellar_list(data, contract_id, timestamp, chain_id="stellar"):
1895+
# receipt = block.receipts().filter(receiptId=receiptId)[0]
1896+
try:
1897+
logger.info("upserting involveed accts...")
1898+
1899+
owner_address = data.get('owner')
1900+
chain = Chain.objects.get(name=chain_id)
1901+
Account.objects.get_or_create(defaults={"chain":chain},id=owner_address)
1902+
1903+
1904+
logger.info(f"creating list..... {data}")
1905+
1906+
listObject = List.objects.create(
1907+
on_chain_id=data["id"],
1908+
chain=chain,
1909+
owner_id=data["owner"],
1910+
default_registration_status=data["default_registration_status"][0],
1911+
name=data["name"],
1912+
description=data["description"],
1913+
cover_image_url=data["cover_img_url"],
1914+
admin_only_registrations=data["admin_only_registrations"],
1915+
created_at=datetime.fromtimestamp(data["created_ms"] / 1000),
1916+
updated_at=datetime.fromtimestamp(data["updated_ms"] / 1000),
1917+
)
1918+
1919+
if data.get("admins"):
1920+
for admin_id in data["admins"]:
1921+
admin_object, _ = Account.objects.get_or_create(defaults={"chain":chain},
1922+
id=admin_id,
1923+
)
1924+
listObject.admins.add(admin_object)
1925+
logger.info(f"created list for chain {chain.name}.....")
1926+
return True
1927+
except Exception as e:
1928+
logger.error(f"Failed to handle new list, Error: {e}")
1929+
return False
1930+
1931+
1932+
def handle_stellar_list_update(data, contract_id, timestamp, chain_id="stellar"):
1933+
try:
1934+
logger.info(f"updating list from result..... {data}")
1935+
1936+
listObject = List.objects.filter(on_chain_id=data["id"]).update(
1937+
owner_id=data["owner"],
1938+
default_registration_status=data["default_registration_status"][0],
1939+
name=data["name"],
1940+
description=data["description"],
1941+
cover_image_url=data["cover_image_url"],
1942+
admin_only_registrations=data["admin_only_registrations"],
1943+
created_at=datetime.fromtimestamp(data["created_at"] / 1000),
1944+
updated_at=datetime.fromtimestamp(data["updated_at"] / 1000),
1945+
)
1946+
return True
1947+
except Exception as e:
1948+
logger.error(f"Failed to handle list update, Error: {e}")
1949+
return False
1950+
1951+
1952+
def handle_new_stellar_list_registration(data, contract_id, tx_hash, chain_id="stellar"):
1953+
logger.info(f"new Project data: {data}")
1954+
# Prepare data for insertion
1955+
chain = Chain.objects.get(name=chain_id)
1956+
parent_list = List.objects.get(on_chain_id=data["list_id"])
1957+
try:
1958+
project = Account.objects.get_or_create({"chain":chain, "id": data["registrant_id"]})
1959+
except Exception as e:
1960+
logger.error(f"Encountered error trying to get create acct: {e}")
1961+
1962+
logger.info(f"creating new List registration")
1963+
1964+
try:
1965+
_ = ListRegistration.objects.create(
1966+
**{
1967+
"id": data["id"],
1968+
"registrant_id": data["registrant_id"],
1969+
"list_id": parent_list.id,
1970+
"status": data["status"],
1971+
"submitted_at": datetime.fromtimestamp(data["submitted_ms"] / 1000),
1972+
"updated_at": datetime.fromtimestamp(data["updated_ms"] / 1000),
1973+
"registered_by_id": data["registered_by"],
1974+
"admin_notes": data.get("admin_notes"),
1975+
"registrant_notes": data.get("registrant_notes"),
1976+
"tx_hash": tx_hash,
1977+
}
1978+
)
1979+
except Exception as e:
1980+
logger.error(f"Encountered error trying to create list: {e}")
1981+
1982+
# Insert activity
1983+
try:
1984+
defaults = {
1985+
"signer_id": data["registered_by"],
1986+
"receiver_id": contract_id,
1987+
"timestamp": data["submitted_ms"],
1988+
"tx_hash": tx_hash,
1989+
}
1990+
1991+
activity, activity_created = Activity.objects.update_or_create(
1992+
action_result=data, type="Register", defaults=defaults
1993+
)
1994+
return True
1995+
except Exception as e:
1996+
logger.error(f"Encountered error trying to insert activity: {e}")
1997+
return False
1998+
1999+
2000+
def handle_stellar_list_admin_ops(data, contract_id, timestamp, tx_hash):
2001+
try:
2002+
round_id, admins = data[0], data[1]
2003+
logger.info(f"updating admins: {admins} for round {round_id}")
2004+
round_obj = Round.objects.get(on_chain_id=round_id) # select related?
2005+
chain = Chain.objects.get(name="stellar")
2006+
2007+
for acct in admins:
2008+
admin, _ = Account.objects.get_or_create(defaults={"chain":chain},id=acct)
2009+
contains = round_obj.admins.acontains(admin)
2010+
if not contains:
2011+
round_obj.admins.add(admin)
2012+
for admin in round_obj.admins.all():
2013+
if not admin.id in admins:
2014+
round_obj.admins.remove(admin)
2015+
2016+
activity = {
2017+
"signer_id": round_obj.owner.id,
2018+
"receiver_id": contract_id,
2019+
"timestamp": timestamp,
2020+
"tx_hash": tx_hash,
2021+
}
2022+
2023+
activity, activity_created = Activity.objects.update_or_create(
2024+
type="Add_List_Admin", defaults=activity
2025+
)
2026+
return True
2027+
except Exception as e:
2028+
logger.error(f"Failed to remove list admin, Error: {e}")
2029+
return False
2030+
18912031
# Campaign Event Indexing Methods
18922032

18932033
async def handle_new_campaign(data: dict, created_at):

lists/api.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,12 @@ class ListsListAPI(APIView, CustomSizePageNumberPagination):
4242
OpenApiParameter.QUERY,
4343
description="Filter lists by account",
4444
),
45+
OpenApiParameter(
46+
"chain",
47+
str,
48+
OpenApiParameter.QUERY,
49+
description="Filter lists by chain id",
50+
),
4551
OpenApiParameter(
4652
"admin",
4753
str,
@@ -72,6 +78,17 @@ class ListsListAPI(APIView, CustomSizePageNumberPagination):
7278
def get(self, request: Request, *args, **kwargs):
7379
lists = List.objects.all().select_related("owner").prefetch_related("admins", "upvotes").annotate(registrations_count=Count('registrations'))
7480
account_id = request.query_params.get("account")
81+
chain = request.query_params.get("chain")
82+
if chain:
83+
lists = lists.filter(chain__id=chain)
84+
if account_id:
85+
try:
86+
account = Chain.objects.get(name=account_id)
87+
lists = lists.filter(owner=account)
88+
except Account.DoesNotExist:
89+
return Response(
90+
{"message": f"Account with ID {account_id} not found."}, status=404
91+
)
7592
if account_id:
7693
try:
7794
account = Account.objects.get(id=account_id)
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# Generated by Django 5.0.6 on 2025-09-22 12:10
2+
3+
import django.db.models.deletion
4+
from django.db import migrations, models
5+
6+
7+
class Migration(migrations.Migration):
8+
9+
dependencies = [
10+
("chains", "0003_add_stellar_chain"),
11+
("lists", "0007_alter_list_cover_image_url_alter_list_description_and_more"),
12+
]
13+
14+
operations = [
15+
migrations.AddField(
16+
model_name="list",
17+
name="chain",
18+
field=models.ForeignKey(
19+
default=1,
20+
help_text="Blockchain this list was created on.",
21+
on_delete=django.db.models.deletion.CASCADE,
22+
related_name="lists",
23+
related_query_name="list",
24+
to="chains.chain",
25+
),
26+
),
27+
]
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
# Generated by Django 5.0.6 on 2025-09-23 11:08
2+
3+
from django.db import migrations, models
4+
5+
6+
class Migration(migrations.Migration):
7+
8+
dependencies = [
9+
("accounts", "0006_alter_account_near_social_profile_data"),
10+
("chains", "0003_add_stellar_chain"),
11+
("lists", "0008_list_chain"),
12+
]
13+
14+
operations = [
15+
migrations.AlterField(
16+
model_name="list",
17+
name="on_chain_id",
18+
field=models.IntegerField(
19+
help_text="List ID in contract", verbose_name="contract list ID"
20+
),
21+
),
22+
migrations.AddConstraint(
23+
model_name="list",
24+
constraint=models.UniqueConstraint(
25+
fields=("on_chain_id", "chain"), name="unique_on_chain_id_per_chain"
26+
),
27+
),
28+
]

0 commit comments

Comments
 (0)