Skip to content

Commit 15b3019

Browse files
authored
Add v2 aci features (HemeraProtocol#179)
* Add v2 aci features * Add time metrics * Configure features dynamically using decorators
1 parent 9d36df3 commit 15b3019

File tree

14 files changed

+944
-510
lines changed

14 files changed

+944
-510
lines changed

api/app/address/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
from functools import wraps
2+
13
from flask_restx.namespace import Namespace
24

35
address_features_namespace = Namespace(

api/app/address/features.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
from functools import wraps
2+
3+
feature_router = {}
4+
5+
6+
class FeatureRegistry:
7+
def __init__(self):
8+
self.features = {}
9+
self.feature_list = []
10+
11+
def register(self, feature_name, subcategory):
12+
def decorator(f):
13+
if feature_name not in self.features:
14+
self.features[feature_name] = {}
15+
self.feature_list.append(feature_name)
16+
self.features[feature_name][subcategory] = f
17+
18+
@wraps(f)
19+
def wrapper(*args, **kwargs):
20+
return f(*args, **kwargs)
21+
22+
return wrapper
23+
24+
return decorator
25+
26+
27+
feature_registry = FeatureRegistry()
28+
register_feature = feature_registry.register

api/app/address/routes.py

Lines changed: 100 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,17 @@
1-
from time import time
2-
from typing import Union
1+
import logging
2+
import time
3+
from typing import Any, Dict, Optional, Union
34

45
import flask
6+
from flask import request
57
from flask_restx import Resource
68
from sqlalchemy import func
79

810
from api.app.address import address_features_namespace
11+
from api.app.address.features import feature_registry, register_feature
912
from api.app.address.models import AddressBaseProfile, ScheduledMetadata
10-
from api.app.af_ens.routes import ACIEnsCurrent, ACIEnsDetail
1113
from api.app.cache import cache
12-
from api.app.deposit_to_l2.routes import ACIDepositToL2Current, ACIDepositToL2Transactions
14+
from api.app.main import app
1315
from common.models import db
1416
from common.utils.exception_control import APIError
1517
from common.utils.format_utils import as_dict, format_to_dict
@@ -18,9 +20,10 @@
1820
get_address_deploy_contract_count,
1921
get_address_first_deploy_contract_time,
2022
)
23+
from indexer.modules.custom.deposit_to_l2.endpoint.routes import ACIDepositToL2Current, ACIDepositToL2Transactions
24+
from indexer.modules.custom.hemera_ens.endpoint.routes import ACIEnsCurrent, ACIEnsDetail
2125
from indexer.modules.custom.opensea.endpoint.routes import ACIOpenseaProfile, ACIOpenseaTransactions
2226
from indexer.modules.custom.uniswap_v3.endpoints.routes import (
23-
UniswapV3WalletLiquidityDetail,
2427
UniswapV3WalletLiquidityHolding,
2528
UniswapV3WalletTradingRecords,
2629
UniswapV3WalletTradingSummary,
@@ -32,6 +35,8 @@
3235
MAX_INTERNAL_TRANSACTION = 10000
3336
MAX_TOKEN_TRANSFER = 10000
3437

38+
logger = app.logger
39+
3540

3641
def get_address_recent_info(address: bytes, last_timestamp: int) -> dict:
3742
pass
@@ -84,14 +89,37 @@ def get(self, address):
8489
return profile, 200
8590

8691

92+
@register_feature("contract_deployer", "value")
93+
def get_contract_deployer_profile(address) -> Optional[Dict[str, Any]]:
94+
address_deploy_contract_count = get_address_deploy_contract_count(address)
95+
address_first_deploy_contract_time = get_address_first_deploy_contract_time(address)
96+
return (
97+
{
98+
"deployed_countract_count": address_deploy_contract_count,
99+
"first_deployed_time": address_first_deploy_contract_time,
100+
}
101+
if address_deploy_contract_count != 0
102+
else None
103+
)
104+
105+
106+
@register_feature("contract_deployer", "events")
107+
def get_contract_deployed_events(address, limit=5, offset=0) -> Optional[Dict[str, Any]]:
108+
count = get_address_deploy_contract_count(address)
109+
if count == 0:
110+
return None
111+
events = get_address_contract_operations(address, limit=limit, offset=offset)
112+
res = []
113+
for event in events:
114+
res.append(format_to_dict(event))
115+
return {"data": res, "total": count}
116+
117+
87118
@address_features_namespace.route("/v1/aci/<address>/contract_deployer/profile")
88119
class ACIContractDeployerProfile(Resource):
89120
def get(self, address):
90121
address = address.lower()
91-
return {
92-
"deployed_countract_count": get_address_deploy_contract_count(address),
93-
"first_deployed_time": get_address_first_deploy_contract_time(address),
94-
}
122+
return get_contract_deployer_profile(address) or {"deployed_countract_count": 0, "first_deployed_time": None}
95123

96124

97125
@address_features_namespace.route("/v1/aci/<address>/contract_deployer/events")
@@ -102,11 +130,11 @@ def get(self, address):
102130
page_size = int(flask.request.args.get("size", PAGE_SIZE))
103131
limit = page_size
104132
offset = (page_index - 1) * page_size
105-
events = get_address_contract_operations(address, limit=limit, offset=offset)
106-
res = []
107-
for event in events:
108-
res.append(format_to_dict(event))
109-
return {"data": res}
133+
134+
return (get_contract_deployed_events(address, limit=limit, offset=offset) or {"data": [], "total": 0}) | {
135+
"size": page_size,
136+
"page": page_index,
137+
}
110138

111139

112140
@address_features_namespace.route("/v1/aci/<address>/all_features")
@@ -126,7 +154,7 @@ def get(self, address):
126154
if features:
127155
feature_list = features.split(",")
128156

129-
timer = time()
157+
timer = time.time()
130158
feature_result = {}
131159

132160
if "contract_deployer" in feature_list:
@@ -178,11 +206,67 @@ def get(self, address):
178206
}
179207
)
180208

181-
print(time() - timer)
209+
print(time.time() - timer)
182210

183211
combined_result = {
184212
"address": address,
185213
"features": feature_data_list,
186214
}
187215

188216
return combined_result, 200
217+
218+
219+
@address_features_namespace.route("/v2/aci/<address>/all_features")
220+
class ACIAllFeatures(Resource):
221+
def get(self, address):
222+
address = address.lower()
223+
requested_features = request.args.get("features")
224+
225+
if requested_features:
226+
feature_list = [f for f in requested_features.split(",") if f in feature_registry.feature_list]
227+
else:
228+
feature_list = feature_registry.feature_list
229+
230+
feature_result = {}
231+
total_start_time = time.time()
232+
233+
for feature in feature_list:
234+
feature_start_time = time.time()
235+
feature_result[feature] = {}
236+
for subcategory in feature_registry.features[feature]:
237+
subcategory_start_time = time.time()
238+
try:
239+
feature_result[feature][subcategory] = feature_registry.features[feature][subcategory](address)
240+
subcategory_end_time = time.time()
241+
logger.debug(
242+
f"Feature '{feature}' subcategory '{subcategory}' execution time: {subcategory_end_time - subcategory_start_time:.4f} seconds"
243+
)
244+
except Exception as e:
245+
logger.error(f"Error in feature '{feature}' subcategory '{subcategory}': {str(e)}")
246+
feature_result[feature][subcategory] = {"error": str(e)}
247+
248+
feature_end_time = time.time()
249+
logger.debug(
250+
f"Total execution time for feature '{feature}': {feature_end_time - feature_start_time:.4f} seconds"
251+
)
252+
253+
feature_data_list = [
254+
{"id": feature_id, **subcategory_dict}
255+
for feature_id in feature_list
256+
if (
257+
subcategory_dict := {
258+
subcategory: feature_result[feature_id][subcategory]
259+
for subcategory in feature_registry.features[feature_id]
260+
if feature_result[feature_id][subcategory] is not None
261+
}
262+
)
263+
]
264+
combined_result = {
265+
"address": address,
266+
"features": feature_data_list,
267+
}
268+
269+
total_end_time = time.time()
270+
logger.debug(f"Total execution time for all features: {total_end_time - total_start_time:.4f} seconds")
271+
272+
return combined_result, 200

0 commit comments

Comments
 (0)