This commit is contained in:
commit
fb9cb50a90
1
.gitignore
vendored
1
.gitignore
vendored
@ -12,3 +12,4 @@ plugins/signatures.json
|
||||
.venv/
|
||||
|
||||
user_data/
|
||||
customPlugins/
|
@ -10,6 +10,7 @@ COPY . /app
|
||||
|
||||
# Add mount point for data volume
|
||||
# VOLUME /data
|
||||
RUN apk add git
|
||||
|
||||
ENTRYPOINT ["python3"]
|
||||
CMD ["server.py"]
|
||||
|
Binary file not shown.
@ -34,7 +34,6 @@ Then access the wallet at http://localhost:5000
|
||||
|
||||
|
||||
Also available as a docker image:
|
||||
|
||||
To run using a HSD running directly on the host:
|
||||
|
||||
```bash
|
||||
@ -47,6 +46,8 @@ If you have HSD running on a different IP/container
|
||||
sudo docker run -p 5000:5000 -e hsd_api=yourapikeyhere -e hsd_ip=hsdcontainer git.woodburn.au/nathanwoodburn/firewallet:latest
|
||||
```
|
||||
|
||||
For Docker you can mount a volume to persist the user data (/app/user_data)
|
||||
|
||||
## Features
|
||||
- Basic wallet functionality
|
||||
- Create new wallet
|
||||
|
59
account.py
59
account.py
@ -402,11 +402,16 @@ def getWalletStatus():
|
||||
|
||||
def getBids(account, domain="NONE"):
|
||||
if domain == "NONE":
|
||||
return hsw.getWalletBids(account)
|
||||
|
||||
|
||||
response = hsw.getWalletBids(account)
|
||||
else:
|
||||
response = hsw.getWalletBidsByName(domain,account)
|
||||
return response
|
||||
# Add backup for bids with no value
|
||||
bids = []
|
||||
for bid in response:
|
||||
if 'value' not in bid:
|
||||
bid['value'] = -1000000
|
||||
bids.append(bid)
|
||||
return bids
|
||||
|
||||
def getReveals(account,domain):
|
||||
return hsw.getWalletRevealsByName(domain,account)
|
||||
@ -659,6 +664,52 @@ def revoke(account,domain):
|
||||
}
|
||||
}
|
||||
|
||||
def sendBatch(account, batch):
|
||||
account_name = check_account(account)
|
||||
password = ":".join(account.split(":")[1:])
|
||||
|
||||
if account_name == False:
|
||||
return {
|
||||
"error": {
|
||||
"message": "Invalid account"
|
||||
}
|
||||
}
|
||||
|
||||
try:
|
||||
response = hsw.rpc_selectWallet(account_name)
|
||||
if response['error'] is not None:
|
||||
return {
|
||||
"error": {
|
||||
"message": response['error']['message']
|
||||
}
|
||||
}
|
||||
response = hsw.rpc_walletPassphrase(password,10)
|
||||
if response['error'] is not None:
|
||||
return {
|
||||
"error": {
|
||||
"message": response['error']['message']
|
||||
}
|
||||
}
|
||||
response = requests.post(f"http://x:{APIKEY}@{ip}:12039",json={
|
||||
"method": "sendbatch",
|
||||
"params": [batch]
|
||||
}).json()
|
||||
if response['error'] is not None:
|
||||
return response
|
||||
if 'result' not in response:
|
||||
return {
|
||||
"error": {
|
||||
"message": "No result"
|
||||
}
|
||||
}
|
||||
|
||||
return response['result']
|
||||
except Exception as e:
|
||||
return {
|
||||
"error": {
|
||||
"message": str(e)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#region settingsAPIs
|
||||
|
46
main.py
46
main.py
@ -278,6 +278,8 @@ def auctions():
|
||||
|
||||
balance = account_module.getBalance(account)
|
||||
locked = balance['locked']
|
||||
# Round to 2 decimals
|
||||
locked = round(locked, 2)
|
||||
|
||||
# Add commas to the numbers
|
||||
locked = "{:,}".format(locked)
|
||||
@ -289,7 +291,7 @@ def auctions():
|
||||
# Sort
|
||||
sort = request.args.get("sort")
|
||||
if sort == None:
|
||||
sort = "domain"
|
||||
sort = "time"
|
||||
sort = sort.lower()
|
||||
sort_price = ""
|
||||
sort_price_next = "⬇"
|
||||
@ -297,10 +299,15 @@ def auctions():
|
||||
sort_state_next = "⬇"
|
||||
sort_domain = ""
|
||||
sort_domain_next = "⬇"
|
||||
sort_time = ""
|
||||
sort_time_next = "⬇"
|
||||
reverse = False
|
||||
|
||||
direction = request.args.get("direction")
|
||||
if direction == None:
|
||||
if sort == "time":
|
||||
direction = "⬆"
|
||||
else:
|
||||
direction = "⬇"
|
||||
|
||||
if direction == "⬆":
|
||||
@ -315,6 +322,10 @@ def auctions():
|
||||
sort_state = direction
|
||||
sort_state_next = reverseDirection(direction)
|
||||
domains = sorted(domains, key=lambda k: k['state'],reverse=reverse)
|
||||
elif sort == "time":
|
||||
sort_time = direction
|
||||
sort_time_next = reverseDirection(direction)
|
||||
bids = sorted(bids, key=lambda k: k['height'],reverse=reverse)
|
||||
else:
|
||||
# Sort by domain
|
||||
bids = sorted(bids, key=lambda k: k['name'],reverse=reverse)
|
||||
@ -342,10 +353,6 @@ def auctions():
|
||||
pending_reveals += 1
|
||||
|
||||
plugins = ""
|
||||
# dashFunctions = plugins_module.getDashboardFunctions()
|
||||
# for function in dashFunctions:
|
||||
# functionOutput = plugins_module.runPluginFunction(function["plugin"],function["function"],{},request.cookies.get("account"))
|
||||
# plugins += render.plugin_output_dash(functionOutput,plugins_module.getPluginFunctionReturns(function["plugin"],function["function"]))
|
||||
|
||||
message = ''
|
||||
if 'message' in request.args:
|
||||
@ -357,7 +364,8 @@ def auctions():
|
||||
sort_price=sort_price,sort_state=sort_state,
|
||||
sort_domain=sort_domain,sort_price_next=sort_price_next,
|
||||
sort_state_next=sort_state_next,sort_domain_next=sort_domain_next,
|
||||
bids=len(bids),reveal=pending_reveals,message=message)
|
||||
bids=len(bids),reveal=pending_reveals,message=message,
|
||||
sort_time=sort_time,sort_time_next=sort_time_next)
|
||||
|
||||
@app.route('/reveal')
|
||||
def revealAllBids():
|
||||
@ -377,7 +385,7 @@ def revealAllBids():
|
||||
return redirect("/auctions?message=No reveals pending")
|
||||
return redirect("/auctions?message=" + response['error']['message'])
|
||||
|
||||
return redirect("/success?tx=" + response['hash'])
|
||||
return redirect("/success?tx=" + response['result']['hash'])
|
||||
|
||||
|
||||
@app.route('/search')
|
||||
@ -1091,7 +1099,7 @@ def settings_action(action):
|
||||
resp = account_module.rescan()
|
||||
if 'error' in resp:
|
||||
return redirect("/settings?error=" + str(resp['error']))
|
||||
return redirect("/settings?success=Resent transactions")
|
||||
return redirect("/settings?success=Rescan started")
|
||||
elif action == "resend":
|
||||
resp = account_module.resendTXs()
|
||||
if 'error' in resp:
|
||||
@ -1292,8 +1300,8 @@ def plugins_index():
|
||||
wallet_status=account_module.getWalletStatus(),
|
||||
plugins=plugins)
|
||||
|
||||
@app.route('/plugin/<plugin>')
|
||||
def plugin(plugin):
|
||||
@app.route('/plugin/<ptype>/<path:plugin>')
|
||||
def plugin(ptype,plugin):
|
||||
# Check if the user is logged in
|
||||
if request.cookies.get("account") is None:
|
||||
return redirect("/login")
|
||||
@ -1302,7 +1310,10 @@ def plugin(plugin):
|
||||
if not account:
|
||||
return redirect("/logout")
|
||||
|
||||
plugin = f"{ptype}/{plugin}"
|
||||
|
||||
if not plugins_module.pluginExists(plugin):
|
||||
print(f"Plugin {plugin} not found")
|
||||
return redirect("/plugins")
|
||||
|
||||
data = plugins_module.getPluginData(plugin)
|
||||
@ -1322,10 +1333,10 @@ def plugin(plugin):
|
||||
wallet_status=account_module.getWalletStatus(),
|
||||
name=data['name'],description=data['description'],
|
||||
author=data['author'],version=data['version'],
|
||||
functions=functions,error=error)
|
||||
source=data['source'],functions=functions,error=error)
|
||||
|
||||
@app.route('/plugin/<plugin>/verify')
|
||||
def plugin_verify(plugin):
|
||||
@app.route('/plugin/<ptype>/<path:plugin>/verify')
|
||||
def plugin_verify(ptype,plugin):
|
||||
# Check if the user is logged in
|
||||
if request.cookies.get("account") is None:
|
||||
return redirect("/login")
|
||||
@ -1334,6 +1345,8 @@ def plugin_verify(plugin):
|
||||
if not account:
|
||||
return redirect("/logout")
|
||||
|
||||
plugin = f"{ptype}/{plugin}"
|
||||
|
||||
if not plugins_module.pluginExists(plugin):
|
||||
return redirect("/plugins")
|
||||
|
||||
@ -1344,8 +1357,8 @@ def plugin_verify(plugin):
|
||||
|
||||
return redirect("/plugin/" + plugin)
|
||||
|
||||
@app.route('/plugin/<plugin>/<function>', methods=["POST"])
|
||||
def plugin_function(plugin,function):
|
||||
@app.route('/plugin/<ptype>/<path:plugin>/<function>', methods=["POST"])
|
||||
def plugin_function(ptype,plugin,function):
|
||||
# Check if the user is logged in
|
||||
if request.cookies.get("account") is None:
|
||||
return redirect("/login")
|
||||
@ -1354,6 +1367,8 @@ def plugin_function(plugin,function):
|
||||
if not account:
|
||||
return redirect("/logout")
|
||||
|
||||
plugin = f"{ptype}/{plugin}"
|
||||
|
||||
if not plugins_module.pluginExists(plugin):
|
||||
return redirect("/plugins")
|
||||
|
||||
@ -1386,7 +1401,6 @@ def plugin_function(plugin,function):
|
||||
return redirect("/plugin/" + plugin + "?error=" + response['error'])
|
||||
|
||||
response = render.plugin_output(response,plugins_module.getPluginFunctionReturns(plugin,function))
|
||||
|
||||
return render_template("plugin-output.html", account=account, sync=account_module.getNodeSync(),
|
||||
wallet_status=account_module.getWalletStatus(),
|
||||
name=data['name'],description=data['description'],output=response)
|
||||
|
84
plugin.py
84
plugin.py
@ -3,10 +3,12 @@ import json
|
||||
import importlib
|
||||
import sys
|
||||
import hashlib
|
||||
import subprocess
|
||||
|
||||
|
||||
def listPlugins():
|
||||
plugins = []
|
||||
customPlugins = []
|
||||
for file in os.listdir("plugins"):
|
||||
if file.endswith(".py"):
|
||||
if file != "main.py":
|
||||
@ -14,17 +16,50 @@ def listPlugins():
|
||||
if "info" not in dir(plugin):
|
||||
continue
|
||||
details = plugin.info
|
||||
details["link"] = file[:-3]
|
||||
details["source"] = "built-in"
|
||||
details["link"] = f"plugins/{file[:-3]}"
|
||||
plugins.append(details)
|
||||
|
||||
# Check for imported plugins
|
||||
if not os.path.exists("user_data/plugins.json"):
|
||||
with open("user_data/plugins.json", "w") as f:
|
||||
json.dump([], f)
|
||||
|
||||
with open("user_data/plugins.json", "r") as f:
|
||||
importurls = json.load(f)
|
||||
|
||||
for importurl in importurls:
|
||||
# Get only repo name
|
||||
importPath = importurl.split("/")[-1].removesuffix(".git")
|
||||
|
||||
# Git clone into customPlugins/<importPath>
|
||||
if not os.path.exists(f"customPlugins/{importPath}"):
|
||||
if os.system(f"git clone {importurl} customPlugins/{importPath}") != 0:
|
||||
continue
|
||||
else:
|
||||
if os.system(f"cd customPlugins/{importPath} && git pull") != 0:
|
||||
continue
|
||||
|
||||
# Import plugins from customPlugins/<importPath>
|
||||
for file in os.listdir(f"customPlugins/{importPath}"):
|
||||
if file.endswith(".py"):
|
||||
if file != "main.py":
|
||||
plugin = importlib.import_module(f"customPlugins.{importPath}."+file[:-3])
|
||||
if "info" not in dir(plugin):
|
||||
continue
|
||||
details = plugin.info
|
||||
details["source"] = importPath
|
||||
details["link"] = f"customPlugins/{importPath}/{file[:-3]}"
|
||||
plugins.append(details)
|
||||
|
||||
# Verify plugin signature
|
||||
signatures = []
|
||||
try:
|
||||
with open("plugins/signatures.json", "r") as f:
|
||||
with open("user_data/plugin_signatures.json", "r") as f:
|
||||
signatures = json.load(f)
|
||||
except:
|
||||
# Write a new signatures file
|
||||
with open("plugins/signatures.json", "w") as f:
|
||||
with open("user_data/plugin_signatures.json", "w") as f:
|
||||
json.dump(signatures, f)
|
||||
|
||||
for plugin in plugins:
|
||||
@ -39,34 +74,31 @@ def listPlugins():
|
||||
|
||||
|
||||
def pluginExists(plugin: str):
|
||||
for file in os.listdir("plugins"):
|
||||
if file == plugin+".py":
|
||||
return True
|
||||
return False
|
||||
return os.path.exists(plugin+".py")
|
||||
|
||||
|
||||
def verifyPlugin(plugin: str):
|
||||
signatures = []
|
||||
try:
|
||||
with open("plugins/signatures.json", "r") as f:
|
||||
with open("user_data/plugin_signatures.json", "r") as f:
|
||||
signatures = json.load(f)
|
||||
except:
|
||||
# Write a new signatures file
|
||||
with open("plugins/signatures.json", "w") as f:
|
||||
with open("user_data/plugin_signatures.json", "w") as f:
|
||||
json.dump(signatures, f)
|
||||
|
||||
# Hash the plugin file
|
||||
pluginHash = hashPlugin(plugin)
|
||||
if pluginHash not in signatures:
|
||||
signatures.append(pluginHash)
|
||||
with open("plugins/signatures.json", "w") as f:
|
||||
with open("user_data/plugin_signatures.json", "w") as f:
|
||||
json.dump(signatures, f)
|
||||
|
||||
|
||||
def hashPlugin(plugin: str):
|
||||
BUF_SIZE = 65536
|
||||
sha256 = hashlib.sha256()
|
||||
with open("plugins/"+plugin+".py", 'rb') as f:
|
||||
with open(plugin+".py", 'rb') as f:
|
||||
while True:
|
||||
data = f.read(BUF_SIZE)
|
||||
if not data:
|
||||
@ -76,19 +108,31 @@ def hashPlugin(plugin: str):
|
||||
|
||||
|
||||
def getPluginData(pluginStr: str):
|
||||
plugin = importlib.import_module("plugins."+pluginStr)
|
||||
plugin = importlib.import_module(pluginStr.replace("/","."))
|
||||
|
||||
# Check if the plugin is verified
|
||||
signatures = []
|
||||
try:
|
||||
with open("plugins/signatures.json", "r") as f:
|
||||
with open("user_data/plugin_signatures.json", "r") as f:
|
||||
signatures = json.load(f)
|
||||
except:
|
||||
# Write a new signatures file
|
||||
with open("plugins/signatures.json", "w") as f:
|
||||
with open("user_data/plugin_signatures.json", "w") as f:
|
||||
json.dump(signatures, f)
|
||||
|
||||
info = plugin.info
|
||||
info["source"] = "built-in"
|
||||
|
||||
# Check if the plugin is in customPlugins
|
||||
if pluginStr.startswith("customPlugins"):
|
||||
# Get git url for dir
|
||||
print(f"cd customPlugins/{pluginStr.split('/')[-2]} && git remote get-url origin")
|
||||
url = subprocess.check_output(f"cd customPlugins/{pluginStr.split('/')[-2]} && git remote get-url origin", shell=True).decode("utf-8").strip()
|
||||
info["source"] = url
|
||||
|
||||
|
||||
|
||||
|
||||
# Hash the plugin file
|
||||
pluginHash = hashPlugin(pluginStr)
|
||||
if pluginHash not in signatures:
|
||||
@ -100,12 +144,12 @@ def getPluginData(pluginStr: str):
|
||||
|
||||
|
||||
def getPluginFunctions(plugin: str):
|
||||
plugin = importlib.import_module("plugins."+plugin)
|
||||
plugin = importlib.import_module(plugin.replace("/","."))
|
||||
return plugin.functions
|
||||
|
||||
|
||||
def runPluginFunction(plugin: str, function: str, params: dict, authentication: str):
|
||||
plugin_module = importlib.import_module("plugins."+plugin)
|
||||
plugin_module = importlib.import_module(plugin.replace("/","."))
|
||||
if function not in plugin_module.functions:
|
||||
return {"error": "Function not found"}
|
||||
|
||||
@ -118,11 +162,11 @@ def runPluginFunction(plugin: str, function: str, params: dict, authentication:
|
||||
# Check if the function is in the signature list
|
||||
signatures = []
|
||||
try:
|
||||
with open("plugins/signatures.json", "r") as f:
|
||||
with open("user_data/plugin_signatures.json", "r") as f:
|
||||
signatures = json.load(f)
|
||||
except:
|
||||
# Write a new signatures file
|
||||
with open("plugins/signatures.json", "w") as f:
|
||||
with open("user_data/plugin_signatures.json", "w") as f:
|
||||
json.dump(signatures, f)
|
||||
|
||||
# Hash the plugin file
|
||||
@ -141,12 +185,12 @@ def runPluginFunction(plugin: str, function: str, params: dict, authentication:
|
||||
|
||||
|
||||
def getPluginFunctionInputs(plugin: str, function: str):
|
||||
plugin = importlib.import_module("plugins."+plugin)
|
||||
plugin = importlib.import_module(plugin.replace("/","."))
|
||||
return plugin.functions[function]["params"]
|
||||
|
||||
|
||||
def getPluginFunctionReturns(plugin: str, function: str):
|
||||
plugin = importlib.import_module("plugins."+plugin)
|
||||
plugin = importlib.import_module(plugin.replace("/","."))
|
||||
return plugin.functions[function]["returns"]
|
||||
|
||||
|
||||
|
518
plugins/batching.py
Normal file
518
plugins/batching.py
Normal file
@ -0,0 +1,518 @@
|
||||
import json
|
||||
import account
|
||||
import requests
|
||||
import os
|
||||
|
||||
|
||||
|
||||
# Plugin Data
|
||||
info = {
|
||||
"name": "Batching Functions",
|
||||
"description": "This is a plugin that provides multiple functions to batch transactions",
|
||||
"version": "1.0",
|
||||
"author": "Nathan.Woodburn/"
|
||||
}
|
||||
# https://hsd-dev.org/api-docs/?shell--cli#sendbatch
|
||||
|
||||
|
||||
# Functions
|
||||
functions = {
|
||||
"transfer":{
|
||||
"name": "Batch transfer",
|
||||
"type": "default",
|
||||
"description": "Transfer a ton of domains",
|
||||
"params": {
|
||||
"domains": {
|
||||
"name":"List of domains to transfer (one per line)",
|
||||
"type":"longText"
|
||||
},
|
||||
"address": {
|
||||
"name":"Address to transfer to",
|
||||
"type":"address"
|
||||
}
|
||||
},
|
||||
"returns": {
|
||||
"status":
|
||||
{
|
||||
"name": "Status",
|
||||
"type": "text"
|
||||
},
|
||||
"transaction":
|
||||
{
|
||||
"name": "Hash of the transaction",
|
||||
"type": "tx"
|
||||
}
|
||||
}
|
||||
},
|
||||
"finalize":{
|
||||
"name": "Batch finalize a transfer",
|
||||
"type": "default",
|
||||
"description": "Finalize transferring a ton of domains",
|
||||
"params": {
|
||||
"domains": {
|
||||
"name":"List of domains to finalize (one per line)",
|
||||
"type":"longText"
|
||||
}
|
||||
},
|
||||
"returns": {
|
||||
"status":
|
||||
{
|
||||
"name": "Status",
|
||||
"type": "text"
|
||||
},
|
||||
"transaction":
|
||||
{
|
||||
"name": "Hash of the transaction",
|
||||
"type": "tx"
|
||||
}
|
||||
}
|
||||
},
|
||||
"cancel":{
|
||||
"name": "Batch cancel a transfer",
|
||||
"type": "default",
|
||||
"description": "Cancel transferring a ton of domains",
|
||||
"params": {
|
||||
"domains": {
|
||||
"name":"List of domains to cancel (one per line)",
|
||||
"type":"longText"
|
||||
}
|
||||
},
|
||||
"returns": {
|
||||
"status":
|
||||
{
|
||||
"name": "Status",
|
||||
"type": "text"
|
||||
},
|
||||
"transaction":
|
||||
{
|
||||
"name": "Hash of the transaction",
|
||||
"type": "tx"
|
||||
}
|
||||
}
|
||||
},
|
||||
"open":{
|
||||
"name": "Batch open auctions",
|
||||
"type": "default",
|
||||
"description": "Open auctions for a ton of domains",
|
||||
"params": {
|
||||
"domains": {
|
||||
"name":"List of domains to open (one per line)",
|
||||
"type":"longText"
|
||||
}
|
||||
},
|
||||
"returns": {
|
||||
"status":
|
||||
{
|
||||
"name": "Status",
|
||||
"type": "text"
|
||||
},
|
||||
"transaction":
|
||||
{
|
||||
"name": "Hash of the transaction",
|
||||
"type": "tx"
|
||||
}
|
||||
}
|
||||
},
|
||||
"bid":{
|
||||
"name": "Batch bid on auctions",
|
||||
"type": "default",
|
||||
"description": "Bid on auctions for a ton of domains",
|
||||
"params": {
|
||||
"domains": {
|
||||
"name":"List of domains to bid on (one per line)",
|
||||
"type":"longText"
|
||||
},
|
||||
"bid": {
|
||||
"name":"Bid amount",
|
||||
"type":"text"
|
||||
},
|
||||
"blind": {
|
||||
"name":"Blind amount",
|
||||
"type":"text"
|
||||
}
|
||||
},
|
||||
"returns": {
|
||||
"status":
|
||||
{
|
||||
"name": "Status",
|
||||
"type": "text"
|
||||
},
|
||||
"transaction":
|
||||
{
|
||||
"name": "Hash of the transaction",
|
||||
"type": "tx"
|
||||
}
|
||||
}
|
||||
},
|
||||
"reveal":{
|
||||
"name": "Batch reveal bids",
|
||||
"type": "default",
|
||||
"description": "Reveal bids for tons of auctions",
|
||||
"params": {
|
||||
"domains": {
|
||||
"name":"List of domains to reveal (one per line)",
|
||||
"type":"longText"
|
||||
}
|
||||
},
|
||||
"returns": {
|
||||
"status":
|
||||
{
|
||||
"name": "Status",
|
||||
"type": "text"
|
||||
},
|
||||
"transaction":
|
||||
{
|
||||
"name": "Hash of the transaction",
|
||||
"type": "tx"
|
||||
}
|
||||
}
|
||||
},
|
||||
"redeem":{
|
||||
"name": "Batch redeem bids",
|
||||
"type": "default",
|
||||
"description": "Redeem lost bids to get funds back",
|
||||
"params": {
|
||||
"domains": {
|
||||
"name":"List of domains to redeem (one per line)",
|
||||
"type":"longText"
|
||||
}
|
||||
},
|
||||
"returns": {
|
||||
"status":
|
||||
{
|
||||
"name": "Status",
|
||||
"type": "text"
|
||||
},
|
||||
"transaction":
|
||||
{
|
||||
"name": "Hash of the transaction",
|
||||
"type": "tx"
|
||||
}
|
||||
}
|
||||
},
|
||||
"register":{
|
||||
"name": "Batch register domains",
|
||||
"type": "default",
|
||||
"description": "Register domains won in auction",
|
||||
"params": {
|
||||
"domains": {
|
||||
"name":"List of domains to redeem (one per line)",
|
||||
"type":"longText"
|
||||
}
|
||||
},
|
||||
"returns": {
|
||||
"status":
|
||||
{
|
||||
"name": "Status",
|
||||
"type": "text"
|
||||
},
|
||||
"transaction":
|
||||
{
|
||||
"name": "Hash of the transaction",
|
||||
"type": "tx"
|
||||
}
|
||||
}
|
||||
},
|
||||
"renew":{
|
||||
"name": "Batch renew domains",
|
||||
"type": "default",
|
||||
"description": "Renew a ton of domain",
|
||||
"params": {
|
||||
"domains": {
|
||||
"name": "Domains to renew (one per line)",
|
||||
"type": "longText"
|
||||
}
|
||||
},
|
||||
"returns": {
|
||||
"status": {
|
||||
"name": "Status",
|
||||
"type": "text"
|
||||
},
|
||||
"transaction":
|
||||
{
|
||||
"name": "Hash of the transaction",
|
||||
"type": "tx"
|
||||
}
|
||||
}
|
||||
},
|
||||
"advancedBid":{
|
||||
"name": "Bid on domains with csv",
|
||||
"type": "default",
|
||||
"description": "Bid on domains using a csv format",
|
||||
"params": {
|
||||
"bids": {
|
||||
"name":"List of bids in format `domain,bid,blind` (one per line)",
|
||||
"type":"longText"
|
||||
}
|
||||
},
|
||||
"returns": {
|
||||
"status":
|
||||
{
|
||||
"name": "Status",
|
||||
"type": "text"
|
||||
},
|
||||
"transaction":
|
||||
{
|
||||
"name": "Hash of the transaction",
|
||||
"type": "tx"
|
||||
}
|
||||
}
|
||||
},
|
||||
"advancedBatch":{
|
||||
"name": "Batch transactions with csv",
|
||||
"type": "default",
|
||||
"description": "Batch transactions using a csv format",
|
||||
"params": {
|
||||
"transactions": {
|
||||
"name":"List of transactions in format `type,domain,param1,param2` (one per line) Eg.<br>TRANSFER,woodburn1,hs1q4rkfe5df7ss6wzhnw388hv27we0hp7ha2np0hk<br>OPEN,woodburn2",
|
||||
"type":"longText"
|
||||
}
|
||||
},
|
||||
"returns": {
|
||||
"status":
|
||||
{
|
||||
"name": "Status",
|
||||
"type": "text"
|
||||
},
|
||||
"transaction":
|
||||
{
|
||||
"name": "Hash of the transaction",
|
||||
"type": "tx"
|
||||
}
|
||||
}
|
||||
},
|
||||
"advancedChangeLookahead":{
|
||||
"name": "Change wallet lookahead",
|
||||
"type": "default",
|
||||
"description": "Change the lookahead of the wallet",
|
||||
"params": {
|
||||
"lookahead": {
|
||||
"name":"Lookahead (default 200)",
|
||||
"type":"number"
|
||||
}
|
||||
},
|
||||
"returns": {
|
||||
"status":
|
||||
{
|
||||
"name": "Status",
|
||||
"type": "text"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def sendBatch(batch, authentication):
|
||||
response = account.sendBatch(authentication, batch)
|
||||
return response
|
||||
|
||||
|
||||
def transfer(params, authentication):
|
||||
domains = params["domains"]
|
||||
address = params["address"]
|
||||
domains = domains.splitlines()
|
||||
domains = [x.strip() for x in domains]
|
||||
domains = [x for x in domains if x != ""]
|
||||
|
||||
wallet = authentication.split(":")[0]
|
||||
owned = account.getDomains(wallet)
|
||||
# Only keep owned domains ["name"]
|
||||
ownedNames = [domain["name"] for domain in owned]
|
||||
|
||||
for domain in domains:
|
||||
if domain not in ownedNames:
|
||||
return {
|
||||
"status":f"Domain {domain} not owned",
|
||||
"transaction":None
|
||||
}
|
||||
|
||||
batch = []
|
||||
for domain in domains:
|
||||
batch.append(['TRANSFER', domain, address])
|
||||
|
||||
response = sendBatch(batch, authentication)
|
||||
if 'error' in response:
|
||||
return {
|
||||
"status":response['error']['message'],
|
||||
"transaction":None
|
||||
}
|
||||
|
||||
return {
|
||||
"status":"Sent batch successfully",
|
||||
"transaction":response['hash']
|
||||
}
|
||||
|
||||
def simple(batchType,params, authentication):
|
||||
domains = params["domains"]
|
||||
domains = domains.splitlines()
|
||||
domains = [x.strip() for x in domains]
|
||||
domains = [x for x in domains if x != ""]
|
||||
|
||||
batch = []
|
||||
for domain in domains:
|
||||
batch.append([batchType, domain])
|
||||
|
||||
print(batch)
|
||||
response = sendBatch(batch, authentication)
|
||||
if 'error' in response:
|
||||
print(response)
|
||||
return {
|
||||
"status":response['error']['message'],
|
||||
"transaction":None
|
||||
}
|
||||
|
||||
return {
|
||||
"status":"Sent batch successfully",
|
||||
"transaction":response['hash']
|
||||
}
|
||||
|
||||
def finalize(params, authentication):
|
||||
return simple("FINALIZE",params,authentication)
|
||||
|
||||
def cancel(params, authentication):
|
||||
return simple("CANCEL",params,authentication)
|
||||
|
||||
def open(params, authentication):
|
||||
return simple("OPEN",params,authentication)
|
||||
|
||||
def bid(params, authentication):
|
||||
domains = params["domains"]
|
||||
domains = domains.splitlines()
|
||||
domains = [x.strip() for x in domains]
|
||||
domains = [x for x in domains if x != ""]
|
||||
|
||||
try:
|
||||
bid = float(params["bid"])
|
||||
blind = float(params["blind"])
|
||||
blind+=bid
|
||||
except:
|
||||
return {
|
||||
"status":"Invalid bid amount",
|
||||
"transaction":None
|
||||
}
|
||||
|
||||
batch = []
|
||||
for domain in domains:
|
||||
batch.append(['BID', domain, bid, blind])
|
||||
|
||||
print(batch)
|
||||
response = sendBatch(batch, authentication)
|
||||
if 'error' in response:
|
||||
return {
|
||||
"status":response['error']['message'],
|
||||
"transaction":None
|
||||
}
|
||||
|
||||
return {
|
||||
"status":"Sent batch successfully",
|
||||
"transaction":response['hash']
|
||||
}
|
||||
|
||||
def reveal(params, authentication):
|
||||
return simple("REVEAL",params,authentication)
|
||||
|
||||
def redeem(params, authentication):
|
||||
return simple("REDEEM",params,authentication)
|
||||
|
||||
def register(params, authentication):
|
||||
domains = params["domains"]
|
||||
domains = domains.splitlines()
|
||||
domains = [x.strip() for x in domains]
|
||||
domains = [x for x in domains if x != ""]
|
||||
|
||||
batch = []
|
||||
for domain in domains:
|
||||
batch.append(['UPDATE', domain,{"records": []}])
|
||||
|
||||
print(batch)
|
||||
response = sendBatch(batch, authentication)
|
||||
if 'error' in response:
|
||||
return {
|
||||
"status":response['error']['message'],
|
||||
"transaction":None
|
||||
}
|
||||
|
||||
return {
|
||||
"status":"Sent batch successfully",
|
||||
"transaction":response['hash']
|
||||
}
|
||||
|
||||
def renew(params, authentication):
|
||||
return simple("RENEW", params, authentication)
|
||||
|
||||
def advancedBid(params, authentication):
|
||||
bids = params["bids"]
|
||||
bids = bids.splitlines()
|
||||
bids = [x.strip() for x in bids]
|
||||
bids = [x for x in bids if x != ""]
|
||||
|
||||
batch = []
|
||||
for bid in bids:
|
||||
# Split the bid
|
||||
line = bid.split(",")
|
||||
domain = line[0]
|
||||
bid = float(line[1])
|
||||
blind = float(line[2])
|
||||
blind+=bid
|
||||
batch.append(['BID', domain, bid, blind])
|
||||
|
||||
print(batch)
|
||||
response = sendBatch(batch, authentication)
|
||||
if 'error' in response:
|
||||
return {
|
||||
"status":response['error']['message'],
|
||||
"transaction":None
|
||||
}
|
||||
|
||||
return {
|
||||
"status":"Sent batch successfully",
|
||||
"transaction":response['hash']
|
||||
}
|
||||
|
||||
def advancedBatch(params, authentication):
|
||||
transactions = params["transactions"]
|
||||
transactions = transactions.splitlines()
|
||||
transactions = [x.strip() for x in transactions]
|
||||
transactions = [x for x in transactions if x != ""]
|
||||
|
||||
batch = []
|
||||
for transaction in transactions:
|
||||
# Split the bid
|
||||
line = transaction.split(",")
|
||||
line[0] = line[0].upper()
|
||||
batch.append(line)
|
||||
|
||||
print(batch)
|
||||
response = sendBatch(batch, authentication)
|
||||
if 'error' in response:
|
||||
return {
|
||||
"status":response['error']['message'],
|
||||
"transaction":None
|
||||
}
|
||||
|
||||
return {
|
||||
"status":"Sent batch successfully",
|
||||
"transaction":response['hash']
|
||||
}
|
||||
|
||||
|
||||
def advancedChangeLookahead(params, authentication):
|
||||
lookahead = params["lookahead"]
|
||||
lookahead = int(lookahead)
|
||||
wallet = authentication.split(":")[0]
|
||||
password = ":".join(authentication.split(":")[1:])
|
||||
APIKEY = os.getenv("hsd_api")
|
||||
ip = os.getenv("hsd_ip")
|
||||
if ip is None:
|
||||
ip = "localhost"
|
||||
|
||||
# Unlock wallet
|
||||
response = requests.post(f"http://x:{APIKEY}@{ip}:12039/wallet/{wallet}/unlock",
|
||||
json={"passphrase": password, "timeout": 10})
|
||||
|
||||
response = requests.patch(f"http://x:{APIKEY}@{ip}:12039/wallet/{wallet}/account/default",
|
||||
json={"lookahead": lookahead})
|
||||
|
||||
|
||||
return {
|
||||
"status":f"Status: {'Success' if response.status_code == 200 else 'Error'}"
|
||||
}
|
114
plugins/customPlugins.py
Normal file
114
plugins/customPlugins.py
Normal file
@ -0,0 +1,114 @@
|
||||
import json
|
||||
import account
|
||||
import requests
|
||||
import os
|
||||
|
||||
# Plugin Data
|
||||
info = {
|
||||
"name": "Custom Plugin Manager",
|
||||
"description": "Import custom plugins from git repositories",
|
||||
"version": "1.0",
|
||||
"author": "Nathan.Woodburn/"
|
||||
}
|
||||
|
||||
# Functions
|
||||
functions = {
|
||||
"add":{
|
||||
"name": "Add Plugin repo",
|
||||
"type": "default",
|
||||
"description": "Add a plugin repo",
|
||||
"params": {
|
||||
"url": {
|
||||
"name":"URL",
|
||||
"type":"text"
|
||||
}
|
||||
},
|
||||
"returns": {
|
||||
"status":
|
||||
{
|
||||
"name": "Status of the function",
|
||||
"type": "text"
|
||||
}
|
||||
}
|
||||
},
|
||||
"remove":{
|
||||
"name": "Remove Plugins",
|
||||
"type": "default",
|
||||
"description": "Remove a plugin repo from the list",
|
||||
"params": {
|
||||
"url": {
|
||||
"name":"URL",
|
||||
"type":"text"
|
||||
}
|
||||
},
|
||||
"returns": {
|
||||
"status":
|
||||
{
|
||||
"name": "Status of the function",
|
||||
"type": "text"
|
||||
}
|
||||
}
|
||||
},
|
||||
"list":{
|
||||
"name": "List Plugins",
|
||||
"type": "default",
|
||||
"description": "List all imported plugins",
|
||||
"params": {},
|
||||
"returns": {
|
||||
"plugins":
|
||||
{
|
||||
"name": "List of plugins",
|
||||
"type": "list"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def add(params, authentication):
|
||||
url = params["url"]
|
||||
if not os.path.exists("user_data/plugins.json"):
|
||||
with open("user_data/plugins.json", "w") as f:
|
||||
json.dump([], f)
|
||||
|
||||
with open("user_data/plugins.json", "r") as f:
|
||||
importurls = json.load(f)
|
||||
|
||||
# Check if the plugin is already imported
|
||||
if url in importurls:
|
||||
return {"status": "Plugin already imported"}
|
||||
|
||||
importurls.append(url)
|
||||
with open("user_data/plugins.json", "w") as f:
|
||||
json.dump(importurls, f)
|
||||
|
||||
return {"status": "Imported"}
|
||||
|
||||
|
||||
def remove(params, authentication):
|
||||
url = params["url"]
|
||||
if not os.path.exists("user_data/plugins.json"):
|
||||
with open("user_data/plugins.json", "w") as f:
|
||||
json.dump([], f)
|
||||
|
||||
with open("user_data/plugins.json", "r") as f:
|
||||
importurls = json.load(f)
|
||||
|
||||
# Check if the plugin is already imported
|
||||
if url not in importurls:
|
||||
return {"status": "Plugin not imported"}
|
||||
|
||||
importurls.remove(url)
|
||||
with open("user_data/plugins.json", "w") as f:
|
||||
json.dump(importurls, f)
|
||||
|
||||
return {"status": "Removed"}
|
||||
|
||||
def list(params, authentication):
|
||||
if not os.path.exists("user_data/plugins.json"):
|
||||
with open("user_data/plugins.json", "w") as f:
|
||||
json.dump([], f)
|
||||
|
||||
with open("user_data/plugins.json", "r") as f:
|
||||
importurls = json.load(f)
|
||||
|
||||
return {"plugins": importurls}
|
@ -51,10 +51,11 @@ def main(params, authentication):
|
||||
|
||||
# Unlock wallet
|
||||
api_key = os.getenv("hsd_api")
|
||||
ip = os.getenv("hsd_ip")
|
||||
if api_key is None:
|
||||
print("API key not set")
|
||||
return {"status": "API key not set", "transaction": "None"}
|
||||
response = requests.post(f'http://x:{api_key}@127.0.0.1:12039/wallet/{wallet}/unlock',
|
||||
response = requests.post(f'http://x:{api_key}@{ip}:12039/wallet/{wallet}/unlock',
|
||||
json={'passphrase': password, 'timeout': 600})
|
||||
if response.status_code != 200:
|
||||
print("Failed to unlock wallet")
|
||||
@ -73,11 +74,11 @@ def main(params, authentication):
|
||||
|
||||
batchTX = "[" + ", ".join(batch) + "]"
|
||||
responseContent = f'{{"method": "sendbatch","params":[ {batchTX} ]}}'
|
||||
response = requests.post(f'http://x:{api_key}@127.0.0.1:12039', data=responseContent)
|
||||
response = requests.post(f'http://x:{api_key}@{ip}:12039', data=responseContent)
|
||||
if response.status_code != 200:
|
||||
print("Failed to create batch")
|
||||
print(f'Status code: {response.status_code}')
|
||||
print(f'Response: {response.text}')
|
||||
print("Failed to create batch",flush=True)
|
||||
print(f'Status code: {response.status_code}',flush=True)
|
||||
print(f'Response: {response.text}',flush=True)
|
||||
return {"status": "Failed", "transaction": "None"}
|
||||
|
||||
batch = response.json()
|
||||
@ -85,9 +86,9 @@ def main(params, authentication):
|
||||
print("Verifying tx...")
|
||||
if batch["error"]:
|
||||
if batch["error"] != "":
|
||||
print("Failed to verify batch")
|
||||
print(batch["error"]["message"])
|
||||
return {"status": "Failed", "transaction": "None"}
|
||||
print("Failed to verify batch",flush=True)
|
||||
print(batch["error"]["message"],flush=True)
|
||||
return {"status": f"Failed: {batch['error']['message']}", "transaction": "None"}
|
||||
|
||||
if 'result' in batch:
|
||||
if batch['result'] != None:
|
||||
|
@ -189,6 +189,7 @@ def bidDomains(bids,domains, sortState=False):
|
||||
bidValue = round(bidValue, 2)
|
||||
blind = lockup - bidValue
|
||||
bidValue = "{:,}".format(bidValue)
|
||||
blind = round(blind, 2)
|
||||
blind = "{:,}".format(blind)
|
||||
|
||||
bidDisplay = f'<b>{bidValue} HNS</b> + {blind} HNS blind'
|
||||
@ -198,6 +199,7 @@ def bidDomains(bids,domains, sortState=False):
|
||||
html += f"<td>{domain['name']}</td>"
|
||||
html += f"<td>{domain['state']}</td>"
|
||||
html += f"<td>{bidDisplay}</td>"
|
||||
html += f"<td>{bid['height']}</td>"
|
||||
html += "</tr>"
|
||||
else:
|
||||
for domain in domains:
|
||||
|
@ -35,7 +35,6 @@
|
||||
<li class="nav-item"><a class="nav-link" href="/receive"><i class="material-icons">call_received</i><span>Receive</span></a></li>
|
||||
<li class="nav-item"><a class="nav-link" href="/plugins"><i class="material-icons">code</i><span>Plugins</span></a><a class="nav-link" href="/settings"><i class="material-icons">settings</i><span>Settings</span></a></li>
|
||||
</ul>
|
||||
<div class="text-center d-none d-md-inline"><button class="btn rounded-circle border-0" id="sidebarToggle" type="button"></button></div>
|
||||
</div>
|
||||
</nav>
|
||||
<div class="d-flex flex-column" id="content-wrapper" style="background: var(--bs-primary);">
|
||||
|
@ -35,7 +35,6 @@
|
||||
<li class="nav-item"><a class="nav-link" href="/receive"><i class="material-icons">call_received</i><span>Receive</span></a></li>
|
||||
<li class="nav-item"><a class="nav-link" href="/plugins"><i class="material-icons">code</i><span>Plugins</span></a><a class="nav-link" href="/settings"><i class="material-icons">settings</i><span>Settings</span></a></li>
|
||||
</ul>
|
||||
<div class="text-center d-none d-md-inline"><button class="btn rounded-circle border-0" id="sidebarToggle" type="button"></button></div>
|
||||
</div>
|
||||
</nav>
|
||||
<div class="d-flex flex-column" id="content-wrapper" style="background: var(--bs-primary);">
|
||||
|
@ -35,7 +35,6 @@
|
||||
<li class="nav-item"><a class="nav-link" href="/receive"><i class="material-icons">call_received</i><span>Receive</span></a></li>
|
||||
<li class="nav-item"><a class="nav-link" href="/plugins"><i class="material-icons">code</i><span>Plugins</span></a><a class="nav-link" href="/settings"><i class="material-icons">settings</i><span>Settings</span></a></li>
|
||||
</ul>
|
||||
<div class="text-center d-none d-md-inline"><button class="btn rounded-circle border-0" id="sidebarToggle" type="button"></button></div>
|
||||
</div>
|
||||
</nav>
|
||||
<div class="d-flex flex-column" id="content-wrapper" style="background: var(--bs-primary);">
|
||||
@ -125,6 +124,7 @@
|
||||
<th><a href="/auctions?direction={{sort_domain_next}}">Domain{{sort_domain}}</a></th>
|
||||
<th><a href="/auctions?sort=state&direction={{sort_state_next}}">State{{sort_state}}</a></th>
|
||||
<th><a href="/auctions?sort=price&direction={{sort_price_next}}">Bid{{sort_price}}</a></th>
|
||||
<th><a href="/auctions?sort=time&direction={{sort_time_next}}">Block{{sort_time}}</a></th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
@ -2,7 +2,7 @@
|
||||
<div class="card shadow border-start-warning py-2">
|
||||
<div class="card-body">
|
||||
<div class="text-uppercase fw-bold text-xs mb-1"><span style="color: var(--bs-dark);">{{name}}</span></div>
|
||||
<div class="text-dark fw-bold h5 mb-0"><span>{{output}}</span></div>
|
||||
<div class="text-dark fw-bold h5 mb-0"><span>{{output | safe}}</span></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
@ -2,3 +2,4 @@
|
||||
<span style="display: block;">Check your transaction on a block explorer</span>
|
||||
<a class="card-link" href="https://niami.io/tx/{{tx}}" target="_blank">Niami</a>
|
||||
<a class="card-link" href="https://3xpl.com/handshake/transaction/{{tx}}" target="_blank">3xpl</a>
|
||||
<a class="card-link" href="https://hns.cymon.de/tx/{{tx}}" target="_blank">Cymon.de</a>
|
@ -35,7 +35,6 @@
|
||||
<li class="nav-item"><a class="nav-link" href="/receive"><i class="material-icons">call_received</i><span>Receive</span></a></li>
|
||||
<li class="nav-item"><a class="nav-link" href="/plugins"><i class="material-icons">code</i><span>Plugins</span></a><a class="nav-link" href="/settings"><i class="material-icons">settings</i><span>Settings</span></a></li>
|
||||
</ul>
|
||||
<div class="text-center d-none d-md-inline"><button class="btn rounded-circle border-0" id="sidebarToggle" type="button"></button></div>
|
||||
</div>
|
||||
</nav>
|
||||
<div class="d-flex flex-column" id="content-wrapper" style="background: var(--bs-primary);">
|
||||
|
@ -35,7 +35,6 @@
|
||||
<li class="nav-item"><a class="nav-link" href="/receive"><i class="material-icons">call_received</i><span>Receive</span></a></li>
|
||||
<li class="nav-item"><a class="nav-link" href="/plugins"><i class="material-icons">code</i><span>Plugins</span></a><a class="nav-link" href="/settings"><i class="material-icons">settings</i><span>Settings</span></a></li>
|
||||
</ul>
|
||||
<div class="text-center d-none d-md-inline"><button class="btn rounded-circle border-0" id="sidebarToggle" type="button"></button></div>
|
||||
</div>
|
||||
</nav>
|
||||
<div class="d-flex flex-column" id="content-wrapper" style="background: var(--bs-primary);">
|
||||
|
@ -35,7 +35,6 @@
|
||||
<li class="nav-item"><a class="nav-link" href="/receive"><i class="material-icons">call_received</i><span>Receive</span></a></li>
|
||||
<li class="nav-item"><a class="nav-link" href="/plugins"><i class="material-icons">code</i><span>Plugins</span></a><a class="nav-link" href="/settings"><i class="material-icons">settings</i><span>Settings</span></a></li>
|
||||
</ul>
|
||||
<div class="text-center d-none d-md-inline"><button class="btn rounded-circle border-0" id="sidebarToggle" type="button"></button></div>
|
||||
</div>
|
||||
</nav>
|
||||
<div class="d-flex flex-column" id="content-wrapper" style="background: var(--bs-primary);">
|
||||
|
@ -35,7 +35,6 @@
|
||||
<li class="nav-item"><a class="nav-link" href="/receive"><i class="material-icons">call_received</i><span>Receive</span></a></li>
|
||||
<li class="nav-item"><a class="nav-link" href="/plugins"><i class="material-icons">code</i><span>Plugins</span></a><a class="nav-link" href="/settings"><i class="material-icons">settings</i><span>Settings</span></a></li>
|
||||
</ul>
|
||||
<div class="text-center d-none d-md-inline"><button class="btn rounded-circle border-0" id="sidebarToggle" type="button"></button></div>
|
||||
</div>
|
||||
</nav>
|
||||
<div class="d-flex flex-column" id="content-wrapper" style="background: var(--bs-primary);">
|
||||
|
@ -35,7 +35,6 @@
|
||||
<li class="nav-item"><a class="nav-link" href="/receive"><i class="material-icons">call_received</i><span>Receive</span></a></li>
|
||||
<li class="nav-item"><a class="nav-link" href="/plugins"><i class="material-icons">code</i><span>Plugins</span></a><a class="nav-link" href="/settings"><i class="material-icons">settings</i><span>Settings</span></a></li>
|
||||
</ul>
|
||||
<div class="text-center d-none d-md-inline"><button class="btn rounded-circle border-0" id="sidebarToggle" type="button"></button></div>
|
||||
</div>
|
||||
</nav>
|
||||
<div class="d-flex flex-column" id="content-wrapper" style="background: var(--bs-primary);">
|
||||
|
@ -35,7 +35,6 @@
|
||||
<li class="nav-item"><a class="nav-link" href="/receive"><i class="material-icons">call_received</i><span>Receive</span></a></li>
|
||||
<li class="nav-item"><a class="nav-link" href="/plugins"><i class="material-icons">code</i><span>Plugins</span></a><a class="nav-link" href="/settings"><i class="material-icons">settings</i><span>Settings</span></a></li>
|
||||
</ul>
|
||||
<div class="text-center d-none d-md-inline"><button class="btn rounded-circle border-0" id="sidebarToggle" type="button"></button></div>
|
||||
</div>
|
||||
</nav>
|
||||
<div class="d-flex flex-column" id="content-wrapper" style="background: var(--bs-primary);">
|
||||
|
@ -35,7 +35,6 @@
|
||||
<li class="nav-item"><a class="nav-link" href="/receive"><i class="material-icons">call_received</i><span>Receive</span></a></li>
|
||||
<li class="nav-item"><a class="nav-link" href="/plugins"><i class="material-icons">code</i><span>Plugins</span></a><a class="nav-link" href="/settings"><i class="material-icons">settings</i><span>Settings</span></a></li>
|
||||
</ul>
|
||||
<div class="text-center d-none d-md-inline"><button class="btn rounded-circle border-0" id="sidebarToggle" type="button"></button></div>
|
||||
</div>
|
||||
</nav>
|
||||
<div class="d-flex flex-column" id="content-wrapper" style="background: var(--bs-primary);">
|
||||
|
@ -35,7 +35,6 @@
|
||||
<li class="nav-item"><a class="nav-link" href="/receive"><i class="material-icons">call_received</i><span>Receive</span></a></li>
|
||||
<li class="nav-item"><a class="nav-link" href="/plugins"><i class="material-icons">code</i><span>Plugins</span></a><a class="nav-link" href="/settings"><i class="material-icons">settings</i><span>Settings</span></a></li>
|
||||
</ul>
|
||||
<div class="text-center d-none d-md-inline"><button class="btn rounded-circle border-0" id="sidebarToggle" type="button"></button></div>
|
||||
</div>
|
||||
</nav>
|
||||
<div class="d-flex flex-column" id="content-wrapper" style="background: var(--bs-primary);">
|
||||
@ -67,7 +66,7 @@
|
||||
<div class="container-fluid" style="margin-bottom: 20px;">
|
||||
<h3 class="text-dark mb-1">{{name}}</h3>
|
||||
<h4 class="text-dark mb-1">{{description}}</h4>
|
||||
<h6 class="text-dark mb-1">Author: {{author}}<br>Version: {{version}}</h6>{{functions|safe}}
|
||||
<h6 class="text-dark mb-1">Author: {{author}}<br>Version: {{version}}<br>Source: {{source}}</h6>{{functions|safe}}
|
||||
</div>
|
||||
</div>
|
||||
<footer class="sticky-footer" style="background: var(--bs-primary-text-emphasis);">
|
||||
|
@ -35,7 +35,6 @@
|
||||
<li class="nav-item"><a class="nav-link" href="/receive"><i class="material-icons">call_received</i><span>Receive</span></a></li>
|
||||
<li class="nav-item"><a class="nav-link" href="/plugins"><i class="material-icons">code</i><span>Plugins</span></a><a class="nav-link" href="/settings"><i class="material-icons">settings</i><span>Settings</span></a></li>
|
||||
</ul>
|
||||
<div class="text-center d-none d-md-inline"><button class="btn rounded-circle border-0" id="sidebarToggle" type="button"></button></div>
|
||||
</div>
|
||||
</nav>
|
||||
<div class="d-flex flex-column" id="content-wrapper" style="background: var(--bs-primary);">
|
||||
|
@ -35,7 +35,6 @@
|
||||
<li class="nav-item"><a class="nav-link" href="/receive"><i class="material-icons">call_received</i><span>Receive</span></a></li>
|
||||
<li class="nav-item"><a class="nav-link" href="/plugins"><i class="material-icons">code</i><span>Plugins</span></a><a class="nav-link" href="/settings"><i class="material-icons">settings</i><span>Settings</span></a></li>
|
||||
</ul>
|
||||
<div class="text-center d-none d-md-inline"><button class="btn rounded-circle border-0" id="sidebarToggle" type="button"></button></div>
|
||||
</div>
|
||||
</nav>
|
||||
<div class="d-flex flex-column" id="content-wrapper" style="background: var(--bs-primary);">
|
||||
|
@ -35,7 +35,6 @@
|
||||
<li class="nav-item"><a class="nav-link" href="/receive"><i class="material-icons">call_received</i><span>Receive</span></a></li>
|
||||
<li class="nav-item"><a class="nav-link" href="/plugins"><i class="material-icons">code</i><span>Plugins</span></a><a class="nav-link" href="/settings"><i class="material-icons">settings</i><span>Settings</span></a></li>
|
||||
</ul>
|
||||
<div class="text-center d-none d-md-inline"><button class="btn rounded-circle border-0" id="sidebarToggle" type="button"></button></div>
|
||||
</div>
|
||||
</nav>
|
||||
<div class="d-flex flex-column" id="content-wrapper" style="background: var(--bs-primary);">
|
||||
|
@ -35,7 +35,6 @@
|
||||
<li class="nav-item"><a class="nav-link" href="/receive"><i class="material-icons">call_received</i><span>Receive</span></a></li>
|
||||
<li class="nav-item"><a class="nav-link" href="/plugins"><i class="material-icons">code</i><span>Plugins</span></a><a class="nav-link" href="/settings"><i class="material-icons">settings</i><span>Settings</span></a></li>
|
||||
</ul>
|
||||
<div class="text-center d-none d-md-inline"><button class="btn rounded-circle border-0" id="sidebarToggle" type="button"></button></div>
|
||||
</div>
|
||||
</nav>
|
||||
<div class="d-flex flex-column" id="content-wrapper" style="background: var(--bs-primary);">
|
||||
|
@ -36,7 +36,6 @@
|
||||
<li class="nav-item"><a class="nav-link" href="/receive"><i class="material-icons">call_received</i><span>Receive</span></a></li>
|
||||
<li class="nav-item"><a class="nav-link" href="/plugins"><i class="material-icons">code</i><span>Plugins</span></a><a class="nav-link" href="/settings"><i class="material-icons">settings</i><span>Settings</span></a></li>
|
||||
</ul>
|
||||
<div class="text-center d-none d-md-inline"><button class="btn rounded-circle border-0" id="sidebarToggle" type="button"></button></div>
|
||||
</div>
|
||||
</nav>
|
||||
<div class="d-flex flex-column" id="content-wrapper" style="background: var(--bs-primary);">
|
||||
|
@ -35,7 +35,6 @@
|
||||
<li class="nav-item"><a class="nav-link" href="/receive"><i class="material-icons">call_received</i><span>Receive</span></a></li>
|
||||
<li class="nav-item"><a class="nav-link" href="/plugins"><i class="material-icons">code</i><span>Plugins</span></a><a class="nav-link" href="/settings"><i class="material-icons">settings</i><span>Settings</span></a></li>
|
||||
</ul>
|
||||
<div class="text-center d-none d-md-inline"><button class="btn rounded-circle border-0" id="sidebarToggle" type="button"></button></div>
|
||||
</div>
|
||||
</nav>
|
||||
<div class="d-flex flex-column" id="content-wrapper" style="background: var(--bs-primary);">
|
||||
|
@ -35,7 +35,6 @@
|
||||
<li class="nav-item"><a class="nav-link" href="/receive"><i class="material-icons">call_received</i><span>Receive</span></a></li>
|
||||
<li class="nav-item"><a class="nav-link" href="/plugins"><i class="material-icons">code</i><span>Plugins</span></a><a class="nav-link" href="/settings"><i class="material-icons">settings</i><span>Settings</span></a></li>
|
||||
</ul>
|
||||
<div class="text-center d-none d-md-inline"><button class="btn rounded-circle border-0" id="sidebarToggle" type="button"></button></div>
|
||||
</div>
|
||||
</nav>
|
||||
<div class="d-flex flex-column" id="content-wrapper" style="background: var(--bs-primary);">
|
||||
|
Loading…
Reference in New Issue
Block a user