Created
February 12, 2024 11:11
-
-
Save catwith1hat/33ccb4718ec496b8f5fd79dfb6a2e4f9 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#! /usr/bin/env nix-shell | |
#! nix-shell -i python --packages python3 python3Packages.requests python3Packages.pytz | |
# catwith1hat@protonmail.com | |
import argparse | |
import requests | |
import json | |
from datetime import datetime, timedelta | |
from pytz import timezone | |
import time | |
import logging | |
import csv | |
import sys | |
GENESIS_TIME = 1606824023 | |
# timedeltas work well with DST switches. That is when measured in | |
# seconds, some days are shorter , and some days are longer. I am not | |
# sure what happens if you drop the delta to 1 hour. | |
RESOLUTION = timedelta(days=1) | |
PROXIES = {} | |
def JsonGet(*args, **kwargs): | |
# Respect free API limits, the worst of which is not more than 20 per minute. | |
time.sleep(3) | |
kwargs['headers'] = {'Content-Type': 'application/json'} | |
kwargs['proxies'] = PROXIES | |
response = requests.get(*args, **kwargs) | |
assert (response.status_code == 200) | |
resp = json.loads(response.content) | |
assert (resp['status'] == "OK") | |
logging.debug("Request: %s -> %s", args, resp['data']) | |
return resp['data'] | |
def GetEpoch(epoch): | |
url = "https://beaconcha.in/api/v1/epoch/" + str(epoch) | |
return JsonGet(url) | |
def GetBalanceAtEpoch(validatorList, epoch): | |
url = "https://beaconcha.in/api/v1/validator/{}/balancehistory?latest_epoch={}&limit=1".format( | |
",".join([str(x) for x in validatorList]), epoch) | |
data = JsonGet(url) | |
return sum([x['balance'] for x in data]) | |
# Inclusive start_epoch, excluding endEpoch. | |
def GetWithdrawalsBetween(validatorList, startEpoch, endEpoch): | |
# Remove 1, so that when we add 100, the start epoch is still in the returned query. | |
curEpoch = startEpoch - 1 | |
amounts = {} | |
# Page through the API as at maximum 100 epochs are returned. | |
while curEpoch < endEpoch: | |
curEpoch += 100 | |
# The API includes the epoch queried for and the 99 before, so 100 in total. | |
url = "https://beaconcha.in/api/v1/validator/{}/withdrawals?epoch={}".format( | |
",".join([str(x) for x in validatorList]), curEpoch) | |
for i in JsonGet(url): | |
if i['epoch'] < startEpoch or i['epoch'] >= endEpoch: | |
continue | |
k = (i['epoch'], i['validatorindex']) | |
if k in amounts: | |
# In case we get duplicate reponses, make sure that we | |
assert (amounts[k] == i['amount']) | |
else: | |
amounts[k] = i['amount'] | |
return sum([v for k, v in amounts.items()]) | |
# RETURN THE BALANCE AT THE BEGINNING OF AN EPOCH | |
# Withdrawals do not drain the balance earned during the epoch | |
def GetBalanceAtTS(validators, ts): | |
epoch = TsToEpoch(ts) | |
return GetBalanceAtEpoch(validators, epoch) | |
def TsToEpoch(ts): | |
m = ts - GENESIS_TIME | |
return int(m // (12 * 32)) | |
def run(validators, cur_date, end_date, tz): | |
def DtToEpoch(dt): | |
return TsToEpoch(datetime.timestamp(tz.localize(dt))) | |
cur_epoch = DtToEpoch(cur_date) | |
cur_balance = GetBalanceAtEpoch(validators, cur_epoch) | |
writer = csv.writer(sys.stdout) | |
writer.writerow(["Date","Starting Balance", "Ending Balance", "Withdrawals", "Allocation Change","Income"]) | |
def split_balance(balance): | |
BALANCE_BY_VALIDATOR = 32000000000 | |
income = balance % BALANCE_BY_VALIDATOR | |
return balance - income, income | |
while cur_date < end_date: | |
next_date = cur_date + RESOLUTION | |
next_epoch = DtToEpoch(next_date) | |
next_balance = GetBalanceAtEpoch(validators, next_epoch) | |
withdrawals = GetWithdrawalsBetween(validators, cur_epoch, next_epoch) | |
cb_stock, cb_income = split_balance(cur_balance) | |
nb_stock, nb_income = split_balance(next_balance) | |
w_stock, w_income = split_balance(withdrawals) | |
# We ignore the withdrawn stock here, as it is implicitly included in the new balance. | |
stock_change = nb_stock - cb_stock | |
# The income is the not yet withdrawn income, which is the difference | |
# between the income part of the next epoch and the current epoch and additionally the withdrawn income. | |
income = nb_income - cb_income + w_income | |
writer.writerow([cur_date.strftime("%Y-%m-%d"), cur_balance, next_balance, withdrawals, stock_change, income]) | |
sys.stdout.flush() | |
# print("{},{},{}".format(cur_date, next_balance - cur_balance, withdrawals)) | |
cur_balance, cur_epoch, cur_date = (next_balance, next_epoch, next_date) | |
def main(): | |
parser = argparse.ArgumentParser(description='Validator') | |
parser.add_argument('--start', type=str, dest='start_date_str') | |
parser.add_argument('--end', type=str, dest='end_date_str') | |
parser.add_argument('--validators', type=str, dest='validators') | |
parser.add_argument('--debug', type=bool, dest='debug') | |
parser.add_argument('--proxy', type=str, dest='proxy') | |
parser.add_argument('--tz', type=str, dest='tz') | |
args = parser.parse_args() | |
if args.debug: | |
logging.root.setLevel(logging.DEBUG) | |
if args.proxy: | |
PROXIES['http'] = args.proxy | |
PROXIES['https'] = args.proxy | |
run(args.validators.split(","), | |
datetime.strptime(args.start_date_str, "%Y-%m-%d"), | |
datetime.strptime(args.end_date_str, "%Y-%m-%d"), timezone(args.tz)) | |
if __name__ == "__main__": | |
main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment