From 1f8a428d426bc1704355fdd8c2dbe8a4f94e7225 Mon Sep 17 00:00:00 2001 From: Koen Date: Mon, 9 Mar 2026 17:22:37 +0200 Subject: [PATCH 1/2] :bug: Show staking data --- lambda/supply_calculator.py | 54 ++++++++++++++++++++++++++++++++----- 1 file changed, 48 insertions(+), 6 deletions(-) diff --git a/lambda/supply_calculator.py b/lambda/supply_calculator.py index 4535359..139f3d1 100644 --- a/lambda/supply_calculator.py +++ b/lambda/supply_calculator.py @@ -42,15 +42,20 @@ def calculate_supply(): # Calculate locked and circulating supply total_supply = data["total_supply"] + is_rewards_claimable = data["is_rewards_claimable"] # Compute per-ATP locked amounts (same logic as display()) now = int(time.time()) factory_global_locks = data["factory_global_locks"] factory_fracs = {f: unlock_frac(lock, now) for f, lock in factory_global_locks.items()} + type_locked = {"LATP": 0, "MATP": 0, "NCATP": 0} + type_names = {0: "LATP", 1: "MATP", 2: "NCATP"} + for a in atps: wts = a.get("withdrawal_ts") frac = factory_fracs.get(a["factory"], 0.0) + atp_type_name = type_names.get(a["atp_type"], "LATP") if a["atp_type"] == 2: # NCATP: claim() always reverts, only unlockable via staker withdrawal if wts is not None and now >= wts: @@ -77,18 +82,19 @@ def calculate_supply(): a["locked"] = 0 else: a["locked"] = max(0, a["allocation"] - unlocked) + type_locked[atp_type_name] += a["locked"] total_atp_locked = sum(a["locked"] for a in atps) locked_future_incentives = data["other_bals"].get("Future Incentives", 0) locked_y1_rewards = data["other_bals"].get("Y1 Network Rewards", 0) locked_investor_wallet = data["other_bals"].get("Investor Wallet", 0) locked_factories = sum(data["factory_bals"].values()) - - # Slashed funds locked_slashed = data["total_slashed_funds"] - - # FlushRewarder: pending rewards locked_flush_rewarder = data["flush_rewarder_locked"] + token_sale_balance = data["token_sale_balance"] + + # Token sale is locked until isRewardsClaimable + locked_token_sale = token_sale_balance if not is_rewards_claimable else 0 total_locked = ( total_atp_locked @@ -98,10 +104,17 @@ def calculate_supply(): + locked_factories + locked_slashed + locked_flush_rewarder + + locked_token_sale ) circulating = total_supply - total_locked + # Contract balances + total_rollup_balance = sum(data["rollup_bals"].values()) + total_governance_balance = sum(data["governance_bals"].values()) + total_gse_balance = sum(data["gse_bals"].values()) + actively_staked = data["actively_staked_rollup"] + # Get current block block_number = retry(lambda: w3.eth.block_number) @@ -120,8 +133,17 @@ def calculate_supply(): "locked_supply_wei": str(total_locked), "percentage_circulating": round((circulating / total_supply * 100), 4) if total_supply > 0 else 0, "percentage_locked": round((total_locked / total_supply * 100), 4) if total_supply > 0 else 0, - "is_rewards_claimable": data["is_rewards_claimable"], + "is_rewards_claimable": is_rewards_claimable, "atp_count": len(atps), + "actively_staked": str(actively_staked), + "actively_staked_formatted": f"{actively_staked / 1e18:,.2f}", + "contract_balances": { + "governance_total": str(total_governance_balance), + "rollup_total": str(total_rollup_balance), + "gse_total": str(total_gse_balance), + "token_sale": str(token_sale_balance), + **{name: str(bal) for name, bal in data["other_bals"].items()}, + }, "breakdown": { "atp_locked": str(total_atp_locked), "future_incentives": str(locked_future_incentives), @@ -130,9 +152,29 @@ def calculate_supply(): "factories": str(locked_factories), "slashed_funds": str(locked_slashed), "flush_rewarder": str(locked_flush_rewarder), - } + "token_sale": str(locked_token_sale), + }, + "atp_type_breakdown": { + name: str(val) for name, val in type_locked.items() + }, } + # Add global lock info if available + if factory_global_locks: + primary_lock = None + for f, lock in factory_global_locks.items(): + frac = factory_fracs.get(f, 0.0) + if frac < 1.0: + if primary_lock is None or lock[2] > primary_lock[2]: + primary_lock = lock + if primary_lock: + result["global_lock"] = { + "start": primary_lock[0], + "cliff": primary_lock[1], + "end": primary_lock[2], + "current_unlock_pct": round(unlock_frac(primary_lock, now) * 100, 4), + } + return result except Exception as e: From e809d3949b397717d39d1b9735a3c9eaec6730f2 Mon Sep 17 00:00:00 2001 From: Koen Date: Fri, 17 Apr 2026 15:37:28 +0530 Subject: [PATCH 2/2] :bug: fix LATP miscalculation --- circulating-supply.py | 81 ++++++++++++++++++++++++++----------- lambda/supply_calculator.py | 27 +++---------- 2 files changed, 63 insertions(+), 45 deletions(-) diff --git a/circulating-supply.py b/circulating-supply.py index 99ad791..c7ed447 100644 --- a/circulating-supply.py +++ b/circulating-supply.py @@ -41,6 +41,10 @@ print("Error: ETH_RPC_URL environment variable is required") sys.exit(1) +# Optional: query at a specific historical block (default: latest) +_eth_block_raw = os.environ.get("ETH_BLOCK", "").strip() +BLOCK_ID = int(_eth_block_raw) if _eth_block_raw else "latest" + # Bootstrap addresses (only these need to be hardcoded) REGISTRY = "0x35b22e09Ee0390539439E24f06Da43D83f90e298" # All other addresses derived from here AZTEC_TOKEN = "0xA27EC0006e59f245217Ff08CD52A7E8b169E62D2" @@ -75,10 +79,29 @@ MULTICALL3 = "0xcA11bde05977b3631167028862bE2a173976CA11" TYPE_NAMES = {0: "LATP", 1: "MATP", 2: "NCATP"} +# Per-ATP type overrides for contracts where getType() returns the wrong value on-chain. +# Map ATP address (lowercase) -> correct type. Only affects display labels (locked calc is the same). +ATP_TYPE_OVERRIDES = { + # Two ATPs on grant factory 0xfd6bde35... are LATPs but return getType()=1 (MATP). + # Add their addresses here once identified, e.g.: + # "0x1234...": 0, # LATP +} + # ── Setup ──────────────────────────────────────────────────────────────────── w3 = Web3(Web3.HTTPProvider(RPC_URL, request_kwargs={"timeout": 120})) +_block_info_cache = {} + + +def get_block_info(): + """Return (block_number, timestamp) for BLOCK_ID. Cached after first call.""" + if "info" not in _block_info_cache: + blk = retry(lambda: w3.eth.get_block(BLOCK_ID)) + _block_info_cache["info"] = (blk["number"], blk["timestamp"]) + return _block_info_cache["info"] + + # Cache for checksummed addresses to avoid repeated conversions _checksum_cache = {} @@ -150,7 +173,8 @@ def multicall(calls: list[tuple[str, bytes]]) -> list[tuple[bool, bytes]]: data = SEL_AGGREGATE3 + encode(["(address,bool,bytes)[]"], [encoded]) raw = retry( lambda: w3.eth.call( - {"to": to_checksum_cached(MULTICALL3), "data": data} + {"to": to_checksum_cached(MULTICALL3), "data": data}, + block_identifier=BLOCK_ID, ) ) return decode(["(bool,bytes)[]"], raw)[0] @@ -280,18 +304,19 @@ def discover_contract_addresses(): def get_logs_safe(address, topics, from_block=None): """Get logs with fallback for range-limited RPCs.""" start_block = from_block if from_block is not None else 0 + to_block = hex(BLOCK_ID) if isinstance(BLOCK_ID, int) else "latest" params = { "address": to_checksum_cached(address), "topics": ["0x" + t.hex() if isinstance(t, bytes) else t for t in topics], "fromBlock": hex(start_block), - "toBlock": "latest", + "toBlock": to_block, } try: return retry(lambda: w3.eth.get_logs(params)) except Exception as e: print(f" get_logs full range failed ({e}), falling back to chunking...") - latest = retry(lambda: w3.eth.block_number) + latest = get_block_info()[0] if isinstance(BLOCK_ID, int) else retry(lambda: w3.eth.block_number) all_logs = [] chunk_size = 2_000_000 current = start_block @@ -501,6 +526,15 @@ def _bool(i): a["staker"] = _addr(idx) idx += 1 + # Apply per-ATP type overrides for misconfigured contracts + for a in atps: + override = ATP_TYPE_OVERRIDES.get(a["address"].lower()) + if override is not None and a["atp_type"] != override: + orig = TYPE_NAMES.get(a["atp_type"], str(a["atp_type"])) + new = TYPE_NAMES.get(override, str(override)) + print(f" Type override: {a['address']} {orig} -> {new}") + a["atp_type"] = override + # Follow-up: discover withdrawal-capable staker implementations from factory registries # Factory → getRegistry() → token registry # Token registry → getNextStakerVersion() → version count @@ -556,7 +590,7 @@ def _bool(i): withdrawal_capable_impls = {} # impl_addr -> set of registries def _get_code(impl_addr): - return impl_addr, retry(lambda a=impl_addr: w3.eth.get_code(to_checksum_cached(a))) + return impl_addr, retry(lambda a=impl_addr: w3.eth.get_code(to_checksum_cached(a), block_identifier=BLOCK_ID)) with ThreadPoolExecutor(max_workers=len(impl_to_regs) or 1) as pool: for impl_addr, code in pool.map(_get_code, impl_to_regs.keys()): @@ -576,7 +610,7 @@ def _get_code(impl_addr): # Step 5: Query WITHDRAWAL_TIMESTAMP from each withdrawal-capable staker implementation # Use the BEST (earliest passed) timestamp for each factory, since users can upgrade # to any available staker version — not just the one they're currently using. - now_ts = int(time.time()) + now_ts = get_block_info()[1] factory_best_withdrawal_ts = {} # factory (lower) -> earliest WITHDRAWAL_TIMESTAMP if withdrawal_capable_impls: impl_addrs = list(withdrawal_capable_impls.keys()) @@ -738,8 +772,7 @@ def display(atps, data): token_sale_balance = data["token_sale_balance"] factory_bals = data["factory_bals"] - now = int(time.time()) - block = retry(lambda: w3.eth.block_number) + block, now = get_block_info() # Per-factory unlock fractions (each factory has its own Registry with its own schedule) factory_fracs = {} @@ -758,31 +791,22 @@ def display(atps, data): # when WITHDRAWAL_TIMESTAMP has passed. for a in atps: wts = a.get("withdrawal_ts") - frac = factory_fracs.get(a["factory"], 0.0) if a["atp_type"] == 2: # NCATP: claim() always reverts, only unlockable via staker withdrawal if wts is not None and now >= wts: a["locked"] = 0 else: a["locked"] = a["allocation"] - elif a["atp_type"] == 1: - # MATP: indefinitely locked until milestone approved or staker withdrawal - # getClaimable() returns 0 for pending milestones regardless of global lock - unlocked = a.get("claimable", 0) + a["claimed"] - if unlocked >= a["allocation"]: - a["locked"] = 0 - elif wts is not None and now >= wts: - a["locked"] = 0 - else: - a["locked"] = max(0, a["allocation"] - unlocked) else: - # LATP: use earliest of global lock end or WITHDRAWAL_TIMESTAMP - # getClaimable() is capped by balance, so also check if global lock fully ended + # LATP/MATP: unlocked = getClaimable() + getClaimed() + # getClaimable() respects both global and local lock schedules. + # For MATPs, getClaimable() returns 0 for pending milestones. + # Note: getClaimable() is capped by balanceOf(this), so staked-out ATPs + # show claimable=0 even if locks ended — the WITHDRAWAL_TIMESTAMP check + # handles that case (tokens can be unstaked once it passes). unlocked = a.get("claimable", 0) + a["claimed"] if unlocked >= a["allocation"]: a["locked"] = 0 - elif frac >= 1.0: - a["locked"] = 0 elif wts is not None and now >= wts: a["locked"] = 0 else: @@ -1011,6 +1035,7 @@ def display(atps, data): "staked": 0, "in_contracts": 0, "claimed": 0, + "types": {}, } factory_info[factory]["count"] += 1 factory_info[factory]["allocation"] += a["allocation"] @@ -1018,6 +1043,8 @@ def display(atps, data): factory_info[factory]["staked"] += a["staked"] factory_info[factory]["in_contracts"] += a["balance"] factory_info[factory]["claimed"] += a["claimed"] + t_name = TYPE_NAMES.get(a["atp_type"], f"Unknown({a['atp_type']})") + factory_info[factory]["types"][t_name] = factory_info[factory]["types"].get(t_name, 0) + 1 print(f"\n{'='*70}") print(f" ATP FACTORY BREAKDOWN") @@ -1027,8 +1054,9 @@ def display(atps, data): # Get factory name factory_name = factory_names.get(factory_addr, factory_addr[:10] + "...") + type_str = ", ".join(f"{cnt} {t}" for t, cnt in sorted(info["types"].items())) print(f"\n {factory_name}") - print(f" ATPs: {info['count']:>4}") + print(f" ATPs: {info['count']:>4} [{type_str}]") print(f" Total allocation: {fmt(info['allocation']):>22} AZTEC") print(f" Locked: {fmt(info['locked']):>22} AZTEC ({pct(info['locked'], info['allocation'])})") print(f" Staked (gov/rollup): {fmt(info['staked']):>22} AZTEC ({pct(info['staked'], info['allocation'])})") @@ -1274,6 +1302,13 @@ def display(atps, data): def main(): + block_num, block_ts = get_block_info() + if isinstance(BLOCK_ID, int): + dt = datetime.fromtimestamp(block_ts, tz=timezone.utc) + print(f"\n *** HISTORICAL MODE: block {block_num} ({dt.strftime('%Y-%m-%d %H:%M UTC')}) ***") + else: + print(f"\n Querying at latest block {block_num}") + # discover_contract_addresses and fetch_atps are independent — run in parallel print("\n" + "=" * 70) print(" DISCOVERING CONTRACTS + FETCHING ATP EVENTS (parallel)") diff --git a/lambda/supply_calculator.py b/lambda/supply_calculator.py index 139f3d1..2ada2be 100644 --- a/lambda/supply_calculator.py +++ b/lambda/supply_calculator.py @@ -5,7 +5,6 @@ import os import sys -import time from datetime import datetime, timezone # Add parent directory to import the original script @@ -26,8 +25,7 @@ def calculate_supply(): fetch_atps, fetch_data, unlock_frac, - w3, - retry + get_block_info, ) print("Discovering contract addresses...") @@ -45,16 +43,14 @@ def calculate_supply(): is_rewards_claimable = data["is_rewards_claimable"] # Compute per-ATP locked amounts (same logic as display()) - now = int(time.time()) + block_number, now = get_block_info() factory_global_locks = data["factory_global_locks"] - factory_fracs = {f: unlock_frac(lock, now) for f, lock in factory_global_locks.items()} type_locked = {"LATP": 0, "MATP": 0, "NCATP": 0} type_names = {0: "LATP", 1: "MATP", 2: "NCATP"} for a in atps: wts = a.get("withdrawal_ts") - frac = factory_fracs.get(a["factory"], 0.0) atp_type_name = type_names.get(a["atp_type"], "LATP") if a["atp_type"] == 2: # NCATP: claim() always reverts, only unlockable via staker withdrawal @@ -62,22 +58,11 @@ def calculate_supply(): a["locked"] = 0 else: a["locked"] = a["allocation"] - elif a["atp_type"] == 1: - # MATP: indefinitely locked until milestone approved or staker withdrawal - unlocked = a.get("claimable", 0) + a["claimed"] - if unlocked >= a["allocation"]: - a["locked"] = 0 - elif wts is not None and now >= wts: - a["locked"] = 0 - else: - a["locked"] = max(0, a["allocation"] - unlocked) else: - # LATP: use earliest of global lock end or WITHDRAWAL_TIMESTAMP + # LATP/MATP: getClaimable() respects both global and local locks unlocked = a.get("claimable", 0) + a["claimed"] if unlocked >= a["allocation"]: a["locked"] = 0 - elif frac >= 1.0: - a["locked"] = 0 elif wts is not None and now >= wts: a["locked"] = 0 else: @@ -115,8 +100,6 @@ def calculate_supply(): total_gse_balance = sum(data["gse_bals"].values()) actively_staked = data["actively_staked_rollup"] - # Get current block - block_number = retry(lambda: w3.eth.block_number) # Format the response result = { @@ -162,8 +145,8 @@ def calculate_supply(): # Add global lock info if available if factory_global_locks: primary_lock = None - for f, lock in factory_global_locks.items(): - frac = factory_fracs.get(f, 0.0) + for _, lock in factory_global_locks.items(): + frac = unlock_frac(lock, now) if frac < 1.0: if primary_lock is None or lock[2] > primary_lock[2]: primary_lock = lock