diff --git a/.gitignore b/.gitignore
index 9a6d587..9a72b84 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,3 +2,9 @@
.env
__pycache__/
+
+templates/assets/css/styles.min.css
+
+ignore/
+
+plugins/signatures.json
diff --git a/FireWalletBrowser.bsdesign b/FireWalletBrowser.bsdesign
new file mode 100644
index 0000000..685900f
Binary files /dev/null and b/FireWalletBrowser.bsdesign differ
diff --git a/README.md b/README.md
index 2d2399f..35db089 100644
--- a/README.md
+++ b/README.md
@@ -40,6 +40,32 @@ 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
```
+## Features
+- Basic wallet functionality
+ - Create new wallet
+ - Import wallet from seed
+ - Send HNS
+ - Receive HNS
+ - Have multiple wallets
+ - View transactions
+ - View balance
+ - View wallet domains
+- Domain management
+ - Transfer domains
+ - DNS Editor
+ - Renew domains
+- Auctions
+ - Send open
+ - Send bid
+ - Send reveal
+ - Send redeem
+- Download a list of all domains
+- Resend all pending transactions
+- Rescan
+- Zap pending transactions
+- View xPub
+- Custom plugin support
+
## Themes
Set a theme in the .env file
**Available themes**
diff --git a/account.py b/account.py
index 0b14571..16e12ec 100644
--- a/account.py
+++ b/account.py
@@ -84,6 +84,31 @@ def createWallet(account: str, password: str):
"password": password
}
+def importWallet(account: str, password: str,seed: str):
+ # Import the wallet
+ data = {
+ "passphrase": password,
+ "mnemonic": seed,
+ }
+
+ response = requests.put(f"http://x:{APIKEY}@{ip}:12039/wallet/{account}",json=data)
+ print(response)
+ print(response.json())
+
+ if response.status_code != 200:
+ return {
+ "error": {
+ "message": "Error creating account"
+ }
+ }
+
+ return {
+ "seed": seed,
+ "account": account,
+ "password": password
+ }
+
+
def listWallets():
# List the wallets
response = hsw.listWallets()
@@ -106,6 +131,12 @@ def getBalance(account: str):
total = total / 1000000
available = available / 1000000
+ domains = getDomains(account)
+ domainValue = 0
+ for domain in domains:
+ domainValue += domain['value']
+ total = total - (domainValue/1000000)
+
# Only keep 2 decimal places
total = round(total, 2)
available = round(available, 2)
@@ -292,7 +323,11 @@ def setDNS(account,domain,records):
TXTRecords = []
for record in records:
if record['type'] == 'TXT':
- TXTRecords.append(record['value'])
+ if 'txt' not in record:
+ TXTRecords.append(record['value'])
+ else:
+ for txt in record['txt']:
+ TXTRecords.append(txt)
elif record['type'] == 'NS':
newRecords.append({
'type': 'NS',
@@ -461,7 +496,21 @@ def finalize(account,domain):
}
try:
- response = hsw.sendFINALIZE(account_name,password,domain)
+ 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 = hsw.rpc_sendFINALIZE(domain)
return response
except Exception as e:
return {
@@ -624,11 +673,46 @@ def getxPub(account):
}
+def signMessage(account,domain,message):
+ 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 = hsw.rpc_signMessageWithName(domain,message)
+ return response
+ except Exception as e:
+ return {
+ "error": {
+ "message": str(e)
+ }
+ }
+
#endregion
-def generateReport(account):
+def generateReport(account,format="{name},{expiry},{value},{maxBid}"):
domains = getDomains(account)
- format = str('{name},{expiry},{value},{maxBid}')
lines = [format.replace("{","").replace("}","")]
for domain in domains:
@@ -650,4 +734,7 @@ def generateReport(account):
line = line.replace("{openHeight}",str(domain['height']))
lines.append(line)
- return lines
\ No newline at end of file
+ return lines
+
+def convertHNS(value: int):
+ return value/1000000
\ No newline at end of file
diff --git a/main.py b/main.py
index 46d0f03..4e6a6d4 100644
--- a/main.py
+++ b/main.py
@@ -10,6 +10,8 @@ import re
from flask_qrcode import QRcode
import domainLookup
import urllib.parse
+import importlib
+import plugin as plugins_module
dotenv.load_dotenv()
@@ -88,12 +90,21 @@ def index():
domain_count = len(domains)
+ domainsMobile = render.domains(domains,True)
domains = render.domains(domains)
+ 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"]))
+
+
return render_template("index.html", account=account, available=available,
total=total, pending=pending, domains=domains,
+ domainsMobile=domainsMobile, plugins=plugins,
domain_count=domain_count, sync=account_module.getNodeSync(),
sort_price=sort_price,sort_expiry=sort_expiry,
sort_domain=sort_domain,sort_price_next=sort_price_next,
@@ -264,6 +275,9 @@ def search():
search_term = request.args.get("q")
search_term = search_term.lower().strip()
+
+ # Replace spaces with hyphens
+ search_term = search_term.replace(" ","-")
# Convert emoji to punycode
search_term = domainLookup.emoji_to_punycode(search_term)
@@ -272,14 +286,23 @@ def search():
domain = account_module.getDomain(search_term)
+ plugins = "
"
+ # Execute domain plugins
+ searchFunctions = plugins_module.getSearchFunctions()
+ for function in searchFunctions:
+ functionOutput = plugins_module.runPluginFunction(function["plugin"],function["function"],{"domain":search_term},account_module.check_account(request.cookies.get("account")))
+ plugins += render.plugin_output(functionOutput,plugins_module.getPluginFunctionReturns(function["plugin"],function["function"]))
+
+ plugins += "
"
+
if 'error' in domain:
return render_template("search.html", account=account,sync=account_module.getNodeSync(),
- search_term=search_term, domain=domain['error'])
+ search_term=search_term, domain=domain['error'],plugins=plugins)
if domain['info'] is None:
return render_template("search.html", account=account, sync=account_module.getNodeSync(),
search_term=search_term,domain=search_term,
- state="AVAILABLE", next="Available Now")
+ state="AVAILABLE", next="Available Now",plugins=plugins)
state = domain['info']['state']
if state == 'CLOSED':
@@ -319,10 +342,11 @@ def search():
dns = render.dns(dns)
txs = render.txs(txs)
+
return render_template("search.html", account=account, sync=account_module.getNodeSync(),
search_term=search_term,domain=domain['info']['name'],
raw=domain,state=state, next=next, owner=owner,
- dns=dns, txs=txs)
+ dns=dns, txs=txs,plugins=plugins)
@app.route('/manage/')
def manage(domain: str):
@@ -370,11 +394,21 @@ def manage(domain: str):
else:
finalize_time = "now"
+ plugins = ""
+ # Execute domain plugins
+ domainFunctions = plugins_module.getDomainFunctions()
+ for function in domainFunctions:
+ functionOutput = plugins_module.runPluginFunction(function["plugin"],function["function"],{"domain":domain},account_module.check_account(request.cookies.get("account")))
+ plugins += render.plugin_output(functionOutput,plugins_module.getPluginFunctionReturns(function["plugin"],function["function"]))
+
+ plugins += "
"
+
+
return render_template("manage.html", account=account, sync=account_module.getNodeSync(),
error=errorMessage, address=address,
domain=domain,expiry=expiry, dns=dns,
raw_dns=urllib.parse.quote(raw_dns),
- finalize_time=finalize_time)
+ finalize_time=finalize_time,plugins=plugins)
@app.route('/manage//finalize')
@@ -390,11 +424,11 @@ def finalize(domain: str):
domain = domain.lower()
print(domain)
response = account_module.finalize(request.cookies.get("account"),domain)
- if 'error' in response:
+ if response['error'] != None:
print(response)
return redirect("/manage/" + domain + "?error=" + response['error']['message'])
- return redirect("/success?tx=" + response['hash'])
+ return redirect("/success?tx=" + response['result']['hash'])
@app.route('/manage//cancel')
def cancelTransfer(domain: str):
@@ -607,6 +641,47 @@ def transfer(domain):
sync=account_module.getNodeSync(),action=action,
content=content,cancel=cancel,confirm=confirm)
+@app.route('/manage//sign')
+def signMessage(domain):
+ if request.cookies.get("account") is None:
+ return redirect("/login")
+
+ account = account_module.check_account(request.cookies.get("account"))
+ if not account:
+ return redirect("/logout")
+
+ # Get the address and amount
+ message = request.args.get("message")
+
+ if message is None:
+ return redirect("/manage/" + domain + "?error=Invalid message")
+
+
+ content = "Message to sign:" + message + "
"
+ signedMessage = account_module.signMessage(request.cookies.get("account"),domain,message)
+ if signedMessage["error"] != None:
+ return redirect("/manage/" + domain + "?error=" + signedMessage["error"])
+ content += "Signature:" + signedMessage["result"] + "
"
+
+ data = {
+ "domain": domain,
+ "message": message,
+ "signature": signedMessage["result"]
+ }
+
+ content += "Full information:" + json.dumps(data,indent=4).replace('\n'," ") + "
"
+
+ content += ""
+
+ copyScript = ""
+ content += "Copy to clipboard " + copyScript
+
+
+
+ return render_template("message.html", account=account,sync=account_module.getNodeSync(),
+ title="Sign Message",content=content)
+
+
@app.route('/manage//transfer/confirm')
def transferConfirm(domain):
if request.cookies.get("account") is None:
@@ -741,18 +816,21 @@ def bid(domain):
if blind == "":
blind = 0
+ bid = float(bid)
+ blind = float(blind)
+
if bid+blind == 0:
return redirect("/auction/" + domain+ "?message=Invalid bid amount")
# Show confirm page
- total = float(bid) + float(blind)
+ total = bid + blind
action = f"Bid on {domain}/"
content = f"Are you sure you want to bid on {domain}/?"
content += "You are about to bid with the following details: "
- content += f"Bid: {request.args.get('bid')} HNS "
- content += f"Blind: {request.args.get('blind')} HNS "
+ content += f"Bid: {str(bid)} HNS "
+ content += f"Blind: {str(blind)} HNS "
content += f"Total: {total} HNS (excluding fees) "
cancel = f"/auction/{domain}"
@@ -775,11 +853,22 @@ def bid_confirm(domain):
return redirect("/logout")
domain = domain.lower()
+ bid = request.args.get("bid")
+ blind = request.args.get("blind")
+
+ if bid == "":
+ bid = 0
+ if blind == "":
+ blind = 0
+
+ bid = float(bid)
+ blind = float(blind)
+
# Send the bid
response = account_module.bid(request.cookies.get("account"),domain,
- float(request.args.get('bid')),
- float(request.args.get('blind')))
+ float(bid),
+ float(blind))
print(response)
if 'error' in response:
return redirect("/auction/" + domain + "?message=" + response['error']['message'])
@@ -820,8 +909,8 @@ def reveal_auction(domain):
return redirect("/auction/" + domain + "?message=" + response['error']['message'])
return redirect("/success?tx=" + response['hash'])
-
-#region settings
+#endregion
+#region Settings
@app.route('/settings')
def settings():
# Check if the user is logged in
@@ -870,14 +959,20 @@ def settings_action(action):
return redirect("/settings?error=" + str(resp['error']))
return redirect("/settings?success=Zapped transactions")
elif action == "xpub":
+ xpub = account_module.getxPub(request.cookies.get("account"))
+ content = " "
+ content += ""
+ content += ""
+ content += "Copy to clipboard "
+
return render_template("message.html", account=account,sync=account_module.getNodeSync(),
- title="xPub Key",content=account_module.getxPub(request.cookies.get("account")))
+ title="xPub Key",
+ content=""+xpub+"
" + content)
return redirect("/settings?error=Invalid action")
#endregion
-#endregion
#region Account
@@ -966,6 +1061,50 @@ def register():
response.set_cookie("account", account+":"+password)
return response
+@app.route('/import-wallet', methods=["POST"])
+def import_wallet():
+ # Get the account and password
+ account = request.form.get("name")
+ password = request.form.get("password")
+ repeatPassword = request.form.get("password_repeat")
+ seed = request.form.get("seed")
+
+ # Check if the passwords match
+ if password != repeatPassword:
+ return render_template("import-wallet.html",
+ error="Passwords do not match",
+ name=account,password=password,password_repeat=repeatPassword,
+ seed=seed)
+
+ # Check if the account is valid
+ if account.count(":") > 0:
+ return render_template("import-wallet.html",
+ error="Invalid account",
+ name=account,password=password,password_repeat=repeatPassword,
+ seed=seed)
+
+ # List wallets
+ wallets = account_module.listWallets()
+ if account in wallets:
+ return render_template("import-wallet.html",
+ error="Account already exists",
+ name=account,password=password,password_repeat=repeatPassword,
+ seed=seed)
+
+ # Create the account
+ response = account_module.importWallet(account,password,seed)
+
+ if 'error' in response:
+ return render_template("import-wallet.html",
+ error=response['error'],
+ name=account,password=password,password_repeat=repeatPassword,
+ seed=seed)
+
+
+ # Set the cookie
+ response = make_response(redirect("/"))
+ response.set_cookie("account", account+":"+password)
+ return response
@app.route('/report')
def report():
@@ -979,6 +1118,126 @@ def report():
#endregion
+#region Plugins
+@app.route('/plugins')
+def plugins_index():
+ # Check if the user is logged in
+ if request.cookies.get("account") is None:
+ return redirect("/login")
+
+ account = account_module.check_account(request.cookies.get("account"))
+ if not account:
+ return redirect("/logout")
+
+ plugins = render.plugins(plugins_module.listPlugins())
+
+ return render_template("plugins.html", account=account, sync=account_module.getNodeSync(),
+ plugins=plugins)
+
+@app.route('/plugin/')
+def plugin(plugin):
+ # Check if the user is logged in
+ if request.cookies.get("account") is None:
+ return redirect("/login")
+
+ account = account_module.check_account(request.cookies.get("account"))
+ if not account:
+ return redirect("/logout")
+
+ if not plugins_module.pluginExists(plugin):
+ return redirect("/plugins")
+
+ data = plugins_module.getPluginData(plugin)
+
+ functions = plugins_module.getPluginFunctions(plugin)
+ functions = render.plugin_functions(functions,plugin)
+
+ if data['verified'] == False:
+ functions = "This plugin is not verified and is disabled for your protection. Please check the code before marking the plugin as verified
Verify " + functions
+
+
+ error = request.args.get("error")
+ if error == None:
+ error = ""
+
+ return render_template("plugin.html", account=account, sync=account_module.getNodeSync(),
+ name=data['name'],description=data['description'],
+ author=data['author'],version=data['version'],
+ functions=functions,error=error)
+
+@app.route('/plugin//verify')
+def plugin_verify(plugin):
+ # Check if the user is logged in
+ if request.cookies.get("account") is None:
+ return redirect("/login")
+
+ account = account_module.check_account(request.cookies.get("account"))
+ if not account:
+ return redirect("/logout")
+
+ if not plugins_module.pluginExists(plugin):
+ return redirect("/plugins")
+
+ data = plugins_module.getPluginData(plugin)
+
+ if data['verified'] == False:
+ plugins_module.verifyPlugin(plugin)
+
+ return redirect("/plugin/" + plugin)
+
+@app.route('/plugin//', methods=["POST"])
+def plugin_function(plugin,function):
+ # Check if the user is logged in
+ if request.cookies.get("account") is None:
+ return redirect("/login")
+
+ account = account_module.check_account(request.cookies.get("account"))
+ if not account:
+ return redirect("/logout")
+
+ if not plugins_module.pluginExists(plugin):
+ return redirect("/plugins")
+
+ data = plugins_module.getPluginData(plugin)
+
+ # Get plugin/main.py listfunctions()
+ if function in plugins_module.getPluginFunctions(plugin):
+ inputs = plugins_module.getPluginFunctionInputs(plugin,function)
+ request_data = {}
+ for input in inputs:
+ request_data[input] = request.form.get(input)
+
+ if inputs[input]['type'] == "address":
+ # Handle hip2
+ address_check = account_module.check_address(request_data[input],True,True)
+ if not address_check:
+ return redirect("/plugin/" + plugin + "?error=Invalid address")
+ request_data[input] = address_check
+ elif inputs[input]['type'] == "dns":
+ # Handle URL encoding of DNS
+ request_data[input] = urllib.parse.unquote(request_data[input])
+
+
+
+
+ response = plugins_module.runPluginFunction(plugin,function,request_data,request.cookies.get("account"))
+ if not response:
+ return redirect("/plugin/" + plugin + "?error=An error occurred")
+ if 'error' in response:
+ 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(),
+ name=data['name'],description=data['description'],output=response)
+
+
+ else:
+ return jsonify({"error": "Function not found"})
+
+#endregion
+
+
#region Assets and default pages
@app.route('/qr/')
def qr(data):
@@ -987,7 +1246,8 @@ def qr(data):
# Theme
@app.route('/assets/css/styles.min.css')
def send_css():
- print("Using theme: " + theme)
+ if theme == "live":
+ return send_from_directory('templates/assets/css', 'styles.min.css')
return send_from_directory('themes', f'{theme}.css')
@app.route('/assets/')
diff --git a/plugin.py b/plugin.py
new file mode 100644
index 0000000..b7bb4cf
--- /dev/null
+++ b/plugin.py
@@ -0,0 +1,193 @@
+import os
+import json
+import importlib
+import sys
+import hashlib
+
+
+
+def listPlugins():
+ plugins = []
+ for file in os.listdir("plugins"):
+ if file.endswith(".py"):
+ if file != "main.py":
+ plugin = importlib.import_module("plugins."+file[:-3])
+ details = plugin.info
+ details["link"] = file[:-3]
+ plugins.append(details)
+
+ # Verify plugin signature
+ signatures = []
+ try:
+ with open("plugins/signatures.json", "r") as f:
+ signatures = json.load(f)
+ except:
+ # Write a new signatures file
+ with open("plugins/signatures.json", "w") as f:
+ json.dump(signatures, f)
+
+ for plugin in plugins:
+ # Hash the plugin file
+ pluginHash = hashPlugin(plugin["link"])
+ if pluginHash not in signatures:
+ plugin["verified"] = False
+ else:
+ plugin["verified"] = True
+
+ return plugins
+
+
+def pluginExists(plugin: str):
+ for file in os.listdir("plugins"):
+ if file == plugin+".py":
+ return True
+ return False
+
+def verifyPlugin(plugin: str):
+ signatures = []
+ try:
+ with open("plugins/signatures.json", "r") as f:
+ signatures = json.load(f)
+ except:
+ # Write a new signatures file
+ with open("plugins/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:
+ json.dump(signatures, f)
+
+
+def hashPlugin(plugin: str):
+ BUF_SIZE = 65536
+ sha256 = hashlib.sha256()
+ with open("plugins/"+plugin+".py", 'rb') as f:
+ while True:
+ data = f.read(BUF_SIZE)
+ if not data:
+ break
+ sha256.update(data)
+ return sha256.hexdigest()
+
+
+
+
+
+
+
+
+def getPluginData(pluginStr: str):
+ plugin = importlib.import_module("plugins."+pluginStr)
+
+ # Check if the plugin is verified
+ signatures = []
+ try:
+ with open("plugins/signatures.json", "r") as f:
+ signatures = json.load(f)
+ except:
+ # Write a new signatures file
+ with open("plugins/signatures.json", "w") as f:
+ json.dump(signatures, f)
+
+ info = plugin.info
+ # Hash the plugin file
+ pluginHash = hashPlugin(pluginStr)
+ if pluginHash not in signatures:
+ info["verified"] = False
+ else:
+ info["verified"] = True
+
+ return info
+
+def getPluginFunctions(plugin: str):
+ plugin = importlib.import_module("plugins."+plugin)
+ return plugin.functions
+
+def runPluginFunction(plugin: str, function: str, params: dict, authentication: str):
+ plugin_module = importlib.import_module("plugins."+plugin)
+ if function not in plugin_module.functions:
+ return {"error": "Function not found"}
+
+ if not hasattr(plugin_module, function):
+ return {"error": "Function not found"}
+
+ # Get the function object from the plugin module
+ plugin_function = getattr(plugin_module, function)
+
+ # Check if the function is in the signature list
+ signatures = []
+ try:
+ with open("plugins/signatures.json", "r") as f:
+ signatures = json.load(f)
+ except:
+ # Write a new signatures file
+ with open("plugins/signatures.json", "w") as f:
+ json.dump(signatures, f)
+
+ # Hash the plugin file
+ pluginHash = hashPlugin(plugin)
+ if pluginHash not in signatures:
+ return {"error": "Plugin not verified"}
+
+
+ # Call the function with provided parameters
+ try:
+ result = plugin_function(params, authentication)
+ return result
+ except Exception as e:
+ print(f"Error running plugin: {e}")
+ return {"error": str(e)}
+ # return plugin.runFunction(function, params, authentication)
+
+def getPluginFunctionInputs(plugin: str, function: str):
+ plugin = importlib.import_module("plugins."+plugin)
+ return plugin.functions[function]["params"]
+
+def getPluginFunctionReturns(plugin: str, function: str):
+ plugin = importlib.import_module("plugins."+plugin)
+ return plugin.functions[function]["returns"]
+
+def getDomainFunctions():
+ plugins = listPlugins()
+ domainFunctions = []
+ for plugin in plugins:
+ functions = getPluginFunctions(plugin["link"])
+ for function in functions:
+ if functions[function]["type"] == "domain":
+ domainFunctions.append({
+ "plugin": plugin["link"],
+ "function": function,
+ "description": functions[function]["description"]
+ })
+ return domainFunctions
+
+def getSearchFunctions():
+ plugins = listPlugins()
+ searchFunctions = []
+ for plugin in plugins:
+ functions = getPluginFunctions(plugin["link"])
+ for function in functions:
+ if functions[function]["type"] == "search":
+ searchFunctions.append({
+ "plugin": plugin["link"],
+ "function": function,
+ "description": functions[function]["description"]
+ })
+ return searchFunctions
+
+def getDashboardFunctions():
+ plugins = listPlugins()
+ dashboardFunctions = []
+ for plugin in plugins:
+ functions = getPluginFunctions(plugin["link"])
+ for function in functions:
+ if functions[function]["type"] == "dashboard":
+ dashboardFunctions.append({
+ "plugin": plugin["link"],
+ "function": function,
+ "description": functions[function]["description"]
+ })
+ return dashboardFunctions
\ No newline at end of file
diff --git a/plugins.md b/plugins.md
new file mode 100644
index 0000000..1f399fe
--- /dev/null
+++ b/plugins.md
@@ -0,0 +1,111 @@
+# Plugins
+
+Plugins can be created to add more functionality to FireWallet Browser
+
+
+## Format
+They are created in python and use the format:
+
+```python
+info = {
+ "name": "Plugin Name",
+ "description": "Plugin Description",
+ "version": "Version number",
+ "author": "Your Name",
+}
+functions = {
+ "internalName":{
+ "name": "Human readable name",
+ "type": "Type of plugin",
+ "description": "Function description",
+ "params": { # For plugins other than default use {} for no params
+ "paramName": {
+ "name":"Human readable paramiter name",
+ "type":"type of paramiter",
+ }
+ },
+ "returns": {
+ "returnName":
+ {
+ "name": "Human readable return name",
+ "type": "type of return"
+ }
+ }
+ }
+}
+
+def internalName(params, authentication): # This should always have the same inputs
+ paramName = params["paramName"]
+ wallet = authentication.split(":")[0]
+
+ # Do stuff
+ output = "Return value of stuff: " + paramName
+
+
+
+ return {"returnName": output}
+
+```
+
+
+## Types
+### Default
+Type: `default`
+This is the default type and is used when no type is specified.
+This type is displayed in the plugin page only.
+This is the onlu type of plugin that takes user input
+
+### Manage & Search
+For manage page use type: `domain`
+For search page use type: `search`
+
+This type is used for domain plugins. It shows in the manage domain page or the search page.
+It gets the `domain` paramater as the only input (in addition to authentication)
+
+### Dashboard
+Type: `dashboard`
+This type is used for dashboard plugins.
+It shows in the dashboard page. It doesn't get any inputs other than the authentication
+
+
+## Inputs
+
+### Plain Text
+Type: `text`
+
+### Long Text
+Type: `longText`
+
+### Number
+Type: `number`
+
+
+### Checkbox
+Type: `checkbox`
+
+### Address
+Type: `address`
+This will handle hip2 resolution for you so the function will always receive a valid address
+
+### DNS
+Type: `dns`
+This isn't done yet but use it over text as it includes parsing
+
+
+
+## Outputs
+### Plain Text
+Type: `text`
+
+
+### List
+Type: `list`
+This is a list if text items (or HTML items)
+
+### Transaction hash
+Type: `tx`
+This will display the hash and links to explorers
+
+### DNS records
+Type: `dns`
+This will display DNS in a table format
diff --git a/plugins/automations.py b/plugins/automations.py
new file mode 100644
index 0000000..38f8e50
--- /dev/null
+++ b/plugins/automations.py
@@ -0,0 +1,83 @@
+import json
+import account
+import requests
+import threading
+import os
+import datetime
+
+APIKEY = os.environ.get("hsd_api")
+ip = os.getenv("hsd_ip")
+if ip is None:
+ ip = "localhost"
+
+
+# Plugin Data
+info = {
+ "name": "Automations",
+ "description": "This plugin will automatically renew domains, reveal and redeem bids.",
+ "version": "1.0",
+ "author": "Nathan.Woodburn/"
+}
+
+
+# Functions
+functions = {
+ "automation":{
+ "name": "Function to automate",
+ "type": "dashboard",
+ "description": "This used type dashboard to trigger the function whenever you access the dashboard.",
+ "params": {},
+ "returns": {
+ "Status":
+ {
+ "name": "Status of the automation",
+ "type": "text"
+ }
+ }
+ }
+}
+
+started = 0
+
+# Main entry point only lets the main function run every 5 mins
+def automation(params, authentication):
+ global started
+ now = datetime.datetime.now().timestamp()
+ # Add 5 mins
+ now = now - 300
+ if now < started:
+ return {"Status": "Waiting before checking for new actions"}
+ started = datetime.datetime.now().timestamp()
+ threading.Thread(target=automations_background, args=(authentication,)).start()
+ return {"Status": "Checking for actions"}
+
+# Background function to run the automations
+def automations_background(authentication):
+ print("Running automations")
+ # Get account details
+ account_name = account.check_account(authentication)
+ password = ":".join(authentication.split(":")[1:])
+
+ if account_name == False:
+ return {
+ "error": {
+ "message": "Invalid account"
+ }
+ }
+
+ try:
+ # Try to select and login to the wallet
+ response = account.hsw.rpc_selectWallet(account_name)
+ if response['error'] is not None:
+ return
+ response = account.hsw.rpc_walletPassphrase(password,10)
+ if response['error'] is not None:
+ return
+ # Try to send the batch of all renew, reveal and redeem actions
+ response = requests.post(f"http://x:{APIKEY}@{ip}:12039",json={
+ "method": "sendbatch",
+ "params": [[["RENEW"], ["REVEAL"], ["REDEEM"]]]
+ }).json()
+ print(response)
+ except Exception as e:
+ print(e)
\ No newline at end of file
diff --git a/plugins/example.py b/plugins/example.py
new file mode 100644
index 0000000..bcf5cfa
--- /dev/null
+++ b/plugins/example.py
@@ -0,0 +1,175 @@
+import json
+import account
+import requests
+
+
+# Plugin Data
+info = {
+ "name": "Example Plugin",
+ "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"
+ }
+ }
+ },
+ "transfer":{
+ "name": "Bulk Transfer Domains",
+ "type": "default",
+ "description": "Transfer domains to another wallet",
+ "params": {
+ "address": {
+ "name":"Address to transfer to",
+ "type":"address"
+ },
+ "domains": {
+ "name":"List of domains to transfer",
+ "type":"longText"
+ }
+ },
+ "returns": {
+ "hash": {
+ "name": "Hash of the transaction",
+ "type": "tx"
+ },
+ "address":{
+ "name": "Address of the new owner",
+ "type": "text"
+ }
+ }
+ },
+ "dns":{
+ "name": "Set DNS for Domains",
+ "type": "default",
+ "description": "Set DNS for domains",
+ "params": {
+ "domains": {
+ "name":"List of domains to set DNS for",
+ "type":"longText"
+ },
+ "dns": {
+ "name":"DNS",
+ "type":"dns"
+ }
+ },
+ "returns": {
+ "hash": {
+ "name": "Hash of the transaction",
+ "type": "tx"
+ },
+ "dns":{
+ "name": "DNS",
+ "type": "dns"
+ }
+ }
+ },
+ "niami": {
+ "name": "Niami info",
+ "type": "domain",
+ "description": "Check the domains niami rating",
+ "params": {},
+ "returns": {
+ "rating":
+ {
+ "name": "Niami Rating",
+ "type": "text"
+ }
+ }
+ },
+ "niamiSearch": {
+ "name": "Niami info",
+ "type": "search",
+ "description": "Check the domains niami rating",
+ "params": {},
+ "returns": {
+ "rating":
+ {
+ "name": "Niami Rating",
+ "type": "text"
+ }
+ }
+ },
+ "connections":{
+ "name": "HSD Connections",
+ "type": "dashboard",
+ "description": "Show the number of connections the HSD node is connected to",
+ "params": {},
+ "returns": {
+ "connections":
+ {
+ "name": "HSD Connections",
+ "type": "text"
+ }
+ }
+ }
+}
+
+def check(params, authentication):
+ domains = params["domains"]
+ domains = domains.splitlines()
+
+ wallet = authentication.split(":")[0]
+ owned = account.getDomains(wallet)
+ # Only keep owned domains ["name"]
+ ownedNames = [domain["name"] for domain in owned]
+
+ domains = [domain for domain in domains if domain in ownedNames]
+
+
+ return {"domains": domains}
+
+def search(params, authentication):
+ search = params["search"].lower()
+ wallet = authentication.split(":")[0]
+ owned = account.getDomains(wallet)
+ # Only keep owned domains ["name"]
+ ownedNames = [domain["name"] for domain in owned]
+
+ domains = [domain for domain in ownedNames if search in domain]
+
+ return {"domains": domains}
+
+
+def transfer(params, authentication):
+ address = params["address"]
+ return {"hash":"f921ffe1bb01884bf515a8079073ee9381cb93a56b486694eda2cce0719f27c0","address":address}
+
+def dns(params,authentication):
+ dns = params["dns"]
+ return {"hash":"f921ffe1bb01884bf515a8079073ee9381cb93a56b486694eda2cce0719f27c0","dns":dns}
+
+def niami(params, authentication):
+ domain = params["domain"]
+ response = requests.get(f"https://api.handshake.niami.io/domain/{domain}")
+ data = response.json()["data"]
+ if 'rating' not in data:
+ return {"rating":"No rating found."}
+ rating = str(data["rating"]["score"]) + " (" + data["rating"]["rarity"] + ")"
+ return {"rating":rating}
+
+def niamiSearch(params, authentication):
+ return niami(params, authentication)
+
+
+def connections(params,authentication):
+ outbound = account.hsd.getInfo()['pool']['outbound']
+ return {"connections": outbound}
\ No newline at end of file
diff --git a/render.py b/render.py
index 49e9a0e..aa00b46 100644
--- a/render.py
+++ b/render.py
@@ -1,8 +1,9 @@
import datetime
import json
import urllib.parse
+from flask import render_template
-def domains(domains):
+def domains(domains, mobile=False):
html = ''
for domain in domains:
owner = domain['owner']
@@ -16,8 +17,10 @@ def domains(domains):
paid = paid / 1000000
-
- html += f'{domain["name"]} {expires} days {paid} HNS Manage '
+ if not mobile:
+ html += f'{domain["name"]} {expires} days {paid} HNS Manage '
+ else:
+ html += f'{domain["name"]} {expires} days '
return html
@@ -63,11 +66,6 @@ def transactions(txs):
html += f'{action} {address} {hash} {confirmations}{amount} HNS '
-
-
-
-
-
return html
@@ -178,4 +176,126 @@ def wallets(wallets):
html = ''
for wallet in wallets:
html += f'{wallet} '
+ return html
+
+def plugins(plugins):
+ html = ''
+ for plugin in plugins:
+ name = plugin['name']
+ link = plugin['link']
+
+ if plugin['verified']:
+ html += f'{name} '
+ else:
+ html += f'{name} (Not verified) '
+ return html
+
+def plugin_functions(functions, pluginName):
+ html = ''
+ for function in functions:
+ name = functions[function]['name']
+ description = functions[function]['description']
+ params = functions[function]['params']
+ returnsRaw = functions[function]['returns']
+
+ returns = ""
+ for output in returnsRaw:
+ returns += f"{returnsRaw[output]['name']}, "
+
+ returns = returns.removesuffix(', ')
+
+ functionType = "default"
+ if "type" in functions[function]:
+ functionType = functions[function]["type"]
+
+
+ html += f''
+ html += f'
'
+ html += f'
{name} '
+ html += f'
{description} '
+ html += f'
Function type: {functionType.capitalize()} '
+
+ if functionType != "default":
+ html += f'
Returns: {returns}
'
+ html += f'
'
+ html += f'
'
+ continue
+
+ # Form
+ html += f''
+ # For debugging
+ html += f'Returns: {returns}
'
+ html += f''
+ html += f''
+
+
+ return html
+
+def plugin_output(outputs, returns):
+
+ html = ''
+
+ for returnOutput in returns:
+ if returnOutput not in outputs:
+ continue
+ html += f''
+ html += f'
'
+ html += f'
{returns[returnOutput]["name"]} '
+
+ output = outputs[returnOutput]
+ if returns[returnOutput]["type"] == "list":
+ html += f'
'
+ for item in output:
+ html += f'{item} '
+ html += f' '
+ elif returns[returnOutput]["type"] == "text":
+ html += f'
{output}
'
+ elif returns[returnOutput]["type"] == "tx":
+ html += render_template('components/tx.html', tx=output)
+ elif returns[returnOutput]["type"] == "dns":
+ output = json.loads(output)
+ html += render_template('components/dns-output.html', dns=dns(output))
+
+
+ html += f'
'
+ html += f'
'
+ return html
+
+def plugin_output_dash(outputs, returns):
+
+ html = ''
+
+ for returnOutput in returns:
+ if returnOutput not in outputs:
+ continue
+ html += render_template('components/dashboard-plugin.html', name=returns[returnOutput]["name"], output=outputs[returnOutput])
return html
\ No newline at end of file
diff --git a/templates/404.html b/templates/404.html
index 59334f2..c5c0fbb 100644
--- a/templates/404.html
+++ b/templates/404.html
@@ -19,7 +19,7 @@
-