diff --git a/cache.py b/cache.py new file mode 100644 index 0000000..aa1555c --- /dev/null +++ b/cache.py @@ -0,0 +1,49 @@ +import os +import json +import hashlib +from functools import wraps +from time import time + +def file_cache(folder="cache", ttl=300): + """ + Decorator to cache function results in the specified folder with a TTL. + + Args: + folder (str): Directory where cached files will be stored. + ttl (int): Time-to-live for the cache in seconds. + """ + if not os.path.exists(folder): + os.makedirs(folder) + + def decorator(func): + @wraps(func) + def wrapper(*args, **kwargs): + # Create a unique cache key based on the function name and arguments + cache_key = hashlib.md5( + f"{func.__name__}-{args}-{kwargs}".encode("utf-8") + ).hexdigest() + cache_file = os.path.join(folder, f"{cache_key}.json") + + # Check if cache exists and is valid + if os.path.exists(cache_file): + try: + with open(cache_file, "r") as f: + cached_data = json.load(f) + # Check if the cache has expired + if time() - cached_data["timestamp"] < ttl: + return cached_data["result"] + except (IOError, ValueError, KeyError): + pass # In case of error, re-compute the result + + # Call the function and cache the result + result = func(*args, **kwargs) + try: + with open(cache_file, "w") as f: + json.dump({"timestamp": time(), "result": result}, f) + except (IOError, TypeError) as e: + print(f"Warning: Could not cache result: {e}") + + return result + + return wrapper + return decorator diff --git a/cache/10118a51009b13b2592c87579e15e61e.json b/cache/10118a51009b13b2592c87579e15e61e.json new file mode 100644 index 0000000..8a30cdc --- /dev/null +++ b/cache/10118a51009b13b2592c87579e15e61e.json @@ -0,0 +1 @@ +{"timestamp": 1733363360.6520803, "result": "120"} \ No newline at end of file diff --git a/cache/1ccff5c6f117409fea0c861aa44b8e62.json b/cache/1ccff5c6f117409fea0c861aa44b8e62.json new file mode 100644 index 0000000..0feb8e4 --- /dev/null +++ b/cache/1ccff5c6f117409fea0c861aa44b8e62.json @@ -0,0 +1 @@ +{"timestamp": 1733363478.77673, "result": 1.14} \ No newline at end of file diff --git a/cache/29409a8a40dd2d547a7a44b8f6758f54.json b/cache/29409a8a40dd2d547a7a44b8f6758f54.json new file mode 100644 index 0000000..33ae4ad --- /dev/null +++ b/cache/29409a8a40dd2d547a7a44b8f6758f54.json @@ -0,0 +1 @@ +{"timestamp": 1733363478.8822353, "result": 4.16} \ No newline at end of file diff --git a/cache/4104ed0427efe63d4ca0dead970a4391.json b/cache/4104ed0427efe63d4ca0dead970a4391.json new file mode 100644 index 0000000..c9943a5 --- /dev/null +++ b/cache/4104ed0427efe63d4ca0dead970a4391.json @@ -0,0 +1 @@ +{"timestamp": 1733363358.2618918, "result": 240.01} \ No newline at end of file diff --git a/cache/598f5dbf97fb0d45cbc6e1a5b0a3b575.json b/cache/598f5dbf97fb0d45cbc6e1a5b0a3b575.json new file mode 100644 index 0000000..be7dff0 --- /dev/null +++ b/cache/598f5dbf97fb0d45cbc6e1a5b0a3b575.json @@ -0,0 +1 @@ +{"timestamp": 1733363355.9661899, "result": 226.21} \ No newline at end of file diff --git a/cache/6eec370e2713cfc84c84e1080b8a191a.json b/cache/6eec370e2713cfc84c84e1080b8a191a.json new file mode 100644 index 0000000..70d9e98 --- /dev/null +++ b/cache/6eec370e2713cfc84c84e1080b8a191a.json @@ -0,0 +1 @@ +{"timestamp": 1733363361.094433, "result": 120.0} \ No newline at end of file diff --git a/cache/94ac30c93587c50252ac382a8d02257f.json b/cache/94ac30c93587c50252ac382a8d02257f.json new file mode 100644 index 0000000..bb4cefe --- /dev/null +++ b/cache/94ac30c93587c50252ac382a8d02257f.json @@ -0,0 +1 @@ +{"timestamp": 1733363355.9663823, "result": 1.47422459502} \ No newline at end of file diff --git a/cache/a071d7bdda25c22e42ad7840f17c4b0e.json b/cache/a071d7bdda25c22e42ad7840f17c4b0e.json new file mode 100644 index 0000000..944c498 --- /dev/null +++ b/cache/a071d7bdda25c22e42ad7840f17c4b0e.json @@ -0,0 +1 @@ +{"timestamp": 1733363357.525003, "result": 0.999445} \ No newline at end of file diff --git a/cache/a0ee60913ba556f39d128e7d7249e788.json b/cache/a0ee60913ba556f39d128e7d7249e788.json new file mode 100644 index 0000000..69713d0 --- /dev/null +++ b/cache/a0ee60913ba556f39d128e7d7249e788.json @@ -0,0 +1 @@ +{"timestamp": 1733363478.8832045, "result": [{"mint": "jupSoLaHXQiZZTSfEWMTRRgpnyFm8f6sZdosWBjx93v", "balance": 0.039815492, "price": 240.01, "value": 9.55611623492, "name": "Jupiter Staked SOL", "symbol": "jupsol"}, {"mint": "27G8MtK7VtTcCHkpASjSDdkWWYfoqT6ggEuKidVJidD4", "balance": 2.402337, "price": 4.16, "value": 9.99372192, "name": "Jupiter Perpetuals Liquidity Provider Token", "symbol": "jlp"}]} \ No newline at end of file diff --git a/cache/b81351778df9f812bbd75ee85a7a073e.json b/cache/b81351778df9f812bbd75ee85a7a073e.json new file mode 100644 index 0000000..cde6576 --- /dev/null +++ b/cache/b81351778df9f812bbd75ee85a7a073e.json @@ -0,0 +1 @@ +{"timestamp": 1733363478.7767982, "result": 115.43342152993999} \ No newline at end of file diff --git a/cache/ccf2a009e56f1b05d471a55d9c9ea8ea.json b/cache/ccf2a009e56f1b05d471a55d9c9ea8ea.json new file mode 100644 index 0000000..ad0363e --- /dev/null +++ b/cache/ccf2a009e56f1b05d471a55d9c9ea8ea.json @@ -0,0 +1 @@ +{"timestamp": 1733363419.3752136, "result": 82.815227} \ No newline at end of file diff --git a/main.py b/main.py index 376fcd5..9bcefc3 100644 --- a/main.py +++ b/main.py @@ -4,6 +4,7 @@ import server from gunicorn.app.base import BaseApplication import os import dotenv +import threading class GunicornApp(BaseApplication): @@ -23,6 +24,7 @@ class GunicornApp(BaseApplication): + if __name__ == '__main__': dotenv.load_dotenv() @@ -37,6 +39,8 @@ if __name__ == '__main__': 'threads': threads, } + threading.Thread(target=server.update_data).start() + gunicorn_app = GunicornApp(server.app, options) print(f'Starting server with {workers} workers and {threads} threads', flush=True) gunicorn_app.run() diff --git a/server.py b/server.py index 03dad7f..ec88a03 100644 --- a/server.py +++ b/server.py @@ -24,6 +24,9 @@ from solana.rpc.types import TokenAccountOpts from pycoingecko import CoinGeckoAPI from cachetools import TTLCache from cachetools import cached +import threading +import time +import cache dotenv.load_dotenv() @@ -31,8 +34,10 @@ app = Flask(__name__) solana_client = Client(os.getenv("SOLANA_URL")) blockFrost_API = os.getenv("BLOCKFROST") -stWDBRN_token_mint = Pubkey.from_string("mNT61ixgiLnggJ4qf5hDCNj7vTiCqnqysosjncrBydf") -vault_sol_address = Pubkey.from_string("NWywvhcqdkJsm1s9VVviPm9UfyDtyCW9t8kDb24PDPN") +stWDBRN_token_mint = Pubkey.from_string( + "mNT61ixgiLnggJ4qf5hDCNj7vTiCqnqysosjncrBydf") +vault_sol_address = Pubkey.from_string( + "NWywvhcqdkJsm1s9VVviPm9UfyDtyCW9t8kDb24PDPN") vault_cardano_address = "stake1uy4qd785pcds7ph2jue2lrhhxa698c5959375lqdv3yphcgwc8qna" fiat = "USD" @@ -48,6 +53,8 @@ def find(name, path): return os.path.join(root, name) # Assets routes + + @app.route("/assets/") def send_assets(path): if path.endswith(".json"): @@ -102,27 +109,24 @@ def index(): vaultBalance = getVaultBalance() vaultBalance = "{:.2f}".format(vaultBalance) - # For testing # tokenSupply = 20 # tokenValue = 1.01 # tokens = [{"symbol":"stWDBRN","name":"Stake With Us","value":1.01}] # solValue = 10 # vaultBalance = "20.00" - - - pie_chart_data = [(token['symbol'].upper(), token['value'], f"{token['name']}: ${'{:.2f}'.format(token['value'])}") for token in tokens] - pie_chart_data.append(("SOL", solValue, f"Solana: ${'{:.2f}'.format(solValue)}")) - - - + pie_chart_data = [(token['symbol'].upper(), token['value'], f"{token['name']}: ${ + '{:.2f}'.format(token['value'])}") for token in tokens] + pie_chart_data.append( + ("SOL", solValue, f"Solana: ${'{:.2f}'.format(solValue)}")) cardanoBalance = getCardanoValue(vault_cardano_address) cardanoBalance = "{:.2f}".format(cardanoBalance) - pie_chart_data.append(("ADA", cardanoBalance, f"Cardano: ${cardanoBalance}")) + pie_chart_data.append( + ("ADA", cardanoBalance, f"Cardano: ${cardanoBalance}")) - return render_template("index.html", value=tokenValue, supply=tokenSupply, pie_chart=pie_chart_data,vault=vaultBalance) + return render_template("index.html", value=tokenValue, supply=tokenSupply, pie_chart=pie_chart_data, vault=vaultBalance) @app.route("/") @@ -150,52 +154,46 @@ def catch_all(path: str): # region Solana - -coin_price_cache = TTLCache(maxsize=100, ttl=3600) -@cached(coin_price_cache) +@cache.file_cache() def get_coin_price(coin_id): try: if coin_id in stablecoins: return 1 * usd_to_fiat - + price = coingecko_client.get_price(ids=coin_id, vs_currencies=fiat) return price[coin_id][fiat.lower()] except: return 0 -token_price_cache = TTLCache(maxsize=100, ttl=3600) -@cached(token_price_cache) -def get_token_price(token_address:str): +@cache.file_cache() +def get_token_price(token_address: str): try: - price = coingecko_client.get_token_price(id='solana',contract_addresses=token_address,vs_currencies=fiat) + price = coingecko_client.get_token_price( + id='solana', contract_addresses=token_address, vs_currencies=fiat) return price[token_address][fiat.lower()] except: return 0 - -token_supply_str_cache = TTLCache(maxsize=1, ttl=3600) -@cached(token_supply_str_cache) +@cache.file_cache() def getTokenSupplyString() -> str: supply = solana_client.get_token_supply(stWDBRN_token_mint) return supply.value.ui_amount_string -token_supply_cache = TTLCache(maxsize=1, ttl=3600) -@cached(token_supply_cache) +@cache.file_cache() def getTokenSupply() -> int: supply = solana_client.get_token_supply(stWDBRN_token_mint) return supply.value.ui_amount -sol_value_cache = TTLCache(maxsize=1, ttl=3600) -@cached(sol_value_cache) +@cache.file_cache() def getSolValue() -> int: - SOLbalance = solana_client.get_balance(vault_sol_address).value / 1000000000 + SOLbalance = solana_client.get_balance( + vault_sol_address).value / 1000000000 SOLPrice = get_coin_price("solana") return SOLbalance * SOLPrice -vault_balance_cache = TTLCache(maxsize=1, ttl=3600) -@cached(vault_balance_cache) +@cache.file_cache() def getVaultBalance() -> int: # Get balance of vault vaultBalance = 0 @@ -210,16 +208,15 @@ def getVaultBalance() -> int: return vaultBalance - -get_tokens_cache = TTLCache(maxsize=1, ttl=3600) -@cached(get_tokens_cache) +@cache.file_cache() def getTokens(): - programID = Pubkey.from_string("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA") - + programID = Pubkey.from_string( + "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA") + tokenAccounts = solana_client.get_token_accounts_by_owner( - vault_sol_address, - TokenAccountOpts(program_id=programID) - ) + vault_sol_address, + TokenAccountOpts(program_id=programID) + ) tokenAccounts = tokenAccounts.value tokens = [] @@ -237,7 +234,7 @@ def getTokens(): token["price"] = get_token_price(token["mint"]) token["value"] = token["price"] * token["balance"] - + if token["value"] < 0.01: continue @@ -257,7 +254,8 @@ def getTokenData(tokenMint): data = json.load(f) return data else: - data = coingecko_client.get_coin_info_from_contract_address_by_id('solana', tokenMint) + data = coingecko_client.get_coin_info_from_contract_address_by_id( + 'solana', tokenMint) with open(f"tokens/{tokenMint}.json", "w") as f: json.dump(data, f) return data @@ -270,7 +268,7 @@ def getTokenPrice(): value = vaultBalance/supply # Round value to 2 decimal places and ensure it shows 2 decimal places value = "{:.2f}".format(value) - + return value @@ -278,11 +276,14 @@ def getTokenPrice(): # region Cardano get_cardano_balance_cache = TTLCache(maxsize=1, ttl=3600) -@cached(get_cardano_balance_cache) + + +@cache.file_cache() def getCardanoBalance(address: str): # Get balance of cardano address try: - response = requests.get(f"https://cardano-mainnet.blockfrost.io/api/v0/accounts/{address}",headers={"project_id": blockFrost_API}) + response = requests.get(f"https://cardano-mainnet.blockfrost.io/api/v0/accounts/{ + address}", headers={"project_id": blockFrost_API}) if response.status_code != 200: print("Error getting cardano balance") return 0 @@ -295,6 +296,7 @@ def getCardanoBalance(address: str): print("Error getting cardano balance") return 0 + def getCardanoValue(address: str): balance = getCardanoBalance(address) price = get_coin_price("cardano") @@ -304,11 +306,32 @@ def getCardanoValue(address: str): # region Error Catching # 404 catch all + + @app.errorhandler(404) def not_found(e): return render_template("404.html"), 404 # endregion + +# region Background Threads +def update_data(): + try: + print("Updating Solana data...") + getSolValue() + getTokens() + getVaultBalance() + print("Updating Cardano data...") + getCardanoBalance(vault_cardano_address) + getCardanoValue(vault_cardano_address) + print("Updating data complete.") + except Exception as e: + print(f"Error updating data: {e}") + +# endregion + + if __name__ == "__main__": + threading.Thread(target=update_data).start() app.run(debug=True, port=5000, host="0.0.0.0")