generated from nathanwoodburn/python-webserver-template
feat: Add initial solana integration
All checks were successful
Build Docker / BuildImage (push) Successful in 51s
All checks were successful
Build Docker / BuildImage (push) Successful in 51s
This commit is contained in:
parent
e612637589
commit
6ff1afd46d
@ -1,3 +1,6 @@
|
|||||||
# FireTax
|
# FireTax
|
||||||
|
|
||||||
Open Source Tax Calculator
|
Open Source Tax Calculator
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
- Add a coingecko free demo api to speed up syncing (save it as api_keys/coingecko.txt)
|
@ -61,6 +61,12 @@ def getAllAddresses():
|
|||||||
|
|
||||||
return addresses
|
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):
|
def deleteAddress(chain: str, address: str):
|
||||||
chain = importlib.import_module("chains."+chain)
|
chain = importlib.import_module("chains."+chain)
|
||||||
if "deleteAddress" not in dir(chain):
|
if "deleteAddress" not in dir(chain):
|
||||||
@ -75,6 +81,13 @@ def syncChains():
|
|||||||
if "sync" in dir(chain):
|
if "sync" in dir(chain):
|
||||||
chain.sync()
|
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):
|
def addAPIKey(chain: str, apiKey: str):
|
||||||
chain = importlib.import_module("chains."+chain)
|
chain = importlib.import_module("chains."+chain)
|
||||||
if "addAPIKey" not in dir(chain):
|
if "addAPIKey" not in dir(chain):
|
||||||
@ -86,4 +99,10 @@ def getTransactions(chain: str):
|
|||||||
chain = importlib.import_module("chains."+chain)
|
chain = importlib.import_module("chains."+chain)
|
||||||
if "getTransactions" not in dir(chain):
|
if "getTransactions" not in dir(chain):
|
||||||
return False
|
return False
|
||||||
return chain.getTransactions()
|
return chain.getTransactions()
|
||||||
|
|
||||||
|
def getTransactionsRender(chain:str):
|
||||||
|
chain = importlib.import_module("chains."+chain)
|
||||||
|
if "getTransactionsRender" not in dir(chain):
|
||||||
|
return False
|
||||||
|
return chain.getTransactionsRender()
|
||||||
|
187
chains/solana.py
187
chains/solana.py
@ -2,6 +2,13 @@ import json
|
|||||||
import requests
|
import requests
|
||||||
import os
|
import os
|
||||||
import time
|
import time
|
||||||
|
from datetime import datetime
|
||||||
|
from price import get_historical_fiat_price
|
||||||
|
import dotenv
|
||||||
|
|
||||||
|
dotenv.load_dotenv()
|
||||||
|
|
||||||
|
fiat = os.getenv("fiat")
|
||||||
|
|
||||||
# Chain Data
|
# Chain Data
|
||||||
info = {
|
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):
|
def validateAddress(address: str):
|
||||||
return True
|
return True
|
||||||
@ -74,13 +101,19 @@ def sync():
|
|||||||
allTxs = []
|
allTxs = []
|
||||||
for address in addresses:
|
for address in addresses:
|
||||||
print("Checking address: " + address["address"])
|
print("Checking address: " + address["address"])
|
||||||
|
transactions = []
|
||||||
resp = requests.get("https://api.helius.xyz/v0/addresses/" + address["address"] + "/transactions/?api-key=" + apiKey)
|
resp = requests.get("https://api.helius.xyz/v0/addresses/" + address["address"] + "/transactions/?api-key=" + apiKey)
|
||||||
if resp.status_code != 200:
|
while True:
|
||||||
print("Error syncing Solana chain")
|
if resp.status_code != 200:
|
||||||
print(resp.status_code)
|
print("Error syncing Solana chain")
|
||||||
return False
|
print(resp.status_code)
|
||||||
transactions = resp.json()
|
print(resp.text)
|
||||||
print(transactions)
|
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({
|
allTxs.append({
|
||||||
@ -91,6 +124,12 @@ def sync():
|
|||||||
|
|
||||||
with open("chain_data/solana.json", "w") as f:
|
with open("chain_data/solana.json", "w") as f:
|
||||||
json.dump(allTxs, f)
|
json.dump(allTxs, f)
|
||||||
|
|
||||||
|
# Parse transactions
|
||||||
|
for address in allTxs:
|
||||||
|
for tx in address["txs"]:
|
||||||
|
parseTransaction(tx,address["address"])
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def getTransactions():
|
def getTransactions():
|
||||||
@ -99,7 +138,137 @@ def getTransactions():
|
|||||||
transactions = []
|
transactions = []
|
||||||
for address in addresses:
|
for address in addresses:
|
||||||
for tx in address["txs"]:
|
for tx in address["txs"]:
|
||||||
transactions.append(tx)
|
transactions.append(parseTransaction(tx,address["address"]))
|
||||||
|
|
||||||
#TODO Parse transactions
|
|
||||||
return 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"<div class='transaction'>"
|
||||||
|
html += f"<div class='transaction-description'><a target='_blank' href='https://solscan.io/tx/{tx['hash']}'>{tx['description'] or "Unknown TX"}</a> ({convertTimestamp(tx['timestamp'])})</div>"
|
||||||
|
html += f"<div class='transaction-sol-change'>SOL change: {renderSOLChange(tx['SOLChange'])}</div>"
|
||||||
|
html += f"<div class='transaction-token-change'>"
|
||||||
|
for tokenChange in tx["TokenChange"]:
|
||||||
|
html += f"<div class='token-change'>"
|
||||||
|
html += f"<div class='token-change-amount'>{tokenChange['tokenAmount']} {getTokenName(tokenChange['mint'])} ({renderFiatChange(tokenChange['fiat'])})</div>"
|
||||||
|
html += f"</div>"
|
||||||
|
html += f"</div>"
|
||||||
|
|
||||||
|
if tx["FiatChange"] != 0:
|
||||||
|
html += f"<div class='transaction-fiat-change'>{tx['FiatChange']} {fiat.upper()}</div>"
|
||||||
|
html += f"</div>"
|
||||||
|
return f"<h3>Transactions: {len(transactions)} (P/L {renderFiatChange(totalChange)})</h3>" + html
|
2
example.env
Normal file
2
example.env
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
fiat=aud
|
||||||
|
usd_to_fiat=1.54
|
52
price.py
Normal file
52
price.py
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
from pycoingecko import CoinGeckoAPI
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import dotenv
|
||||||
|
|
||||||
|
dotenv.load_dotenv()
|
||||||
|
|
||||||
|
fiat = os.getenv("fiat")
|
||||||
|
usd_to_fiat = float(os.getenv("usd_to_fiat"))
|
||||||
|
stablecoins = ["usdc", "usdt", "dai"]
|
||||||
|
|
||||||
|
# Get api key
|
||||||
|
if os.path.exists("api_keys/coingecko.txt"):
|
||||||
|
with open("api_keys/coingecko.txt", "r") as f:
|
||||||
|
apiKey = f.read()
|
||||||
|
else:
|
||||||
|
apiKey = None
|
||||||
|
|
||||||
|
|
||||||
|
def get_historical_fiat_price(coin_id, date):
|
||||||
|
"""
|
||||||
|
Fetches the historical price of a cryptocurrency in fiat on a specific date.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
coin_id (str): The CoinGecko ID of the cryptocurrency (e.g., 'bitcoin', 'ethereum').
|
||||||
|
date (str): The date in YYYY-MM-DD format.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
float: The historical price of the cryptocurrency in specified fiat currency.
|
||||||
|
"""
|
||||||
|
|
||||||
|
if coin_id in stablecoins:
|
||||||
|
return 1 * usd_to_fiat
|
||||||
|
|
||||||
|
historical_prices = {}
|
||||||
|
if os.path.exists(f"chain_data/{coin_id}_price.json"):
|
||||||
|
with open(f"chain_data/{coin_id}_price.json", "r") as f:
|
||||||
|
historical_prices = json.load(f)
|
||||||
|
if date in historical_prices:
|
||||||
|
return historical_prices[date]
|
||||||
|
|
||||||
|
|
||||||
|
cg = CoinGeckoAPI()
|
||||||
|
historical_data = cg.get_coin_history_by_id(id=coin_id, date=date, localization='false', demo_api_key=apiKey)
|
||||||
|
price_data = historical_data['market_data']['current_price']
|
||||||
|
fiat_price = price_data[fiat]
|
||||||
|
historical_prices[date] = fiat_price
|
||||||
|
|
||||||
|
with open(f"chain_data/{coin_id}_price.json", "w") as f:
|
||||||
|
json.dump(historical_prices, f)
|
||||||
|
|
||||||
|
return fiat_price
|
@ -1,4 +1,5 @@
|
|||||||
flask
|
flask
|
||||||
gunicorn
|
gunicorn
|
||||||
requests
|
requests
|
||||||
python-dotenv
|
python-dotenv
|
||||||
|
pycoingecko
|
19
server.py
19
server.py
@ -90,8 +90,10 @@ def chains(path: str):
|
|||||||
|
|
||||||
# Get chain info
|
# Get chain info
|
||||||
chain = chain_module.getChainData(path)
|
chain = chain_module.getChainData(path)
|
||||||
transactions = chain_module.getTransactions(path)
|
transactions = chain_module.getTransactionsRender(path)
|
||||||
return render_template("chain.html",chain=chain['name'],transactions=transactions)
|
addresses = chain_module.getAddresses(path)
|
||||||
|
|
||||||
|
return render_template("chain.html",chain=chain['name'],transactions=transactions,addresses=addresses)
|
||||||
|
|
||||||
@app.route("/chains/<path:path>", methods=["POST"])
|
@app.route("/chains/<path:path>", methods=["POST"])
|
||||||
def chainsPost(path: str):
|
def chainsPost(path: str):
|
||||||
@ -107,7 +109,7 @@ def chainsPost(path: str):
|
|||||||
|
|
||||||
if not chain_module.importAddress(path, address):
|
if not chain_module.importAddress(path, address):
|
||||||
return jsonify({"error": "Error importing address"}), 400
|
return jsonify({"error": "Error importing address"}), 400
|
||||||
return redirect('/')
|
return redirect('/chains/'+path)
|
||||||
|
|
||||||
@app.route("/chains/<path:path>/delete/<address>")
|
@app.route("/chains/<path:path>/delete/<address>")
|
||||||
def chainsDelete(path: str, address: str):
|
def chainsDelete(path: str, address: str):
|
||||||
@ -118,7 +120,7 @@ def chainsDelete(path: str, address: str):
|
|||||||
|
|
||||||
if not chain_module.deleteAddress(path, address):
|
if not chain_module.deleteAddress(path, address):
|
||||||
return jsonify({"error": "Error deleting address"}), 400
|
return jsonify({"error": "Error deleting address"}), 400
|
||||||
return redirect('/')
|
return redirect('/chains/'+path)
|
||||||
|
|
||||||
@app.route("/chains/<path:path>/addAPIKey", methods=["POST"])
|
@app.route("/chains/<path:path>/addAPIKey", methods=["POST"])
|
||||||
def chainsAddAPIKey(path: str):
|
def chainsAddAPIKey(path: str):
|
||||||
@ -130,7 +132,7 @@ def chainsAddAPIKey(path: str):
|
|||||||
apiKey = request.form['apiKey']
|
apiKey = request.form['apiKey']
|
||||||
if not chain_module.addAPIKey(path, apiKey):
|
if not chain_module.addAPIKey(path, apiKey):
|
||||||
return jsonify({"error": "Error adding API key"}), 400
|
return jsonify({"error": "Error adding API key"}), 400
|
||||||
return redirect('/')
|
return redirect('/chains/'+path)
|
||||||
|
|
||||||
|
|
||||||
@app.route("/chains/sync")
|
@app.route("/chains/sync")
|
||||||
@ -138,6 +140,13 @@ def chainsSync():
|
|||||||
chain_module.syncChains()
|
chain_module.syncChains()
|
||||||
return redirect('/')
|
return redirect('/')
|
||||||
|
|
||||||
|
@app.route("/chains/sync/<path:path>")
|
||||||
|
def chainSync(path: str):
|
||||||
|
path = path.lower()
|
||||||
|
chain_module.syncChain(path)
|
||||||
|
return redirect('/chains/'+path)
|
||||||
|
|
||||||
|
|
||||||
@app.route("/<path:path>")
|
@app.route("/<path:path>")
|
||||||
def catch_all(path: str):
|
def catch_all(path: str):
|
||||||
if os.path.isfile("templates/" + path):
|
if os.path.isfile("templates/" + path):
|
||||||
|
@ -26,4 +26,12 @@ a:hover {
|
|||||||
margin: auto;
|
margin: auto;
|
||||||
margin-top: 10px;
|
margin-top: 10px;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.transaction {
|
||||||
|
border: 1px solid #ffffff;
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 10px 20px;
|
||||||
|
margin: auto;
|
||||||
|
margin-top: 10px;
|
||||||
}
|
}
|
@ -13,7 +13,17 @@
|
|||||||
<div class="spacer"></div>
|
<div class="spacer"></div>
|
||||||
<div class="centre">
|
<div class="centre">
|
||||||
<h1>Nathan.Woodburn/ {{chain}}</h1>
|
<h1>Nathan.Woodburn/ {{chain}}</h1>
|
||||||
|
<a href="/chains/sync/{{chain}}">Sync</a>
|
||||||
|
|
||||||
|
<div class="address-list">
|
||||||
|
<h3>Imported Addresses</h3>
|
||||||
|
<div class="address-list-item">
|
||||||
|
{% for address in addresses %}
|
||||||
|
{{address}} <a href="/chains/{{ chain }}/delete/{{address}}">Delete</a>
|
||||||
|
<br>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<form method="post">
|
<form method="post">
|
||||||
<h3>Import address</h3>
|
<h3>Import address</h3>
|
||||||
<input type="text" name="address" placeholder="Address">
|
<input type="text" name="address" placeholder="Address">
|
||||||
@ -21,7 +31,7 @@
|
|||||||
</form>
|
</form>
|
||||||
|
|
||||||
<form action="/chains/{{chain}}/addAPIKey" method="post">
|
<form action="/chains/{{chain}}/addAPIKey" method="post">
|
||||||
<h3>Add API Key</h3>
|
<h3>Set API Key</h3>
|
||||||
<input type="text" name="apiKey" placeholder="API Key">
|
<input type="text" name="apiKey" placeholder="API Key">
|
||||||
<input type="submit" value="Add">
|
<input type="submit" value="Add">
|
||||||
</form>
|
</form>
|
||||||
|
@ -14,6 +14,12 @@
|
|||||||
<div class="centre">
|
<div class="centre">
|
||||||
<h1>Nathan.Woodburn/</h1>
|
<h1>Nathan.Woodburn/</h1>
|
||||||
|
|
||||||
|
<div class="chains-list">
|
||||||
|
<h3>Integrated chains</h3>
|
||||||
|
{% for chain in chains %}
|
||||||
|
<p><a href="/chains/{{ chain.link }}">{{ chain.name }}</a></p>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
<div class="address-list">
|
<div class="address-list">
|
||||||
<h3>Imported Addresses</h3>
|
<h3>Imported Addresses</h3>
|
||||||
{% for chain in addresses %}
|
{% for chain in addresses %}
|
||||||
@ -27,12 +33,7 @@
|
|||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</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