Skip to content
Merged
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
8 changes: 8 additions & 0 deletions backend/spherre/indexer/config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import os

NETWORK = "starknet-sepolia"

SPHERRE_CONTRACT_ADDRESS = ""
SERVER_URL = ""
MONGO_URL = ""
Comment on lines +5 to +7
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Fail fast when required config is missing.

Leaving SPHERRE_CONTRACT_ADDRESS, SERVER_URL, and MONGO_URL as empty strings makes felt.from_hex("") crash at runtime and the runner attempt to dial an empty URL. Please plumb these values from env or config and raise a clear error if they aren’t provided before starting the indexer.

DNA_TOKEN = os.environ.get("DNA_TOKEN")
42 changes: 42 additions & 0 deletions backend/spherre/indexer/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import asyncio
from functools import wraps

import click

from spherre.indexer.config import DNA_TOKEN, MONGO_URL, SERVER_URL
from spherre.indexer.service.indexers import run_main_indexer


def async_command(f):
@wraps(f)
def wrapper(*args, **kwargs):
return asyncio.run(f(*args, **kwargs))

return wrapper


@click.group()
def cli():
pass


@cli.command()
@click.option("--server-url", default=None, help="Apibara stream url.")
@click.option("--mongo-url", default=None, help="MongoDB url.")
@click.option("--restart", is_flag=True, help="Restart indexing from the beginning.")
@async_command
async def start(server_url, mongo_url, restart):
"""Start the indexer."""
if server_url is None:
server_url = SERVER_URL
if mongo_url is None:
mongo_url = MONGO_URL

print("Starting Apibara indexer...")
print(f"Server url: {server_url}")
await run_main_indexer(
restart=restart,
server_url=server_url,
mongo_url=mongo_url,
dna_token=DNA_TOKEN,
)
Empty file.
23 changes: 23 additions & 0 deletions backend/spherre/indexer/service/event_handlers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
from typing import Callable, Dict

from spherre.indexer.service.types import (
AccountCreationEvent,
BaseEventModel,
EventEnum,
)


class EventHandlers:
@classmethod
def handle_account_creation(cls, data: AccountCreationEvent):
pass

@classmethod
def handle_token_transafer(cls):
pass
Comment on lines +10 to +17
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Implement the handlers (and fix the typo).

Both handlers are pass, so every processed event is dropped. On top of that, handle_token_transafer is misspelled, so any future call using the correct name will fail. Please implement the expected persistence/side effects now (or at least raise NotImplementedError) and correct the method name to avoid silent breakage.

🤖 Prompt for AI Agents
In backend/spherre/indexer/service/event_handlers.py around lines 4-11, the two
handlers are unimplemented and the token handler name is misspelled; change
handle_token_transafer to handle_token_transfer, give it the correct signature
(accepting the token event data), and either implement the expected
persistence/side-effects (write event data to the index/store and emit any
required downstream events) or explicitly raise NotImplementedError in both
handle_account_creation(data: AccountCreationEvent) and
handle_token_transfer(data: TokenTransferEvent) so events are not silently
dropped; also update any callers/tests to use the corrected method name.



DATA_HANDLERS: Dict[EventEnum, Callable[[BaseEventModel], None]] = {
EventEnum.ACCOUNT_CREATION: EventHandlers.handle_account_creation,
EventEnum.TOKEN_TRANSFER: EventHandlers.handle_token_transafer,
}
137 changes: 137 additions & 0 deletions backend/spherre/indexer/service/indexers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
from apibara.indexer import IndexerRunner, IndexerRunnerConfiguration, Info
from apibara.indexer.indexer import IndexerConfiguration
from apibara.protocol.proto.stream_pb2 import Cursor, DataFinality
from apibara.starknet import EventFilter, Filter, StarkNetIndexer, felt
from apibara.starknet.cursor import starknet_cursor
from apibara.starknet.proto.starknet_pb2 import Block
from loguru import logger

from spherre.indexer.config import NETWORK, SPHERRE_CONTRACT_ADDRESS
from spherre.indexer.service.event_handlers import DATA_HANDLERS
from spherre.indexer.service.types import (
EVENT_SELECTORS,
EventEnum,
)
from spherre.indexer.service.utils import DATA_TRANSFORMERS


class SpherreMainIndexer(StarkNetIndexer):
def start_account_indexer(self, address: str):
# create a new filter for the account address
account_address = felt.from_hex(address)
filter = (
EventFilter()
.with_from_address(account_address)
.with_keys([EVENT_SELECTORS.values()])
)
# add the filter to the indexer
self.update_filter(filter)
Comment on lines +19 to +28
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Convert dict values view to list.

Line 27: EVENT_SELECTORS.values() returns a dict_values view object, which may not be accepted by with_keys(). Convert to a list explicitly.

Apply this diff:

         filter = (
             EventFilter()
             .with_from_address(account_address)
-            .with_keys([EVENT_SELECTORS.values()])
+            .with_keys(list(EVENT_SELECTORS.values()))
         )
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def start_account_indexer(self, address: str):
# create a new filter for the account address
account_address = felt.from_hex(address)
filter = (
EventFilter()
.with_from_address(account_address)
.with_keys([EVENT_SELECTORS.values()])
)
# add the filter to the indexer
self.update_filter(filter)
def start_account_indexer(self, address: str):
# create a new filter for the account address
account_address = felt.from_hex(address)
filter = (
EventFilter()
.with_from_address(account_address)
.with_keys(list(EVENT_SELECTORS.values()))
)
# add the filter to the indexer
self.update_filter(filter)
🤖 Prompt for AI Agents
In backend/spherre/indexer/service/indexers.py around lines 21 to 30, the call
to with_keys currently passes EVENT_SELECTORS.values() which yields a
dict_values view that may be rejected; change it to pass a concrete list by
wrapping it with list(EVENT_SELECTORS.values()), so the filter is constructed
with a list of keys and then call update_filter as before.


def indexer_id(self) -> str:
return "spherre"

def initial_configuration(self) -> Filter:
# Return initial configuration of the indexer.
address = felt.from_hex(SPHERRE_CONTRACT_ADDRESS)
return IndexerConfiguration(
filter=Filter().add_event(
EventFilter()
.with_from_address(address)
.with_keys([EVENT_SELECTORS[EventEnum.ACCOUNT_CREATION]])
),
starting_cursor=starknet_cursor(10_000),
finality=DataFinality.DATA_STATUS_ACCEPTED,
)

async def handle_data(self, info: Info, data: Block):
# Handle one block of data
for event_with_tx in data.events:
tx_hash = felt.to_hex(event_with_tx.transaction.meta.hash)
event = event_with_tx.event
event_address = felt.to_hex(event.from_address)
logger.info("Transaction Hash:", tx_hash)

if event_address != SPHERRE_CONTRACT_ADDRESS:
event_type = event.keys
event_enum = EVENT_SELECTORS.inverse[event_type]
transformed_data = DATA_TRANSFORMERS[event_enum](event)
if transformed_data:
DATA_HANDLERS[event_enum](transformed_data)
logger.info(
(
f"Event with type '{event_type}' from address"
f"'{event_address}' handled"
)
)
else:
logger.error(
(
f"Failed to handle event of type '{event_type}'"
f"from address '{event_address}'"
)
)
else:
event_type = event.keys
if event_type == EVENT_SELECTORS[EventEnum.ACCOUNT_CREATION]:
transformed_data = DATA_TRANSFORMERS[EventEnum.ACCOUNT_CREATION](
event
)
if transformed_data:
DATA_HANDLERS[EventEnum.ACCOUNT_CREATION](transformed_data)
self.start_account_indexer(transformed_data.account_address)
logger.info(
(
"Account created for address "
f"'{transformed_data.account_address}'"
)
)
else:
account_address = felt.to_hex(event.data[0])
logger.error(
(
"Failed to handle account creation event"
f"of address '{account_address}'"
)
)

async def handle_invalidate(self, _info: Info, _cursor: Cursor):
raise ValueError("data must be finalized")


def run_in_thread(self):
pass
Comment on lines +101 to +102
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Remove unused function.

This function is not called anywhere and has no implementation. Either implement it if it's needed for the threading strategy mentioned in previous review comments, or remove it.

-def run_in_thread(self):
-    pass
-
-
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def run_in_thread(self):
pass
🤖 Prompt for AI Agents
In backend/spherre/indexer/service/indexers.py around lines 91 to 92, remove the
unused empty method run_in_thread (or implement it if your threading strategy
requires it). If no callers exist and threading behavior is not needed, delete
the def run_in_thread(self): pass definition to avoid dead code; if threading is
required, implement the method to start the indexer in a background thread
(e.g., accept necessary args, create and start a Thread/Executor, and ensure
proper lifecycle/error handling) and update any callers accordingly.



async def run_indexer(
server_url=None,
mongo_url=None,
restart=None,
dna_token=None,
indexer_cls=None,
indexer_args=None,
):
runner = IndexerRunner(
config=IndexerRunnerConfiguration(
stream_url=server_url,
storage_url=mongo_url,
token=dna_token,
),
reset_state=restart,
)

await runner.run(indexer_cls(indexer_args), ctx={"network": NETWORK})


async def run_main_indexer(
server_url=None, mongo_url=None, restart=None, dna_token=None
):
runner = IndexerRunner(
config=IndexerRunnerConfiguration(
stream_url=server_url,
storage_url=mongo_url,
token=dna_token,
),
reset_state=restart,
)

await runner.run(SpherreMainIndexer(), ctx={"network": NETWORK})
85 changes: 85 additions & 0 deletions backend/spherre/indexer/service/types.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
from enum import Enum
from typing import Any, List, get_args, get_origin

from apibara.starknet import felt
from bidict import bidict
from pydantic import BaseModel, ValidationInfo, field_validator


class EventEnum(Enum):
ACCOUNT_CREATION = 1
TOKEN_TRANSFER = 2
TRANSACTION_PROPOSAL = 3
TRANSACTION_APPROVAL = 4
TRANSACTION_EXECUTION = 5


EVENT_SELECTORS = bidict(
{
EventEnum.ACCOUNT_CREATION: felt.from_hex(
"0x347f3a34b919109f055acc8440a003ecda76b4c63c101bbc99b9d00296db557"
),
EventEnum.TOKEN_TRANSFER: felt.from_hex(
"0x99cd8bde557814842a3121e8ddfd433a539b8c9f14bf31ebf108d12e6196e9"
),
EventEnum.TRANSACTION_PROPOSAL: felt.from_hex(""),
EventEnum.TRANSACTION_APPROVAL: felt.from_hex(
"0x8fac5268bfb16142e4e1fce5f985e337db83f8b307525a0960480cd8473436"
),
EventEnum.TRANSACTION_EXECUTION: felt.from_hex(
"0x1dcde06aabdbca2f80aa51392b345d7549d7757aa855f7e37f5d335ac8243b1"
),
}
)
Comment on lines +17 to +33
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Selector initialization currently raises immediately.

felt.from_hex("") throws, so importing this module blows up before the indexer can start. Please either provide the real selector or guard optional selectors instead of hard‑coding an empty string.

🤖 Prompt for AI Agents
In backend/spherre/indexer/service/types.py around lines 17 to 33, the
EventEnum.TRANSACTION_PROPOSAL entry calls felt.from_hex("") which raises at
import time; replace the empty string with the correct selector hex if
available, or remove/omit the TRANSACTION_PROPOSAL key from EVENT_SELECTORS and
handle missing selectors elsewhere, or wrap the conversion in a guard that only
calls felt.from_hex when the string is non-empty (or uses None/Optional for
absent selectors) so importing this module no longer throws.



class BaseEventModel(BaseModel):
def __init__(self, *args, **kwargs):
if args and not kwargs:
# If only positional args provided, map to fields
field_names = list(self.model_fields.keys())
kwargs = dict(zip(field_names, args))
super().__init__(**kwargs)

@field_validator("*", mode="before") # Apply to all fields
@classmethod
def restructure_data_before_parsing(cls, v: Any, info: ValidationInfo):
field_info = cls.model_fields[info.field_name]
field_type = field_info.annotation

print(f"\n{info.field_name}:")
print(f" Annotation: {field_type}")

new_value = v

if isinstance(field_type, str):
new_value = felt.to_hex(v)
return new_value
elif isinstance(field_type, int):
new_value = felt.to_int(v)
elif get_origin(field_type):
args = get_args(field_type)
if isinstance(args, str):
new_value = [felt.to_hex(val) for val in v]
elif isinstance(args, int):
new_value = [felt.to_int(val) for val in v]

return new_value
Comment on lines +44 to +67
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Major: Fix type checking logic and remove debug prints.

The validator has two issues:

  1. Lines 50-51: Debug print statements should be removed or use logger
  2. Lines 55, 62: isinstance(field_type, str) checks if field_type is an instance of str, but field_type is a type annotation (the class str itself), not a string. This always returns False

Apply this diff to fix the type checks:

     @field_validator("*", mode="before")
     @classmethod
     def restructure_data_before_parsing(cls, v: Any, info: ValidationInfo):
         field_info = cls.model_fields[info.field_name]
         field_type = field_info.annotation

-        print(f"\n{info.field_name}:")
-        print(f"  Annotation: {field_type}")

         new_value = v

-        if isinstance(field_type, str):
+        if field_type is str:
             new_value = felt.to_hex(v)
             return new_value
-        elif isinstance(field_type, int):
+        elif field_type is int:
             new_value = felt.to_int(v)
         elif get_origin(field_type):
             args = get_args(field_type)
-            if isinstance(args, str):
+            if args and args[0] is str:
                 new_value = [felt.to_hex(val) for val in v]
-            elif isinstance(args, int):
+            elif args and args[0] is int:
                 new_value = [felt.to_int(val) for val in v]

         return new_value
🤖 Prompt for AI Agents
In backend/spherre/indexer/service/types.py around lines 44 to 67, remove the
two debug print statements and fix the type-checking logic: use "if field_type
is str" and "elif field_type is int" (or "==") to check annotations rather than
isinstance, and when handling generic types use get_origin(field_type) and
inspect get_args(field_type) for element types (compare args[0] to str or int)
before mapping felt.to_hex / felt.to_int over v; ensure you return new_value in
each branch and handle None/empty inputs safely.



class AccountCreationEvent(BaseEventModel):
account_address: str
owner: str
name: str
description: str
members: List[str]
threshold: int
deployer: int
date_deployed: int


class TokenTransferEvent(BaseEventModel):
account_address: str
from_address: str
to_address: str
amount: int
58 changes: 58 additions & 0 deletions backend/spherre/indexer/service/utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
from typing import Callable, Dict

from apibara.starknet import felt
from apibara.starknet.proto.starknet_pb2 import Event
from loguru import logger
from pydantic import ValidationError

from spherre.indexer.service.types import (
AccountCreationEvent,
BaseEventModel,
EventEnum,
)


class DataTransformer:
@classmethod
def transform_account_creation_event(cls, event: Event) -> AccountCreationEvent:
account_address = felt.to_hex(event.data[0])
owner = felt.to_hex(event.data[1])
name = felt.to_hex(event.data[2])
description = felt.to_hex(event.data[3])
members_array_length = felt.to_int(event.data[4])
members = []
for i in range(5, members_array_length + 1 + 4):
members.append(felt.to_hex(event.data[i]))
threshold = felt.to_int(event.data[i + 1])
deployer = felt.to_int(event.data[i + 2])
date_deployed = felt.to_int(event.data[i + 3])
Comment on lines +17 to +28
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Critical: Fix array parsing logic and return type.

The array parsing has critical errors:

  1. Line 24: The range range(5, members_array_length + 1 + 4) is incorrect. To read members_array_length items starting at index 5, use range(5, 5 + members_array_length)
  2. Lines 26-28: If members_array_length is 0, the loop never executes and i is undefined, causing UnboundLocalError when accessing event.data[i + 1]
  3. Line 17: Return type is AccountCreationEvent but line 52 returns None, should be Optional[AccountCreationEvent]

Apply this diff to fix the logic:

     @classmethod
-    def transform_account_creation_event(cls, event: Event) -> AccountCreationEvent:
+    def transform_account_creation_event(cls, event: Event) -> Optional[AccountCreationEvent]:
         account_address = felt.to_hex(event.data[0])
         owner = felt.to_hex(event.data[1])
         name = felt.to_hex(event.data[2])
         description = felt.to_hex(event.data[3])
         members_array_length = felt.to_int(event.data[4])
         members = []
-        for i in range(5, members_array_length + 1 + 4):
+        cursor = 5
+        for _ in range(members_array_length):
-            members.append(felt.to_hex(event.data[i]))
+            members.append(felt.to_hex(event.data[cursor]))
+            cursor += 1
-        threshold = felt.to_int(event.data[i + 1])
-        deployer = felt.to_int(event.data[i + 2])
-        date_deployed = felt.to_int(event.data[i + 3])
+        threshold = felt.to_int(event.data[cursor])
+        deployer = felt.to_int(event.data[cursor + 1])
+        date_deployed = felt.to_int(event.data[cursor + 2])

Add the import:

-from typing import Callable, Dict
+from typing import Callable, Dict, Optional
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def transform_account_creation_event(cls, event: Event) -> AccountCreationEvent:
account_address = felt.to_hex(event.data[0])
owner = felt.to_hex(event.data[1])
name = felt.to_hex(event.data[2])
description = felt.to_hex(event.data[3])
members_array_length = felt.to_int(event.data[4])
members = []
for i in range(5, members_array_length + 1 + 4):
members.append(felt.to_hex(event.data[i]))
threshold = felt.to_int(event.data[i + 1])
deployer = felt.to_int(event.data[i + 2])
date_deployed = felt.to_int(event.data[i + 3])
def transform_account_creation_event(cls, event: Event) -> Optional[AccountCreationEvent]:
account_address = felt.to_hex(event.data[0])
owner = felt.to_hex(event.data[1])
name = felt.to_hex(event.data[2])
description = felt.to_hex(event.data[3])
members_array_length = felt.to_int(event.data[4])
members = []
cursor = 5
for _ in range(members_array_length):
members.append(felt.to_hex(event.data[cursor]))
cursor += 1
threshold = felt.to_int(event.data[cursor])
deployer = felt.to_int(event.data[cursor + 1])
date_deployed = felt.to_int(event.data[cursor + 2])
🤖 Prompt for AI Agents
In backend/spherre/indexer/service/utils.py around lines 17 to 28, fix the
member-array parsing and return type: import Optional from typing; change the
function return annotation to Optional[AccountCreationEvent]; compute start=5
and end=start+members_array_length, iterate for i in range(start, end) to build
members, then set next_index = end and read threshold =
felt.to_int(event.data[next_index]), deployer =
felt.to_int(event.data[next_index + 1]), date_deployed =
felt.to_int(event.data[next_index + 2]) so no reliance on an undefined i when
members_array_length == 0.

print("Account Deployed")
print("Account Address:", account_address)
print("Owner:", owner)
print("Name:", name)
print("Description:", description)
print("Members:", members)
print("Threshold:", threshold)
print("Deployer:", deployer)
print("Date Deployed:", date_deployed)
try:
account_data = AccountCreationEvent(
account_address=account_address,
owner=owner,
name=name,
description=description,
members=members,
threshold=threshold,
deployer=deployer,
date_deployed=date_deployed,
)
except ValidationError as err:
logger.error("Error parsing account creation event data")
logger.error(err)
return None
return account_data


DATA_TRANSFORMERS: Dict[EventEnum, Callable[[Event], BaseEventModel]] = {
EventEnum.ACCOUNT_CREATION: DataTransformer.transform_account_creation_event,
}