Skip to content

Commit 95f1896

Browse files
authored
Feature/deposit or transfer sum (HemeraProtocol#136)
* add fbtc staking feature
1 parent a2f1aaa commit 95f1896

File tree

13 files changed

+646
-0
lines changed

13 files changed

+646
-0
lines changed

api/app/api.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
from api.app.user_operation.routes import user_operation_namespace
1212
from indexer.modules.custom.merchant_moe.endpoints.routes import merchant_moe_namespace
1313
from indexer.modules.custom.opensea.endpoint.routes import opensea_namespace
14+
from indexer.modules.custom.staking_fbtc.endpoints.routes import staking_namespace
1415
from indexer.modules.custom.uniswap_v3.endpoints.routes import uniswap_v3_namespace
1516

1617
# from api.app.l2_explorer.routes import l2_explorer_namespace
@@ -23,6 +24,7 @@
2324
api.add_namespace(uniswap_v3_namespace)
2425
api.add_namespace(token_deposit_namespace)
2526
api.add_namespace(user_operation_namespace)
27+
api.add_namespace(staking_namespace)
2628
api.add_namespace(merchant_moe_namespace)
2729

2830
# api.add_namespace(l2_explorer_namespace)

indexer/modules/custom/staking_fbtc/__init__.py

Whitespace-only changes.
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
[1]
2+
STAKED_PROTOCOL_DICT = {"0x3d9bcca8bc7d438a4c5171435f41a0af5d5e6083": "pump_btc"}
3+
STAKED_TOPIC0_DICT = {"0x3d9bcca8bc7d438a4c5171435f41a0af5d5e6083": "0xebedb8b3c678666e7f36970bc8f57abf6d8fa2e828c0da91ea5b75bf68ed101a"}
4+
STAKED_ABI_DICT = {"0xebedb8b3c678666e7f36970bc8f57abf6d8fa2e828c0da91ea5b75bf68ed101a": {"anonymous": False, "inputs": [{"indexed": True, "internalType": "address", "name": "user", "type": "address"}, {"indexed": False, "internalType": "uint256", "name": "amount", "type": "uint256"}], "name": "Stake", "type": "event"}}
5+
TRANSFERRED_CONTRACTS_DICT = {"0x047d41f2544b7f63a8e991af2068a363d210d6da": "bed_rock","0x1ff7d7c0a7d8e94046708c611dec5056a9d2b823": "solv","0xab13b8eecf5aa2460841d75da5d5d861fd5b8a39":"mezo","0x19b5cc75846bf6286d599ec116536a333c4c2c14":"fuel","0x604dd02d620633ae427888d41bfd15e38483736e":"astherus","0xf047ab4c75cebf0eb9ed34ae2c186f3611aeafa6":"zircuit","0x40328669bc9e3780dfa0141dbc87450a4af6ea11":"karak"}
6+
FBTC_ADDRESS = 0xc96de26018a54d51c097160568752c4e3bd6c364
7+
[5000]
8+
STAKED_PROTOCOL_DICT = {"0xd6ab15b2458b6ec3e94ce210174d860fdbdd6b96": "pump_btc"}
9+
STAKED_TOPIC0_DICT = {"0xd6ab15b2458b6ec3e94ce210174d860fdbdd6b96": "0xebedb8b3c678666e7f36970bc8f57abf6d8fa2e828c0da91ea5b75bf68ed101a"}
10+
STAKED_ABI_DICT = {"0xebedb8b3c678666e7f36970bc8f57abf6d8fa2e828c0da91ea5b75bf68ed101a": {"anonymous": False, "inputs": [{"indexed": True, "internalType": "address", "name": "user", "type": "address"}, {"indexed": False, "internalType": "uint256", "name": "amount", "type": "uint256"}], "name": "Stake", "type": "event"}}
11+
TRANSFERRED_CONTRACTS_DICT = {"0xf9775085d726e782e83585033b58606f7731ab18": "bed_rock","0x900637b3258e6b86fe2e713fbca4510ad934ee7e": "solv","0x0a5e1fe85be84430c6eb482512046a04b25d2484":"pell","0x27272698e0962a4bdf33f70a53d6aea3fef217c4":"minterest","0x233493e9dc68e548ac27e4933a600a3a4682c0c3":"init_capital","0x7fa704e73262e5a9f48382087f69c6aba0408eaa":"init_capital"}
12+
FBTC_ADDRESS = 0xc96de26018a54d51c097160568752c4e3bd6c364
13+
MINTEREST_LEND_AND_REDEEM_TOKEN = 0x27272698e0962a4bdf33f70a53d6aea3fef217c4
14+
[56]
15+
STAKED_PROTOCOL_DICT = {"0x2b4b9047c9fea54705218388bfc7aa7bada4bb5e": "pump_btc"}
16+
STAKED_TOPIC0_DICT = {"0x2b4b9047c9fea54705218388bfc7aa7bada4bb5e": "0xebedb8b3c678666e7f36970bc8f57abf6d8fa2e828c0da91ea5b75bf68ed101a"}
17+
STAKED_ABI_DICT = {"0xebedb8b3c678666e7f36970bc8f57abf6d8fa2e828c0da91ea5b75bf68ed101a": {"anonymous": False, "inputs": [{"indexed": True, "internalType": "address", "name": "user", "type": "address"}, {"indexed": False, "internalType": "uint256", "name": "amount", "type": "uint256"}], "name": "Stake", "type": "event"}}
18+
FBTC_ADDRESS = 0xc96de26018a54d51c097160568752c4e3bd6c364
19+
TRANSFERRED_CONTRACTS_DICT = {"0x128463a60784c4d3f46c23af3f65ed859ba87974":"astherus","0x84e5c854a7ff9f49c888d69deca578d406c26800":"bed_rock"}

indexer/modules/custom/staking_fbtc/domain/__init__.py

Whitespace-only changes.
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
from dataclasses import dataclass
2+
from typing import Optional
3+
4+
from indexer.domain import Domain, FilterData
5+
6+
7+
@dataclass
8+
class StakedFBTCDetail(FilterData):
9+
vault_address: str
10+
protocol_id: str
11+
wallet_address: str
12+
amount: int
13+
changed_amount: int
14+
block_number: int
15+
block_timestamp: int
16+
17+
18+
@dataclass
19+
class StakedFBTCCurrentStatus(FilterData):
20+
vault_address: str
21+
protocol_id: str
22+
wallet_address: str
23+
amount: int
24+
changed_amount: int
25+
block_number: int
26+
block_timestamp: int
27+
28+
29+
@dataclass
30+
class TransferredFBTCDetail(FilterData):
31+
vault_address: str
32+
protocol_id: str
33+
wallet_address: str
34+
amount: int
35+
changed_amount: int
36+
block_number: int
37+
block_timestamp: int
38+
39+
40+
@dataclass
41+
class TransferredFBTCCurrentStatus(FilterData):
42+
vault_address: str
43+
protocol_id: str
44+
wallet_address: str
45+
amount: int
46+
changed_amount: int
47+
block_number: int
48+
block_timestamp: int
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
#!/usr/bin/python3
2+
# -*- coding: utf-8 -*-
3+
from flask_restx.namespace import Namespace
4+
5+
staking_namespace = Namespace("Staking Explorer", path="/", description="Staking Feature Explorer API")
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import binascii
2+
import math
3+
import re
4+
from datetime import datetime, timedelta
5+
from decimal import Decimal, getcontext
6+
from operator import or_
7+
8+
from flask import request
9+
from flask_restx import Resource
10+
from sqlalchemy import and_, asc, desc, func
11+
12+
from api.app import explorer
13+
from api.app.cache import cache
14+
from api.app.explorer import explorer_namespace
15+
from common.models import db
16+
from common.models import db as postgres_db
17+
from common.models.tokens import Tokens
18+
from common.utils.exception_control import APIError
19+
from common.utils.format_utils import as_dict, format_to_dict, format_value_for_json, row_to_dict
20+
from common.utils.web3_utils import is_eth_address
21+
from indexer.modules.custom.staking_fbtc.endpoints import staking_namespace
22+
from indexer.modules.custom.staking_fbtc.models.feature_staked_fbtc_detail_records import FeatureStakedFBTCDetailRecords
23+
24+
FBTC_ADDRESS = "0xc96de26018a54d51c097160568752c4e3bd6c364"
25+
26+
27+
@staking_namespace.route("/v1/aci/<wallet_address>/staking/current_holding")
28+
class StakingWalletHolding(Resource):
29+
def get(self, wallet_address):
30+
wallet_address = wallet_address.lower()
31+
address_bytes = bytes.fromhex(wallet_address[2:])
32+
results = (
33+
db.session.query(
34+
FeatureStakedFBTCDetailRecords.vault_address,
35+
func.max(FeatureStakedFBTCDetailRecords.protocol_id).label("protocol_id"),
36+
func.sum(FeatureStakedFBTCDetailRecords.amount).label("total_amount"),
37+
)
38+
.filter(FeatureStakedFBTCDetailRecords.wallet_address == address_bytes)
39+
.group_by(FeatureStakedFBTCDetailRecords.contract_address)
40+
.all()
41+
)
42+
43+
erc20_data = db.session.query(Tokens).filter(Tokens.address == bytes.fromhex(FBTC_ADDRESS[2:])).first()
44+
erc20_infos = {}
45+
result = []
46+
for holding in results:
47+
contract_address = "0x" + holding.vault_address.hex()
48+
total_amount = holding.total_amount
49+
protocol_id = holding.protocol_id
50+
token_amount = total_amount / (10**erc20_data.decimals)
51+
result.append(
52+
{
53+
"protocol_id": protocol_id,
54+
"vault_address": contract_address,
55+
"total_amount": str(total_amount),
56+
"token_amount": str(token_amount),
57+
}
58+
)
59+
60+
return result, 200
Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
import ast
2+
import configparser
3+
import logging
4+
import os
5+
from collections import defaultdict
6+
from typing import Any, Dict, List, Tuple
7+
8+
from indexer.domain.log import Log
9+
from indexer.executors.batch_work_executor import BatchWorkExecutor
10+
from indexer.jobs import FilterTransactionDataJob
11+
from indexer.modules.custom import common_utils
12+
from indexer.modules.custom.staking_fbtc import utils
13+
from indexer.modules.custom.staking_fbtc.domain.feature_staked_fbtc_detail import (
14+
StakedFBTCCurrentStatus,
15+
StakedFBTCDetail,
16+
)
17+
from indexer.specification.specification import TopicSpecification, TransactionFilterByLogs
18+
from indexer.utils.abi import decode_log
19+
20+
logger = logging.getLogger(__name__)
21+
22+
23+
class ExportLockedFBTCDetailJob(FilterTransactionDataJob):
24+
dependency_types = [Log]
25+
output_types = [StakedFBTCDetail, StakedFBTCCurrentStatus]
26+
able_to_reorg = True
27+
28+
def __init__(self, **kwargs):
29+
super().__init__(**kwargs)
30+
self._batch_work_executor = BatchWorkExecutor(
31+
kwargs["batch_size"],
32+
kwargs["max_workers"],
33+
job_name=self.__class__.__name__,
34+
)
35+
self._is_batch = kwargs["batch_size"] > 1
36+
self._batch_size = kwargs["batch_size"]
37+
self._max_worker = kwargs["max_workers"]
38+
self._chain_id = common_utils.get_chain_id(self._web3)
39+
self._load_config("config.ini", self._chain_id)
40+
self._service = kwargs["config"].get("db_service")
41+
self._current_holdings = utils.get_staked_fbtc_status(
42+
self._service, list(self.staked_abi_dict.keys()), int(kwargs["config"].get("start_block"))
43+
)
44+
45+
def _load_config(self, filename, chain_id):
46+
base_path = os.path.dirname(os.path.abspath(__file__))
47+
full_path = os.path.join(base_path, filename)
48+
config = configparser.ConfigParser()
49+
config.read(full_path)
50+
51+
try:
52+
staked_protocol_dict_str = config.get(str(chain_id), "STAKED_PROTOCOL_DICT")
53+
self.staked_protocol_dict = ast.literal_eval(staked_protocol_dict_str)
54+
55+
staked_topic0_dict_str = config.get(str(chain_id), "STAKED_TOPIC0_DICT")
56+
self.staked_topic0_dict = ast.literal_eval(staked_topic0_dict_str)
57+
58+
staked_abi_dict_str = config.get(str(chain_id), "STAKED_ABI_DICT")
59+
self.staked_abi_dict = ast.literal_eval(staked_abi_dict_str)
60+
61+
except (configparser.NoOptionError, configparser.NoSectionError) as e:
62+
raise ValueError(f"Missing required configuration in {filename}: {str(e)}")
63+
except (SyntaxError, ValueError) as e:
64+
raise ValueError(f"Error parsing configuration in {filename}: {str(e)}")
65+
66+
def get_filter(self):
67+
return TransactionFilterByLogs(
68+
[
69+
TopicSpecification(topics=list(self.staked_abi_dict.keys())),
70+
]
71+
)
72+
73+
def _collect(self, **kwargs):
74+
logs = self._data_buff[Log.type()]
75+
staked_details, current_status_list, current_holdings = collect_detail(
76+
logs, self._current_holdings, self.staked_abi_dict, self.staked_protocol_dict
77+
)
78+
for data in staked_details:
79+
self._collect_item(StakedFBTCDetail.type(), data)
80+
for data in current_status_list:
81+
self._collect_item(StakedFBTCCurrentStatus.type(), data)
82+
self._current_holdings = current_holdings
83+
84+
def _process(self, **kwargs):
85+
self._data_buff[StakedFBTCDetail.type()].sort(key=lambda x: x.block_number)
86+
self._data_buff[StakedFBTCCurrentStatus.type()].sort(key=lambda x: x.block_number)
87+
88+
89+
def collect_detail(
90+
logs: List[Log],
91+
current_holdings: Dict[str, Dict[str, StakedFBTCCurrentStatus]],
92+
staked_abi_dict: Dict[str, Any],
93+
staked_protocol_dict: Dict[str, str],
94+
) -> Tuple[List[StakedFBTCDetail], List[StakedFBTCCurrentStatus], Dict[str, Dict[str, StakedFBTCCurrentStatus]]]:
95+
current_status = defaultdict(
96+
lambda: defaultdict(
97+
lambda: StakedFBTCCurrentStatus(
98+
vault_address="",
99+
protocol_id="",
100+
wallet_address="",
101+
amount=0,
102+
changed_amount=0,
103+
block_number=0,
104+
block_timestamp=0,
105+
)
106+
)
107+
)
108+
for contract_address, wallet_dict in current_holdings.items():
109+
for wallet_address, status in wallet_dict.items():
110+
current_status[contract_address][wallet_address] = status
111+
112+
logs_by_address = defaultdict(lambda: defaultdict(list))
113+
for log in logs:
114+
if log.topic0 in staked_abi_dict and log.address in staked_protocol_dict:
115+
logs_by_address[log.address][log.block_number].append(log)
116+
117+
staked_details = []
118+
119+
for address, blocks in logs_by_address.items():
120+
protocol_id = staked_protocol_dict[address]
121+
122+
for block_number in sorted(blocks.keys()):
123+
block_logs = blocks[block_number]
124+
block_changes = defaultdict(int)
125+
block_timestamp = block_logs[0].block_timestamp
126+
127+
for log in block_logs:
128+
decode_staked = decode_log(staked_abi_dict[log.topic0], log)
129+
wallet_address = decode_staked["user"]
130+
amount = decode_staked["amount"]
131+
block_changes[wallet_address] += amount
132+
133+
for wallet_address, change in block_changes.items():
134+
current_amount = current_status[address][wallet_address].amount
135+
new_amount = current_amount + change
136+
137+
current_status[address][wallet_address] = StakedFBTCCurrentStatus(
138+
vault_address=address,
139+
protocol_id=protocol_id,
140+
wallet_address=wallet_address,
141+
amount=new_amount,
142+
changed_amount=change,
143+
block_number=block_number,
144+
block_timestamp=block_timestamp,
145+
)
146+
147+
staked_details.append(
148+
StakedFBTCDetail(
149+
vault_address=address,
150+
protocol_id=protocol_id,
151+
wallet_address=wallet_address,
152+
amount=new_amount,
153+
changed_amount=change,
154+
block_number=block_number,
155+
block_timestamp=block_timestamp,
156+
)
157+
)
158+
159+
current_status_list = [status for address_dict in current_status.values() for status in address_dict.values()]
160+
161+
updated_current_holdings = {}
162+
for contract_address, wallet_dict in current_status.items():
163+
updated_current_holdings[contract_address] = {}
164+
for wallet_address, status in wallet_dict.items():
165+
updated_current_holdings[contract_address][wallet_address] = status
166+
167+
return staked_details, current_status_list, updated_current_holdings

0 commit comments

Comments
 (0)