Skip to content

Commit 1a9d25f

Browse files
authored
Merge branch 'main' into felix-adapter
2 parents 64fae8d + e0af9e3 commit 1a9d25f

6 files changed

Lines changed: 343 additions & 0 deletions

File tree

constants/summary_columns.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,17 @@ class SummaryColumn(Enum):
9393

9494
FELIX_USDE_PTS = ("felix_usde_pts", SummaryColumnType.ETHENA_PTS)
9595

96+
# Terminal Finance
97+
TERMINAL_TUSDE_PTS = (
98+
"terminal_tusde_pts",
99+
SummaryColumnType.ETHENA_PTS,
100+
)
101+
102+
TERMINAL_TERMMAX_TUSDE_PTS = (
103+
"terminal_termmax_tusde_pts",
104+
SummaryColumnType.ETHENA_PTS,
105+
)
106+
96107
def __init__(self, column_name: str, col_type: SummaryColumnType):
97108
self.column_name = column_name
98109
self.col_type = col_type

constants/terminal.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
PAGINATION_SIZE = 1000
2+
3+
PRE_DEPOSIT_START_BLOCK= 22594324
4+
5+
TUSDE_ADDRESS = "0xa01227a26a7710bc75071286539e47adb6dea417"
6+
TUSDE_DECIMALS = 18
7+
8+
TERMMAX_API_URL = "https://data-manager-api.termmax.ts.finance/v1"

integrations/integration_ids.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -540,6 +540,18 @@ class IntegrationID(Enum):
540540
# Felix
541541
FELIX_USDE = ("felix_usde", "Felix USDe", Token.USDE)
542542

543+
# Terminal Finance
544+
TERMINAL_TUSDE = (
545+
"terminal_tusde",
546+
"Terminal Finance tUSDe",
547+
Token.USDE
548+
)
549+
550+
TERMINAL_TERMMAX_TUSDE = (
551+
"terminal_termmax_tusde",
552+
"Terminal Finance tUSDe on TermMax",
553+
Token.USDE
554+
)
543555

544556
def __init__(self, column_name: str, description: str, token: Token = Token.USDE):
545557
self.column_name = column_name
Lines changed: 190 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,190 @@
1+
import json
2+
import logging
3+
from copy import deepcopy
4+
from typing import Callable, Dict, List, Optional, Set, Tuple
5+
6+
from eth_typing import ChecksumAddress
7+
from web3 import Web3
8+
from web3.contract import Contract
9+
from web3.constants import ADDRESS_ZERO
10+
11+
from constants.chains import Chain
12+
from constants.summary_columns import SummaryColumn
13+
from constants.terminal import PRE_DEPOSIT_START_BLOCK, TUSDE_ADDRESS, PAGINATION_SIZE
14+
from integrations.cached_balances_integration import CachedBalancesIntegration
15+
from integrations.integration_ids import IntegrationID
16+
from utils.web3_utils import fetch_events_logs_with_retry, w3
17+
from utils.terminal import convert_to_decimals
18+
19+
with open("abi/ERC20_abi.json") as f:
20+
ERC20_ABI = json.load(f)
21+
22+
class TerminalIntegration(
23+
CachedBalancesIntegration
24+
):
25+
def __init__(
26+
self,
27+
integration_id: IntegrationID,
28+
start_block: int,
29+
chain: Chain = Chain.ETHEREUM,
30+
summary_cols: Optional[List[SummaryColumn]] = None,
31+
reward_multiplier: int = 1,
32+
balance_multiplier: int = 1,
33+
excluded_addresses: Optional[Set[ChecksumAddress]] = None,
34+
end_block: Optional[int] = None,
35+
ethereal_multiplier: int = 0,
36+
ethereal_multiplier_func: Optional[Callable[[int, str], int]] = None,
37+
):
38+
super().__init__(
39+
integration_id,
40+
start_block,
41+
chain,
42+
summary_cols,
43+
reward_multiplier,
44+
balance_multiplier,
45+
excluded_addresses,
46+
end_block,
47+
ethereal_multiplier,
48+
ethereal_multiplier_func,
49+
)
50+
51+
def find_closest_cached_data(
52+
self, block: int, cached_data: Dict[int, Dict[ChecksumAddress, float]]
53+
) -> Tuple[int, Dict[ChecksumAddress, float]]:
54+
"""
55+
Find the closest previous block in cached data to the given block.
56+
"""
57+
if not cached_data:
58+
return PRE_DEPOSIT_START_BLOCK, {}
59+
60+
latest_cached_block = max(cached_data.keys())
61+
if latest_cached_block >= block:
62+
logging.error("Requested block is not newer than cached data")
63+
return PRE_DEPOSIT_START_BLOCK, {}
64+
65+
return latest_cached_block, deepcopy(cached_data[latest_cached_block])
66+
67+
def fetch_transfers(
68+
self, tusde: Contract, from_block: int, to_block: int
69+
):
70+
return fetch_events_logs_with_retry(
71+
f"Terminal Finance tUSDe transfers",
72+
contract_event=tusde.events.Transfer,
73+
from_block=from_block,
74+
to_block=to_block,
75+
)
76+
77+
def process_transfer(
78+
self,
79+
transfer: Dict,
80+
cached_balances: Dict[ChecksumAddress, float]
81+
):
82+
sender = transfer["args"]["from"]
83+
receiver = transfer["args"]["to"]
84+
value = transfer["args"]["value"]
85+
86+
if sender != ADDRESS_ZERO:
87+
cached_balances[sender] = cached_balances.get(sender) - convert_to_decimals(value)
88+
# If balance is negative, set it to 0 (possible due to rounding errors)
89+
if cached_balances[sender] < 0: cached_balances[sender] = 0
90+
91+
if receiver != ADDRESS_ZERO:
92+
cached_balances[receiver] = cached_balances.get(receiver, 0.0) + convert_to_decimals(value)
93+
94+
def get_block_balances(
95+
self, cached_data: Dict[int, Dict[ChecksumAddress, float]], blocks: List[int]
96+
) -> Dict[int, Dict[ChecksumAddress, float]]:
97+
"""Get user balances for specified blocks, using cached data when available.
98+
99+
Args:
100+
cached_data (Dict[int, Dict[ChecksumAddress, float]]): Dictionary mapping block numbers
101+
to user balances at that block. Used to avoid recomputing known balances.
102+
The inner dictionary maps user addresses to their token balance.
103+
blocks (List[int]): List of block numbers to get balances for.
104+
105+
Returns:
106+
Dict[int, Dict[ChecksumAddress, float]]: Dictionary mapping block numbers to user balances,
107+
where each inner dictionary maps user addresses to their token balance
108+
at that block.
109+
"""
110+
logging.info("Getting block data for Terminal Finance tUSDe")
111+
block_data: Dict[int, Dict[ChecksumAddress, float]] = {}
112+
if not blocks:
113+
logging.error("No blocks provided to get_block_balances")
114+
return block_data
115+
116+
tusde = w3.eth.contract(
117+
address=Web3.to_checksum_address(TUSDE_ADDRESS),
118+
abi=ERC20_ABI,
119+
)
120+
sorted_blocks = sorted(blocks)
121+
cached_block, cached_balances = self.find_closest_cached_data(
122+
sorted_blocks[0], cached_data
123+
)
124+
for target_block in sorted(blocks):
125+
while cached_block < target_block:
126+
to_block = min(cached_block + PAGINATION_SIZE, target_block)
127+
for transfer in self.fetch_transfers(tusde, cached_block + 1, to_block):
128+
self.process_transfer(transfer, cached_balances)
129+
cached_block = to_block
130+
131+
block_data[target_block] = deepcopy(cached_balances)
132+
133+
return block_data
134+
135+
if __name__ == "__main__":
136+
integration = TerminalIntegration(
137+
integration_id=IntegrationID.EXAMPLE,
138+
start_block=22000000,
139+
summary_cols=[SummaryColumn.TEMPLATE_PTS],
140+
chain=Chain.ETHEREUM,
141+
reward_multiplier=20,
142+
excluded_addresses={
143+
Web3.to_checksum_address("0x0000000000000000000000000000000000000000")
144+
},
145+
end_block=40000000,
146+
)
147+
148+
# Test without cached data
149+
print("Testing without cached data...")
150+
without_cached_data_output = integration.get_block_balances(
151+
cached_data={}, blocks=[22619607, 22619668, 22619716]
152+
)
153+
154+
assert without_cached_data_output == {
155+
22619607: { "0xf651032419e3a19A3f8B1A350427b94356C86Bf4": 3.0 },
156+
22619668: { "0xf651032419e3a19A3f8B1A350427b94356C86Bf4": 1.0 },
157+
22619716: {
158+
"0xf651032419e3a19A3f8B1A350427b94356C86Bf4": 1.0,
159+
"0xaFC2a7Dfa6A14a4BbAb663F0966a779458dA123C": 0.0,
160+
},
161+
}
162+
163+
# Test with cached data
164+
print("Testing with cached data...")
165+
with_cached_data_output = integration.get_block_balances(
166+
cached_data= {
167+
22619716: { "0xf651032419e3a19A3f8B1A350427b94356C86Bf4": 1.0 },
168+
22790716: {
169+
"0xf651032419e3a19A3f8B1A350427b94356C86Bf4": 1.0,
170+
"0xFD46bC7c3025a6864Fccfc9a4e781E4D0D6F3ce5": 3e-6
171+
}
172+
},
173+
blocks=[22790965, 22790912],
174+
)
175+
176+
assert with_cached_data_output == {
177+
22790912: {
178+
"0xf651032419e3a19A3f8B1A350427b94356C86Bf4": 1.0,
179+
"0xFD46bC7c3025a6864Fccfc9a4e781E4D0D6F3ce5": 3e-6,
180+
"0xfA270DE8C80d37afF947e75968F97F3ABCb39FB0": 499.8
181+
},
182+
22790965: {
183+
"0xf651032419e3a19A3f8B1A350427b94356C86Bf4": 1.0,
184+
"0xFD46bC7c3025a6864Fccfc9a4e781E4D0D6F3ce5": 3e-6,
185+
"0xfA270DE8C80d37afF947e75968F97F3ABCb39FB0": 499.8,
186+
"0x8484fBedae4E9b2e26Df44b92cD5f81B71C8150E": 542.230021
187+
}
188+
}
189+
190+
print("Tests passed!")
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
import logging
2+
import requests
3+
from typing import Callable, Dict, List, Optional, Set
4+
5+
from eth_typing import ChecksumAddress
6+
from web3 import Web3
7+
8+
from constants.chains import Chain
9+
from constants.summary_columns import SummaryColumn
10+
from constants.terminal import TERMMAX_API_URL
11+
from integrations.cached_balances_integration import CachedBalancesIntegration
12+
from integrations.integration_ids import IntegrationID
13+
from utils.terminal import convert_to_decimals
14+
15+
class TerminalTermMaxIntegration(
16+
CachedBalancesIntegration
17+
):
18+
def __init__(
19+
self,
20+
integration_id: IntegrationID,
21+
start_block: int,
22+
chain: Chain = Chain.ETHEREUM,
23+
summary_cols: Optional[List[SummaryColumn]] = None,
24+
reward_multiplier: int = 1,
25+
balance_multiplier: int = 1,
26+
excluded_addresses: Optional[Set[ChecksumAddress]] = None,
27+
end_block: Optional[int] = None,
28+
ethereal_multiplier: int = 0,
29+
ethereal_multiplier_func: Optional[Callable[[int, str], int]] = None,
30+
):
31+
super().__init__(
32+
integration_id,
33+
start_block,
34+
chain,
35+
summary_cols,
36+
reward_multiplier,
37+
balance_multiplier,
38+
excluded_addresses,
39+
end_block,
40+
ethereal_multiplier,
41+
ethereal_multiplier_func,
42+
)
43+
44+
def fetch_balances(self, block: int) -> Dict[ChecksumAddress, float]:
45+
data = requests.get(
46+
f"{TERMMAX_API_URL}/integrations/terminal/tusde_effective_balances",
47+
params={ "block_id": block }
48+
)
49+
50+
if data.status_code != 200:
51+
logging.error(f"Failed to effective balances for block {block}")
52+
return {}
53+
54+
accounts = data.json()["data"]["effective_balances"]
55+
56+
balances = {}
57+
for account in accounts:
58+
balances[account["account_address"]] = convert_to_decimals(int(account["balance"]))
59+
60+
return balances
61+
62+
def get_block_balances(
63+
self, cached_data: Dict[int, Dict[ChecksumAddress, float]], blocks: List[int]
64+
) -> Dict[int, Dict[ChecksumAddress, float]]:
65+
"""Get user balances for specified blocks, using cached data when available.
66+
67+
Args:
68+
cached_data (Dict[int, Dict[ChecksumAddress, float]]): Dictionary mapping block numbers
69+
to user balances at that block. Used to avoid recomputing known balances.
70+
The inner dictionary maps user addresses to their token balance.
71+
blocks (List[int]): List of block numbers to get balances for.
72+
73+
Returns:
74+
Dict[int, Dict[ChecksumAddress, float]]: Dictionary mapping block numbers to user balances,
75+
where each inner dictionary maps user addresses to their token balance
76+
at that block.
77+
"""
78+
logging.info("Getting block data for Terminal Finance tUSDe on TermMax...")
79+
block_data: Dict[int, Dict[ChecksumAddress, float]] = {}
80+
if not blocks:
81+
logging.error("No blocks provided to get_block_balances")
82+
return block_data
83+
84+
for target_block in sorted(blocks):
85+
block_data[target_block] = self.fetch_balances(target_block)
86+
87+
return block_data
88+
89+
if __name__ == "__main__":
90+
integration = TerminalTermMaxIntegration(
91+
integration_id=IntegrationID.EXAMPLE,
92+
start_block=22000000,
93+
summary_cols=[SummaryColumn.TEMPLATE_PTS],
94+
chain=Chain.ETHEREUM,
95+
reward_multiplier=20,
96+
excluded_addresses={
97+
Web3.to_checksum_address("0x0000000000000000000000000000000000000000")
98+
},
99+
end_block=40000000,
100+
)
101+
102+
data_output = integration.get_block_balances(
103+
cached_data={}, blocks=[22985000, 22987200]
104+
)
105+
106+
assert data_output == {
107+
22985000: {
108+
"0x03d96DC162Dc483B03ED56eF2884bBC8921F6C1A": 5.0,
109+
"0x53cfae9AF39Fa1eeD00D4402ed6cEAbB112a3724": 1000.0
110+
},
111+
22987200: {
112+
"0xD6B34b1674792D9ed63baB92cB8D0518C257c18a": 1.0,
113+
"0x53cfae9AF39Fa1eeD00D4402ed6cEAbB112a3724": 1000.0,
114+
"0x03d96DC162Dc483B03ED56eF2884bBC8921F6C1A": 5.0
115+
},
116+
}
117+
118+
print("Tests passed!")

utils/terminal.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
from constants.terminal import TUSDE_DECIMALS
2+
3+
def convert_to_decimals(value: int) -> float:
4+
return value / (10 ** TUSDE_DECIMALS)

0 commit comments

Comments
 (0)