diff --git a/Pipfile b/Pipfile index 2683ed8235ef94b5dd4097a23d1821b1c4fe81bc..cc57e9704de7a63efa0310c794f12a4f662a72d5 100644 --- a/Pipfile +++ b/Pipfile @@ -4,12 +4,12 @@ verify_ssl = true name = "pypi" [packages] -commandlines = "*" ipaddress = "*" tabulate = "*" PyNaCl = "*" -"e1839a8" = {path = ".", editable = true} +e1839a8 = {path = ".",editable = true} duniterpy = "*" +click = "*" [dev-packages] pre-commit = "*" diff --git a/Pipfile.lock b/Pipfile.lock index 0139803abc63d598202218b9acc4dc9a609a6516..673396f7c1ee1f8429b97524d0ec1eacb4080c50 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "2e209d428ac040de3bbcb1d6166f6ee0e1e20741913b75f8b01bc703469b9e9b" + "sha256": "52d928682e98b0179c9a450c61cac3c22051596232760700cafa68823d718822" }, "pipfile-spec": 6, "requires": { @@ -116,13 +116,13 @@ ], "version": "==3.0.4" }, - "commandlines": { + "click": { "hashes": [ - "sha256:86b650b78470ac95966d7b1a9d215c16591bccb34b28ae2bb9026c3b4166fd64", - "sha256:b7dbf71b8dec42c16e9694b87e0e121d288e5a40d5d4f0dd1c0651ab7af06837" + "sha256:2335065e6395b9e67ca716de5f7526736bfa6ceead690adf616d925bdc622b13", + "sha256:5b94b49521f6456670fdb30cd82a4eca9412788a93fa6dd6df72c94d5a8ff2d7" ], "index": "pypi", - "version": "==0.4.1" + "version": "==7.0" }, "duniterpy": { "hashes": [ @@ -355,6 +355,7 @@ "sha256:2335065e6395b9e67ca716de5f7526736bfa6ceead690adf616d925bdc622b13", "sha256:5b94b49521f6456670fdb30cd82a4eca9412788a93fa6dd6df72c94d5a8ff2d7" ], + "index": "pypi", "version": "==7.0" }, "coverage": { diff --git a/README.md b/README.md index b093a0542e90851fd31d98bdd3b8a72b9179229e..d0286d2c1e28fe8057150a0055f8ab128b799b7a 100644 --- a/README.md +++ b/README.md @@ -55,9 +55,9 @@ silkaj <sub-command> -p <address>:<port> ### Dependencies Silkaj is based on Python dependencies: +- [Click](https://click.palletsprojects.com/): Command Line Interface Creation Kit. - [DuniterPy](https://git.duniter.org/clients/python/duniterpy/): Python APIs library to implement duniter clients softwares. - [Tabulate](https://bitbucket.org/astanin/python-tabulate/overview): to display charts. -- [Commandlines](https://github.com/chrissimpkins/commandlines): to parse command and sub-commands. - [PyNaCl](https://github.com/pyca/pynacl/): Cryptography (NaCl) library. ### Names diff --git a/bin/silkaj b/bin/silkaj index 4fc7cda6a98422c1616829ff1b950051bd48104b..13aa69ab5842fed7c6c607a76c790af187a04076 100755 --- a/bin/silkaj +++ b/bin/silkaj @@ -17,9 +17,7 @@ You should have received a copy of the GNU Affero General Public License along with Silkaj. If not, see <https://www.gnu.org/licenses/>. """ -from asyncio import get_event_loop - -from silkaj.cli_tools import manage_cmd +from silkaj.cli_tools import cli if __name__ == "__main__": - get_event_loop().run_until_complete(manage_cmd()) + cli(obj={}) diff --git a/release.sh b/release.sh index 41bb857a66b9b557ae0b6812da00e15dff04aff0..7a48b68b648e80d34d0e67903fb6e2879fa99130 100755 --- a/release.sh +++ b/release.sh @@ -22,7 +22,7 @@ check_branch() { } update_version() { - sed -i "s/SILKAJ_VERSION = \"silkaj.*\"/SILKAJ_VERSION = \"silkaj $VERSION\"/" silkaj/constants.py + sed -i "s/SILKAJ_VERSION = \".*\"/SILKAJ_VERSION = \"$VERSION\"/" silkaj/constants.py sed -i "s/version=\".*\",/version=\"$VERSION\",/" setup.py git diff } diff --git a/setup.py b/setup.py index e9bd6a49b9d3a2b79b46647362724872c51fcb4e..abca0f9260ebc56c90e44d03f120eb76bdcb818d 100644 --- a/setup.py +++ b/setup.py @@ -24,7 +24,7 @@ setuptools.setup( "Operating System :: OS Independent", ), install_requires=[ - "commandlines", + "Click", "duniterpy", "ipaddress", "tabulate", diff --git a/silkaj/auth.py b/silkaj/auth.py index b8afd2797adcf2ade2b58bf9cbd58bc97a4c2170..06a9acc983a701b982eca000453e4ab84be486b6 100644 --- a/silkaj/auth.py +++ b/silkaj/auth.py @@ -16,6 +16,7 @@ along with Silkaj. If not, see <https://www.gnu.org/licenses/>. """ from silkaj.tools import message_exit +from click import command, option, pass_context from getpass import getpass from os import path from re import compile, search @@ -23,23 +24,22 @@ from duniterpy.key import SigningKey from duniterpy.key import ScryptParams -def auth_method(cli_args): - if cli_args.contains_switches("auth-seed"): +@pass_context +def auth_method(ctx): + if ctx.obj["AUTH_SEED"]: return auth_by_seed() - if cli_args.contains_switches("auth-file"): - return auth_by_auth_file(cli_args) - if cli_args.contains_switches("auth-wif"): + if ctx.obj["AUTH_FILE"]: + return auth_by_auth_file() + if ctx.obj["AUTH_WIF"]: return auth_by_wif() else: - return auth_by_scrypt(cli_args) + return auth_by_scrypt() -def generate_auth_file(cli_args): - if cli_args.contains_definitions("file"): - file = cli_args.get_definition("file") - else: - file = "authfile" - key = auth_method(cli_args) +@command("authfile", help="Generate authentication file") +@option("--file", default="authfile", show_default=True, help="Path file") +def generate_auth_file(file): + key = auth_method() key.save_seedhex_file(file) print( "Authentication file 'authfile' generated and stored in current\ @@ -48,11 +48,9 @@ def generate_auth_file(cli_args): ) -def auth_by_auth_file(cli_args): - if cli_args.contains_definitions("file"): - file = cli_args.get_definition("file") - else: - file = "authfile" +@pass_context +def auth_by_auth_file(ctx): + file = ctx.obj["AUTH_FILE_PATH"] if not path.isfile(file): message_exit('Error: the file "' + file + '" does not exist') with open(file) as f: @@ -79,33 +77,28 @@ def auth_by_seed(): message_exit(error) -def auth_by_scrypt(cli_args): +@pass_context +def auth_by_scrypt(ctx): salt = getpass("Please enter your Scrypt Salt (Secret identifier): ") password = getpass("Please enter your Scrypt password (masked): ") - if ( - cli_args.contains_definitions("n") - and cli_args.contains_definitions("r") - and cli_args.contains_definitions("p") - ): - n, r, p = ( - cli_args.get_definition("n"), - cli_args.get_definition("r"), - cli_args.get_definition("p"), - ) + if ctx.obj["AUTH_SCRYPT_PARAMS"]: + n, r, p = ctx.obj["AUTH_SCRYPT_PARAMS"].split(",") + if n.isnumeric() and r.isnumeric() and p.isnumeric(): - scrypt_params = ScryptParams(int(n), int(r), int(p)) + n, r, p = int(n), int(r), int(p) if n <= 0 or n > 65536 or r <= 0 or r > 512 or p <= 0 or p > 32: message_exit("Error: the values of Scrypt parameters are not good") + scrypt_params = ScryptParams(n, r, p) else: message_exit("one of n, r or p is not a number") else: scrypt_params = None - print("Using default values. Scrypt parameters not specified or wrong format") - n, r, p = 4096, 16, 1 - print("Scrypt parameters used: N: {0}, r: {1}, p: {2}".format(n, r, p)) - return SigningKey.from_credentials(salt, password, scrypt_params) + try: + return SigningKey.from_credentials(salt, password, scrypt_params) + except ValueError as error: + message_exit(error) def auth_by_wif(): diff --git a/silkaj/cert.py b/silkaj/cert.py index 6dcd97af843cf9aa0666f79132e6eb11b22c7f8a..ff6c4df668e63dc92f5ceee641c876351ba1f222 100644 --- a/silkaj/cert.py +++ b/silkaj/cert.py @@ -15,22 +15,26 @@ You should have received a copy of the GNU Affero General Public License along with Silkaj. If not, see <https://www.gnu.org/licenses/>. """ +from click import command, argument from time import time from tabulate import tabulate from duniterpy.api.bma import wot from duniterpy.documents import BlockUID, Identity, Certification from silkaj.auth import auth_method -from silkaj.tools import convert_time, message_exit +from silkaj.tools import convert_time, message_exit, coroutine from silkaj.network_tools import ClientInstance, HeadBlock from silkaj.blockchain_tools import BlockchainParams from silkaj.license import license_approval from silkaj.wot import is_member, get_uid_from_pubkey, get_informations_for_identity -async def send_certification(cli_args): +@command("cert", help="Send certification") +@argument("id_to_certify") +@coroutine +async def send_certification(id_to_certify): client = ClientInstance().client - id_to_certify = await get_informations_for_identity(cli_args.subsubcmd) + id_to_certify = await get_informations_for_identity(id_to_certify) main_id_to_certify = id_to_certify["uids"][0] # Display license and ask for confirmation @@ -39,7 +43,7 @@ async def send_certification(cli_args): license_approval(currency) # Authentication - key = auth_method(cli_args) + key = auth_method() # Check whether current user is member issuer_pubkey = key.pubkey diff --git a/silkaj/cli_tools.py b/silkaj/cli_tools.py index 488447ae0f6b79be7bb253ec57c7645b83a6bb0a..a9d40bc85d35f00930ef07e2184b670b3cbc9344 100644 --- a/silkaj/cli_tools.py +++ b/silkaj/cli_tools.py @@ -17,189 +17,92 @@ along with Silkaj. If not, see <https://www.gnu.org/licenses/>. # -*- coding: utf-8 -*- -from commandlines import Command +from click import group, help_option, version_option, option, pass_context + from silkaj.tx import send_transaction from silkaj.money import cmd_amount from silkaj.cert import send_certification from silkaj.commands import ( currency_info, difficulties, - set_network_sort_keys, network_info, argos_info, list_blocks, ) -from silkaj.tools import message_exit from silkaj.wot import received_sent_certifications, id_pubkey_correspondence from silkaj.auth import generate_auth_file -from silkaj.license import display_license -from silkaj.constants import ( - SILKAJ_VERSION, - G1_SYMBOL, - GTEST_SYMBOL, - G1_DEFAULT_ENDPOINT, - G1_TEST_DEFAULT_ENDPOINT, +from silkaj.license import license_command +from silkaj.constants import SILKAJ_VERSION + + +@group() +@help_option("-h", "--help") +@version_option(SILKAJ_VERSION, "-v", "--version") +@option( + "--peer", + "-p", + help="Default endpoint will reach Ğ1 currency with `https://g1.duniter.org` endpoint.\ + Custom endpoint can be specified with `-p` option followed by <domain>:<port>", ) - - -def usage(): - message_exit( - "Silkaj: command line client for Duniter currencies\ - \n\nhelp: -h, --help, --usage \ - \nversion: -v, --version \ - \nabout: display informations about the programm\ - \n \ - \nEndpoint:\ - \nDefault endpoint will reach " - + G1_SYMBOL - + " currency with `https://" - + G1_DEFAULT_ENDPOINT[0] - + "` endpoint.\ - \nUse one of these options at the end of the command:\ - \n - `--gtest` to reach " - + GTEST_SYMBOL - + " currency with `https://" - + G1_TEST_DEFAULT_ENDPOINT[0] - + "` endpoint\ - \n - custom endpoint can be specified with `-p` option followed by <domain>:<port> or <domain> with 443 as default port\ - \n \ - \nCommands: \ - \n - info: Display information about currency \ - \n \ - \n - amount: Get amount of accounts \ - \n pubkeys and/or ids separated with colon: <pubkey:id:pubkey>\ - \n - authentication: see below section\ - \n \ - \n - tx/transaction: Send transaction\ - \n - authentication: see below section\ - \n - amount:\ - \n --amountUD=<relative value> | --amount=<quantitative value>\ - \n [--allSources] \ - \n --output=<public key>[!checksum]:[<public key>[!checksum]] \ - \n [--comment=<comment>] \ - \n [--outputBackChange=<public key[!checksum]>] \ - \n [-y | --yes], don't ask for prompt confirmation \ - \n \ - \n - cert: Send certification\ - \n - e.g.: silkaj cert <id> <auth>\ - \n - authentication: see below section\ - \n \ - \n - net/network: Display current network with many information \ - \n [--discover] Discover all network (could take a while), optional \ - \n [-s | --sort] Sort column names comma-separated (for example \"-s block,diffi\"), optional \ - \n Default sort is block,member,diffi,uid \ - \n \ - \n - diffi: list proof-of-work difficulty to generate next block \ - \n \ - \n - blocks n: display last n blocks (`0` for current window size) \ - \n last blocks are displayed under n <= 30.\ - \n To force display last ones, use `--last` option\ - \n \ - \n - argos: display currency information formated for Argos or BitBar\ - \n \ - \n - generate_auth_file: Generate file to store the seed of the account\ - \n - authentication: see below section\ - \n \ - \n - id/identities <pubkey> or <identity>: get corresponding identity or pubkey from pubkey or identity.\ - \n it could autocomplete the pubkey corresponding to an identity with three or four following characters.\ - \n \ - \n - wot <pubkey> or <identity>: display received and sent certifications for an account.\ - \n \ - \n - license: display Ğ1 currency license.\ - \n \ - \nAuthentication:\ - \n for amount, transaction, certification, and generate_auth_file commands\ - \n - Scrypt is the default authentication method with 4096,16,1 as default values\ - \n you can specify others values specifying following parameters: -n <N> -r <r> -p <p>\ - \n - Seed: --auth-seed\ - \n - File: --auth-file [--file=<path file>], './authfile' will be taken if there is no path specified\ - \n - WIF: --auth-wif" - ) - - -async def manage_cmd(): - cli_args = Command() - if cli_args.is_version_request(): - message_exit(SILKAJ_VERSION) - - subcmd = [ - "license", - "about", - "info", - "diffi", - "net", - "network", - "blocks", - "argos", - "amount", - "tx", - "transaction", - "cert", - "generate_auth_file", - "id", - "identities", - "wot", - ] - if ( - cli_args.is_help_request() - or cli_args.is_usage_request() - or cli_args.subcmd not in subcmd - ): - usage() - - if cli_args.subcmd == "about": - about() - elif cli_args.subcmd == "info": - await currency_info() - - elif cli_args.subcmd == "diffi": - await difficulties() - - elif cli_args.subcmd == "net" or cli_args.subcmd == "network": - if cli_args.contains_switches("sort"): - set_network_sort_keys(cli_args.get_definition("sort")) - if cli_args.contains_switches("s"): - set_network_sort_keys(cli_args.get_definition("s")) - await network_info(cli_args.contains_switches("discover")) - - elif ( - cli_args.subcmd == "blocks" - and cli_args.subsubcmd - and int(cli_args.subsubcmd) >= 0 - ): - await list_blocks(int(cli_args.subsubcmd), cli_args.contains_switches("last")) - - elif cli_args.subcmd == "argos": - await argos_info() - - elif cli_args.subcmd == "amount": - await cmd_amount(cli_args) - - elif cli_args.subcmd == "tx" or cli_args.subcmd == "transaction": - await send_transaction(cli_args) - - elif cli_args.subcmd == "cert": - await send_certification(cli_args) - - elif cli_args.subcmd == "generate_auth_file": - generate_auth_file(cli_args) - - elif cli_args.subcmd == "id" or cli_args.subcmd == "identities": - await id_pubkey_correspondence(cli_args.subsubcmd) - - elif cli_args.subcmd == "wot": - await received_sent_certifications(cli_args.subsubcmd) - - elif cli_args.subcmd == "license": - display_license() - - +@option( + "--gtest", "-gt", is_flag=True, help="ĞTest: `https://g1-test.duniter.org` endpoint" +) +@option( + "--auth-scrypt", + "--scrypt", + is_flag=True, + help="Scrypt authentication: default method", +) +@option("--nrp", help='Scrypt parameters: defaults N,r,p: "4096,16,1"') +@option( + "--auth-file", + "-af", + is_flag=True, + help="Authentication file. Defaults to: './authfile'", +) +@option( + "--file", + default="authfile", + show_default=True, + help="Path file specification with '--auth-file'", +) +@option("--auth-seed", "--seed", is_flag=True, help="Seed hexadecimal authentication") +@option("--auth-wif", "--wif", is_flag=True, help="WIF and EWIF authentication methods") +@pass_context +def cli(ctx, peer, gtest, auth_scrypt, nrp, auth_file, file, auth_seed, auth_wif): + ctx.obj = dict() + ctx.ensure_object(dict) + ctx.obj["PEER"] = peer + ctx.obj["GTEST"] = gtest + ctx.obj["AUTH_SCRYPT"] = auth_scrypt + ctx.obj["AUTH_SCRYPT_PARAMS"] = nrp + ctx.obj["AUTH_FILE"] = auth_file + ctx.obj["AUTH_FILE_PATH"] = file + ctx.obj["AUTH_SEED"] = auth_seed + ctx.obj["AUTH_WIF"] = auth_wif + + +cli.add_command(argos_info) +cli.add_command(generate_auth_file) +cli.add_command(cmd_amount) +cli.add_command(list_blocks) +cli.add_command(send_certification) +cli.add_command(difficulties) +cli.add_command(id_pubkey_correspondence) +cli.add_command(currency_info) +cli.add_command(license_command) +cli.add_command(network_info) +cli.add_command(send_transaction) +cli.add_command(received_sent_certifications) + + +@cli.command("about", help="Display programm information") def about(): print( "\ \n @@@@@@@@@@@@@\ \n @@@ @ @@@\ -\n @@@ @@ @@@@@@ @@. ", +\n @@@ @@ @@@@@@ @@. Silkaj", SILKAJ_VERSION, "\ \n @@ @@@ @@@@@@@@@@@ @@,\ diff --git a/silkaj/commands.py b/silkaj/commands.py index 24c9c4f64ab8acb04022a9ef5de940af6756d66c..3395715a1d77155c5ead8128332f8b9a00b1bd16 100644 --- a/silkaj/commands.py +++ b/silkaj/commands.py @@ -15,6 +15,7 @@ You should have received a copy of the GNU Affero General Public License along with Silkaj. If not, see <https://www.gnu.org/licenses/>. """ +from click import command, option, argument, IntRange from datetime import datetime from time import sleep from os import system, popen @@ -24,6 +25,7 @@ from operator import itemgetter from duniterpy.api.client import Client from duniterpy.api.bma import blockchain, node +from silkaj.tools import coroutine from silkaj.wot import get_uid_from_pubkey from silkaj.network_tools import ( discover_peers, @@ -36,6 +38,8 @@ from silkaj.tools import convert_time, message_exit, CurrencySymbol from silkaj.constants import NO_MATCHING_ID +@command("info", help="Display information about currency") +@coroutine async def currency_info(): head_block = await HeadBlock().head_block ep = EndPoint().ep @@ -82,6 +86,11 @@ def power(nbr, pow=0): return "{0:.1f} × 10^{1}".format(nbr, pow) +@command( + "diffi", + help="Display the current Proof of Work difficulty level to generate the next block", +) +@coroutine async def difficulties(): client = ClientInstance().client while True: @@ -116,18 +125,6 @@ async def difficulties(): await client.close() -network_sort_keys = ["block", "member", "diffi", "uid"] - - -def set_network_sort_keys(some_keys): - global network_sort_keys - if some_keys.endswith(","): - message_exit( - "Argument 'sort' ends with a comma, you have probably inserted a space after the comma, which is incorrect." - ) - network_sort_keys = some_keys.split(",") - - def get_network_sort_key(endpoint): t = list() for akey in network_sort_keys: @@ -138,7 +135,21 @@ def get_network_sort_key(endpoint): return tuple(t) -async def network_info(discover): +@command("net", help="Display network view") +@option( + "--discover", "-d", is_flag=True, help="Discover the network (could take a while)" +) +@option( + "--sort", + "-s", + default="block,member,diffi,uid", + show_default=True, + help="Sort column names comma-separated", +) +@coroutine +async def network_info(discover, sort): + global network_sort_keys + network_sort_keys = sort.split(",") rows, columns = popen("stty size", "r").read().split() wide = int(columns) if wide < 146: @@ -224,19 +235,28 @@ async def network_info(discover): print(tabulate(endpoints, headers="keys", tablefmt="orgtbl", stralign="center")) -async def list_blocks(nbr, last): +@command("blocks", help="Display blocks: default: 0 for current window size") +@argument("number", default=0, type=IntRange(0, 5000)) +@option( + "--detailed", + "-d", + is_flag=True, + help="Force detailed view. Compact view happen over 30 blocks", +) +@coroutine +async def list_blocks(number, detailed): head_block = await HeadBlock().head_block current_nbr = head_block["number"] - if nbr == 0: - nbr = head_block["issuersFrame"] + if number == 0: + number = head_block["issuersFrame"] client = ClientInstance().client - blocks = await client(blockchain.blocks, nbr, current_nbr - nbr + 1) + blocks = await client(blockchain.blocks, number, current_nbr - number + 1) list_issuers, j = list(), 0 issuers_dict = dict() while j < len(blocks): issuer = OrderedDict() issuer["pubkey"] = blocks[j]["issuer"] - if last or nbr <= 30: + if detailed or number <= 30: issuer["block"] = blocks[j]["number"] issuer["gentime"] = convert_time(blocks[j]["time"], "hour") issuer["mediantime"] = convert_time(blocks[j]["medianTime"], "hour") @@ -258,11 +278,11 @@ async def list_blocks(nbr, last): await client.close() print( "Last {0} blocks from n°{1} to n°{2}".format( - nbr, current_nbr - nbr + 1, current_nbr + number, current_nbr - number + 1, current_nbr ), end=" ", ) - if last or nbr <= 30: + if detailed or number <= 30: sorted_list = sorted(list_issuers, key=itemgetter("block"), reverse=True) print( "\n" @@ -291,7 +311,7 @@ async def list_blocks(nbr, last): i += 1 i = 0 while i < len(list_issued): - list_issued[i]["percent"] = list_issued[i]["blocks"] / nbr * 100 + list_issued[i]["percent"] = list_issued[i]["blocks"] / number * 100 i += 1 sorted_list = sorted(list_issued, key=itemgetter("blocks"), reverse=True) print( @@ -308,6 +328,8 @@ async def list_blocks(nbr, last): ) +@command("argos", help="Display currency information formated for Argos or BitBar") +@coroutine async def argos_info(): pretty_names = {"g1": "Ğ1", "gtest": "Ğtest"} head_block = await HeadBlock().head_block diff --git a/silkaj/constants.py b/silkaj/constants.py index 3cf7e0892d4bed7e135755e9e7ddd398a450472f..575f9409437bd2aaa3103d17ebabf4ff4820e5c3 100644 --- a/silkaj/constants.py +++ b/silkaj/constants.py @@ -15,7 +15,7 @@ You should have received a copy of the GNU Affero General Public License along with Silkaj. If not, see <https://www.gnu.org/licenses/>. """ -SILKAJ_VERSION = "silkaj 0.6.0" +SILKAJ_VERSION = "0.6.0" NO_MATCHING_ID = "No matching identity" G1_SYMBOL = "Ğ1" GTEST_SYMBOL = "ĞTest" diff --git a/silkaj/crypto_tools.py b/silkaj/crypto_tools.py index 99cfd7ae338542d01927528744f6eaf7eb520c01..96d742f10f1098da6a72505beb3fadc462898d6e 100644 --- a/silkaj/crypto_tools.py +++ b/silkaj/crypto_tools.py @@ -46,7 +46,7 @@ def check_public_key(pubkey, display_error): return False elif display_error: - print("Error: bad format for following public key:") + print("Error: bad format for following public key:", pubkey) return False diff --git a/silkaj/license.py b/silkaj/license.py index 55467fd85e930861a61816e4ac260b21fdb2c0ae..f85e0709953bf75a17bee8458aaa5681d63b7fd6 100644 --- a/silkaj/license.py +++ b/silkaj/license.py @@ -18,6 +18,7 @@ along with Silkaj. If not, see <https://www.gnu.org/licenses/>. import webbrowser from pydoc import pager from sys import exit +from click import command def license_approval(currency): @@ -28,6 +29,11 @@ def license_approval(currency): exit(1) +@command("license", help="Display Ğ1 license") +def license_command(): + display_license() + + def display_license(): language = input("In which language would you like to display Ğ1 license [en/fr]? ") if language == "en": diff --git a/silkaj/money.py b/silkaj/money.py index f95fd9c9b9b1f52f0f3643f156cc5fda9fbe0441..2da4c89e004ed3851a97142148f72c322efb5331 100644 --- a/silkaj/money.py +++ b/silkaj/money.py @@ -15,18 +15,30 @@ You should have received a copy of the GNU Affero General Public License along with Silkaj. If not, see <https://www.gnu.org/licenses/>. """ +from click import command, argument, pass_context + from silkaj.network_tools import ClientInstance, HeadBlock -from silkaj.tools import CurrencySymbol +from silkaj.tools import CurrencySymbol, message_exit, coroutine from silkaj.auth import auth_method from silkaj.wot import check_public_key from duniterpy.api.bma import tx, blockchain from duniterpy.documents.transaction import InputSource -async def cmd_amount(cli_args): +@command("balance", help="Get wallet balance") +@argument("pubkeys", nargs=-1) +@pass_context +@coroutine +async def cmd_amount(ctx, pubkeys): client = ClientInstance().client - if not cli_args.subsubcmd.startswith("--auth-"): - pubkeys = cli_args.subsubcmd.split(":") + if not ( + ctx.obj["AUTH_SCRYPT"] + or ctx.obj["AUTH_FILE"] + or ctx.obj["AUTH_SEED"] + or ctx.obj["AUTH_WIF"] + ): + if not pubkeys: + message_exit("You should specify one or many pubkeys") for pubkey in pubkeys: pubkey = check_public_key(pubkey, True) if not pubkey: @@ -40,7 +52,7 @@ async def cmd_amount(cli_args): if len(pubkeys) > 1: await show_amount_from_pubkey("Total", total) else: - key = auth_method(cli_args) + key = auth_method() pubkey = key.pubkey await show_amount_from_pubkey(pubkey, await get_amount_from_pubkey(pubkey)) await client.close() diff --git a/silkaj/network_tools.py b/silkaj/network_tools.py index 5439198282475d802131b791f4e9304d0dc29d8b..4042e6a843535d7c6aac04dd02e5729dc325a938 100644 --- a/silkaj/network_tools.py +++ b/silkaj/network_tools.py @@ -20,7 +20,7 @@ from ipaddress import ip_address import socket import logging from sys import exit, stderr -from commandlines import Command +from click import pass_context from duniterpy.api.client import Client from duniterpy.api.bma import blockchain, network @@ -123,20 +123,18 @@ def singleton(class_): @singleton class EndPoint(object): - def __init__(self): - cli_args = Command() + @pass_context + def __init__(ctx, self): ep = dict() - if cli_args.contains_switches("p"): - peer = cli_args.get_definition("p") + peer = ctx.obj["PEER"] + if peer: if ":" in peer: ep["domain"], ep["port"] = peer.rsplit(":", 1) else: ep["domain"], ep["port"] = peer, "443" else: ep["domain"], ep["port"] = ( - G1_TEST_DEFAULT_ENDPOINT - if cli_args.contains_switches("gtest") - else G1_DEFAULT_ENDPOINT + G1_TEST_DEFAULT_ENDPOINT if ctx.obj["GTEST"] else G1_DEFAULT_ENDPOINT ) if ep["domain"].startswith("[") and ep["domain"].endswith("]"): ep["domain"] = ep["domain"][1:-1] diff --git a/silkaj/tools.py b/silkaj/tools.py index b9dd0d1a560fc4960049c345d8d8bc4a22609504..965b8657b6a7fd428ad04bfe08146ec48fb42020 100644 --- a/silkaj/tools.py +++ b/silkaj/tools.py @@ -17,6 +17,8 @@ along with Silkaj. If not, see <https://www.gnu.org/licenses/>. from datetime import datetime from sys import exit +from asyncio import get_event_loop +from functools import update_wrapper from silkaj.constants import G1_SYMBOL, GTEST_SYMBOL from silkaj.blockchain_tools import BlockchainParams @@ -60,3 +62,11 @@ class CurrencySymbol(object): def message_exit(message): print(message) exit(1) + + +def coroutine(f): + def wrapper(*args, **kwargs): + loop = get_event_loop() + return loop.run_until_complete(f(*args, **kwargs)) + + return update_wrapper(wrapper, f) diff --git a/silkaj/tx.py b/silkaj/tx.py index 4e152e4449f1a97a0c002e097bedb01ff1e79281..995a5c1819358c97731367a5340b729816ef682e 100644 --- a/silkaj/tx.py +++ b/silkaj/tx.py @@ -18,11 +18,12 @@ along with Silkaj. If not, see <https://www.gnu.org/licenses/>. from re import compile, search import math from time import sleep +from click import command, option, FloatRange from tabulate import tabulate from silkaj.network_tools import ClientInstance, HeadBlock from silkaj.crypto_tools import check_public_key -from silkaj.tools import message_exit, CurrencySymbol +from silkaj.tools import message_exit, CurrencySymbol, coroutine from silkaj.auth import auth_method from silkaj.wot import get_uid_from_pubkey from silkaj.money import get_sources, get_amount_from_pubkey, UDValue @@ -33,29 +34,46 @@ from duniterpy.documents import BlockUID, Transaction from duniterpy.documents.transaction import OutputSource, Unlock, SIGParameter -async def send_transaction(cli_args): +@command("tx", help="Send transaction") +@option("--amount", type=FloatRange(0.01), help="Quantitative value") +@option("--amountUD", type=float, help="Relative value") +@option("--allSources", is_flag=True, help="Send all sources") +@option( + "--output", + required=True, + help="Pubkey(s)’ recipients + optional checksum: <pubkey>[!checksum]:[<pubkey>[!checksum]]", +) +@option("--comment", default="", help="Comment") +@option( + "--outputBackChange", + help="Pubkey recipient to send the rest of the transaction: <pubkey[!checksum]>", +) +@option("--yes", "-y", is_flag=True, help="Assume yes. Do not prompt confirmation") +@coroutine +async def send_transaction( + amount, amountud, allsources, output, comment, outputbackchange, yes +): """ Main function """ - tx_amount, output, comment, allSources, outputBackChange = await cmd_transaction( - cli_args - ) - key = auth_method(cli_args) + tx_amount = await transaction_amount(amount, amountud, allsources) + key = auth_method() issuer_pubkey = key.pubkey pubkey_amount = await get_amount_from_pubkey(issuer_pubkey) + if allsources: + tx_amount = pubkey_amount[0] outputAddresses = output.split(":") check_transaction_values( comment, outputAddresses, - outputBackChange, + outputbackchange, pubkey_amount[0] < tx_amount * len(outputAddresses), issuer_pubkey, ) if ( - cli_args.contains_switches("yes") - or cli_args.contains_switches("y") + yes or input( tabulate( await transaction_confirmation( @@ -73,43 +91,22 @@ async def send_transaction(cli_args): tx_amount, outputAddresses, comment, - allSources, - outputBackChange, + allsources, + outputbackchange, ) -async def cmd_transaction(cli_args): +async def transaction_amount(amount, amountUD, allSources): """ - Retrieve values from command line interface + Check command line interface amount option + Return transaction amount """ - if not ( - cli_args.contains_definitions("amount") - or cli_args.contains_definitions("amountUD") - ): - message_exit("--amount or --amountUD is not set") - if not cli_args.contains_definitions("output"): - message_exit("--output is not set") - - if cli_args.contains_definitions("amount"): - tx_amount = float(cli_args.get_definition("amount")) * 100 - if cli_args.contains_definitions("amountUD"): - tx_amount = ( - float(cli_args.get_definition("amountUD")) * await UDValue().ud_value - ) - - output = cli_args.get_definition("output") - comment = ( - cli_args.get_definition("comment") - if cli_args.contains_definitions("comment") - else "" - ) - allSources = cli_args.contains_switches("allSources") - - if cli_args.contains_definitions("outputBackChange"): - outputBackChange = cli_args.get_definition("outputBackChange") - else: - outputBackChange = None - return tx_amount, output, comment, allSources, outputBackChange + if not (amount or amountUD or allSources): + message_exit("--amount nor --amountUD nor --allSources is set") + if amount: + return amount * 100 + if amountUD: + return amountUD * await UDValue().ud_value def check_transaction_values( diff --git a/silkaj/wot.py b/silkaj/wot.py index 71eaa78436b60f6eac2bbcb4d97ff0a450a86bfe..fa8fe2f7bf85e12ec75fc1146cc3da281800ba88 100644 --- a/silkaj/wot.py +++ b/silkaj/wot.py @@ -15,6 +15,7 @@ You should have received a copy of the GNU Affero General Public License along with Silkaj. If not, see <https://www.gnu.org/licenses/>. """ +from click import command, argument from time import time from tabulate import tabulate from collections import OrderedDict @@ -22,7 +23,7 @@ from duniterpy.api.bma import wot, blockchain from silkaj.network_tools import ClientInstance from silkaj.crypto_tools import check_public_key -from silkaj.tools import message_exit, convert_time +from silkaj.tools import message_exit, convert_time, coroutine from silkaj.blockchain_tools import BlockchainParams from silkaj.constants import NO_MATCHING_ID @@ -41,6 +42,12 @@ def get_sent_certifications(certs, time_first_block, params): return sent, expire +@command( + "wot", + help="Check received and sent certifications and consult the membership status of any given identity", +) +@argument("id") +@coroutine async def received_sent_certifications(id): """ get searched id @@ -144,6 +151,9 @@ def date_approximation(block_id, time_first_block, avgentime): return time_first_block + block_id * avgentime +@command("id", help="Find corresponding identity or pubkey from pubkey or identity") +@argument("id_pubkey") +@coroutine async def id_pubkey_correspondence(id_pubkey): client = ClientInstance().client if check_public_key(id_pubkey, False): diff --git a/tests/test_end_to_end.py b/tests/test_end_to_end.py index baf6f1710952ec6e68cc58a00e519b40f43a95d6..5b3ed8b9a5fd0b776ac5b987c3a7c5003a9194d3 100644 --- a/tests/test_end_to_end.py +++ b/tests/test_end_to_end.py @@ -22,14 +22,14 @@ def test_wot(): def test_id(): """tests 'silkaj id' certification on gtest""" - output = check_output(["silkaj", "id", "elois", "--gtest"]).decode() + output = check_output(["silkaj", "--gtest", "id", "elois"]).decode() assert "D7CYHJXjaH4j7zRdWngUbsURPnSnjsCYtvo6f8dvW3C" in output def test_amount(): """tests 'silkaj amount' command on gtest""" - output = check_output(["silkaj", "amount", "3dnbnYY9i2bHMQUGyFp5GVvJ2wBkVpus31cDJA5cfRpj", "--gtest"]).decode() + output = check_output(["silkaj", "--gtest", "balance", "3dnbnYY9i2bHMQUGyFp5GVvJ2wBkVpus31cDJA5cfRpj"]).decode() assert "Total amount of: 3dnbnYY9i2bHMQUGyFp5GVvJ2wBkVpus31cDJA5cfRpj" in output assert "Total Relative =" in output assert "UD ĞTest" in output