Last active
November 23, 2022 13:11
-
-
Save rishipr/a416b5c408357ff860b4ed41f946983e to your computer and use it in GitHub Desktop.
Rebase Awards Tracker for Wonderland $TIME Staking (MEMO) π§ββοΈ
This file contains hidden or 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
''' | |
πΈ : Created by @0xRishi | |
π§ββοΈ : Generates CSV of MEMO (staked TIME) balances every 8 hours since first MEMO transaction + overlays all non-rebase MEMO transactions | |
π : Used to create wonderland-apy.vercel.app (historical implied APY chart) | |
(π©, π©) : How to | |
1) Input your AVAX wallet address into `personal_avax_addr`, your Snowtrace API key into `snowtrace_key`, and your Moralis API key into `moralis_key` | |
2) Run with `python3` | |
π : Sample output | |
date timestamp blocknumber type delta_memo implied_apy balance_at_block_memo | |
XXXX-XX-XX XX:XX:XX XXXXXXXXXX XXXXXXX transaction 0.34454045 0.34454045 | |
XXXX-XX-XX XX:XX:XX XXXXXXXXXX XXXXXXX rebase 0.0020437560000000077 64,947% 0.346584206 | |
XXXX-XX-XX XX:XX:XX XXXXXXXXXX XXXXXXX rebase 0.0020560809999999874 64,988% 0.348640287 | |
XXXX-XX-XX XX:XX:XX XXXXXXXXXX XXXXXXX rebase 0.002074844999999992 66,334% 0.350715132 | |
XXXX-XX-XX XX:XX:XX XXXXXXXXXX XXXXXXX rebase 0.0020969470000000157 68,373% 0.352812079 | |
XXXX-XX-XX XX:XX:XX XXXXXXXXXX XXXXXXX rebase 0.002105852999999991 67,612% 0.354917932 | |
πΌ : | |
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, | |
INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR | |
PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY CLAIM, DAMAGES | |
OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT | |
OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. | |
''' | |
# Packages | |
import requests | |
from datetime import datetime, date | |
from pprint import pprint | |
import time | |
import math | |
import csv | |
# Config variables | |
memo_addr = '0x136acd46c134e8269052c62a67042d6bdedde3c9' # Don't edit | |
personal_avax_addr = 'ENTER_AVAX_ADDRESS_HERE' # Input | |
snowtrace_key = 'ENTER_SNOWTRACE_API_KEY_HERE' # Input - get from snowtrace.io | |
moralis_key = 'ENTER_MORALIS_API_KEY_HERE' # Input - get from moralis.io | |
# Support functions | |
def get_ts_from_block(blocknum): | |
url = f"https://api.snowtrace.io/api?module=block&action=getblockreward&blockno={blocknum}&apikey={snowtrace_key}" | |
r = requests.get(url).json()['result']['timeStamp'] | |
return int(r) | |
def get_block_from_ts(ts): | |
url = f"https://api.snowtrace.io/api?module=block&action=getblocknobytime×tamp={ts}&closest=before&apikey={snowtrace_key}" | |
r = requests.get(url).json()['result'] | |
return int(r) | |
def convert_ts_to_human_date(ts): | |
return datetime.utcfromtimestamp( | |
ts).strftime('%Y-%m-%d %H:%M:%S') | |
def get_memo_transactions_from_addr(avax_addr): | |
url = f"https://api.snowtrace.io/api?module=account&action=tokentx&address={avax_addr}&sort=asc&apikey={snowtrace_key}&contractaddress={memo_addr}" | |
results = requests.get(url).json()['result'] | |
results = [r for r in results if r['contractAddress'] | |
== memo_addr] | |
final_res = [] | |
num_results = len(results) | |
i = 0 | |
for r in results: | |
i += 1 | |
try: | |
print( | |
f"Fetched data for {i} of {num_results} non-rebase transactions") | |
item = { | |
'date': convert_ts_to_human_date(int(r['timeStamp'])), | |
'timestamp': int(r['timeStamp']), | |
'blocknumber': r['blockNumber'], | |
'type': 'transaction', | |
'delta_memo': '', | |
'implied_apy': '', | |
'balance_at_block_memo': get_memo_balance_from_block(r['blockNumber']) | |
} | |
final_res.append(item) | |
except: | |
continue | |
return final_res | |
def get_memo_balance_from_block(blocknum): | |
try: | |
base_url = f'https://deep-index.moralis.io/api/v2/{personal_avax_addr}/erc20?chain=avalanche&to_block={blocknum}' | |
results = requests.get(base_url, headers={ | |
"x-api-key": moralis_key}).json() | |
results = [r for r in results if r['token_address'] | |
== memo_addr] # only get MEMO balance | |
balance = float(results[0]['balance']) / 1000000000 | |
return balance | |
except: | |
return 0 | |
# Script start | |
def main(): | |
print( | |
f"\nGetting all non-rebase MEMO transactions for address {personal_avax_addr}...") | |
memo_transactions = get_memo_transactions_from_addr( | |
personal_avax_addr) | |
initial_block = memo_transactions[0]['blocknumber'] | |
initial_timestamp = get_ts_from_block(initial_block) | |
current_timestamp = int(time.time()) | |
current_block = get_block_from_ts(current_timestamp) | |
starting_memo_balance = get_memo_balance_from_block(initial_block) | |
current_memo_balance = get_memo_balance_from_block(current_block) | |
print( | |
f"\n\nStarting MEMO Balance: {starting_memo_balance} - As of {convert_ts_to_human_date(initial_timestamp)} UTC") | |
print( | |
f"Current MEMO Balance: {current_memo_balance} - As of {convert_ts_to_human_date(current_timestamp)} UTC\n\n") | |
snapshots_to_parse = math.ceil( | |
(current_timestamp - initial_timestamp) / 28800) # 8 hours = 28,800 seconds | |
timestamp = initial_timestamp | |
output_data = [] | |
print(f"{snapshots_to_parse} snapshots (8 hrs each) to parse between {convert_ts_to_human_date(initial_timestamp)} UTC and {convert_ts_to_human_date(current_timestamp)} UTC\n") | |
# Get 8 hour snapshots (proxy for rebases) | |
print("----- MEMO 8 HOUR BALANCE SNAPSHOTS -----") | |
i = 1 | |
while timestamp <= current_timestamp: | |
human_date = convert_ts_to_human_date(timestamp) | |
block_number = get_block_from_ts(timestamp) | |
balance = get_memo_balance_from_block(block_number) | |
print( | |
f"Snapshot {i} of {snapshots_to_parse} - {human_date} ({timestamp}): {balance} MEMO") | |
item = { | |
'date': human_date, | |
'timestamp': timestamp, | |
'blocknumber': block_number, | |
# first record is the first stake transaction, after which snapshots are taken | |
'type': 'rebase' if i > 1 else 'transaction', | |
'delta_memo': '', | |
'implied_apy': '', | |
'balance_at_block_memo': balance | |
} | |
output_data.append(item) | |
# add 8 hours to timestamp (8 hours = 28,800 seconds) | |
timestamp += 28800 | |
i += 1 | |
time.sleep(0.4) # avoid rate limiting | |
# Merge & sort rebase and transaction lists | |
print("\nMerging & sorting rebase + non-rebase transactions...") | |
output_data = output_data + \ | |
[x for x in memo_transactions[1:len( | |
memo_transactions)] if x not in output_data] # de-dupe first transaction as already captured in snapshot loop | |
output_data = sorted( | |
output_data, key=lambda x: x['timestamp']) # make sure results are sorted by time stamp (from latest to most recent) | |
# Calculate deltas / implied APYs of each rebase | |
print("\nCalculating deltas / implied APYs of each rebase...") | |
output_data[0]['delta_memo'] = output_data[0]['balance_at_block_memo'] | |
last_index = len(output_data) - 1 | |
for i in range(len(output_data)): | |
if i == last_index: | |
break | |
delta = output_data[i+1]['balance_at_block_memo'] - \ | |
output_data[i]['balance_at_block_memo'] | |
output_data[i+1]['delta_memo'] = delta | |
if output_data[i+1]['type'] == 'rebase': | |
output_data[i + | |
1]['implied_apy'] = f"{format(int(math.pow(1+(delta/output_data[i]['balance_at_block_memo']), 1095) * 100), ',')}%" if output_data[i]['balance_at_block_memo'] > 0 else "" | |
# Remove any zero'd snapshots | |
output_data = [x for x in output_data if x['delta_memo'] > 0] | |
# Write output to CSV | |
print("\nOutputting results to CSV") | |
keys = output_data[0].keys() | |
date_ran = date.today().strftime("%b-%d-%Y") | |
file_name = f'time-wonderland-{date_ran}-analysis-{personal_avax_addr}.csv' | |
try: | |
with open(file_name, 'w', newline='') as output_file: | |
dict_writer = csv.DictWriter(output_file, keys) | |
dict_writer.writeheader() | |
dict_writer.writerows(output_data) | |
print(f"\nData saved to CSV - {file_name}\n") | |
except: | |
print("\nFailed to save data to CSV\n") | |
if __name__ == "__main__": | |
main() |
This is amazing, thank you so much for creating this. I know that the moralis API has some price data, but is there any way of getting the historical prices and add them here?
This script is great! I've been using it for a couple weeks now to track my rebase rewards. I'm curious what happens if someone is to wrap their MEMO into wMEMO. I don't believe this script will work correctly with wMEMO, so I am not sure how to correctly track rebase rewards after wrapping.
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Hey @rishipr I noticed that sometimes -depending on when the intial transaction happened- the last rebase was not being accounted for.
the reason is that you are checking for the balance every 8 hours from when the stacking started, but in reality the first rebase happens sooner than 8hrs. For example if you stake at 22:00 the first rebase will happen at 00:00.
I had a go with it and you can grab it from here: https://controlc.com/3e09f555 [UPDATED]
There is a chance you might need to change the hours[] i've introduced. The next rebase counter at wonderland.money shows that rebases are happening at 00:00, 08:00, 16:00 but my timezone is GMT+2 so you might need to change hours to ['22:00','06:00,''14:00'], but TBH I find it to be more likely to be a bug of the website front end.
Thanks for doing this for the community and for the easy to read code!
I HAVE 0 KNOWLEDGE OF BLOCKCHAIN TRANSACTIONS ETC AND IT'S THE FIRST TIME EVER I AM TOUCHING PYTHON SO APOLOGIES IF GOT SOMETHING WRONG OR THE CODE SUCKS!!!