Skip to content

Commit b777dd5

Browse files
authored
Merge pull request #193 from Spherre-Labs/feat/implement-indexer
Implement apibara indexer
2 parents afd5838 + 3ae49f8 commit b777dd5

File tree

7 files changed

+353
-0
lines changed

7 files changed

+353
-0
lines changed

backend/spherre/indexer/config.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import os
2+
3+
NETWORK = "starknet-sepolia"
4+
5+
SPHERRE_CONTRACT_ADDRESS = ""
6+
SERVER_URL = ""
7+
MONGO_URL = ""
8+
DNA_TOKEN = os.environ.get("DNA_TOKEN")

backend/spherre/indexer/main.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import asyncio
2+
from functools import wraps
3+
4+
import click
5+
6+
from spherre.indexer.config import DNA_TOKEN, MONGO_URL, SERVER_URL
7+
from spherre.indexer.service.indexers import run_main_indexer
8+
9+
10+
def async_command(f):
11+
@wraps(f)
12+
def wrapper(*args, **kwargs):
13+
return asyncio.run(f(*args, **kwargs))
14+
15+
return wrapper
16+
17+
18+
@click.group()
19+
def cli():
20+
pass
21+
22+
23+
@cli.command()
24+
@click.option("--server-url", default=None, help="Apibara stream url.")
25+
@click.option("--mongo-url", default=None, help="MongoDB url.")
26+
@click.option("--restart", is_flag=True, help="Restart indexing from the beginning.")
27+
@async_command
28+
async def start(server_url, mongo_url, restart):
29+
"""Start the indexer."""
30+
if server_url is None:
31+
server_url = SERVER_URL
32+
if mongo_url is None:
33+
mongo_url = MONGO_URL
34+
35+
print("Starting Apibara indexer...")
36+
print(f"Server url: {server_url}")
37+
await run_main_indexer(
38+
restart=restart,
39+
server_url=server_url,
40+
mongo_url=mongo_url,
41+
dna_token=DNA_TOKEN,
42+
)

backend/spherre/indexer/service/account_creation.py

Whitespace-only changes.
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
from typing import Callable, Dict
2+
3+
from spherre.indexer.service.types import (
4+
AccountCreationEvent,
5+
BaseEventModel,
6+
EventEnum,
7+
)
8+
9+
10+
class EventHandlers:
11+
@classmethod
12+
def handle_account_creation(cls, data: AccountCreationEvent):
13+
pass
14+
15+
@classmethod
16+
def handle_token_transafer(cls):
17+
pass
18+
19+
20+
DATA_HANDLERS: Dict[EventEnum, Callable[[BaseEventModel], None]] = {
21+
EventEnum.ACCOUNT_CREATION: EventHandlers.handle_account_creation,
22+
EventEnum.TOKEN_TRANSFER: EventHandlers.handle_token_transafer,
23+
}
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
from apibara.indexer import IndexerRunner, IndexerRunnerConfiguration, Info
2+
from apibara.indexer.indexer import IndexerConfiguration
3+
from apibara.protocol.proto.stream_pb2 import Cursor, DataFinality
4+
from apibara.starknet import EventFilter, Filter, StarkNetIndexer, felt
5+
from apibara.starknet.cursor import starknet_cursor
6+
from apibara.starknet.proto.starknet_pb2 import Block
7+
from loguru import logger
8+
9+
from spherre.indexer.config import NETWORK, SPHERRE_CONTRACT_ADDRESS
10+
from spherre.indexer.service.event_handlers import DATA_HANDLERS
11+
from spherre.indexer.service.types import (
12+
EVENT_SELECTORS,
13+
EventEnum,
14+
)
15+
from spherre.indexer.service.utils import DATA_TRANSFORMERS
16+
17+
18+
class SpherreMainIndexer(StarkNetIndexer):
19+
def start_account_indexer(self, address: str):
20+
# create a new filter for the account address
21+
account_address = felt.from_hex(address)
22+
filter = (
23+
EventFilter()
24+
.with_from_address(account_address)
25+
.with_keys([EVENT_SELECTORS.values()])
26+
)
27+
# add the filter to the indexer
28+
self.update_filter(filter)
29+
30+
def indexer_id(self) -> str:
31+
return "spherre"
32+
33+
def initial_configuration(self) -> Filter:
34+
# Return initial configuration of the indexer.
35+
address = felt.from_hex(SPHERRE_CONTRACT_ADDRESS)
36+
return IndexerConfiguration(
37+
filter=Filter().add_event(
38+
EventFilter()
39+
.with_from_address(address)
40+
.with_keys([EVENT_SELECTORS[EventEnum.ACCOUNT_CREATION]])
41+
),
42+
starting_cursor=starknet_cursor(10_000),
43+
finality=DataFinality.DATA_STATUS_ACCEPTED,
44+
)
45+
46+
async def handle_data(self, info: Info, data: Block):
47+
# Handle one block of data
48+
for event_with_tx in data.events:
49+
tx_hash = felt.to_hex(event_with_tx.transaction.meta.hash)
50+
event = event_with_tx.event
51+
event_address = felt.to_hex(event.from_address)
52+
logger.info("Transaction Hash:", tx_hash)
53+
54+
if event_address != SPHERRE_CONTRACT_ADDRESS:
55+
event_type = event.keys
56+
event_enum = EVENT_SELECTORS.inverse[event_type]
57+
transformed_data = DATA_TRANSFORMERS[event_enum](event)
58+
if transformed_data:
59+
DATA_HANDLERS[event_enum](transformed_data)
60+
logger.info(
61+
(
62+
f"Event with type '{event_type}' from address"
63+
f"'{event_address}' handled"
64+
)
65+
)
66+
else:
67+
logger.error(
68+
(
69+
f"Failed to handle event of type '{event_type}'"
70+
f"from address '{event_address}'"
71+
)
72+
)
73+
else:
74+
event_type = event.keys
75+
if event_type == EVENT_SELECTORS[EventEnum.ACCOUNT_CREATION]:
76+
transformed_data = DATA_TRANSFORMERS[EventEnum.ACCOUNT_CREATION](
77+
event
78+
)
79+
if transformed_data:
80+
DATA_HANDLERS[EventEnum.ACCOUNT_CREATION](transformed_data)
81+
self.start_account_indexer(transformed_data.account_address)
82+
logger.info(
83+
(
84+
"Account created for address "
85+
f"'{transformed_data.account_address}'"
86+
)
87+
)
88+
else:
89+
account_address = felt.to_hex(event.data[0])
90+
logger.error(
91+
(
92+
"Failed to handle account creation event"
93+
f"of address '{account_address}'"
94+
)
95+
)
96+
97+
async def handle_invalidate(self, _info: Info, _cursor: Cursor):
98+
raise ValueError("data must be finalized")
99+
100+
101+
def run_in_thread(self):
102+
pass
103+
104+
105+
async def run_indexer(
106+
server_url=None,
107+
mongo_url=None,
108+
restart=None,
109+
dna_token=None,
110+
indexer_cls=None,
111+
indexer_args=None,
112+
):
113+
runner = IndexerRunner(
114+
config=IndexerRunnerConfiguration(
115+
stream_url=server_url,
116+
storage_url=mongo_url,
117+
token=dna_token,
118+
),
119+
reset_state=restart,
120+
)
121+
122+
await runner.run(indexer_cls(indexer_args), ctx={"network": NETWORK})
123+
124+
125+
async def run_main_indexer(
126+
server_url=None, mongo_url=None, restart=None, dna_token=None
127+
):
128+
runner = IndexerRunner(
129+
config=IndexerRunnerConfiguration(
130+
stream_url=server_url,
131+
storage_url=mongo_url,
132+
token=dna_token,
133+
),
134+
reset_state=restart,
135+
)
136+
137+
await runner.run(SpherreMainIndexer(), ctx={"network": NETWORK})
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
from enum import Enum
2+
from typing import Any, List, get_args, get_origin
3+
4+
from apibara.starknet import felt
5+
from bidict import bidict
6+
from pydantic import BaseModel, ValidationInfo, field_validator
7+
8+
9+
class EventEnum(Enum):
10+
ACCOUNT_CREATION = 1
11+
TOKEN_TRANSFER = 2
12+
TRANSACTION_PROPOSAL = 3
13+
TRANSACTION_APPROVAL = 4
14+
TRANSACTION_EXECUTION = 5
15+
16+
17+
EVENT_SELECTORS = bidict(
18+
{
19+
EventEnum.ACCOUNT_CREATION: felt.from_hex(
20+
"0x347f3a34b919109f055acc8440a003ecda76b4c63c101bbc99b9d00296db557"
21+
),
22+
EventEnum.TOKEN_TRANSFER: felt.from_hex(
23+
"0x99cd8bde557814842a3121e8ddfd433a539b8c9f14bf31ebf108d12e6196e9"
24+
),
25+
EventEnum.TRANSACTION_PROPOSAL: felt.from_hex(""),
26+
EventEnum.TRANSACTION_APPROVAL: felt.from_hex(
27+
"0x8fac5268bfb16142e4e1fce5f985e337db83f8b307525a0960480cd8473436"
28+
),
29+
EventEnum.TRANSACTION_EXECUTION: felt.from_hex(
30+
"0x1dcde06aabdbca2f80aa51392b345d7549d7757aa855f7e37f5d335ac8243b1"
31+
),
32+
}
33+
)
34+
35+
36+
class BaseEventModel(BaseModel):
37+
def __init__(self, *args, **kwargs):
38+
if args and not kwargs:
39+
# If only positional args provided, map to fields
40+
field_names = list(self.model_fields.keys())
41+
kwargs = dict(zip(field_names, args))
42+
super().__init__(**kwargs)
43+
44+
@field_validator("*", mode="before") # Apply to all fields
45+
@classmethod
46+
def restructure_data_before_parsing(cls, v: Any, info: ValidationInfo):
47+
field_info = cls.model_fields[info.field_name]
48+
field_type = field_info.annotation
49+
50+
print(f"\n{info.field_name}:")
51+
print(f" Annotation: {field_type}")
52+
53+
new_value = v
54+
55+
if isinstance(field_type, str):
56+
new_value = felt.to_hex(v)
57+
return new_value
58+
elif isinstance(field_type, int):
59+
new_value = felt.to_int(v)
60+
elif get_origin(field_type):
61+
args = get_args(field_type)
62+
if isinstance(args, str):
63+
new_value = [felt.to_hex(val) for val in v]
64+
elif isinstance(args, int):
65+
new_value = [felt.to_int(val) for val in v]
66+
67+
return new_value
68+
69+
70+
class AccountCreationEvent(BaseEventModel):
71+
account_address: str
72+
owner: str
73+
name: str
74+
description: str
75+
members: List[str]
76+
threshold: int
77+
deployer: int
78+
date_deployed: int
79+
80+
81+
class TokenTransferEvent(BaseEventModel):
82+
account_address: str
83+
from_address: str
84+
to_address: str
85+
amount: int
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
from typing import Callable, Dict
2+
3+
from apibara.starknet import felt
4+
from apibara.starknet.proto.starknet_pb2 import Event
5+
from loguru import logger
6+
from pydantic import ValidationError
7+
8+
from spherre.indexer.service.types import (
9+
AccountCreationEvent,
10+
BaseEventModel,
11+
EventEnum,
12+
)
13+
14+
15+
class DataTransformer:
16+
@classmethod
17+
def transform_account_creation_event(cls, event: Event) -> AccountCreationEvent:
18+
account_address = felt.to_hex(event.data[0])
19+
owner = felt.to_hex(event.data[1])
20+
name = felt.to_hex(event.data[2])
21+
description = felt.to_hex(event.data[3])
22+
members_array_length = felt.to_int(event.data[4])
23+
members = []
24+
for i in range(5, members_array_length + 1 + 4):
25+
members.append(felt.to_hex(event.data[i]))
26+
threshold = felt.to_int(event.data[i + 1])
27+
deployer = felt.to_int(event.data[i + 2])
28+
date_deployed = felt.to_int(event.data[i + 3])
29+
print("Account Deployed")
30+
print("Account Address:", account_address)
31+
print("Owner:", owner)
32+
print("Name:", name)
33+
print("Description:", description)
34+
print("Members:", members)
35+
print("Threshold:", threshold)
36+
print("Deployer:", deployer)
37+
print("Date Deployed:", date_deployed)
38+
try:
39+
account_data = AccountCreationEvent(
40+
account_address=account_address,
41+
owner=owner,
42+
name=name,
43+
description=description,
44+
members=members,
45+
threshold=threshold,
46+
deployer=deployer,
47+
date_deployed=date_deployed,
48+
)
49+
except ValidationError as err:
50+
logger.error("Error parsing account creation event data")
51+
logger.error(err)
52+
return None
53+
return account_data
54+
55+
56+
DATA_TRANSFORMERS: Dict[EventEnum, Callable[[Event], BaseEventModel]] = {
57+
EventEnum.ACCOUNT_CREATION: DataTransformer.transform_account_creation_event,
58+
}

0 commit comments

Comments
 (0)