diff --git a/README.md b/README.md index 6b90d6a..b8831d2 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,6 @@ # FireTax -Open Source Tax Calculator \ No newline at end of file +Open Source Tax Calculator + +## Notes +- Add a coingecko free demo api to speed up syncing (save it as api_keys/coingecko.txt) \ No newline at end of file diff --git a/chain_module.py b/chain_module.py index d5dc5c1..d2852c1 100644 --- a/chain_module.py +++ b/chain_module.py @@ -61,6 +61,12 @@ def getAllAddresses(): return addresses +def getAddresses(chain: str): + chain = importlib.import_module("chains."+chain) + if "listAddresses" in dir(chain): + return chain.listAddresses() + return [] + def deleteAddress(chain: str, address: str): chain = importlib.import_module("chains."+chain) if "deleteAddress" not in dir(chain): @@ -75,6 +81,13 @@ def syncChains(): if "sync" in dir(chain): chain.sync() +def syncChain(chain: str): + chain = importlib.import_module("chains."+chain) + if "sync" in dir(chain): + chain.sync() + return True + return False + def addAPIKey(chain: str, apiKey: str): chain = importlib.import_module("chains."+chain) if "addAPIKey" not in dir(chain): @@ -86,4 +99,10 @@ def getTransactions(chain: str): chain = importlib.import_module("chains."+chain) if "getTransactions" not in dir(chain): return False - return chain.getTransactions() \ No newline at end of file + return chain.getTransactions() + +def getTransactionsRender(chain:str): + chain = importlib.import_module("chains."+chain) + if "getTransactionsRender" not in dir(chain): + return False + return chain.getTransactionsRender() diff --git a/chains/solana.py b/chains/solana.py index 725f3dc..04c8cff 100644 --- a/chains/solana.py +++ b/chains/solana.py @@ -2,6 +2,13 @@ import json import requests import os import time +from datetime import datetime +from price import get_historical_fiat_price +import dotenv + +dotenv.load_dotenv() + +fiat = os.getenv("fiat") # Chain Data info = { @@ -14,7 +21,27 @@ info = { } - +known_tokens = { + "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v":"usdc", + "cbbtcf3aa214zXHbiAZQwf4122FBYbraNdFqgw4iMij":"coinbase-wrapped-btc", + "Grass7B4RdKfBCjTKgSqnXkqjwiGvQyFbuSCUJr3XXjs":"grass", + "J1toso1uCk3RLmjorhTtrVwY9HJ7X8V9yYac6Y7kGCPn":"jito-staked-sol", + "3NZ9JMVBmGAqocybic2c7LQCJScmgsAZ6vQqTDzcqmJh":"bitcoin", + "So11111111111111111111111111111111111111112":"solana", + "27G8MtK7VtTcCHkpASjSDdkWWYfoqT6ggEuKidVJidD4":"jupiter-perpetuals-liquidity-provider-token", + "JUPyiwrYJFskUPiHa7hkeR8VUtAeFoSYbKedZNsDvCN":"jupiter" +} +other_token_names = { + "jtojtomepa8beP8AuQc6eXt5FriJwfFMwQx2v2f9mCL":"JITO", + "9YZ2syoQHvMeksp4MYZoYMtLyFWkkyBgAsVuuJzSZwVu":"WDBRNT", + "J1toso1uCk3RLmjorhTtrVwY9HJ7X8V9yYac6Y7kGCPn":"JitoSOL", + "8HWnGWTAXiFFtNsZwgk2AyvbUqqt8gcDVVcRVCZCfXC1":"Nathan.Woodburn/", + "cbbtcf3aa214zXHbiAZQwf4122FBYbraNdFqgw4iMij":"cbBTC", + "3NZ9JMVBmGAqocybic2c7LQCJScmgsAZ6vQqTDzcqmJh":"wBTC (Wormhole)", + "So11111111111111111111111111111111111111112":"wSOL", + "27G8MtK7VtTcCHkpASjSDdkWWYfoqT6ggEuKidVJidD4":"JLP", + "JUPyiwrYJFskUPiHa7hkeR8VUtAeFoSYbKedZNsDvCN":"JUP" +} def validateAddress(address: str): return True @@ -74,13 +101,19 @@ def sync(): allTxs = [] for address in addresses: print("Checking address: " + address["address"]) + transactions = [] resp = requests.get("https://api.helius.xyz/v0/addresses/" + address["address"] + "/transactions/?api-key=" + apiKey) - if resp.status_code != 200: - print("Error syncing Solana chain") - print(resp.status_code) - return False - transactions = resp.json() - print(transactions) + while True: + if resp.status_code != 200: + print("Error syncing Solana chain") + print(resp.status_code) + print(resp.text) + break + if len(resp.json()) == 0: + break + transactions.extend(resp.json()) + resp = requests.get("https://api.helius.xyz/v0/addresses/" + address["address"] + "/transactions/?api-key=" + apiKey + "&before=" + resp.json()[-1]["signature"]) + print("Checking page...") allTxs.append({ @@ -91,6 +124,12 @@ def sync(): with open("chain_data/solana.json", "w") as f: json.dump(allTxs, f) + + # Parse transactions + for address in allTxs: + for tx in address["txs"]: + parseTransaction(tx,address["address"]) + return True def getTransactions(): @@ -99,7 +138,137 @@ def getTransactions(): transactions = [] for address in addresses: for tx in address["txs"]: - transactions.append(tx) + transactions.append(parseTransaction(tx,address["address"])) - #TODO Parse transactions return transactions + +def parseTransaction(tx,address): + solChange = getSOLChange(tx,address) + tokenChange = getTokenChange(tx, address) + fiatChange = getFiatChange(tx, address) + return { + "hash": tx["signature"], + "timestamp": tx["timestamp"], + "SOLChange": solChange, + "TokenChange": tokenChange, + "FiatChange": fiatChange, + "type": tx["type"], + "description": tx["description"] + } + +def getSOLChange(tx,address): + for account in tx["accountData"]: + if account["account"] == address: + return account["nativeBalanceChange"] * 0.000000001 + return 0 + +def getTokenChange(tx,address): + changes = [] + for transfer in tx["tokenTransfers"]: + if transfer["fromUserAccount"] == address: + changes.append({ + "userAccount": transfer["fromUserAccount"], + "tokenAccount": transfer["fromTokenAccount"], + "tokenAmount": transfer["tokenAmount"] * -1, + "mint": transfer["mint"], + "tokenStandard": transfer["tokenStandard"], + "toUserAccount": transfer["toUserAccount"], + "toTokenAccount": transfer["toTokenAccount"], + "fiat": get_token_fiat(transfer,address,tx["timestamp"]) + }) + if transfer["toUserAccount"] == address: + changes.append({ + "userAccount": transfer["toUserAccount"], + "tokenAccount": transfer["toTokenAccount"], + "tokenAmount": transfer["tokenAmount"], + "mint": transfer["mint"], + "tokenStandard": transfer["tokenStandard"], + "fromUserAccount": transfer["fromUserAccount"], + "fromTokenAccount": transfer["fromTokenAccount"], + "fiat": get_token_fiat(transfer,address,tx["timestamp"]) + }) + + return changes + +def get_token_fiat(transfer,address,timestamp): + date = datetime.fromtimestamp(timestamp).strftime('%d-%m-%Y') + tokenID = transfer["mint"] + if tokenID in known_tokens: + tokenID = known_tokens[tokenID] + return transfer["tokenAmount"] * get_historical_fiat_price(tokenID, date) + return "Unknown" + +def getFiatChange(tx,address): + solChange = getSOLChange(tx,address) + timestamp = tx["timestamp"] + date = datetime.fromtimestamp(timestamp).strftime('%d-%m-%Y') + + fiat_rate = get_historical_fiat_price("solana", date) + + tokenChanges = getTokenChange(tx, address) + tokenFiatChange = 0 + for tokenChange in tokenChanges: + if 'fiat' in tokenChange: + if tokenChange["fiat"] != "Unknown": + tokenFiatChange += tokenChange["fiat"] + + return (fiat_rate * solChange) + tokenFiatChange + +def getTokenName(token_ID): + name = token_ID + if token_ID in known_tokens: + name = known_tokens[token_ID].upper() + if token_ID in other_token_names: + name = other_token_names[token_ID] + return name +def convertTimestamp(timestamp): + return datetime.fromtimestamp(timestamp).strftime('%d %b %Y %I:%M %p') + +def renderSOLChange(solChange): + if solChange > 0: + if solChange < 0.001: + return f"+ <0.001 SOL" + return f"+{round(solChange,4)} SOL" + + if solChange > -0.001: + return f"- <0.001 SOL" + return f"{round(solChange,4)} SOL" + +def renderFiatChange(fiatChange): + if fiatChange == "Unknown": + return "Unknown Fiat Value" + if fiatChange > 0: + if fiatChange < 0.01: + return f"+ <0.01 {fiat.upper()}" + return f"+{round(fiatChange,2)} {fiat.upper()}" + + if fiatChange > -0.01: + return f"- <0.01 {fiat.upper()}" + return f"{round(fiatChange,2)} {fiat.upper()}" + + +def getTransactionsRender(): + transactions = getTransactions() + html = "" + + totalChange = 0 + for tx in transactions: + totalChange += tx["FiatChange"] + # If fiat change <= 0.001 then don't show + if abs(tx["FiatChange"]) <= 0.005 and len(tx["TokenChange"]) <= 1: + continue + + html += f"