diff --git a/.pylintrc b/.pylintrc new file mode 100644 index 0000000..f9f8918 --- /dev/null +++ b/.pylintrc @@ -0,0 +1,402 @@ +[MASTER] + +# Specify a configuration file. +#rcfile= + +# Python code to execute, usually for sys.path manipulation such as +# pygtk.require(). +#init-hook= + +# Add files or directories to the blacklist. They should be base names, not +# paths. +ignore=third_party + +# Add files or directories matching the regex patterns to the blacklist. The +# regex matches against base names, not paths. +ignore-patterns=object_detection_grpc_client.py,prediction_pb2.py,prediction_pb2_grpc.py + +# Pickle collected data for later comparisons. +persistent=no + +# List of plugins (as comma separated values of python modules names) to load, +# usually to register additional checkers. +load-plugins= + +# Use multiple processes to speed up Pylint. +jobs=4 + +# Allow loading of arbitrary C extensions. Extensions are imported into the +# active Python interpreter and may run arbitrary code. +unsafe-load-any-extension=no + +# A comma-separated list of package or module names from where C extensions may +# be loaded. Extensions are loading into the active Python interpreter and may +# run arbitrary code +extension-pkg-whitelist= + + +[MESSAGES CONTROL] + +# Only show warnings with the listed confidence levels. Leave empty to show +# all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED +confidence= + +# Enable the message, report, category or checker with the given id(s). You can +# either give multiple identifier separated by comma (,) or put this option +# multiple time (only on the command line, not in the configuration file where +# it should appear only once). See also the "--disable" option for examples. +#enable= + +# Disable the message, report, category or checker with the given id(s). You +# can either give multiple identifiers separated by comma (,) or put this +# option multiple times (only on the command line, not in the configuration +# file where it should appear only once).You can also use "--disable=all" to +# disable everything first and then reenable specific checks. For example, if +# you want to run only the similarities checker, you can use "--disable=all +# --enable=similarities". If you want to run only the classes checker, but have +# no Warning level messages displayed, use"--disable=all --enable=classes +# --disable=W" +# +# Kubeflow disables string-interpolation because we are starting to use f +# style strings +disable=import-star-module-level,old-octal-literal,oct-method,print-statement,unpacking-in-except,parameter-unpacking,backtick,old-raise-syntax,old-ne-operator,long-suffix,dict-view-method,dict-iter-method,metaclass-assignment,next-method-called,raising-string,indexing-exception,raw_input-builtin,long-builtin,file-builtin,execfile-builtin,coerce-builtin,cmp-builtin,buffer-builtin,basestring-builtin,apply-builtin,filter-builtin-not-iterating,using-cmp-argument,useless-suppression,range-builtin-not-iterating,suppressed-message,missing-docstring,no-absolute-import,old-division,cmp-method,reload-builtin,zip-builtin-not-iterating,intern-builtin,unichr-builtin,reduce-builtin,standarderror-builtin,unicode-builtin,xrange-builtin,coerce-method,delslice-method,getslice-method,setslice-method,input-builtin,round-builtin,hex-method,nonzero-method,map-builtin-not-iterating,relative-import,invalid-name,bad-continuation,no-member,locally-disabled,fixme,import-error,too-many-locals,no-name-in-module,too-many-instance-attributes,no-self-use,logging-fstring-interpolation + + +[REPORTS] + +# Set the output format. Available formats are text, parseable, colorized, msvs +# (visual studio) and html. You can also give a reporter class, eg +# mypackage.mymodule.MyReporterClass. +output-format=text + +# Put messages in a separate file for each module / package specified on the +# command line instead of printing them on stdout. Reports (if any) will be +# written in a file name "pylint_global.[txt|html]". This option is deprecated +# and it will be removed in Pylint 2.0. +files-output=no + +# Tells whether to display a full report or only the messages +reports=no + +# Python expression which should return a note less than 10 (10 is the highest +# note). You have access to the variables errors warning, statement which +# respectively contain the number of errors / warnings messages and the total +# number of statements analyzed. This is used by the global evaluation report +# (RP0004). +evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10) + +# Template used to display messages. This is a python new-style format string +# used to format the message information. See doc for all details +#msg-template= + + +[BASIC] + +# Good variable names which should always be accepted, separated by a comma +good-names=i,j,k,ex,Run,_ + +# Bad variable names which should always be refused, separated by a comma +bad-names=foo,bar,baz,toto,tutu,tata + +# Colon-delimited sets of names that determine each other's naming style when +# the name regexes allow several styles. +name-group= + +# Include a hint for the correct naming format with invalid-name +include-naming-hint=no + +# List of decorators that produce properties, such as abc.abstractproperty. Add +# to this list to register other decorators that produce valid properties. +property-classes=abc.abstractproperty + +# Regular expression matching correct function names +function-rgx=[a-z_][a-z0-9_]{2,30}$ + +# Naming hint for function names +function-name-hint=[a-z_][a-z0-9_]{2,30}$ + +# Regular expression matching correct variable names +variable-rgx=[a-z_][a-z0-9_]{2,30}$ + +# Naming hint for variable names +variable-name-hint=[a-z_][a-z0-9_]{2,30}$ + +# Regular expression matching correct constant names +const-rgx=(([A-Z_][A-Z0-9_]*)|(__.*__))$ + +# Naming hint for constant names +const-name-hint=(([A-Z_][A-Z0-9_]*)|(__.*__))$ + +# Regular expression matching correct attribute names +attr-rgx=[a-z_][a-z0-9_]{2,30}$ + +# Naming hint for attribute names +attr-name-hint=[a-z_][a-z0-9_]{2,30}$ + +# Regular expression matching correct argument names +argument-rgx=[a-z_][a-z0-9_]{2,30}$ + +# Naming hint for argument names +argument-name-hint=[a-z_][a-z0-9_]{2,30}$ + +# Regular expression matching correct class attribute names +class-attribute-rgx=([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$ + +# Naming hint for class attribute names +class-attribute-name-hint=([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$ + +# Regular expression matching correct inline iteration names +inlinevar-rgx=[A-Za-z_][A-Za-z0-9_]*$ + +# Naming hint for inline iteration names +inlinevar-name-hint=[A-Za-z_][A-Za-z0-9_]*$ + +# Regular expression matching correct class names +class-rgx=[A-Z_][a-zA-Z0-9]+$ + +# Naming hint for class names +class-name-hint=[A-Z_][a-zA-Z0-9]+$ + +# Regular expression matching correct module names +module-rgx=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$ + +# Naming hint for module names +module-name-hint=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$ + +# Regular expression matching correct method names +method-rgx=[a-z_][a-z0-9_]{2,30}$ + +# Naming hint for method names +method-name-hint=[a-z_][a-z0-9_]{2,30}$ + +# Regular expression which should only match function or class names that do +# not require a docstring. +no-docstring-rgx=^_ + +# Minimum line length for functions/classes that require docstrings, shorter +# ones are exempt. +docstring-min-length=-1 + + +[ELIF] + +# Maximum number of nested blocks for function / method body +max-nested-blocks=5 + + +[TYPECHECK] + +# Tells whether missing members accessed in mixin class should be ignored. A +# mixin class is detected if its name ends with "mixin" (case insensitive). +ignore-mixin-members=yes + +# List of module names for which member attributes should not be checked +# (useful for modules/projects where namespaces are manipulated during runtime +# and thus existing member attributes cannot be deduced by static analysis. It +# supports qualified module names, as well as Unix pattern matching. +ignored-modules= + +# List of class names for which member attributes should not be checked (useful +# for classes with dynamically set attributes). This supports the use of +# qualified names. +ignored-classes=optparse.Values,thread._local,_thread._local + +# List of members which are set dynamically and missed by pylint inference +# system, and so shouldn't trigger E1101 when accessed. Python regular +# expressions are accepted. +generated-members= + +# List of decorators that produce context managers, such as +# contextlib.contextmanager. Add to this list to register other decorators that +# produce valid context managers. +contextmanager-decorators=contextlib.contextmanager + + +[FORMAT] + +# Maximum number of characters on a single line. +max-line-length=140 + +# Regexp for a line that is allowed to be longer than the limit. +ignore-long-lines=^\s*(# )??$ + +# Allow the body of an if to be on the same line as the test if there is no +# else. +single-line-if-stmt=no + +# List of optional constructs for which whitespace checking is disabled. `dict- +# separator` is used to allow tabulation in dicts, etc.: {1 : 1,\n222: 2}. +# `trailing-comma` allows a space between comma and closing bracket: (a, ). +# `empty-line` allows space-only lines. +no-space-check=trailing-comma,dict-separator + +# Maximum number of lines in a module +max-module-lines=1000 + +# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 +# tab). +# Use 2 spaces consistent with TensorFlow style. +indent-string=' ' + +# Number of spaces of indent required inside a hanging or continued line. +indent-after-paren=4 + +# Expected format of line ending, e.g. empty (any line ending), LF or CRLF. +expected-line-ending-format= + + +[MISCELLANEOUS] + +# List of note tags to take in consideration, separated by a comma. +notes=FIXME,XXX,TODO + + +[VARIABLES] + +# Tells whether we should check for unused import in __init__ files. +init-import=no + +# A regular expression matching the name of dummy variables (i.e. expectedly +# not used). +dummy-variables-rgx=(_+[a-zA-Z0-9]*?$)|dummy + +# List of additional names supposed to be defined in builtins. Remember that +# you should avoid to define new builtins when possible. +additional-builtins= + +# List of strings which can identify a callback function by name. A callback +# name must start or end with one of those strings. +callbacks=cb_,_cb + +# List of qualified module names which can have objects that can redefine +# builtins. +redefining-builtins-modules=six.moves,future.builtins + + +[LOGGING] + +# Logging modules to check that the string format arguments are in logging +# function parameter format +logging-modules=logging + + +[SIMILARITIES] + +# Minimum lines number of a similarity. +min-similarity-lines=4 + +# Ignore comments when computing similarities. +ignore-comments=yes + +# Ignore docstrings when computing similarities. +ignore-docstrings=yes + +# Ignore imports when computing similarities. +ignore-imports=no + + +[SPELLING] + +# Spelling dictionary name. Available dictionaries: none. To make it working +# install python-enchant package. +spelling-dict= + +# List of comma separated words that should not be checked. +spelling-ignore-words= + +# A path to a file that contains private dictionary; one word per line. +spelling-private-dict-file= + +# Tells whether to store unknown words to indicated private dictionary in +# --spelling-private-dict-file option instead of raising a message. +spelling-store-unknown-words=no + + +[IMPORTS] + +# Deprecated modules which should not be used, separated by a comma +deprecated-modules=regsub,TERMIOS,Bastion,rexec + +# Create a graph of every (i.e. internal and external) dependencies in the +# given file (report RP0402 must not be disabled) +import-graph= + +# Create a graph of external dependencies in the given file (report RP0402 must +# not be disabled) +ext-import-graph= + +# Create a graph of internal dependencies in the given file (report RP0402 must +# not be disabled) +int-import-graph= + +# Force import order to recognize a module as part of the standard +# compatibility libraries. +known-standard-library= + +# Force import order to recognize a module as part of a third party library. +known-third-party=enchant + +# Analyse import fallback blocks. This can be used to support both Python 2 and +# 3 compatible code, which means that the block might have code that exists +# only in one or another interpreter, leading to false positives when analysed. +analyse-fallback-blocks=no + + +[DESIGN] + +# Maximum number of arguments for function / method +max-args=7 + +# Argument names that match this expression will be ignored. Default to name +# with leading underscore +ignored-argument-names=_.* + +# Maximum number of locals for function / method body +max-locals=15 + +# Maximum number of return / yield for function / method body +max-returns=6 + +# Maximum number of branch for function / method body +max-branches=12 + +# Maximum number of statements in function / method body +max-statements=50 + +# Maximum number of parents for a class (see R0901). +max-parents=7 + +# Maximum number of attributes for a class (see R0902). +max-attributes=7 + +# Minimum number of public methods for a class (see R0903). +min-public-methods=0 + +# Maximum number of public methods for a class (see R0904). +max-public-methods=20 + +# Maximum number of boolean expressions in a if statement +max-bool-expr=5 + + +[CLASSES] + +# List of method names used to declare (i.e. assign) instance attributes. +defining-attr-methods=__init__,__new__,setUp + +# List of valid names for the first argument in a class method. +valid-classmethod-first-arg=cls + +# List of valid names for the first argument in a metaclass class method. +valid-metaclass-classmethod-first-arg=mcs + +# List of member names, which should be excluded from the protected access +# warning. +exclude-protected=_asdict,_fields,_replace,_source,_make + + +[EXCEPTIONS] + +# Exceptions that will emit a warning when being caught. Defaults to +# "Exception" +overgeneral-exceptions=Exception \ No newline at end of file diff --git a/cic/MetaRequestHandler.py b/cic/MetaRequestHandler.py new file mode 100644 index 0000000..1dd2323 --- /dev/null +++ b/cic/MetaRequestHandler.py @@ -0,0 +1,101 @@ +from __future__ import annotations + +# standard imports +import json +import logging +import os +from typing import TYPE_CHECKING, Dict, Union + +from cic_types.condiments import MetadataPointer +from cic_types.ext.metadata.signer import Signer +from cic_types.processor import generate_metadata_pointer + +if TYPE_CHECKING: + from cic.cmd.arg import CmdCtrl + from cic.http import HTTPSession + +# local imports + +# external imports + + +logg = logging.getLogger(__file__) + + +class Metadata: + """ + :cvar base_url: The base url or the metadata server. + :type base_url: str + """ + + base_url = None + ctrl: CmdCtrl = None + +class MetadataRequestsHandler(Metadata): + def __init__( + self, + cic_type: MetadataPointer, + identifier: bytes, + engine: str = "pgp", + ): + """""" + logg.debug(f"ctrl: {self.ctrl}") + self.opener: HTTPSession = self.ctrl.remote_openers["meta"] + self.cic_type = cic_type + self.engine = engine + self.headers = {"X-CIC-AUTOMERGE": "server", "Content-Type": "application/json"} + self.identifier = identifier + if cic_type == MetadataPointer.NONE: + self.metadata_pointer = identifier.hex() + else: + self.metadata_pointer = generate_metadata_pointer( + identifier=self.identifier, cic_type=self.cic_type + ) + if self.base_url: + self.url = os.path.join(self.base_url, self.metadata_pointer) + + def create(self, data: Union[Dict, str]): + """""" + data = json.dumps(data).encode("utf-8") + + result = self.opener.open( + method="POST", url=self.url, data=data, headers=self.headers + ) + logg.debug( + f"url: {self.url}, data: {data}, headers: {self.headers}, result: {result}" + ) + metadata = json.loads(result) + return self.edit(data=metadata) + + def edit(self, data: Union[Dict, str]): + """""" + cic_meta_signer = Signer() + signature = cic_meta_signer.sign_digest(data=data) + algorithm = cic_meta_signer.get_operational_key().get("algo") + formatted_data = { + "m": json.dumps(data), + "s": { + "engine": self.engine, + "algo": algorithm, + "data": signature, + "digest": data.get("digest"), + }, + } + formatted_data = json.dumps(formatted_data).encode("utf-8") + result = self.opener.open( + method="PUT", url=self.url, data=formatted_data, headers=self.headers + ) + logg.info(f"signed metadata submission returned: {result}.") + try: + decoded_identifier = self.identifier.decode("utf-8") + except UnicodeDecodeError: + decoded_identifier = self.identifier.hex() + return result + + def query(self): + """""" + result = self.opener.open(method="GET", url=self.url) + result_data = json.loads(result) + if not isinstance(result_data, dict): + raise ValueError(f"invalid result data object: {result_data}.") + return result diff --git a/cic/actions/deploy.py b/cic/actions/deploy.py index 425eb2d..330b9d3 100644 --- a/cic/actions/deploy.py +++ b/cic/actions/deploy.py @@ -1,32 +1,26 @@ -# standard imports -import logging -import importlib -import os +from __future__ import annotations +# standard imports +import importlib +import logging +import os +from typing import TYPE_CHECKING +# local imports +from cic import Processor, Proof +from cic.attachment import Attachment +from cic.meta import Meta, MetadataWriter +from cic.network import Network +from cic.output import HTTPWriter, KeyedWriterFactory +from cic.token import Token # external imports -from cic_types.ext.metadata import MetadataRequestsHandler +from cic.MetaRequestHandler import MetadataRequestsHandler from cic_types.ext.metadata.signer import Signer as MetadataSigner -# local imports -from cic import ( - Proof, - Processor, - ) -from cic.output import ( - HTTPWriter, - KeyedWriterFactory, - ) -from cic.meta import ( - Meta, - MetadataWriter, - ) -from cic.attachment import Attachment -from cic.network import Network -from cic.token import Token -from typing import Optional logg = logging.getLogger(__name__) - +if TYPE_CHECKING: + from cic.cmd.arg import CmdCtrl + from cic.actions.types import Options @@ -47,19 +41,25 @@ def init_writers_from_config(config): return w -def deploy(config, target: str,contract_directory: str, metadata_endpoint: Optional[str], keystore_directory: str, key_file_path: str, gpg_passphrase: str): - modname = 'cic.ext.{}'.format(target) +def deploy(ctrl: CmdCtrl, target: str, contract_directory: str, keystore_directory: str, options: Options): + auth_passphrase=options.auth_passphrase, + auth_key_file_path=options.auth_keyfile_path, + metadata_endpoint=options.metadata_endpoint, + + modname = f'cic.ext.{target}' cmd_mod = importlib.import_module(modname) - writers = init_writers_from_config(config) + writers = init_writers_from_config(ctrl.config) output_directory = os.path.join(contract_directory, 'out') output_writer_path_meta = output_directory if metadata_endpoint != None: MetadataRequestsHandler.base_url = metadata_endpoint - MetadataSigner.gpg_path = os.path.join('/tmp') - MetadataSigner.key_file_path = key_file_path - MetadataSigner.gpg_passphrase = gpg_passphrase - writers['proof'] = KeyedWriterFactory(MetadataWriter, HTTPWriter).new + MetadataRequestsHandler.ctrl = ctrl + + MetadataSigner.gpg_path = '/tmp' + MetadataSigner.key_file_path = auth_key_file_path # This is a p2p key for add data to meta + MetadataSigner.gpg_passphrase = auth_passphrase + writers['proof'] = KeyedWriterFactory(MetadataWriter, None).new writers['attachment'] = KeyedWriterFactory(None, HTTPWriter).new writers['meta'] = MetadataWriter output_writer_path_meta = metadata_endpoint @@ -78,18 +78,18 @@ def deploy(config, target: str,contract_directory: str, metadata_endpoint: Optio chain_spec = None try: - chain_spec = config.get('CHAIN_SPEC') + chain_spec = ctrl.config.get('CHAIN_SPEC') except KeyError: chain_spec = cn.chain_spec - config.add(chain_spec, 'CHAIN_SPEC', exists_ok=True) + ctrl.config.add(chain_spec, 'CHAIN_SPEC', exists_ok=True) logg.debug(f'CHAIN_SPEC config set to {str(chain_spec)}') - (rpc, signer) = cmd_mod.parse_adapter(config, keystore_directory) + (rpc, signer) = cmd_mod.parse_adapter(ctrl.config, keystore_directory) - ref = cn.resource(target) + target_network_reference = cn.resource(target) chain_spec = cn.chain_spec(target) - logg.debug('found reference {} chain spec {} for target {}'.format(ref['contents'], chain_spec, target)) - c = getattr(cmd_mod, 'new')(chain_spec, ref['contents'], cp, signer_hint=signer, rpc=rpc, outputs_writer=writers['ext'](path=output_directory)) + logg.debug(f'found reference {target_network_reference["contents"]} chain spec {chain_spec} for target {target}') + c = getattr(cmd_mod, 'new')(chain_spec, target_network_reference['contents'], cp, signer_hint=signer, rpc=rpc, outputs_writer=writers['ext'](path=output_directory)) c.apply_token(ct) p = Processor(proof=cp, attachment=ca, metadata=cm, extensions=[c]) diff --git a/cic/actions/types.py b/cic/actions/types.py new file mode 100644 index 0000000..948ff1e --- /dev/null +++ b/cic/actions/types.py @@ -0,0 +1,28 @@ +from collections import namedtuple + +Contract = namedtuple( + "Contract", + [ + "token", + "proof", + "meta", + "attachment", + "network", + ], +) + +Options = namedtuple( + "Options", + [ + "auth_db_path", + "auth_keyfile_path", + "auth_passphrase", + "contract_registry", + "key_account", + "chain_spec", + "rpc_provider", + "metadata_endpoint", + "wallet_keyfile", + "wallet_passphrase", + ], +) \ No newline at end of file diff --git a/cic/auth.py b/cic/auth.py new file mode 100644 index 0000000..dadfb54 --- /dev/null +++ b/cic/auth.py @@ -0,0 +1,74 @@ +# standard imports +import hashlib +import logging +import os + +# external imports +import gnupg + +# local imports +from cic.errors import AuthError + +logg = logging.getLogger(__name__) + + +class PGPAuthCrypt: + + typ = "gnupg" + + def __init__(self, db_dir, auth_key, pgp_dir=None): + self.db_dir = db_dir + try: + bytes.fromhex(auth_key) + except TypeError: + raise AuthError(f"invalid key {auth_key}") from TypeError + except ValueError: + raise AuthError(f"invalid key {auth_key}") from ValueError + self.auth_key = auth_key + self.gpg = gnupg.GPG(gnupghome=pgp_dir) + self.secret = None + self.__passphrase = None + + def get_secret(self, passphrase=""): + if passphrase is None: + passphrase = "" + p = os.path.join(self.db_dir, ".secret") + try: + f = open(p, "rb") + except FileNotFoundError: + h = hashlib.sha256() + h.update(bytes.fromhex(self.auth_key)) + h.update(passphrase.encode("utf-8")) + z = h.digest() + secret = self.gpg.encrypt(z, [self.auth_key], always_trust=True) + if not secret.ok: + raise AuthError(f"could not encrypt secret for {self.auth_key}") from FileNotFoundError + + d = os.path.dirname(p) + os.makedirs(d, exist_ok=True) + f = open(p, "wb") + f.write(secret.data) + f.close() + f = open(p, "rb") + secret = self.gpg.decrypt_file(f, passphrase=passphrase) + if not secret.ok: + raise AuthError("could not decrypt encryption secret. wrong password?") + f.close() + self.secret = secret.data + self.__passphrase = passphrase + + def get_passphrase(self): + return self.__passphrase + + def fingerprint(self): + return self.auth_key + + def sign(self, plaintext, encoding, passphrase="", detach=True): + r = self.gpg.sign(plaintext, passphrase=passphrase, detach=detach) + if len(r.data) == 0: + raise AuthError("signing failed: " + r.status) + + if encoding == "base64": + r = r.data + + return r diff --git a/cic/cmd/__init__.py b/cic/cmd/__init__.py index e69de29..bc0cfe4 100644 --- a/cic/cmd/__init__.py +++ b/cic/cmd/__init__.py @@ -0,0 +1 @@ +from cic.cmd.arg import CmdCtrl diff --git a/cic/cmd/arg.py b/cic/cmd/arg.py new file mode 100644 index 0000000..6e7b913 --- /dev/null +++ b/cic/cmd/arg.py @@ -0,0 +1,221 @@ +# standard imports +import importlib +import logging +import os +import sys + +# external imports +import chainlib.eth.cli +import cic.cmd.easy as cmd_easy +import cic.cmd.export as cmd_export +import cic.cmd.ext as cmd_ext +import cic.cmd.init as cmd_init +import cic.cmd.show as cmd_show +from chainlib.chain import ChainSpec +from cic.auth import PGPAuthCrypt +from cic.crypt.aes import AESCTREncrypt +from cic.http import HTTPSession, PGPClientSession + +# local imports +from cic.notify import NotifyWriter + +notifier = NotifyWriter() +logg = logging.getLogger(__name__) + +script_dir = os.path.dirname(os.path.realpath(__file__)) +data_dir = os.path.join(script_dir, "..", "data") +base_config_dir = os.path.join(data_dir, "config") + + +class NullWriter: + def notify(self, v): + pass + + def ouch(self, v): + pass + + def write(self, v): + sys.stdout.write(str(v)) + + +class CmdCtrl: + + __cmd_alias = { + "u": "user", + "t": "tag", + } + + __auth_for = [ + "user", + ] + + def __init__(self, *_args, argv=None, _description=None, logger=None, **_kwargs): + self.args(argv) + + self.logging(logger) + + self.module() + + self.load_config() + + self.notifier() + + self.auth() + + self.blockchain() + + self.remote_openers = {} + if self.get("META_URL") is not None: + auth_client_session = PGPClientSession(self.__auth) + self.remote_openers["meta"] = HTTPSession( + self.get("META_URL"), + auth=auth_client_session, + origin=self.config.get("META_HTTP_ORIGIN"), + ) + + def blockchain(self): + self.chain_spec = ChainSpec.from_chain_str(self.config.get("CHAIN_SPEC")) + self.rpc = chainlib.eth.cli.Rpc() + self.__conn = self.rpc.connect_by_config(self.config) + + def args(self, argv): + self.argparser = chainlib.eth.cli.ArgumentParser( + chainlib.eth.cli.argflag_std_read + ) + sub = self.argparser.add_subparsers() + sub.dest = "command" + + sub_init = sub.add_parser("init", help="initialize new cic data directory") + cmd_init.process_args(sub_init) + + sub_show = sub.add_parser( + "show", help="display summary of current state of cic data directory" + ) + cmd_show.process_args(sub_show) + + sub_export = sub.add_parser( + "export", help="export cic data directory state to a specified target" + ) + cmd_export.process_args(sub_export) + + sub_ext = sub.add_parser("ext", help="extension helpers") + cmd_ext.process_args(sub_ext) + + sub_easy = sub.add_parser("easy", help="Easy Mode Contract Deployment") + cmd_easy.process_args(sub_easy) + + self.cmd_args = self.argparser.parse_args(argv) + + def module(self): + self.cmd_string = self.cmd_args.command + cmd_string_translate = self.__cmd_alias.get(self.cmd_string) + if cmd_string_translate is not None: + self.cmd_string = cmd_string_translate + + if self.cmd_string is None: + self.cmd_string = "none" + self.argparser.print_help() + exit(1) + + modname = f"cic.cmd.{self.cmd_string}" + self.logger.debug(f"using module {modname}") + self.cmd_mod = importlib.import_module(modname) + + def logging(self, logger): + self.logger = logger + if self.logger is None: + self.logger = logging.getLogger() + if self.cmd_args.vv: + self.logger.setLevel(logging.DEBUG) + elif self.cmd_args.v: + self.logger.setLevel(logging.INFO) + + def load_config(self): + override_dir = self.cmd_args.config + if override_dir is None: + p = os.environ.get("HOME") + if p is not None: + p = os.path.join(p, ".config", "cic", "cli") + try: + os.stat(p) + override_dir = p + logg.info( + f"applying user config override from standard location: {p}" + ) + except FileNotFoundError: + pass + extra_args = self.cmd_mod.extra_args() + self.config = chainlib.eth.cli.Config.from_args( + self.cmd_args, + base_config_dir=base_config_dir, + extra_args=extra_args, + default_config_dir=override_dir, + ) + + self.config.add(False, "_SEQ") + + self.config.censor("AUTH_PASSPHRASE") + + self.logger.debug(f"loaded config:\n{self.config}") + + def auth(self): + typ = self.get("AUTH_TYPE") + if typ != "gnupg": + raise NotImplementedError("Valid aut implementations are: gnupg") + default_auth_db_path = None + if os.environ.get("HOME") is not None: + default_auth_db_path = os.path.join( + os.environ["HOME"], ".local/share/cic/clicada" + ) + auth_db_path = self.get("AUTH_DB_PATH", default_auth_db_path) + self.__auth = PGPAuthCrypt( + auth_db_path, self.get("AUTH_KEY"), self.get("AUTH_KEYRING_PATH") + ) + self.__auth.get_secret(self.get("AUTH_PASSPHRASE")) + self.encrypter = AESCTREncrypt(auth_db_path, self.__auth.secret) + logg.debug(f"loaded auth: {self.__auth}") + logg.debug(f"AUTH_PASSPHRASE: {self.get('AUTH_PASSPHRASE')}") + logg.debug(f"AUTH_KEY: {self.get('AUTH_KEY')}") + logg.debug(f"AUTH_DB_PATH: {self.get('AUTH_DB_PATH')}") + logg.debug(f"AUTH_KEYRING_PATH: {self.get('AUTH_KEYRING_PATH')}") + + def get(self, k, default=None): + r = self.config.get(k, default) + if k in [ + "_FORCE", + ]: + if r is None: + return False + return self.config.true(k) + return r + + def chain(self): + return self.chain_spec + + def conn(self): + return self.__conn + + def execute(self): + self.cmd_mod.execute(self) + + def opener(self, k): + return self.remote_openers[k] + + def notifier(self): + if logg.root.level >= logging.WARNING: + logging.disable() + self.writer = notifier + else: + self.writer = NullWriter() + + def notify(self, v): + self.writer.notify(v) + + def ouch(self, v): + self.writer.ouch(v) + print() + + def write(self, v): + self.writer.write("") + self.writer.write(v) + print() diff --git a/cic/cmd/easy.py b/cic/cmd/easy.py index 3411d39..a70edbf 100644 --- a/cic/cmd/easy.py +++ b/cic/cmd/easy.py @@ -1,9 +1,11 @@ +from __future__ import annotations + # standard import import importlib import json import logging import os -import subprocess +from typing import TYPE_CHECKING import requests @@ -18,6 +20,9 @@ from cic.meta import Meta from cic.network import Network from cic.token import Token +if TYPE_CHECKING: + from cic.cmd.arg import CmdCtrl + from cic.actions.types import Options, Contract log = logging.getLogger(__name__) @@ -47,7 +52,17 @@ def process_args(argparser): ) -def validate_args(args): +def extra_args(): + return { + "path": "_TOKEN_PATH", + "skip_gen": "_TOKEN_SKIP_GEN", + "skip_deploy": "_TOKEN_SKIP_DEPLOY", + "target": "_TOKEN_TARGET", + "p": "RPC_PROVIDER", + } + + +def validate_args(_args): pass @@ -56,18 +71,6 @@ CONTRACTS = [ "url": "https://gitlab.com/cicnet/eth-erc20/-/raw/master/python/giftable_erc20_token/data/GiftableToken", "name": "Giftable Token", }, - { - "url": "https://gitlab.com/cicnet/erc20-demurrage-token/-/raw/master/python/erc20_demurrage_token/data/DemurrageTokenMultiCap", - "name": "Demurrage Token Multi Cap (Might not work)", - }, - { - "url": "https://gitlab.com/cicnet/erc20-demurrage-token/-/raw/master/python/erc20_demurrage_token/data/DemurrageTokenMultiNocap", - "name": "Demurrage Token Multi No Cap (Might not work)", - }, - { - "url": "https://gitlab.com/cicnet/erc20-demurrage-token/-/raw/master/python/erc20_demurrage_token/data/DemurrageTokenSingleCap", - "name": "Demurrage Token Single Cap (Might not work)", - }, { "url": "https://gitlab.com/cicnet/erc20-demurrage-token/-/raw/master/python/erc20_demurrage_token/data/DemurrageTokenSingleNocap", "name": "Demurrage Token Single No Cap", @@ -95,8 +98,8 @@ def get_contract_args(data: list): def print_contract_args(json_path: str): - json_data = json.load(open(json_path)) - print(f"Contract Args:") + json_data = json.load(open(json_path, encoding="utf-8")) + print("Contract Args:") for contract_arg in get_contract_args(json_data): print( f"\t{contract_arg.get('name', '')} - {contract_arg.get('type', '')}" @@ -104,8 +107,8 @@ def print_contract_args(json_path: str): def select_contract(): - print(f"Contracts:") - print(f"\t C - Custom (path/url to contract)") + print("Contracts:") + print("\t C - Custom (path/url to contract)") for idx, contract in enumerate(CONTRACTS): print(f"\t {idx} - {contract['name']}") @@ -129,41 +132,40 @@ def select_contract(): json_path = possible_json_path # possible_bin_location is url else: - bin_path = download_file(contract["url"] + ".bin", directory) + bin_path = download_file(possible_bin_location, directory) else: print("Invalid selection") exit(1) - extra_args = [] - extra_args_types = [] + contract_extra_args = [] + contract_extra_args_types = [] if os.path.exists(json_path): - json_data = json.load(open(json_path)) + json_data = json.load(open(json_path, encoding="utf-8")) for contract_arg in get_contract_args(json_data): arg_name = contract_arg.get("name") arg_type = contract_arg.get("type") if arg_name not in ["_decimals", "_name", "_symbol"]: val = input(f"Enter value for {arg_name} ({arg_type}): ") - extra_args.append(val) - extra_args_types.append(arg_type) + contract_extra_args.append(val) + if arg_type == "uint128": + contract_extra_args_types.append("uint256") + else: + contract_extra_args_types.append(arg_type) + return { "bin_path": bin_path, "json_path": json_path, - "extra_args": extra_args, - "extra_args_types": extra_args_types, + "extra_args": contract_extra_args, + "extra_args_types": contract_extra_args_types, } -def init_token( - directory: str, - code="", - extra_args=[], - extra_args_types=[], -): +def init_token(directory: str, code=""): contract = select_contract() code = contract["bin_path"] - extra_args = contract["extra_args"] - extra_args_types = contract["extra_args_types"] + contract_extra_args = contract["extra_args"] + contract_extra_args_types = contract["extra_args_types"] name = input("Enter Token Name (Foo Token): ") or "Foo Token" symbol = input("Enter Token Symbol (FOO): ") or "FOO" @@ -175,8 +177,8 @@ def init_token( name=name, symbol=symbol, precision=precision, - extra_args=extra_args, - extra_args_types=extra_args_types, + extra_args=contract_extra_args, + extra_args_types=contract_extra_args_types, supply=supply, code=code, ) @@ -230,8 +232,30 @@ def init_attachment(directory): return contract_attchment +def load_contract(directory) -> Contract: + token = Token(path=directory) + proof = Proof(path=directory) + meta = Meta(path=directory) + attachment = Attachment(path=directory) + network = Network(directory) + + token.load() + proof.load() + meta.load() + attachment.load() + network.load() + return Contract( + token=token, proof=proof, meta=meta, attachment=attachment, network=network + ) + + + + + def init_network( - directory, registry_address,key_account_address, chain_spec, rpc_provider, targets=["eth"] + directory, + options: Options, + targets=["eth"], ): contract_network = Network(directory, targets=targets) contract_network.start() @@ -240,17 +264,19 @@ def init_network( m = importlib.import_module(f"cic.ext.{target}.start") m.extension_start( contract_network, - registry_address=registry_address, - chain_spec=chain_spec, - rpc_provider=rpc_provider, - key_account_address=key_account_address + registry_address=options.contract_registry, + chain_spec=options.chain_spec, + rpc_provider=options.rpc_provider, + key_account_address=options.key_account, ) contract_network.load() return contract_network -def execute(config, eargs): - directory = eargs.path + + + +def generate(directory: str, target: str, options: Options) -> Contract: if os.path.exists(directory): contine = input( "Directory already exists, Would you like to delete it? (y/n): " @@ -261,77 +287,119 @@ def execute(config, eargs): else: print(f"Deleted {directory}") os.system(f"rm -rf {directory}") - target = eargs.target - if not eargs.skip_gen: - os.makedirs(directory) - # Defaults - default_contract_registry = config.get( - "CIC_CONTRACT_REGISTRY_ADDRESS", - "0xcf60ebc445b636a5ab787f9e8bc465a2a3ef8299", - ) - default_key_account = config.get( - "CIC_KEY_ACCOUNT_ADDRESS", "eb3907ecad74a0013c259d5874ae7f22dcbcc95c" - ) - default_metadata_endpoint = "http://localhost:63380" or config.get( - "META_ENDPOINT", "http://localhost:63380" - ) - default_wallet_keyfile = config.get( - "WALLET_KEY_FILE", - "/home/will/grassroots/cic-internal-integration/apps/cic-ussd/tests/data/pgp/privatekeys_meta.asc", - ) - default_wallet_passphrase = config.get("WALLET_PASSPHRASE", "merman") + os.makedirs(directory) - default_chain_spec = config.get("CHAIN_SPEC", "evm:byzantium:8996:bloxberg") - default_rpc_provider = config.get("RPC_PROVIDER", "http://localhost:63545") + token = init_token(directory) + proof = init_proof(directory) + meta = init_meta(directory) + attachment = init_attachment(directory) + network = init_network( + directory, + options, + targets=[target], + ) + return Contract( + token=token, proof=proof, meta=meta, attachment=attachment, network=network + ) - # Options - contract_registry = ( - input(f"Enter Contract Registry ({default_contract_registry}): ") - or default_contract_registry - ) - rpc_provider = ( - input(f"Enter RPC Provider ({default_rpc_provider}): ") - or default_rpc_provider - ) - chain_spec = ChainSpec.from_chain_str( - (input(f"Enter ChainSpec ({default_chain_spec}): ") or default_chain_spec) - ) - key_account = ( - input(f"Enter KeyAccount ({default_key_account}): ") or default_key_account - ) - metadata_endpoint = ( - input(f"Enter Metadata Endpoint ({default_metadata_endpoint}): ") - or default_metadata_endpoint - ) - token = init_token(directory) - proof = init_proof(directory) - meta = init_meta(directory) - attachment = init_attachment(directory) - network = init_network( - directory, - registry_address=contract_registry, - key_account_address=key_account, - chain_spec=chain_spec, - rpc_provider=rpc_provider, - targets=[target], - ) - print(f"[cic.header]\nversion = {proof.version()}\n") - print(f"[cic.token]\n{token}") - print(f"[cic.proof]\n{proof}") - print(f"[cic.meta]\n{meta}") - print(f"[cic.attachment]\n{attachment}") - print(f"[cic.network]\n{network}") - if not eargs.skip_deploy: +def get_options(ctrl: CmdCtrl) -> Options: + # Defaults + default_contract_registry = ctrl.config.get( + "CIC_REGISTRY_ADDRESS", + "0xcf60ebc445b636a5ab787f9e8bc465a2a3ef8299", # Comes from /home/will/grassroots/cic-staff-installer/var/cic-staff-client/CIC_REGISTRY_ADDRESS + ) + default_key_account = ctrl.config.get( + "AUTH_KEY", + "eb3907ecad74a0013c259d5874ae7f22dcbcc95c", # comes from wallet `eth-keyfile -z -d $WALLET_KEY_FILE` + ) + # https://meta.grassrootseconomics.net + # https://auth.grassrootseconomics.net Authenticated Meta + + default_metadata_endpoint = ctrl.config.get("META_URL", "https://auth.grassecon.net") + # Keyring folder needs to be dumped out as a private key file from $HOME/.config/cic/staff-client/.gnupg + default_wallet_keyfile = ctrl.config.get( + "WALLET_KEY_FILE", + "/home/will/grassroots/cic-internal-integration/apps/cic-ussd/tests/data/pgp/privatekeys_meta.asc", + ) # Show possible wallet keys + + # Should be an input??? + default_wallet_passphrase = ctrl.config.get("WALLET_PASSPHRASE", "merman") + + default_chain_spec = ctrl.config.get("CHAIN_SPEC", "evm:byzantium:8996:bloxberg") + default_rpc_provider = ctrl.config.get( + "RPC_PROVIDER", "https://rpc.grassecon.net" + ) + contract_registry = ( + input(f"Enter Contract Registry ({default_contract_registry}): ") + or default_contract_registry + ) + rpc_provider = ( + input(f"Enter RPC Provider ({default_rpc_provider}): ") or default_rpc_provider + ) + chain_spec = ChainSpec.from_chain_str( + (input(f"Enter ChainSpec ({default_chain_spec}): ") or default_chain_spec) + ) + key_account = ( + input(f"Enter KeyAccount ({default_key_account}): ") or default_key_account + ) + metadata_endpoint = ( + input(f"Enter Metadata Endpoint ({default_metadata_endpoint}): ") + or default_metadata_endpoint + ) + auth_passphrase = ctrl.config.get( + "AUTH_PASSPHRASE" + ) + auth_keyfile_path = ctrl.config.get( + "AUTH_KEYFILE_PATH" + ) + auth_db_path = ctrl.config.get("AUTH_DB_PATH") + return Options( + auth_db_path, + auth_keyfile_path, + auth_passphrase, + contract_registry, + key_account, + chain_spec, + rpc_provider, + metadata_endpoint, + default_wallet_keyfile, + default_wallet_passphrase, + ) + + +def print_contract(contract: Contract): + print(f"[cic.header]\nversion = {contract.proof.version()}\n") + print(f"[cic.token]\n{contract.token}") + print(f"[cic.proof]\n{contract.proof}") + print(f"[cic.meta]\n{contract.meta}") + print(f"[cic.attachment]\n{contract.attachment}") + print(f"[cic.network]\n{contract.network}") + + +def execute(ctrl: CmdCtrl): + directory = ctrl.config.get("_TOKEN_PATH") + target = ctrl.config.get("_TOKEN_TARGET") + skip_gen = ctrl.config.get("_TOKEN_SKIP_GEN") + skip_deploy = ctrl.config.get("_TOKEN_SKIP_DEPLOY") + + options = get_options(ctrl) + + if not skip_gen: + contract = generate(directory, target, options) + else: + contract = load_contract(directory) + + print_contract(contract) + + if not skip_deploy: ready_to_deploy = input("Ready to deploy? (y/n): ") if ready_to_deploy == "y": deploy( - config, + ctrl=ctrl, contract_directory=directory, - gpg_passphrase=default_wallet_passphrase, - key_file_path=default_wallet_keyfile, - metadata_endpoint=metadata_endpoint, - keystore_directory="/home/will/grassroots/cic-internal-integration/apps/contract-migration/keystore", + options=options, + keystore_directory="/home/will/grassroots/cic-internal-integration/apps/contract-migration/keystore", # Meta Signer meta.ge.net but not auth.ge.net(usumbufu determins if you can even interact with the server) and this ensures data integrity target=target, ) print("Deployed") @@ -339,13 +407,6 @@ def execute(config, eargs): print("Not deploying") -# -# -# rpc="http://localhost:63545" - -# python -m cic.runnable.cic_cmd init --target eth --name "$token_name" --symbol $token_symbol --precision 6 $token_symbol_lowercase - -# python -m cic.runnable.cic_cmd ext -p $rpc -i $chain_spec --registry $contract_registry -d $token_symbol_lowercase eth -vv -# python -m cic.runnable.cic_cmd export -p $rpc --metadata-endpoint http://localhost:63380 -vv -y /home/will/grassroots/cic-internal-integration/apps/contract-migration/keystore -o $token_symbol_lowercase/out -d $token_symbol_lowercase eth if __name__ == "__main__": - execute() + # execute() + print("Not Implemented") diff --git a/cic/crypt/aes.py b/cic/crypt/aes.py new file mode 100644 index 0000000..8d8afb4 --- /dev/null +++ b/cic/crypt/aes.py @@ -0,0 +1,42 @@ +# standard imports +import os +import logging +import hashlib + +from Crypto.Cipher import AES +from Crypto.Util import Counter + +from .base import Encrypter + +logg = logging.getLogger(__name__) + + +class AESCTREncrypt(Encrypter): + + aes_block_size = 1 << 7 + counter_bytes = int(128 / 8) + + def __init__(self, db_dir, secret): + self.secret = secret + + + def key_to_iv(self, k): + h = hashlib.sha256() + h.update(k.encode('utf-8')) + h.update(self.secret) + z = h.digest() + return int.from_bytes(z[:self.counter_bytes], 'big') + + + def encrypt(self, k, v): + iv = self.key_to_iv(k) + ctr = Counter.new(self.aes_block_size, initial_value=iv) + cipher = AES.new(self.secret, AES.MODE_CTR, counter=ctr) + return cipher.encrypt(v) + + + def decrypt(self, k, v): + iv = self.key_to_iv(k) + ctr = Counter.new(self.aes_block_size, initial_value=iv) + cipher = AES.new(self.secret, AES.MODE_CTR, counter=ctr) + return cipher.decrypt(v) diff --git a/cic/crypt/base.py b/cic/crypt/base.py new file mode 100644 index 0000000..19bc150 --- /dev/null +++ b/cic/crypt/base.py @@ -0,0 +1,8 @@ +class Encrypter: + + def encrypt(self, v): + raise NotImplementedError() + + + def decrypt(self, v): + raise NotImplementedError() diff --git a/cic/data/config/config.ini b/cic/data/config/config.ini index 38530b9..61fd81e 100644 --- a/cic/data/config/config.ini +++ b/cic/data/config/config.ini @@ -5,8 +5,19 @@ proof_writer = cic.output.KVWriter ext_writer = cic.output.KVWriter [cic] -contract_registry_address = -key_account_address = +registry_address = 0xcf60ebc445b636a5ab787f9e8bc465a2a3ef8299 [meta] -endpoint=https://meta.grassecon.net +url = https://auth.grassecon.net +http_origin = + +[auth] +type = gnupg +db_path = /home/will/.local/share/cic/clicada +keyfile_path = /home/will/.config/cic/staff-client/user.asc +keyring_path = /home/will/.config/cic/staff-client/.gnupg +key = CCE2E1D2D0E36ADE0405E2D0995BB21816313BD5 +passphrase = + + + diff --git a/cic/errors.py b/cic/errors.py new file mode 100644 index 0000000..a8e2a56 --- /dev/null +++ b/cic/errors.py @@ -0,0 +1,6 @@ +class AuthError(Exception): + pass + + +class MetadataNotFoundError(Exception): + pass diff --git a/cic/ext/eth/__init__.py b/cic/ext/eth/__init__.py index a8ec650..019b41e 100644 --- a/cic/ext/eth/__init__.py +++ b/cic/ext/eth/__init__.py @@ -198,7 +198,7 @@ class CICEth(Extension): writer.write('token_address', self.token_address.encode('utf-8')) self.add_outputs('token', r) - if self.token_details['supply'] > 0: + if int(self.token_details['supply']) > 0: c = GiftableToken(self.chain_spec, signer=self.signer, nonce_oracle=nonce_oracle, gas_oracle=self.fee_oracle) o = c.mint_to(self.token_address, self.resources['token']['key_account'], self.resources['token']['key_account'], self.token_details['supply']) r = None diff --git a/cic/extension.py b/cic/extension.py index 85d76d0..3f56ac7 100644 --- a/cic/extension.py +++ b/cic/extension.py @@ -1,11 +1,13 @@ # standard imports import logging +from typing import TYPE_CHECKING # external imports from hexathon import valid as valid_hex # local imports from cic.output import StdoutWriter +from cic.token import Token logg = logging.getLogger(__name__) @@ -26,7 +28,16 @@ class Extension: :param writer: Writer interface receiving the output of the processor :type writer: cic.output.OutputWriter """ - def __init__(self, chain_spec, resources, proof, signer=None, rpc=None, outputs_writer=StdoutWriter()): + + def __init__( + self, + chain_spec, + resources, + proof, + signer=None, + rpc=None, + outputs_writer=StdoutWriter(), + ): self.resources = resources self.proof = proof self.chain_spec = chain_spec @@ -38,9 +49,8 @@ class Extension: self.outputs = [] self.outputs_writer = outputs_writer - # TODO: apply / prepare token can be factored out - def apply_token(self, token): + def apply_token(self, token: Token): """Initialize extension with token data from settings. :param token: Token object @@ -48,10 +58,27 @@ class Extension: :rtype: dict :returns: Token data state of extension after load """ - return self.prepare_token(token.name, token.symbol, token.precision, token.code, token.supply) + return self.prepare_token( + token.name, + token.symbol, + token.precision, + token.code, + token.supply, + token.extra_args, + token.extra_args_types, + ) - - def prepare_token(self, name, symbol, precision, code, supply, extra=[], extra_types=[], positions=None): + def prepare_token( + self, + name, + symbol, + precision, + code, + supply, + extra=[], + extra_types=[], + positions=None, + ): """Initialize extension token data. :param name: Token name @@ -65,7 +92,7 @@ class Extension: :param supply: Token supply (in smallest precision units) :type supply: int :param extra: Extra parameters to pass to token application constructor - :type extra: list + :type extra: list :param extra_types: Type specifications for extra parameters :type extra_types: list :param positions: Sequence of parameter indices to pass to application constructor @@ -74,24 +101,22 @@ class Extension: :returns: Token data state of extension after load """ self.token_details = { - 'name': name, - 'symbol': symbol, - 'precision': precision, - 'code': code, - 'supply': supply, - 'extra': extra, - 'extra_types': extra_types, - 'positions': positions, - } + "name": name, + "symbol": symbol, + "precision": precision, + "code": code, + "supply": supply, + "extra": extra, + "extra_types": extra_types, + "positions": positions, + } + logg.debug(f"token details: {self.token_details}") return self.token_details - def prepare_extension(self): - """Prepare extension for publishing (noop) - """ + """Prepare extension for publishing (noop)""" pass - def parse_code_as_file(self, v): """Helper method to load application bytecode from file into extensions token data state. @@ -101,18 +126,17 @@ class Extension: :type v: str """ try: - f = open(v, 'r') + f = open(v, "r") r = f.read() f.close() self.parse_code_as_hex(r) except FileNotFoundError as e: - logg.debug('could not parse code as file: {}'.format(e)) + logg.debug("could not parse code as file: {}".format(e)) pass except IsADirectoryError as e: - logg.debug('could not parse code as file: {}'.format(e)) + logg.debug("could not parse code as file: {}".format(e)) pass - def parse_code_as_hex(self, v): """Helper method to load application bytecode from hex data into extension token data state. @@ -124,10 +148,9 @@ class Extension: try: self.token_code = valid_hex(v) except ValueError as e: - logg.debug('could not parse code as hex: {}'.format(e)) + logg.debug("could not parse code as hex: {}".format(e)) pass - def load_code(self, hint=None): """Attempt to load token application bytecode using token settings. @@ -136,24 +159,23 @@ class Extension: :rtype: str (hex) :return: Bytecode loaded into extension token data state """ - code = self.token_details['code'] - if hint == 'hex': + code = self.token_details["code"] + if hint == "hex": self.token_code = valid_hex(code) for m in [ - self.parse_code_as_hex, - self.parse_code_as_file, - ]: + self.parse_code_as_hex, + self.parse_code_as_file, + ]: m(code) if self.token_code != None: break if self.token_code == None: - raise RuntimeError('could not successfully parse token code') + raise RuntimeError("could not successfully parse token code") return self.token_code - def process(self, writer=None): """Adapter used by Processor to process the extensions implementing the Extension base class. @@ -168,25 +190,25 @@ class Extension: writer = self.outputs_writer tasks = [] - self.token_address = self.resources['token']['reference'] + self.token_address = self.resources["token"]["reference"] # TODO: get token details when token address is not none if self.token_address == None: - if self.token_details['code'] == None: - raise RuntimeError('neither token address nor token code has been set') + if self.token_details["code"] == None: + raise RuntimeError("neither token address nor token code has been set") self.load_code() - tasks.append('token') + tasks.append("token") for k in self.resources.keys(): - if k == 'token': + if k == "token": continue - if self.resources[k]['reference'] != None: + if self.resources[k]["reference"] != None: tasks.append(k) self.prepare_extension() for task in tasks: - logg.debug('extension adapter process {}'.format(task)) - r = getattr(self, 'process_' + task)(writer=writer) + logg.debug("extension adapter process {}".format(task)) + r = getattr(self, "process_" + task)(writer=writer) - return (self.token_address, self.token_details.get('symbol')) + return (self.token_address, self.token_details.get("symbol")) diff --git a/cic/http.py b/cic/http.py new file mode 100644 index 0000000..7ed3ba0 --- /dev/null +++ b/cic/http.py @@ -0,0 +1,111 @@ +# standard imports +import hashlib +import logging +import os +import ssl +import urllib.parse +from http.client import HTTPResponse +from socket import getservbyname +from urllib.request import HTTPSHandler + +# external imports +from usumbufu.client.base import BaseTokenStore, ClientSession +from usumbufu.client.bearer import BearerClientSession +from usumbufu.client.hoba import HobaClientSession + +logg = logging.getLogger(__name__) + + +class PGPClientSession(HobaClientSession): + + alg = "969" + + def __init__(self, auth): + self.auth = auth + self.origin = None + self.fingerprint = self.auth.fingerprint() + + def sign_auth_challenge(self, plaintext, hoba, encoding): + passphrase = self.auth.get_passphrase() + r = self.auth.sign(plaintext, encoding, passphrase=passphrase, detach=True) + + hoba.signature = r + return str(hoba) + + def __str__(self): + return "clicada hoba/pgp auth" + + def __repr__(self): + return "clicada hoba/pgp auth" + + +class HTTPSession: + + token_dir = f"/run/user/{os.getuid()}/clicada/usumbufu/.token" + + def __init__(self, url, auth=None, origin=None): + self.base_url = url + url_parts = urllib.parse.urlsplit(self.base_url) + url_parts_origin_host = url_parts[1].split(":") + host = url_parts_origin_host[0] + try: + host = host + ":" + url_parts_origin_host[1] + except IndexError: + host = host + ":" + str(getservbyname(url_parts[0])) + logg.info( + f"changed origin with missing port number from {url_parts[1]} to {host}" + ) + url_parts_origin = ( + url_parts[0], + host, + "", + "", + "", + ) + + self.origin = origin + if self.origin is None: + self.origin = urllib.parse.urlunsplit(url_parts_origin) + else: + logg.debug(f"overriding http origin for {url} with {self.origin}") + + h = hashlib.sha256() + h.update(self.base_url.encode("utf-8")) + z = h.digest() + + token_store_dir = os.path.join(self.token_dir, z.hex()) + os.makedirs(token_store_dir, exist_ok=True) + self.token_store = BaseTokenStore(path=token_store_dir) + logg.debug( + f"token store: \n{self.token_store}\n origin: {self.origin}\n token_store_dir: {token_store_dir}\n" + ) + self.session = ClientSession(self.origin, token_store=self.token_store) + + bearer_handler = BearerClientSession(self.origin, token_store=self.token_store) + self.session.add_subhandler(bearer_handler) + + if auth is not None: + auth.origin = self.origin + self.session.add_subhandler(auth) + ctx = ssl.create_default_context() + ctx.load_verify_locations( + capath="/home/will/grassroots/cic-staff-installer/keys/ge.ca" + ) + https_handler = HTTPSHandler(context=ctx) + self.session.add_parent(parent=https_handler) + self.opener = urllib.request.build_opener(self.session) + + def open(self, url, method=None, data: bytes = None, headers=None): + logg.debug(f"headers: {headers}") + logg.debug(f"token store: \n{self.token_store}\n origin: {self.origin}") + req = urllib.request.Request(url=url, data=data, headers=headers, method=method) + logg.debug(f"open {url} with opener {self}") + logg.debug(req.get_full_url()) + logg.debug(f"handlers {self.opener.handlers}") + response: HTTPResponse = self.opener.open(req) + status = response.getcode() + logg.debug(f"{url} returned {status}") + return response.read().decode("utf-8") + + def __str__(self): + return str(self.session) diff --git a/cic/meta.py b/cic/meta.py index 955ed38..3f429ce 100644 --- a/cic/meta.py +++ b/cic/meta.py @@ -1,16 +1,24 @@ +from __future__ import annotations + # standard imports import base64 import json import logging import os +# types +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from cic.cmd.arg import CmdCtrl # external imports from cic_types import MetadataPointer -from cic_types.ext.metadata import MetadataRequestsHandler from cic_types.processor import generate_metadata_pointer from hexathon import strip_0x +from cic.MetaRequestHandler import MetadataRequestsHandler from cic.output import OutputWriter +from cic.utils import object_to_str # local imports from .base import Data, data_dir @@ -28,103 +36,94 @@ class Meta(Data): :param writer: Writer interface receiving the output of the processor :type writer: cic.output.OutputWriter """ - def __init__(self, path='.', writer=None, name="", location="", country_code="", contact={}): + + def __init__( + self, path=".", writer=None, name="", location="", country_code="", contact={} + ): super(Meta, self).__init__() - self.name = None + self.name = name self.contact = contact self.country_code = country_code self.location = location self.path = path self.writer = writer - self.meta_path = os.path.join(self.path, 'meta.json') - + self.meta_path = os.path.join(self.path, "meta.json") def load(self): - """Load metadata from settings. - """ + """Load metadata from settings.""" super(Meta, self).load() - f = open(self.meta_path, 'r') + f = open(self.meta_path, "r", encoding="utf-8") o = json.load(f) f.close() - self.name = o['name'] - self.contact = o['contact'] - self.country_code = o['country_code'] - self.location = o['location'] + self.name = o["name"] + self.contact = o["contact"] + self.country_code = o["country_code"] + self.location = o["location"] self.inited = True - def start(self): - """Initialize metadata settings from template. - """ + """Initialize metadata settings from template.""" super(Meta, self).start() - meta_template_file_path = os.path.join(data_dir, 'meta_template_v{}.json'.format(self.version())) - - f = open(meta_template_file_path) - o = json.load(f) - f.close() - - o['name'] = self.name - o['contact'] = self.contact - o['country_code'] = self.country_code - o['location'] = self.location + meta_template_file_path = os.path.join( + data_dir, f"meta_template_v{self.version()}.json" + ) - f = open(self.meta_path, 'w') + f = open(meta_template_file_path, encoding="utf-8") + o = json.load(f) + f.close() + + o["name"] = self.name + o["contact"] = self.contact + o["country_code"] = self.country_code + o["location"] = self.location + + f = open(self.meta_path, "w", encoding="utf-8") json.dump(o, f, sort_keys=True, indent="\t") f.close() - def reference(self, token_address): - """Calculate the mutable reference for the token metadata. - """ + """Calculate the mutable reference for the token metadata.""" token_address_bytes = bytes.fromhex(strip_0x(token_address)) - return generate_metadata_pointer(token_address_bytes, MetadataPointer.TOKEN_META) - + return generate_metadata_pointer( + token_address_bytes, MetadataPointer.TOKEN_META + ) def asdict(self): - """Output proof state to dict. - """ + """Output proof state to dict.""" return { - 'name': self.name, - 'contact': self.contact, - } + "name": self.name, + "country_code": self.country_code, + "location": self.location, + "contact": self.contact, + } - def process(self, token_address=None, token_symbol=None, writer=None): """Serialize and publish metadata. - See cic.processor.Processor.process + See cic.processor.Processor.process """ - if writer == None: + if writer is None: writer = self.writer v = json.dumps(self.asdict()) token_address_bytes = bytes.fromhex(strip_0x(token_address)) k = generate_metadata_pointer(token_address_bytes, MetadataPointer.TOKEN_META) - writer.write(k, v.encode('utf-8')) + writer.write(k, v.encode("utf-8")) - token_symbol_bytes = token_symbol.encode('utf-8') - k = generate_metadata_pointer(token_symbol_bytes, MetadataPointer.TOKEN_META_SYMBOL) - writer.write(k, v.encode('utf-8')) + token_symbol_bytes = token_symbol.encode("utf-8") + k = generate_metadata_pointer( + token_symbol_bytes, MetadataPointer.TOKEN_META_SYMBOL + ) + writer.write(k, v.encode("utf-8")) return (k, v) - def __str__(self): - s = f"contact.name = {self.name}\n" - s = f"contact.country_code = {self.country_code}\n" - s = f"contact.location = {self.location}\n" - for contact_key in self.contact.keys(): - contact_value = self.contact[contact_key] - if not contact_value: - s += f"contact.{contact_key} = \n" - continue - s += f"contact.{contact_key} = {contact_value}\n" - - return s + return object_to_str(self, ["name", "contact", "country_code", "location"]) class MetadataWriter(OutputWriter): @@ -138,14 +137,13 @@ class MetadataWriter(OutputWriter): def write(self, k, v): rq = MetadataRequestsHandler(MetadataPointer.NONE, bytes.fromhex(k)) try: - v = v.decode('utf-8') + v = v.decode("utf-8") v = json.loads(v) - logg.debug('metadatawriter bindecode {} {}'.format(k, v)) + logg.debug(f"metadatawriter bindecode {k} {v}") except UnicodeDecodeError: - v = base64.b64encode(v).decode('utf-8') + v = base64.b64encode(v).decode("utf-8") v = json.loads(json.dumps(v)) - logg.debug('metadatawriter b64encode {} {}'.format(k, v)) + logg.debug(f"metadatawriter b64encode {k} {v}") r = rq.create(v) - logg.info('metadata submitted at {}'.format(k)) + logg.info(f"metadata submitted at {k}") return r - diff --git a/cic/notify.py b/cic/notify.py new file mode 100644 index 0000000..8ee25b9 --- /dev/null +++ b/cic/notify.py @@ -0,0 +1,33 @@ +# standard imports +import os +import sys +import shutil + + +class NotifyWriter: + + def __init__(self, writer=sys.stdout): + (c, r) = shutil.get_terminal_size() + self.cols = c + self.fmt = "\r{:" + "<{}".format(c) + "}" + self.w = writer + self.notify_max = self.cols - 4 + + + def notify(self, v): + if len(v) > self.notify_max: + v = v[:self.notify_max] + self.write('\x1b[0;36m... ' + v + '\x1b[0;39m') + + + def ouch(self, v): + if len(v) > self.notify_max: + v = v[:self.notify_max] + self.write('\x1b[0;91m!!! ' + v + '\x1b[0;39m') + + + def write(self, v): + s = str(v) + if len(s) > self.cols: + s = s[:self.cols] + self.w.write(self.fmt.format(s)) diff --git a/cic/output.py b/cic/output.py index 5d514d0..cfe8805 100644 --- a/cic/output.py +++ b/cic/output.py @@ -4,6 +4,7 @@ import sys import logging import urllib.request + logg = logging.getLogger(__name__) @@ -12,7 +13,7 @@ class OutputWriter: def __init__(self, *args, **kwargs): pass - def write(self, k, v): + def write(self, k, v, **kwargs): raise NotImplementedError() @@ -51,7 +52,7 @@ class HTTPWriter(OutputWriter): path = self.path if k != None: path = os.path.join(path, k) - logg.debug('http writer post {}'.format(path)) + logg.debug(f'http writer post {path} \n key: {k}, value: {v}') rq = urllib.request.Request(path, method='POST', data=v) r = urllib.request.urlopen(rq) logg.info('http writer submitted at {}'.format(r.read())) diff --git a/cic/processor.py b/cic/processor.py index c5e4399..df709f9 100644 --- a/cic/processor.py +++ b/cic/processor.py @@ -18,18 +18,25 @@ class Processor: :param extensions: Extension contexts to publish to :type extensions: list of cic.extension.Extension """ - def __init__(self, proof=None, attachment=None, metadata=None, outputs_writer=None, extensions=[]): + + def __init__( + self, + proof=None, + attachment=None, + metadata=None, + outputs_writer=None, + extensions=[], + ): self.token_address = None self.extensions = extensions self.cores = { - 'metadata': metadata, - 'attachment': attachment, - 'proof': proof, - } + "metadata": metadata, + "attachment": attachment, + "proof": proof, + } self.outputs = [] self.__outputs_writer = outputs_writer - def writer(self): """Return the writer instance that the process is using. @@ -38,7 +45,6 @@ class Processor: """ return self.__outputs_writer - def get_outputs(self): """Return all written outputs. @@ -53,7 +59,6 @@ class Processor: outputs += self.outputs return outputs - def process(self, writer=None): """Serializes and publishes all token data. @@ -66,18 +71,23 @@ class Processor: """ tasks = [ - 'attachment', - 'proof', - 'metadata', - ] + "attachment", + "proof", + "metadata", + ] for ext in self.extensions: - (token_address, token_symbol) = ext.process() - + # (token_address, token_symbol) = ext.process() + token_address="1a4b2d1B564456f07d5920FeEcdF86077F7bba1E" + token_symbol="WILLY" for task in tasks: a = self.cores.get(task) if a == None: logg.debug('skipping missing task receiver "{}"'.format(task)) continue - v = a.process(token_address=token_address, token_symbol=token_symbol, writer=self.__outputs_writer) + v = a.process( + token_address=token_address, + token_symbol=token_symbol, + writer=self.__outputs_writer, + ) self.outputs.append(v) diff --git a/cic/proof.py b/cic/proof.py index 2d43780..729ef69 100644 --- a/cic/proof.py +++ b/cic/proof.py @@ -1,19 +1,19 @@ # standard imports -import os import json import logging +import os import tempfile -import cbor2 + +from cic_types import MetadataPointer +from cic_types.processor import generate_metadata_pointer # external imports from hexathon import strip_0x -from cic_types import MetadataPointer -from cic_types.processor import generate_metadata_pointer -from cic_types.ext.metadata import MetadataRequestsHandler + +from cic.utils import object_to_str # local imports from .base import * -from cic.output import OutputWriter logg = logging.getLogger(__name__) @@ -180,4 +180,6 @@ class Proof(Data): return root_key def __str__(self): - return "description = {}\n".format(self.description) + return object_to_str( + self, ["description", "issuer", "namespace", "version()", "proofs"] + ) diff --git a/cic/runnable/test_cmd.py b/cic/runnable/test_cmd.py new file mode 100644 index 0000000..fba9c1e --- /dev/null +++ b/cic/runnable/test_cmd.py @@ -0,0 +1,19 @@ +# standard imports +import sys +import logging + +# local imports +from cic.cmd import CmdCtrl + +logging.basicConfig(level=logging.DEBUG) +logg = logging.getLogger() + + +ctrl = CmdCtrl(argv=sys.argv[1:], logger=logg) + +def main(): + ctrl.execute() + + +if __name__ == '__main__': + main() diff --git a/cic/token.py b/cic/token.py index 2056ddd..7a94407 100644 --- a/cic/token.py +++ b/cic/token.py @@ -105,10 +105,10 @@ class Token(Data): f.close() def __str__(self): - s = """name = {} -symbol = {} -precision = {} -""".format( - self.name, self.symbol, self.precision - ) + s = f"name = {self.name}\n" + s += f"symbol = {self.symbol}\n" + s += f"precision = {self.precision}\n" + s += f"supply = {self.supply}\n" + for idx, extra in enumerate(self.extra_args): + s += f"extra_args[{idx}]({self.extra_args_types[idx]}) = {extra}\n" return s diff --git a/cic/utils.py b/cic/utils.py new file mode 100644 index 0000000..9f934fa --- /dev/null +++ b/cic/utils.py @@ -0,0 +1,24 @@ +def object_to_str(obj, keys): + """Return a string representation of an object.""" + s = "" + for key in keys: + value = eval("obj." + key) + key = key.replace("()", "") + if type(value) == str: + s += f"{key} = {value}\n" + elif type(value) == list: + for idx, vv in enumerate(value): + if not vv: + s += f"{key}[{idx}] = \n" + continue + s += f"{key}[{idx}] = {vv}\n" + elif type(value) == dict: + for vv_key in value.keys(): + vv_value = value[vv_key] + if not vv_value: + s += f"{key}.{vv_key} = \n" + continue + s += f"{key}.{vv_key} = {vv_value}\n" + else: + s += f"{key} = {str(value)}\n" + return s diff --git a/eth_requirements.txt b/eth_requirements.txt index 5e5c822..93410bb 100644 --- a/eth_requirements.txt +++ b/eth_requirements.txt @@ -1,4 +1,4 @@ -chainlib-eth~=0.0.13 +chainlib-eth~=0.0.21 funga-eth~=0.5.1 eth-token-index~=0.2.4 eth-address-index~=0.2.4 diff --git a/requirements.txt b/requirements.txt index 1cf00c6..907b01a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,6 @@ funga-eth~=0.5.1 -cic-types~=0.2.1a5 -confini~=0.5.1 -chainlib~=0.0.13 +cic-types~=0.2.1a8 +confini~=0.5.3 +chainlib~=0.0.17 cbor2==5.4.1 -click==8.0.3 \ No newline at end of file +usumbufu==0.3.6 diff --git a/tests/test_meta.py b/tests/test_meta.py index 6858f0b..bbb4626 100644 --- a/tests/test_meta.py +++ b/tests/test_meta.py @@ -1,31 +1,55 @@ # standard imports -import unittest import logging import os +import unittest -# external imports -from hexathon import strip_0x - # local imports from cic.meta import Meta +# external imports +from hexathon import strip_0x + # test imports -from tests.base_cic import ( - TestCICBase, - test_data_dir, - ) +from tests.base_cic import TestCICBase, test_data_dir logging.basicConfig(level=logging.DEBUG) logg = logging.getLogger() class TestCICMeta(TestCICBase): - def test_meta(self): - fp = os.path.join(test_data_dir, 'proof') + fp = os.path.join(test_data_dir, "proof") m = Meta(fp) m.load() + self.assertEquals( + str(m), + """name = Test +contact.phone = 0700-123456 +country_code = KE +location = Kilifi +""", + ) + + def test_meta_with_initial_values(self): + fp = os.path.join(test_data_dir, "proof") + m = Meta( + fp, + name="TestName", + location="TestLocation", + country_code="TestCC", + contact={ + "phone": "0723578455158", + }, + ) + self.assertEquals( + str(m), + """name = TestName +contact.phone = 0723578455158 +country_code = TestCC +location = TestLocation +""", + ) -if __name__ == '__main__': +if __name__ == "__main__": unittest.main() diff --git a/tests/test_proof.py b/tests/test_proof.py index 4e31018..87afcbc 100644 --- a/tests/test_proof.py +++ b/tests/test_proof.py @@ -1,26 +1,39 @@ # standard imports +import logging import os import unittest -import logging # local imports from cic import Proof from cic.attachment import Attachment # test imports -from tests.base_cic import ( - test_data_dir, - TestCICBase, - root_merged_hash, - ) +from tests.base_cic import TestCICBase, root_merged_hash, test_data_dir logging.basicConfig(level=logging.DEBUG) logg = logging.getLogger() + class TestProof(TestCICBase): + def test_proof(self): + proof_path = os.path.join(test_data_dir, "proof") + attach = Attachment(proof_path, writer=self.outputs_writer) + attach.load() + c = Proof(path=proof_path, attachments=attach) + c.load() + self.assertEquals( + str(c), + """description = foo bar baz +issuer = the man +namespace = ge +version = 0 +proofs[0] = 2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae +proofs[1] = fcde2b2edba56bf408601fb721fe9b5c338d10ee429ea04fae5511b68fbf8fb9 +""", + ) def test_proof_serialize_merge(self): - proof_path = os.path.join(test_data_dir, 'proof') + proof_path = os.path.join(test_data_dir, "proof") attach = Attachment(proof_path, writer=self.outputs_writer) attach.load() @@ -31,5 +44,5 @@ class TestProof(TestCICBase): self.assertEqual(v, root_merged_hash) -if __name__ == '__main__': +if __name__ == "__main__": unittest.main() diff --git a/tests/testdata/proof/meta.json b/tests/testdata/proof/meta.json index 0023838..f961905 100644 --- a/tests/testdata/proof/meta.json +++ b/tests/testdata/proof/meta.json @@ -1,7 +1,8 @@ { - "name": "", - "location": "", - "country_code": "", + "name": "Test", + "location": "Kilifi", + "country_code": "KE", "contact": { + "phone": "0700-123456" } }