From 42d0d0879004a18089ca109d2b93ac0179cb1021 Mon Sep 17 00:00:00 2001 From: David Tavarez <337107+davidtavarez@users.noreply.github.com> Date: Tue, 18 Apr 2023 10:11:10 +0200 Subject: [PATCH 01/17] adding .gitignore --- .gitignore | 52 +++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 47 insertions(+), 5 deletions(-) diff --git a/.gitignore b/.gitignore index b36dafa..ba6ceee 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,48 @@ -node_modules .DS_Store -Thumbs.db -*.log -*.autogenerated -/dist +.huskyrc.json +out +log.log +**/node_modules +*.pyc +*.vsix +**/.vscode/.ropeproject/** +**/testFiles/**/.cache/** +*.noseids +.nyc_output +.vscode-test +__pycache__ +npm-debug.log +**/.mypy_cache/** +!yarn.lock +coverage/ +cucumber-report.json +**/.vscode-test/** +**/.vscode test/** +**/.vscode-smoke/** +**/.venv*/ +port.txt +precommit.hook +pythonFiles/lib/** +debug_coverage*/** +languageServer/** +languageServer.*/** +bin/** +obj/** +.pytest_cache +tmp/** +.python-version +.vs/ +test-results*.xml +xunit-test-results.xml +build/ci/performance/performance-results.json +!build/ +debug*.log +debugpy*.log +pydevd*.log +nodeLanguageServer/** +nodeLanguageServer.*/** +dist/** +# translation files +*.xlf +package.nls.*.json +l10n/ \ No newline at end of file From 59a60ebbdb5f3cee6b2e6d43e4a00e43d669d270 Mon Sep 17 00:00:00 2001 From: David Tavarez <337107+davidtavarez@users.noreply.github.com> Date: Tue, 18 Apr 2023 10:13:03 +0200 Subject: [PATCH 02/17] rewriting cli using python + nostr --- build.sh | 5 + build/release.js | 32 -- requirements.txt | 22 ++ src/apps/__init__.py | 0 src/apps/misc/__init__.py | 0 src/apps/misc/app.py | 124 +++++++ src/apps/node/__init__.py | 0 src/apps/node/app.py | 260 ++++++++++++++ src/apps/transaction/__init__.py | 0 src/apps/transaction/app.py | 452 ++++++++++++++++++++++++ src/apps/transaction/nostr/__init__.py | 0 src/apps/transaction/nostr/transport.py | 369 +++++++++++++++++++ src/apps/wallet/__init__.py | 0 src/apps/wallet/app.py | 405 +++++++++++++++++++++ src/cli.py | 97 +++++ src/modules/__init__.py | 0 src/modules/api/__init__.py | 28 ++ src/modules/api/foreign/__init__.py | 26 ++ src/modules/api/foreign/rpc.py | 7 + src/modules/api/node/__init__.py | 64 ++++ src/modules/api/node/rest.py | 25 ++ src/modules/api/owner/__init__.py | 52 +++ src/modules/api/owner/rpc.py | 158 +++++++++ src/modules/utils/__init__.py | 0 src/modules/utils/coingecko.py | 81 +++++ src/modules/utils/helpers.py | 181 ++++++++++ src/modules/utils/kdf.py | 47 +++ src/modules/utils/nostr.py | 7 + 28 files changed, 2410 insertions(+), 32 deletions(-) create mode 100644 build.sh delete mode 100644 build/release.js create mode 100644 requirements.txt create mode 100644 src/apps/__init__.py create mode 100644 src/apps/misc/__init__.py create mode 100644 src/apps/misc/app.py create mode 100644 src/apps/node/__init__.py create mode 100644 src/apps/node/app.py create mode 100644 src/apps/transaction/__init__.py create mode 100644 src/apps/transaction/app.py create mode 100644 src/apps/transaction/nostr/__init__.py create mode 100644 src/apps/transaction/nostr/transport.py create mode 100644 src/apps/wallet/__init__.py create mode 100644 src/apps/wallet/app.py create mode 100644 src/cli.py create mode 100644 src/modules/__init__.py create mode 100644 src/modules/api/__init__.py create mode 100644 src/modules/api/foreign/__init__.py create mode 100644 src/modules/api/foreign/rpc.py create mode 100644 src/modules/api/node/__init__.py create mode 100644 src/modules/api/node/rest.py create mode 100644 src/modules/api/owner/__init__.py create mode 100644 src/modules/api/owner/rpc.py create mode 100644 src/modules/utils/__init__.py create mode 100644 src/modules/utils/coingecko.py create mode 100644 src/modules/utils/helpers.py create mode 100644 src/modules/utils/kdf.py create mode 100644 src/modules/utils/nostr.py diff --git a/build.sh b/build.sh new file mode 100644 index 0000000..6f59626 --- /dev/null +++ b/build.sh @@ -0,0 +1,5 @@ +#!/bin/bash + +pip install -r requirements.txt +cd src +../.venv/bin/python -m nuitka --standalone --onefile --nofollow-import-to=*.tests --nofollow-import-to=*.distutils --nofollow-import-to=tornado.test --assume-yes-for-downloads --output-filename=GrinPP --output-dir="../build" --enable-console --remove-output --warn-unusual-code --include-package=asn1crypto,certifi,cffi,charset_normalizer,click,coincurve,colorama,commonmark,cryptography,idna,psutil,pycparser,pygments,pynostr,requests,rich,shellingham,timeago,tlv8,tornado,typer,urllib3 --include-module=apps,modules --noinclude-pytest-mode=nofollow --noinclude-unittest-mode=nofollow --noinclude-setuptools-mode=nofollow --noinclude-custom-mode=distutils:nofollow --product-name="Grin++ CLI" --product-version="0.2.0" --file-description="Fast, Private and Secure Grin Wallet" cli.py \ No newline at end of file diff --git a/build/release.js b/build/release.js deleted file mode 100644 index c2840da..0000000 --- a/build/release.js +++ /dev/null @@ -1,32 +0,0 @@ -const { exec } = require('pkg'); -const jetpack = require('fs-jetpack'); -const os = require('os'); - -const platform = os.platform(); -const package = jetpack.cwd('.').read('package.json', 'json'); -const distFolder = 'dist/'; -const distName = (package.productName).replace(/\s/g, ''); -let distExtension = '.exe'; - -console.log('Packaging release executable for ' + platform); - -if (platform === 'win32') { - //windows - //TODO: remove --public option for production, this is so we can get proper stack trace line #, also depends on import 'source-map-support/register' - exec([ 'package.json', '--target', 'host', '--output', distFolder + distName + distExtension ]).then(() => { - console.log('Packaging done!'); - }).catch((err) => { - console.log('Packaging Error!'); - console.log(err); - }); -} else { - //unix - distExtension = ''; - distName.toLowerCase(); - exec([ 'package.json', '--target', 'host', '--output', distFolder + distName + distExtension ]).then(() => { - console.log('Packaging done!'); - }).catch((err) => { - console.log('Packaging Error!'); - console.log(err); - }); -} diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..77c76d4 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,22 @@ +asn1crypto==1.5.1 +certifi==2022.12.7 +cffi==1.15.1 +charset-normalizer==3.1.0 +click==8.1.3 +coincurve==18.0.0 +colorama==0.4.6 +commonmark==0.9.1 +cryptography==40.0.1 +idna==3.4 +psutil==5.9.4 +pycparser==2.21 +Pygments==2.15.0 +pynostr==0.6.2 +requests==2.28.2 +rich==12.6.0 +shellingham==1.5.0.post1 +timeago==1.0.16 +tlv8==0.10.0 +tornado==6.2 +typer==0.7.0 +urllib3==1.26.15 diff --git a/src/apps/__init__.py b/src/apps/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/apps/misc/__init__.py b/src/apps/misc/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/apps/misc/app.py b/src/apps/misc/app.py new file mode 100644 index 0000000..62cc2fb --- /dev/null +++ b/src/apps/misc/app.py @@ -0,0 +1,124 @@ +#!/usr/bin/env python3 + +from typing import Optional + +import psutil +import typer +from rich import box +from rich.console import Console +from rich.table import Table + +from modules.utils.coingecko import get_grin_price, is_currency_supported +from modules.utils.helpers import ( + check_wallet_reachability, + find_processes_by_name, + kill_proc_tree, +) + +app = typer.Typer() + +console = Console(width=125, style="grey93") +error_console = Console(stderr=True, style="bright_red") + + +@app.command(name="wenmoon") +def simple_grin_price( + currency: str = typer.Option( + "usd", help="Currency like usd, eur, btc, etc.", prompt=True + ) +): + """ + Get Grin price from CoinGecko. + """ + if not is_currency_supported(currency): + error_console.print(f"Currency not supported.") + typer.Abort() + + moon = get_grin_price(currency) + table: Table = Table(box=box.HORIZONTALS, show_header=False) + direction: str = "" + style: str = "" + if moon["24h_change"] >= 0: + style = "green3" + direction = "⬆︎" + else: + style = "bright_red" + direction = "⬇︎" + change: str = f"[{style}]{moon['24h_change']:10,.2f}% {direction}" + + table.add_column("", justify="right", style="bold") + table.add_column("", justify="right") + table.add_row("Price:", f"{moon['price']:10,.6f}") + table.add_row("24h Change:", f"{change}") + table.add_row("Market cap:", f"{moon['market_cap']:10,.6f}") + table.add_row("24h Volume:", f"{moon['24h_vol']:10,.6f}") + + console.print(table) + + +@app.command(name="grinchck") +def grinchck_test( + address: str = typer.Option( + ..., help="Address of the wallet you want to check", prompt="Wallet address" + ) +): + """ + Check if a Slatepack Addresss is reachable via the Tor network. + """ + reachable: bool = False + try: + with console.status("Checking wallet reachability..."): + reachable = check_wallet_reachability(address) + except Exception as err: + error_console.print(f"Error: {err} ¯\_(ツ)_/¯") + raise typer.Abort() + + if reachable: + console.print( + f"Address [green3]{address}[/green3] is reachable via the Tor Network <(^_^)>" + ) + else: + error_console.print( + f"Addres [dark_orange]{address}[/dark_orange] is not reachable the Tor Network \_(-_-)_/" + ) + raise typer.Exit() + + +@app.command(name="tor") +def tor_control( + stop: Optional[bool] = typer.Option( + False, + help="Stop the tor process", + rich_help_panel="Actions", + ) +): + """ + Check the status of Tor. + + If --stop is used, tor will be stopped + """ + running: bool = False + tor_process_name: str = "tor" + if psutil.WINDOWS: + tor_process_name = "tor.exe" + tor_processes: list[psutil.Process] = find_processes_by_name(tor_process_name) + + if len(tor_processes) > 0: + running = True + + if not running: + error_console.print("Tor is not running") + raise typer.Abort() + elif running: + for process in tor_processes: + console.print(f"Tor is running (PID: [bold]{process.pid})") + if stop: + with console.status("Stopping tor..."): + for process in tor_processes: + kill_proc_tree(process.pid) + if len(find_processes_by_name(tor_process_name)) > 0: + error_console.print("Tor is still running") + else: + console.print("Tor [bold]stopped[/bold] successfully ✔") + + raise typer.Exit() diff --git a/src/apps/node/__init__.py b/src/apps/node/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/apps/node/app.py b/src/apps/node/app.py new file mode 100644 index 0000000..f9e0188 --- /dev/null +++ b/src/apps/node/app.py @@ -0,0 +1,260 @@ +#!/usr/bin/env python3 + +import json +import subprocess +import time +from pathlib import Path + +import psutil +import typer +from rich import box +from rich.console import Console +from rich.progress import Progress +from rich.table import Table + +from modules.api.foreign.rpc import get_list_of_settings +from modules.api.node.rest import ( + get_node_connected_peers, + get_node_state, + get_sync_state, + shutdown_node, +) +from modules.utils.helpers import find_processes_by_name + +app = typer.Typer() + +console = Console(width=125, style="grey93") +error_console = Console(stderr=True, style="bright_red") + + +@app.command(name="stop") +def stop_node(): + """ + Close all running Wallets and stop the running Tor Listeners. + """ + running: bool = False + node_process_name: str = "GrinNode" + if psutil.WINDOWS: + node_process_name = "GrinNode.exe" + if len(find_processes_by_name(node_process_name)) > 0: + running = True + + if not running: + error_console.print("Grin node is not running") + raise typer.Abort() + + elif running: + with console.status("Stopping node..."): + attempt = 0 + while attempt < 10: + time.sleep(1) + attempt += 1 + try: + shutdown_node() + break + except: + pass + if len(find_processes_by_name(node_process_name)) == 0: + console.print("Node and wallet Tor Listeners successfully [bold]stopped[/bold]") + else: + error_console.print("Unable to stop Node.") + raise typer.Abort() + + raise typer.Exit() + + +@app.command(name="start") +def start_node(): + """ + Launch the Grin node in the background. + """ + data = json.loads("{}") + error = None + with console.status("Starting node..."): + if not Path(f"{Path(__file__).parent.resolve()}/../bin/GrinNode.exe").is_file(): + error = "Can't find the Node" + raise typer.Exit() + + if psutil.WINDOWS: + subprocess.Popen( + f"{Path('../bin/GrinNode.exe').absolute()} --headless", + shell=True, + start_new_session=True, + stdout=subprocess.DEVNULL, + stderr=subprocess.STDOUT, + ) + + while data is None: + try: + data = get_node_state() + except Exception: + pass + if error: + error_console.print(error) + raise typer.Abort() + + console.print("Node successfully [bold]launched[/bold] ✔") + + raise typer.Exit() + + +@app.command(name="resync") +def resync_node(): + """ + Delete the local chain data and let the node sync again from scratch. + """ + with console.status("Stopping node..."): + while True: + try: + shutdown_node() + break + except Exception: + pass + + console.print("Node [bold]stopped[/bold] successfully ✔") + + +@app.command(name="status") +def get_node_status(): + """ + Get the status of the running node. + """ + + percentage: float + message: str + + error: Exception + + try: + message, percentage = get_sync_state() + except Exception as err: + error_console.print(f"Error: {err} ¯\_(ツ)_/¯") + raise typer.Abort() + + console.print("Ctrl+C to quit...\n", style="grey42 italic", justify="right") + + with Progress() as progress: + task = progress.add_task( + f"[bold white]{message}", total=100, completed=int(percentage) + ) + + while True: + progress.update( + task, description=f"[bold white]{message}", completed=percentage + ) + time.sleep(1) + try: + message, percentage = get_sync_state() + except Exception as err: + error = err + break + + if error: + error_console.print(error) + raise typer.Abort() + + raise typer.Exit() + + +@app.command(name="tip") +def get_node_tip(): + """ + Print the heights of the node, this is important to see if the running Node is synchronized or not. + """ + + try: + data = get_node_state() + table = Table(box=box.HORIZONTALS, expand=True) + table.add_column("node height", justify="center") + table.add_column("network height", justify="center") + table.add_column("chain height", justify="center") + table.add_column("current hash", justify="center") + + table.add_row( + str(data["header_height"]), + str(data["network"]["height"]), + str(data["chain"]["height"]), + data["chain"]["hash"], + ) + + console.print(table) + except Exception as err: + error_console.print(f"Error: {err} ¯\_(ツ)_/¯") + raise typer.Abort() + + raise typer.Exit() + + +@app.command(name="settings") +def get_node_settings(): + """ + Obtain the node's settings like amount of confirmations, minimum of outbound connections, etc. + """ + data = json.loads("{}") + + try: + data = get_list_of_settings() + except Exception as err: + error_console.print(f"Error: {err} ¯\_(ツ)_/¯") + raise typer.Abort() + + table = Table(box=box.HORIZONTALS, expand=True) + table.add_column("setting", justify="right") + table.add_column("value", justify="full", style="bold") + + table.add_row("confirmations required:", str(data["result"]["min_confirmations"])) + table.add_row("maximum of outbounds:", str(data["result"]["max_peers"])) + table.add_row( + "maximum of inbounds:", + str(data["result"]["max_peers"] - data["result"]["min_peers"]), + ) + table.add_row( + "peers preferred:", + f'[{", ".join(sorted(data["result"]["preferred_peers"]))}]' + if data["result"]["preferred_peers"] != None + else "[ ]", + ) + table.add_row( + "peers allowed:", + ", ".join(sorted(data["result"]["allowed_peers"])) + if data["result"]["allowed_peers"] != None + else "[ ]", + ) + table.add_row( + "peers blocked:", + ", ".join(sorted(data["result"]["blocked_peers"])) + if data["result"]["blocked_peers"] != None + else "[ ]", + ) + + console.print(table) + + raise typer.Exit() + + +@app.command(name="peers") +def get_connected_peers(): + """ + List the peers connected to the running node. + """ + + try: + data = get_node_connected_peers() + + table = Table(box=box.HORIZONTALS, expand=True) + table.add_column("", justify="center", width=5) + table.add_column("address", justify="center", width=20) + table.add_column("agent", justify="center", width=20) + table.add_column("direction", justify="center", width=20) + + i = 1 + for peer in data: + table.add_row(str(i), peer["addr"], peer["user_agent"], peer["direction"]) + i += 1 + + console.print(table) + except Exception as err: + error_console.print(f"Error: {err} ¯\_(ツ)_/¯") + raise typer.Abort() + + raise typer.Exit() diff --git a/src/apps/transaction/__init__.py b/src/apps/transaction/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/apps/transaction/app.py b/src/apps/transaction/app.py new file mode 100644 index 0000000..fb5d3f7 --- /dev/null +++ b/src/apps/transaction/app.py @@ -0,0 +1,452 @@ +#!/usr/bin/env python3 + +from datetime import datetime +from typing import Optional + +import psutil +import timeago +import typer +from rich import box +from rich.console import Console +from rich.prompt import Confirm +from rich.table import Table + +from apps.transaction.nostr import transport +from modules.api import TransactionsFilterOptions +from modules.api.owner.rpc import ( + add_initial_signature, + add_signature_to_transaction, + broadcast_transaction, + cancel_transaction, + estimate_transaction_fee, + get_transaction_details, + get_wallet_transactions, + send_coins, +) +from modules.utils.helpers import get_wallet_session + +app = typer.Typer() + +app.add_typer( + transport.plugin, + name="nostr", + no_args_is_help=True, + help="Nostr Transport Plugin for Grin transactions.", +) +console = Console(width=125, style="grey93") +error_console = Console(stderr=True, style="bright_red") + + +@app.command(name="list") +def list_wallet_transactions( + wallet: str = typer.Option( + ..., + help="Name of the wallet from which you want to list transactions.", + prompt="Wallet name", + ), + password: str = typer.Option( + ..., help="Wallet password.", prompt="Password", hide_input=True + ), + status: TransactionsFilterOptions = typer.Option( + default=TransactionsFilterOptions.pending.value, + help="Status of the transactions you want to list.", + ), +): + """ + List the transactions of a running Wallet. Default status: Pending. + """ + + try: + session_token = get_wallet_session(wallet=wallet, password=password) + + transactions = get_wallet_transactions(session_token, status.value) + except Exception as err: + error_console.print(f"Error: {err} ¯\_(ツ)_/¯") + raise typer.Abort() + + if len(transactions) == 0: + console.print(f"No [bold]{status.value}[/bold] transaction were found.") + raise typer.Exit() + + table = Table(title="", box=box.HORIZONTALS, expand=True) + table.add_column("id", justify="center", width=5) + table.add_column("amount ツ", justify="right", width=15) + table.add_column("fees ツ", justify="right", width=15) + table.add_column("when?", justify="center", width=15) + table.add_column("status", justify="center", width=10) + table.add_column("height", justify="center") + table.add_column("explorer", justify="center") + + style: str = "" + status: str = "" + for transaction in transactions: + style = "" + status = transaction["type"].lower() + + if status == "sent": + style = "deep_sky_blue1" + elif status == "received": + style = "spring_green3" + elif status == "canceled": + style = "grey66" + elif status == "sending (not finalized)": + style = "royal_blue1" + status = "unfinalized" + elif status == "sending (finalized)": + style = "cornflower_blue" + status = "unconfirmed" + elif status == "receiving (unconfirmed)": + style = "cyan3" + status = "unconfirmed" + + amount: float = 0 + fee = 0 + if "fee" in transaction: + fee = transaction["fee"] + + if transaction["amount_debited"] - transaction["amount_credited"] > 0: + amount = transaction["amount_credited"] - transaction["amount_debited"] + if "fee" in transaction: + amount = amount - fee + else: + amount = transaction["amount_debited"] - transaction["amount_credited"] + amount = abs(amount) + amount = amount / pow(10, 9) + fee = fee / pow(10, 9) + + relative_date = timeago.format( + datetime.fromtimestamp(transaction["creation_date_time"]) + ) + link: str = "" + confirmed_height = "-" + if "confirmed_height" in transaction: + confirmed_height = transaction["confirmed_height"] + link = f"[link=https://grinexplorer.net/output/{transaction['outputs'][0]['commitment']}]View in Explorer[/link]" + + table.add_row( + f"{transaction['id']}", + f"{amount:10,.9f}", + f"{fee:10,.9f}" if fee > 0 else "-", + f"{relative_date}", + status, + f"{confirmed_height}", + link, + style=style, + ) + console.print(table) + + raise typer.Exit() + + +@app.command(name="send") +def send_grin( + wallet: str = typer.Option( + ..., + help="Name of the wallet from which you wish to send the coins.", + prompt="Wallet name", + ), + password: str = typer.Option( + ..., help="Wallet password.", prompt="Password", hide_input=True + ), + amount: float = typer.Option( + ..., help="Amount of ツ you want to send.", prompt="Amount of ツ" + ), + address: Optional[str] = typer.Option( + "", help="Slatepack Address where you want to send the ツ" + ), + auto: Optional[bool] = typer.Option( + False, help="Auto answer 'Yes' to all confirmations" + ), +): + """ + Send ツ to someone + """ + + session_token: str + fee: float + + try: + session_token = get_wallet_session(wallet=wallet, password=password) + fee = float( + estimate_transaction_fee(session_token=session_token, amount=amount)["fee"] + ) / pow(10, 9) + except Exception as err: + error_console.print(f"Error: {err} ¯\_(ツ)_/¯") + raise typer.Abort() + + table: Table = Table( + title="Transaction details", box=box.HORIZONTALS, expand=True, show_header=False + ) + table.add_column("", justify="right", style="bold") + table.add_column("", justify="left") + table.add_row("wallet:", f"{wallet}") + table.add_row("amount:", f"{amount:10,.9f} ツ") + table.add_row("fee:", f"{fee:10.9f} ツ") + if address: + table.add_row("receiver:", f"{address}") + + console.print(table) + console.print("") + proceed: bool = False + if auto: + proceed = True + else: + proceed = Confirm.ask( + "Are you sure you want to create this transaction?", default=False + ) + if proceed: + sent: bool = False + slatepack: str = "" + try: + session_token = get_wallet_session(wallet=wallet, password=password) + with console.status("Building transaction..."): + transaction = send_coins( + session_token=session_token, amount=amount, address=address + ) + slatepack = transaction["slatepack"] + if transaction["status"] == "FINALIZED": + sent = True + except Exception as err: + error_console.print(f"Error: {err} ¯\_(ツ)_/¯") + raise typer.Abort() + + if sent: + console.print(f"Transaction built with {address} via Tor successfully ✔") + elif not sent: + error_console.print(f"Unable to build transaction with {address} via Tor ✗") + + console.print("\nPlease share the next Slatepack with the recipient:") + console.print( + f"\n[yellow1]{slatepack.strip().rstrip()}[yellow1]\n", + ) + console.print( + "*** Ask the recipient to add his signature by receiving this slatepack.", + style="italic", + ) + console.print( + "Then execute the [bold]finalize[/bold] command and insert the signed Slatepack by him/she. ***", + style="italic", + ) + + raise typer.Exit() + + +@app.command(name="cancel") +def transaction_cancelation( + wallet: str = typer.Option( + ..., help="Name of the wallet from which you wish to cancel the transaction." + ), + password: str = typer.Option( + ..., help="Wallet password.", prompt="Password", hide_input=True + ), + id: int = typer.Option( + ..., + help="Id of the transaction you want to be canceled.", + prompt="Transaction Id", + ), +): + """ + Cancel a transaction using the Transaction Id. + """ + + try: + session_token = get_wallet_session(wallet=wallet, password=password) + + if cancel_transaction(session_token=session_token, id=id): + console.print("Transaction [bold]canceled[/bold] successfully ✔") + else: + error_console.print("Unable to cancel the transaction ✗") + + except Exception as err: + error_console.print(f"Error: {err} ¯\_(ツ)_/¯") + raise typer.Abort() + + raise typer.Exit() + + +@app.command(name="finalize") +def transaction_finalization( + wallet: str = typer.Option( + ..., + help="Name of the wallet from which you wish to finalize the transaction.", + prompt="Wallet name", + ), + password: str = typer.Option( + ..., help="Wallet password.", prompt="Password", hide_input=True + ), +): + """ + Finalize an unfinalized transaction. + """ + + try: + token = get_wallet_session(wallet=wallet, password=password) + if not psutil.WINDOWS: + import readline + slatepack: str = console.input("Please, insert the Slatepack down below:\n") + if add_signature_to_transaction(session_token=token, slatepack=slatepack): + console.print("Transaction [bold]finalized[/bold] successfully ✔") + else: + error_console.print("Unable to finalized the transaction ✗") + except Exception as err: + error_console.print(f"Error: {err} ¯\_(ツ)_/¯") + raise typer.Abort() + + raise typer.Exit() + + +@app.command(name="receive") +def transaction_receive( + wallet: str = typer.Option( + ..., + help="Name of the wallet where you want to receive the coins.", + prompt="Wallet name", + ), + password: str = typer.Option( + ..., help="Wallet password.", prompt="Password", hide_input=True + ), +): + """ + Receive a transaction using Slatepack Messsage + """ + + try: + session_token = get_wallet_session(wallet=wallet, password=password) + if not psutil.WINDOWS: + import readline + slatepack = console.input("Paste the Slatepack down below:\n") + + if len(slatepack.strip().rstrip()) == 0: + raise Exception("Empty Slatepack") + + signed_slatepack = add_initial_signature( + session_token=session_token, slatepack=slatepack + ) + + console.print("\nPlease share the next Slatepack with the sender:") + console.print( + f"\n[yellow1]{signed_slatepack['slatepack'].strip().rstrip()}[yellow1]\n", + ) + console.print( + "*** The sender must know [bold]finalize[/bold] the transaction. ***", + style="italic", + ) + + except Exception as err: + error_console.print(f"Error: {err} ¯\_(ツ)_/¯") + raise typer.Abort() + + raise typer.Exit() + + +@app.command(name="post") +def transaction_reposting( + wallet: str = typer.Option( + ..., + help="Name of the wallet from which you wish to post the transaction.", + prompt="Wallet name", + ), + password: str = typer.Option( + ..., help="Wallet password.", prompt="Password", hide_input=True + ), + id: int = typer.Option( + ..., + help="Id of the transaction you want to be repost", + prompt="Transaction ID", + ), +): + """ + Post a transaction to the network using the Id. + """ + + try: + session_token = get_wallet_session(wallet=wallet, password=password) + if broadcast_transaction(session_token=session_token, id=id): + console.print("Transaction [bold]posted[/bold] successfully ✔") + else: + error_console.print("Unable to post the transaction ✗") + + except Exception as err: + error_console.print(f"Error: {err} ¯\_(ツ)_/¯") + raise typer.Abort() + + raise typer.Exit() + + +@app.command(name="details") +def transaction_information( + wallet: str = typer.Option( + ..., help="Name of the wallet you want query", prompt="Wallet name" + ), + password: str = typer.Option( + ..., help="Wallet password.", prompt="Password", hide_input=True + ), + id: int = typer.Option( + ..., + help="Id of the transaction you want to read", + prompt="Transaction Id", + ), + slatepack: bool = typer.Option(False, help="Return only the Slatepack"), +): + """ + Get the information of a transaction using the Transaction Id. + """ + + details: dict = {} + + try: + session_token = get_wallet_session(wallet=wallet, password=password) + details = get_transaction_details(session_token=session_token, id=id) + except Exception as err: + error_console.print(f"Error: {err} ¯\_(ツ)_/¯") + raise typer.Abort() + + if slatepack: + console.print( + f"\n[yellow1]{details['armored_slatepack']}[yellow1]\n", + ) + raise typer.Exit() + + amount = 0 + fee = 0 + if "fee" in details: + fee = details["fee"] + + if details["amount_debited"] - details["amount_credited"] > 0: + amount = details["amount_credited"] - details["amount_debited"] + if "fee" in details: + amount = amount - fee + else: + amount = details["amount_debited"] - details["amount_credited"] + amount = abs(amount) + amount = amount / pow(10, 9) + fee = fee / pow(10, 9) + + table = Table(box=box.HORIZONTALS, expand=True, show_header=False) + table.add_column("", justify="right", style="bold") + table.add_column("", justify="left") + table.add_row("amount:", f"{amount:10,.9f} ツ") + table.add_row("fee:", f"{fee:10.9f} ツ") + table.add_row( + "created on:", f"{datetime.fromtimestamp(details['creation_date_time'])}" + ) + table.add_row("type:", f"{details['type'].lower()}") + if "confirmed_height" in details: + table.add_row("[bold]confirmed height:", f"{details['confirmed_height']}") + if details["kernels"]: + kernels = "" + for kernel in details["kernels"]: + commitment = kernel["commitment"] + kernels = f"{commitment}" + table.add_row("kernels:", kernels) + if "outputs" in details: + outputs = "" + for output in details["outputs"]: + commitment = output["commitment"] + keychain_path = output["keychain_path"] + status = output["status"] + outputs = f"[bold]commitment:[/bold] {commitment}\n[bold]keychain path:[/bold] {keychain_path}\n[bold]status:[/bold] {status.lower()}" + table.add_row("outputs:", outputs) + console.print(table) + + raise typer.Exit() diff --git a/src/apps/transaction/nostr/__init__.py b/src/apps/transaction/nostr/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/apps/transaction/nostr/transport.py b/src/apps/transaction/nostr/transport.py new file mode 100644 index 0000000..e23f449 --- /dev/null +++ b/src/apps/transaction/nostr/transport.py @@ -0,0 +1,369 @@ +#!/usr/bin/env python3 + +import json +import ssl +import time +import uuid +from datetime import datetime, timedelta +from typing import Optional + +import psutil +import tornado.ioloop +import typer +from pynostr.base_relay import RelayPolicy +from pynostr.encrypted_dm import EncryptedDirectMessage +from pynostr.event import Event, EventKind +from pynostr.filters import Filters, FiltersList +from pynostr.key import PrivateKey +from pynostr.message_pool import EventMessage, MessagePool, NoticeMessage +from pynostr.message_type import ClientMessageType, RelayMessageType +from pynostr.relay import Relay +from pynostr.relay_manager import RelayManager +from pynostr.utils import get_public_key, get_timestamp +from rich import box +from rich.console import Console +from rich.prompt import Confirm, Prompt +from rich.table import Table +from tornado import gen + +from modules.api import TransactionsFilterOptions +from modules.api.owner.rpc import ( + add_initial_signature, + add_signature_to_transaction, + broadcast_transaction, + cancel_transaction, + estimate_transaction_fee, + get_transaction_details, + get_wallet_slatepack_address, + get_wallet_transactions, + send_coins, +) +from modules.utils.coingecko import get_grin_price, is_currency_supported +from modules.utils.helpers import ( + check_wallet_reachability, + find_processes_by_name, + get_nostr_private_key, + get_wallet_nostr_public_key, + get_wallet_session, + kill_proc_tree, +) + +plugin = typer.Typer() + +console = Console(width=125, style="grey93") +error_console = Console(stderr=True, style="bright_red") + + +@plugin.command(name="send") +def send_tx_to_nostr( + wallet: str = typer.Option( + ..., + help="Name of the wallet from which you wish to get the transaction.", + prompt="Wallet name", + ), + password: str = typer.Option( + ..., help="Wallet password.", prompt="Password", hide_input=True + ), + id: int = typer.Option( + ..., + help="Id of the transaction you want to be repost", + prompt="Transaction ID", + ), + recipient_npub: str = typer.Option( + ..., + help="Recipient Public Key. npub or hex key.", + prompt="Recipient's npub key", + ), + relay_url: str = typer.Option( + ..., + help="The Nostr relay to connect to.", + prompt="Relay to connect to", + ), +): + """ + Send an unfinalized transaction through Nosrt + """ + + transaction: dict = {} + recipient = get_public_key(identity_str=recipient_npub) + + if not recipient: + error_console.print("Invalid recipient npub key") + raise typer.Abort() + + console.print( + f"Recipient Nostr public key: [dark_orange3]{recipient_npub}[/dark_orange3] (∩`-´)⊃━☆゚.*・。゚" + ) + + try: + session_token = get_wallet_session(wallet=wallet, password=password) + transaction = get_transaction_details(session_token=session_token, id=id) + except Exception as err: + error_console.print(f"Error: {err} ¯\_(ツ)_/¯") + raise typer.Abort() + + console.print(f"Transaction with [bold]id={id}[/bold] found ✔") + + slatepack_address = get_wallet_slatepack_address(session_token) + nostr_private_key, raw_key = get_nostr_private_key( + slatepack_address=slatepack_address, password=password + ) + + console.print(f"Sender [bold]Nostr key[/bold] loaded ✔") + + @gen.coroutine + def transaction_sent(message_json): + if message_json[0] == RelayMessageType.OK: + if len(message_json) == 4 and "blocked" in message_json[3]: + error_console.print(f" Error: {message_json[3]} ") + ascii = r""" +░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ +░░░░░░░░░░░░░▄▄▄▄▄▄▄░░░░░░░░░ +░░░░░░░░░▄▀▀▀░░░░░░░▀▄░░░░░░░ +░░░░░░░▄▀░░░░░░░░░░░░▀▄░░░░░░ +░░░░░░▄▀░░░░░░░░░░▄▀▀▄▀▄░░░░░ +░░░░▄▀░░░░░░░░░░▄▀░░██▄▀▄░░░░ +░░░▄▀░░▄▀▀▀▄░░░░█░░░▀▀░█▀▄░░░ +░░░█░░█▄▄░░░█░░░▀▄░░░░░▐░█░░░ +░░▐▌░░█▀▀░░▄▀░░░░░▀▄▄▄▄▀░░█░░ +░░▐▌░░█░░░▄▀░░░░░░░░░░░░░░█░░ +░░▐▌░░░▀▀▀░░░░░░░░░░░░░░░░▐▌░ +░░▐▌░░░░░░░░░░░░░░░▄░░░░░░▐▌░ +░░▐▌░░░░░░░░░▄░░░░░█░░░░░░▐▌░ +░░░█░░░░░░░░░▀█▄░░▄█░░░░░░▐▌░ +░░░▐▌░░░░░░░░░░▀▀▀▀░░░░░░░▐▌░ +░░░░█░░░░░░░░░░░░░░░░░░░░░█░░ +░░░░▐▌▀▄░░░░░░░░░░░░░░░░░▐▌░░ +░░░░░█░░▀░░░░░░░░░░░░░░░░▀░░░ +░░░░░░░░░░░░░░░░░░░░░░░░░░░░░⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ + """ + console.print(ascii) + else: + console.print(f"Transaction [bold]sent[/bold] via Nostr ✔") + ascii = r""" +░░▄░░░▄░▄▄▄▄░░░░░░░░░░░░░░░ +░░█▀▄▀█░█▄▄░░░░░░░░░░░░░░░░ +░░█░░░█░█▄▄▄░░░░░░░░░░░░░░░ +░░░░░░░░░░░░░░░░░░░░░░░░░░░ +░▄▄▄░▄░░░▄░░▄▄▄░▄▄▄▄▄░░▄▄▄░ +█░░░░█░░░█░█░░░░░░█░░░█░░░█ +█░▀█░█░░░█░░▀▀▄░░░█░░░█▀▀▀█ +░▀▀▀░░▀▀▀░░▄▄▄▀░░░▀░░░▀░░░▀ + """ + console.print(ascii) + relay.close() + elif message_json[0] == RelayMessageType.NOTICE: + error_console.print(f"\nError: {message_json[1]} ¯\_(ツ)_/¯\n") + + relay.close() + + io_loop = tornado.ioloop.IOLoop.current() + relay = Relay( + url=relay_url, + message_pool=MessagePool(), + io_loop=io_loop, + policy=RelayPolicy(should_read=True, should_write=True), + close_on_eose=False, + message_callback=transaction_sent, + ) + + dm = EncryptedDirectMessage( + pubkey=nostr_private_key.public_key.hex(), + recipient_pubkey=recipient.hex(), + cleartext_content=transaction["armored_slatepack"], + ) + dm.encrypt( + private_key_hex=nostr_private_key.hex(), + ) + + event = dm.to_event() + + expiration_date = datetime.now() + timedelta(days=2) + event.add_tag("expiration", f"{int(expiration_date.timestamp())}") + + event.sign(nostr_private_key.hex()) + + with console.status("Sending transaction via Nostr..."): + relay.publish(event.to_message()) + try: + io_loop.run_sync(func=relay.connect) + except gen.Return: + pass + + raise typer.Exit() + + +@plugin.command(name="receive") +def grab_txs_from_nostr( + wallet: str = typer.Option( + ..., + help="Name of the wallet in which you want to insert transactions.", + prompt="Wallet name", + ), + password: str = typer.Option( + ..., help="Wallet password.", prompt="Password", hide_input=True + ), + relay_url: str = typer.Option( + ..., + help="The Nostr relay to connect to.", + prompt="Relay to connect to", + ), +): + """ + Grab pending transactions from Nostr + """ + + messages: list = [] + nostr_private_key: PrivateKey = None + + try: + session_token = get_wallet_session(wallet=wallet, password=password) + slatepack_address = get_wallet_slatepack_address(session_token) + nostr_private_key, raw_key = get_nostr_private_key( + slatepack_address=slatepack_address, password=password + ) + + if not nostr_private_key: + raise Exception("Nostr private not loaded") + except Exception as err: + error_console.print(f"Error: {err} ¯\_(ツ)_/¯") + raise typer.Abort() + + console.print(f"Receiver [bold]Nostr key[/bold] loaded ✔") + + def decrypt_dm(message_json): + if message_json[0] == RelayMessageType.EVENT: + event = Event.from_dict(message_json[2]) + if event.kind == EventKind.ENCRYPTED_DIRECT_MESSAGE: + message = EncryptedDirectMessage.from_event(event) + message.decrypt( + private_key_hex=nostr_private_key.hex(), public_key_hex=event.pubkey + ) + messages.append( + { + "slatepack": message.cleartext_content, + "sender": event.pubkey, + "id": event.id, + } + ) + + filters = FiltersList( + [ + Filters( + kinds=[EventKind.ENCRYPTED_DIRECT_MESSAGE], + until=get_timestamp(), + pubkey_refs=[ + nostr_private_key.public_key.hex(), + ], + ) + ] + ) + + relay_manager = RelayManager(error_threshold=3, timeout=0) + relay_manager.add_relay( + url=relay_url, + close_on_eose=True, + message_callback=decrypt_dm, + ) + + subscription_id = uuid.uuid4().hex + relay_manager.add_subscription_on_all_relays(subscription_id, filters) + with console.status("Grabbing slatepacks..."): + relay_manager.run_sync() + relay_manager.close_subscription_on_all_relays(subscription_id) + relay_manager.close_all_relay_connections() + relay_manager.remove_closed_relays() + + console.print(f"Slatepacks found: [bold]{len(messages)}[/bold]") + if not messages: + raise typer.Exit() + + new_txs = [] + + for message in messages: + try: + message["signed_slate"] = add_initial_signature( + session_token=session_token, slatepack=message["slatepack"] + ) + new_txs.append(message) + except: + pass + + console.print(f"New Transactions received: [bold]{len(new_txs)}[/bold]") + if not new_txs: + raise typer.Exit() + + if Confirm.ask( + f"Would you like to review them and send back the signed Slatepack via Nostr?", + default=True, + ): + + def check_reply(message_json): + if message_json[0] == RelayMessageType.OK: + console.print(f"Slate sent via Nostr ✔") + elif message_json[0] == RelayMessageType.NOTICE: + error_console.print(f"\nError: {message_json[1]} ¯\_(ツ)_/¯\n") + + relay_manager.add_relay( + url=relay_url, + close_on_eose=True, + message_callback=check_reply, + policy=RelayPolicy(should_read=False, should_write=False) + ) + + subscription_id = uuid.uuid4().hex + relay_manager.add_subscription_on_all_relays(subscription_id, filters) + + for idx, item in enumerate(new_txs): + console.print("") + sender: str = "-" + if "sender" in item["signed_slate"]: + sender = item["signed_slate"]["sender"]["slatepack"] + amount: float = float(item["signed_slate"]["slate"]["amt"]) / pow(10, 9) + fee: float = float(item["signed_slate"]["slate"]["fee"]) / pow(10, 9) + table = Table( + box=box.HORIZONTALS, + expand=True, + show_header=False, + title=f"Slate Id: [bold]{item['signed_slate']['slate']['id']}[/bold]", + ) + table.add_column("", justify="right", style="bold") + table.add_column("", justify="left") + table.add_row("amount:", f"{amount:10,.9f} ツ") + table.add_row("fee:", f"{fee:10.9f} ツ") + table.add_row("sender:", f"{sender}") + console.print(f"[bold]Transaction #{idx+1}[/bold]") + console.print(table) + + if Confirm.ask( + f"Would you like to send back the signed Slatepack via Nostr?", + default=True, + ): + dm = EncryptedDirectMessage( + pubkey=nostr_private_key.public_key.hex(), + recipient_pubkey=item["sender"], + cleartext_content=item["signed_slate"]["slatepack"], + ) + dm.encrypt( + private_key_hex=nostr_private_key.hex(), + ) + + reply = dm.to_event() + + expiration_date = datetime.now() + timedelta(days=2) + reply.add_tag("expiration", f"{int(expiration_date.timestamp())}") + # create 'e' tag reference to the note you're replying to + reply.add_event_ref(item["id"]) + # create 'p' tag reference to the pubkey you're replying to + reply.add_pubkey_ref(item["sender"]) + reply.sign(nostr_private_key.hex()) + + relay_manager.publish_event(reply) + with console.status("Sending response..."): + relay_manager.run_sync() + + relay_manager.close_subscription_on_all_relays(subscription_id) + relay_manager.close_all_relay_connections() + relay_manager.remove_closed_relays() + + console.print("") diff --git a/src/apps/wallet/__init__.py b/src/apps/wallet/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/apps/wallet/app.py b/src/apps/wallet/app.py new file mode 100644 index 0000000..3ddac6c --- /dev/null +++ b/src/apps/wallet/app.py @@ -0,0 +1,405 @@ +#!/usr/bin/env python3 + +import json + +import typer +from rich import box +from rich.console import Console +from rich.prompt import Confirm +from rich.table import Table + +from modules.api.owner.rpc import ( + close_wallet_by_name, + create_wallet, + delete_wallet_by_name, + get_list_of_wallets, + get_wallet_balance, + get_wallet_seed, + get_wallet_slatepack_address, + open_wallet_by_name, + restore_wallet, +) +from modules.utils.helpers import ( + get_wallet_nostr_public_key, + get_wallet_session, + save_wallet_nostr_key, + save_wallet_session, +) + +app = typer.Typer() +console = Console(width=125, style="grey93") +error_console = Console(stderr=True, style="bright_red") + + +@app.command(name="open") +def wallet_open( + wallet: str = typer.Option( + ..., help="Name of the wallet you want to open", prompt=True + ), + password: str = typer.Option( + ..., + prompt=True, + hide_input=True, + help="Wallet password.", + ), +): + """ + Open a Wallet, then start the Tor Listener automatically. + """ + + try: + result = open_wallet_by_name(wallet, password) + console.print(f"Wallet [bold]{wallet}[/bold] opened ✔") + console.print(f"Tor Listener for wallet [bold]{wallet}[/bold] running ✔") + session_saved = save_wallet_session( + wallet=wallet, + session_token=result["session_token"], + password=password, + ) + if not session_saved: + error_console.print(f"Error: Unable to save session information ✗") + else: + console.print(f"Session information created ✔") + + nostr_key_saved = save_wallet_nostr_key( + wallet=wallet, + slatepack_address=result["slatepack_address"], + password=password, + ) + + if not nostr_key_saved: + error_console.print(f"Error: Unable to generate Nostr key ✗") + else: + console.print(f"Nostr root key loaded ✔") + + console.print( + f"Slatepack Address: [green3]{result['slatepack_address']}[/green3] " + ) + except Exception as err: + error_console.print(f"Error: {err} ¯\_(ツ)_/¯") + raise typer.Abort() + + raise typer.Exit() + + +@app.command(name="close") +def wallet_close( + wallet: str = typer.Option( + ..., help="Name of the wallet you want to close", prompt=True + ), + password: str = typer.Option( + ..., + prompt=True, + hide_input=True, + help="Wallet password.", + ), +): + """ + Close a running Wallet and stop the Tor Listener. + """ + + try: + session_token: str = get_wallet_session(wallet=wallet, password=password) + with console.status("closing walet..."): + close_wallet_by_name(session_token) + console.print( + f"Wallet [bold]{wallet}[/bold] closed, and Tor Listener [bold]stopped[/bold] successfully ✔" + ) + except Exception as err: + error_console.print(f"Error: {err} ¯\_(ツ)_/¯") + raise typer.Abort() + + raise typer.Exit() + + +@app.command(name="create") +def wallet_creation( + name: str = typer.Option( + ..., help="Name of the wallet you want to create", prompt=True + ), + password: str = typer.Option( + ..., + prompt=True, + confirmation_prompt=True, + hide_input=True, + help="Wallet password.", + ), + words: int = typer.Option(24, help="Number of words for the mnemonic seed"), +): + """ + Create a new Wallet, open it and start the Tor Listener. + """ + + try: + wallet = create_wallet(wallet=name, password=password, words=words) + console.print(f"Wallet [bold]{wallet}[/bold] created ✔") + + session_saved = save_wallet_session( + wallet=name, + session_token=wallet["session_token"], + password=password, + ) + console.print(f"Tor Listener for wallet [bold]{wallet}[/bold] running ✔") + + if not session_saved: + raise Exception("An error occurred while saving session information") + + nostr_key_saved = save_wallet_nostr_key( + wallet=name, + slatepack_address=wallet["slatepack_address"], + password=password, + ) + + if not nostr_key_saved: + error_console.print(f"Error: {err} ✗") + else: + console.print(f"Nostr root key loaded ✔") + + console.print(f"Wallet seed phrase: [bold]{wallet['wallet_seed']}[/bold] ✇") + console.print( + f"Slatepack Address: [green3]{wallet['slatepack_address']}[/green3] " + ) + except Exception as err: + error_console.print(f"Error: {err} ¯\_(ツ)_/¯") + raise typer.Abort() + + raise typer.Exit() + + +@app.command(name="recover") +def wallet_restore( + name: str = typer.Option( + ..., help="Name for the wallet you want to recover", prompt=True + ), + seed: str = typer.Option(..., help="Seed phrase", prompt=True), + password: str = typer.Option( + ..., + prompt=True, + confirmation_prompt=True, + hide_input=True, + help="Wallet password.", + ), +): + """ + Recover a Wallet using the seed phrase. + """ + + try: + wallet = restore_wallet(wallet=name, password=password, seed=seed) + console.print(f"Wallet [bold]{wallet}[/bold] recovered ✔") + session_saved = save_wallet_session( + wallet=name, + session_token=wallet["session_token"], + password=password, + ) + if not session_saved: + error_console.print(f"Error: {err} ✗") + else: + console.print(f"Session information created ✔") + + nostr_key_saved = save_wallet_nostr_key( + wallet=name, + slatepack_address=wallet["slatepack_address"], + password=password, + ) + + if not nostr_key_saved: + error_console.print(f"Error: {err} ✗") + else: + console.print(f"Nostr root key loaded ✔") + + console.print( + f"Slatepack Address: [green3]{wallet['slatepack_address']}[/green3] " + ) + except Exception as err: + error_console.print(f"Error: {err} ¯\_(ツ)_/¯") + raise typer.Abort() + + raise typer.Exit() + + +@app.command(name="delete") +def wallet_removal( + name: str = typer.Option( + ..., help="Name of the wallet you want to delete", prompt=True + ), + password: str = typer.Option( + ..., prompt=True, hide_input=True, help="Wallet password." + ), +): + """ + Delete a Wallet. + """ + + if Confirm.ask("Are you sure you want to delete the wallet?", default=False): + try: + seed = get_wallet_seed(wallet=name, password=password) + console.print( + f"Wallet seed phrase: [bold italic]{seed}[/bold italic]", style="" + ) + + delete_wallet_by_name(wallet=name, password=password) + + console.print(f"Wallet [bold]{name}[/bold] deleted") + except Exception as err: + error_console.print(f"Error: {err} ¯\_(ツ)_/¯") + raise typer.Abort() + + raise typer.Exit() + + +@app.command(name="backup") +def wallet_backup( + wallet: str = typer.Option(..., help="Name of the wallet you want to backup."), + password: str = typer.Option( + ..., prompt=True, hide_input=True, help="Wallet password." + ), +): + """ + Backup a Wallet. + """ + + try: + seed = get_wallet_seed(wallet=wallet, password=password) + console.print(f"wallet seed: [bold white]{seed}[/bold white]") + + except Exception as err: + error_console.print(f"Error: {err} ¯\_(ツ)_/¯") + raise typer.Abort() + + raise typer.Exit() + + +@app.command(name="list") +def list_wallets(): + """ + List the created Wallets. + """ + data = json.loads("{}") + + try: + data = get_list_of_wallets() + except Exception as err: + error_console.print(f"Error: {err} ¯\_(ツ)_/¯") + raise typer.Abort() + + if data["wallets"]: + table = Table(title="Wallets", box=box.HORIZONTALS, expand=True) + table.add_column("") + table.add_column("name") + i = 1 + for wallet in data["wallets"]: + table.add_row( + f"{i}", + f"[bold yellow]{wallet}", + ) + i += 1 + + console.print(table) + else: + console.print("No wallet found") + + raise typer.Exit() + + +@app.command(name="balance") +def wallet_balance( + wallet: str = typer.Option( + ..., help="Name of the wallet you want to check", prompt="Wallet name" + ), + password: str = typer.Option( + ..., + prompt=True, + hide_input=True, + help="Wallet password.", + ), +): + """ + Get the balance of a running Wallet. + """ + + try: + session_token = get_wallet_session(wallet=wallet, password=password) + + balance = get_wallet_balance(session_token) + + table = Table( + title="Wallet's Balance", + box=box.HORIZONTALS, + show_footer=True, + show_header=True, + expand=True, + ) + + table.add_column("", "Total", justify="right") + table.add_column( + "amount ツ", f'{balance["total"] / pow(10, 9):10,.9f} ', justify="right" + ) + table.add_row( + "Spendable:", + f'[green3]{balance["spendable"] / pow(10, 9):10,.9f}', + style="bold", + ) + table.add_row( + "Immature:", f'[dark_orange3]{balance["immature"] / pow(10, 9):10,.9f}' + ) + table.add_row( + "Unconfirmed:", f'[gold1]{balance["unconfirmed"] / pow(10, 9):10,.9f}' + ) + table.add_row( + "Locked:", f'[bright_black]{balance["locked"] / pow(10, 9):10,.9f}' + ) + + console.print(table) + except Exception as err: + error_console.print(f"Error: {err} ¯\_(ツ)_/¯") + raise typer.Abort() + + raise typer.Exit() + + +@app.command(name="address") +def wallet_address( + wallet: str = typer.Option( + ..., help="Name of the opened wallet you want to query", prompt="Wallet" + ), + password: str = typer.Option( + ..., + prompt=True, + hide_input=True, + help="Wallet password.", + ), + nostr: bool = typer.Option( + False, help="Return the nostr Public Key associated with the current address" + ), + check: bool = typer.Option(False, help="Check if address are reachable via Tor"), +): + """ + Get the Slatepack Address of a running Wallet. + """ + + try: + session_token = get_wallet_session(wallet=wallet, password=password) + + slatepack_address = get_wallet_slatepack_address(session_token) + + console.print(f"Slatepack Address: [green3]{slatepack_address}[/green3] ") + + if nostr: + nostr_public_key = get_wallet_nostr_public_key( + wallet=wallet, + slatepack_address=slatepack_address, + password=password, + ) + if nostr_public_key: + console.print( + f"Nostr' Public Key: [dark_orange3]{nostr_public_key}[/dark_orange3] (∩`-´)⊃━☆゚.*・。゚" + ) + else: + error_console.print("No nostr key was found for this address") + + except Exception as err: + error_console.print(f"Error: {err} ¯\_(ツ)_/¯") + raise typer.Abort() + + raise typer.Exit() diff --git a/src/cli.py b/src/cli.py new file mode 100644 index 0000000..8b2d5cd --- /dev/null +++ b/src/cli.py @@ -0,0 +1,97 @@ +#!/usr/bin/env python3 + +import os +from pathlib import Path +from typing import Optional + +import typer +from rich.console import Console + +from apps.misc import app as misc_app +from apps.node import app as node_app +from apps.transaction import app as transaction_app +from apps.wallet import app as wallet_app + +__appname__ = "Grin++ CLI" +__version__ = "0.2.0" + + +Path(Path.home().joinpath(os.getenv("GRINPP_CLI_DATA_PATH", ".grinplusplus"))).mkdir( + parents=True, exist_ok=True +) + + +def version_callback(value: bool): + if value: + typer.echo(f"{__appname__} v{__version__}") + raise typer.Exit() + + +def main( + debug: bool = typer.Option( + default=False, + help="Enable the verbose mode for troubleshooting purposes.", + ), + json: bool = typer.Option( + default=False, + help="Return json instead", + ), + version: Optional[bool] = typer.Option( + None, + "--version", + callback=version_callback, + is_eager=True, + help="Return current version.", + ), +): + """ + Grin++: Fast, Private and Secure Grin Wallet. + + Grin is a lightweight implementation of the Mimblewimble protocol. The main goals and features of the Grin project are: Privacy, Scalability, Simplicity, Simple Cryptography and Decentralization. Grin wants to be usable by everyone, regardless of borders, culture, capabilities or access. To learn more about Grin, visit GRIN.MW. + """ + if debug: + state["verbose"] = True + if json: + state["json"] = True + + +cli = typer.Typer( + callback=main, + no_args_is_help=True, + epilog="If you need support, please join the Grin++ group on Telegram: https://t.me/GrinPP", +) +state = {"verbose": False, "json": False} +console = Console(width=125, style="grey93") +error_console = Console(stderr=True, style="bright_red") + +cli.add_typer( + misc_app.app, + name="misc", + no_args_is_help=True, + help="Miscellaneous cool things! Give it a try (งツ)ว", +) + +cli.add_typer( + wallet_app.app, + name="wallet", + no_args_is_help=True, + help="Wallet management commands. Use this set of commands to create, open and manage your wallets.", +) + +cli.add_typer( + node_app.app, + name="node", + no_args_is_help=True, + help="Manage the status of the local Grin++ node. Launch, stop, (re)Sync, and many more.", +) + +cli.add_typer( + transaction_app.app, + name="transaction", + no_args_is_help=True, + help="Execute all actions regarding transactions. These actions require to be executed upon an open wallet.", +) + + +if __name__ == "__main__": + cli() diff --git a/src/modules/__init__.py b/src/modules/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/modules/api/__init__.py b/src/modules/api/__init__.py new file mode 100644 index 0000000..d57bc25 --- /dev/null +++ b/src/modules/api/__init__.py @@ -0,0 +1,28 @@ +from enum import Enum + +import psutil + +from modules.utils.helpers import find_processes_by_name + + +class TransactionsFilterOptions(str, Enum): + coinbase = "coinbase" + sent = "sent" + pending = "pending" + received = "received" + canceled = "canceled" + all = "all" + + +def _get_process_status_error() -> str: + if not _is_node_running(): + error = "Grin node is not running" + return "Unable to connect to the node" + + +def _is_node_running() -> bool: + node_process_name = "GrinNode" + if psutil.WINDOWS: + node_process_name = "GrinNode.exe" + + return len(find_processes_by_name(node_process_name)) > 0 diff --git a/src/modules/api/foreign/__init__.py b/src/modules/api/foreign/__init__.py new file mode 100644 index 0000000..6369fe8 --- /dev/null +++ b/src/modules/api/foreign/__init__.py @@ -0,0 +1,26 @@ +#!/usr/bin/env python3 + +import uuid + +import requests + +from modules.api import _get_process_status_error + + +def _owner_foreign_rpc_call(method: str, params: dict = {}) -> dict: + owner_foreign_rpc_url = "http://127.0.0.1:3413/v2/foreign" + + call_params = { + "jsonrpc": "2.0", + "method": method, + "id": str(uuid.uuid4()), + "params": params, + } + + try: + data: dict = requests.post(url=owner_foreign_rpc_url, json=call_params).json() + if "error" in data: + raise Exception(data["error"]["message"]) + return data + except requests.exceptions.ConnectionError: + raise Exception(_get_process_status_error()) diff --git a/src/modules/api/foreign/rpc.py b/src/modules/api/foreign/rpc.py new file mode 100644 index 0000000..710474c --- /dev/null +++ b/src/modules/api/foreign/rpc.py @@ -0,0 +1,7 @@ +#!/usr/bin/env python3 + +from modules.api.foreign import _owner_foreign_rpc_call + + +def get_list_of_settings() -> dict: + return _owner_foreign_rpc_call("get_config") diff --git a/src/modules/api/node/__init__.py b/src/modules/api/node/__init__.py new file mode 100644 index 0000000..e21791b --- /dev/null +++ b/src/modules/api/node/__init__.py @@ -0,0 +1,64 @@ +#!/usr/bin/env python3 + +import requests + +from modules.api import _get_process_status_error + + +def _node_rest_call(method: str, params: str = "") -> dict: + node_rest_api_url = f"http://127.0.0.1:3413/v1/{method}{params}" + try: + return requests.get(url=node_rest_api_url).json() + except requests.exceptions.ConnectionError: + raise Exception(_get_process_status_error()) + + +def _parse_status_response(data: dict) -> tuple[str, float]: + percentage: float = 0 + message: str = "" + + if data["sync_status"] == "NOT_CONNECTED": + message = "Waiting for Peers" + elif data["sync_status"] == "FULLY_SYNCED": + message = "Running" + percentage = 100 * float(data["chain"]["height"]) / float(data["header_height"]) + elif data["sync_status"] == "SYNCING_HEADERS": + message = "1/4 Syncing Headers" + if data["network"]["height"] > 0: + percentage = ( + 100 * float(data["header_height"]) / float(data["network"]["height"]) + ) + else: + percentage = 0 + elif data["sync_status"] == "DOWNLOADING_TXHASHSET": + message = "2/4 Downloading State" + if data["state"]["download_size"] > 0: + percentage = ( + 100 + * float(data["state"]["downloaded"]) + / float(data["state"]["download_size"]) + ) + else: + percentage = 0 + elif data["sync_status"] == "PROCESSING_TXHASHSET": + message = "3/4 Validating State" + percentage = data["state"]["processing_status"] + elif data["sync_status"] == "SYNCING_BLOCKS": + message = "4/4 Syncing Blocks" + if data["chain"]["height"] == 0 or data["chain"]["height"] == 0: + percentage = 0 + elif data["header_height"] < 10080 or data["chain"]["height"] < 10080: + percentage = ( + 100 * float(data["chain"]["height"]) / float(data["header_height"]) + ) + elif data["header_height"] - data["chain"]["height"] > 10080: + percentage = 0 + else: + remaining = 100 * ( + (float(data["header_height"]) - float(data["chain"]["height"])) / 10080 + ) + if remaining <= 0: + remaining = 1 + percentage = 100 - remaining + + return message, percentage diff --git a/src/modules/api/node/rest.py b/src/modules/api/node/rest.py new file mode 100644 index 0000000..ce42fd6 --- /dev/null +++ b/src/modules/api/node/rest.py @@ -0,0 +1,25 @@ +#!/usr/bin/env python3 + +from modules.api import _is_node_running +from modules.api.node import _node_rest_call, _parse_status_response + + +def get_node_state() -> dict: + return _node_rest_call("status") + + +def get_sync_state() -> tuple[str, float]: + return _parse_status_response(get_node_state()) + + +def get_node_connected_peers() -> list: + peers: list = [] + peer: dict + for peer in _node_rest_call("peers", "/connected"): + peers.append(peer) + return peers + + +def shutdown_node() -> bool: + _node_rest_call("shutdown") + return _is_node_running() == False diff --git a/src/modules/api/owner/__init__.py b/src/modules/api/owner/__init__.py new file mode 100644 index 0000000..74d0281 --- /dev/null +++ b/src/modules/api/owner/__init__.py @@ -0,0 +1,52 @@ +#!/usr/bin/env python3 + +import uuid + +import requests + +from modules.api import _get_process_status_error + + +def _owner_wallet_rpc_call(method: str, params: dict = {}) -> dict: + owner_wallet_rpc_url = "http://127.0.0.1:3421/v2" + + call_params = { + "jsonrpc": "2.0", + "method": method, + "id": str(uuid.uuid4()), + "params": params, + } + + try: + response: requests.Response = requests.post( + url=owner_wallet_rpc_url, json=call_params + ) + if len(response.text) > 0: + data = response.json() + if "error" in data: + raise Exception(data["error"]["message"]) + return data["result"] + return {} + except requests.exceptions.ConnectionError: + raise Exception(_get_process_status_error()) + + +def _filter_transactions_by_status(transactions: list, status: str): + transactions.sort(key=lambda t: t["creation_date_time"], reverse=True) + + if status == "coinbase": + return filter(lambda t: "coinbase" in str(t["type"]).lower(), transactions) + elif status == "sent": + return filter(lambda t: "sent" in str(t["type"]).lower(), transactions) + elif status == "pending": + return filter( + lambda t: "sending" in str(t["type"]).lower() + or "receiving" in str(t["type"]).lower(), + transactions, + ) + elif status == "received": + return filter(lambda t: str(t["type"]).lower() == "received", transactions) + elif status == "canceled": + return filter(lambda t: "canceled" in str(t["type"]).lower(), transactions) + else: + return transactions diff --git a/src/modules/api/owner/rpc.py b/src/modules/api/owner/rpc.py new file mode 100644 index 0000000..510639f --- /dev/null +++ b/src/modules/api/owner/rpc.py @@ -0,0 +1,158 @@ +#!/usr/bin/env python3 + +from typing import Literal + +from modules.api.owner import _filter_transactions_by_status, _owner_wallet_rpc_call + + +def get_list_of_wallets() -> list[str]: + wallets: list[str] = [] + wallet: str + for wallet in _owner_wallet_rpc_call("list_wallets", {}): + wallets.append(wallet) + return wallets + + +def open_wallet_by_name(name: str, password: str): + return _owner_wallet_rpc_call("login", {"username": name, "password": password}) + + +def get_wallet_transactions( + session_token: str, + status: Literal["all", "coinbase", "sent", "pending", "received", "canceled"], +) -> list: + transactions: list = _owner_wallet_rpc_call( + "list_txs", {"session_token": session_token} + )["txs"] + return list(_filter_transactions_by_status(transactions, status)) + + +def close_wallet_by_name(session_token: str) -> None: + _owner_wallet_rpc_call("logout", {"session_token": session_token}) + return + + +def get_wallet_balance(session_token: str): + return _owner_wallet_rpc_call("get_balance", {"session_token": session_token}) + + +def get_wallet_slatepack_address(session_token: str) -> str: + response: dict = _owner_wallet_rpc_call( + "get_slatepack_address", {"session_token": session_token} + ) + return response["slatepack"] + + +def get_wallet_seed(wallet: str, password: str) -> str: + response: dict = _owner_wallet_rpc_call( + "get_wallet_seed", {"username": wallet, "password": password} + ) + return response["wallet_seed"] + + +def delete_wallet_by_name(wallet: str, password: str) -> bool: + response: dict = _owner_wallet_rpc_call( + "delete_wallet", {"username": wallet, "password": password} + ) + return response["status"] == "SUCCESS" + + +def create_wallet(wallet: str, password: str, words: int) -> dict: + return _owner_wallet_rpc_call( + "create_wallet", + { + "username": wallet, + "password": password, + "num_seed_words": words, + }, + ) + + +def estimate_transaction_fee(session_token: str, amount: float) -> dict: + params: dict = { + "session_token": session_token, + "amount": amount * pow(10, 9), + "fee_base": 500000, + "change_outputs": 1, + "selection_strategy": {"strategy": "SMALLEST"}, + } + return _owner_wallet_rpc_call( + "estimate_fee", + params, + ) + + +def send_coins( + session_token: str, + amount: float, + address: str = "", +) -> dict: + params: dict = { + "session_token": session_token, + "address": address, + "amount": amount * pow(10, 9), + "fee_base": 500000, + "change_outputs": 1, + "selection_strategy": {"strategy": "SMALLEST", "inputs": []}, + "post_tx": {"method": "FLUFF"}, + } + return _owner_wallet_rpc_call("send", params) + + +def cancel_transaction(session_token: str, id: int) -> dict: + response: dict = _owner_wallet_rpc_call( + "cancel_tx", + { + "session_token": session_token, + "tx_id": id, + }, + ) + return response["status"] == "SUCCESS" + + +def add_signature_to_transaction(session_token: str, slatepack: str) -> dict: + response: dict = _owner_wallet_rpc_call( + "finalize", + { + "session_token": session_token, + "slatepack": slatepack.rstrip().strip(), + }, + ) + return response["status"] == "FINALIZED" + + +def add_initial_signature(session_token: str, slatepack: str) -> dict: + return _owner_wallet_rpc_call( + "receive", + { + "session_token": session_token, + "slatepack": slatepack.rstrip().strip(), + }, + ) + + +def broadcast_transaction(session_token: str, id: int) -> bool: + response = _owner_wallet_rpc_call( + "repost_tx", + {"session_token": session_token, "tx_id": id, "method": "FLUFF"}, + ) + if "status" in response: + return response["status"] != "FAILED" + return True + + +def get_transaction_details(session_token: str, id: int) -> dict: + transactions = _owner_wallet_rpc_call("list_txs", {"session_token": session_token}) + + return next((x for x in transactions["txs"] if x["id"] == id)) + + +def restore_wallet(wallet: str, password: str, seed: str) -> dict: + return _owner_wallet_rpc_call( + "restore_wallet", + { + "username": wallet, + "password": password, + "wallet_seed": seed.strip(), + }, + ) diff --git a/src/modules/utils/__init__.py b/src/modules/utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/modules/utils/coingecko.py b/src/modules/utils/coingecko.py new file mode 100644 index 0000000..6b700f7 --- /dev/null +++ b/src/modules/utils/coingecko.py @@ -0,0 +1,81 @@ +#!/usr/bin/env python3 + + +import requests + + +def is_currency_supported(currency: str) -> bool: + return currency in [ + "btc", + "eth", + "ltc", + "bch", + "bnb", + "eos", + "xrp", + "xlm", + "link", + "dot", + "yfi", + "usd", + "aed", + "ars", + "aud", + "bdt", + "bhd", + "bmd", + "brl", + "cad", + "chf", + "clp", + "cny", + "czk", + "dkk", + "eur", + "gbp", + "hkd", + "huf", + "idr", + "ils", + "inr", + "jpy", + "krw", + "kwd", + "lkr", + "mmk", + "mxn", + "myr", + "ngn", + "nok", + "nzd", + "php", + "pkr", + "pln", + "rub", + "sar", + "sek", + "sgd", + "thb", + "try", + "twd", + "uah", + "vef", + "vnd", + "zar", + "xdr", + "xag", + "xau", + "bits", + "sats", + ] + + +def get_grin_price(currency: str) -> dict: + results: dict = requests.get( + url=f"https://api.coingecko.com/api/v3/simple/price?ids=grin&vs_currencies={currency}&include_market_cap=true&include_24hr_vol=true&include_24hr_change=true&include_last_updated_at=true&precision=true" + ).json() + results["price"] = results["grin"][currency] + results["market_cap"] = results["grin"][currency + "_market_cap"] + results["24h_change"] = results["grin"][currency + "_24h_change"] + results["24h_vol"] = results["grin"][currency + "_24h_vol"] + return results diff --git a/src/modules/utils/helpers.py b/src/modules/utils/helpers.py new file mode 100644 index 0000000..9f65d47 --- /dev/null +++ b/src/modules/utils/helpers.py @@ -0,0 +1,181 @@ +#!/usr/bin/env python3 + +import os +import signal +from pathlib import Path + +import psutil +import requests +from cryptography.fernet import Fernet +from pynostr.key import PrivateKey + +from modules.utils.kdf import derive_key, encode_derived_key + + +def find_processes_by_name(name: str) -> list[psutil.Process]: + "Return a list of processes matching 'name'." + ls: list[psutil.Process] = [] + for p in psutil.process_iter(["name", "exe", "cmdline"]): + if ( + name == p.info["name"] + or p.info["exe"] + and os.path.basename(p.info["exe"]) == name + or p.info["cmdline"] + and p.info["cmdline"][0] == name + ): + ls.append(p) + return ls + + +def kill_proc_tree( + pid: int, + sig: signal.Signals = signal.SIGTERM, + include_parent: bool = True, + timeout: float = None, + on_terminate: callable = None, +) -> tuple[list[psutil.Process], list[psutil.Process]]: + """Kill a process tree (including grandchildren) with signal + "sig" and return a (gone, still_alive) tuple. + "on_terminate", if specified, is a callback function which is + called as soon as a child terminates. + """ + assert pid != os.getpid(), "won't kill myself" + parent = psutil.Process(pid) + children = parent.children(recursive=True) + if include_parent: + children.append(parent) + for p in children: + try: + p.send_signal(sig) + except psutil.NoSuchProcess: + pass + gone, alive = psutil.wait_procs(children, timeout=timeout, callback=on_terminate) + return (gone, alive) + + +def save_wallet_session( + wallet: str, + session_token: str, + password: str, +) -> bool: + """Save the the session token and the nostr public key encrypted. + + Returns a boolean value indicating whether the session was saved or not. + """ + + passphrase: str = f"{wallet}{password}" + key: bytes = derive_key(passphrase=passphrase.encode(), generate_salt=True) + encoded_key = encode_derived_key(key) + + token_path = Path.home().joinpath( + os.getenv("GRINPP_CLI_DATA_PATH", ".grinplusplus"), f"{wallet}.token" + ) + with open(token_path, "wb+") as file: + file.write(Fernet(encoded_key).encrypt(session_token.encode())) + + return token_path.exists() + + +def get_wallet_session(wallet: str, password: str) -> str: + """Return a string containing the Session token.""" + session_token: str = "" + encrypted_session_token: bytes = bytes() + + passphrase: str = f"{wallet}{password}" + key: bytes = derive_key(passphrase.encode(), generate_salt=False) + encoded_key: bytes = encode_derived_key(key) + + token_path: Path = Path.home().joinpath( + os.getenv("GRINPP_CLI_DATA_PATH", ".grinplusplus"), f"{wallet}.token" + ) + with open(token_path, "rb") as file: + encrypted_session_token = file.read() + + try: + session_token = Fernet(encoded_key).decrypt(encrypted_session_token).decode() + except: + raise Exception("Invalid password.") + + return session_token + + +def save_wallet_nostr_key( + wallet: str, + slatepack_address: str, + password: str, +) -> bool: + """Save the the encrypted nostr public key . + + Returns a boolean value indicating whether the key was created or not. + """ + nostr_key: PrivateKey = None + raw_key: bytes = None + + nostr_key, raw_key = get_nostr_private_key( + slatepack_address=slatepack_address, password=password + ) + + nostr_key_path: Path = Path.home().joinpath( + os.getenv("GRINPP_CLI_DATA_PATH", ".grinplusplus"), f"{wallet}.nostr" + ) + + with open(nostr_key_path, "wb+") as file: + file.write( + Fernet(encode_derived_key(raw_key)).encrypt( + nostr_key.public_key.bech32().encode() + ) + ) + + return nostr_key_path.exists() + + +def get_wallet_nostr_public_key( + wallet: str, slatepack_address: str, password: str +) -> str: + """Return a string containing the nostr Public key.""" + nostr_public_key: str = "" + + passphrase: str = f"{slatepack_address}{password}" + key: bytes = derive_key(passphrase=passphrase.encode(), generate_salt=True) + + encrypted_nostr_key: bytes = bytes() + nostr_key_path: Path = Path.home().joinpath( + os.getenv("GRINPP_CLI_DATA_PATH", ".grinplusplus"), f"{wallet}.nostr" + ) + with open(nostr_key_path, "rb") as file: + encrypted_nostr_key = file.read() + try: + nostr_public_key = ( + Fernet(encode_derived_key(key)).decrypt(encrypted_nostr_key).decode() + ) + except: + raise Exception("Invalid password.") + + return nostr_public_key + + +def get_nostr_private_key( + slatepack_address: str, password: str +) -> tuple[PrivateKey, bytes]: + """Return a tuple: Nostr Private Key and raw bytes""" + passphrase: str = f"{slatepack_address}{password}" + key: bytes = derive_key(passphrase=passphrase.encode(), generate_salt=True) + return PrivateKey(raw_secret=key), key + + +def check_wallet_reachability( + slatepack_address: str, api_url: str = "http://192.227.214.130/" +) -> bool: + try: + return ( + requests.post( + url=api_url, + data={ + "wallet": slatepack_address, + }, + ).status_code + == 200 + ) + except requests.exceptions.ConnectionError: + error = "Unable to connect to the GrinChck API" + raise Exception(error) diff --git a/src/modules/utils/kdf.py b/src/modules/utils/kdf.py new file mode 100644 index 0000000..4cbb689 --- /dev/null +++ b/src/modules/utils/kdf.py @@ -0,0 +1,47 @@ +#!/usr/bin/env python3 +import base64 +import os + +from cryptography.hazmat.backends import default_backend +from cryptography.hazmat.primitives import hashes +from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC + + +def derive_key(passphrase, generate_salt=False): + salt = _SaltManager(generate_salt) + + kdf = PBKDF2HMAC( + algorithm=hashes.SHA256(), + length=32, + salt=salt.get(), + iterations=1000, + backend=default_backend(), + ) + return kdf.derive(passphrase) + + +def encode_derived_key(derived_key: bytes): + return base64.urlsafe_b64encode(derived_key) + + +class _SaltManager(object): + def __init__(self, generate, path=".salt"): + self.generate = generate + self.path = path + + def get(self): + if self.generate: + return self._generate_and_store() + return self._read() + + def _generate_and_store(self): + if not os.path.exists(self.path): + salt = os.urandom(16) + with open(self.path, "xb") as f: + f.write(salt) + return salt + return self._read() + + def _read(self): + with open(self.path, "rb+") as f: + return f.read() diff --git a/src/modules/utils/nostr.py b/src/modules/utils/nostr.py new file mode 100644 index 0000000..247a540 --- /dev/null +++ b/src/modules/utils/nostr.py @@ -0,0 +1,7 @@ +#!/usr/bin/env python3 + +import json +import ssl +import time + +from pynostr.relay_manager import RelayManager From 2990bb7371d1626ca77d45699581fd01cc096a60 Mon Sep 17 00:00:00 2001 From: David Tavarez <337107+davidtavarez@users.noreply.github.com> Date: Tue, 18 Apr 2023 10:15:35 +0200 Subject: [PATCH 03/17] ignoring Thumbs.db --- .gitignore | 3 +- README.md | 8 - cli.js | 32 - lib/GrinPP.class.js | 45 - lib/api/SendToHTTP.js | 79 - lib/api/WalletAPI.js | 66 - lib/cli/DateFormatter.js | 30 - lib/cli/Tables.js | 55 - lib/cli/Wallet.js | 20 - lib/cli/commands/Clear.js | 17 - lib/cli/commands/CreateWallet.js | 32 - lib/cli/commands/Exit.js | 23 - lib/cli/commands/Finalize.js | 42 - lib/cli/commands/OpenWallet.js | 37 - lib/cli/commands/Receive.js | 46 - lib/cli/commands/RestoreWallet.js | 33 - lib/cli/commands/Send.js | 61 - lib/cli/commands/Summary.js | 79 - lib/cli/commands/index.js | 15 - package-lock.json | 2523 ----------------------------- package.json | 27 - 21 files changed, 2 insertions(+), 3271 deletions(-) delete mode 100644 README.md delete mode 100644 cli.js delete mode 100644 lib/GrinPP.class.js delete mode 100644 lib/api/SendToHTTP.js delete mode 100644 lib/api/WalletAPI.js delete mode 100644 lib/cli/DateFormatter.js delete mode 100644 lib/cli/Tables.js delete mode 100644 lib/cli/Wallet.js delete mode 100644 lib/cli/commands/Clear.js delete mode 100644 lib/cli/commands/CreateWallet.js delete mode 100644 lib/cli/commands/Exit.js delete mode 100644 lib/cli/commands/Finalize.js delete mode 100644 lib/cli/commands/OpenWallet.js delete mode 100644 lib/cli/commands/Receive.js delete mode 100644 lib/cli/commands/RestoreWallet.js delete mode 100644 lib/cli/commands/Send.js delete mode 100644 lib/cli/commands/Summary.js delete mode 100644 lib/cli/commands/index.js delete mode 100644 package-lock.json delete mode 100644 package.json diff --git a/.gitignore b/.gitignore index ba6ceee..588d338 100644 --- a/.gitignore +++ b/.gitignore @@ -45,4 +45,5 @@ dist/** # translation files *.xlf package.nls.*.json -l10n/ \ No newline at end of file +l10n/ +Thumbs.db \ No newline at end of file diff --git a/README.md b/README.md deleted file mode 100644 index 6dcfe9c..0000000 --- a/README.md +++ /dev/null @@ -1,8 +0,0 @@ -# GrinPP-CLI - -## Build instructions -* Install Node-js (https://nodejs.org/en/) -* npm install -* npm run release - -GrinNode must be running to use the CLI diff --git a/cli.js b/cli.js deleted file mode 100644 index 18cd2a4..0000000 --- a/cli.js +++ /dev/null @@ -1,32 +0,0 @@ -#!/usr/bin/env node -const commander = require('commander'); -const GrinPP = require('./lib/GrinPP.class'); -const commands = require('./lib/cli/commands'); - -async function cli() { - var program = new commander.Command() - .version('0.7.5', '-v, --version', 'output the current version') - .name('GrinPP') - .description('Grin++ CLI'); - - Object.keys(commands.outer).forEach((key) => { - commands.outer[key].add_command(program); - }); - - program.on('--help', function(){ - console.log('') - console.log('Examples:'); - console.log(' $ GrinPP help'); - console.log(' $ GrinPP create username password'); - console.log(' $ GrinPP restore username password word1 word2...word24'); - console.log(' $ GrinPP open username password'); - }); - - await program.parseAsync(process.argv); - - if (global.session_token != null) { - await new GrinPP(global.session_token).execute(); - } -} - -cli(); \ No newline at end of file diff --git a/lib/GrinPP.class.js b/lib/GrinPP.class.js deleted file mode 100644 index 369c227..0000000 --- a/lib/GrinPP.class.js +++ /dev/null @@ -1,45 +0,0 @@ -const commander = require('commander'); - -const commands = require('./cli/commands'); -const readline = require('readline'); - -class GrinPP { - constructor($token) { - this.token = $token; - this.program = new commander.Command() - .usage(' [options]'); - Object.keys(commands.inner).forEach((key) => { - commands.inner[key].add_command(this.program); - }); - } - - async execute() { - this.program._exit = () => {}; - - const args = (await this._waitForInput()).split(' '); - args.unshift('1', ''); - - try { - await this.program.parseAsync(args); - } catch (e) { - console.error("Error:"); - console.error(e); - } - - await this.execute(); - } - - _waitForInput() { - const rl = readline.createInterface({ - input: process.stdin, - output: process.stdout, - }); - - return new Promise(resolve => rl.question('> ', ans => { - rl.close(); - resolve(ans); - })); - } -} - -module.exports = GrinPP; \ No newline at end of file diff --git a/lib/api/SendToHTTP.js b/lib/api/SendToHTTP.js deleted file mode 100644 index 1f8367d..0000000 --- a/lib/api/SendToHTTP.js +++ /dev/null @@ -1,79 +0,0 @@ -const WalletAPI = require('./WalletAPI'); - -async function callFinalize(slate) { - const params = { - session_token: global.session_token, - slate: slate, - post_tx: { method: 'STEM' } - }; - - const response = await WalletAPI.OwnerRPC('finalize', params); - if (response != null && response.result != null) { - console.log('Slate finalized:'); - console.log(JSON.stringify(response.result.slate)); - return response.result.slate; - } else { - console.error("Failed to finalize!\n"); - console.error(JSON.stringify(response)); - return null; - } -} - -async function callReceive(httpAddress, slate) { - const response = await WalletAPI.RPC(httpAddress + '/v2/foreign', 'receive_tx', [slate, null, '']); - if (response.result != null && response.result.Ok != null) { - console.log('Slate Received:'); - console.log(JSON.stringify(response.result.Ok)); - return response.result.Ok; - } else { - console.error('Failed while contacting receiver!'); - console.error(JSON.stringify(response)); - return null; - } -} - -async function callSend(httpAddress, amount) { - console.log(global.session_token); - const params = { - session_token: global.session_token, - amount: amount, - fee_base: 1000000, - selection_strategy: { strategy: 'SMALLEST' }, - post_tx: { method: 'STEM' }, - address: httpAddress - }; - - const response = await WalletAPI.OwnerRPC('send', params); - if (response != null && response.result != null) { - console.log('Slate created:'); - console.log(JSON.stringify(response.result.slate)); - return response.result.slate; - } else { - console.error("Failed to send!\n"); - console.error(JSON.stringify(response)); - return null; - } -} - -async function send(httpAddress, amount) { - console.log("Sending to: " + httpAddress); - - const sent_slate = await callSend(httpAddress, amount); - if (sent_slate == null) { - return; - } - - const received_slate = await callReceive(httpAddress, sent_slate); - if (received_slate == null) { - return; - } - - const finalized_slate = await callFinalize(received_slate); - if (finalized_slate == null) { - return; - } - - console.log('Transaction sent successfully!'); -}; - -module.exports = {send} \ No newline at end of file diff --git a/lib/api/WalletAPI.js b/lib/api/WalletAPI.js deleted file mode 100644 index 9da5434..0000000 --- a/lib/api/WalletAPI.js +++ /dev/null @@ -1,66 +0,0 @@ -const axios = require('axios'); - -class WalletAPI { - static async OwnerRPC(method, params) { - const url = 'http://localhost:3421/v2'; - - return await this.RPC(url, method, params); - } - - static async RPC(url, method, params) { - try { - const body = { - id: '0', - jsonrpc: '2.0', - method: method, - params: params - }; - const response = await axios.post(url, body); - return { - error: response.data.error || null, - result: response.data.result || null - }; - } catch (e) { - return { - status_code: e.response.status, - body: e.response.data - }; - } - } - - static async POST(action, headers, body = {}) { - const url = 'http://localhost:3420/v1/wallet/owner/' + action; - - try { - const response = await axios.post(url, body, { headers: headers}); - return { - status_code: response.status, - body: response.data - }; - } catch (e) { - return { - status_code: e.response.status, - body: e.response.data - }; - } - } - - static async GET(action, headers, params = {}) { - const url = 'http://localhost:3420/v1/wallet/owner/' + action; - - try { - const response = await axios.get(url, { params: params, headers: headers}); - return { - status_code: response.status, - body: response.data - }; - } catch (e) { - return { - status_code: e.response.status, - body: e.response.data - }; - } - } -} - -module.exports = WalletAPI; \ No newline at end of file diff --git a/lib/cli/DateFormatter.js b/lib/cli/DateFormatter.js deleted file mode 100644 index 8d1888c..0000000 --- a/lib/cli/DateFormatter.js +++ /dev/null @@ -1,30 +0,0 @@ -function format(date) { - var seconds = Math.floor((new Date() - date) / 1000); - var intervalType; - - var interval = Math.floor(seconds / 86400); - if (interval >= 1) { - return date.toLocaleDateString(); - } else { - interval = Math.floor(seconds / 3600); - if (interval >= 1) { - intervalType = "hour"; - } else { - interval = Math.floor(seconds / 60); - if (interval >= 1) { - intervalType = "minute"; - } else { - interval = seconds; - intervalType = "second"; - } - } - } - - if (interval > 1 || interval === 0) { - intervalType += 's'; - } - - return interval + ' ' + intervalType + ' ago'; -} - -module.exports = { format }; \ No newline at end of file diff --git a/lib/cli/Tables.js b/lib/cli/Tables.js deleted file mode 100644 index b88029a..0000000 --- a/lib/cli/Tables.js +++ /dev/null @@ -1,55 +0,0 @@ -const Table = require('cli-table3'); -const DateFormatter = require('./DateFormatter'); - -function create_table($options = {}) { - $options.chars = { 'top': '═' , 'top-mid': '╤' , 'top-left': '╔' , 'top-right': '╗' - , 'bottom': '═' , 'bottom-mid': '╧' , 'bottom-left': '╚' , 'bottom-right': '╝' - , 'left': '║' , 'left-mid': '╟' , 'mid': '─' , 'mid-mid': '┼' - , 'right': '║' , 'right-mid': '╢' , 'middle': '│' }; - - return new Table($options); -} - -function format_amount(amount) { - var calculatedAmount = Math.abs(amount) / Math.pow(10, 9); - var formatted = calculatedAmount.toFixed(9) + "ツ"; - if (amount < 0) { - formatted = "-" + formatted; - } - - return formatted; -} - -function totals(summary) { - var table = create_table({ head: ['Status', 'Amount']}); - - table.push( - { 'Spendable': [format_amount(summary.spendable)] }, - { 'Total': [format_amount(summary.total)] }, - { 'Immature': [format_amount(summary.immature)] }, - { 'Unconfirmed': [format_amount(summary.unconfirmed)] }, - { 'Locked': [format_amount(summary.locked)] } - ); - - console.log(table.toString()); -} - -function transactions(txs) { - var table = create_table({ head: ["ID", "Created Dt/Tm", "Status", "Address", "Amount"] }); - - txs.forEach((tx, index) => { - table.push( - [ - tx.id, - DateFormatter.format(new Date(tx.creation_date_time * 1000)), - tx.status, - tx.address, - format_amount(tx.amount) - ] - ); - }); - - console.log(table.toString()); -} - -module.exports = { totals, transactions } \ No newline at end of file diff --git a/lib/cli/Wallet.js b/lib/cli/Wallet.js deleted file mode 100644 index 58ae20b..0000000 --- a/lib/cli/Wallet.js +++ /dev/null @@ -1,20 +0,0 @@ -class Wallet { - static set_user(body) { - global.session_token = body.session_token; - global.tor_address = body.tor_address; - global.listener_port = body.listener_port; - - this.display_info(); - } - - static display_info() { - console.log(`Listening on port ${global.listener_port}\n`); - if (global.tor_address != null) { - console.log(`${global.tor_address}`); - console.log(`http://${global.tor_address}.grinplusplus.com`); - console.log(); - } - } -} - -module.exports = Wallet; \ No newline at end of file diff --git a/lib/cli/commands/Clear.js b/lib/cli/commands/Clear.js deleted file mode 100644 index df9f321..0000000 --- a/lib/cli/commands/Clear.js +++ /dev/null @@ -1,17 +0,0 @@ -const WalletAPI = require('../../api/WalletAPI'); -const Tables = require('../Tables'); -const Wallet = require('../Wallet'); - -class Summary { - static add_command(program) { - program.command('clear') - .description('Clears screen') - .action(this.run); - } - - static async run(command_obj) { - console.clear(); - } -} - -module.exports = Summary; \ No newline at end of file diff --git a/lib/cli/commands/CreateWallet.js b/lib/cli/commands/CreateWallet.js deleted file mode 100644 index ced8665..0000000 --- a/lib/cli/commands/CreateWallet.js +++ /dev/null @@ -1,32 +0,0 @@ -const WalletAPI = require('../../api/WalletAPI'); -const Wallet = require('../Wallet'); - -class CreateWallet { - static add_command(program) { - program.command('create ') - .description('Create new wallet') - .action(this.run); - } - - static async run(username, password) { - const headers = { - username: username, - password: password - }; - - const response = await WalletAPI.POST('create_wallet', headers); - if (response != null && response.status_code == 200) { - console.log('Wallet successfully created!\n'); - console.log(response.body.wallet_seed); - console.log(); - - Wallet.set_user(response.body); - } else { - console.error("Failed to create wallet!\n"); - console.error(`Status: ${response.status_code}`); - console.error(`Error: ${response.body}`); - } - } -} - -module.exports = CreateWallet; \ No newline at end of file diff --git a/lib/cli/commands/Exit.js b/lib/cli/commands/Exit.js deleted file mode 100644 index 376ebba..0000000 --- a/lib/cli/commands/Exit.js +++ /dev/null @@ -1,23 +0,0 @@ -const WalletAPI = require('../../api/WalletAPI'); - -class Exit { - static add_command(program) { - program.command('exit') - .description('Exit') - .action(this.run); - } - - static async run() { - if (global.session_token != null) { - const headers = { - session_token: global.session_token - }; - - await WalletAPI.POST('logout', headers); - } - - process.exit(0); - } -} - -module.exports = Exit; \ No newline at end of file diff --git a/lib/cli/commands/Finalize.js b/lib/cli/commands/Finalize.js deleted file mode 100644 index 056e17e..0000000 --- a/lib/cli/commands/Finalize.js +++ /dev/null @@ -1,42 +0,0 @@ -const fs = require('fs'); -const WalletAPI = require('../../api/WalletAPI'); - -var help = false; -var command = null; -class Receive { - static add_command(program) { - command = program.command('finalize ') - .description('Finalize slate file') - .action(this.run) - .allowUnknownOption(true); - command._exit = () => {}; - command.on('--help', () => { help = true; }) - } - - static async run(file) { - if (help) { - help = false; - return; - } - - const slate = fs.readFileSync(file, 'utf-8'); - - const params = { - session_token: global.session_token, - slate: slate, - file: file + '.finalized', - post_tx: { method: 'STEM' } - }; - - const response = await WalletAPI.OwnerRPC('finalize', params); - if (response != null && response.result != null) { - console.log('Finalized successfully'); - console.log(JSON.stringify(response.result.slate)); - } else { - console.error("Failed to finalize!\n"); - console.error(JSON.stringify(response)); - } - } -} - -module.exports = Receive; \ No newline at end of file diff --git a/lib/cli/commands/OpenWallet.js b/lib/cli/commands/OpenWallet.js deleted file mode 100644 index fafceb4..0000000 --- a/lib/cli/commands/OpenWallet.js +++ /dev/null @@ -1,37 +0,0 @@ -const WalletAPI = require('../../api/WalletAPI'); -const Wallet = require('../Wallet'); - -class OpenWallet { - static add_command(program) { - program.command('open ') - .description('Open wallet') - .action(this.run); - } - - static async run(username, password) { - process.stdout.write('Logging in') - const interval = setInterval(() => { process.stdout.write('.'); }, 2500); - - const headers = { - username: username, - password: password - }; - - const response = await WalletAPI.POST('login', headers); - if (response != null && response.status_code == 200) { - clearInterval(interval); - console.clear(); - - Wallet.set_user(response.body); - } else { - clearInterval(interval); - console.clear(); - - console.error("Failed to open wallet!\n"); - console.error(`Status: ${response.status_code}`); - console.error(`Error: ${response.body}`); - } - } -} - -module.exports = OpenWallet; \ No newline at end of file diff --git a/lib/cli/commands/Receive.js b/lib/cli/commands/Receive.js deleted file mode 100644 index cf1bf5d..0000000 --- a/lib/cli/commands/Receive.js +++ /dev/null @@ -1,46 +0,0 @@ -const fs = require('fs'); -const WalletAPI = require('../../api/WalletAPI'); - -var help = false; -var command = null; -class Receive { - static add_command(program) { - command = program.command('receive ') - .description('Receive slate file') - .option('-m, --message ', 'Message') - .action(this.run) - .allowUnknownOption(true); - command._exit = () => {}; - command.on('--help', () => { help = true; }) - } - - static async run(file, command_obj) { - if (help) { - help = false; - return; - } - - const slate = fs.readFileSync(file, 'utf-8'); - - const params = { - session_token: global.session_token, - slate: slate, - file: file + '.response' - }; - - if (command_obj.message != null && command_obj.message.length > 0) { - params.message = command_obj.message; - } - - const response = await WalletAPI.OwnerRPC('receive', params); - if (response != null && response.result != null) { - console.log('Received successfully'); - console.log(JSON.stringify(response.result.slate)); - } else { - console.error("Failed to receive!\n"); - console.error(JSON.stringify(response)); - } - } -} - -module.exports = Receive; \ No newline at end of file diff --git a/lib/cli/commands/RestoreWallet.js b/lib/cli/commands/RestoreWallet.js deleted file mode 100644 index 1105a27..0000000 --- a/lib/cli/commands/RestoreWallet.js +++ /dev/null @@ -1,33 +0,0 @@ -const WalletAPI = require('../../api/WalletAPI'); -const Wallet = require('../Wallet'); - -class RestoreWallet { - static add_command(program) { - program.command('restore ') - .description('Restore wallet from seed') - .action(this.run); - } - - static async run(username, password, seed_words) { - const headers = { - username: username, - password: password - }; - - const body = { - wallet_seed: seed_words.join(' ') - }; - - const response = await WalletAPI.POST('restore_wallet', headers, body); - if (response != null && response.status_code == 200) { - console.log('Wallet successfully restored!\n'); - Wallet.set_user(response.body); - } else { - console.error("Failed to restore wallet!\n"); - console.error(`Status: ${response.status_code}`); - console.error(`Error: ${response.body}`); - } - } -} - -module.exports = RestoreWallet; \ No newline at end of file diff --git a/lib/cli/commands/Send.js b/lib/cli/commands/Send.js deleted file mode 100644 index 7625135..0000000 --- a/lib/cli/commands/Send.js +++ /dev/null @@ -1,61 +0,0 @@ -const WalletAPI = require('../../api/WalletAPI'); -const SendToHTTP = require('../../api/SendToHTTP'); - -var help = false; -var command = null; -class Send { - static add_command(program) { - command = program.command('send ') - .usage('-m -d ') - .description('Send grins (ex. send -m tor -d 3ngg5chiucyvjxaymy46fypqbr3nfskj2lluygnz6hqys5lwxrcpqzad 2.5)') - .requiredOption('-m, --method ', 'Send method (tor, http, file)') - .requiredOption('-d, --destination ', 'Destination file or address') - .action(this.run) - .allowUnknownOption(true); - command._exit = () => {}; - command.on('--help', () => { help = true; }) - } - - static async run(amount, command_obj) { - if (help) { - help = false; - return; - } - - if (command_obj.destination == null) { - console.error('destination required'); - return; - } - - var params = { - session_token: global.session_token, - amount: amount * Math.pow(10, 9), - fee_base: 1000000, - selection_strategy: { strategy: 'SMALLEST' }, - post_tx: { method: 'STEM' } - }; - - const method = command_obj.method == null ? null : command_obj.method.toLowerCase(); - if (method == 'tor') { - params.address = command_obj.destination; - } else if (method == 'file') { - params.file = command_obj.destination; - } else if (method == 'http') { - return await SendToHTTP.send(command_obj.destination, amount * Math.pow(10, 9)); - } else { - console.error('method required (tor, http, file)'); - return; - } - - const response = await WalletAPI.OwnerRPC('send', params); - if (response != null && response.result != null) { - console.log('Sent successfully'); - console.log(JSON.stringify(response.result.slate)); - } else { - console.error("Failed to send!\n"); - console.error(JSON.stringify(response)); - } - } -} - -module.exports = Send; \ No newline at end of file diff --git a/lib/cli/commands/Summary.js b/lib/cli/commands/Summary.js deleted file mode 100644 index ec19b78..0000000 --- a/lib/cli/commands/Summary.js +++ /dev/null @@ -1,79 +0,0 @@ -const WalletAPI = require('../../api/WalletAPI'); -const Tables = require('../Tables'); -const Wallet = require('../Wallet'); - -var help = false; -var command = null; -class Summary { - static add_command(program) { - command = program.command('summary') - .description('Wallet summary') - .option('-c, --canceled', 'include canceled txs') - .action(this.run) - .allowUnknownOption(true); - command._exit = () => {}; - command.on('--help', () => { help = true; }) - } - - static get_status(txn, lastConfirmedHeight) { - const status = txn.type; - if (status == "Sent" || status == "Received") { - if ((txn.confirmed_height + 9) > lastConfirmedHeight) { - return status + " (" + (lastConfirmedHeight - txn.confirmed_height + 1) + " Confirmations)"; - } - - return status; - } else if (status == "Sending (Finalized)") { - return "Sending (Unconfirmed)"; - } else { - return status; - } - } - - static async run(command_obj) { - if (help) { - help = false; - return; - } - - Wallet.display_info(); - - const headers = { - session_token: global.session_token - }; - - const response = await WalletAPI.GET('retrieve_summary_info', headers, {}); - if (response != null && response.status_code == 200) { - const summary = response.body; - Tables.totals({ - spendable: summary.amount_currently_spendable, - total: summary.total, - immature: summary.amount_immature, - unconfirmed: summary.amount_awaiting_confirmation, - locked: summary.amount_locked - }); - - const txs = []; - response.body.transactions.forEach((tx, index) => { - const status = Summary.get_status(tx, response.body.last_confirmed_height); - if (command_obj.canceled || status != 'Canceled') { - txs.push({ - id: tx.id, - amount: tx.amount_credited - tx.amount_debited, - address: tx.address, - status: status, - creation_date_time: tx.creation_date_time - }); - } - }); - - Tables.transactions(txs); - } else { - console.error("Failed to retrieve wallet summary!\n"); - console.error(`Status: ${response.status_code}`); - console.error(`Error: ${response.body}`); - } - } -} - -module.exports = Summary; \ No newline at end of file diff --git a/lib/cli/commands/index.js b/lib/cli/commands/index.js deleted file mode 100644 index 7fea844..0000000 --- a/lib/cli/commands/index.js +++ /dev/null @@ -1,15 +0,0 @@ -const outer = {}; -outer.CreateWallet = require('./CreateWallet'); -outer.RestoreWallet = require('./RestoreWallet'); -outer.OpenWallet = require('./OpenWallet'); - -const inner = {}; -inner.Summary = require('./Summary'); -inner.Send = require('./Send'); -inner.Receive = require('./Receive'); -inner.Finalize = require('./Finalize'); -inner.Clear = require('./Clear'); -inner.Exit = require('./Exit'); - -exports.outer = outer; -exports.inner = inner; \ No newline at end of file diff --git a/package-lock.json b/package-lock.json deleted file mode 100644 index 6840fd6..0000000 --- a/package-lock.json +++ /dev/null @@ -1,2523 +0,0 @@ -{ - "name": "grinpp", - "version": "1.0.0", - "lockfileVersion": 1, - "requires": true, - "dependencies": { - "@babel/parser": { - "version": "7.4.5", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.4.5.tgz", - "integrity": "sha512-9mUqkL1FF5T7f0WDFfAoDdiMVPWsdD1gZYzSnaXsxUCUqzuch/8of9G3VUSNiZmMBoRxT3neyVsqeiL/ZPcjew==", - "dev": true - }, - "@babel/runtime": { - "version": "7.4.5", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.4.5.tgz", - "integrity": "sha512-TuI4qpWZP6lGOGIuGWtp9sPluqYICmbk8T/1vpSysqJxRPkudh/ofFWyqdcMsDf2s7KvDL4/YHgKyvcS3g9CJQ==", - "dev": true, - "requires": { - "regenerator-runtime": "^0.13.2" - } - }, - "@mrmlnc/readdir-enhanced": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/@mrmlnc/readdir-enhanced/-/readdir-enhanced-2.2.1.tgz", - "integrity": "sha512-bPHp6Ji8b41szTOcaP63VlnbbO5Ny6dwAATtY6JTjh5N2OLrb5Qk/Th5cRkRQhkWCt+EJsYrNB0MiL+Gpn6e3g==", - "dev": true, - "requires": { - "call-me-maybe": "^1.0.1", - "glob-to-regexp": "^0.3.0" - } - }, - "@nodelib/fs.stat": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-1.1.3.tgz", - "integrity": "sha512-shAmDyaQC4H92APFoIaVDHCx5bStIocgvbwQyxPRrbUY20V1EYTbSDchWbuwlMG3V17cprZhA6+78JfB+3DTPw==", - "dev": true - }, - "@types/color-name": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@types/color-name/-/color-name-1.1.1.tgz", - "integrity": "sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ==" - }, - "@types/events": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@types/events/-/events-3.0.0.tgz", - "integrity": "sha512-EaObqwIvayI5a8dCzhFrjKzVwKLxjoG9T6Ppd5CEo07LRKfQ8Yokw54r5+Wq7FaBQ+yXRvQAYPrHwya1/UFt9g==", - "dev": true - }, - "@types/glob": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.1.1.tgz", - "integrity": "sha512-1Bh06cbWJUHMC97acuD6UMG29nMt0Aqz1vF3guLfG+kHHJhy3AyohZFFxYk2f7Q1SQIrNwvncxAE0N/9s70F2w==", - "dev": true, - "requires": { - "@types/events": "*", - "@types/minimatch": "*", - "@types/node": "*" - } - }, - "@types/minimatch": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.3.tgz", - "integrity": "sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA==", - "dev": true - }, - "@types/node": { - "version": "12.7.5", - "resolved": "https://registry.npmjs.org/@types/node/-/node-12.7.5.tgz", - "integrity": "sha512-9fq4jZVhPNW8r+UYKnxF1e2HkDWOWKM5bC2/7c9wPV835I0aOrVbS/Hw/pWPk2uKrNXQqg9Z959Kz+IYDd5p3w==", - "dev": true - }, - "ajv": { - "version": "6.10.2", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.10.2.tgz", - "integrity": "sha512-TXtUUEYHuaTEbLZWIKUr5pmBuhDLy+8KYtPYdcV8qC+pOZL+NKqYwvWSRrVXHn+ZmRRAu8vJTAznH7Oag6RVRw==", - "dev": true, - "requires": { - "fast-deep-equal": "^2.0.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - } - }, - "ansi-regex": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", - "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==" - }, - "ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "requires": { - "color-convert": "^1.9.0" - } - }, - "arr-diff": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", - "integrity": "sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=", - "dev": true - }, - "arr-flatten": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/arr-flatten/-/arr-flatten-1.1.0.tgz", - "integrity": "sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg==", - "dev": true - }, - "arr-union": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/arr-union/-/arr-union-3.1.0.tgz", - "integrity": "sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ=", - "dev": true - }, - "array-union": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/array-union/-/array-union-1.0.2.tgz", - "integrity": "sha1-mjRBDk9OPaI96jdb5b5w8kd47Dk=", - "dev": true, - "requires": { - "array-uniq": "^1.0.1" - } - }, - "array-uniq": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/array-uniq/-/array-uniq-1.0.3.tgz", - "integrity": "sha1-r2rId6Jcx/dOBYiUdThY39sk/bY=", - "dev": true - }, - "array-unique": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz", - "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=", - "dev": true - }, - "asn1": { - "version": "0.2.4", - "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz", - "integrity": "sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg==", - "dev": true, - "requires": { - "safer-buffer": "~2.1.0" - } - }, - "assert-plus": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", - "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", - "dev": true - }, - "assign-symbols": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/assign-symbols/-/assign-symbols-1.0.0.tgz", - "integrity": "sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c=", - "dev": true - }, - "asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=", - "dev": true - }, - "atob": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz", - "integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==", - "dev": true - }, - "aws-sign2": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", - "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=", - "dev": true - }, - "aws4": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.8.0.tgz", - "integrity": "sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ==", - "dev": true - }, - "axios": { - "version": "0.19.2", - "resolved": "https://registry.npmjs.org/axios/-/axios-0.19.2.tgz", - "integrity": "sha512-fjgm5MvRHLhx+osE2xoekY70AhARk3a6hkN+3Io1jc00jtquGvxYlKlsFUhmUET0V5te6CcZI7lcv2Ym61mjHA==", - "requires": { - "follow-redirects": "1.5.10" - } - }, - "balanced-match": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", - "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", - "dev": true - }, - "base": { - "version": "0.11.2", - "resolved": "https://registry.npmjs.org/base/-/base-0.11.2.tgz", - "integrity": "sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg==", - "dev": true, - "requires": { - "cache-base": "^1.0.1", - "class-utils": "^0.3.5", - "component-emitter": "^1.2.1", - "define-property": "^1.0.0", - "isobject": "^3.0.1", - "mixin-deep": "^1.2.0", - "pascalcase": "^0.1.1" - }, - "dependencies": { - "define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", - "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", - "dev": true, - "requires": { - "is-descriptor": "^1.0.0" - } - }, - "is-accessor-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", - "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", - "dev": true, - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-data-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", - "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", - "dev": true, - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-descriptor": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", - "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", - "dev": true, - "requires": { - "is-accessor-descriptor": "^1.0.0", - "is-data-descriptor": "^1.0.0", - "kind-of": "^6.0.2" - } - } - } - }, - "bcrypt-pbkdf": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", - "integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=", - "dev": true, - "requires": { - "tweetnacl": "^0.14.3" - } - }, - "brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "requires": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "braces": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", - "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", - "dev": true, - "requires": { - "arr-flatten": "^1.1.0", - "array-unique": "^0.3.2", - "extend-shallow": "^2.0.1", - "fill-range": "^4.0.0", - "isobject": "^3.0.1", - "repeat-element": "^1.1.2", - "snapdragon": "^0.8.1", - "snapdragon-node": "^2.0.1", - "split-string": "^3.0.2", - "to-regex": "^3.0.1" - }, - "dependencies": { - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - } - } - }, - "byline": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/byline/-/byline-5.0.0.tgz", - "integrity": "sha1-dBxSFkaOrcRXsDQQEYrXfejB3bE=", - "dev": true - }, - "cache-base": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/cache-base/-/cache-base-1.0.1.tgz", - "integrity": "sha512-AKcdTnFSWATd5/GCPRxr2ChwIJ85CeyrEyjRHlKxQ56d4XJMGym0uAiKn0xbLOGOl3+yRpOTi484dVCEc5AUzQ==", - "dev": true, - "requires": { - "collection-visit": "^1.0.0", - "component-emitter": "^1.2.1", - "get-value": "^2.0.6", - "has-value": "^1.0.0", - "isobject": "^3.0.1", - "set-value": "^2.0.0", - "to-object-path": "^0.3.0", - "union-value": "^1.0.0", - "unset-value": "^1.0.0" - } - }, - "call-me-maybe": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/call-me-maybe/-/call-me-maybe-1.0.1.tgz", - "integrity": "sha1-JtII6onje1y95gJQoV8DHBak1ms=", - "dev": true - }, - "camelcase": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", - "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==" - }, - "caseless": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", - "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=", - "dev": true - }, - "chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - } - }, - "class-utils": { - "version": "0.3.6", - "resolved": "https://registry.npmjs.org/class-utils/-/class-utils-0.3.6.tgz", - "integrity": "sha512-qOhPa/Fj7s6TY8H8esGu5QNpMMQxz79h+urzrNYN6mn+9BnxlDGf5QZ+XeCDsxSjPqsSR56XOZOJmpeurnLMeg==", - "dev": true, - "requires": { - "arr-union": "^3.1.0", - "define-property": "^0.2.5", - "isobject": "^3.0.0", - "static-extend": "^0.1.1" - }, - "dependencies": { - "define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", - "dev": true, - "requires": { - "is-descriptor": "^0.1.0" - } - } - } - }, - "cli-table3": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/cli-table3/-/cli-table3-0.5.1.tgz", - "integrity": "sha512-7Qg2Jrep1S/+Q3EceiZtQcDPWxhAvBw+ERf1162v4sikJrvojMHFqXt8QIVha8UlH9rgU0BeWPytZ9/TzYqlUw==", - "requires": { - "colors": "^1.1.2", - "object-assign": "^4.1.0", - "string-width": "^2.1.1" - }, - "dependencies": { - "ansi-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", - "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=" - }, - "is-fullwidth-code-point": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=" - }, - "string-width": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", - "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", - "requires": { - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^4.0.0" - } - }, - "strip-ansi": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", - "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", - "requires": { - "ansi-regex": "^3.0.0" - } - } - } - }, - "cliui": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", - "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", - "requires": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.0", - "wrap-ansi": "^6.2.0" - } - }, - "collection-visit": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/collection-visit/-/collection-visit-1.0.0.tgz", - "integrity": "sha1-S8A3PBZLwykbTTaMgpzxqApZ3KA=", - "dev": true, - "requires": { - "map-visit": "^1.0.0", - "object-visit": "^1.0.0" - } - }, - "color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dev": true, - "requires": { - "color-name": "1.1.3" - } - }, - "color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", - "dev": true - }, - "colors": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/colors/-/colors-1.4.0.tgz", - "integrity": "sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==", - "optional": true - }, - "combined-stream": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", - "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "dev": true, - "requires": { - "delayed-stream": "~1.0.0" - } - }, - "commander": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", - "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==" - }, - "component-emitter": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz", - "integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==", - "dev": true - }, - "concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", - "dev": true - }, - "copy-descriptor": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/copy-descriptor/-/copy-descriptor-0.1.1.tgz", - "integrity": "sha1-Z29us8OZl8LuGsOpJP1hJHSPV40=", - "dev": true - }, - "core-util-is": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", - "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=", - "dev": true - }, - "dashdash": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", - "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", - "dev": true, - "requires": { - "assert-plus": "^1.0.0" - } - }, - "debug": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", - "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", - "requires": { - "ms": "2.0.0" - } - }, - "decamelize": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", - "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=" - }, - "decode-uri-component": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz", - "integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=", - "dev": true - }, - "deep-is": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz", - "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=", - "dev": true - }, - "define-property": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-2.0.2.tgz", - "integrity": "sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ==", - "dev": true, - "requires": { - "is-descriptor": "^1.0.2", - "isobject": "^3.0.1" - }, - "dependencies": { - "is-accessor-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", - "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", - "dev": true, - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-data-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", - "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", - "dev": true, - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-descriptor": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", - "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", - "dev": true, - "requires": { - "is-accessor-descriptor": "^1.0.0", - "is-data-descriptor": "^1.0.0", - "kind-of": "^6.0.2" - } - } - } - }, - "delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=", - "dev": true - }, - "dir-glob": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-2.2.2.tgz", - "integrity": "sha512-f9LBi5QWzIW3I6e//uxZoLBlUt9kcp66qo0sSCxL6YZKc75R1c4MFCoe/LaZiBGmgujvQdxc5Bn3QhfyvK5Hsw==", - "dev": true, - "requires": { - "path-type": "^3.0.0" - } - }, - "ecc-jsbn": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", - "integrity": "sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=", - "dev": true, - "requires": { - "jsbn": "~0.1.0", - "safer-buffer": "^2.1.0" - } - }, - "emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" - }, - "escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", - "dev": true - }, - "escodegen": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.11.1.tgz", - "integrity": "sha512-JwiqFD9KdGVVpeuRa68yU3zZnBEOcPs0nKW7wZzXky8Z7tffdYUHbe11bPCV5jYlK6DVdKLWLm0f5I/QlL0Kmw==", - "dev": true, - "requires": { - "esprima": "^3.1.3", - "estraverse": "^4.2.0", - "esutils": "^2.0.2", - "optionator": "^0.8.1", - "source-map": "~0.6.1" - } - }, - "esprima": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-3.1.3.tgz", - "integrity": "sha1-/cpRzuYTOJXjyI1TXOSdv/YqRjM=", - "dev": true - }, - "estraverse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", - "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", - "dev": true - }, - "esutils": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", - "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", - "dev": true - }, - "expand-brackets": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz", - "integrity": "sha1-t3c14xXOMPa27/D4OwQVGiJEliI=", - "dev": true, - "requires": { - "debug": "^2.3.3", - "define-property": "^0.2.5", - "extend-shallow": "^2.0.1", - "posix-character-classes": "^0.1.0", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.1" - }, - "dependencies": { - "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "requires": { - "ms": "2.0.0" - } - }, - "define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", - "dev": true, - "requires": { - "is-descriptor": "^0.1.0" - } - }, - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", - "dev": true - } - } - }, - "expand-template": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", - "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", - "dev": true - }, - "extend": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", - "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", - "dev": true - }, - "extend-shallow": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", - "integrity": "sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg=", - "dev": true, - "requires": { - "assign-symbols": "^1.0.0", - "is-extendable": "^1.0.1" - }, - "dependencies": { - "is-extendable": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", - "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", - "dev": true, - "requires": { - "is-plain-object": "^2.0.4" - } - } - } - }, - "extglob": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz", - "integrity": "sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==", - "dev": true, - "requires": { - "array-unique": "^0.3.2", - "define-property": "^1.0.0", - "expand-brackets": "^2.1.4", - "extend-shallow": "^2.0.1", - "fragment-cache": "^0.2.1", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.1" - }, - "dependencies": { - "define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", - "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", - "dev": true, - "requires": { - "is-descriptor": "^1.0.0" - } - }, - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - }, - "is-accessor-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", - "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", - "dev": true, - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-data-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", - "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", - "dev": true, - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-descriptor": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", - "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", - "dev": true, - "requires": { - "is-accessor-descriptor": "^1.0.0", - "is-data-descriptor": "^1.0.0", - "kind-of": "^6.0.2" - } - } - } - }, - "extsprintf": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", - "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=", - "dev": true - }, - "fast-deep-equal": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz", - "integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk=", - "dev": true - }, - "fast-glob": { - "version": "2.2.7", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-2.2.7.tgz", - "integrity": "sha512-g1KuQwHOZAmOZMuBtHdxDtju+T2RT8jgCC9aANsbpdiDDTSnjgfuVsIBNKbUeJI3oKMRExcfNDtJl4OhbffMsw==", - "dev": true, - "requires": { - "@mrmlnc/readdir-enhanced": "^2.2.1", - "@nodelib/fs.stat": "^1.1.2", - "glob-parent": "^3.1.0", - "is-glob": "^4.0.0", - "merge2": "^1.2.3", - "micromatch": "^3.1.10" - } - }, - "fast-json-stable-stringify": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz", - "integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I=", - "dev": true - }, - "fast-levenshtein": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=", - "dev": true - }, - "fill-range": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", - "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=", - "dev": true, - "requires": { - "extend-shallow": "^2.0.1", - "is-number": "^3.0.0", - "repeat-string": "^1.6.1", - "to-regex-range": "^2.1.0" - }, - "dependencies": { - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - } - } - }, - "find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", - "requires": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" - } - }, - "follow-redirects": { - "version": "1.5.10", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.5.10.tgz", - "integrity": "sha512-0V5l4Cizzvqt5D44aTXbFZz+FtyXV1vrDN6qrelxtfYQKW0KO0W2T/hkE8xvGa/540LkZlkaUjO4ailYTFtHVQ==", - "requires": { - "debug": "=3.1.0" - } - }, - "for-in": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", - "integrity": "sha1-gQaNKVqBQuwKxybG4iAMMPttXoA=", - "dev": true - }, - "forever-agent": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", - "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=", - "dev": true - }, - "fragment-cache": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/fragment-cache/-/fragment-cache-0.2.1.tgz", - "integrity": "sha1-QpD60n8T6Jvn8zeZxrxaCr//DRk=", - "dev": true, - "requires": { - "map-cache": "^0.2.2" - } - }, - "from2": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/from2/-/from2-2.3.0.tgz", - "integrity": "sha1-i/tVAr3kpNNs/e6gB/zKIdfjgq8=", - "dev": true, - "requires": { - "inherits": "^2.0.1", - "readable-stream": "^2.0.0" - }, - "dependencies": { - "readable-stream": { - "version": "2.3.6", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", - "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", - "dev": true, - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true - }, - "string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dev": true, - "requires": { - "safe-buffer": "~5.1.0" - } - } - } - }, - "fs-extra": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.1.tgz", - "integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==", - "dev": true, - "requires": { - "graceful-fs": "^4.1.2", - "jsonfile": "^4.0.0", - "universalify": "^0.1.0" - } - }, - "fs-jetpack": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/fs-jetpack/-/fs-jetpack-2.2.2.tgz", - "integrity": "sha512-USJrUxck7SIXSvYPzU5fuR5iqLHRDSzb0kHvCJlQhUGEVai3P9yZDu/2b+bAzprbWLCc2YcslxBLBUInDmYkYA==", - "dev": true, - "requires": { - "minimatch": "^3.0.2", - "rimraf": "^2.6.3" - } - }, - "fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", - "dev": true - }, - "get-caller-file": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", - "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==" - }, - "get-value": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/get-value/-/get-value-2.0.6.tgz", - "integrity": "sha1-3BXKHGcjh8p2vTesCjlbogQqLCg=", - "dev": true - }, - "getpass": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", - "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", - "dev": true, - "requires": { - "assert-plus": "^1.0.0" - } - }, - "glob": { - "version": "7.1.4", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.4.tgz", - "integrity": "sha512-hkLPepehmnKk41pUGm3sYxoFs/umurYfYJCerbXEyFIWcAzvpipAgVkBqqT9RBKMGjnq6kMuyYwha6csxbiM1A==", - "dev": true, - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - }, - "glob-parent": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-3.1.0.tgz", - "integrity": "sha1-nmr2KZ2NO9K9QEMIMr0RPfkGxa4=", - "dev": true, - "requires": { - "is-glob": "^3.1.0", - "path-dirname": "^1.0.0" - }, - "dependencies": { - "is-glob": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz", - "integrity": "sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=", - "dev": true, - "requires": { - "is-extglob": "^2.1.0" - } - } - } - }, - "glob-to-regexp": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.3.0.tgz", - "integrity": "sha1-jFoUlNIGbFcMw7/kSWF1rMTVAqs=", - "dev": true - }, - "globby": { - "version": "9.2.0", - "resolved": "https://registry.npmjs.org/globby/-/globby-9.2.0.tgz", - "integrity": "sha512-ollPHROa5mcxDEkwg6bPt3QbEf4pDQSNtd6JPL1YvOvAo/7/0VAm9TccUeoTmarjPw4pfUthSCqcyfNB1I3ZSg==", - "dev": true, - "requires": { - "@types/glob": "^7.1.1", - "array-union": "^1.0.2", - "dir-glob": "^2.2.2", - "fast-glob": "^2.2.6", - "glob": "^7.1.3", - "ignore": "^4.0.3", - "pify": "^4.0.1", - "slash": "^2.0.0" - } - }, - "graceful-fs": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.2.tgz", - "integrity": "sha512-IItsdsea19BoLC7ELy13q1iJFNmd7ofZH5+X/pJr90/nRoPEX0DJo1dHDbgtYWOhJhcCgMDTOw84RZ72q6lB+Q==", - "dev": true - }, - "har-schema": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", - "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=", - "dev": true - }, - "har-validator": { - "version": "5.1.3", - "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.3.tgz", - "integrity": "sha512-sNvOCzEQNr/qrvJgc3UG/kD4QtlHycrzwS+6mfTrrSq97BvaYcPZZI1ZSqGSPR73Cxn4LKTD4PttRwfU7jWq5g==", - "dev": true, - "requires": { - "ajv": "^6.5.5", - "har-schema": "^2.0.0" - } - }, - "has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", - "dev": true - }, - "has-value": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-value/-/has-value-1.0.0.tgz", - "integrity": "sha1-GLKB2lhbHFxR3vJMkw7SmgvmsXc=", - "dev": true, - "requires": { - "get-value": "^2.0.6", - "has-values": "^1.0.0", - "isobject": "^3.0.0" - } - }, - "has-values": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-values/-/has-values-1.0.0.tgz", - "integrity": "sha1-lbC2P+whRmGab+V/51Yo1aOe/k8=", - "dev": true, - "requires": { - "is-number": "^3.0.0", - "kind-of": "^4.0.0" - }, - "dependencies": { - "kind-of": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-4.0.0.tgz", - "integrity": "sha1-IIE989cSkosgc3hpGkUGb65y3Vc=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "http-signature": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", - "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", - "dev": true, - "requires": { - "assert-plus": "^1.0.0", - "jsprim": "^1.2.2", - "sshpk": "^1.7.0" - } - }, - "ignore": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz", - "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==", - "dev": true - }, - "inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", - "dev": true, - "requires": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true - }, - "into-stream": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/into-stream/-/into-stream-5.1.0.tgz", - "integrity": "sha512-cbDhb8qlxKMxPBk/QxTtYg1DQ4CwXmadu7quG3B7nrJsgSncEreF2kwWKZFdnjc/lSNNIkFPsjI7SM0Cx/QXPw==", - "dev": true, - "requires": { - "from2": "^2.3.0", - "p-is-promise": "^2.0.0" - } - }, - "is-accessor-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", - "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", - "dev": true, - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "is-buffer": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", - "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", - "dev": true - }, - "is-data-descriptor": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", - "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", - "dev": true, - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "is-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", - "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", - "dev": true, - "requires": { - "is-accessor-descriptor": "^0.1.6", - "is-data-descriptor": "^0.1.4", - "kind-of": "^5.0.0" - }, - "dependencies": { - "kind-of": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", - "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", - "dev": true - } - } - }, - "is-extendable": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", - "integrity": "sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik=", - "dev": true - }, - "is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", - "dev": true - }, - "is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==" - }, - "is-glob": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz", - "integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==", - "dev": true, - "requires": { - "is-extglob": "^2.1.1" - } - }, - "is-number": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", - "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", - "dev": true, - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "is-plain-object": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", - "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", - "dev": true, - "requires": { - "isobject": "^3.0.1" - } - }, - "is-typedarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", - "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=", - "dev": true - }, - "is-windows": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", - "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==", - "dev": true - }, - "isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", - "dev": true - }, - "isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", - "dev": true - }, - "isstream": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", - "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=", - "dev": true - }, - "jsbn": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", - "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=", - "dev": true - }, - "json-schema": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", - "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=", - "dev": true - }, - "json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true - }, - "json-stringify-safe": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", - "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=", - "dev": true - }, - "jsonfile": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", - "integrity": "sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=", - "dev": true, - "requires": { - "graceful-fs": "^4.1.6" - } - }, - "jsprim": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", - "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=", - "dev": true, - "requires": { - "assert-plus": "1.0.0", - "extsprintf": "1.3.0", - "json-schema": "0.2.3", - "verror": "1.10.0" - } - }, - "kind-of": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", - "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==", - "dev": true - }, - "levn": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", - "integrity": "sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=", - "dev": true, - "requires": { - "prelude-ls": "~1.1.2", - "type-check": "~0.3.2" - } - }, - "locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", - "requires": { - "p-locate": "^4.1.0" - } - }, - "map-cache": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz", - "integrity": "sha1-wyq9C9ZSXZsFFkW7TyasXcmKDb8=", - "dev": true - }, - "map-visit": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/map-visit/-/map-visit-1.0.0.tgz", - "integrity": "sha1-7Nyo8TFE5mDxtb1B8S80edmN+48=", - "dev": true, - "requires": { - "object-visit": "^1.0.0" - } - }, - "merge2": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.3.0.tgz", - "integrity": "sha512-2j4DAdlBOkiSZIsaXk4mTE3sRS02yBHAtfy127xRV3bQUFqXkjHCHLW6Scv7DwNRbIWNHH8zpnz9zMaKXIdvYw==", - "dev": true - }, - "micromatch": { - "version": "3.1.10", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", - "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", - "dev": true, - "requires": { - "arr-diff": "^4.0.0", - "array-unique": "^0.3.2", - "braces": "^2.3.1", - "define-property": "^2.0.2", - "extend-shallow": "^3.0.2", - "extglob": "^2.0.4", - "fragment-cache": "^0.2.1", - "kind-of": "^6.0.2", - "nanomatch": "^1.2.9", - "object.pick": "^1.3.0", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.2" - } - }, - "mime-db": { - "version": "1.40.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.40.0.tgz", - "integrity": "sha512-jYdeOMPy9vnxEqFRRo6ZvTZ8d9oPb+k18PKoYNYUe2stVEBPPwsln/qWzdbmaIvnhZ9v2P+CuecK+fpUfsV2mA==", - "dev": true - }, - "mime-types": { - "version": "2.1.24", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.24.tgz", - "integrity": "sha512-WaFHS3MCl5fapm3oLxU4eYDw77IQM2ACcxQ9RIxfaC3ooc6PFuBMGZZsYpvoXS5D5QTWPieo1jjLdAm3TBP3cQ==", - "dev": true, - "requires": { - "mime-db": "1.40.0" - } - }, - "minimatch": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", - "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", - "dev": true, - "requires": { - "brace-expansion": "^1.1.7" - } - }, - "minimist": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", - "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", - "dev": true - }, - "mixin-deep": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.2.tgz", - "integrity": "sha512-WRoDn//mXBiJ1H40rqa3vH0toePwSsGb45iInWlTySa+Uu4k3tYUSxa2v1KqAiLtvlrSzaExqS1gtk96A9zvEA==", - "dev": true, - "requires": { - "for-in": "^1.0.2", - "is-extendable": "^1.0.1" - }, - "dependencies": { - "is-extendable": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", - "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", - "dev": true, - "requires": { - "is-plain-object": "^2.0.4" - } - } - } - }, - "mkdirp": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", - "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", - "dev": true, - "requires": { - "minimist": "0.0.8" - }, - "dependencies": { - "minimist": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", - "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", - "dev": true - } - } - }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" - }, - "multistream": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/multistream/-/multistream-2.1.1.tgz", - "integrity": "sha512-xasv76hl6nr1dEy3lPvy7Ej7K/Lx3O/FCvwge8PeVJpciPPoNCbaANcNiBug3IpdvTveZUcAV0DJzdnUDMesNQ==", - "dev": true, - "requires": { - "inherits": "^2.0.1", - "readable-stream": "^2.0.5" - }, - "dependencies": { - "readable-stream": { - "version": "2.3.6", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", - "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", - "dev": true, - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true - }, - "string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dev": true, - "requires": { - "safe-buffer": "~5.1.0" - } - } - } - }, - "nanomatch": { - "version": "1.2.13", - "resolved": "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.13.tgz", - "integrity": "sha512-fpoe2T0RbHwBTBUOftAfBPaDEi06ufaUai0mE6Yn1kacc3SnTErfb/h+X94VXzI64rKFHYImXSvdwGGCmwOqCA==", - "dev": true, - "requires": { - "arr-diff": "^4.0.0", - "array-unique": "^0.3.2", - "define-property": "^2.0.2", - "extend-shallow": "^3.0.2", - "fragment-cache": "^0.2.1", - "is-windows": "^1.0.2", - "kind-of": "^6.0.2", - "object.pick": "^1.3.0", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.1" - } - }, - "oauth-sign": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", - "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==", - "dev": true - }, - "object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" - }, - "object-copy": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/object-copy/-/object-copy-0.1.0.tgz", - "integrity": "sha1-fn2Fi3gb18mRpBupde04EnVOmYw=", - "dev": true, - "requires": { - "copy-descriptor": "^0.1.0", - "define-property": "^0.2.5", - "kind-of": "^3.0.3" - }, - "dependencies": { - "define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", - "dev": true, - "requires": { - "is-descriptor": "^0.1.0" - } - }, - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "object-visit": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/object-visit/-/object-visit-1.0.1.tgz", - "integrity": "sha1-95xEk68MU3e1n+OdOV5BBC3QRbs=", - "dev": true, - "requires": { - "isobject": "^3.0.0" - } - }, - "object.pick": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/object.pick/-/object.pick-1.3.0.tgz", - "integrity": "sha1-h6EKxMFpS9Lhy/U1kaZhQftd10c=", - "dev": true, - "requires": { - "isobject": "^3.0.1" - } - }, - "once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", - "dev": true, - "requires": { - "wrappy": "1" - } - }, - "optionator": { - "version": "0.8.2", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.2.tgz", - "integrity": "sha1-NkxeQJ0/TWMB1sC0wFu6UBgK62Q=", - "dev": true, - "requires": { - "deep-is": "~0.1.3", - "fast-levenshtein": "~2.0.4", - "levn": "~0.3.0", - "prelude-ls": "~1.1.2", - "type-check": "~0.3.2", - "wordwrap": "~1.0.0" - } - }, - "os-tmpdir": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", - "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=", - "dev": true - }, - "p-is-promise": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/p-is-promise/-/p-is-promise-2.1.0.tgz", - "integrity": "sha512-Y3W0wlRPK8ZMRbNq97l4M5otioeA5lm1z7bkNkxCka8HSPjR0xRWmpCmc9utiaLP9Jb1eD8BgeIxTW4AIF45Pg==", - "dev": true - }, - "p-limit": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.2.2.tgz", - "integrity": "sha512-WGR+xHecKTr7EbUEhyLSh5Dube9JtdiG78ufaeLxTgpudf/20KqyMioIUZJAezlTIi6evxuoUs9YXc11cU+yzQ==", - "requires": { - "p-try": "^2.0.0" - } - }, - "p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", - "requires": { - "p-limit": "^2.2.0" - } - }, - "p-try": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", - "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==" - }, - "pascalcase": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/pascalcase/-/pascalcase-0.1.1.tgz", - "integrity": "sha1-s2PlXoAGym/iF4TS2yK9FdeRfxQ=", - "dev": true - }, - "path-dirname": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/path-dirname/-/path-dirname-1.0.2.tgz", - "integrity": "sha1-zDPSTVJeCZpTiMAzbG4yuRYGCeA=", - "dev": true - }, - "path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==" - }, - "path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", - "dev": true - }, - "path-parse": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz", - "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==", - "dev": true - }, - "path-type": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-3.0.0.tgz", - "integrity": "sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg==", - "dev": true, - "requires": { - "pify": "^3.0.0" - }, - "dependencies": { - "pify": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", - "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", - "dev": true - } - } - }, - "performance-now": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", - "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=", - "dev": true - }, - "pify": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", - "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", - "dev": true - }, - "pkg": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/pkg/-/pkg-4.4.0.tgz", - "integrity": "sha512-bFNJ3v56QwqB6JtAl/YrczlmEKBPBVJ3n5nW905kgvG1ex9DajODpTs0kLAFxyLwoubDQux/RPJFL6WrnD/vpg==", - "dev": true, - "requires": { - "@babel/parser": "~7.4.4", - "@babel/runtime": "~7.4.4", - "chalk": "~2.4.2", - "escodegen": "~1.11.1", - "fs-extra": "~7.0.1", - "globby": "~9.2.0", - "into-stream": "~5.1.0", - "minimist": "~1.2.0", - "multistream": "~2.1.1", - "pkg-fetch": "~2.6.2", - "progress": "~2.0.3", - "resolve": "1.6.0", - "stream-meter": "~1.0.4" - } - }, - "pkg-fetch": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/pkg-fetch/-/pkg-fetch-2.6.2.tgz", - "integrity": "sha512-7DN6YYP1Kct02mSkhfblK0HkunJ7BJjGBkSkFdIW/QKIovtAMaICidS7feX+mHfnZ98OP7xFJvBluVURlrHJxA==", - "dev": true, - "requires": { - "@babel/runtime": "~7.4.4", - "byline": "~5.0.0", - "chalk": "~2.4.1", - "expand-template": "~2.0.3", - "fs-extra": "~7.0.1", - "minimist": "~1.2.0", - "progress": "~2.0.0", - "request": "~2.88.0", - "request-progress": "~3.0.0", - "semver": "~6.0.0", - "unique-temp-dir": "~1.0.0" - }, - "dependencies": { - "semver": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.0.0.tgz", - "integrity": "sha512-0UewU+9rFapKFnlbirLi3byoOuhrSsli/z/ihNnvM24vgF+8sNBiI1LZPBSH9wJKUwaUbw+s3hToDLCXkrghrQ==", - "dev": true - } - } - }, - "posix-character-classes": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/posix-character-classes/-/posix-character-classes-0.1.1.tgz", - "integrity": "sha1-AerA/jta9xoqbAL+q7jB/vfgDqs=", - "dev": true - }, - "prelude-ls": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", - "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=", - "dev": true - }, - "process-nextick-args": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", - "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", - "dev": true - }, - "progress": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", - "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", - "dev": true - }, - "psl": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/psl/-/psl-1.4.0.tgz", - "integrity": "sha512-HZzqCGPecFLyoRj5HLfuDSKYTJkAfB5thKBIkRHtGjWwY7p1dAyveIbXIq4tO0KYfDF2tHqPUgY9SDnGm00uFw==", - "dev": true - }, - "punycode": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", - "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", - "dev": true - }, - "regenerator-runtime": { - "version": "0.13.3", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.3.tgz", - "integrity": "sha512-naKIZz2GQ8JWh///G7L3X6LaQUAMp2lvb1rvwwsURe/VXwD6VMfr+/1NuNw3ag8v2kY1aQ/go5SNn79O9JU7yw==", - "dev": true - }, - "regex-not": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/regex-not/-/regex-not-1.0.2.tgz", - "integrity": "sha512-J6SDjUgDxQj5NusnOtdFxDwN/+HWykR8GELwctJ7mdqhcyy1xEc4SRFHUXvxTp661YaVKAjfRLZ9cCqS6tn32A==", - "dev": true, - "requires": { - "extend-shallow": "^3.0.2", - "safe-regex": "^1.1.0" - } - }, - "repeat-element": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/repeat-element/-/repeat-element-1.1.3.tgz", - "integrity": "sha512-ahGq0ZnV5m5XtZLMb+vP76kcAM5nkLqk0lpqAuojSKGgQtn4eRi4ZZGm2olo2zKFH+sMsWaqOCW1dqAnOru72g==", - "dev": true - }, - "repeat-string": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", - "integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc=", - "dev": true - }, - "request": { - "version": "2.88.0", - "resolved": "https://registry.npmjs.org/request/-/request-2.88.0.tgz", - "integrity": "sha512-NAqBSrijGLZdM0WZNsInLJpkJokL72XYjUpnB0iwsRgxh7dB6COrHnTBNwN0E+lHDAJzu7kLAkDeY08z2/A0hg==", - "dev": true, - "requires": { - "aws-sign2": "~0.7.0", - "aws4": "^1.8.0", - "caseless": "~0.12.0", - "combined-stream": "~1.0.6", - "extend": "~3.0.2", - "forever-agent": "~0.6.1", - "form-data": "~2.3.2", - "har-validator": "~5.1.0", - "http-signature": "~1.2.0", - "is-typedarray": "~1.0.0", - "isstream": "~0.1.2", - "json-stringify-safe": "~5.0.1", - "mime-types": "~2.1.19", - "oauth-sign": "~0.9.0", - "performance-now": "^2.1.0", - "qs": "~6.5.2", - "safe-buffer": "^5.1.2", - "tough-cookie": "~2.4.3", - "tunnel-agent": "^0.6.0", - "uuid": "^3.3.2" - }, - "dependencies": { - "form-data": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", - "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", - "dev": true, - "requires": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.6", - "mime-types": "^2.1.12" - } - }, - "qs": { - "version": "6.5.2", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", - "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==", - "dev": true - } - } - }, - "request-progress": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/request-progress/-/request-progress-3.0.0.tgz", - "integrity": "sha1-TKdUCBx/7GP1BeT6qCWqBs1mnb4=", - "dev": true, - "requires": { - "throttleit": "^1.0.0" - } - }, - "require-directory": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=" - }, - "require-main-filename": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", - "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==" - }, - "resolve": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.6.0.tgz", - "integrity": "sha512-mw7JQNu5ExIkcw4LPih0owX/TZXjD/ZUF/ZQ/pDnkw3ZKhDcZZw5klmBlj6gVMwjQ3Pz5Jgu7F3d0jcDVuEWdw==", - "dev": true, - "requires": { - "path-parse": "^1.0.5" - } - }, - "resolve-url": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/resolve-url/-/resolve-url-0.2.1.tgz", - "integrity": "sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo=", - "dev": true - }, - "ret": { - "version": "0.1.15", - "resolved": "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz", - "integrity": "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==", - "dev": true - }, - "rimraf": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", - "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", - "dev": true, - "requires": { - "glob": "^7.1.3" - } - }, - "safe-buffer": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.0.tgz", - "integrity": "sha512-fZEwUGbVl7kouZs1jCdMLdt95hdIv0ZeHg6L7qPeciMZhZ+/gdesW4wgTARkrFWEpspjEATAzUGPG8N2jJiwbg==", - "dev": true - }, - "safe-regex": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/safe-regex/-/safe-regex-1.1.0.tgz", - "integrity": "sha1-QKNmnzsHfR6UPURinhV91IAjvy4=", - "dev": true, - "requires": { - "ret": "~0.1.10" - } - }, - "safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "dev": true - }, - "set-blocking": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", - "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=" - }, - "set-value": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.1.tgz", - "integrity": "sha512-JxHc1weCN68wRY0fhCoXpyK55m/XPHafOmK4UWD7m2CI14GMcFypt4w/0+NV5f/ZMby2F6S2wwA7fgynh9gWSw==", - "dev": true, - "requires": { - "extend-shallow": "^2.0.1", - "is-extendable": "^0.1.1", - "is-plain-object": "^2.0.3", - "split-string": "^3.0.1" - }, - "dependencies": { - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - } - } - }, - "slash": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-2.0.0.tgz", - "integrity": "sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A==", - "dev": true - }, - "snapdragon": { - "version": "0.8.2", - "resolved": "https://registry.npmjs.org/snapdragon/-/snapdragon-0.8.2.tgz", - "integrity": "sha512-FtyOnWN/wCHTVXOMwvSv26d+ko5vWlIDD6zoUJ7LW8vh+ZBC8QdljveRP+crNrtBwioEUWy/4dMtbBjA4ioNlg==", - "dev": true, - "requires": { - "base": "^0.11.1", - "debug": "^2.2.0", - "define-property": "^0.2.5", - "extend-shallow": "^2.0.1", - "map-cache": "^0.2.2", - "source-map": "^0.5.6", - "source-map-resolve": "^0.5.0", - "use": "^3.1.0" - }, - "dependencies": { - "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "requires": { - "ms": "2.0.0" - } - }, - "define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", - "dev": true, - "requires": { - "is-descriptor": "^0.1.0" - } - }, - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", - "dev": true - }, - "source-map": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", - "dev": true - } - } - }, - "snapdragon-node": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/snapdragon-node/-/snapdragon-node-2.1.1.tgz", - "integrity": "sha512-O27l4xaMYt/RSQ5TR3vpWCAB5Kb/czIcqUFOM/C4fYcLnbZUc1PkjTAMjof2pBWaSTwOUd6qUHcFGVGj7aIwnw==", - "dev": true, - "requires": { - "define-property": "^1.0.0", - "isobject": "^3.0.0", - "snapdragon-util": "^3.0.1" - }, - "dependencies": { - "define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", - "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", - "dev": true, - "requires": { - "is-descriptor": "^1.0.0" - } - }, - "is-accessor-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", - "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", - "dev": true, - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-data-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", - "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", - "dev": true, - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-descriptor": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", - "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", - "dev": true, - "requires": { - "is-accessor-descriptor": "^1.0.0", - "is-data-descriptor": "^1.0.0", - "kind-of": "^6.0.2" - } - } - } - }, - "snapdragon-util": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/snapdragon-util/-/snapdragon-util-3.0.1.tgz", - "integrity": "sha512-mbKkMdQKsjX4BAL4bRYTj21edOf8cN7XHdYUJEe+Zn99hVEYcMvKPct1IqNe7+AZPirn8BCDOQBHQZknqmKlZQ==", - "dev": true, - "requires": { - "kind-of": "^3.2.0" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, - "optional": true - }, - "source-map-resolve": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.5.2.tgz", - "integrity": "sha512-MjqsvNwyz1s0k81Goz/9vRBe9SZdB09Bdw+/zYyO+3CuPk6fouTaxscHkgtE8jKvf01kVfl8riHzERQ/kefaSA==", - "dev": true, - "requires": { - "atob": "^2.1.1", - "decode-uri-component": "^0.2.0", - "resolve-url": "^0.2.1", - "source-map-url": "^0.4.0", - "urix": "^0.1.0" - } - }, - "source-map-url": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/source-map-url/-/source-map-url-0.4.0.tgz", - "integrity": "sha1-PpNdfd1zYxuXZZlW1VEo6HtQhKM=", - "dev": true - }, - "split-string": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/split-string/-/split-string-3.1.0.tgz", - "integrity": "sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw==", - "dev": true, - "requires": { - "extend-shallow": "^3.0.0" - } - }, - "sshpk": { - "version": "1.16.1", - "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.16.1.tgz", - "integrity": "sha512-HXXqVUq7+pcKeLqqZj6mHFUMvXtOJt1uoUx09pFW6011inTMxqI8BA8PM95myrIyyKwdnzjdFjLiE6KBPVtJIg==", - "dev": true, - "requires": { - "asn1": "~0.2.3", - "assert-plus": "^1.0.0", - "bcrypt-pbkdf": "^1.0.0", - "dashdash": "^1.12.0", - "ecc-jsbn": "~0.1.1", - "getpass": "^0.1.1", - "jsbn": "~0.1.0", - "safer-buffer": "^2.0.2", - "tweetnacl": "~0.14.0" - } - }, - "static-extend": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/static-extend/-/static-extend-0.1.2.tgz", - "integrity": "sha1-YICcOcv/VTNyJv1eC1IPNB8ftcY=", - "dev": true, - "requires": { - "define-property": "^0.2.5", - "object-copy": "^0.1.0" - }, - "dependencies": { - "define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", - "dev": true, - "requires": { - "is-descriptor": "^0.1.0" - } - } - } - }, - "stream-meter": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/stream-meter/-/stream-meter-1.0.4.tgz", - "integrity": "sha1-Uq+Vql6nYKJJFxZwTb/5D3Ov3R0=", - "dev": true, - "requires": { - "readable-stream": "^2.1.4" - }, - "dependencies": { - "readable-stream": { - "version": "2.3.6", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", - "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", - "dev": true, - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true - }, - "string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dev": true, - "requires": { - "safe-buffer": "~5.1.0" - } - } - } - }, - "string-width": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.0.tgz", - "integrity": "sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg==", - "requires": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.0" - } - }, - "strip-ansi": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", - "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", - "requires": { - "ansi-regex": "^5.0.0" - } - }, - "supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "requires": { - "has-flag": "^3.0.0" - } - }, - "throttleit": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/throttleit/-/throttleit-1.0.0.tgz", - "integrity": "sha1-nnhYNtr0Z0MUWlmEtiaNgoUorGw=", - "dev": true - }, - "to-object-path": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/to-object-path/-/to-object-path-0.3.0.tgz", - "integrity": "sha1-KXWIt7Dn4KwI4E5nL4XB9JmeF68=", - "dev": true, - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "to-regex": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/to-regex/-/to-regex-3.0.2.tgz", - "integrity": "sha512-FWtleNAtZ/Ki2qtqej2CXTOayOH9bHDQF+Q48VpWyDXjbYxA4Yz8iDB31zXOBUlOHHKidDbqGVrTUvQMPmBGBw==", - "dev": true, - "requires": { - "define-property": "^2.0.2", - "extend-shallow": "^3.0.2", - "regex-not": "^1.0.2", - "safe-regex": "^1.1.0" - } - }, - "to-regex-range": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz", - "integrity": "sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg=", - "dev": true, - "requires": { - "is-number": "^3.0.0", - "repeat-string": "^1.6.1" - } - }, - "tough-cookie": { - "version": "2.4.3", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.4.3.tgz", - "integrity": "sha512-Q5srk/4vDM54WJsJio3XNn6K2sCG+CQ8G5Wz6bZhRZoAe/+TxjWB/GlFAnYEbkYVlON9FMk/fE3h2RLpPXo4lQ==", - "dev": true, - "requires": { - "psl": "^1.1.24", - "punycode": "^1.4.1" - }, - "dependencies": { - "punycode": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", - "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=", - "dev": true - } - } - }, - "tunnel-agent": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", - "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", - "dev": true, - "requires": { - "safe-buffer": "^5.0.1" - } - }, - "tweetnacl": { - "version": "0.14.5", - "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", - "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=", - "dev": true - }, - "type-check": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", - "integrity": "sha1-WITKtRLPHTVeP7eE8wgEsrUg23I=", - "dev": true, - "requires": { - "prelude-ls": "~1.1.2" - } - }, - "uid2": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/uid2/-/uid2-0.0.3.tgz", - "integrity": "sha1-SDEm4Rd03y9xuLY53NeZw3YWK4I=", - "dev": true - }, - "union-value": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.1.tgz", - "integrity": "sha512-tJfXmxMeWYnczCVs7XAEvIV7ieppALdyepWMkHkwciRpZraG/xwT+s2JN8+pr1+8jCRf80FFzvr+MpQeeoF4Xg==", - "dev": true, - "requires": { - "arr-union": "^3.1.0", - "get-value": "^2.0.6", - "is-extendable": "^0.1.1", - "set-value": "^2.0.1" - } - }, - "unique-temp-dir": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unique-temp-dir/-/unique-temp-dir-1.0.0.tgz", - "integrity": "sha1-bc6VsmgcoAPuv7MEpBX5y6vMU4U=", - "dev": true, - "requires": { - "mkdirp": "^0.5.1", - "os-tmpdir": "^1.0.1", - "uid2": "0.0.3" - } - }, - "universalify": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", - "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", - "dev": true - }, - "unset-value": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unset-value/-/unset-value-1.0.0.tgz", - "integrity": "sha1-g3aHP30jNRef+x5vw6jtDfyKtVk=", - "dev": true, - "requires": { - "has-value": "^0.3.1", - "isobject": "^3.0.0" - }, - "dependencies": { - "has-value": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/has-value/-/has-value-0.3.1.tgz", - "integrity": "sha1-ex9YutpiyoJ+wKIHgCVlSEWZXh8=", - "dev": true, - "requires": { - "get-value": "^2.0.3", - "has-values": "^0.1.4", - "isobject": "^2.0.0" - }, - "dependencies": { - "isobject": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz", - "integrity": "sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk=", - "dev": true, - "requires": { - "isarray": "1.0.0" - } - } - } - }, - "has-values": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/has-values/-/has-values-0.1.4.tgz", - "integrity": "sha1-bWHeldkd/Km5oCCJrThL/49it3E=", - "dev": true - } - } - }, - "uri-js": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz", - "integrity": "sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==", - "dev": true, - "requires": { - "punycode": "^2.1.0" - } - }, - "urix": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/urix/-/urix-0.1.0.tgz", - "integrity": "sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI=", - "dev": true - }, - "use": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/use/-/use-3.1.1.tgz", - "integrity": "sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==", - "dev": true - }, - "util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", - "dev": true - }, - "uuid": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.3.tgz", - "integrity": "sha512-pW0No1RGHgzlpHJO1nsVrHKpOEIxkGg1xB+v0ZmdNH5OAeAwzAVrCnI2/6Mtx+Uys6iaylxa+D3g4j63IKKjSQ==", - "dev": true - }, - "verror": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", - "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", - "dev": true, - "requires": { - "assert-plus": "^1.0.0", - "core-util-is": "1.0.2", - "extsprintf": "^1.2.0" - } - }, - "which-module": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz", - "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=" - }, - "wordwrap": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", - "integrity": "sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus=", - "dev": true - }, - "wrap-ansi": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", - "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", - "requires": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "dependencies": { - "ansi-styles": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", - "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==", - "requires": { - "@types/color-name": "^1.1.1", - "color-convert": "^2.0.1" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" - } - } - }, - "wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", - "dev": true - }, - "y18n": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.0.tgz", - "integrity": "sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w==" - }, - "yargs": { - "version": "15.1.0", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.1.0.tgz", - "integrity": "sha512-T39FNN1b6hCW4SOIk1XyTOWxtXdcen0t+XYrysQmChzSipvhBO8Bj0nK1ozAasdk24dNWuMZvr4k24nz+8HHLg==", - "requires": { - "cliui": "^6.0.0", - "decamelize": "^1.2.0", - "find-up": "^4.1.0", - "get-caller-file": "^2.0.1", - "require-directory": "^2.1.1", - "require-main-filename": "^2.0.0", - "set-blocking": "^2.0.0", - "string-width": "^4.2.0", - "which-module": "^2.0.0", - "y18n": "^4.0.0", - "yargs-parser": "^16.1.0" - } - }, - "yargs-parser": { - "version": "16.1.0", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-16.1.0.tgz", - "integrity": "sha512-H/V41UNZQPkUMIT5h5hiwg4QKIY1RPvoBV4XcjUbRM8Bk2oKqqyZ0DIEbTFZB0XjbtSPG8SAa/0DxCQmiRgzKg==", - "requires": { - "camelcase": "^5.0.0", - "decamelize": "^1.2.0" - } - } - } -} diff --git a/package.json b/package.json deleted file mode 100644 index 5196ee7..0000000 --- a/package.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "name": "grinpp", - "productName": "grinpp", - "version": "1.0.0", - "description": "CLI for Grin++", - "main": "cli.js", - "scripts": { - "test": "echo \"Error: no test specified\" && exit 1", - "release": "node build/release.js", - "start": "node cli.js" - }, - "author": "David Burkett", - "license": "MIT", - "bin": { - "grinpp": "./cli.js" - }, - "dependencies": { - "axios": "^0.19.2", - "cli-table3": "^0.5.1", - "commander": "^4.1.1", - "yargs": "^15.1.0" - }, - "devDependencies": { - "fs-jetpack": "^2.2.2", - "pkg": "^4.4.0" - } -} From 0a44e414d5e3e8d19382ada6996299d2c36380fa Mon Sep 17 00:00:00 2001 From: David Tavarez <337107+davidtavarez@users.noreply.github.com> Date: Tue, 18 Apr 2023 14:24:20 +0200 Subject: [PATCH 04/17] updating requirements --- requirements.txt | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/requirements.txt b/requirements.txt index 77c76d4..0d2531b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,17 +6,19 @@ click==8.1.3 coincurve==18.0.0 colorama==0.4.6 commonmark==0.9.1 -cryptography==40.0.1 +cryptography==40.0.2 idna==3.4 -psutil==5.9.4 +markdown-it-py==2.2.0 +mdurl==0.1.2 +psutil==5.9.5 pycparser==2.21 Pygments==2.15.0 pynostr==0.6.2 requests==2.28.2 -rich==12.6.0 +rich==13.3.4 shellingham==1.5.0.post1 timeago==1.0.16 tlv8==0.10.0 -tornado==6.2 +tornado==6.3 typer==0.7.0 urllib3==1.26.15 From 058b7686dead208b1d92c6ba69033d0cf6e70721 Mon Sep 17 00:00:00 2001 From: David Tavarez <337107+davidtavarez@users.noreply.github.com> Date: Tue, 18 Apr 2023 21:46:15 +0200 Subject: [PATCH 05/17] ignoring vscode --- .gitignore | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 588d338..b9ee75d 100644 --- a/.gitignore +++ b/.gitignore @@ -46,4 +46,5 @@ dist/** *.xlf package.nls.*.json l10n/ -Thumbs.db \ No newline at end of file +Thumbs.db +.vscode \ No newline at end of file From 47c2ebb48a170ef0a1eba9930a3cda667fef80ec Mon Sep 17 00:00:00 2001 From: David Tavarez <337107+davidtavarez@users.noreply.github.com> Date: Wed, 19 Apr 2023 21:08:31 +0200 Subject: [PATCH 06/17] ignore more files --- .gitignore | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 588d338..6a36299 100644 --- a/.gitignore +++ b/.gitignore @@ -46,4 +46,5 @@ dist/** *.xlf package.nls.*.json l10n/ -Thumbs.db \ No newline at end of file +Thumbs.db +**/.vscode**/ From e38dbc0dde50eb75a90b6cec2ef80d81853e4ca6 Mon Sep 17 00:00:00 2001 From: David Tavarez <337107+davidtavarez@users.noreply.github.com> Date: Sat, 13 May 2023 22:39:18 +0200 Subject: [PATCH 07/17] refactoring modules: nostr, utils, wallet. --- src/apps/misc/app.py | 30 ++-- src/apps/node/app.py | 6 +- src/apps/transaction/app.py | 18 +-- src/apps/transaction/nostr/transport.py | 39 +++-- src/apps/wallet/app.py | 185 +++++++++-------------- src/modules/api/__init__.py | 4 +- src/modules/api/node/__init__.py | 2 +- src/modules/api/owner/__init__.py | 4 +- src/modules/nostr/__init__.py | 132 +++++++++++++++++ src/modules/utils/coingecko.py | 186 +++++++++++++++--------- src/modules/utils/grinchck.py | 37 +++++ src/modules/utils/helpers.py | 181 ----------------------- src/modules/utils/kdf.py | 81 +++++++---- src/modules/utils/nostr.py | 7 - src/modules/utils/processes.py | 93 ++++++++++++ src/modules/wallet/__init__.py | 0 src/modules/wallet/session.py | 82 +++++++++++ 17 files changed, 632 insertions(+), 455 deletions(-) create mode 100644 src/modules/nostr/__init__.py create mode 100644 src/modules/utils/grinchck.py delete mode 100644 src/modules/utils/helpers.py delete mode 100644 src/modules/utils/nostr.py create mode 100644 src/modules/utils/processes.py create mode 100644 src/modules/wallet/__init__.py create mode 100644 src/modules/wallet/session.py diff --git a/src/apps/misc/app.py b/src/apps/misc/app.py index 62cc2fb..2401270 100644 --- a/src/apps/misc/app.py +++ b/src/apps/misc/app.py @@ -8,12 +8,8 @@ from rich.console import Console from rich.table import Table -from modules.utils.coingecko import get_grin_price, is_currency_supported -from modules.utils.helpers import ( - check_wallet_reachability, - find_processes_by_name, - kill_proc_tree, -) +from modules.utils import grinchck, processes +from modules.utils.coingecko import get_grin_price app = typer.Typer() @@ -30,12 +26,12 @@ def simple_grin_price( """ Get Grin price from CoinGecko. """ - if not is_currency_supported(currency): - error_console.print(f"Currency not supported.") - typer.Abort() - - moon = get_grin_price(currency) - table: Table = Table(box=box.HORIZONTALS, show_header=False) + try: + moon = get_grin_price(currency) + except Exception as err: + error_console.print(f"Error: {err}") + raise typer.Abort() + table = Table(box=box.HORIZONTALS, show_header=False) direction: str = "" style: str = "" if moon["24h_change"] >= 0: @@ -68,7 +64,9 @@ def grinchck_test( reachable: bool = False try: with console.status("Checking wallet reachability..."): - reachable = check_wallet_reachability(address) + reachable = grinchck.connect( + slatepack_address=address, api_url="http://192.227.214.130/" + ) except Exception as err: error_console.print(f"Error: {err} ¯\_(ツ)_/¯") raise typer.Abort() @@ -101,7 +99,7 @@ def tor_control( tor_process_name: str = "tor" if psutil.WINDOWS: tor_process_name = "tor.exe" - tor_processes: list[psutil.Process] = find_processes_by_name(tor_process_name) + tor_processes: list[psutil.Process] = processes.find(tor_process_name) if len(tor_processes) > 0: running = True @@ -115,8 +113,8 @@ def tor_control( if stop: with console.status("Stopping tor..."): for process in tor_processes: - kill_proc_tree(process.pid) - if len(find_processes_by_name(tor_process_name)) > 0: + processes.kill(process.pid) + if len(processes.find(tor_process_name)) > 0: error_console.print("Tor is still running") else: console.print("Tor [bold]stopped[/bold] successfully ✔") diff --git a/src/apps/node/app.py b/src/apps/node/app.py index f9e0188..72aaeba 100644 --- a/src/apps/node/app.py +++ b/src/apps/node/app.py @@ -19,7 +19,7 @@ get_sync_state, shutdown_node, ) -from modules.utils.helpers import find_processes_by_name +from modules.utils import processes app = typer.Typer() @@ -36,7 +36,7 @@ def stop_node(): node_process_name: str = "GrinNode" if psutil.WINDOWS: node_process_name = "GrinNode.exe" - if len(find_processes_by_name(node_process_name)) > 0: + if len(processes.find(node_process_name)) > 0: running = True if not running: @@ -54,7 +54,7 @@ def stop_node(): break except: pass - if len(find_processes_by_name(node_process_name)) == 0: + if len(processes.find(node_process_name)) == 0: console.print("Node and wallet Tor Listeners successfully [bold]stopped[/bold]") else: error_console.print("Unable to stop Node.") diff --git a/src/apps/transaction/app.py b/src/apps/transaction/app.py index fb5d3f7..53a477c 100644 --- a/src/apps/transaction/app.py +++ b/src/apps/transaction/app.py @@ -23,7 +23,7 @@ get_wallet_transactions, send_coins, ) -from modules.utils.helpers import get_wallet_session +from modules.wallet import session app = typer.Typer() @@ -57,7 +57,7 @@ def list_wallet_transactions( """ try: - session_token = get_wallet_session(wallet=wallet, password=password) + session_token = session.token(wallet=wallet, password=password) transactions = get_wallet_transactions(session_token, status.value) except Exception as err: @@ -166,7 +166,7 @@ def send_grin( fee: float try: - session_token = get_wallet_session(wallet=wallet, password=password) + session_token = session.token(wallet=wallet, password=password) fee = float( estimate_transaction_fee(session_token=session_token, amount=amount)["fee"] ) / pow(10, 9) @@ -198,7 +198,7 @@ def send_grin( sent: bool = False slatepack: str = "" try: - session_token = get_wallet_session(wallet=wallet, password=password) + session_token = session.token(wallet=wallet, password=password) with console.status("Building transaction..."): transaction = send_coins( session_token=session_token, amount=amount, address=address @@ -250,7 +250,7 @@ def transaction_cancelation( """ try: - session_token = get_wallet_session(wallet=wallet, password=password) + session_token = session.token(wallet=wallet, password=password) if cancel_transaction(session_token=session_token, id=id): console.print("Transaction [bold]canceled[/bold] successfully ✔") @@ -280,7 +280,7 @@ def transaction_finalization( """ try: - token = get_wallet_session(wallet=wallet, password=password) + token = session.token(wallet=wallet, password=password) if not psutil.WINDOWS: import readline slatepack: str = console.input("Please, insert the Slatepack down below:\n") @@ -311,7 +311,7 @@ def transaction_receive( """ try: - session_token = get_wallet_session(wallet=wallet, password=password) + session_token = session.token(wallet=wallet, password=password) if not psutil.WINDOWS: import readline slatepack = console.input("Paste the Slatepack down below:\n") @@ -360,7 +360,7 @@ def transaction_reposting( """ try: - session_token = get_wallet_session(wallet=wallet, password=password) + session_token = session.token(wallet=wallet, password=password) if broadcast_transaction(session_token=session_token, id=id): console.print("Transaction [bold]posted[/bold] successfully ✔") else: @@ -395,7 +395,7 @@ def transaction_information( details: dict = {} try: - session_token = get_wallet_session(wallet=wallet, password=password) + session_token = session.token(wallet=wallet, password=password) details = get_transaction_details(session_token=session_token, id=id) except Exception as err: error_console.print(f"Error: {err} ¯\_(ツ)_/¯") diff --git a/src/apps/transaction/nostr/transport.py b/src/apps/transaction/nostr/transport.py index e23f449..a57f8d8 100644 --- a/src/apps/transaction/nostr/transport.py +++ b/src/apps/transaction/nostr/transport.py @@ -7,7 +7,6 @@ from datetime import datetime, timedelta from typing import Optional -import psutil import tornado.ioloop import typer from pynostr.base_relay import RelayPolicy @@ -26,6 +25,7 @@ from rich.table import Table from tornado import gen +from modules import nostr from modules.api import TransactionsFilterOptions from modules.api.owner.rpc import ( add_initial_signature, @@ -38,15 +38,7 @@ get_wallet_transactions, send_coins, ) -from modules.utils.coingecko import get_grin_price, is_currency_supported -from modules.utils.helpers import ( - check_wallet_reachability, - find_processes_by_name, - get_nostr_private_key, - get_wallet_nostr_public_key, - get_wallet_session, - kill_proc_tree, -) +from modules.wallet import session plugin = typer.Typer() @@ -96,7 +88,7 @@ def send_tx_to_nostr( ) try: - session_token = get_wallet_session(wallet=wallet, password=password) + session_token = session.token(wallet=wallet, password=password) transaction = get_transaction_details(session_token=session_token, id=id) except Exception as err: error_console.print(f"Error: {err} ¯\_(ツ)_/¯") @@ -104,9 +96,13 @@ def send_tx_to_nostr( console.print(f"Transaction with [bold]id={id}[/bold] found ✔") - slatepack_address = get_wallet_slatepack_address(session_token) - nostr_private_key, raw_key = get_nostr_private_key( - slatepack_address=slatepack_address, password=password + address = get_wallet_slatepack_address(session_token) + + wallet_raw_secret = nostr.generate_raw_secret( + wallet=wallet, address=address, password=password + ) + nostr_private_key = nostr.retrieve_private_key( + wallet=wallet, raw_secret=wallet_raw_secret ) console.print(f"Sender [bold]Nostr key[/bold] loaded ✔") @@ -217,10 +213,13 @@ def grab_txs_from_nostr( nostr_private_key: PrivateKey = None try: - session_token = get_wallet_session(wallet=wallet, password=password) - slatepack_address = get_wallet_slatepack_address(session_token) - nostr_private_key, raw_key = get_nostr_private_key( - slatepack_address=slatepack_address, password=password + session_token = session.token(wallet=wallet, password=password) + address = get_wallet_slatepack_address(session_token) + wallet_raw_secret = nostr.generate_raw_secret( + wallet=wallet, address=address, password=password + ) + nostr_private_key = nostr.retrieve_private_key( + wallet=wallet, raw_secret=wallet_raw_secret ) if not nostr_private_key: @@ -308,7 +307,7 @@ def check_reply(message_json): url=relay_url, close_on_eose=True, message_callback=check_reply, - policy=RelayPolicy(should_read=False, should_write=False) + policy=RelayPolicy(should_read=False, should_write=False), ) subscription_id = uuid.uuid4().hex @@ -361,7 +360,7 @@ def check_reply(message_json): relay_manager.publish_event(reply) with console.status("Sending response..."): relay_manager.run_sync() - + relay_manager.close_subscription_on_all_relays(subscription_id) relay_manager.close_all_relay_connections() relay_manager.remove_closed_relays() diff --git a/src/apps/wallet/app.py b/src/apps/wallet/app.py index 3ddac6c..6063a50 100644 --- a/src/apps/wallet/app.py +++ b/src/apps/wallet/app.py @@ -1,6 +1,7 @@ #!/usr/bin/env python3 -import json +import os +import pathlib import typer from rich import box @@ -8,6 +9,7 @@ from rich.prompt import Confirm from rich.table import Table +from modules import nostr from modules.api.owner.rpc import ( close_wallet_by_name, create_wallet, @@ -19,12 +21,8 @@ open_wallet_by_name, restore_wallet, ) -from modules.utils.helpers import ( - get_wallet_nostr_public_key, - get_wallet_session, - save_wallet_nostr_key, - save_wallet_session, -) +from modules.utils import grinchck +from modules.wallet import session app = typer.Typer() console = Console(width=125, style="grey93") @@ -48,33 +46,16 @@ def wallet_open( """ try: - result = open_wallet_by_name(wallet, password) + response = open_wallet_by_name(wallet, password) console.print(f"Wallet [bold]{wallet}[/bold] opened ✔") - console.print(f"Tor Listener for wallet [bold]{wallet}[/bold] running ✔") - session_saved = save_wallet_session( - wallet=wallet, - session_token=result["session_token"], - password=password, - ) - if not session_saved: - error_console.print(f"Error: Unable to save session information ✗") - else: - console.print(f"Session information created ✔") - nostr_key_saved = save_wallet_nostr_key( + if not session.store( wallet=wallet, - slatepack_address=result["slatepack_address"], + token=response["session_token"], password=password, - ) - - if not nostr_key_saved: - error_console.print(f"Error: Unable to generate Nostr key ✗") - else: - console.print(f"Nostr root key loaded ✔") - - console.print( - f"Slatepack Address: [green3]{result['slatepack_address']}[/green3] " - ) + ): + close_wallet_by_name(response["session_token"]) + raise Exception("Unable to save session information ✗") except Exception as err: error_console.print(f"Error: {err} ¯\_(ツ)_/¯") raise typer.Abort() @@ -99,12 +80,10 @@ def wallet_close( """ try: - session_token: str = get_wallet_session(wallet=wallet, password=password) + session_token: str = session.token(wallet=wallet, password=password) with console.status("closing walet..."): close_wallet_by_name(session_token) - console.print( - f"Wallet [bold]{wallet}[/bold] closed, and Tor Listener [bold]stopped[/bold] successfully ✔" - ) + console.print(f"Wallet [bold]{wallet}[/bold] closed successfully ✔") except Exception as err: error_console.print(f"Error: {err} ¯\_(ツ)_/¯") raise typer.Abort() @@ -131,34 +110,18 @@ def wallet_creation( """ try: - wallet = create_wallet(wallet=name, password=password, words=words) - console.print(f"Wallet [bold]{wallet}[/bold] created ✔") + response = create_wallet(wallet=name, password=password, words=words) + console.print(f"Wallet [bold]{name}[/bold] created ✔") + console.print(f"Wallet seed phrase: [bold]{response['wallet_seed']}[/bold] ✇") - session_saved = save_wallet_session( + if not session.store( wallet=name, - session_token=wallet["session_token"], + token=response["session_token"], password=password, - ) - console.print(f"Tor Listener for wallet [bold]{wallet}[/bold] running ✔") - - if not session_saved: - raise Exception("An error occurred while saving session information") - - nostr_key_saved = save_wallet_nostr_key( - wallet=name, - slatepack_address=wallet["slatepack_address"], - password=password, - ) + ): + close_wallet_by_name(response["session_token"]) + raise Exception("Unable to save session information ✗") - if not nostr_key_saved: - error_console.print(f"Error: {err} ✗") - else: - console.print(f"Nostr root key loaded ✔") - - console.print(f"Wallet seed phrase: [bold]{wallet['wallet_seed']}[/bold] ✇") - console.print( - f"Slatepack Address: [green3]{wallet['slatepack_address']}[/green3] " - ) except Exception as err: error_console.print(f"Error: {err} ¯\_(ツ)_/¯") raise typer.Abort() @@ -185,32 +148,17 @@ def wallet_restore( """ try: - wallet = restore_wallet(wallet=name, password=password, seed=seed) - console.print(f"Wallet [bold]{wallet}[/bold] recovered ✔") - session_saved = save_wallet_session( - wallet=name, - session_token=wallet["session_token"], - password=password, - ) - if not session_saved: - error_console.print(f"Error: {err} ✗") - else: - console.print(f"Session information created ✔") + response = restore_wallet(wallet=name, password=password, seed=seed) + console.print(f"Wallet [bold]{name}[/bold] recovered ✔") - nostr_key_saved = save_wallet_nostr_key( + if not session.store( wallet=name, - slatepack_address=wallet["slatepack_address"], + token=response["session_token"], password=password, - ) + ): + raise Exception("Unable to save session information ✗") - if not nostr_key_saved: - error_console.print(f"Error: {err} ✗") - else: - console.print(f"Nostr root key loaded ✔") - - console.print( - f"Slatepack Address: [green3]{wallet['slatepack_address']}[/green3] " - ) + console.print(f"Session information created ✔") except Exception as err: error_console.print(f"Error: {err} ¯\_(ツ)_/¯") raise typer.Abort() @@ -261,7 +209,7 @@ def wallet_backup( try: seed = get_wallet_seed(wallet=wallet, password=password) - console.print(f"wallet seed: [bold white]{seed}[/bold white]") + console.print(f"Wallet seed phrase: [bold white]{seed}[/bold white]") except Exception as err: error_console.print(f"Error: {err} ¯\_(ツ)_/¯") @@ -275,30 +223,28 @@ def list_wallets(): """ List the created Wallets. """ - data = json.loads("{}") try: data = get_list_of_wallets() + if "wallets" in data and data["wallets"]: + table = Table(title="Wallets", box=box.HORIZONTALS, expand=True) + table.add_column("") + table.add_column("name") + i = 1 + for wallet in data["wallets"]: + table.add_row( + f"{i}", + f"[bold yellow]{wallet}", + ) + i += 1 + + console.print(table) + else: + console.print("No wallet found") except Exception as err: error_console.print(f"Error: {err} ¯\_(ツ)_/¯") raise typer.Abort() - if data["wallets"]: - table = Table(title="Wallets", box=box.HORIZONTALS, expand=True) - table.add_column("") - table.add_column("name") - i = 1 - for wallet in data["wallets"]: - table.add_row( - f"{i}", - f"[bold yellow]{wallet}", - ) - i += 1 - - console.print(table) - else: - console.print("No wallet found") - raise typer.Exit() @@ -319,7 +265,7 @@ def wallet_balance( """ try: - session_token = get_wallet_session(wallet=wallet, password=password) + session_token = session.token(wallet=wallet, password=password) balance = get_wallet_balance(session_token) @@ -369,35 +315,42 @@ def wallet_address( hide_input=True, help="Wallet password.", ), - nostr: bool = typer.Option( - False, help="Return the nostr Public Key associated with the current address" + test: bool = typer.Option( + False, help="Check if wallet address is reachable via Tor" ), - check: bool = typer.Option(False, help="Check if address are reachable via Tor"), + npub: bool = typer.Option(False, help="Print the Nostr Public Key"), ): """ Get the Slatepack Address of a running Wallet. """ try: - session_token = get_wallet_session(wallet=wallet, password=password) - - slatepack_address = get_wallet_slatepack_address(session_token) + session_token = session.token(wallet=wallet, password=password) + address = get_wallet_slatepack_address(session_token) + console.print(f"Slatepack Address: [green3]{address}[/green3]") - console.print(f"Slatepack Address: [green3]{slatepack_address}[/green3] ") - - if nostr: - nostr_public_key = get_wallet_nostr_public_key( - wallet=wallet, - slatepack_address=slatepack_address, - password=password, + if test: + reachable = grinchck.connect( + slatepack_address=address, api_url="http://192.227.214.130/" ) - if nostr_public_key: + if reachable: console.print( - f"Nostr' Public Key: [dark_orange3]{nostr_public_key}[/dark_orange3] (∩`-´)⊃━☆゚.*・。゚" + f"Address [green3]{address}[/green3] is reachable via the Tor Network <(^_^)>" ) else: - error_console.print("No nostr key was found for this address") - + error_console.print( + f"Addres [dark_orange]{address}[/dark_orange] is not reachable the Tor Network \_(-_-)_/" + ) + if npub: + wallet_raw_secret = nostr.generate_raw_secret( + wallet=wallet, address=address, password=password + ) + nostr_private_key = nostr.retrieve_private_key( + wallet=wallet, raw_secret=wallet_raw_secret + ) + console.print( + f"Nostr' Public Key: [dark_orange3]{nostr_private_key.public_key}[/dark_orange3] (∩`-´)⊃━☆゚.*・。゚" + ) except Exception as err: error_console.print(f"Error: {err} ¯\_(ツ)_/¯") raise typer.Abort() diff --git a/src/modules/api/__init__.py b/src/modules/api/__init__.py index d57bc25..d9258ce 100644 --- a/src/modules/api/__init__.py +++ b/src/modules/api/__init__.py @@ -2,7 +2,7 @@ import psutil -from modules.utils.helpers import find_processes_by_name +from modules.utils.processes import find class TransactionsFilterOptions(str, Enum): @@ -25,4 +25,4 @@ def _is_node_running() -> bool: if psutil.WINDOWS: node_process_name = "GrinNode.exe" - return len(find_processes_by_name(node_process_name)) > 0 + return len(find(node_process_name)) > 0 diff --git a/src/modules/api/node/__init__.py b/src/modules/api/node/__init__.py index e21791b..484b4d6 100644 --- a/src/modules/api/node/__init__.py +++ b/src/modules/api/node/__init__.py @@ -14,8 +14,8 @@ def _node_rest_call(method: str, params: str = "") -> dict: def _parse_status_response(data: dict) -> tuple[str, float]: - percentage: float = 0 message: str = "" + percentage: float = 0.0 if data["sync_status"] == "NOT_CONNECTED": message = "Waiting for Peers" diff --git a/src/modules/api/owner/__init__.py b/src/modules/api/owner/__init__.py index 74d0281..3adf2cb 100644 --- a/src/modules/api/owner/__init__.py +++ b/src/modules/api/owner/__init__.py @@ -48,5 +48,5 @@ def _filter_transactions_by_status(transactions: list, status: str): return filter(lambda t: str(t["type"]).lower() == "received", transactions) elif status == "canceled": return filter(lambda t: "canceled" in str(t["type"]).lower(), transactions) - else: - return transactions + + return transactions diff --git a/src/modules/nostr/__init__.py b/src/modules/nostr/__init__.py new file mode 100644 index 0000000..da12fda --- /dev/null +++ b/src/modules/nostr/__init__.py @@ -0,0 +1,132 @@ +import os +from pathlib import Path +from typing import Optional + +from cryptography.fernet import Fernet +from pynostr.key import PrivateKey + +from modules.utils.kdf import derive_key, encode_key + + +def _derive_passphrase(password: str): + """ + Derive a key using a passphrase. The method will try to get an already stored `salt` or generate a new one if necessary. + + Parameters + ---------- + password : str + Password of the wallet. + + Returns + ------- + bytes + Raw bytes of the derived key. + """ + return derive_key(passphrase=password.encode()) + + +def _generate_private_key(nsec: Optional[str] = None) -> PrivateKey: + """ + Generates a Nostr Private Key. It will use an nsec if passed as a parameter. + + Parameters + ---------- + nsec : str + An already generate nsec. It will create a completely new private if nsec is + None. + + Returns + ------- + PrivateKey + A Nostr Private Key. + """ + if nsec: + return PrivateKey.from_nsec(nsec=nsec) + return PrivateKey() + + +def _store_private_key(raw_secret: bytes, key_path: Path) -> PrivateKey: + """ + Save a Nostr Privat Key inside `key_path`. The content is encrypted using + `raw_secret`. + + Parameters + ---------- + raw_secret: bytes + Secret key used to decrypt/encrypt the line with the nsec. + key_path : Path + Walle name. This is uses to name the file like: `[wallet].nostr`. + + Returns + ------ + PrivateKey + A Nostr Private Key. + """ + private_key = _generate_private_key() + with open(key_path, "wb+") as file: + file.write( + Fernet(encode_key(raw_secret)).encrypt(private_key.bech32().encode()) + ) + if key_path.exists(): + return private_key + raise Exception("Unable to store generated Nostr Private Key") + + +def generate_raw_secret(wallet: str, address: str, password: str) -> bytes: + """ + Get a raw secret based on wallet address to encrypt/decrypt the file containing the + nsec. + + Parameters + ---------- + wallet : str + Name of the wallet. + address : str + Slatepack Address of the wallet. + password : str + Password of the wallet. + + Returns + ------- + bytes + Raw bytes of the derived key. + """ + """ + Returns a tuple: Nostr Private Key and raw bytes + """ + return _derive_passphrase(f"{wallet}{address}{password}") + + +def retrieve_private_key(wallet: str, raw_secret: bytes) -> PrivateKey: + """ + Returns the corresponding Nostr Private Key of a opened wallet. This will create a + new Private Key in case the contents of the file containing the nsec cannot be + decrypted. + + Parameters + ---------- + wallet : str + Walle name. This is uses to name the file like: `[wallet].nostr`. + raw_secret: bytes + Secret key used to decrypt/encrypt the line with the nsec. + + Returns + ------ + PrivateKey + A Nostr Private Key. + """ + + key_path: Path = Path.home().joinpath( + os.getenv("GRINPP_CLI_DATA_PATH", ".grinplusplus"), f"{wallet}.nostr" + ) + if key_path.exists(): + file = open(key_path, "rb") + nostr_key = file.read() + file.close() + try: + nsec = Fernet(encode_key(raw_secret)).decrypt(nostr_key).decode() + return _generate_private_key(nsec=nsec) + except: + return _store_private_key(raw_secret, key_path) + + return _store_private_key(raw_secret, key_path) diff --git a/src/modules/utils/coingecko.py b/src/modules/utils/coingecko.py index 6b700f7..6f37502 100644 --- a/src/modules/utils/coingecko.py +++ b/src/modules/utils/coingecko.py @@ -1,81 +1,123 @@ -#!/usr/bin/env python3 - +""" +This modules defines some useful tools to use with the CoinGecko API. +""" import requests +SUPPORTED_CURRENCIES = [ + "btc", + "eth", + "ltc", + "bch", + "bnb", + "eos", + "xrp", + "xlm", + "link", + "dot", + "yfi", + "usd", + "aed", + "ars", + "aud", + "bdt", + "bhd", + "bmd", + "brl", + "cad", + "chf", + "clp", + "cny", + "czk", + "dkk", + "eur", + "gbp", + "hkd", + "huf", + "idr", + "ils", + "inr", + "jpy", + "krw", + "kwd", + "lkr", + "mmk", + "mxn", + "myr", + "ngn", + "nok", + "nzd", + "php", + "pkr", + "pln", + "rub", + "sar", + "sek", + "sgd", + "thb", + "try", + "twd", + "uah", + "vef", + "vnd", + "zar", + "xdr", + "xag", + "xau", + "bits", + "sats", +] +"""List of the supported currencies by CoinGecko API.""" + + +def __is_currency_supported(currency: str) -> bool: + """ + Return whether or nothe the currency is supported by CoinGecko API -def is_currency_supported(currency: str) -> bool: - return currency in [ - "btc", - "eth", - "ltc", - "bch", - "bnb", - "eos", - "xrp", - "xlm", - "link", - "dot", - "yfi", - "usd", - "aed", - "ars", - "aud", - "bdt", - "bhd", - "bmd", - "brl", - "cad", - "chf", - "clp", - "cny", - "czk", - "dkk", - "eur", - "gbp", - "hkd", - "huf", - "idr", - "ils", - "inr", - "jpy", - "krw", - "kwd", - "lkr", - "mmk", - "mxn", - "myr", - "ngn", - "nok", - "nzd", - "php", - "pkr", - "pln", - "rub", - "sar", - "sek", - "sgd", - "thb", - "try", - "twd", - "uah", - "vef", - "vnd", - "zar", - "xdr", - "xag", - "xau", - "bits", - "sats", - ] + Parameters + ---------- + currency : str + The VS currency, for example eur, usd, btc or eth. + + Returns + ------- + bool + a boolean value containing True if the currency is supported or False if it not supported. + """ + + return currency in SUPPORTED_CURRENCIES def get_grin_price(currency: str) -> dict: - results: dict = requests.get( + """ + Get the price of Grin using the CoinGecko API + + Parameters + ---------- + currency : str + The VS currency, for example eur, usd, btc or eth. + + Raises + ------ + Exception + If the currency uses is not supported. + + Returns + ------ + dict + A dictionary with: price, market cap, 24h change and 24h volume. + """ + + if not __is_currency_supported(currency=currency): + raise Exception(f"{currency} is not a supported currency by CoinGecko API") + + result: dict = requests.get( url=f"https://api.coingecko.com/api/v3/simple/price?ids=grin&vs_currencies={currency}&include_market_cap=true&include_24hr_vol=true&include_24hr_change=true&include_last_updated_at=true&precision=true" ).json() - results["price"] = results["grin"][currency] - results["market_cap"] = results["grin"][currency + "_market_cap"] - results["24h_change"] = results["grin"][currency + "_24h_change"] - results["24h_vol"] = results["grin"][currency + "_24h_vol"] - return results + + return { + "price": result["grin"][currency], + "market_cap": result["grin"][currency + "_market_cap"], + "24h_change": result["grin"][currency + "_24h_change"], + "24h_vol": result["grin"][currency + "_24h_vol"], + } diff --git a/src/modules/utils/grinchck.py b/src/modules/utils/grinchck.py new file mode 100644 index 0000000..8b044b0 --- /dev/null +++ b/src/modules/utils/grinchck.py @@ -0,0 +1,37 @@ +""" +Mimblewimble transactions are interactive, meaning both parties need some kind of communication to interact with each other and exchange the necessary data to create a transaction. Slatepack addresses are also used to derive a Tor address. By default, the sender's wallet will try to communicate with the receiver's wallet via Tor. However, if the Tor connection between the wallets is not successful for whatever reason, grin defaults to manually exchanging slate text messages, also called slatepacks. manually. + +This module uses the Grin Address Checker API to check whether or not an address is accessible through Tor. +""" +from requests import Response, post +from requests.exceptions import ConnectionError + + +def connect(slatepack_address: str, api_url: str) -> bool: + """ + Attempts to connect to a wallet through Tor. This will return True if the wallet is reached. + + Parameters + ---------- + slatepack_address : str + Slatepack address if the wallet. + api_url : str + URL of grinchck API. + + Raises + ------ + ConnectionError + If the API is not available. + + Returns + ------ + bool + True when the wallet is reachable, otherwise False. + """ + try: + data: dict = {"wallet": slatepack_address} + result: Response = post(url=api_url, data=data) + return result.status_code == 200 + except ConnectionError: + error = "Unable to connect to the GrinChck API" + raise Exception(error) diff --git a/src/modules/utils/helpers.py b/src/modules/utils/helpers.py deleted file mode 100644 index 9f65d47..0000000 --- a/src/modules/utils/helpers.py +++ /dev/null @@ -1,181 +0,0 @@ -#!/usr/bin/env python3 - -import os -import signal -from pathlib import Path - -import psutil -import requests -from cryptography.fernet import Fernet -from pynostr.key import PrivateKey - -from modules.utils.kdf import derive_key, encode_derived_key - - -def find_processes_by_name(name: str) -> list[psutil.Process]: - "Return a list of processes matching 'name'." - ls: list[psutil.Process] = [] - for p in psutil.process_iter(["name", "exe", "cmdline"]): - if ( - name == p.info["name"] - or p.info["exe"] - and os.path.basename(p.info["exe"]) == name - or p.info["cmdline"] - and p.info["cmdline"][0] == name - ): - ls.append(p) - return ls - - -def kill_proc_tree( - pid: int, - sig: signal.Signals = signal.SIGTERM, - include_parent: bool = True, - timeout: float = None, - on_terminate: callable = None, -) -> tuple[list[psutil.Process], list[psutil.Process]]: - """Kill a process tree (including grandchildren) with signal - "sig" and return a (gone, still_alive) tuple. - "on_terminate", if specified, is a callback function which is - called as soon as a child terminates. - """ - assert pid != os.getpid(), "won't kill myself" - parent = psutil.Process(pid) - children = parent.children(recursive=True) - if include_parent: - children.append(parent) - for p in children: - try: - p.send_signal(sig) - except psutil.NoSuchProcess: - pass - gone, alive = psutil.wait_procs(children, timeout=timeout, callback=on_terminate) - return (gone, alive) - - -def save_wallet_session( - wallet: str, - session_token: str, - password: str, -) -> bool: - """Save the the session token and the nostr public key encrypted. - - Returns a boolean value indicating whether the session was saved or not. - """ - - passphrase: str = f"{wallet}{password}" - key: bytes = derive_key(passphrase=passphrase.encode(), generate_salt=True) - encoded_key = encode_derived_key(key) - - token_path = Path.home().joinpath( - os.getenv("GRINPP_CLI_DATA_PATH", ".grinplusplus"), f"{wallet}.token" - ) - with open(token_path, "wb+") as file: - file.write(Fernet(encoded_key).encrypt(session_token.encode())) - - return token_path.exists() - - -def get_wallet_session(wallet: str, password: str) -> str: - """Return a string containing the Session token.""" - session_token: str = "" - encrypted_session_token: bytes = bytes() - - passphrase: str = f"{wallet}{password}" - key: bytes = derive_key(passphrase.encode(), generate_salt=False) - encoded_key: bytes = encode_derived_key(key) - - token_path: Path = Path.home().joinpath( - os.getenv("GRINPP_CLI_DATA_PATH", ".grinplusplus"), f"{wallet}.token" - ) - with open(token_path, "rb") as file: - encrypted_session_token = file.read() - - try: - session_token = Fernet(encoded_key).decrypt(encrypted_session_token).decode() - except: - raise Exception("Invalid password.") - - return session_token - - -def save_wallet_nostr_key( - wallet: str, - slatepack_address: str, - password: str, -) -> bool: - """Save the the encrypted nostr public key . - - Returns a boolean value indicating whether the key was created or not. - """ - nostr_key: PrivateKey = None - raw_key: bytes = None - - nostr_key, raw_key = get_nostr_private_key( - slatepack_address=slatepack_address, password=password - ) - - nostr_key_path: Path = Path.home().joinpath( - os.getenv("GRINPP_CLI_DATA_PATH", ".grinplusplus"), f"{wallet}.nostr" - ) - - with open(nostr_key_path, "wb+") as file: - file.write( - Fernet(encode_derived_key(raw_key)).encrypt( - nostr_key.public_key.bech32().encode() - ) - ) - - return nostr_key_path.exists() - - -def get_wallet_nostr_public_key( - wallet: str, slatepack_address: str, password: str -) -> str: - """Return a string containing the nostr Public key.""" - nostr_public_key: str = "" - - passphrase: str = f"{slatepack_address}{password}" - key: bytes = derive_key(passphrase=passphrase.encode(), generate_salt=True) - - encrypted_nostr_key: bytes = bytes() - nostr_key_path: Path = Path.home().joinpath( - os.getenv("GRINPP_CLI_DATA_PATH", ".grinplusplus"), f"{wallet}.nostr" - ) - with open(nostr_key_path, "rb") as file: - encrypted_nostr_key = file.read() - try: - nostr_public_key = ( - Fernet(encode_derived_key(key)).decrypt(encrypted_nostr_key).decode() - ) - except: - raise Exception("Invalid password.") - - return nostr_public_key - - -def get_nostr_private_key( - slatepack_address: str, password: str -) -> tuple[PrivateKey, bytes]: - """Return a tuple: Nostr Private Key and raw bytes""" - passphrase: str = f"{slatepack_address}{password}" - key: bytes = derive_key(passphrase=passphrase.encode(), generate_salt=True) - return PrivateKey(raw_secret=key), key - - -def check_wallet_reachability( - slatepack_address: str, api_url: str = "http://192.227.214.130/" -) -> bool: - try: - return ( - requests.post( - url=api_url, - data={ - "wallet": slatepack_address, - }, - ).status_code - == 200 - ) - except requests.exceptions.ConnectionError: - error = "Unable to connect to the GrinChck API" - raise Exception(error) diff --git a/src/modules/utils/kdf.py b/src/modules/utils/kdf.py index 4cbb689..de2be69 100644 --- a/src/modules/utils/kdf.py +++ b/src/modules/utils/kdf.py @@ -1,14 +1,52 @@ -#!/usr/bin/env python3 +""" +Key derivation functions derive bytes suitable for cryptographic operations from passwords or other data sources using a pseudo-random function (PRF). +""" import base64 import os +from pathlib import Path from cryptography.hazmat.backends import default_backend from cryptography.hazmat.primitives import hashes from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC -def derive_key(passphrase, generate_salt=False): - salt = _SaltManager(generate_salt) +class __SaltManager(object): + """Salt Manager internal class.""" + + def __init__(self): + self.path = str(Path.home() / ".salt") + + def get(self): + if not os.path.exists(self.path): + return self._generate_and_store() + return self._read() + + def _generate_and_store(self): + salt = os.urandom(16) + with open(self.path, "xb") as f: + f.write(salt) + return salt + + def _read(self): + with open(self.path, "rb+") as f: + return f.read() + + +def derive_key(passphrase) -> bytes: + """ + Derive a key using a passphrase. The method will try to get an already stored `salt` or generate a new one if necessary. + + Parameters + ---------- + passphrase : str + Passphrase or password. + + Returns + ------- + bytes + Raw bytes of the derived key. + """ + salt = __SaltManager() kdf = PBKDF2HMAC( algorithm=hashes.SHA256(), @@ -17,31 +55,22 @@ def derive_key(passphrase, generate_salt=False): iterations=1000, backend=default_backend(), ) - return kdf.derive(passphrase) - - -def encode_derived_key(derived_key: bytes): - return base64.urlsafe_b64encode(derived_key) + return kdf.derive(passphrase) -class _SaltManager(object): - def __init__(self, generate, path=".salt"): - self.generate = generate - self.path = path - def get(self): - if self.generate: - return self._generate_and_store() - return self._read() +def encode_key(key: bytes): + """ + Encode bytes using the URL- and filesystem-safe Base64 alphabet. Argument *derived_key* is a bytes-like object to encode. The result is returned as a bytes object. - def _generate_and_store(self): - if not os.path.exists(self.path): - salt = os.urandom(16) - with open(self.path, "xb") as f: - f.write(salt) - return salt - return self._read() + Parameters + ---------- + key : bytes + Bytes of the key to encode. - def _read(self): - with open(self.path, "rb+") as f: - return f.read() + Returns + ------- + bytes + Raw bytes. + """ + return base64.urlsafe_b64encode(key) diff --git a/src/modules/utils/nostr.py b/src/modules/utils/nostr.py deleted file mode 100644 index 247a540..0000000 --- a/src/modules/utils/nostr.py +++ /dev/null @@ -1,7 +0,0 @@ -#!/usr/bin/env python3 - -import json -import ssl -import time - -from pynostr.relay_manager import RelayManager diff --git a/src/modules/utils/processes.py b/src/modules/utils/processes.py new file mode 100644 index 0000000..bf6d4b5 --- /dev/null +++ b/src/modules/utils/processes.py @@ -0,0 +1,93 @@ +""" +Simple actions such as finding and killing processes are useful for troubleshooting. +""" +from __future__ import annotations + +import os +import signal +from collections.abc import Callable + +import psutil + + +def find(name: str) -> list[psutil.Process]: + """ + Return a list of processes matching with 'name'. + + Parameters + ---------- + name : str + Name of the process. + + Returns + ------- + bool + A list of `psutil.Process`. + """ + name = name.casefold() + result: list[psutil.Process] = [] + processes = {process.pid: process for process in psutil.process_iter()} + for process in processes.values(): + if not process.is_running(): + continue + try: + if process.name().casefold() == name: + result.append(process) + elif os.path.basename(process.exe()).casefold() == name: + result.append(process) + elif process.cmdline() and process.cmdline()[0].casefold() == name: + result.append(process) + except psutil.AccessDenied: + continue + except psutil.NoSuchProcess: + continue + + return result + + +def kill( + pid: int, + sig: signal.Signals = signal.SIGTERM, + include_parent: bool = True, + timeout: float | None = None, + on_terminate: Callable[[psutil.Process], object] | None = None, +) -> tuple[list[psutil.Process], list[psutil.Process]]: + """ + Kill a process tree (including grandchildren) with signal + "sig" and return a (gone, still_alive) tuple. + "on_terminate", if specified, is a callback function which is + called as soon as a child terminates. + + Return a (gone, alive) tuple indicating which processes + are gone and which ones are still alive. + + Parameters + ---------- + pid : int + PID of the process. + sig : signal.Signals + SIGTERM by default. + include_parent : bool + Kill also the parent processs. + timeout : float | None + Function will return as soon as all processes terminate or when *timeout* occurs. + on_terminate : Callable[[psutil.Process], object] | None + *callback* or function which gets called every time a process terminates. + + Returns + ------- + bool + A `(gone, alive)` tuple indicating which processes are gone and which ones are still alive. + """ + assert pid != os.getpid(), "I won't kill myself!" + parent = psutil.Process(pid) + children = parent.children(recursive=True) + if include_parent: + children.append(parent) + for p in children: + try: + p.send_signal(sig) + except psutil.NoSuchProcess: + pass + gone, alive = psutil.wait_procs(children, timeout=timeout, callback=on_terminate) + return (gone, alive) diff --git a/src/modules/wallet/__init__.py b/src/modules/wallet/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/modules/wallet/session.py b/src/modules/wallet/session.py new file mode 100644 index 0000000..f07139f --- /dev/null +++ b/src/modules/wallet/session.py @@ -0,0 +1,82 @@ +""" +After opening a wallet, a session token is needed to communicate with the Grin API. The token is used to identify the wallet. This module contains what is needed to store the token information. +""" +import os +from pathlib import Path + +from cryptography.fernet import Fernet + +from modules.utils.kdf import derive_key, encode_key + + +def store( + wallet: str, + token: str, + password: str, +) -> bool: + """ + Saves the session token locally. The token is encrypted before saving. + + Parameters + ---------- + wallet : str + Walle name. This is uses to name the file like: `[wallet].token`. + token : str + Session token used to communicate with the Grin API. + password: str + Password used to encrypt the session token. + + Returns + ------ + bool + It would be True if the file is correctly stored. + """ + + passphrase: str = f"{wallet}{password}" + key: bytes = derive_key(passphrase=passphrase.encode()) + encoded_key = encode_key(key) + + token_path = Path.home().joinpath( + os.getenv("GRINPP_CLI_DATA_PATH", ".grinplusplus"), f"{wallet}.token" + ) + with open(token_path, "wb+") as file: + file.write(Fernet(encoded_key).encrypt(token.encode())) + + return token_path.exists() + + +def token(wallet: str, password: str) -> str: + """ + Returns the corresponding session token of an opened wallet. + + Parameters + ---------- + wallet : str + Walle name. This is uses to name the file like: `[wallet].token`. + password: str + Password used to encrypt the session token. + + Returns + ------ + str + A string containing the session token. + """ + session_token: str = "" + encrypted_session_token: bytes = bytes() + + passphrase: str = f"{wallet}{password}" + key: bytes = derive_key(passphrase.encode()) + encoded_key: bytes = encode_key(key) + + token_path: Path = Path.home().joinpath( + os.getenv("GRINPP_CLI_DATA_PATH", ".grinplusplus"), f"{wallet}.token" + ) + with open(token_path, "rb") as file: + encrypted_session_token = file.read() + + try: + session_token = Fernet(encoded_key).decrypt(encrypted_session_token).decode() + except: + raise Exception("Invalid password.") + + return session_token From 72168c9c5380e4b3e0d4f92816cbd81c275afc0e Mon Sep 17 00:00:00 2001 From: David Tavarez <337107+davidtavarez@users.noreply.github.com> Date: Mon, 15 May 2023 20:12:31 +0200 Subject: [PATCH 08/17] refactoring modules: api (node) --- src/apps/misc/app.py | 2 +- src/apps/node/app.py | 44 ++++++------- src/apps/transaction/app.py | 2 +- src/apps/transaction/nostr/transport.py | 2 +- src/apps/wallet/app.py | 2 +- src/cli.py | 2 +- src/modules/api/node/__init__.py | 51 +++++++++++++++ src/modules/api/owner/v2/__init__.py | 18 +++++ src/modules/api/owner/v2/node.py | 87 +++++++++++++++++++++++++ src/modules/api/owner/v3/__init__.py | 21 ++++++ src/modules/api/owner/v3/wallet.py | 9 +++ 11 files changed, 212 insertions(+), 28 deletions(-) create mode 100644 src/modules/api/owner/v2/__init__.py create mode 100644 src/modules/api/owner/v2/node.py create mode 100644 src/modules/api/owner/v3/__init__.py create mode 100644 src/modules/api/owner/v3/wallet.py diff --git a/src/apps/misc/app.py b/src/apps/misc/app.py index 2401270..76caba6 100644 --- a/src/apps/misc/app.py +++ b/src/apps/misc/app.py @@ -14,7 +14,7 @@ app = typer.Typer() console = Console(width=125, style="grey93") -error_console = Console(stderr=True, style="bright_red") +error_console = Console(stderr=True, style="bright_red", width=125) @app.command(name="wenmoon") diff --git a/src/apps/node/app.py b/src/apps/node/app.py index 72aaeba..f953246 100644 --- a/src/apps/node/app.py +++ b/src/apps/node/app.py @@ -11,6 +11,7 @@ from rich.console import Console from rich.progress import Progress from rich.table import Table +from requests import exceptions from modules.api.foreign.rpc import get_list_of_settings from modules.api.node.rest import ( @@ -19,12 +20,13 @@ get_sync_state, shutdown_node, ) +from modules.api.owner.v2 import node from modules.utils import processes app = typer.Typer() console = Console(width=125, style="grey93") -error_console = Console(stderr=True, style="bright_red") +error_console = Console(stderr=True, style="bright_red", width=125) @app.command(name="stop") @@ -98,8 +100,8 @@ def start_node(): raise typer.Exit() -@app.command(name="resync") -def resync_node(): +@app.command(name="clean") +def clean(): """ Delete the local chain data and let the node sync again from scratch. """ @@ -117,38 +119,34 @@ def resync_node(): @app.command(name="status") def get_node_status(): """ - Get the status of the running node. + Get the current status of the running node. """ - percentage: float - message: str + connerr: exceptions.ConnectionError + error = "" - error: Exception - - try: - message, percentage = get_sync_state() - except Exception as err: - error_console.print(f"Error: {err} ¯\_(ツ)_/¯") - raise typer.Abort() + message = "Getting Node Status..." + percentage = 0.0 console.print("Ctrl+C to quit...\n", style="grey42 italic", justify="right") - - with Progress() as progress: + with Progress(console=console) as progress: task = progress.add_task( f"[bold white]{message}", total=100, completed=int(percentage) ) - while True: progress.update( task, description=f"[bold white]{message}", completed=percentage ) time.sleep(1) try: - message, percentage = get_sync_state() - except Exception as err: - error = err + node_status = node.get_status() + message, percentage = node.parse_node_status(node_status) + except exceptions.ConnectionError as err: + error = "Unable to communicate with the API" + break + except Exception as e: + error = str(e) break - if error: error_console.print(error) raise typer.Abort() @@ -233,13 +231,13 @@ def get_node_settings(): @app.command(name="peers") -def get_connected_peers(): +def list_connected_peers(): """ - List the peers connected to the running node. + List the inbound and outbound peers connected to the running node. """ try: - data = get_node_connected_peers() + data = node.get_connected_peers() table = Table(box=box.HORIZONTALS, expand=True) table.add_column("", justify="center", width=5) diff --git a/src/apps/transaction/app.py b/src/apps/transaction/app.py index 53a477c..e9762cc 100644 --- a/src/apps/transaction/app.py +++ b/src/apps/transaction/app.py @@ -34,7 +34,7 @@ help="Nostr Transport Plugin for Grin transactions.", ) console = Console(width=125, style="grey93") -error_console = Console(stderr=True, style="bright_red") +error_console = Console(stderr=True, style="bright_red", width=125) @app.command(name="list") diff --git a/src/apps/transaction/nostr/transport.py b/src/apps/transaction/nostr/transport.py index a57f8d8..cc74c7f 100644 --- a/src/apps/transaction/nostr/transport.py +++ b/src/apps/transaction/nostr/transport.py @@ -43,7 +43,7 @@ plugin = typer.Typer() console = Console(width=125, style="grey93") -error_console = Console(stderr=True, style="bright_red") +error_console = Console(stderr=True, style="bright_red", width=125) @plugin.command(name="send") diff --git a/src/apps/wallet/app.py b/src/apps/wallet/app.py index 6063a50..a23adcd 100644 --- a/src/apps/wallet/app.py +++ b/src/apps/wallet/app.py @@ -26,7 +26,7 @@ app = typer.Typer() console = Console(width=125, style="grey93") -error_console = Console(stderr=True, style="bright_red") +error_console = Console(stderr=True, style="bright_red", width=125) @app.command(name="open") diff --git a/src/cli.py b/src/cli.py index 8b2d5cd..0143b34 100644 --- a/src/cli.py +++ b/src/cli.py @@ -62,7 +62,7 @@ def main( ) state = {"verbose": False, "json": False} console = Console(width=125, style="grey93") -error_console = Console(stderr=True, style="bright_red") +error_console = Console(stderr=True, style="bright_red", width=125) cli.add_typer( misc_app.app, diff --git a/src/modules/api/node/__init__.py b/src/modules/api/node/__init__.py index 484b4d6..d354065 100644 --- a/src/modules/api/node/__init__.py +++ b/src/modules/api/node/__init__.py @@ -13,6 +13,57 @@ def _node_rest_call(method: str, params: str = "") -> dict: raise Exception(_get_process_status_error()) +""" +{ + "chain": { + "hash": "000181a8336f6ae49ba314083c3e5a817a335616e0e0ad204db28733fd825b52", + "height": 2266638, + "previous_hash": "000371e99eb51d745c38925960c304a5c681f9cefc19c941fa72befd6d567a36", + "total_difficulty": 2064958700115730 + }, + "header_height": 2268377, + "network": { + "height": 2268379, + "num_inbound": 0, + "num_outbound": 5, + "total_difficulty": 2065383457733505 + }, + "protocol_version": 1000, + "state": { + "download_size": 0, + "downloaded": 0, + "processing_status": 0 + }, + "sync_status": "SYNCING_BLOCKS", + "user_agent": "Grin++ 1.2.8" +} + +{ + "chain": { + "hash": "000226d54b1db6763bb26744cbfb4aa172d66c2697a57dcff04f5662001b4697", + "height": 2268385, + "previous_hash": "0002dce631821930aa8c051042b0919f281279d86ae846df1d1a7d58ed95dd4e", + "total_difficulty": 2065384900184026 + }, + "header_height": 2268385, + "network": { + "height": 2268385, + "num_inbound": 0, + "num_outbound": 7, + "total_difficulty": 2065384900184026 + }, + "protocol_version": 1000, + "state": { + "download_size": 0, + "downloaded": 0, + "processing_status": 0 + }, + "sync_status": "FULLY_SYNCED", + "user_agent": "Grin++ 1.2.8" +} +""" + + def _parse_status_response(data: dict) -> tuple[str, float]: message: str = "" percentage: float = 0.0 diff --git a/src/modules/api/owner/v2/__init__.py b/src/modules/api/owner/v2/__init__.py new file mode 100644 index 0000000..384724b --- /dev/null +++ b/src/modules/api/owner/v2/__init__.py @@ -0,0 +1,18 @@ +from uuid import uuid4 + +import requests + + +def call_owner_rpc_v2(method: str, params: dict = {}) -> dict: + url = "http://127.0.0.1:3413/v2/owner" + + params = {"jsonrpc": "2.0", "method": method, "id": str(uuid4()), "params": params} + + response = requests.post(url=url, json=params) + + data = response.json() + + if "error" in data: + raise Exception(data["error"]["message"]) + + return data["result"]["Ok"] diff --git a/src/modules/api/owner/v2/node.py b/src/modules/api/owner/v2/node.py new file mode 100644 index 0000000..4e813c3 --- /dev/null +++ b/src/modules/api/owner/v2/node.py @@ -0,0 +1,87 @@ +from modules.api.owner.v2 import call_owner_rpc_v2 + + +def parse_node_status(status: dict) -> tuple[str, float]: + """ + Parse status response in order to get a human-like messange and a percentage. + + Returns + ------ + tuple[str, float] + A tuple containing the huma-like message and percentage. + """ + message = "Waiting for Peers" + percentage = 0.0 + + if status["sync_status"] == "no_sync": + message = "Running" + percentage = 100 + + elif status["sync_status"] == "header_sync": + message = "1/7 Syncing Headers" + if status["sync_info"]["highest_height"] > 0: + percentage = ( + status["sync_info"]["current_height"] + * 100 + / status["sync_info"]["highest_height"] + ) + + elif status["sync_status"] == "txhashset_download": + message = "2/7 Downloading Chain State" + if status["sync_info"]["downloaded_size"] > 0: + percentage = ( + status["sync_info"]["downloaded_size"] + * 100 + / status["sync_info"]["total_size"] + ) + elif status["sync_status"] == "syncing": + message = "Preparing Chain State for Validation" + + elif status["sync_status"] == "txhashset_rangeproofs_validation": + message = "3/7 Validating Chain State" + elif status["sync_status"] == "txhashset_kernels_validation": + message = "4/7 Validating Chain State" + elif status["sync_status"] == "txhashset_kernels_validation": + message = "4/7 Validating Chain State" + + elif status["sync_status"] == "txhashset_processing": + message = "5/7 Validating Chain State" + + elif status["sync_status"] == "body_sync": + message = "7/7 Syncing Blocks" + if status["sync_info"]["highest_height"] > 0: + percentage = ( + status["sync_info"]["current_height"] + * 100 + / status["sync_info"]["highest_height"] + ) + + return message, percentage + + +def get_status() -> dict: + """ + Get various information about the node, the network and the current sync status. + + Returns + ------ + dict + A dict containing protocol_version, user_agent, connections, tip and + sync_status. + """ + result = call_owner_rpc_v2("get_status") + + return result + + +def get_connected_peers() -> dict: + """ + Retrieves a list of all connected peers. + + Returns + ------ + dict + A list containing peers. + """ + result = call_owner_rpc_v2("get_connected_peers") + return result diff --git a/src/modules/api/owner/v3/__init__.py b/src/modules/api/owner/v3/__init__.py new file mode 100644 index 0000000..d3d44ec --- /dev/null +++ b/src/modules/api/owner/v3/__init__.py @@ -0,0 +1,21 @@ +from uuid import uuid4 + +import requests + + +def call_owner_rpc_v3(method: str, params: dict = {}) -> dict: + url = "http://127.0.0.1:3420/v3/owner" + + params = { + "jsonrpc": "2.0", + "method": method, + "id": str(uuid4()), + "params": params, + } + + response = requests.post(url=url, json=params) + + data = response.json() + if "error" in data: + raise Exception(data["error"]["message"]) + return data["result"]["Ok"] diff --git a/src/modules/api/owner/v3/wallet.py b/src/modules/api/owner/v3/wallet.py new file mode 100644 index 0000000..8fd645a --- /dev/null +++ b/src/modules/api/owner/v3/wallet.py @@ -0,0 +1,9 @@ +from rich import print + +from modules.api.owner.v3 import call_owner_rpc_v3 + + +def open_wallet_by_name(name: str, password: str) -> dict: + result = call_owner_rpc_v3("open_wallet", {"username": name, "password": password}) + print(result) + return result From b59217a69693165c14f2e8a4bc1e8524afde5c55 Mon Sep 17 00:00:00 2001 From: David Tavarez <337107+davidtavarez@users.noreply.github.com> Date: Mon, 15 May 2023 22:25:21 +0200 Subject: [PATCH 09/17] testing 3420 --- src/apps/wallet/app.py | 5 ++++- src/modules/api/owner/v3/__init__.py | 1 - src/modules/api/owner/v3/wallet.py | 20 +++++++++++++++++++- 3 files changed, 23 insertions(+), 3 deletions(-) diff --git a/src/apps/wallet/app.py b/src/apps/wallet/app.py index a23adcd..b95a79d 100644 --- a/src/apps/wallet/app.py +++ b/src/apps/wallet/app.py @@ -23,6 +23,7 @@ ) from modules.utils import grinchck from modules.wallet import session +from modules.api.owner.v3 import wallet app = typer.Typer() console = Console(width=125, style="grey93") @@ -110,7 +111,9 @@ def wallet_creation( """ try: - response = create_wallet(wallet=name, password=password, words=words) + response = wallet.create_wallet( + name=name, password=password, mnemonic_length=words + ) console.print(f"Wallet [bold]{name}[/bold] created ✔") console.print(f"Wallet seed phrase: [bold]{response['wallet_seed']}[/bold] ✇") diff --git a/src/modules/api/owner/v3/__init__.py b/src/modules/api/owner/v3/__init__.py index d3d44ec..ebab290 100644 --- a/src/modules/api/owner/v3/__init__.py +++ b/src/modules/api/owner/v3/__init__.py @@ -14,7 +14,6 @@ def call_owner_rpc_v3(method: str, params: dict = {}) -> dict: } response = requests.post(url=url, json=params) - data = response.json() if "error" in data: raise Exception(data["error"]["message"]) diff --git a/src/modules/api/owner/v3/wallet.py b/src/modules/api/owner/v3/wallet.py index 8fd645a..00b394a 100644 --- a/src/modules/api/owner/v3/wallet.py +++ b/src/modules/api/owner/v3/wallet.py @@ -1,9 +1,27 @@ from rich import print +from typing import Optional + from modules.api.owner.v3 import call_owner_rpc_v3 -def open_wallet_by_name(name: str, password: str) -> dict: +def open_wallet(name: str, password: str) -> dict: result = call_owner_rpc_v3("open_wallet", {"username": name, "password": password}) print(result) return result + + +def create_wallet( + name: str, password: str, mnemonic_length: int = 42, mnemonic: Optional[str] = None +) -> dict: + result = call_owner_rpc_v3( + "open_wallet", + { + "username": name, + "password": password, + "mnemonic_length": mnemonic_length, + "mnemonic": mnemonic, + }, + ) + print(result) + return result From 8c102a64ff8870ef2bbd046fa80cc5b809b29878 Mon Sep 17 00:00:00 2001 From: David Tavarez <337107+davidtavarez@users.noreply.github.com> Date: Wed, 14 Jun 2023 23:36:55 +0200 Subject: [PATCH 10/17] migrating API --- src/apps/transaction/app.py | 31 ++++++++-- src/apps/wallet/app.py | 78 ++++++++++++-------------- src/cli.py | 5 +- src/modules/api/owner/v3/__init__.py | 11 ++-- src/modules/api/owner/v3/helpers.py | 27 +++++++++ src/modules/api/owner/v3/wallet.py | 84 +++++++++++++++++++++++----- 6 files changed, 167 insertions(+), 69 deletions(-) create mode 100644 src/modules/api/owner/v3/helpers.py diff --git a/src/apps/transaction/app.py b/src/apps/transaction/app.py index e9762cc..f1d04e4 100644 --- a/src/apps/transaction/app.py +++ b/src/apps/transaction/app.py @@ -2,6 +2,7 @@ from datetime import datetime from typing import Optional +from pathlib import Path import psutil import timeago @@ -23,6 +24,7 @@ get_wallet_transactions, send_coins, ) +from modules.api.owner.v3.wallet import get_stored_tx, get_tx_details, retrieve_txs from modules.wallet import session app = typer.Typer() @@ -33,8 +35,8 @@ no_args_is_help=True, help="Nostr Transport Plugin for Grin transactions.", ) -console = Console(width=125, style="grey93") -error_console = Console(stderr=True, style="bright_red", width=125) +console = Console(width=155, style="grey93") +error_console = Console(stderr=True, style="bright_red", width=155) @app.command(name="list") @@ -59,7 +61,7 @@ def list_wallet_transactions( try: session_token = session.token(wallet=wallet, password=password) - transactions = get_wallet_transactions(session_token, status.value) + transactions = retrieve_txs(session_token, status.value) except Exception as err: error_console.print(f"Error: {err} ¯\_(ツ)_/¯") raise typer.Abort() @@ -69,7 +71,8 @@ def list_wallet_transactions( raise typer.Exit() table = Table(title="", box=box.HORIZONTALS, expand=True) - table.add_column("id", justify="center", width=5) + table.add_column("tx_id", justify="center", width=5) + table.add_column("slate_id", justify="center", width=40) table.add_column("amount ツ", justify="right", width=15) table.add_column("fees ツ", justify="right", width=15) table.add_column("when?", justify="center", width=15) @@ -125,6 +128,7 @@ def list_wallet_transactions( table.add_row( f"{transaction['id']}", + f"{transaction['slate_id']}", f"{amount:10,.9f}", f"{fee:10,.9f}" if fee > 0 else "-", f"{relative_date}", @@ -374,7 +378,7 @@ def transaction_reposting( @app.command(name="details") -def transaction_information( +def details( wallet: str = typer.Option( ..., help="Name of the wallet you want query", prompt="Wallet name" ), @@ -387,6 +391,7 @@ def transaction_information( prompt="Transaction Id", ), slatepack: bool = typer.Option(False, help="Return only the Slatepack"), + slate: bool = typer.Option(False, help="Export transaction Slate"), ): """ Get the information of a transaction using the Transaction Id. @@ -396,7 +401,7 @@ def transaction_information( try: session_token = session.token(wallet=wallet, password=password) - details = get_transaction_details(session_token=session_token, id=id) + details = get_tx_details(session_token=session_token, tx_id=id) except Exception as err: error_console.print(f"Error: {err} ¯\_(ツ)_/¯") raise typer.Abort() @@ -425,6 +430,8 @@ def transaction_information( table = Table(box=box.HORIZONTALS, expand=True, show_header=False) table.add_column("", justify="right", style="bold") table.add_column("", justify="left") + table.add_row("tx_id:", f"{details['id']}") + table.add_row("slate_id:", f"{details['slate_id']}") table.add_row("amount:", f"{amount:10,.9f} ツ") table.add_row("fee:", f"{fee:10.9f} ツ") table.add_row( @@ -449,4 +456,16 @@ def transaction_information( table.add_row("outputs:", outputs) console.print(table) + if slate: + import json + file_name = f"{details['slate_id'].replace('-','_')}.json"; + file_path = ( + Path().resolve().joinpath(file_name) + ) + with open(file_path, "w", encoding="utf-8") as f: + json.dump(details["slate"], f, ensure_ascii=True, indent=4) + console.print( + f"*[italic]Slate exported to path: [yellow1]{file_path}[yellow1]\n", + ) + raise typer.Exit() diff --git a/src/apps/wallet/app.py b/src/apps/wallet/app.py index b95a79d..d051d33 100644 --- a/src/apps/wallet/app.py +++ b/src/apps/wallet/app.py @@ -1,8 +1,5 @@ #!/usr/bin/env python3 -import os -import pathlib - import typer from rich import box from rich.console import Console @@ -10,20 +7,20 @@ from rich.table import Table from modules import nostr -from modules.api.owner.rpc import ( - close_wallet_by_name, + +from modules.utils import grinchck +from modules.wallet import session +from modules.api.owner.v3.wallet import ( + delete_wallet, + get_mnemonic, + list_wallets, + open_wallet, + close_wallet, create_wallet, - delete_wallet_by_name, - get_list_of_wallets, - get_wallet_balance, - get_wallet_seed, - get_wallet_slatepack_address, - open_wallet_by_name, restore_wallet, + retrieve_summary_info, + get_slatepack_address, ) -from modules.utils import grinchck -from modules.wallet import session -from modules.api.owner.v3 import wallet app = typer.Typer() console = Console(width=125, style="grey93") @@ -31,7 +28,7 @@ @app.command(name="open") -def wallet_open( +def open( wallet: str = typer.Option( ..., help="Name of the wallet you want to open", prompt=True ), @@ -47,7 +44,7 @@ def wallet_open( """ try: - response = open_wallet_by_name(wallet, password) + response = open_wallet(wallet=wallet, password=password) console.print(f"Wallet [bold]{wallet}[/bold] opened ✔") if not session.store( @@ -55,7 +52,7 @@ def wallet_open( token=response["session_token"], password=password, ): - close_wallet_by_name(response["session_token"]) + close_wallet(response["session_token"]) raise Exception("Unable to save session information ✗") except Exception as err: error_console.print(f"Error: {err} ¯\_(ツ)_/¯") @@ -65,7 +62,7 @@ def wallet_open( @app.command(name="close") -def wallet_close( +def close( wallet: str = typer.Option( ..., help="Name of the wallet you want to close", prompt=True ), @@ -83,7 +80,7 @@ def wallet_close( try: session_token: str = session.token(wallet=wallet, password=password) with console.status("closing walet..."): - close_wallet_by_name(session_token) + close_wallet(session_token) console.print(f"Wallet [bold]{wallet}[/bold] closed successfully ✔") except Exception as err: error_console.print(f"Error: {err} ¯\_(ツ)_/¯") @@ -93,7 +90,7 @@ def wallet_close( @app.command(name="create") -def wallet_creation( +def create( name: str = typer.Option( ..., help="Name of the wallet you want to create", prompt=True ), @@ -111,9 +108,7 @@ def wallet_creation( """ try: - response = wallet.create_wallet( - name=name, password=password, mnemonic_length=words - ) + response = create_wallet(wallet=name, password=password, mnemonic_length=words) console.print(f"Wallet [bold]{name}[/bold] created ✔") console.print(f"Wallet seed phrase: [bold]{response['wallet_seed']}[/bold] ✇") @@ -122,7 +117,7 @@ def wallet_creation( token=response["session_token"], password=password, ): - close_wallet_by_name(response["session_token"]) + close_wallet(response["session_token"]) raise Exception("Unable to save session information ✗") except Exception as err: @@ -133,7 +128,7 @@ def wallet_creation( @app.command(name="recover") -def wallet_restore( +def recover( name: str = typer.Option( ..., help="Name for the wallet you want to recover", prompt=True ), @@ -151,7 +146,7 @@ def wallet_restore( """ try: - response = restore_wallet(wallet=name, password=password, seed=seed) + response = restore_wallet(wallet=name, password=password, mnemonic=seed) console.print(f"Wallet [bold]{name}[/bold] recovered ✔") if not session.store( @@ -170,7 +165,7 @@ def wallet_restore( @app.command(name="delete") -def wallet_removal( +def delete( name: str = typer.Option( ..., help="Name of the wallet you want to delete", prompt=True ), @@ -184,12 +179,12 @@ def wallet_removal( if Confirm.ask("Are you sure you want to delete the wallet?", default=False): try: - seed = get_wallet_seed(wallet=name, password=password) + seed = get_mnemonic(wallet=name, password=password) console.print( f"Wallet seed phrase: [bold italic]{seed}[/bold italic]", style="" ) - delete_wallet_by_name(wallet=name, password=password) + delete_wallet(wallet=name, password=password) console.print(f"Wallet [bold]{name}[/bold] deleted") except Exception as err: @@ -200,7 +195,7 @@ def wallet_removal( @app.command(name="backup") -def wallet_backup( +def backup( wallet: str = typer.Option(..., help="Name of the wallet you want to backup."), password: str = typer.Option( ..., prompt=True, hide_input=True, help="Wallet password." @@ -211,7 +206,7 @@ def wallet_backup( """ try: - seed = get_wallet_seed(wallet=wallet, password=password) + seed = get_mnemonic(wallet=wallet, password=password) console.print(f"Wallet seed phrase: [bold white]{seed}[/bold white]") except Exception as err: @@ -222,19 +217,19 @@ def wallet_backup( @app.command(name="list") -def list_wallets(): +def wallet_list(): """ List the created Wallets. """ try: - data = get_list_of_wallets() - if "wallets" in data and data["wallets"]: + wallets = list_wallets() + if wallets: table = Table(title="Wallets", box=box.HORIZONTALS, expand=True) table.add_column("") table.add_column("name") i = 1 - for wallet in data["wallets"]: + for wallet in wallets: table.add_row( f"{i}", f"[bold yellow]{wallet}", @@ -252,7 +247,7 @@ def list_wallets(): @app.command(name="balance") -def wallet_balance( +def get_balance( wallet: str = typer.Option( ..., help="Name of the wallet you want to check", prompt="Wallet name" ), @@ -268,9 +263,9 @@ def wallet_balance( """ try: - session_token = session.token(wallet=wallet, password=password) + token = session.token(wallet=wallet, password=password) - balance = get_wallet_balance(session_token) + balance = retrieve_summary_info(token) table = Table( title="Wallet's Balance", @@ -329,12 +324,13 @@ def wallet_address( try: session_token = session.token(wallet=wallet, password=password) - address = get_wallet_slatepack_address(session_token) + address = get_slatepack_address(session_token) console.print(f"Slatepack Address: [green3]{address}[/green3]") if test: reachable = grinchck.connect( - slatepack_address=address, api_url="http://192.227.214.130/" + slatepack_address=address["slatepack"], + api_url="http://192.227.214.130/", ) if reachable: console.print( @@ -346,7 +342,7 @@ def wallet_address( ) if npub: wallet_raw_secret = nostr.generate_raw_secret( - wallet=wallet, address=address, password=password + wallet=wallet, address=address["slatepack"], password=password ) nostr_private_key = nostr.retrieve_private_key( wallet=wallet, raw_secret=wallet_raw_secret diff --git a/src/cli.py b/src/cli.py index 0143b34..b173f02 100644 --- a/src/cli.py +++ b/src/cli.py @@ -47,7 +47,10 @@ def main( """ Grin++: Fast, Private and Secure Grin Wallet. - Grin is a lightweight implementation of the Mimblewimble protocol. The main goals and features of the Grin project are: Privacy, Scalability, Simplicity, Simple Cryptography and Decentralization. Grin wants to be usable by everyone, regardless of borders, culture, capabilities or access. To learn more about Grin, visit GRIN.MW. + Grin is a lightweight implementation of the Mimblewimble protocol. The main goals and features + of the Grin project are: Privacy, Scalability, Simplicity, Simple Cryptography and + Decentralization. Grin wants to be usable by everyone, regardless of borders, culture, + capabilities or access. To learn more about Grin, visit GRIN.MW. """ if debug: state["verbose"] = True diff --git a/src/modules/api/owner/v3/__init__.py b/src/modules/api/owner/v3/__init__.py index ebab290..7524ce4 100644 --- a/src/modules/api/owner/v3/__init__.py +++ b/src/modules/api/owner/v3/__init__.py @@ -1,5 +1,5 @@ from uuid import uuid4 - +from rich import print import requests @@ -13,8 +13,7 @@ def call_owner_rpc_v3(method: str, params: dict = {}) -> dict: "params": params, } - response = requests.post(url=url, json=params) - data = response.json() - if "error" in data: - raise Exception(data["error"]["message"]) - return data["result"]["Ok"] + response = requests.post(url=url, json=params).json() + if "error" in response: + raise Exception(response["error"]["message"]) + return response["result"]["Ok"] diff --git a/src/modules/api/owner/v3/helpers.py b/src/modules/api/owner/v3/helpers.py new file mode 100644 index 0000000..83b9075 --- /dev/null +++ b/src/modules/api/owner/v3/helpers.py @@ -0,0 +1,27 @@ +def filter_transactions(transactions: list[dict], status: str) -> list: + transactions.sort(key=lambda t: t["creation_date_time"], reverse=True) + + if status == "coinbase": + return list( + filter(lambda t: "coinbase" in str(t["type"]).lower(), transactions) + ) + elif status == "sent": + return list(filter(lambda t: "sent" in str(t["type"]).lower(), transactions)) + elif status == "pending": + return list( + filter( + lambda t: "sending" in str(t["type"]).lower() + or "receiving" in str(t["type"]).lower(), + transactions, + ) + ) + elif status == "received": + return list( + filter(lambda t: str(t["type"]).lower() == "received", transactions) + ) + elif status == "canceled": + return list( + filter(lambda t: "canceled" in str(t["type"]).lower(), transactions) + ) + + return transactions diff --git a/src/modules/api/owner/v3/wallet.py b/src/modules/api/owner/v3/wallet.py index 00b394a..c1e679a 100644 --- a/src/modules/api/owner/v3/wallet.py +++ b/src/modules/api/owner/v3/wallet.py @@ -1,27 +1,81 @@ -from rich import print -from typing import Optional +from modules.api.owner.v3 import call_owner_rpc_v3 as call +from modules.api.owner.v3.helpers import filter_transactions -from modules.api.owner.v3 import call_owner_rpc_v3 +def list_wallets() -> list[str]: + wallets: list[str] = [] + for wallet in call("list_wallets", {}): + wallets.append(wallet) + return wallets -def open_wallet(name: str, password: str) -> dict: - result = call_owner_rpc_v3("open_wallet", {"username": name, "password": password}) - print(result) - return result +def open_wallet( + wallet: str, + password: str, +) -> dict: + return call("open_wallet", {"name": wallet, "password": password}) -def create_wallet( - name: str, password: str, mnemonic_length: int = 42, mnemonic: Optional[str] = None -) -> dict: - result = call_owner_rpc_v3( - "open_wallet", +def close_wallet(session_token: str) -> dict: + return call("close_wallet", {"session_token": session_token}) + + +def create_wallet(wallet: str, password: str, mnemonic_length: int = 24) -> dict: + return call( + "create_wallet", { - "username": name, + "username": wallet, "password": password, "mnemonic_length": mnemonic_length, + }, + ) + + +def get_mnemonic( + wallet: str, + password: str, +) -> dict: + return call("get_mnemonic", {"name": wallet, "password": password}) + + +def delete_wallet( + wallet: str, + password: str, +) -> dict: + return call("delete_wallet", {"name": wallet, "password": password}) + + +def restore_wallet(wallet: str, password: str, mnemonic: str) -> dict: + return call( + "create_wallet", + { + "username": wallet, + "password": password, "mnemonic": mnemonic, }, ) - print(result) - return result + + +def retrieve_summary_info(session_token: str) -> dict: + return call("retrieve_summary_info", {"session_token": session_token}) + + +def get_slatepack_address(session_token: str) -> dict: + return call("get_slatepack_address", {"session_token": session_token}) + + +def retrieve_txs(session_token: str, status: str) -> list: + result = call("retrieve_txs", {"session_token": session_token}) + return filter_transactions(result[1], status) + + +def get_stored_tx(session_token: str, slate_id: str) -> dict: + return call("get_stored_tx", {"session_token": session_token, "slate_id": slate_id}) + + +def slate_from_slatepack_message(session_token: str) -> dict: + return call("slate_from_slatepack_message", {"session_token": session_token}) + + +def get_tx_details(session_token: str, tx_id: int) -> dict: + return call("get_tx_details", {"session_token": session_token, "tx_id": tx_id}) From 5ddf6db60d3de37d9f19b38e568ec819ed46b9e2 Mon Sep 17 00:00:00 2001 From: David Tavarez <337107+davidtavarez@users.noreply.github.com> Date: Thu, 15 Jun 2023 11:10:31 +0200 Subject: [PATCH 11/17] fixing table --- build.sh | 2 +- src/apps/transaction/app.py | 29 ++++++---- src/apps/wallet/app.py | 85 +++++++++++++++++++++++++++- src/modules/api/owner/v3/__init__.py | 1 + src/modules/api/owner/v3/wallet.py | 14 +++++ 5 files changed, 116 insertions(+), 15 deletions(-) diff --git a/build.sh b/build.sh index 6f59626..3743dba 100644 --- a/build.sh +++ b/build.sh @@ -2,4 +2,4 @@ pip install -r requirements.txt cd src -../.venv/bin/python -m nuitka --standalone --onefile --nofollow-import-to=*.tests --nofollow-import-to=*.distutils --nofollow-import-to=tornado.test --assume-yes-for-downloads --output-filename=GrinPP --output-dir="../build" --enable-console --remove-output --warn-unusual-code --include-package=asn1crypto,certifi,cffi,charset_normalizer,click,coincurve,colorama,commonmark,cryptography,idna,psutil,pycparser,pygments,pynostr,requests,rich,shellingham,timeago,tlv8,tornado,typer,urllib3 --include-module=apps,modules --noinclude-pytest-mode=nofollow --noinclude-unittest-mode=nofollow --noinclude-setuptools-mode=nofollow --noinclude-custom-mode=distutils:nofollow --product-name="Grin++ CLI" --product-version="0.2.0" --file-description="Fast, Private and Secure Grin Wallet" cli.py \ No newline at end of file +.venv/bin/python -m nuitka --standalone --onefile --nofollow-import-to=*.tests --nofollow-import-to=*.distutils --nofollow-import-to=tornado.test --assume-yes-for-downloads --output-filename=GrinPP --output-dir="../build" --enable-console --remove-output --warn-unusual-code --include-package=asn1crypto,certifi,cffi,charset_normalizer,click,coincurve,colorama,commonmark,cryptography,idna,psutil,pycparser,pygments,pynostr,requests,rich,shellingham,timeago,tlv8,tornado,typer,urllib3 --include-module=apps,modules --noinclude-pytest-mode=nofollow --noinclude-unittest-mode=nofollow --noinclude-setuptools-mode=nofollow --noinclude-custom-mode=distutils:nofollow --product-name="Grin++ CLI" --product-version="0.2.0" --file-description="Fast, Private and Secure Grin Wallet" src/cli.py \ No newline at end of file diff --git a/src/apps/transaction/app.py b/src/apps/transaction/app.py index f1d04e4..c2a10ab 100644 --- a/src/apps/transaction/app.py +++ b/src/apps/transaction/app.py @@ -24,7 +24,13 @@ get_wallet_transactions, send_coins, ) -from modules.api.owner.v3.wallet import get_stored_tx, get_tx_details, retrieve_txs +from modules.api.owner.v3.wallet import ( + get_stored_tx, + get_top_level_directory, + get_tx_details, + post_tx, + retrieve_txs, +) from modules.wallet import session app = typer.Typer() @@ -128,7 +134,7 @@ def list_wallet_transactions( table.add_row( f"{transaction['id']}", - f"{transaction['slate_id']}", + f"{transaction.get('slate_id', '')}", f"{amount:10,.9f}", f"{fee:10,.9f}" if fee > 0 else "-", f"{relative_date}", @@ -344,7 +350,7 @@ def transaction_receive( @app.command(name="post") -def transaction_reposting( +def post( wallet: str = typer.Option( ..., help="Name of the wallet from which you wish to post the transaction.", @@ -364,8 +370,8 @@ def transaction_reposting( """ try: - session_token = session.token(wallet=wallet, password=password) - if broadcast_transaction(session_token=session_token, id=id): + token = session.token(wallet=wallet, password=password) + if post_tx(session_token=token, tx_id=id): console.print("Transaction [bold]posted[/bold] successfully ✔") else: error_console.print("Unable to post the transaction ✗") @@ -431,7 +437,7 @@ def details( table.add_column("", justify="right", style="bold") table.add_column("", justify="left") table.add_row("tx_id:", f"{details['id']}") - table.add_row("slate_id:", f"{details['slate_id']}") + table.add_row("slate_id:", f"{details.get('slate_id','')}") table.add_row("amount:", f"{amount:10,.9f} ツ") table.add_row("fee:", f"{fee:10.9f} ツ") table.add_row( @@ -440,13 +446,13 @@ def details( table.add_row("type:", f"{details['type'].lower()}") if "confirmed_height" in details: table.add_row("[bold]confirmed height:", f"{details['confirmed_height']}") - if details["kernels"]: + if details["kernels"] and details["kernels"] is not None: kernels = "" for kernel in details["kernels"]: commitment = kernel["commitment"] kernels = f"{commitment}" table.add_row("kernels:", kernels) - if "outputs" in details: + if "outputs" in details and details["outputs"] is not None: outputs = "" for output in details["outputs"]: commitment = output["commitment"] @@ -458,10 +464,9 @@ def details( if slate: import json - file_name = f"{details['slate_id'].replace('-','_')}.json"; - file_path = ( - Path().resolve().joinpath(file_name) - ) + + file_name = f"{details['slate_id'].replace('-','_')}.json" + file_path = Path().resolve().joinpath(file_name) with open(file_path, "w", encoding="utf-8") as f: json.dump(details["slate"], f, ensure_ascii=True, indent=4) console.print( diff --git a/src/apps/wallet/app.py b/src/apps/wallet/app.py index d051d33..81bd5bf 100644 --- a/src/apps/wallet/app.py +++ b/src/apps/wallet/app.py @@ -13,11 +13,13 @@ from modules.api.owner.v3.wallet import ( delete_wallet, get_mnemonic, + get_top_level_directory, list_wallets, open_wallet, close_wallet, create_wallet, restore_wallet, + retrieve_outputs, retrieve_summary_info, get_slatepack_address, ) @@ -194,7 +196,7 @@ def delete( raise typer.Exit() -@app.command(name="backup") +@app.command(name="seed") def backup( wallet: str = typer.Option(..., help="Name of the wallet you want to backup."), password: str = typer.Option( @@ -202,7 +204,7 @@ def backup( ), ): """ - Backup a Wallet. + Export the Seed the Wallet. """ try: @@ -355,3 +357,82 @@ def wallet_address( raise typer.Abort() raise typer.Exit() + + +@app.command(name="outputs") +def list_outputs( + wallet: str = typer.Option( + ..., help="Name of the wallet you want to check", prompt="Wallet name" + ), + password: str = typer.Option( + ..., + prompt=True, + hide_input=True, + help="Wallet password.", + ), +): + """ + Get the balance of a running Wallet. + """ + + try: + token = session.token(wallet=wallet, password=password) + + outputs = retrieve_outputs(token) + + table = Table( + title="Wallet's Outputs", + box=box.HORIZONTALS, + show_footer=True, + show_header=True, + expand=True, + ) + table = Table(title="", box=box.HORIZONTALS, expand=True) + table.add_column("amount ツ", justify="right", width=15) + table.add_column("commitment", justify="center") + table.add_column("status", justify="center", width=10) + table.add_column("tx_id", justify="center") + table.add_column("explorer", justify="center") + + for output in outputs: + amount = output["amount"] / pow(10, 9) + link = f"[link=https://grinexplorer.net/output/{output['commitment']}]Open[/link]" + table.add_row( + f"{amount:10,.9f}", + f"{output['commitment']}", + f"{output['status']}", + f"{output['transaction_id']}", + link, + ) + console.print(table) + except Exception as err: + error_console.print(f"Error: {err} ¯\_(ツ)_/¯") + raise typer.Abort() + + raise typer.Exit() + + +@app.command(name="directory") +def get_directory( + wallet: str = typer.Option( + ..., help="Name of the wallet you want query", prompt="Wallet name" + ), + password: str = typer.Option( + ..., help="Wallet password.", prompt="Password", hide_input=True + ), +): + """ + Get the information of a transaction using the Transaction Id. + """ + + try: + session_token = session.token(wallet=wallet, password=password) + folder = get_top_level_directory(session_token=session_token) + except Exception as err: + error_console.print(f"Error: {err} ¯\_(ツ)_/¯") + raise typer.Abort() + + console.print( + f"*[italic]Slate exported to path: [yellow1]{folder}[yellow1]\n", + ) + raise typer.Exit() diff --git a/src/modules/api/owner/v3/__init__.py b/src/modules/api/owner/v3/__init__.py index 7524ce4..9616879 100644 --- a/src/modules/api/owner/v3/__init__.py +++ b/src/modules/api/owner/v3/__init__.py @@ -16,4 +16,5 @@ def call_owner_rpc_v3(method: str, params: dict = {}) -> dict: response = requests.post(url=url, json=params).json() if "error" in response: raise Exception(response["error"]["message"]) + print(response["result"]["Ok"]) return response["result"]["Ok"] diff --git a/src/modules/api/owner/v3/wallet.py b/src/modules/api/owner/v3/wallet.py index c1e679a..445210c 100644 --- a/src/modules/api/owner/v3/wallet.py +++ b/src/modules/api/owner/v3/wallet.py @@ -79,3 +79,17 @@ def slate_from_slatepack_message(session_token: str) -> dict: def get_tx_details(session_token: str, tx_id: int) -> dict: return call("get_tx_details", {"session_token": session_token, "tx_id": tx_id}) + + +def retrieve_outputs(session_token: str) -> dict: + return call("retrieve_outputs", {"session_token": session_token}) + + +def get_top_level_directory(session_token: str) -> dict: + return call("get_top_level_directory", {"session_token": session_token}) + + +def post_tx(session_token: str, tx_id: int) -> dict: + return call( + "post_tx", {"session_token": session_token, "tx_id": tx_id, "method": "FLUFF"} + ) From b57ccb180a7c2ddad2b42501a16884bdaf2e89d3 Mon Sep 17 00:00:00 2001 From: David Tavarez <337107+davidtavarez@users.noreply.github.com> Date: Thu, 15 Jun 2023 13:34:43 +0200 Subject: [PATCH 12/17] ignoring build folder --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 6a36299..09fb8d8 100644 --- a/.gitignore +++ b/.gitignore @@ -48,3 +48,4 @@ package.nls.*.json l10n/ Thumbs.db **/.vscode**/ +build/** \ No newline at end of file From b67a4110e3cc2019633b1e1a9a5e9bb0f4aaecfa Mon Sep 17 00:00:00 2001 From: David Tavarez <337107+davidtavarez@users.noreply.github.com> Date: Thu, 15 Jun 2023 13:34:53 +0200 Subject: [PATCH 13/17] adding more commands --- src/apps/transaction/app.py | 120 ++++++++++++++++++--------- src/modules/api/owner/v3/__init__.py | 2 +- src/modules/api/owner/v3/wallet.py | 52 ++++++++++++ 3 files changed, 135 insertions(+), 39 deletions(-) diff --git a/src/apps/transaction/app.py b/src/apps/transaction/app.py index c2a10ab..1bc1e59 100644 --- a/src/apps/transaction/app.py +++ b/src/apps/transaction/app.py @@ -3,7 +3,9 @@ from datetime import datetime from typing import Optional from pathlib import Path +import base64 +import json import psutil import timeago import typer @@ -15,24 +17,25 @@ from apps.transaction.nostr import transport from modules.api import TransactionsFilterOptions from modules.api.owner.rpc import ( - add_initial_signature, add_signature_to_transaction, - broadcast_transaction, - cancel_transaction, - estimate_transaction_fee, - get_transaction_details, - get_wallet_transactions, - send_coins, ) from modules.api.owner.v3.wallet import ( + cancel_tx, + decode_slatepack, + finalize_tx, get_stored_tx, - get_top_level_directory, get_tx_details, post_tx, + receive_tx, retrieve_txs, + estimate_fee, + send_tx, ) from modules.wallet import session +if not psutil.WINDOWS: + import readline + app = typer.Typer() app.add_typer( @@ -46,7 +49,7 @@ @app.command(name="list") -def list_wallet_transactions( +def list_transactions( wallet: str = typer.Option( ..., help="Name of the wallet from which you want to list transactions.", @@ -149,7 +152,7 @@ def list_wallet_transactions( @app.command(name="send") -def send_grin( +def send( wallet: str = typer.Option( ..., help="Name of the wallet from which you wish to send the coins.", @@ -158,9 +161,7 @@ def send_grin( password: str = typer.Option( ..., help="Wallet password.", prompt="Password", hide_input=True ), - amount: float = typer.Option( - ..., help="Amount of ツ you want to send.", prompt="Amount of ツ" - ), + amount: Optional[float] = typer.Option(None, help="Amount of ツ you want to send."), address: Optional[str] = typer.Option( "", help="Slatepack Address where you want to send the ツ" ), @@ -172,25 +173,28 @@ def send_grin( Send ツ to someone """ - session_token: str fee: float try: - session_token = session.token(wallet=wallet, password=password) - fee = float( - estimate_transaction_fee(session_token=session_token, amount=amount)["fee"] - ) / pow(10, 9) + token = session.token(wallet=wallet, password=password) + if not amount: + estimate = estimate_fee(session_token=token) + fee = float(estimate["fee"]) / pow(10, 9) + amount_prt = float(estimate["amount"]) / pow(10, 9) + else: + estimate = estimate_fee(session_token=token, amount=amount) + fee = float(estimate["fee"]) / pow(10, 9) + amount_prt = float(estimate["amount"]) / pow(10, 9) except Exception as err: error_console.print(f"Error: {err} ¯\_(ツ)_/¯") raise typer.Abort() - table: Table = Table( title="Transaction details", box=box.HORIZONTALS, expand=True, show_header=False ) table.add_column("", justify="right", style="bold") table.add_column("", justify="left") table.add_row("wallet:", f"{wallet}") - table.add_row("amount:", f"{amount:10,.9f} ツ") + table.add_row("amount:", f"{amount_prt:10,.9f} ツ") table.add_row("fee:", f"{fee:10.9f} ツ") if address: table.add_row("receiver:", f"{address}") @@ -202,7 +206,7 @@ def send_grin( proceed = True else: proceed = Confirm.ask( - "Are you sure you want to create this transaction?", default=False + "Are you sure you want to send this transaction?", default=False ) if proceed: sent: bool = False @@ -210,9 +214,14 @@ def send_grin( try: session_token = session.token(wallet=wallet, password=password) with console.status("Building transaction..."): - transaction = send_coins( - session_token=session_token, amount=amount, address=address - ) + if not amount: + transaction = send_tx(session_token=session_token, address=address) + else: + transaction = send_tx( + session_token=session_token, + amount=amount, + address=address, + ) slatepack = transaction["slatepack"] if transaction["status"] == "FINALIZED": sent = True @@ -249,7 +258,7 @@ def transaction_cancelation( password: str = typer.Option( ..., help="Wallet password.", prompt="Password", hide_input=True ), - id: int = typer.Option( + tx_id: int = typer.Option( ..., help="Id of the transaction you want to be canceled.", prompt="Transaction Id", @@ -260,9 +269,9 @@ def transaction_cancelation( """ try: - session_token = session.token(wallet=wallet, password=password) + token = session.token(wallet=wallet, password=password) - if cancel_transaction(session_token=session_token, id=id): + if cancel_tx(session_token=token, tx_id=tx_id): console.print("Transaction [bold]canceled[/bold] successfully ✔") else: error_console.print("Unable to cancel the transaction ✗") @@ -275,7 +284,7 @@ def transaction_cancelation( @app.command(name="finalize") -def transaction_finalization( +def finalize( wallet: str = typer.Option( ..., help="Name of the wallet from which you wish to finalize the transaction.", @@ -291,10 +300,8 @@ def transaction_finalization( try: token = session.token(wallet=wallet, password=password) - if not psutil.WINDOWS: - import readline slatepack: str = console.input("Please, insert the Slatepack down below:\n") - if add_signature_to_transaction(session_token=token, slatepack=slatepack): + if finalize_tx(session_token=token, slatepack=slatepack): console.print("Transaction [bold]finalized[/bold] successfully ✔") else: error_console.print("Unable to finalized the transaction ✗") @@ -306,7 +313,7 @@ def transaction_finalization( @app.command(name="receive") -def transaction_receive( +def receive( wallet: str = typer.Option( ..., help="Name of the wallet where you want to receive the coins.", @@ -321,17 +328,16 @@ def transaction_receive( """ try: - session_token = session.token(wallet=wallet, password=password) + token = session.token(wallet=wallet, password=password) if not psutil.WINDOWS: import readline + slatepack = console.input("Paste the Slatepack down below:\n") if len(slatepack.strip().rstrip()) == 0: raise Exception("Empty Slatepack") - signed_slatepack = add_initial_signature( - session_token=session_token, slatepack=slatepack - ) + signed_slatepack = receive_tx(session_token=token, slatepack=slatepack) console.print("\nPlease share the next Slatepack with the sender:") console.print( @@ -463,8 +469,6 @@ def details( console.print(table) if slate: - import json - file_name = f"{details['slate_id'].replace('-','_')}.json" file_path = Path().resolve().joinpath(file_name) with open(file_path, "w", encoding="utf-8") as f: @@ -474,3 +478,43 @@ def details( ) raise typer.Exit() + + +@app.command(name="decode") +def decode( + wallet: str = typer.Option( + ..., + help="Name of the wallet where you want to receive the coins.", + prompt="Wallet name", + ), + password: str = typer.Option( + ..., help="Wallet password.", prompt="Password", hide_input=True + ), +): + """ + decode a transaction using Slatepack Messsage + """ + + try: + token = session.token(wallet=wallet, password=password) + + slatepack = console.input("Paste the Slatepack down below:\n") + + if len(slatepack.strip().rstrip()) == 0: + raise Exception("Empty Slatepack") + + decoded_slatepack = decode_slatepack(session_token=token, message=slatepack) + + file_name = f"{decoded_slatepack['id'].replace('-','_')}.json" + file_path = Path().resolve().joinpath(file_name) + with open(file_path, "w", encoding="utf-8") as f: + json.dump(decoded_slatepack, f, ensure_ascii=True, indent=4) + console.print( + f"\n\n[bold]Slate exported to path: [italic yellow1]{file_path}[/italic yellow1]\n", + ) + + except Exception as err: + error_console.print(f"Error: {err} ¯\_(ツ)_/¯") + raise typer.Abort() + + raise typer.Exit() diff --git a/src/modules/api/owner/v3/__init__.py b/src/modules/api/owner/v3/__init__.py index 9616879..04b62bf 100644 --- a/src/modules/api/owner/v3/__init__.py +++ b/src/modules/api/owner/v3/__init__.py @@ -16,5 +16,5 @@ def call_owner_rpc_v3(method: str, params: dict = {}) -> dict: response = requests.post(url=url, json=params).json() if "error" in response: raise Exception(response["error"]["message"]) - print(response["result"]["Ok"]) + ##print(response["result"]) return response["result"]["Ok"] diff --git a/src/modules/api/owner/v3/wallet.py b/src/modules/api/owner/v3/wallet.py index 445210c..28ec725 100644 --- a/src/modules/api/owner/v3/wallet.py +++ b/src/modules/api/owner/v3/wallet.py @@ -93,3 +93,55 @@ def post_tx(session_token: str, tx_id: int) -> dict: return call( "post_tx", {"session_token": session_token, "tx_id": tx_id, "method": "FLUFF"} ) + + +def estimate_fee( + session_token: str, + amount: float = -1, +) -> dict: + params: dict = { + "session_token": session_token, + "fee_base": 500000, + "selection_strategy": {"strategy": "SMALLEST", "inputs": []}, + } + if amount > 0: + params["amount"] = str(amount * pow(10, 9)) + + return call("estimate_fee", params) + + +def send_tx( + session_token: str, + amount: float = -1, + address: str = "", +) -> dict: + params: dict = { + "session_token": session_token, + "address": address, + "fee_base": 500000, + "change_outputs": 1, + "selection_strategy": {"strategy": "SMALLEST", "inputs": []}, + "post_tx": {"method": "FLUFF"}, + } + if amount > 0: + params["amount"] = str(amount * pow(10, 9)) + + return call("send", params) + + +def cancel_tx(session_token: str, tx_id: int) -> dict: + return call("cancel_tx", {"session_token": session_token, "tx_id": tx_id}) + + +def receive_tx(session_token: str, slatepack: str) -> dict: + return call("receive_tx", {"session_token": session_token, "slatepack": slatepack}) + + +def decode_slatepack(session_token: str, message: str) -> dict: + return call( + "decode_slatepack_message", {"session_token": session_token, "message": message} + ) + + +def finalize_tx(session_token: str, slatepack: str) -> dict: + return call("finalize_tx", {"session_token": session_token, "slatepack": slatepack}) From bdaa60f72fc76a9cafdc1394a43a1d49c7b432b5 Mon Sep 17 00:00:00 2001 From: David Tavarez <337107+davidtavarez@users.noreply.github.com> Date: Thu, 15 Jun 2023 15:14:22 +0200 Subject: [PATCH 14/17] fixing txs order --- src/apps/transaction/app.py | 6 +++--- src/modules/api/owner/v3/helpers.py | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/apps/transaction/app.py b/src/apps/transaction/app.py index 1bc1e59..7ebf262 100644 --- a/src/apps/transaction/app.py +++ b/src/apps/transaction/app.py @@ -251,7 +251,7 @@ def send( @app.command(name="cancel") -def transaction_cancelation( +def cancel( wallet: str = typer.Option( ..., help="Name of the wallet from which you wish to cancel the transaction." ), @@ -365,7 +365,7 @@ def post( password: str = typer.Option( ..., help="Wallet password.", prompt="Password", hide_input=True ), - id: int = typer.Option( + tx_id: int = typer.Option( ..., help="Id of the transaction you want to be repost", prompt="Transaction ID", @@ -377,7 +377,7 @@ def post( try: token = session.token(wallet=wallet, password=password) - if post_tx(session_token=token, tx_id=id): + if post_tx(session_token=token, tx_id=tx_id): console.print("Transaction [bold]posted[/bold] successfully ✔") else: error_console.print("Unable to post the transaction ✗") diff --git a/src/modules/api/owner/v3/helpers.py b/src/modules/api/owner/v3/helpers.py index 83b9075..5f3b34e 100644 --- a/src/modules/api/owner/v3/helpers.py +++ b/src/modules/api/owner/v3/helpers.py @@ -1,5 +1,5 @@ def filter_transactions(transactions: list[dict], status: str) -> list: - transactions.sort(key=lambda t: t["creation_date_time"], reverse=True) + transactions.sort(key=lambda t: t["creation_date_time"], reverse=False) if status == "coinbase": return list( From 945950dc90e98a547fe5b628ae165b1b5e61fee6 Mon Sep 17 00:00:00 2001 From: David Tavarez <337107+davidtavarez@users.noreply.github.com> Date: Thu, 15 Jun 2023 15:17:25 +0200 Subject: [PATCH 15/17] tx_id instead of id --- src/apps/transaction/app.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/apps/transaction/app.py b/src/apps/transaction/app.py index 7ebf262..23c17de 100644 --- a/src/apps/transaction/app.py +++ b/src/apps/transaction/app.py @@ -397,7 +397,7 @@ def details( password: str = typer.Option( ..., help="Wallet password.", prompt="Password", hide_input=True ), - id: int = typer.Option( + tx_id: int = typer.Option( ..., help="Id of the transaction you want to read", prompt="Transaction Id", @@ -413,7 +413,7 @@ def details( try: session_token = session.token(wallet=wallet, password=password) - details = get_tx_details(session_token=session_token, tx_id=id) + details = get_tx_details(session_token=session_token, tx_id=tx_id) except Exception as err: error_console.print(f"Error: {err} ¯\_(ツ)_/¯") raise typer.Abort() From 0e0379aad94f5454d3d40edab3371455bf3582e6 Mon Sep 17 00:00:00 2001 From: David Tavarez <337107+davidtavarez@users.noreply.github.com> Date: Sun, 1 Oct 2023 13:51:47 +0200 Subject: [PATCH 16/17] preparing release --- requirements.txt | 27 ++++++++++++++------------- src/cli.py | 2 +- 2 files changed, 15 insertions(+), 14 deletions(-) diff --git a/requirements.txt b/requirements.txt index 0d2531b..3ebb4ae 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,24 +1,25 @@ asn1crypto==1.5.1 -certifi==2022.12.7 -cffi==1.15.1 -charset-normalizer==3.1.0 -click==8.1.3 +certifi==2023.7.22 +cffi==1.16.0 +charset-normalizer==3.3.0 +click==8.1.7 coincurve==18.0.0 colorama==0.4.6 commonmark==0.9.1 -cryptography==40.0.2 +cryptography==41.0.4 idna==3.4 -markdown-it-py==2.2.0 +markdown-it-py==3.0.0 mdurl==0.1.2 psutil==5.9.5 pycparser==2.21 -Pygments==2.15.0 +Pygments==2.16.1 pynostr==0.6.2 -requests==2.28.2 -rich==13.3.4 -shellingham==1.5.0.post1 +requests==2.31.0 +rich==13.6.0 +shellingham==1.5.3 timeago==1.0.16 tlv8==0.10.0 -tornado==6.3 -typer==0.7.0 -urllib3==1.26.15 +tornado==6.3.3 +typer==0.9.0 +typing_extensions==4.8.0 +urllib3==2.0.5 diff --git a/src/cli.py b/src/cli.py index b173f02..386f24e 100644 --- a/src/cli.py +++ b/src/cli.py @@ -71,7 +71,7 @@ def main( misc_app.app, name="misc", no_args_is_help=True, - help="Miscellaneous cool things! Give it a try (งツ)ว", + help="Miscellaneous cool things! Give it a try ʘ‿ʘ", ) cli.add_typer( From 57786939e0f95ba39ba395208288593a86ca685e Mon Sep 17 00:00:00 2001 From: David Tavarez <337107+davidtavarez@users.noreply.github.com> Date: Mon, 30 Oct 2023 18:27:24 +0100 Subject: [PATCH 17/17] fixing node tip method --- src/apps/node/app.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/apps/node/app.py b/src/apps/node/app.py index f953246..937a63b 100644 --- a/src/apps/node/app.py +++ b/src/apps/node/app.py @@ -161,7 +161,7 @@ def get_node_tip(): """ try: - data = get_node_state() + data = node.get_status() table = Table(box=box.HORIZONTALS, expand=True) table.add_column("node height", justify="center") table.add_column("network height", justify="center") @@ -169,10 +169,10 @@ def get_node_tip(): table.add_column("current hash", justify="center") table.add_row( - str(data["header_height"]), - str(data["network"]["height"]), - str(data["chain"]["height"]), - data["chain"]["hash"], + str(data["sync_info"]["current_height"]), + str(data["sync_info"]["highest_height"]), + str(data["tip"]["height"]), + data["tip"]["last_block_pushed"], ) console.print(table)