generated from nathanwoodburn/python-webserver-template
feat: Initial version
All checks were successful
Build Docker / BuildImage (push) Successful in 1m5s
All checks were successful
Build Docker / BuildImage (push) Successful in 1m5s
This commit is contained in:
parent
8e00541b26
commit
db0b194635
@ -1,4 +1,8 @@
|
|||||||
flask
|
flask
|
||||||
gunicorn
|
gunicorn
|
||||||
requests
|
requests
|
||||||
python-dotenv
|
python-dotenv
|
||||||
|
solders
|
||||||
|
solana
|
||||||
|
pycoingecko
|
||||||
|
cachetools
|
118
server.py
118
server.py
@ -1,4 +1,4 @@
|
|||||||
from functools import cache
|
from functools import lru_cache
|
||||||
import json
|
import json
|
||||||
from flask import (
|
from flask import (
|
||||||
Flask,
|
Flask,
|
||||||
@ -15,11 +15,30 @@ import json
|
|||||||
import requests
|
import requests
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
import dotenv
|
import dotenv
|
||||||
|
import solana
|
||||||
|
import solana.rpc
|
||||||
|
from solana.rpc.api import Client
|
||||||
|
import solana.rpc.api
|
||||||
|
from solders.pubkey import Pubkey
|
||||||
|
from solana.rpc.types import TokenAccountOpts
|
||||||
|
from pycoingecko import CoinGeckoAPI
|
||||||
|
from cachetools import TTLCache
|
||||||
|
from cachetools import cached
|
||||||
|
|
||||||
dotenv.load_dotenv()
|
dotenv.load_dotenv()
|
||||||
|
|
||||||
app = Flask(__name__)
|
app = Flask(__name__)
|
||||||
|
|
||||||
|
solana_client = Client(os.environ["SOLANA_URL"])
|
||||||
|
token = Pubkey.from_string("mNT61ixgiLnggJ4qf5hDCNj7vTiCqnqysosjncrBydf")
|
||||||
|
vault = Pubkey.from_string("NWywvhcqdkJsm1s9VVviPm9UfyDtyCW9t8kDb24PDPN")
|
||||||
|
|
||||||
|
fiat = "USD"
|
||||||
|
usd_to_fiat = 1
|
||||||
|
stablecoins = ["usdc", "usdt", "dai"]
|
||||||
|
|
||||||
|
coingecko_client = CoinGeckoAPI()
|
||||||
|
|
||||||
|
|
||||||
def find(name, path):
|
def find(name, path):
|
||||||
for root, dirs, files in os.walk(path):
|
for root, dirs, files in os.walk(path):
|
||||||
@ -74,7 +93,9 @@ def wellknown(path):
|
|||||||
# region Main routes
|
# region Main routes
|
||||||
@app.route("/")
|
@app.route("/")
|
||||||
def index():
|
def index():
|
||||||
return render_template("index.html")
|
tokenSupply = getTokenSupplyString()
|
||||||
|
tokenValue = getTokenPrice()
|
||||||
|
return render_template("index.html", value=tokenValue, supply=tokenSupply)
|
||||||
|
|
||||||
|
|
||||||
@app.route("/<path:path>")
|
@app.route("/<path:path>")
|
||||||
@ -102,6 +123,99 @@ def catch_all(path: str):
|
|||||||
# endregion
|
# endregion
|
||||||
|
|
||||||
|
|
||||||
|
# region Solana
|
||||||
|
|
||||||
|
coin_price_cache = TTLCache(maxsize=100, ttl=3600)
|
||||||
|
@cached(coin_price_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):
|
||||||
|
try:
|
||||||
|
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=100, ttl=3600)
|
||||||
|
@cached(token_supply_str_cache)
|
||||||
|
def getTokenSupplyString() -> str:
|
||||||
|
supply = solana_client.get_token_supply(token)
|
||||||
|
return supply.value.ui_amount_string
|
||||||
|
|
||||||
|
token_supply_cache = TTLCache(maxsize=100, ttl=3600)
|
||||||
|
@cached(token_supply_cache)
|
||||||
|
def getTokenSupply() -> int:
|
||||||
|
supply = solana_client.get_token_supply(token)
|
||||||
|
return supply.value.ui_amount
|
||||||
|
|
||||||
|
vault_balance_cache = TTLCache(maxsize=100, ttl=3600)
|
||||||
|
@cached(vault_balance_cache)
|
||||||
|
def getVaultBalance() -> int:
|
||||||
|
# Get balance of vault
|
||||||
|
# SOLbalance = solana_client.get_balance(vault).value / 1000000000
|
||||||
|
# SOLPrice = get_coin_price("solana")
|
||||||
|
# Ignore SOL for now as the vault will be handling tokens
|
||||||
|
SOLbalance = 0
|
||||||
|
SOLPrice = 0
|
||||||
|
|
||||||
|
programID = Pubkey.from_string("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA")
|
||||||
|
|
||||||
|
tokenAccounts = solana_client.get_token_accounts_by_owner(
|
||||||
|
vault,
|
||||||
|
TokenAccountOpts(program_id=programID)
|
||||||
|
)
|
||||||
|
|
||||||
|
tokenAccounts = tokenAccounts.value
|
||||||
|
tokens = []
|
||||||
|
|
||||||
|
tokenValue = 0
|
||||||
|
|
||||||
|
for tokenAccount in tokenAccounts:
|
||||||
|
pubkey = tokenAccount.pubkey
|
||||||
|
account = solana_client.get_token_account_balance(pubkey)
|
||||||
|
mint = tokenAccount.account.data[:32]
|
||||||
|
mint = Pubkey(mint)
|
||||||
|
# Decode the mint
|
||||||
|
token = {
|
||||||
|
"mint": str(mint),
|
||||||
|
"balance": account.value.ui_amount
|
||||||
|
}
|
||||||
|
|
||||||
|
token["price"] = get_token_price(token["mint"])
|
||||||
|
token["value"] = token["price"] * token["balance"]
|
||||||
|
tokenValue += token["value"]
|
||||||
|
tokens.append(token)
|
||||||
|
|
||||||
|
print(tokens)
|
||||||
|
return (SOLbalance * SOLPrice) + tokenValue
|
||||||
|
|
||||||
|
|
||||||
|
def getTokenPrice():
|
||||||
|
# Get number of tokens minted
|
||||||
|
supply = getTokenSupply()
|
||||||
|
vaultBalance = getVaultBalance()
|
||||||
|
value = vaultBalance/supply
|
||||||
|
# Round value to 2 decimal places and ensure it shows 2 decimal places
|
||||||
|
value = "{:.2f}".format(value)
|
||||||
|
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
# endregion
|
||||||
|
|
||||||
# region Error Catching
|
# region Error Catching
|
||||||
# 404 catch all
|
# 404 catch all
|
||||||
@app.errorhandler(404)
|
@app.errorhandler(404)
|
||||||
|
BIN
stWDBRN.bsdesign
Normal file
BIN
stWDBRN.bsdesign
Normal file
Binary file not shown.
7
templates/assets/bootstrap/css/bootstrap.min.css
vendored
Normal file
7
templates/assets/bootstrap/css/bootstrap.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
6
templates/assets/bootstrap/js/bootstrap.min.js
vendored
Normal file
6
templates/assets/bootstrap/js/bootstrap.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
BIN
templates/assets/img/01.jpg
Normal file
BIN
templates/assets/img/01.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 164 KiB |
BIN
templates/assets/img/02.jpg
Normal file
BIN
templates/assets/img/02.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 40 KiB |
BIN
templates/assets/img/03.jpg
Normal file
BIN
templates/assets/img/03.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 141 KiB |
Binary file not shown.
Before Width: | Height: | Size: 44 KiB After Width: | Height: | Size: 25 KiB |
88
templates/assets/js/wallet.js
Normal file
88
templates/assets/js/wallet.js
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
// Get a list of all the wallets available
|
||||||
|
// Try to connect to window.solana and window.solflare
|
||||||
|
|
||||||
|
const solflareWallet = window.solflare;
|
||||||
|
const phantomWallet = window.solana;
|
||||||
|
|
||||||
|
var wallets = [];
|
||||||
|
// Check which are valid
|
||||||
|
if (phantomWallet && phantomWallet.isPhantom) {
|
||||||
|
console.log("Phantom wallet found");
|
||||||
|
wallets.push("Phantom");
|
||||||
|
}
|
||||||
|
if (solflareWallet && solflareWallet.isSolflare) {
|
||||||
|
console.log("Solflare wallet found");
|
||||||
|
wallets.push("Solflare");
|
||||||
|
}
|
||||||
|
|
||||||
|
async function connectPhantomWallet() {
|
||||||
|
try {
|
||||||
|
const { publicKey } = await phantomWallet.connect();
|
||||||
|
console.log("Connected to Phantom wallet with public key", publicKey.toBase58());
|
||||||
|
return publicKey.toBase58();
|
||||||
|
}
|
||||||
|
catch (err) {
|
||||||
|
console.error("Wallet connection failed", err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function connectSolflareWallet() {
|
||||||
|
try {
|
||||||
|
await solflareWallet.connect();
|
||||||
|
const publicKey = solflareWallet.publicKey.toBase58();
|
||||||
|
console.log("Connected to Solflare wallet with public key", publicKey);
|
||||||
|
return publicKey;
|
||||||
|
}
|
||||||
|
catch (err) {
|
||||||
|
console.error("Wallet connection failed", err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Inject buttons to connect the wallets
|
||||||
|
function injectButtons() {
|
||||||
|
// Get div to hold buttons
|
||||||
|
var div = document.getElementById("wallet-buttons");
|
||||||
|
if (!div) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Clear the div
|
||||||
|
div.innerHTML = "";
|
||||||
|
|
||||||
|
// If no wallets found, add message
|
||||||
|
if (wallets.length == 0) {
|
||||||
|
var p = document.createElement("p");
|
||||||
|
p.innerText = "No wallets found";
|
||||||
|
div.appendChild(p);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add buttons for each wallet
|
||||||
|
for (var i = 0; i < wallets.length; i++) {
|
||||||
|
var button = document.createElement("button");
|
||||||
|
button.className = "btn btn-primary";
|
||||||
|
button.style.margin = "5px";
|
||||||
|
button.innerText = wallets[i];
|
||||||
|
if (wallets[i] == "Phantom") {
|
||||||
|
button.onclick = async function () {
|
||||||
|
var publicKey = await connectPhantomWallet();
|
||||||
|
if (publicKey) {
|
||||||
|
window.location.href = "/?publicKey=" + publicKey;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
if (wallets[i] == "Solflare") {
|
||||||
|
button.onclick = async function () {
|
||||||
|
var publicKey = await connectSolflareWallet();
|
||||||
|
if (publicKey) {
|
||||||
|
window.location.href = "/?publicKey=" + publicKey;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
div.appendChild(button);
|
||||||
|
}
|
||||||
|
console.log("Wallet buttons injected");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait for page to load
|
||||||
|
window.addEventListener("load", injectButtons);
|
@ -1,19 +1,101 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html data-bs-theme="dark" lang="en-au">
|
||||||
|
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, shrink-to-fit=no">
|
||||||
<title>Nathan.Woodburn/</title>
|
<title>Home - Vault | Woodburn</title>
|
||||||
<link rel="icon" href="/assets/img/favicon.png" type="image/png">
|
<meta name="twitter:description" content="Woodburn Vault">
|
||||||
<link rel="stylesheet" href="/assets/css/index.css">
|
<meta name="twitter:image" content="https://vault.woodburn.au/assets/img/favicon.png">
|
||||||
|
<meta property="og:description" content="Woodburn Vault">
|
||||||
|
<meta property="og:image" content="https://vault.woodburn.au/assets/img/favicon.png">
|
||||||
|
<meta name="description" content="Woodburn Vault">
|
||||||
|
<meta property="og:type" content="website">
|
||||||
|
<meta property="og:title" content="Vault | Woodburn">
|
||||||
|
<meta name="twitter:card" content="summary">
|
||||||
|
<meta name="twitter:title" content="Vault | Woodburn">
|
||||||
|
<script type="application/ld+json">
|
||||||
|
{
|
||||||
|
"@context": "http://schema.org",
|
||||||
|
"@type": "WebSite",
|
||||||
|
"name": "Vault | Woodburn",
|
||||||
|
"url": "https://vault.woodburn.au"
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<link rel="icon" type="image/png" sizes="512x512" href="assets/img/favicon.png">
|
||||||
|
<link rel="stylesheet" href="assets/bootstrap/css/bootstrap.min.css">
|
||||||
|
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Catamaran:100,200,300,400,500,600,700,800,900&display=swap">
|
||||||
|
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Lato:100,100i,300,300i,400,400i,700,700i,900,900i&display=swap">
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
<div class="spacer"></div>
|
<nav class="navbar navbar-expand-lg fixed-top bg-dark navbar-custom navbar-dark">
|
||||||
<div class="centre">
|
<div class="container"><a class="navbar-brand" href="#">Vault | Woodburn</a><button class="navbar-toggler" data-bs-toggle="collapse"><span class="visually-hidden">Toggle navigation</span><span class="navbar-toggler-icon"></span></button></div>
|
||||||
<h1>Nathan.Woodburn/</h1>
|
</nav>
|
||||||
</div>
|
<header class="text-center text-white masthead">
|
||||||
|
<div class="masthead-content">
|
||||||
|
<div class="container">
|
||||||
|
<h1 class="masthead-heading mb-0">Woodburn Portfolio Vault</h1>
|
||||||
|
<h2 class="masthead-subheading mb-0">An easy way to start crypto investing</h2>
|
||||||
|
<p>stWDBRN Token Supply: {{supply}}<br>Current Token Value: {{value}} USD</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="bg-circle-1 bg-circle"></div>
|
||||||
|
<div class="bg-circle-2 bg-circle"></div>
|
||||||
|
<div class="bg-circle-3 bg-circle"></div>
|
||||||
|
<div class="bg-circle-4 bg-circle"></div>
|
||||||
|
</header>
|
||||||
|
<section>
|
||||||
|
<div class="container">
|
||||||
|
<div class="row align-items-center">
|
||||||
|
<div class="col-lg-6 order-lg-2">
|
||||||
|
<div class="p-5"><img class="rounded-circle img-fluid" src="assets/img/01.jpg"></div>
|
||||||
|
</div>
|
||||||
|
<div class="col-lg-6 order-lg-1">
|
||||||
|
<div class="p-5">
|
||||||
|
<h2 class="display-4">1. Buy tokens</h2>
|
||||||
|
<p>To get started buy stWDBRN tokens at the current price to buy a percent of the vault value. The buy in value is then invested in various projects in order to increase the token's value.</p><a class="btn btn-primary" role="button" href="mailto:vault@woodburn.au">Buy</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<div class="container">
|
||||||
|
<div class="row align-items-center">
|
||||||
|
<div class="col-lg-6 order-lg-1">
|
||||||
|
<div class="p-5"><img class="rounded-circle img-fluid" src="assets/img/02.jpg"></div>
|
||||||
|
</div>
|
||||||
|
<div class="col-lg-6 order-lg-2">
|
||||||
|
<div class="p-5">
|
||||||
|
<h2 class="display-4">2. HODL and check price</h2>
|
||||||
|
<p>Hodl your stWDBRN while the value fluctuates. Check this site to see the current value.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<div class="container">
|
||||||
|
<div class="row align-items-center">
|
||||||
|
<div class="col-lg-6 order-lg-2">
|
||||||
|
<div class="p-5"><img class="rounded-circle img-fluid" src="assets/img/03.jpg"></div>
|
||||||
|
</div>
|
||||||
|
<div class="col-lg-6 order-lg-1">
|
||||||
|
<div class="p-5">
|
||||||
|
<h2 class="display-4">3. Sell your tokens</h2>
|
||||||
|
<p>When you want to cash out just sell your tokens back at the current token price.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<footer class="py-5 bg-black">
|
||||||
|
<div class="container">
|
||||||
|
<p class="text-center text-white m-0 small">Copyright © Vault | Woodburn 2024</p>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
<script src="assets/bootstrap/js/bootstrap.min.js"></script>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
Loading…
Reference in New Issue
Block a user