generated from nathanwoodburn/python-webserver-template
feat: Started on SOL integration
All checks were successful
Build Docker / BuildImage (push) Successful in 35s
All checks were successful
Build Docker / BuildImage (push) Successful in 35s
This commit is contained in:
parent
8c6cad61a2
commit
e612637589
2
.gitignore
vendored
2
.gitignore
vendored
@ -4,3 +4,5 @@ __pycache__/
|
|||||||
.env
|
.env
|
||||||
.vs/
|
.vs/
|
||||||
.venv/
|
.venv/
|
||||||
|
api_keys/
|
||||||
|
chain_data/
|
89
chain_module.py
Normal file
89
chain_module.py
Normal file
@ -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()
|
35
chains/example.py
Normal file
35
chains/example.py
Normal file
@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
105
chains/solana.py
Normal file
105
chains/solana.py
Normal file
@ -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
|
63
server.py
63
server.py
@ -15,6 +15,7 @@ import json
|
|||||||
import requests
|
import requests
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
import dotenv
|
import dotenv
|
||||||
|
import chain_module
|
||||||
|
|
||||||
dotenv.load_dotenv()
|
dotenv.load_dotenv()
|
||||||
|
|
||||||
@ -74,8 +75,68 @@ def wellknown(path):
|
|||||||
# region Main routes
|
# region Main routes
|
||||||
@app.route("/")
|
@app.route("/")
|
||||||
def index():
|
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/<path:path>")
|
||||||
|
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/<path:path>", 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/<path:path>/delete/<address>")
|
||||||
|
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/<path:path>/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("/<path:path>")
|
@app.route("/<path:path>")
|
||||||
def catch_all(path: str):
|
def catch_all(path: str):
|
||||||
|
29
templates/assets/css/chain.css
Normal file
29
templates/assets/css/chain.css
Normal file
@ -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;
|
||||||
|
|
||||||
|
}
|
@ -18,3 +18,24 @@ a {
|
|||||||
a:hover {
|
a:hover {
|
||||||
text-decoration: underline;
|
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;
|
||||||
|
}
|
33
templates/chain.html
Normal file
33
templates/chain.html
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Nathan.Woodburn/</title>
|
||||||
|
<link rel="icon" href="/assets/img/favicon.png" type="image/png">
|
||||||
|
<link rel="stylesheet" href="/assets/css/chain.css">
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div class="spacer"></div>
|
||||||
|
<div class="centre">
|
||||||
|
<h1>Nathan.Woodburn/ {{chain}}</h1>
|
||||||
|
|
||||||
|
<form method="post">
|
||||||
|
<h3>Import address</h3>
|
||||||
|
<input type="text" name="address" placeholder="Address">
|
||||||
|
<input type="submit" value="Check">
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<form action="/chains/{{chain}}/addAPIKey" method="post">
|
||||||
|
<h3>Add API Key</h3>
|
||||||
|
<input type="text" name="apiKey" placeholder="API Key">
|
||||||
|
<input type="submit" value="Add">
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
{{transactions|safe}}
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
@ -13,6 +13,26 @@
|
|||||||
<div class="spacer"></div>
|
<div class="spacer"></div>
|
||||||
<div class="centre">
|
<div class="centre">
|
||||||
<h1>Nathan.Woodburn/</h1>
|
<h1>Nathan.Woodburn/</h1>
|
||||||
|
|
||||||
|
<div class="address-list">
|
||||||
|
<h3>Imported Addresses</h3>
|
||||||
|
{% for chain in addresses %}
|
||||||
|
<div class="address-list-item">
|
||||||
|
<h4>{{ chain.chain }}</h4>
|
||||||
|
{% for address in chain.addresses %}
|
||||||
|
{{address}} <a href="/chains/{{ chain.chain }}/delete/{{address}}">Delete</a>
|
||||||
|
<br>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="chains-list">
|
||||||
|
<h3>Add New Address</h3>
|
||||||
|
{% for chain in chains %}
|
||||||
|
<p><a href="/chains/{{ chain.link }}">{{ chain.name }}</a></p>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user