Skip to content
Open
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
1 change: 1 addition & 0 deletions constants/chains.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,4 @@ class Chain(Enum):
SEPOLIA = "Sepolia"
TON = "Ton"
HYPEREVM = "HyperEVM"
AVALANCHE="Avalanche"
2 changes: 2 additions & 0 deletions constants/summary_columns.py
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,8 @@ class SummaryColumn(Enum):

THRUSTER_POOL_PTS = ("thruster_pool_pts", SummaryColumnType.ETHENA_PTS)

BLACKHOLE_POOL_PTS = ("blackhole_pool_pts", SummaryColumnType.ETHENA_PTS)

def __init__(self, column_name: str, col_type: SummaryColumnType):
self.column_name = column_name
self.col_type = col_type
Expand Down
151 changes: 151 additions & 0 deletions integrations/blackhole_integration.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
from typing import Dict, List, Optional, Set
import requests
from eth_typing import ChecksumAddress
from web3 import Web3

from constants.chains import Chain
from constants.summary_columns import SummaryColumn
from integrations.cached_balances_integration import CachedBalancesIntegration
from integrations.integration_ids import IntegrationID


class BlackholeIntegration(CachedBalancesIntegration):
"""Integration for tracking Blackhole LP positions (sUSDe)"""

def __init__(
self,
integration_id: IntegrationID,
start_block: int,
chain: Chain = Chain.AVALANCHE,
summary_cols: Optional[List[SummaryColumn]] = None,
reward_multiplier: int = 30,
balance_multiplier: int = 1,
excluded_addresses: Optional[Set[ChecksumAddress]] = None,
end_block: Optional[int] = None,
ethereal_multiplier: int = 0,
ethereal_multiplier_func: Optional[callable] = None,
ticker: str = "sUSDe",
token_address: str = "0x211cc4dd073734da055fbf44a2b4667d5e5fe5d2",
):
super().__init__(
integration_id,
start_block,
chain,
summary_cols,
reward_multiplier,
balance_multiplier,
excluded_addresses,
end_block,
ethereal_multiplier,
ethereal_multiplier_func,
)
# Base URL without query params
self.api_url = (
"https://api.blackhole.xyz/"
"totalPoolBalances"
)
self.ticker = ticker
self.token_address = token_address

def get_block_balances(
self, cached_data: Dict[int, Dict[ChecksumAddress, float]], blocks: List[int]
) -> Dict[int, Dict[ChecksumAddress, float]]:
"""Fetch balances for sUSDe on Blackhole by block number, with isNext pagination."""
result = {}

for block in blocks:
if block in cached_data:
print(f"[INFO] Using cached data for block {block}")
result[block] = cached_data[block]
continue

block_data: Dict[ChecksumAddress, float] = {}
page_number = 1

try:
while True:
print(f"[INFO] Fetching block={block}, page={page_number}")

response = requests.get(
self.api_url,
params={
"ticker": self.ticker,
"tokenAddress": self.token_address,
"blockNumber": block,
"pageNumber": page_number,
},
timeout=10,
)
print(f"[INFO] Response status={response.status_code}")

try:
data = response.json()
except Exception as e:
print(
f"[DEBUG] Failed to decode JSON: {e}, "
f"text={response.text[:200]}"
)
break

print(f"[INFO] Keys in response: {list(data.keys())}")

balances = {}
if "data" in data and isinstance(data["data"], dict):
balances = data["data"].get("balances", {})
print(f"[INFO] Using 'data.balances', count={len(balances)}")
is_next = data["data"].get("isNext", False)
elif "aggregatedBalances" in data:
balances = data["aggregatedBalances"]
print(
f"[INFO] Using 'aggregatedBalances', count={len(balances)}"
)
is_next = data.get("isNext", False)
else:
print("[INFO] No balances field found")
break

if not balances:
print(
f"[INFO] No balances found, breaking loop at page {page_number}"
)
break

for addr, bal in balances.items():
print(f"[INFO] addr={addr}, bal={bal}")
checksum_addr = Web3.to_checksum_address(addr)
value = float(bal)
if value > 0:
block_data[checksum_addr] = block_data.get(
checksum_addr, 0.0
) + value

if is_next:
page_number += 1
print(f"[INFO] isNext=True → Moving to next page={page_number}")
else:
print("[INFO] isNext=False → Stopping pagination")
break

except Exception as e:
print(
f"Error fetching data for block {block}, page {page_number}: {str(e)}"
)
break

result[block] = block_data

return result


if __name__ == "__main__":
# Simple test
integration = BlackholeIntegration(
integration_id=IntegrationID.BLACKHOLE_SUSDE_POOL,
start_block=69219496,
chain=Chain.AVALANCHE,
summary_cols=[SummaryColumn.BLACKHOLE_POOL_PTS],
reward_multiplier=1,
)

result = integration.get_block_balances(cached_data={}, blocks=[69219496])
print("Block balances:", result)
6 changes: 6 additions & 0 deletions integrations/integration_ids.py
Original file line number Diff line number Diff line change
Expand Up @@ -561,6 +561,12 @@ class IntegrationID(Enum):
Token.USDE,
)

BLACKHOLE_SUSDE_POOL = (
"blackhole_susde_pool",
"Blackhole sUSDe Pool",
Token.SUSDE
)

def __init__(self, column_name: str, description: str, token: Token = Token.USDE):
self.column_name = column_name
self.description = description
Expand Down