11import os
22import json
3+ import csv
34from dataclasses import dataclass
4- from typing import Dict , List , Tuple
5+ from typing import Dict , List , Tuple , Union
56
67from web3 import Web3
78from web3 .types import BlockIdentifier
1011from abis import lt_abi # local file in this folder
1112from decimal import Decimal , getcontext
1213
13- # increase decimal precision for exact scaling and sums
14+ # Increase decimal precision for exact scaling and sums
1415getcontext ().prec = 60
1516
1617
1718# --------------------------- Configuration ---------------------------------
1819
19- # LT vaults per pool name
20+ # LT vaults per pool name (same as in all_users_check.py)
2021LT_POOLS : Dict [str , str ] = {
2122 "yb_cbBTC" : "0xD6a1147666f6E4d7161caf436d9923D44d901112" ,
2223 "yb_wBTC" : "0x6095a220C5567360d459462A25b1AD5aEAD45204" ,
2324 "yb_tBTC" : "0x2B513eBe7070Cff91cf699a0BFe5075020C732FF" ,
2425}
2526
27+ # Token decimals per LT pool
28+ DECIMALS : Dict [str , int ] = {
29+ "yb_cbBTC" : 8 ,
30+ "yb_wBTC" : 8 ,
31+ "yb_tBTC" : 18 ,
32+ }
33+
34+ # Default snapshot block (override with env LT_SNAPSHOT_BLOCK)
35+ SNAPSHOT_BLOCK = int (os .environ .get ("LT_SNAPSHOT_BLOCK" , 23550167 )) # 22pm utc
36+
2637# Default scan start block (override with env LT_START_BLOCK or LT_START_BLOCK_<POOL>)
2738DEFAULT_START_BLOCK = int (os .environ .get ("LT_START_BLOCK" , 23_434_000 ))
2839
29- # Multicall options (mirrors fetch_data_events.py style, simplified )
40+ # Multicall options (mirrors style in other scripts )
3041MULTICALL_OPTIONS = {
3142 "provider_url" : os .environ .get ("WEB3_PROVIDER_URL" , "" ),
3243 "batch" : 100 ,
@@ -66,14 +77,6 @@ def get_pool_start_block(pool_name: str) -> int:
6677 return int (os .environ .get (env_key , DEFAULT_START_BLOCK ))
6778
6879
69- # Token decimals per LT pool
70- DECIMALS : Dict [str , int ] = {
71- "yb_cbBTC" : 8 ,
72- "yb_wBTC" : 8 ,
73- "yb_tBTC" : 18 ,
74- }
75-
76-
7780def scale_amount (raw : int , decimals : int ) -> Decimal :
7881 # high precision division
7982 return Decimal (raw ) / (Decimal (10 ) ** decimals )
@@ -97,18 +100,21 @@ def gather_user_aggregates(
97100 start_block : int ,
98101 to_block : BlockIdentifier ,
99102) -> Tuple [Dict [str , UserAgg ], int ]:
100- """Return mapping owner->UserAgg and latest scanned block (int)."""
103+ """Return mapping owner->UserAgg and latest scanned (int) end block.
104+
105+ Scans Deposit and Withdraw events from start_block..to_block (inclusive).
106+ """
101107 contract = w3 .eth .contract (address = checksum (pool_addr ), abi = json .loads (lt_abi ))
102108
103- latest_block = w3 .eth .get_block ("latest" )["number" ] if to_block == "latest" else int (to_block ) # type: ignore[arg-type]
109+ end_block = w3 .eth .get_block ("latest" )["number" ] if to_block == "latest" else int (to_block )
104110 start_block = int (start_block )
105111 decimals = DECIMALS .get (pool_name , 18 )
106112
107113 dep_logs = fetch_event_logs_chunked (
108- contract .events .Deposit (), start_block , latest_block , LOG_CHUNK
114+ contract .events .Deposit (), start_block , end_block , LOG_CHUNK
109115 )
110116 wd_logs = fetch_event_logs_chunked (
111- contract .events .Withdraw (), start_block , latest_block , LOG_CHUNK
117+ contract .events .Withdraw (), start_block , end_block , LOG_CHUNK
112118 )
113119
114120 aggs : Dict [str , UserAgg ] = {}
@@ -129,18 +135,29 @@ def gather_user_aggregates(
129135 a .withdraws_assets_norm += scale_amount (int (ev ["args" ].get ("assets" , 0 )), decimals )
130136 a .shares_out += int (ev ["args" ].get ("shares" , 0 ))
131137
132- return aggs , latest_block
138+ return aggs , end_block
133139
134140
135- def preview_withdraw_many (
136- w3 : Web3 , pool_addr : str , shares_by_owner : Dict [str , int ]
141+ def preview_withdraw_many_at (
142+ w3 : Web3 ,
143+ pool_addr : str ,
144+ shares_by_owner : Dict [str , int ],
145+ block_id : Union [int , str ],
137146) -> Dict [str , int ]:
138- """Use multicall to preview_withdraw for each owner with net shares > 0."""
147+ """Use multicall to preview_withdraw for each owner at a specific block.
148+
149+ Returns: mapping owner -> preview_withdraw result (raw int asset amount).
150+ """
151+ if not shares_by_owner :
152+ return {}
153+
139154 contract = w3 .eth .contract (address = checksum (pool_addr ), abi = json .loads (lt_abi ))
140155 multicall = Multicall (** MULTICALL_OPTIONS )
141156
142157 calls , addresses , owners = [], [], []
143158 for owner , shares in shares_by_owner .items ():
159+ if int (shares ) <= 0 :
160+ continue
144161 calls .append (contract .functions .preview_withdraw (int (shares )))
145162 addresses .append (contract .address )
146163 owners .append (owner )
@@ -149,12 +166,11 @@ def preview_withdraw_many(
149166 return {}
150167
151168 results = multicall .aggregate (
152- calls , use_try = True , addresses = addresses , block_identifier = "latest"
169+ calls , use_try = True , addresses = addresses , block_identifier = block_id
153170 )
154171
155172 out : Dict [str , int ] = {}
156173 for owner , value in zip (owners , results ):
157- # value may be None if call reverted; treat as 0
158174 try :
159175 out [owner ] = int (value ) if value is not None else 0
160176 except Exception :
@@ -167,96 +183,131 @@ def main():
167183 abi = json .loads (lt_abi )
168184 print (f"Loaded LT ABI with events: { [e ['name' ] for e in abi if e .get ('type' )== 'event' ]} " )
169185
170- all_rows : List [Dict ] = []
186+ snapshot_block = SNAPSHOT_BLOCK
187+ print (f"Snapshot block: { snapshot_block } " )
188+
189+ rows : List [Dict [str , str ]] = []
171190
172191 for pool_name , addr in LT_POOLS .items ():
173192 start_block = get_pool_start_block (pool_name )
174- print (f"\n Pool { pool_name } @ { checksum (addr )} | scanning from block { start_block } ..." )
193+ print (f"\n Pool { pool_name } @ { checksum (addr )} | scanning from block { start_block } " )
194+
195+ # 1) Aggregates up to snapshot
196+ aggs_then , end_then = gather_user_aggregates (
197+ w3 , pool_name , addr , start_block , to_block = snapshot_block
198+ )
199+ owners_then = {
200+ owner : agg .net_shares () for owner , agg in aggs_then .items () if agg .net_shares () > 0
201+ }
202+ print (f" Snapshot owners with shares > 0 at { snapshot_block } : { len (owners_then )} " )
203+
204+ # 2) Aggregates up to latest
205+ aggs_now , end_now = gather_user_aggregates (
206+ w3 , pool_name , addr , start_block , to_block = "latest"
207+ )
208+ owners_now = {
209+ owner : agg .net_shares () for owner , agg in aggs_now .items () if agg .net_shares () > 0
210+ }
211+ print (f" Latest owners with shares > 0 at { end_now } : { len (owners_now )} " )
175212
176- aggs , latest = gather_user_aggregates ( w3 , pool_name , addr , start_block , to_block = "latest" )
177- print ( f" Found { len ( aggs ) } unique owners with activity (latest block { latest } )." )
213+ # 3) Preview withdraw amounts at snapshot for snapshot owners
214+ preview_then_raw = preview_withdraw_many_at ( w3 , addr , owners_then , block_id = snapshot_block )
178215
179- shares_by_owner = {owner : a .net_shares () for owner , a in aggs .items () if a .net_shares () > 0 }
180- print (f" Previewing withdraw for { len (shares_by_owner )} owners with net shares > 0 ..." )
216+ # 4) Preview withdraw amounts now for the SAME owner set, using current shares
217+ owners_now_subset = {owner : owners_now .get (owner , 0 ) for owner in owners_then .keys ()}
218+ preview_now_raw = preview_withdraw_many_at (w3 , addr , owners_now_subset , block_id = "latest" )
181219
182- est_assets_now = preview_withdraw_many (w3 , addr , shares_by_owner )
183220 decimals = DECIMALS .get (pool_name , 18 )
184221 factor = Decimal (10 ) ** decimals
185222
186- # Build rows
187- for owner , agg in aggs .items ():
188- net_shares = agg .net_shares ()
189- unrealized_assets_norm = (
190- Decimal (int (est_assets_now .get (owner , 0 ))) / factor
191- if net_shares > 0
223+ # 5) Build rows per snapshot owner with requested metrics
224+ for owner in owners_then .keys ():
225+ agg_now = aggs_now .get (owner , UserAgg ())
226+ assets_then = int (preview_then_raw .get (owner , 0 ))
227+ assets_now = int (preview_now_raw .get (owner , 0 ))
228+
229+ then_norm = Decimal (assets_then ) / factor if assets_then else Decimal (0 )
230+ now_norm = Decimal (assets_now ) / factor if assets_now else Decimal (0 )
231+
232+ # Totals since inception (to latest)
233+ est_total_assets_norm = agg_now .withdraws_assets_norm + now_norm
234+ total_profit_norm = est_total_assets_norm - agg_now .deposits_assets_norm
235+
236+ # Post-snapshot flows
237+ agg_then = aggs_then .get (owner , UserAgg ())
238+ deposited_since_snap = agg_now .deposits_assets_norm - agg_then .deposits_assets_norm
239+ withdrawn_since_snap = agg_now .withdraws_assets_norm - agg_then .withdraws_assets_norm
240+ if deposited_since_snap < 0 :
241+ deposited_since_snap = Decimal (0 )
242+ if withdrawn_since_snap < 0 :
243+ withdrawn_since_snap = Decimal (0 )
244+
245+ # From-snapshot profit = (current_withdrawable + withdrawn_after_snap) - (snapshot_balance + deposited_after_snap)
246+ from_snap_profit = (now_norm + withdrawn_since_snap ) - (
247+ then_norm + deposited_since_snap
248+ )
249+
250+ # Relative growth (ratios, not percents)
251+ snap_denom = then_norm + deposited_since_snap
252+ snap_rel_growth = (from_snap_profit / snap_denom ) if snap_denom > 0 else Decimal (0 )
253+ all_rel_growth = (
254+ (total_profit_norm / agg_now .deposits_assets_norm )
255+ if agg_now .deposits_assets_norm > 0
192256 else Decimal (0 )
193257 )
194- est_total_assets_norm = agg .withdraws_assets_norm + unrealized_assets_norm
195- est_profit_norm = est_total_assets_norm - agg .deposits_assets_norm
196258
197- all_rows .append (
259+ rows .append (
198260 {
261+ # Requested metrics only
199262 "pool" : pool_name ,
200263 "owner" : owner ,
201- "deposited_assets" : str (agg .deposits_assets_norm ),
202- "withdrawn_assets" : str (agg .withdraws_assets_norm ),
203- "shares_in" : str (agg .shares_in ),
204- "shares_out" : str (agg .shares_out ),
205- "net_shares" : str (net_shares ),
206- "est_assets_unrealized" : str (unrealized_assets_norm ),
207- "est_total_assets" : str (est_total_assets_norm ),
208- "est_profit" : str (est_profit_norm ),
209- "est_profit_relative" : str (est_profit_norm / agg .deposits_assets_norm * 100 ),
264+ "deposited_assets" : str (agg_now .deposits_assets_norm ),
265+ "withdrawn_assets" : str (agg_now .withdraws_assets_norm ),
266+ "snapshot_balance" : str (then_norm ),
267+ "current_withdrawable" : str (now_norm ),
268+ "total_profit" : str (total_profit_norm ),
269+ "from_snap_profit" : str (from_snap_profit ),
270+ "snap_rel_growth" : str (snap_rel_growth ),
271+ "all_rel_growth" : str (all_rel_growth ),
210272 }
211273 )
212274
213- # Quick summary
214- realized = sum (Decimal (r ["withdrawn_assets" ]) for r in all_rows if r ["pool" ] == pool_name )
215- unrealized = sum (
216- Decimal (r ["est_assets_unrealized" ]) for r in all_rows if r ["pool" ] == pool_name
217- )
218- deposited = sum (Decimal (r ["deposited_assets" ]) for r in all_rows if r ["pool" ] == pool_name )
219- rel = realized + unrealized - deposited
220- rel_pct = (rel / deposited * 100 ) if deposited != 0 else Decimal (0 )
275+ # 6) Quick pool summary over the snapshot owner set
276+ total_then = sum (Decimal (r ["snapshot_balance" ]) for r in rows if r ["pool" ] == pool_name )
277+ total_now = sum (Decimal (r ["current_withdrawable" ]) for r in rows if r ["pool" ] == pool_name )
278+ pool_growth = total_now - total_then
279+ pool_growth_rel = (pool_growth / total_then ) if total_then != 0 else Decimal (0 )
221280 print (
222- f" Summary: deposited= { deposited } realized= { realized } unrealized= { unrealized } est_profit= { rel } (relative= { rel_pct :.4f } % )"
281+ f" Summary (snapshot owners): then= { total_then } now= { total_now } growth= { pool_growth } (rel= { pool_growth_rel :.6f } )"
223282 )
224283
225- # Optional: write CSV
226- # filepath + csv
284+ # Output CSV next to this script
227285 script_dir = os .path .dirname (os .path .abspath (__file__ ))
228- out_csv = script_dir + "/out_data.csv"
229- if out_csv :
230- import csv
231-
232- header = (
233- list (all_rows [0 ].keys ())
234- if all_rows
235- else [
236- "pool" ,
237- "owner" ,
238- "deposited_assets" ,
239- "withdrawn_assets" ,
240- "shares_in" ,
241- "shares_out" ,
242- "net_shares" ,
243- "est_assets_unrealized" ,
244- "est_total_assets" ,
245- "est_profit" ,
246- "est_profit_relative" ,
247- ]
248- )
249- with open (out_csv , "w" , newline = "" ) as f :
250- w = csv .DictWriter (f , fieldnames = header )
251- w .writeheader ()
252- for row in all_rows :
253- w .writerow (row )
254- print (f"\n Wrote per-user summary to { out_csv } " )
255- else :
256- # Print top few for a quick look
257- print ("\n Sample rows:" )
258- for row in all_rows [:10 ]:
259- print (row )
286+ # Match the location and style of all_users_check output
287+ out_csv = os .path .join (script_dir , "out_data.csv" )
288+ header = (
289+ list (rows [0 ].keys ())
290+ if rows
291+ else [
292+ "pool" ,
293+ "owner" ,
294+ "deposited_assets" ,
295+ "withdrawn_assets" ,
296+ "snapshot_balance" ,
297+ "current_withdrawable" ,
298+ "total_profit" ,
299+ "from_snap_profit" ,
300+ "snap_rel_growth" ,
301+ "all_rel_growth" ,
302+ ]
303+ )
304+
305+ with open (out_csv , "w" , newline = "" ) as f :
306+ writer = csv .DictWriter (f , fieldnames = header )
307+ writer .writeheader ()
308+ for row in rows :
309+ writer .writerow (row )
310+ print (f"\n Wrote per-user summary with requested metrics to { out_csv } " )
260311
261312
262313if __name__ == "__main__" :
0 commit comments