Skip to content

Instantly share code, notes, and snippets.

@catwith1hat
Created February 12, 2024 11:11
Show Gist options
  • Save catwith1hat/33ccb4718ec496b8f5fd79dfb6a2e4f9 to your computer and use it in GitHub Desktop.
Save catwith1hat/33ccb4718ec496b8f5fd79dfb6a2e4f9 to your computer and use it in GitHub Desktop.
#! /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