From e6126375891197eda4f47665b20a798c9cbaf1e8 Mon Sep 17 00:00:00 2001 From: Nathan Woodburn Date: Thu, 28 Nov 2024 14:05:27 +1100 Subject: [PATCH] feat: Started on SOL integration --- .gitignore | 2 + chain_module.py | 89 ++++++++++++++++++++++++++++ chains/example.py | 35 +++++++++++ chains/solana.py | 105 +++++++++++++++++++++++++++++++++ server.py | 63 +++++++++++++++++++- templates/assets/css/chain.css | 29 +++++++++ templates/assets/css/index.css | 21 +++++++ templates/chain.html | 33 +++++++++++ templates/index.html | 20 +++++++ 9 files changed, 396 insertions(+), 1 deletion(-) create mode 100644 chain_module.py create mode 100644 chains/example.py create mode 100644 chains/solana.py create mode 100644 templates/assets/css/chain.css create mode 100644 templates/chain.html diff --git a/.gitignore b/.gitignore index 7d847cc..d29af16 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,5 @@ __pycache__/ .env .vs/ .venv/ +api_keys/ +chain_data/ \ No newline at end of file diff --git a/chain_module.py b/chain_module.py new file mode 100644 index 0000000..d5dc5c1 --- /dev/null +++ b/chain_module.py @@ -0,0 +1,89 @@ +import os +import json +import importlib +import sys +import hashlib + +if not os.path.exists("chain_data"): + os.mkdir("chain_data") + +if not os.path.exists("api_keys"): + os.mkdir("api_keys") + +def listChains(): + chains = [] + for file in os.listdir("chains"): + if file.endswith(".py"): + if file != "main.py": + chain = importlib.import_module("chains."+file[:-3]) + if "info" not in dir(chain): + continue + details = chain.info + details["link"] = file[:-3] + chains.append(details) + + return chains + + +def chainExists(chain: str): + for file in os.listdir("chains"): + if file == chain+".py": + return True + return False + + +def validateChainAddress(chain: str, address: str): + chain = importlib.import_module("chains."+chain) + if "validateAddress" not in dir(chain): + return False + return chain.validateAddress(address) + +def getChainData(chain: str): + chain = importlib.import_module("chains."+chain) + return chain.info + +def importAddress(chain: str, address: str): + chain = importlib.import_module("chains."+chain) + if "importAddress" not in dir(chain): + return False + return chain.importAddress(address) + + +def getAllAddresses(): + addresses = [] + for file in os.listdir("chains"): + if file.endswith(".py"): + if file != "main.py": + chain = importlib.import_module("chains."+file[:-3]) + if "listAddresses" in dir(chain): + chainAddresses = chain.listAddresses() + addresses.append({"chain": file[:-3].capitalize(), "addresses": chainAddresses}) + + return addresses + +def deleteAddress(chain: str, address: str): + chain = importlib.import_module("chains."+chain) + if "deleteAddress" not in dir(chain): + return False + return chain.deleteAddress(address) + +def syncChains(): + for file in os.listdir("chains"): + if file.endswith(".py"): + if file != "main.py": + chain = importlib.import_module("chains."+file[:-3]) + if "sync" in dir(chain): + chain.sync() + +def addAPIKey(chain: str, apiKey: str): + chain = importlib.import_module("chains."+chain) + if "addAPIKey" not in dir(chain): + return False + return chain.addAPIKey(apiKey) + + +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 diff --git a/chains/example.py b/chains/example.py new file mode 100644 index 0000000..02cdb1f --- /dev/null +++ b/chains/example.py @@ -0,0 +1,35 @@ +import json +import requests + + +# Chain Data +info = { + "name": "Example Chain", + "description": "This is a plugin to be used as an example", + "version": "1.0", + "author": "Nathan.Woodburn/" +} + + +# Functions +functions = { + "search":{ + "name": "Search Owned", + "type": "default", + "description": "Search for owned domains containing a string", + "params": { + "search": { + "name":"Search string", + "type":"text" + } + }, + "returns": { + "domains": + { + "name": "List of owned domains", + "type": "list" + } + } + } +} + diff --git a/chains/solana.py b/chains/solana.py new file mode 100644 index 0000000..725f3dc --- /dev/null +++ b/chains/solana.py @@ -0,0 +1,105 @@ +import json +import requests +import os +import time + +# Chain Data +info = { + "name": "Solana", + "ticker": "SOL", + "description": "Solana Chain", + "version": "1.0", + "author": "Nathan.Woodburn/", + "APIInfo": "This chain uses helius RPC service. Please provide a helius API key." +} + + + + +def validateAddress(address: str): + return True + +def importAddress(address: str): + if not os.path.exists("chain_data/solana.json"): + addresses = [{"address": address,"txs":[],"lastSynced":0}] + with open("chain_data/solana.json", "w") as f: + json.dump(addresses, f) + return True + + with open("chain_data/solana.json", "r") as f: + addresses = json.load(f) + print(addresses) + for existingAddress in addresses: + if existingAddress["address"] == address: + return True + + addresses.append({"address": address,"txs":[],"lastSynced":0}) + with open("chain_data/solana.json", "w") as f: + json.dump(addresses, f) + return True + + +def listAddresses(): + with open("chain_data/solana.json", "r") as f: + addresses = json.load(f) + addresses = [address["address"] for address in addresses] + return addresses + +def deleteAddress(address: str): + with open("chain_data/solana.json", "r") as f: + addresses = json.load(f) + for existingAddress in addresses: + if existingAddress["address"] == address: + addresses.remove(existingAddress) + with open("chain_data/solana.json", "w") as f: + json.dump(addresses, f) + return True + return False + +def addAPIKey(key: str): + with open("api_keys/solana.txt", "w") as f: + f.write(key) + return True + +def sync(): + with open("chain_data/solana.json", "r") as f: + addresses = json.load(f) + + # Get SOLScan API key + if os.path.exists("api_keys/solana.txt"): + with open("api_keys/solana.txt", "r") as f: + apiKey = f.read() + else: + return False + allTxs = [] + for address in addresses: + print("Checking address: " + address["address"]) + 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) + + + allTxs.append({ + "address": address["address"], + "txs": transactions, + "lastSynced": time.time() + }) + + with open("chain_data/solana.json", "w") as f: + json.dump(allTxs, f) + return True + +def getTransactions(): + with open("chain_data/solana.json", "r") as f: + addresses = json.load(f) + transactions = [] + for address in addresses: + for tx in address["txs"]: + transactions.append(tx) + + #TODO Parse transactions + return transactions diff --git a/server.py b/server.py index 240a1a8..68fb3f7 100644 --- a/server.py +++ b/server.py @@ -15,6 +15,7 @@ import json import requests from datetime import datetime import dotenv +import chain_module dotenv.load_dotenv() @@ -74,8 +75,68 @@ def wellknown(path): # region Main routes @app.route("/") def index(): - return render_template("index.html") + # List chains + chains = chain_module.listChains() + addresses = chain_module.getAllAddresses() + return render_template("index.html",chains=chains,addresses=addresses) + +@app.route("/chains/") +def chains(path: str): + path = path.lower() + # Check if the chain exists + if not chain_module.chainExists(path): + return render_template("404.html"), 404 + + # Get chain info + chain = chain_module.getChainData(path) + transactions = chain_module.getTransactions(path) + return render_template("chain.html",chain=chain['name'],transactions=transactions) + +@app.route("/chains/", methods=["POST"]) +def chainsPost(path: str): + path = path.lower() + # Check if the chain exists + if not chain_module.chainExists(path): + return jsonify({"error": "Chain not found"}), 404 + + # Check if the address is valid + address = request.form['address'] + if not chain_module.validateChainAddress(path, address): + return jsonify({"error": "Invalid address"}), 400 + + if not chain_module.importAddress(path, address): + return jsonify({"error": "Error importing address"}), 400 + return redirect('/') + +@app.route("/chains//delete/
") +def chainsDelete(path: str, address: str): + path = path.lower() + # Check if the chain exists + if not chain_module.chainExists(path): + return render_template("404.html"), 404 + + if not chain_module.deleteAddress(path, address): + return jsonify({"error": "Error deleting address"}), 400 + return redirect('/') + +@app.route("/chains//addAPIKey", methods=["POST"]) +def chainsAddAPIKey(path: str): + path = path.lower() + # Check if the chain exists + if not chain_module.chainExists(path): + return jsonify({"error": "Chain not found"}), 404 + + apiKey = request.form['apiKey'] + if not chain_module.addAPIKey(path, apiKey): + return jsonify({"error": "Error adding API key"}), 400 + return redirect('/') + + +@app.route("/chains/sync") +def chainsSync(): + chain_module.syncChains() + return redirect('/') @app.route("/") def catch_all(path: str): diff --git a/templates/assets/css/chain.css b/templates/assets/css/chain.css new file mode 100644 index 0000000..bd685ed --- /dev/null +++ b/templates/assets/css/chain.css @@ -0,0 +1,29 @@ +body { + background-color: #000000; + color: #ffffff; +} +h1 { + font-size: 50px; + margin: 0; + padding: 0; +} +.centre { + margin-top: 10%; + text-align: center; +} +a { + color: #ffffff; + text-decoration: none; +} +a:hover { + text-decoration: underline; +} +.chains-list { + width: fit-content; + border: 1px solid #ffffff; + border-radius: 10px; + padding: 10px 20px; + margin: auto; + margin-top: 10px; + +} \ No newline at end of file diff --git a/templates/assets/css/index.css b/templates/assets/css/index.css index 9635f9c..3108b7a 100644 --- a/templates/assets/css/index.css +++ b/templates/assets/css/index.css @@ -17,4 +17,25 @@ a { } a:hover { text-decoration: underline; +} +.chains-list { + width: fit-content; + border: 1px solid #ffffff; + border-radius: 10px; + padding: 10px 20px; + margin: auto; + margin-top: 10px; + +} +.address-list { + width: fit-content; + border: 1px solid #ffffff; + border-radius: 10px; + padding: 10px 20px; + margin: auto; + margin-top: 10px; + +} +.address-list-item { + margin-bottom: 10px; } \ No newline at end of file diff --git a/templates/chain.html b/templates/chain.html new file mode 100644 index 0000000..5d5dc56 --- /dev/null +++ b/templates/chain.html @@ -0,0 +1,33 @@ + + + + + + + Nathan.Woodburn/ + + + + + +
+
+

Nathan.Woodburn/ {{chain}}

+ +
+

Import address

+ + +
+ +
+

Add API Key

+ + +
+
+
+ {{transactions|safe}} +
+ + \ No newline at end of file diff --git a/templates/index.html b/templates/index.html index bb349f8..23ec283 100644 --- a/templates/index.html +++ b/templates/index.html @@ -13,6 +13,26 @@

Nathan.Woodburn/

+ +
+

Imported Addresses

+ {% for chain in addresses %} +
+

{{ chain.chain }}

+ {% for address in chain.addresses %} + {{address}} Delete +
+ {% endfor %} +
+ {% endfor %} +
+ +
+

Add New Address

+ {% for chain in chains %} +

{{ chain.name }}

+ {% endfor %} +