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 += "" + 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 += "" + 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 = "
" + 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} HNSManage' + if not mobile: + html += f'{domain["name"]}{expires} days{paid} HNSManage' + 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'' + 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 param in params: + html += f'
    ' + paramName = params[param]["name"] + paramType = params[param]["type"] + if paramType == "text": + html += f'' + html += f'' + elif paramType == "longText": + html += f'' + html += f'' + elif paramType == "number": + html += f'' + html += f'' + elif paramType == "checkbox": + html += f'
    ' + elif paramType == "address": + # render components/address.html + address = render_template('components/address.html', paramName=paramName, param=param) + html += address + elif paramType == "dns": + html += render_template('components/dns-input.html', paramName=paramName, param=param) + + + + + html += f'
    ' + + html += f'' + 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 @@
    -
    {{plugins|safe}} -
    +
    @@ -148,6 +148,28 @@ {{domains | safe}} +
    +
    +
    +
    +
    +
    +
    +
    +
    Domains
    +
    +
    + + + + + + + + + {{domainsMobile | safe}} + +
    Domain{{sort_domain}}Expires{{sort_expiry}}
    diff --git a/templates/login.html b/templates/login.html index 1ea980d..5c53471 100644 --- a/templates/login.html +++ b/templates/login.html @@ -37,7 +37,8 @@

    - + +
    diff --git a/templates/manage.html b/templates/manage.html index cde165f..51faad0 100644 --- a/templates/manage.html +++ b/templates/manage.html @@ -19,7 +19,7 @@
    -