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!" )
0 commit comments